From 0c2aca703953263e570ca6d1b248704034ac38d2 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Wed, 21 Aug 2024 13:24:56 +0200 Subject: [PATCH] Open Ticket v4 Beta --- .github/CODE_OF_CONDUCT.md | 128 ++ .github/CONTENT_CREATORS.md | 41 + .github/CONTRIBUTING.md | 69 ++ .github/FUNDING.yml | 1 + .github/ISSUE_TEMPLATE/bug_report.md | 32 + .github/SECURITY.md | 49 + .gitignore | 13 + LICENSE | 674 +++++++++++ README.md | 142 +++ config/general.json | 95 ++ config/options.json | 122 ++ config/panels.json | 39 + config/questions.json | 28 + config/transcripts.json | 57 + database/global.json | 7 + database/options.json | 1 + database/stats.json | 1 + database/tickets.json | 1 + database/users.json | 1 + languages/custom.json | 477 ++++++++ languages/czech.json | 477 ++++++++ languages/dutch.json | 477 ++++++++ languages/english.json | 477 ++++++++ languages/portuguese.json | 477 ++++++++ plugins/example-plugin/config.json | 5 + plugins/example-plugin/index.ts | 44 + plugins/example-plugin/plugin.json | 23 + src/actions/addTicketUser.ts | 87 ++ src/actions/claimTicket.ts | 276 +++++ src/actions/clearTickets.ts | 45 + src/actions/closeTicket.ts | 328 +++++ src/actions/createTicket.ts | 229 ++++ src/actions/createTicketPermissions.ts | 106 ++ src/actions/createTranscript.ts | 164 +++ src/actions/deleteTicket.ts | 463 +++++++ src/actions/handleTranscriptErrors.ts | 164 +++ src/actions/handleVerifyBar.ts | 32 + src/actions/moveTicket.ts | 220 ++++ src/actions/pinTicket.ts | 274 +++++ src/actions/reactionRole.ts | 94 ++ src/actions/removeTicketUser.ts | 81 ++ src/actions/renameTicket.ts | 78 ++ src/actions/reopenTicket.ts | 448 +++++++ src/actions/unclaimTicket.ts | 305 +++++ src/actions/unpinTicket.ts | 267 ++++ src/builders/buttons.ts | 351 ++++++ src/builders/dropdowns.ts | 37 + src/builders/embeds.ts | 1272 ++++++++++++++++++++ src/builders/files.ts | 45 + src/builders/messages.ts | 1055 ++++++++++++++++ src/builders/modals.ts | 173 +++ src/commands/add.ts | 87 ++ src/commands/autoclose.ts | 98 ++ src/commands/autodelete.ts | 93 ++ src/commands/blacklist.ts | 127 ++ src/commands/claim.ts | 138 +++ src/commands/clear.ts | 122 ++ src/commands/close.ts | 137 +++ src/commands/delete.ts | 149 +++ src/commands/help.ts | 112 ++ src/commands/move.ts | 92 ++ src/commands/panel.ts | 82 ++ src/commands/pin.ts | 137 +++ src/commands/remove.ts | 87 ++ src/commands/rename.ts | 80 ++ src/commands/reopen.ts | 142 +++ src/commands/role.ts | 41 + src/commands/stats.ts | 107 ++ src/commands/ticket.ts | 269 +++++ src/commands/unclaim.ts | 136 +++ src/commands/unpin.ts | 136 +++ src/core/api/api.ts | 60 + src/core/api/defaults/action.ts | 154 +++ src/core/api/defaults/base.ts | 45 + src/core/api/defaults/builder.ts | 532 ++++++++ src/core/api/defaults/checker.ts | 378 ++++++ src/core/api/defaults/client.ts | 147 +++ src/core/api/defaults/code.ts | 56 + src/core/api/defaults/config.ts | 470 ++++++++ src/core/api/defaults/console.ts | 42 + src/core/api/defaults/cooldown.ts | 42 + src/core/api/defaults/database.ts | 256 ++++ src/core/api/defaults/event.ts | 331 +++++ src/core/api/defaults/flag.ts | 53 + src/core/api/defaults/helpmenu.ts | 323 +++++ src/core/api/defaults/language.ts | 510 ++++++++ src/core/api/defaults/permission.ts | 30 + src/core/api/defaults/post.ts | 44 + src/core/api/defaults/responder.ts | 255 ++++ src/core/api/defaults/session.ts | 42 + src/core/api/defaults/startscreen.ts | 48 + src/core/api/defaults/stat.ts | 363 ++++++ src/core/api/defaults/verifybar.ts | 72 ++ src/core/api/defaults/worker.ts | 35 + src/core/api/main.ts | 180 +++ src/core/api/modules/action.ts | 58 + src/core/api/modules/base.ts | 749 ++++++++++++ src/core/api/modules/builder.ts | 1245 +++++++++++++++++++ src/core/api/modules/checker.ts | 1465 ++++++++++++++++++++++ src/core/api/modules/client.ts | 1470 +++++++++++++++++++++++ src/core/api/modules/code.ts | 55 + src/core/api/modules/config.ts | 96 ++ src/core/api/modules/console.ts | 663 ++++++++++ src/core/api/modules/cooldown.ts | 348 ++++++ src/core/api/modules/database.ts | 316 +++++ src/core/api/modules/defaults.ts | 319 +++++ src/core/api/modules/event.ts | 99 ++ src/core/api/modules/flag.ts | 73 ++ src/core/api/modules/helpmenu.ts | 216 ++++ src/core/api/modules/language.ts | 107 ++ src/core/api/modules/permission.ts | 211 ++++ src/core/api/modules/plugin.ts | 233 ++++ src/core/api/modules/post.ts | 90 ++ src/core/api/modules/responder.ts | 884 ++++++++++++++ src/core/api/modules/session.ts | 155 +++ src/core/api/modules/startscreen.ts | 219 ++++ src/core/api/modules/stat.ts | 212 ++++ src/core/api/modules/verifybar.ts | 43 + src/core/api/modules/worker.ts | 93 ++ src/core/api/openticket/blacklist.ts | 28 + src/core/api/openticket/option.ts | 421 +++++++ src/core/api/openticket/panel.ts | 128 ++ src/core/api/openticket/question.ts | 180 +++ src/core/api/openticket/role.ts | 122 ++ src/core/api/openticket/ticket.ts | 251 ++++ src/core/api/openticket/transcript.ts | 802 +++++++++++++ src/core/startup/init.ts | 205 ++++ src/core/startup/manageMigration.ts | 84 ++ src/core/startup/migration.ts | 8 + src/core/startup/pluginLauncher.ts | 197 +++ src/data/framework/checkerLoader.ts | 591 +++++++++ src/data/framework/codeLoader.ts | 481 ++++++++ src/data/framework/commandLoader.ts | 1076 +++++++++++++++++ src/data/framework/configLoader.ts | 12 + src/data/framework/cooldownLoader.ts | 17 + src/data/framework/databaseLoader.ts | 53 + src/data/framework/eventLoader.ts | 236 ++++ src/data/framework/flagLoader.ts | 16 + src/data/framework/helpMenuLoader.ts | 263 ++++ src/data/framework/languageLoader.ts | 15 + src/data/framework/liveStatusLoader.ts | 6 + src/data/framework/permissionLoader.ts | 82 ++ src/data/framework/postLoader.ts | 14 + src/data/framework/startScreenLoader.ts | 54 + src/data/framework/statLoader.ts | 133 ++ src/data/openticket/blacklistLoader.ts | 12 + src/data/openticket/optionLoader.ts | 107 ++ src/data/openticket/panelLoader.ts | 165 +++ src/data/openticket/questionLoader.ts | 38 + src/data/openticket/roleLoader.ts | 18 + src/data/openticket/ticketLoader.ts | 35 + src/data/openticket/transcriptLoader.ts | 456 +++++++ src/index.ts | 742 ++++++++++++ tsconfig.json | 22 + 154 files changed, 33881 insertions(+) create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTENT_CREATORS.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/SECURITY.md create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config/general.json create mode 100644 config/options.json create mode 100644 config/panels.json create mode 100644 config/questions.json create mode 100644 config/transcripts.json create mode 100644 database/global.json create mode 100644 database/options.json create mode 100644 database/stats.json create mode 100644 database/tickets.json create mode 100644 database/users.json create mode 100644 languages/custom.json create mode 100644 languages/czech.json create mode 100644 languages/dutch.json create mode 100644 languages/english.json create mode 100644 languages/portuguese.json create mode 100644 plugins/example-plugin/config.json create mode 100644 plugins/example-plugin/index.ts create mode 100644 plugins/example-plugin/plugin.json create mode 100644 src/actions/addTicketUser.ts create mode 100644 src/actions/claimTicket.ts create mode 100644 src/actions/clearTickets.ts create mode 100644 src/actions/closeTicket.ts create mode 100644 src/actions/createTicket.ts create mode 100644 src/actions/createTicketPermissions.ts create mode 100644 src/actions/createTranscript.ts create mode 100644 src/actions/deleteTicket.ts create mode 100644 src/actions/handleTranscriptErrors.ts create mode 100644 src/actions/handleVerifyBar.ts create mode 100644 src/actions/moveTicket.ts create mode 100644 src/actions/pinTicket.ts create mode 100644 src/actions/reactionRole.ts create mode 100644 src/actions/removeTicketUser.ts create mode 100644 src/actions/renameTicket.ts create mode 100644 src/actions/reopenTicket.ts create mode 100644 src/actions/unclaimTicket.ts create mode 100644 src/actions/unpinTicket.ts create mode 100644 src/builders/buttons.ts create mode 100644 src/builders/dropdowns.ts create mode 100644 src/builders/embeds.ts create mode 100644 src/builders/files.ts create mode 100644 src/builders/messages.ts create mode 100644 src/builders/modals.ts create mode 100644 src/commands/add.ts create mode 100644 src/commands/autoclose.ts create mode 100644 src/commands/autodelete.ts create mode 100644 src/commands/blacklist.ts create mode 100644 src/commands/claim.ts create mode 100644 src/commands/clear.ts create mode 100644 src/commands/close.ts create mode 100644 src/commands/delete.ts create mode 100644 src/commands/help.ts create mode 100644 src/commands/move.ts create mode 100644 src/commands/panel.ts create mode 100644 src/commands/pin.ts create mode 100644 src/commands/remove.ts create mode 100644 src/commands/rename.ts create mode 100644 src/commands/reopen.ts create mode 100644 src/commands/role.ts create mode 100644 src/commands/stats.ts create mode 100644 src/commands/ticket.ts create mode 100644 src/commands/unclaim.ts create mode 100644 src/commands/unpin.ts create mode 100644 src/core/api/api.ts create mode 100644 src/core/api/defaults/action.ts create mode 100644 src/core/api/defaults/base.ts create mode 100644 src/core/api/defaults/builder.ts create mode 100644 src/core/api/defaults/checker.ts create mode 100644 src/core/api/defaults/client.ts create mode 100644 src/core/api/defaults/code.ts create mode 100644 src/core/api/defaults/config.ts create mode 100644 src/core/api/defaults/console.ts create mode 100644 src/core/api/defaults/cooldown.ts create mode 100644 src/core/api/defaults/database.ts create mode 100644 src/core/api/defaults/event.ts create mode 100644 src/core/api/defaults/flag.ts create mode 100644 src/core/api/defaults/helpmenu.ts create mode 100644 src/core/api/defaults/language.ts create mode 100644 src/core/api/defaults/permission.ts create mode 100644 src/core/api/defaults/post.ts create mode 100644 src/core/api/defaults/responder.ts create mode 100644 src/core/api/defaults/session.ts create mode 100644 src/core/api/defaults/startscreen.ts create mode 100644 src/core/api/defaults/stat.ts create mode 100644 src/core/api/defaults/verifybar.ts create mode 100644 src/core/api/defaults/worker.ts create mode 100644 src/core/api/main.ts create mode 100644 src/core/api/modules/action.ts create mode 100644 src/core/api/modules/base.ts create mode 100644 src/core/api/modules/builder.ts create mode 100644 src/core/api/modules/checker.ts create mode 100644 src/core/api/modules/client.ts create mode 100644 src/core/api/modules/code.ts create mode 100644 src/core/api/modules/config.ts create mode 100644 src/core/api/modules/console.ts create mode 100644 src/core/api/modules/cooldown.ts create mode 100644 src/core/api/modules/database.ts create mode 100644 src/core/api/modules/defaults.ts create mode 100644 src/core/api/modules/event.ts create mode 100644 src/core/api/modules/flag.ts create mode 100644 src/core/api/modules/helpmenu.ts create mode 100644 src/core/api/modules/language.ts create mode 100644 src/core/api/modules/permission.ts create mode 100644 src/core/api/modules/plugin.ts create mode 100644 src/core/api/modules/post.ts create mode 100644 src/core/api/modules/responder.ts create mode 100644 src/core/api/modules/session.ts create mode 100644 src/core/api/modules/startscreen.ts create mode 100644 src/core/api/modules/stat.ts create mode 100644 src/core/api/modules/verifybar.ts create mode 100644 src/core/api/modules/worker.ts create mode 100644 src/core/api/openticket/blacklist.ts create mode 100644 src/core/api/openticket/option.ts create mode 100644 src/core/api/openticket/panel.ts create mode 100644 src/core/api/openticket/question.ts create mode 100644 src/core/api/openticket/role.ts create mode 100644 src/core/api/openticket/ticket.ts create mode 100644 src/core/api/openticket/transcript.ts create mode 100644 src/core/startup/init.ts create mode 100644 src/core/startup/manageMigration.ts create mode 100644 src/core/startup/migration.ts create mode 100644 src/core/startup/pluginLauncher.ts create mode 100644 src/data/framework/checkerLoader.ts create mode 100644 src/data/framework/codeLoader.ts create mode 100644 src/data/framework/commandLoader.ts create mode 100644 src/data/framework/configLoader.ts create mode 100644 src/data/framework/cooldownLoader.ts create mode 100644 src/data/framework/databaseLoader.ts create mode 100644 src/data/framework/eventLoader.ts create mode 100644 src/data/framework/flagLoader.ts create mode 100644 src/data/framework/helpMenuLoader.ts create mode 100644 src/data/framework/languageLoader.ts create mode 100644 src/data/framework/liveStatusLoader.ts create mode 100644 src/data/framework/permissionLoader.ts create mode 100644 src/data/framework/postLoader.ts create mode 100644 src/data/framework/startScreenLoader.ts create mode 100644 src/data/framework/statLoader.ts create mode 100644 src/data/openticket/blacklistLoader.ts create mode 100644 src/data/openticket/optionLoader.ts create mode 100644 src/data/openticket/panelLoader.ts create mode 100644 src/data/openticket/questionLoader.ts create mode 100644 src/data/openticket/roleLoader.ts create mode 100644 src/data/openticket/ticketLoader.ts create mode 100644 src/data/openticket/transcriptLoader.ts create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b6c95d8 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +support@dj-dj.be. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/.github/CONTENT_CREATORS.md b/.github/CONTENT_CREATORS.md new file mode 100644 index 0000000..f72f41f --- /dev/null +++ b/.github/CONTENT_CREATORS.md @@ -0,0 +1,41 @@ +# 🎥 Content Creators +Hi there! We're searching for content creators to create tutorials & setup guides for Open Ticket. +You can get many great rewards from helpin us! + +You can choose which type of tutorial you want to make. You can create a quick setup, full setup, plugin tutorial or something else! +You are not required to talk in the video, but make sure it has some kind of subtitles instead. + +### 📄 Requirements +- 📌 At least 2k subscribers! +- 📌 A little bit of channel activity! *(minimum 2 uploads/month)* +- 📌 Video length of at least 3 minutes! +- 📌 Provide a clear link to the Open Ticket repository! +- 📌 Recommended language is English! +- 📌 Recommended platform is YouTube! +- 📌 Must be for the latest version of Open Ticket! +- 📌 **(OPTIONAL)** Speech in the video *(AI is allowed)* + + +### 🎁 Rewards +We won't give out any compensation in money, but we will give you some advantages! +- ✅ Advertisement of tutorial in the [README.md](../README.md)! +- ✅ Advertisement of channel in our [discord server](https://discord.dj-dj.be)! +- ✅ `Content Creator` role in our [discord server](https://discord.dj-dj.be)! +- ✅ Advertisement of tutorial & channel in documentation! +- ✅ Lot's of of views generated from GitHub traffic! +- ✅ A completely free 24/7 hosted Open Ticket bot! +- ✅ We will advertise the video to all our socials! + +### 💤 Outdated Content +When a tutorial or setup guide is outdated or no longer applicable, +we will remove it from the `README.md` & documentation. +Your channel will still stay in the list of contributors, so don't worry about that! + +You will be noticed at least a month before removal. +This way you can take time to create an updated tutorial if you want. + +### 💬 Contact Us +If you've read all these guidelines, requirements & rewards, +you can create a ticket in our [discord server](https://discord.dj-dj.be) to start working on it! + +We will try to help you as fast as possible! \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..86a645a --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# Contributing Guidelines +Open Ticket Logo + +[![discord](https://img.shields.io/badge/discord-support%20server-5865F2.svg?style=flat-square&logo=discord)](https://discord.com/invite/26vT9wt3n3) [![version](https://img.shields.io/badge/version-4.0.0-brightgreen.svg?style=flat-square)](https://github.com/DJj123dj/open-ticket/releases/tag/v4.0.0) + +These are the Contributing Guidelines of Open Ticket!
+Here you can find everything you need to know about contributing to Open Ticket.
+This includes new features, translations & bug fixes. + +### 💬 Translations +#### Step 1: View & Check +1. Check the [`./languages/`](./languages/) folder for existing translations. +2. When it doesn't exist yet, visit **[step 2](#step-2-translation)**. +3. When it does exist, check if it is outdated, incorrect or incomplete. +4. If you match one of these requirements, you can also visit **[step 2](#step-2-translating)**. + +#### Step 2: Translation +1. If your language doesn't exist yet, copy the [`english.json`](./../languages/english.json) file and rename it to your language. +2. You are allowed to use a little bit of `Google Translate` or `DeepL` when mentioned in the upload. (❌ Not Recommended) +3. Only translate existing values in the json file. Don't add or remove any values from the file. +4. If you are unable to translate something, leave it in `English`. + +#### Step 3: Metadata +Metadata can be found in the `_TRANSLATION` variable. +|Value |Notes | +|-------------|-------------------------------------------------------------------------------------------------| +|`otversion` |The Open Ticket version of this translation. Don't edit this! | +|`translator` |The translator discord username. When improving existing translation, add your name to the list. | +|`lastedited` |The last edited date in the `DD/MM/YYYY` format. | +|`language` |The full name of the language with capital letter. | + +> #### ✅ You are also allowed to add your username to the [`README.md`](./../README.md) translator list! + +#### Step 4: Uploading +1. When adding a new language, please mention @DJj123dj. He will add the code required for the language to work. +2. There are currently 3 ways of uploading translations: + - Create a pull request to the [__`dev` branch!__](https://github.com/DJj123dj/open-ticket/tree/dev) + - Create a ticket in our [discord server](https://discord.dj-dj.be)! + - Send a DM to @DJj123dj on discord! +3. Now you'll need to wait until the next Open Ticket version for it to be added. + +#### Step 5: Rewards +Translators get some rewards for translating Open Ticket! +- ✅ Credits in the [`README.md`](./../README.md) translator list! +- ✅ Credits in the changelog! +- ✅ Credits in the documentation! +- ✅ Credits in the translation JSON file! +- ✅ A special role in our [discord server](https://discord.dj-dj.be)! + +### 🧩 Plugins (temporary) +Currently plugins can only be added to the list via our [discord server](https://discord.dj-dj.be)! + +### 📦 Features (temporary) +Currently features can only be suggested in our [discord server](https://discord.dj-dj.be)! + +### 🕷️ Bug Fixes (temporary) +Currently bug fixes can only be reported in the following ways: +- Create an issue in the repository! +- Create a ticket in our [discord server](https://discord.dj-dj.be)! + +#### Please try to always include the `otdebug.txt` file! + +--- +Open Ticket Logo + +**Contributing Guidelines**
+[changelog](https://otgithub.dj-dj.be/releases) - [documentation](https://otdocs.dj-dj.be) - [tutorial](https://www.youtube.com/watch?v=2jK9kAf6ASU) - [website](https://openticket.dj-dj.be) - [discord](https://discord.dj-dj.be)
+ +© 2024 - [DJdj Development](https://www.dj-dj.be) - [Terms](https://www.dj-dj.be/terms#terms) - [Privacy Policy](https://www.dj-dj.be/terms#privacy) \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..1032364 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: DJj123dj diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..311eb2f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug Report +about: Report bugs to help us improve Open Ticket! +title: "[BUG] Your Bug" +labels: bug +assignees: DJj123dj +--- + +### Bug Description: +A clear and concise description of what the bug is. + +### Steps To Reproduce: +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +> Leave empty when you are unable to reproduce the bug! + +### Expected Behaviour: +A clear and concise description of what you expected to happen. + +### Screenshots: +If applicable, add screenshots to help explain your problem. + +### Files: +Please include the `otdebug.txt` file in your report! With this file, we can take a deep-dive in to the error that happend. +> This file doesn't contain any credentials or sensitive information! + +### Additional Notes: +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..36d5567 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,49 @@ +# Security Policy +Open Ticket Logo + +[![discord](https://img.shields.io/badge/discord-support%20server-5865F2.svg?style=flat-square&logo=discord)](https://discord.com/invite/26vT9wt3n3) [![version](https://img.shields.io/badge/version-4.0.0-brightgreen.svg?style=flat-square)](https://github.com/DJj123dj/open-ticket/releases/tag/v4.0.0) + +This is the Security Policy of Open Ticket!
+Here you can find a list of all supported & deprecated versions of the bot.
+You will also find some guidelines about how to report vulnerabilities, bugs & errors. + +### 📦 Supported Versions +Below, you can find a list with the status of every Open Ticket version.
+This list will be updated on every release. + +- 🟦 Early Access **(ideas, bugs, support, html transcripts)** +- ✅ Supported **(features, bugs, support, docs, html transcripts)** +- 🚧 Maintenance **(support, docs, html transcripts)** +- 🟧 Deprecated **(docs)** +- ❌ Fully Deprecated + +| Version | Supported | Notes | +|------------|-----------|--------------------------------| +| 4.0.0-beta | 🟦 | For v4.0.0 Full Release | +| 3.5.8 | ✅ | Supported until October 2024 | +| 3.5.7 | ✅ | Supported until September 2024 | +| 3.5.6 | 🟧 | Documentation Only | +| < 3.5.6 | 🟧 | Documentation Only | +| < 3.5.0 | ❌ | | + +### 🕷️ Reporting Vulnerabilities +You can report vulnerabilities, errors & bugs using one of the following methods: +- Create an issue on GitHub! +- Create a ticket in our [discord server](https://discord.dj-dj.be)! +- Message @DJj123dj in DM on discord! +- Send an E-mail to [support@dj-dj.be](mailto:support@dj-dj.be)! + +When you report a problem, please **give a small description** about what & how it happend.
+Try to explain the issue in as much detail as possible. + +If possible, try to provide screenshots! + +### Please upload the `otdebug.txt` file together with your bug report! + +--- +Open Ticket Logo + +**Security Policy**
+[changelog](https://otgithub.dj-dj.be/releases) - [documentation](https://otdocs.dj-dj.be) - [tutorial](https://www.youtube.com/watch?v=2jK9kAf6ASU) - [website](https://openticket.dj-dj.be) - [discord](https://discord.dj-dj.be)
+ +© 2024 - [DJdj Development](https://www.dj-dj.be) - [Terms](https://www.dj-dj.be/terms#terms) - [Privacy Policy](https://www.dj-dj.be/terms#privacy) \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c28ae79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +package-lock.json +devconfig/ +devdatabase/ +otdebug.txt +.DS_Store +*/.DS_Store +*/*/.DS_Store +*/*/*/.DS_Store +TEMP/ +.vscode/ +plugins/* +!plugins/example-plugin/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e72bfdd --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..add080b --- /dev/null +++ b/README.md @@ -0,0 +1,142 @@ +> ### 🎥 Content Creators 🎥 +> Hey there! We're searching for content creators that would want to create a tutorial or setup guide for Open Ticket!
+> [📌 More Information](.github/CONTENT_CREATORS.md) +--- + +

+Open Ticket +
Powered By
+Open Ticket
+Discord Invite Link +Open Ticket Version +Open Ticket Documentation +Open Ticket License +Open Ticket Stars +

+ +

+Open Ticket is the most advanced & customisable discord ticket bot that you will ever find! You can customise up to 300+ variables! This includes Html Transcripts, Advanced Plugins, Custom Embeds, Questions/Modals, Stats & more! +You're also able to customise every little aspect of the bot! From embeds to transcripts. Open Ticket is also translated in more than 32 Languages! If you need any help, feel free to join our discord server! +

+ +

⭐️ Help us grow by giving a star! ⭐️

+ +> [!WARNING] +> Open Ticket v4 is still in beta! Please report all bugs in our [discord server](https://discord.dj-dj.be)!
+> The translation count may not be accurate! + +### 📌 Features +- **📄 html transcripts** - Make use of the most customisable, beautiful and easy-to-use HTML Transcripts! +- **✅ ticket actions** - Close, Reopen, Delete, Rename & Move all your tickets! +- **🇬🇧 translation** - Open Ticket has been translated in more than **32 languages** by our community! +- **🎨 customisation** - Open Ticket has been created around customisation, everything can be customised! +- **🖥️ interactions** - The bot has full support for Buttons, Dropdowns, Slash Commands and Modals! +- **∞ unlimited everywhere** - Create an infinite amount of tickets & panels! +- **📝 advanced plugins** - Create the most advanced plugins that you want or use premade ones by our community! +- **👥 user management** - Add & Remove users from all tickets! +- **📊 detailed stats** - Open Ticket has ticket, user & global staticstics available for everyone! +- **🚫 blacklist** - Blacklist users to prevent them from creating a ticket! +- **❓ questions** - Let users answer questions in a modal before the ticket is created! +- **📦 commands** - Open Ticket supports both slash & text commands! +- **📥 extra types** - The bot also supports Reaction Roles & Url Buttons, because why not ¯\\_(ツ)_/¯ +- **🎛️ dependencies** - We try to use as little external dependencies as needed! + +> ### 📦 Resources +> Resources might not be accurate yet! *(because v4 is still in beta)*
+> Open Ticket Tutorial +> Open Ticket Docs +> Open Ticket Plugins + +## 📸 Preview +*Previews are not available yet in the beta release!* + +## 🧩 Plugins +View all available Open Ticket plugins in our [Official Plugin Repository](https://github.com/DJj123dj/open-discord-plugins)! + +### 📦 Official *(made by us)* +|Name |Description | +|----------------------|-------------------------| +|`nothing yet :)` |Lorem Ipsum Beta 4.0 🙃 | + +### ✅ Verified *(made by community)* +|Name |Description | +|----------------------|-------------------------| +|`nothing yet :)` |Lorem Ipsum Beta 4.0 🙃 | + +## 🛠️ Contributors +### 🖥️ Team +The team behind Open Ticket! + + + + + + + + + +
Profile PictureProfile Picture
DJj123djSanke
+ +### ❤️ Sponsors +A big thanks to all our sponsors! Without them, it wouldn't be possible to create this project! + +|Profile Picture|Profile Picture|Profile Picture| +|----|-|-| +|**[SpyEye](https://github.com/SpyEye2)**|**[DOSEV5](https://github.com/DOSEV5)**|**[Mods HD](https://github.com/mods-hd)**| + +### 💬 Translators +|Language |Maintainer (discord name) |Status | +|---------------------|--------------------------|---------------| +|🇬🇧 English |djj123dj |🟢 Up To Date | +|🇳🇱 Dutch |djj123dj |🟢 Up To Date | +|🇵🇹 Portuguese |quiradon |🟢 Up To Date | +|🇨🇿 Czech |spyeye_ |🟢 Up To Date | +|🇫🇷 French |sankedev & tostam |🟠 Temporary Outdated (beta) | +|🇷🇴 Romanian |sankedev |🟠 Temporary Outdated (beta) | +|🇪🇸 Spanish |redactado & josuens |🟠 Temporary Outdated (beta) | +|🇩🇪 German |david.3 |🟠 Temporary Outdated (beta) | +|🇮🇹 Italian |maurizio26 |🟠 Temporary Outdated (beta) | +|🇦🇪 Arabic |deqressing |🟠 Temporary Outdated (beta) | +|🇩🇰 Danish |.the_gamer |🟠 Temporary Outdated (beta) | +|🇷🇺 Russian |apexo & ander |🟠 Temporary Outdated (beta) | +|🇺🇦 Ukrainian |ander |🟠 Temporary Outdated (beta) | +|🇹🇷 Turkish |0x15d3 |🟠 Temporary Outdated (beta) | +|🇵🇱 Polish |mkevas |🟠 Temporary Outdated (beta) | +|🇸🇮 Slovenian |n1kkec |🔴 Outdated | +|🇹🇭 Thai |modshd |🟠 Temporary Outdated (beta) | +|🇳🇴 Norwegian |noonenook |🟠 Temporary Outdated (beta) | +|🇬🇷 Greek |stefanos__. |🔴 Outdated | +|🇮🇩 Indonesian |erxg |🟠 Temporary Outdated (beta) | +|❓ Kurdish |raze.hama |🟠 Temporary Outdated (beta) | +|🇭🇺 Hungarian |kornel0706 |🟠 Temporary Outdated (beta) | +|❓ Persian |sasanwm |🟠 Temporary Outdated (beta) | +|🇱🇻 Latvian |ronalds1398 |🟠 Temporary Outdated (beta) | +|🇪🇪 Estonian |iamnotmega |🟠 Temporary Outdated (beta) | +|🇯🇵 Japanese |iamnotmega |🟠 Temporary Outdated (beta) | +|🇰🇷 Korean |iamnotmega |🟠 Temporary Outdated (beta) | +|🇨🇳 Simplified Chinese |iamnotmega |🟠 Temporary Outdated (beta) | +|🇨🇳 Traditional Chinese|iamnotmega |🟠 Temporary Outdated (beta) | +|🇫🇮 Finnish |iamnotmega |🟠 Temporary Outdated (beta) | +|🇸🇪 Swedish |iamnotmega |🟠 Temporary Outdated (beta) | +|🇻🇳 Vietnamese |iamnotmega |🟠 Temporary Outdated (beta) | + + + +## ⭐️ Star History +Please help us grow by giving a star! It would help us a lot! + + + + + + Star History Chart + + + +--- +Open Ticket Logo + +**README.md**
+[changelog](https://otgithub.dj-dj.be/releases) - [documentation](https://otdocs.dj-dj.be) - [tutorial](https://www.youtube.com/watch?v=2jK9kAf6ASU) - [website](https://openticket.dj-dj.be) - [discord](https://discord.dj-dj.be)
+ +© 2024 - [DJdj Development](https://www.dj-dj.be) - [Terms](https://www.dj-dj.be/terms#terms) - [Privacy Policy](https://www.dj-dj.be/terms#privacy) \ No newline at end of file diff --git a/config/general.json b/config/general.json new file mode 100644 index 0000000..b45ca8f --- /dev/null +++ b/config/general.json @@ -0,0 +1,95 @@ +{ + "_INFO":{ + "support":"https://otdocs.dj-dj.be", + "discord":"https://discord.dj-dj.be", + "version":"open-ticket-v4.0.0" + }, + + "token":"your bot token here! Or leave empty when you use tokenFromENV", + "tokenFromENV":false, + + "mainColor":"#f8ba00", + "language":"english", + "prefix":"!ticket ", + "serverId":"discord server id", + "globalAdmins":["discord role id"], + + "slashCommands":true, + "textCommands":true, + + "status":{ + "enabled":true, + "type":"listening OR watching OR playing OR custom", + "text":"/help", + "status":"online OR invisible OR idle OR dnd" + }, + + "system":{ + "removeParticipantsOnClose":false, + "replyOnTicketCreation":true, + "replyOnReactionRole":true, + "useTranslatedConfigChecker":true, + "preferSlashOverText":true, + "sendErrorOnUnknownCommand":true, + "questionFieldsInCodeBlock":true, + "disableVerifyBars":false, + "useRedErrorEmbeds":true, + "emojiStyle":"before (OR after OR double OR disabled)", + + "enableTicketClaimButtons":true, + "enableTicketCloseButtons":true, + "enableTicketPinButtons":true, + "enableTicketDeleteButtons":true, + "enableTicketActionWithReason":true, + "enableDeleteWithoutTranscript":true, + + "logs":{ + "enabled":false, + "channel":"discord channel id" + }, + + "limits":{ + "enabled":true, + "globalMaximum":50, + "userMaximum":3 + }, + + "permissions":{ + "help":"everyone (OR admin OR none OR role id)", + "panel":"admin (OR everyone OR none OR role id)", + "ticket":"none (OR admin OR everyone OR role id)", + "close":"everyone (OR admin OR none OR role id)", + "delete":"admin (OR everyone OR none OR role id)", + "reopen":"everyone (OR admin OR none OR role id)", + "claim":"admin (OR everyone OR none OR role id)", + "unclaim":"admin (OR everyone OR none OR role id)", + "pin":"admin (OR everyone OR none OR role id)", + "unpin":"admin (OR everyone OR none OR role id)", + "move":"admin (OR everyone OR none OR role id)", + "rename":"admin (OR everyone OR none OR role id)", + "add":"admin (OR everyone OR none OR role id)", + "remove":"admin (OR everyone OR none OR role id)", + "blacklist":"admin (OR everyone OR none OR role id)", + "stats":"everyone (OR admin OR none OR role id)", + "clear":"admin (OR everyone OR none OR role id)", + "autoclose":"admin (OR everyone OR none OR role id)", + "autodelete":"admin (OR everyone OR none OR role id)" + }, + + "messages":{ + "creation":{"dm":false, "logs":true}, + "closing":{"dm":false, "logs":true}, + "deleting":{"dm":false, "logs":true}, + "reopening":{"dm":false, "logs":true}, + "claiming":{"dm":false, "logs":true}, + "pinning":{"dm":false, "logs":true}, + "adding":{"dm":false, "logs":true}, + "removing":{"dm":false, "logs":true}, + "renaming":{"dm":false, "logs":true}, + "moving":{"dm":false, "logs":true}, + "blacklisting":{"dm":false, "logs":true}, + "roleAdding":{"dm":false, "logs":true}, + "roleRemoving":{"dm":false, "logs":true} + } + } +} \ No newline at end of file diff --git a/config/options.json b/config/options.json new file mode 100644 index 0000000..6dc8604 --- /dev/null +++ b/config/options.json @@ -0,0 +1,122 @@ +[ + { + "id":"example-ticket", + "name":"Question", + "description":"Create this ticket if you have a question! (or leave empty)", + "type":"ticket", + + "button":{ + "emoji":"🎫 (or leave empty)", + "label":"question (or leave empty)", + "color":"gray OR red OR green OR blue" + }, + + "ticketAdmins":["discord role id"], + "readonlyAdmins":["discord role id"], + "allowCreationByBlacklistedUsers":false, + "questions":["example-question-1","example-question-2"], + + "channel":{ + "prefix":"question-", + "suffix":"user-name OR user-id OR random-number OR random-hex OR counter-dynamic OR counter-fixed", + "category":"category id (or leave empty)", + "closedCategory":"category id (or leave empty)", + "backupCategory":"category id (or leave empty)", + "claimedCategory":[ + {"user":"user id","category":"category id"} + ], + "description":"This is a question ticket (or leave empty)" + }, + + "dmMessage":{ + "enabled":false, + "text":"Thanks for creating a ticket in our server! (or leave empty)", + "embed":{ + "enabled":false, + "title":"Embed Title! (or leave empty)", + "description":"Description (or leave empty)", + "customColor":"#f8ab00 (or leave empty)", + + "image":"https://www.example.com/image.png (or leave empty)", + "thumbnail":"https://www.example.com/image.png (or leave empty)", + "fields":[ + {"name":"field name","value":"field value","inline":false} + ], + "timestamp":false + } + }, + "ticketMessage":{ + "enabled":true, + "text":"", + "embed":{ + "enabled":true, + "title":"Question Ticket", + "description":"Thanks for creating a 'Question' ticket in our server!\nOur support team will help you soon!", + "customColor":"#f8ab00 (or leave empty)", + + "image":"https://www.example.com/image.png (or leave empty)", + "thumbnail":"https://www.example.com/image.png (or leave empty)", + "fields":[ + {"name":"field name","value":"field value","inline":false} + ], + "timestamp":false + }, + "ping":{ + "@here":true, + "@everyone":false, + "custom":["discord role id"] + } + }, + "autoclose":{ + "enableInactiveHours":false, + "inactiveHours":24, + "enableUserLeave":true, + "disableOnClaim":false + }, + "autodelete":{ + "enableInactiveDays":false, + "inactiveDays":7, + "enableUserLeave":true, + "disableOnClaim":false + }, + "cooldown":{ + "enabled":false, + "cooldownMinutes":10 + }, + "limits":{ + "enabled":false, + "globalMaximum":20, + "userMaximum":3 + } + }, + { + "id":"example-website", + "name":"Website", + "description":"Go to our website.", + "type":"website", + + "button":{ + "emoji":"😃", + "label":"Visit our website" + }, + + "url":"https://www.dj-dj.be" + }, + { + "id":"example-role", + "name":"Update Ping", + "description":"Click here to get notified on updates!", + "type":"role", + + "button":{ + "emoji":"📢", + "label":"Update Ping", + "color":"gray OR red OR green OR blue" + }, + + "roles":["discord role id"], + "mode":"add&remove OR remove OR add", + "removeRolesOnAdd":["discord role id"], + "addOnMemberJoin":false + } +] \ No newline at end of file diff --git a/config/panels.json b/config/panels.json new file mode 100644 index 0000000..805ced5 --- /dev/null +++ b/config/panels.json @@ -0,0 +1,39 @@ +[ + { + "id":"example-embed", + "name":"Example Embed", + "dropdown":false, + "options":["example-ticket","example-website","example-role"], + + "text":"", + "embed":{ + "enabled":true, + "title":"Tickets:", + "description":"Create a ticket by clicking one of the buttons below!", + + "customColor":"#f8ab00 (or leave empty)", + "url":"https://openticket.dj-dj.be (or leave empty)", + + "image":"https://www.example.com/image.png (or leave empty)", + "thumbnail":"https://www.example.com/image.png (or leave empty)", + + "footer":"Open Ticket v4.0.0 (or leave empty)", + "fields":[ + {"name":"field name","value":"field value","inline":false} + ], + "timestamp":false + }, + "settings":{ + "dropdownPlaceholder":"Create a ticket!", + + "enableMaxTicketsWarningInText":false, + "enableMaxTicketsWarningInEmbed":true, + + "describeOptionsLayout":"simple OR normal OR detailed", + "describeOptionsCustomTitle":"", + "describeOptionsInText":false, + "describeOptionsInEmbedFields":true, + "describeOptionsInEmbedDescription":false + } + } +] \ No newline at end of file diff --git a/config/questions.json b/config/questions.json new file mode 100644 index 0000000..0d2556e --- /dev/null +++ b/config/questions.json @@ -0,0 +1,28 @@ +[ + { + "id":"example-question-1", + "name":"Example Question 1", + "type":"short", + + "required":true, + "placeholder":"Insert your short answer here!", + "length":{ + "enabled":false, + "min":0, + "max":1000 + } + }, + { + "id":"example-question-2", + "name":"Example Question 2", + "type":"paragraph", + + "required":false, + "placeholder":"Insert your long answer here!", + "length":{ + "enabled":false, + "min":0, + "max":1000 + } + } +] \ No newline at end of file diff --git a/config/transcripts.json b/config/transcripts.json new file mode 100644 index 0000000..5ce9a88 --- /dev/null +++ b/config/transcripts.json @@ -0,0 +1,57 @@ +{ + "general":{ + "enabled":false, + + "enableChannel":false, + "enableCreatorDM":false, + "enableParticipantDM":false, + "enableActiveAdminDM":false, + "enableEveryAdminDM":false, + + "channel":"transcript channel id (or leave empty)", + "mode":"html OR text" + }, + "embedSettings":{ + "customColor":"#f8ab00 (or leave empty)", + "listAllParticipants":false, + "includeTicketStats":false + }, + "textTranscriptStyle":{ + "layout":"simple OR normal OR detailed", + "includeStats":true, + "includeIds":false, + "includeEmbeds":true, + "includeFiles":true, + "includeBotMessages":true, + + "fileMode":"custom OR channel-name OR channel-id OR user-name OR user-id", + "customFileName":"this-is-a-transcript (or leave empty)" + }, + "htmlTranscriptStyle":{ + "background":{ + "enableCustomBackground":false, + "backgroundColor":"#f8ba00 (or leave empty)", + "backgroundImage":"https://www.example.com/image.png (or leave empty)" + + }, + "header":{ + "enableCustomHeader":false, + "backgroundColor":"#202225", + "decoColor":"#f8ba00", + "textColor":"#ffffff" + + }, + "stats":{ + "enableCustomStats":false, + "backgroundColor":"#202225", + "keyTextColor":"#737373", + "valueTextColor":"#ffffff", + "hideBackgroundColor":"#40444a", + "hideTextColor":"#ffffff" + }, + "favicon":{ + "enableCustomFavicon":false, + "imageUrl":"https://transcripts.dj-dj.be/favicon.png" + } + } +} \ No newline at end of file diff --git a/database/global.json b/database/global.json new file mode 100644 index 0000000..fb44b71 --- /dev/null +++ b/database/global.json @@ -0,0 +1,7 @@ +[ + {"category":"openticket:last-version","key":"openticket:version","value":"v4.0.0"}, + {"category":"openticket:last-version","key":"openticket:api","value":"v1.0.0"}, + {"category":"openticket:last-version","key":"openticket:transcripts","value":"v2.0.0"}, + {"category":"openticket:last-version","key":"openticket:livestatus","value":"v2.0.0"}, + {"category":"openticket:last-version","key":"openticket:last-version","value":"v4.0.0"} +] \ No newline at end of file diff --git a/database/options.json b/database/options.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/database/options.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/database/stats.json b/database/stats.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/database/stats.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/database/tickets.json b/database/tickets.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/database/tickets.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/database/users.json b/database/users.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/database/users.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/languages/custom.json b/languages/custom.json new file mode 100644 index 0000000..8d36a30 --- /dev/null +++ b/languages/custom.json @@ -0,0 +1,477 @@ +{ + "_TRANSLATION":{ + "otversion":"v4.0.0", + "translator":"DJj123dj", + "lastedited":"21/08/2024", + "language":"Custom" + }, + "checker":{ + "system":{ + "typeError":"[ERROR]", + "headerOpenTicket":"OPEN TICKET", + "typeWarning":"[WARNING]", + "typeInfo":"[INFO]", + "headerConfigChecker":"CONFIG CHECKER", + "headerDescription":"check for errors in you config files!", + "footerError":"the bot won't start until all {0}'s are fixed!", + "footerWarning":"it's recommended to fix all {0}'s before starting!", + "footerSupport":"SUPPORT: {0} - DOCS: {1}", + "compactInformation":"use {0} for more information!", + "dataPath":"path", + "dataDocs":"docs", + "dataMessages":"message" + }, + "messages":{ + "stringTooShort":"This string can't be shorter than {0} characters!", + "stringTooLong":"This string can't be longer than {0} characters!", + "stringLengthInvalid":"This string needs to be {0} characters long!", + "stringStartsWith":"This string needs to start with {0}!", + "stringEndsWith":"This string needs to end with {0}!", + "stringContains":"This string needs to contain {0}!", + "stringChoices":"This string can only be one of the following values: {0}!", + "stringRegex":"This string is invalid!", + + "numberTooShort":"This number can't be shorter than {0} characters!", + "numberTooLong":"This number can't be longer than {0} characters!", + "numberLengthInvalid":"This number needs to be {0} characters long!", + "numberTooSmall":"This number needs to be at least {0}!", + "numberTooLarge":"This number needs to be at most {0}!", + "numberNotEqual":"This number needs to be {0}!", + "numberStep":"This number needs to be a multiple of {0}!", + "numberStepOffset":"This number needs to be a multiple of {0} starting with {1}!", + "numberStartsWith":"This number needs to start with {0}!", + "numberEndsWith":"This number needs to end with {0}!", + "numberContains":"This number needs to contain {0}!", + "numberChoices":"This number can only be one of the following values: {0}!", + "numberFloat":"This number can't be a decimal!", + "numberNegative":"This number can't be negative!", + "numberPositive":"This number can't be positive!", + "numberZero":"This number can't be zero!", + + "booleanTrue":"This boolean can't be true!", + "booleanFalse":"This boolean can't be false!", + + "arrayEmptyDisabled":"This array isn't allowed to be empty!", + "arrayEmptyRequired":"This array is required to be empty!", + "arrayTooShort":"This array needs to have a length of at least {0}!", + "arrayTooLong":"This array needs to have a length of at most {0}!", + "arrayLengthInvalid":"This array needs to have a length of {0}!", + "arrayInvalidTypes":"This array can only contain the following types: {0}!", + "arrayDouble":"This array doesn't allow the same value twice!", + + "discordInvalidId":"This is an invalid discord {0} id!", + "discordInvalidIdOptions":"This is an invalid discord {0} id! You can also use one of these: {1}!", + "discordInvalidToken":"This is an invalid discord token (syntactically)!", + "colorInvalid":"This is an invalid hex color!", + "emojiTooShort":"This string needs to have at least {0} emoji's!", + "emojiTooLong":"This string needs to have at most {0} emoji's!", + "emojiCustom":"This emoji can't be a custom discord emoji!", + "emojiInvalid":"This is an invalid emoji!", + "urlInvalid":"This url is invalid!", + "urlInvalidHttp":"This url can only use the https:// protocol!", + "urlInvalidProtocol":"This url can only use the http:// & https:// protocols!", + "urlInvalidHostname":"This url has a disallowed hostname!", + "urlInvalidExtension":"This url has an invalid extension! Choose between: {0}!", + "urlInvalidPath":"This url has an invalid path!", + "idNotUnique":"This id isn't unique, use another id instead!", + "idNonExistent":"The id {0} doesn't exist!", + + "invalidType":"This property needs to be the type: {0}!", + "propertyMissing":"The property {0} is missing from this object!", + "propertyOptional":"The property {0} is optional in this object!", + "objectDisabled":"This object is disabled, enable it using {0}!", + "nullInvalid":"This property can't be null!", + "switchInvalidType":"This needs to be one of the following types: {0}!", + "objectSwitchInvalid":"This object needs to be one of the following types: {0}!", + + "invalidLanguage":"This is an invalid language!", + "invalidButton":"This button needs to have at least an {0} or {1}!", + "unusedOption":"The option {0} isn't used anywhere!", + "unusedQuestion":"The question {0} isn't used anywhere!", + "dropdownOption":"A panel with dropdown enabled can only contain options of the 'ticket' type!" + } + }, + "actions":{ + "buttons":{ + "create":"Visit Ticket", + "close":"Close Ticket", + "delete":"Delete Ticket", + "reopen":"Reopen Ticket", + "claim":"Claim Ticket", + "unclaim":"Unclaim Ticket", + "pin":"Pin Ticket", + "unpin":"Unpin Ticket", + "clear":"Delete Tickets", + "helpSwitchSlash":"View Slash Commands", + "helpSwitchText":"View Text Commands", + "helpPage":"Page {0}", + "withReason":"With Reason", + "withoutTranscript":"Without Transcript" + }, + "titles":{ + "created":"Ticket Created", + "close":"Ticket Closed", + "delete":"Ticket Deleted", + "reopen":"Ticket Reopened", + "claim":"Ticket Claimed", + "unclaim":"Ticket Unclaimed", + "pin":"Ticket Pinned", + "unpin":"Ticket Unpinned", + "rename":"Ticket Renamed", + "move":"Ticket Moved", + "add":"Ticket User Added", + "remove":"Ticket User Removed", + + "help":"Available Commands", + "statsReset":"Reset Stats", + "blacklistAdd":"User Blacklisted", + "blacklistRemove":"User Released", + "blacklistGet":"Blacklisted User", + "blacklistView":"Current Blacklist", + "blacklistAddDm":"Added To Blacklist", + "blacklistRemoveDm":"Removed From Blacklist", + "clear":"Tickets Cleared", + "roles":"Roles Updated", + + "autoclose":"Ticket Autoclosed", + "autocloseEnabled":"Autoclose Enabled", + "autocloseDisabled":"Autoclose Disabled", + "autodelete":"Ticket Autodeleted", + "autodeleteEnabled":"Autodelete Enabled", + "autodeleteDisabled":"Autodelete Disabled" + }, + "descriptions":{ + "create":"Your ticket has been created. Click the button below to access it!", + "close":"The ticket has been closed succesfully!", + "delete":"The ticket has been deleted succesfully!", + "reopen":"The ticket has been reopened succesfully!", + "claim":"The ticket has been claimed succesfully!", + "unclaim":"The ticket has been unclaimed succesfully!", + "pin":"The ticket has been pinned succesfully!", + "unpin":"The ticket has been unpinned succesfully!", + "rename":"The ticket has been renamed to {0} succesfully!", + "move":"The ticket has been moved to {0} succesfully!", + "add":"{0} has been added to the ticket succesfully!", + "remove":"{0} has been removed from the ticket succesfully!", + + "helpExplanation":"`` => required parameter\n`[name]` => optional parameter", + "statsReset":"The bot stats have been reset successfully!", + "statsError":"Unable to view ticket stats!\n{0} is not a ticket!", + "blacklistAdd":"{0} has been blacklisted successfully!", + "blacklistRemove":"{0} has been released successfully!", + "blacklistGetSuccess":"{0} is currently blacklisted!", + "blacklistGetEmpty":"{0} is currently not blacklisted!", + "blacklistViewEmpty":"No-one has been blacklisted yet!", + "blacklistViewTip":"Use \"/blacklist add\" to blacklist a user!", + "clearVerify":"Are you sure you want to delete multiple tickets?\nThis action can't be undone!", + "clearReady":"{0} tickets have been deleted succesfully!", + "rolesEmpty":"No roles have been updated!", + + "autocloseLeave":"This ticket has been autoclosed because the creator left the server!", + "autocloseTimeout":"This ticket has been autoclosed because it has been inactive for more than `{0}h`!", + "autodeleteLeave":"This ticket has been autodeleted because the creator left the server!", + "autodeleteTimeout":"This ticket has been autodeleted because it has been inactive for more than `{0} days`!", + "autocloseEnabled":"Autoclose has been enabled in this ticket!\nIt will be closed when it is inactive for more than `{0}h`!", + "autocloseDisabled":"Autoclose has been disabled in this ticket!\nIt won't be closed automatically anymore!", + "autodeleteEnabled":"Autodelete has been enabled in this ticket!\nIt will be deleted when it is inactive for more than `{0} days`!", + "autodeleteDisabled":"Autodelete has been disabled in this ticket!\nIt won't be deleted automatically anymore!", + + "ticketMessageLimit":"You can only create {0} ticket(s) at the same time!", + "ticketMessageAutoclose":"This ticket will be autoclosed when inactive for {0}h!", + "ticketMessageAutodelete":"This ticket will be autodeleted when inactive for {0} days!", + "panelReady":"You can find the panel below!\nThis message can now be deleted!" + }, + "modal":{ + "closePlaceholder":"Why did you close this ticket?", + "deletePlaceholder":"Why did you delete this ticket?", + "reopenPlaceholder":"Why did you reopen this ticket?", + "claimPlaceholder":"Why did you claim this ticket?", + "unclaimPlaceholder":"Why did you unclaim this ticket?", + "pinPlaceholder":"Why did you pin this ticket?", + "unpinPlaceholder":"Why did you unpin this ticket?" + }, + "logs":{ + "createLog":"A new ticket got created by {0}!", + "closeLog":"This ticket has been closed by {0}!", + "closeDm":"Your ticket has been closed in our server!", + "deleteLog":"This ticket has been deleted by {0}!", + "deleteDm":"Your ticket has been deleted in our server!", + "reopenLog":"This ticket has been reopened by {0}!", + "reopenDm":"Your ticket has been reopened in our server!", + "claimLog":"This ticket has been claimed by {0}!", + "claimDm":"Your ticket has been claimed in our server!", + "unclaimLog":"This ticket has been unclaimed by {0}!", + "unclaimDm":"Your ticket has been unclaimed in our server!", + "pinLog":"This ticket has been pinned by {0}!", + "pinDm":"Your ticket has been pinned in our server!", + "unpinLog":"This ticket has been unpinned by {0}!", + "unpinDm":"Your ticket has been unpinned in our server!", + "renameLog":"This ticket has been renamed to {0} by {1}!", + "renameDm":"Your ticket has been renamed to {0} in our server!", + "moveLog":"This ticket has been moved to {0} by {1}!", + "moveDm":"Your ticket has been moved to {0} in our server!", + "addLog":"{0} has been added to this ticket by {1}!", + "addDm":"{0} has been added to your ticket in our server!", + "removeLog":"{0} has been removed from this ticket by {1}!", + "removeDm":"{0} has been removed from your ticket in our server!", + + "blacklistAddLog":"{0} was blacklisted by {1}!", + "blacklistRemoveLog":"{0} was removed from the blacklist by {1}!", + "blacklistAddDm":"You have been blacklisted in our server!\nFrom now on, you are unable to create a ticket!", + "blacklistRemoveDm":"You have been removed from the blacklist in our server!\nNow you can create tickets again!", + "clearLog":"{0} tickets have been deleted by {1}!" + } + }, + "transcripts":{ + "success":{ + "visit":"Visit Transcript", + "ready":"Transcript Created", + "textFileDescription":"This is the text transcript of a deleted ticket!", + "htmlProgress":"Please wait while this html transcript is getting processed...", + + "createdChannel":"A new {0} transcript has been created in the server!", + "createdCreator":"A new {0} transcript has been created for one of your tickets!", + "createdParticipant":"A new {0} transcript has been created in one of the tickets you participated in!", + "createdActiveAdmin":"A new {0} transcript has been created in one of the tickets you participated as admin!", + "createdEveryAdmin":"A new {0} transcript has been created in one of the tickets you were admin in!", + "createdOther":"A new {0} transcript has been created!" + }, + "errors":{ + "retry":"Retry", + "continue":"Delete Without Transcript", + "backup":"Create Backup Transcript", + "error":"Something went wrong while trying to create the transcript.\nWhat would you like to do?\n\nThis ticket won't be deleted until you click one of these buttons." + } + }, + "errors":{ + "titles":{ + "internalError":"Internal Error", + "optionMissing":"Command Option Missing", + "optionInvalid":"Command Option Invalid", + "unknownCommand":"Unknown Command", + "noPermissions":"No Permissions", + "unknownTicket":"Unknown Ticket", + "deprecatedTicket":"Deprecated Ticket", + "unknownOption":"Unknown Option", + "unknownPanel":"Unknown Panel", + "notInGuild":"Not In Server", + "channelRename":"Unable To Rename Channel", + "busy":"Ticket Is Busy" + }, + "descriptions":{ + "askForInfo":"Contact the owner of this bot for more info!", + "askForInfoResolve":"Contact the bot owner of this bot if this issue doesn't resolve after a few tries.", + "internalError":"Failed to respond to this {0} due to an internal error!", + "optionMissing":"A required parameter is missing in this command!", + "optionInvalid":"A parameter in this command is invalid!", + "optionInvalidChoose":"Choose between", + "unknownCommand":"Try visiting the help menu for more info!", + "noPermissions":"You are not allowed to use this {0}!", + "noPermissionsList":"Required Permissions: (one of them)", + "noPermissionsCooldown":"You are not allowed to use this {0} because you have a cooldown!", + "noPermissionsBlacklist":"You are not allowed to use this {0} because you have been blacklisted!", + "noPermissionsLimitGlobal":"You are not allowed to create a ticket because the server reached the max tickets limit!", + "noPermissionsLimitGlobalUser":"You are not allowed to create a ticket because you reached the max tickets limit!", + "noPermissionsLimitOption":"You are not allowed to create a ticket because the server reached the max tickets limit for this option!", + "noPermissionsLimitOptionUser":"You are not allowed to create a ticket because you reached the max tickets limit for this option!", + "unknownTicket":"Try this command again in a valid ticket!", + "deprecatedTicket":"The current channel is not a valid ticket! It might have been a ticket from an old Open Ticket version!", + "notInGuild":"This {0} doesn't work in DM! Please try it again in a server!", + "channelRename":"Due to discord ratelimits, it's currently impossible for the bot to rename the channel. The channel will automatically be renamed over 10 minutes if the bot isn't rebooted.", + "channelRenameSource":"The source of this error is: {0}", + "busy":"Unable to use this {0}!\nThe ticket is currently being processed by the bot.\n\nPlease try again in a few seconds!" + }, + "optionInvalidReasons":{ + "stringRegex":"Value doesn't match pattern!", + "stringMinLength":"Value needs to be at least {0} characters!", + "stringMaxLength":"Value needs to be at most {0} characters!", + "numberInvalid":"Invalid number!", + "numberMin":"Number needs to be at least {0}!", + "numberMax":"Number needs to be at most {0}!", + "numberDecimal":"Number is not allowed to be a decimal!", + "numberNegative":"Number is not allowed to be negative!", + "numberPositive":"Number is not allowed to be positive!", + "numberZero":"Number is not allowed to be zero!", + "channelNotFound":"Unable to find channel!", + "userNotFound":"Unable to find user!", + "roleNotFound":"Unable to find role!", + "memberNotFound":"Unable to find user!", + "mentionableNotFound":"Unable to find user or role!", + "channelType":"Invalid channel type!", + "notInGuild":"This option requires you to be in a server!" + }, + "permissions":{ + "developer":"You need to be the developer of the bot.", + "owner":"You need to be the server owner.", + "admin":"You need to be a server admin.", + "moderator":"You need to be a moderator.", + "support":"You need to be in the support team.", + "member":"You need to be a member.", + "discord-administrator":"You need to have the `ADMINISTRATOR` permission." + }, + "actionInvalid":{ + "close":"Ticket is already closed!", + "reopen":"Ticket is not closed yet!", + "claim":"Ticket is already claimed!", + "unclaim":"Ticket is not claimed yet!", + "pin":"Ticket is already pinned!", + "unpin":"Ticket is not pinned yet!", + "add":"This user is already able to access the ticket!", + "remove":"Unable to remove this user from the ticket!" + } + }, + "params":{ + "uppercase":{ + "ticket":"Ticket", + "tickets":"Tickets", + "reason":"Reason", + "creator":"Creator", + "remaining":"Time Remaining", + "added":"Added", + "removed":"Removed", + "filter":"Filter", + "claimedBy":"Claimed By {0}", + "method":"Method", + "type":"Type", + "blacklisted":"Blacklisted", + "panel":"Panel", + "command":"Command", + "system":"System", + "true":"True", + "false":"False", + "syntax":"Syntax", + "originalName":"Original Name", + "newName":"New Name", + "until":"Until", + "validOptions":"Valid Options", + "validPanels":"Valid Panels", + "autoclose":"Autoclose", + "autodelete":"Autodelete", + "startupDate":"Startup Date", + "version":"Version", + "name":"Name", + "role":"Role", + "status":"Status", + "claimed":"Claimed", + "pinned":"Pinned", + "creationDate":"Creation Date" + }, + "lowercase":{ + "text":"text", + "html":"html", + "command":"command", + "modal":"modal", + "button":"button", + "dropdown":"dropdown", + "method":"method" + } + }, + "commands":{ + "reason":"Specify an optional reason that will be visible in logs.", + "help":"Get a list of all the available commands.", + "panel":"Spawn a message with a dropdown or buttons (for ticket creation).", + "panelId":"The identifier of the panel that you want to spawn.", + "panelAutoUpdate":"Do you want this panel to automatically update when edited?", + "ticket":"Instantly create a ticket.", + "ticketId":"The identifier of the ticket that you want to create.", + "close":"Close a ticket.", + "delete":"Delete a ticket.", + "deleteNoTranscript":"Delete this ticket without creating a transcript.", + "reopen":"Reopen a ticket.", + "claim":"Claim a ticket.", + "claimUser":"Claim this ticket to someone else instead of yourself.", + "unclaim":"Unclaim a ticket.", + "pin":"Pin a ticket.", + "unpin":"Unpin a ticket.", + "move":"Move a ticket.", + "moveId":"The identifier of the option that you want to move to.", + "rename":"Rename a ticket.", + "renameName":"The new name for this ticket.", + "add":"Add a user to a ticket.", + "addUser":"The user to add.", + "remove":"Remove a user from a ticket.", + "removeUser":"The user to remove.", + "blacklist":"Manage the ticket blacklist.", + "blacklistView":"View a list of the current blacklist.", + "blacklistAdd":"Add a user to the blacklist.", + "blacklistRemove":"Remove a user from the blacklist.", + "blacklistGet":"Get the details from a blacklisted user.", + "blacklistGetUser":"The user to get details from.", + "stats":"View statistics from the bot, a member or a ticket.", + "statsReset":"Reset all the stats of the bot (and start counting from zero).", + "statsGlobal":"View the global stats.", + "statsUser":"View the stats from a user in the server.", + "statsUserUser":"The user to view.", + "statsTicket":"View the stats of a ticket in the server.", + "statsTicketTicket":"The ticket to view.", + "clear":"Delete multiple tickets at the same time.", + "clearFilter":"The filter for clearing tickets.", + "clearFilters":{ + "all":"All", + "open":"Open", + "close":"Closed", + "claim":"Claimed", + "unclaim":"Unclaimed", + "pin":"Pinned", + "unpin":"Unpinned", + "autoclose":"Autoclosed" + }, + "autoclose":"Manage autoclose in a ticket.", + "autocloseDisable":"Disable autoclose in this ticket.", + "autocloseEnable":"Enable autoclose in this ticket.", + "autocloseEnableTime":"The amount of hours this ticket needs to be inactive to close it.", + "autodelete":"Manage autodelete in a ticket.", + "autodeleteDisable":"Disable autodelete in this ticket.", + "autodeleteEnable":"Enable autodelete in this ticket.", + "autodeleteEnableTime":"The amount of days this ticket needs to be inactive to delete it." + }, + "helpMenu":{ + "help":"Get a list of all the available commands.", + "ticket":"Instantly create a ticket.", + "close":"Close a ticket, this disables writing in this channel.", + "delete":"Delete a ticket, this creates a transcript when enabled.", + "reopen":"Reopen a ticket, this enables writing in this channel again.", + "pin":"Pin a ticket. This will move the ticket to the top and will add a '📌' emoij to the name.", + "unpin":"Unpin a ticket. The ticket will stay on it's position but will lose the '📌' emoij.", + "move":"Move a ticket. This will change the type of this ticket.", + "rename":"Rename a ticket. This will change the channel name of this ticket.", + "claim":"Claim a ticket. With this, you can let your team know you are handling this ticket.", + "unclaim":"Unclaim a ticket. With this, you can let your team know that this ticket is free again.", + "add":"Add a user to a ticket. This will allow the user to read & write in this ticket.", + "remove":"Remove a user from a ticket. This will remove the ability to read & write for a user in this ticket.", + "panel":"Spawn a message with a dropdown or buttons (for ticket creation).", + "blacklistView":"View a list of the current blacklist.", + "blacklistAdd":"Add a user to the blacklist.", + "blacklistRemove":"Remove a user from the blacklist.", + "blacklistGet":"Get the details from a blacklisted user.", + "statsGlobal":"View the global stats.", + "statsTicket":"View the stats of a ticket in the server.", + "statsUser":"View the stats from a user in the server.", + "statsReset":"Reset all the stats of the bot (and start counting from zero).", + "autocloseDisable":"Disable autoclose in this ticket.", + "autocloseEnable":"Enable autoclose in this ticket.", + "autodeleteDisable":"Disable autodelete in this ticket.", + "autodeleteEnable":"Enable autodelete in this ticket." + }, + "stats":{ + "scopes":{ + "global":"Global Stats", + "system":"System Stats", + "user":"User Stats", + "ticket":"Ticket Stats", + "participants":"Participants" + }, + "properties":{ + "ticketsCreated":"Tickets Created", + "ticketsClosed":"Tickets Closed", + "ticketsDeleted":"Tickets Deleted", + "ticketsReopened":"Tickets Reopened", + "ticketsAutoclosed":"Tickets Autoclosed", + "ticketsClaimed":"Tickets Claimed", + "ticketsPinned":"Tickets Pinned", + "ticketsMoved":"Tickets Moved", + "usersBlacklisted":"Users Blacklisted", + "transcriptsCreated":"Transcripts Created" + } + } +} \ No newline at end of file diff --git a/languages/czech.json b/languages/czech.json new file mode 100644 index 0000000..c19fca7 --- /dev/null +++ b/languages/czech.json @@ -0,0 +1,477 @@ +{ + "_TRANSLATION":{ + "otversion":"v4.0.0", + "translator":"spyeye_", + "lastedited":"21/08/2024", + "language":"Czech" + }, + "checker":{ + "system":{ + "typeError":"[ERROR]", + "headerOpenTicket":"OPEN TICKET", + "typeWarning":"[WARNING]", + "typeInfo":"[INFO]", + "headerConfigChecker":"CONFIG CHECKER", + "headerDescription":"kontrola chyb v konfiguračních souborech!", + "footerError":"Bot se nespustí, dokud všechny {0} nebudou opraveny!", + "footerWarning":"Doporučejeme opravit všechny {0} před spuštěním!", + "footerSupport":"PODPORA: {0} - DOKUMENTACE: {1}", + "compactInformation":"Použij {0} pro více informací!", + "dataPath":"path", + "dataDocs":"docs", + "dataMessages":"message" + }, + "messages":{ + "stringTooShort":"Tento string nesmí být kratší než {0} znaků!", + "stringTooLong":"Tento string nesmí být delší než {0} znaků!", + "stringLengthInvalid":"Tento string musí mít délku {0} znaků!", + "stringStartsWith":"Tento string musí začínat {0}!", + "stringEndsWith":"Tento string musí končit {0}!", + "stringContains":"Tento string musí obsahovat {0}!", + "stringChoices":"Tento string může být pouze jednou z následujících možností: {0}!", + "stringRegex":"Tento string je neplatný!", + + "numberTooShort":"Toto číslo nesmí být kratší než {0} znaků!", + "numberTooLong":"Toto číslo nesmí být delší než {0} znaků.!", + "numberLengthInvalid":"Toto číslo musí mít délku {0} znaků.!", + "numberTooSmall":"Toto číslo musí být alespoň {0}.!", + "numberTooLarge":"Toto číslo musí být nejvýše {0}!", + "numberNotEqual":"Toto číslo musí být {0}!", + "numberStep":"Toto číslo musí být násobkem {0}.!", + "numberStepOffset":"Toto číslo musí být násobkem {0} žačínající {1}.!", + "numberStartsWith":"Toto číslo musí začínat {0}!", + "numberEndsWith":"Toto číslo musí končit {0}!", + "numberContains":"Toto číslo musí obsahovat {0}!", + "numberChoices":"Toto číslo může být pouze jedno z následujících hodnot: {0}!", + "numberFloat":"Toto číslo nemůže být desetinné!", + "numberNegative":"Toto číslo nemůže být záporné!", + "numberPositive":"Toto číslo nemůže být kladné!", + "numberZero":"Toto číslo nemůže být nula!", + + "booleanTrue":"Tento boolean nemůže být true!", + "booleanFalse":"Tento boolean nemůže být false!", + + "arrayEmptyDisabled":"Toto pole nesmí být prázdné.!", + "arrayEmptyRequired":"Toto pole musí být prázdné!", + "arrayTooShort":"Toto pole musí mít délku alespoň {0}!", + "arrayTooLong":"Toto pole musí mít délku maximálně {0}!", + "arrayLengthInvalid":"Toto pole musí mít délku {0}!", + "arrayInvalidTypes":"Toto pole může obsahovat pouze následující typy: {0}!", + "arrayDouble":"Toto pole neumožňuje zadat stejnou hodnotu dvakrát.!", + + "discordInvalidId":"Toto je neplatné discord {0} id!", + "discordInvalidIdOptions":"Toto je neplatné Discord ID {0}! Můžete také použít jeden z těchto: {1}!", + "discordInvalidToken":"Toto je neplatný discord token (syntakticky)!", + "colorInvalid":"Toto je neplatná hex barva!", + "emojiTooShort":"Tento string musí obsahovat alespoň {0} emotikonů.!", + "emojiTooLong":"Tento string musí obsahovat nejvýše {0} emotikonů.!", + "emojiCustom":"Tento emotikon nemůže být custom emotikon!", + "emojiInvalid":"Toto je neplatný emotikon!", + "urlInvalid":"Tato url adresa je neplatná!", + "urlInvalidHttp":"Tato url může používat pouze protokol https://!", + "urlInvalidProtocol":"Tato url může používat pouze protokoly http:// a https://!", + "urlInvalidHostname":"Tato url má zakázané hostitelské jméno!", + "urlInvalidExtension":"Tato url má neplatnou příponu! Vyberte si mezi: {0}!", + "urlInvalidPath":"Tato url má neplatnou cestu!", + "idNotUnique":"Toto id není jedinečné, místo něj použijte jiné id!", + "idNonExistent":"Id {0} neexistuje!", + + "invalidType":"Toto nastavení musí být typu: {0}!", + "propertyMissing":"U tohoto objektu chybí vlastnost {0}!", + "propertyOptional":"Vlastnost {0} je v tomto objektu nepovinná!", + "objectDisabled":"Tento objekt je zakázán, povolte jej pomocí {0}!", + "nullInvalid":"Tato vlastnost nemůže být nulová!", + "switchInvalidType":"Musí se jednat o jeden z následujících typů: {0}!", + "objectSwitchInvalid":"Tento objekt musí být jednoho z následujících typů: {0}!", + + "invalidLanguage":"Jedná se o neplatný jazyk!", + "invalidButton":"Toto tlačítko musí mít alespoň {0} nebo {1}!", + "unusedOption":"Možnost {0} se nikde nepoužívá!", + "unusedQuestion":"Otázka {0} není nikde použita!", + "dropdownOption":"Panel s povoleným rozevíracím seznamem může obsahovat pouze možnosti typu „ticketu“!" + } + }, + "actions":{ + "buttons":{ + "create":"Zobrazit ticket", + "close":"Uzavřít ticket", + "delete":"Vymazat ticket", + "reopen":"Znovu otevřít ticket", + "claim":"Převzít ticket", + "unclaim":"Zrušit převzetí ticketu", + "pin":"Připnout ticket", + "unpin":"Odepnout ticket", + "clear":"Vymazat tickety", + "helpSwitchSlash":"Zobrazit slash příkazy", + "helpSwitchText":"Zobrazit textové příkazy", + "helpPage":"Strana {0}", + "withReason":"S důvodem", + "withoutTranscript":"Bez důvodu" + }, + "titles":{ + "created":"Ticket vytvořen", + "close":"Ticket uzavřen", + "delete":"Ticket vymazán", + "reopen":"Ticket znovu otevřen", + "claim":"Ticket převzat", + "unclaim":"Ticket již není převezmut", + "pin":"Ticket připnut", + "unpin":"Ticket odepnut", + "rename":"Ticket přejmenován", + "move":"Ticket přesunut", + "add":"Uživatel přidán", + "remove":"Uživatel odebrán", + + "help":"Dostupné příkazy:", + "statsReset":"Resetovat statistiky", + "blacklistAdd":"Uživatel blacklistován", + "blacklistRemove":"Uživatel odebrán z blacklistu", + "blacklistGet":"Uživatel na blacklistu", + "blacklistView":"Aktuální blacklist", + "blacklistAddDm":"Přidán na blacklistu", + "blacklistRemoveDm":"Odebrán z blacklistu", + "clear":"Tickety vyčištěny", + "roles":"Role aktualizovány", + + "autoclose":"Ticket automaticky uzavřen", + "autocloseEnabled":"Automatické uzavření povoleno", + "autocloseDisabled":"Automatické uzavření zakázáno", + "autodelete":"Ticket automaticky vymazán", + "autodeleteEnabled":"Automatické mazání povoleno", + "autodeleteDisabled":"Automatické mazání zakázáno" + }, + "descriptions":{ + "create":"Tvůj ticket byl vytvořen. Klikni na tlačítko pod zprávou pro zobrazení!", + "close":"Ticket byl úspěšně uzavřen!", + "delete":"Ticket byl úspěšně vymazán!", + "reopen":"Ticket byl úspěšně znovu otevřen!", + "claim":"Ticket byl úspěšně převzat!", + "unclaim":"Ticket již není převezmut!", + "pin":"Ticket byl úspěšně připnut!", + "unpin":"Ticket byl úspěšně odepnut!", + "rename":"Ticket byl úspěšně přejmenován na {0}!", + "move":"Ticket byl úspěšně přesunut do {0}!", + "add":"{0} byl úspěšně přidán do ticketu!", + "remove":"{0} byl úspěšně odstraněn z ticketu!", + + "helpExplanation":"`` => povinný parametr\n`[name]` => nepovinný parametr", + "statsReset":"Statistiky bota byly úspěšně resetovány!", + "statsError":"Nelze zobrazit statistiky ticketu!\n{0} není ticket!", + "blacklistAdd":"{0} byl přídán na blacklist!", + "blacklistRemove":"{0} byl odebrán z blacklistu!", + "blacklistGetSuccess":"{0} je nyní na blacklistu!", + "blacklistGetEmpty":"{0} nyní není na blacklistu!", + "blacklistViewEmpty":"Na blacklistu ještě nikdo není", + "blacklistViewTip":"Použij \"/blacklist add\" pro přidání uživatele na blacklist!", + "clearVerify":"Opravdu chcete smazat více ticketů?\nTuto akci nelze vrátit zpět!", + "clearReady":"{0} ticketů bylo úspěšně smazány!", + "rolesEmpty":"Žádné role nebyly aktualizovány!", + + "autocloseLeave":"Tento ticket byl automaticky uzavřen, protože jeho autor opustil server.!", + "autocloseTimeout":"Tento ticket byl automaticky uzavřen, protože byl neaktivní déle než `{0}h`!", + "autodeleteLeave":"Tento ticket byl automaticky smazán, protože jeho autor opustil server!", + "autodeleteTimeout":"Tento ticket byl automaticky smazán, protože je neaktivní déle než `{0} dní`!", + "autocloseEnabled":"U tohoto ticketu bylo povoleno automatické uzavření!\nTicket bude uzavřen, pokud bude neaktivní déle než `{0}h`!", + "autocloseDisabled":"U tohoto ticketu bylo vypnuto automatické zavírání!\nUž se nebude automaticky zavírat!", + "autodeleteEnabled":"U tohoto ticketu bylo povoleno automatické mazání!\nBude smazán, pokud je neaktivní déle než `{0} dní`!", + "autodeleteDisabled":"Automatické mazání bylo v tomto zakázáno!\nUž se nebude automaticky mazat!", + + "ticketMessageLimit":"Současně lze vytvořit pouze {0} ticketu!", + "ticketMessageAutoclose":"Tento ticket se automaticky uzavře, pokud je neaktivní po dobu {0}h!", + "ticketMessageAutodelete":"Tento ticket bude automaticky smazán, pokud bude neaktivní po dobu {0} dnů!", + "panelReady":"Panel najdete níže!\nTuto zprávu lze nyní smazat!" + }, + "modal":{ + "closePlaceholder":"Proč jste tento ticket uzavřel?", + "deletePlaceholder":"Proč jste tento ticket smazali?", + "reopenPlaceholder":"Proč jste tento ticket znovu otevřeli?", + "claimPlaceholder":"Proč jste si vyžádal tento ticket?", + "unclaimPlaceholder":"Proč jste zrušili převzetí tohoto ticketu?", + "pinPlaceholder":"Proč jste připnuli tento ticket?", + "unpinPlaceholder":"Proč jste odepnuli tento ticket?" + }, + "logs":{ + "createLog":"Nový ticket byl vytvořen uživatelem {0}!", + "closeLog":"Tento ticket byl uzavřen uživatelem {0}!", + "closeDm":"Tvůj ticket byl uzavřen na našem serveru!", + "deleteLog":"Tento ticket byl smazán uživatelem {0}!", + "deleteDm":"Tvůj ticket byl smazán na našem serveru!", + "reopenLog":"Tento ticket byl znovu otevřen uživatelem {0}!", + "reopenDm":"Tvůj ticket byl znovu otevřen na našem serveru!", + "claimLog":"Tento ticket byl přidělen uživatelem {0}!", + "claimDm":"Tvůj ticket byl přidělen na našem serveru!", + "unclaimLog":"Tento ticket byl odhlášen uživatelem {0}!", + "unclaimDm":"Tvůj ticket byl odhlášen na našem serveru!", + "pinLog":"Tento ticket byl připnut uživatelem {0}!", + "pinDm":"Tvůj ticket byl připnut na našem serveru!", + "unpinLog":"Tento ticket byl odepnut uživatelem {0}!", + "unpinDm":"Tvůj ticket byl odepnut na našem serveru!", + "renameLog":"Tento ticket byl přejmenován na {0} uživatelem {1}!", + "renameDm":"Tvůj ticket byl přejmenován na {0} na našem serveru!", + "moveLog":"Tento ticket byl přesunut do {0} uživatelem {1}!", + "moveDm":"Tvůj ticket byl přesunut do {0} na našem serveru!", + "addLog":"{0} byl přidán do tohoto ticketu uživatelem {1}!", + "addDm":"{0} byl přidán do tvého ticketu na našem serveru!", + "removeLog":"{0} byl odstraněn z tohoto ticketu uživatelem {1}!", + "removeDm":"{0} byl odstraněn z tvého ticketu na našem serveru!", + + "blacklistAddLog":"{0} byl přidán na blacklist uživatelem {1}!", + "blacklistRemoveLog":"{0} byl odstraněn z blacklistu uživatelem {1}!", + "blacklistAddDm":"Byl jsi přidán na blacklist na našem serveru!\nOd teď nemůžeš vytvářet tickety!", + "blacklistRemoveDm":"Byl jsi odstraněn z blacklistu na našem serveru!\nNyní můžeš opět vytvářet tickety!", + "clearLog":"{0} ticketů bylo smazáno uživatelem {1}!" + } + }, + "transcripts":{ + "success":{ + "visit":"Zobrazit přepis", + "ready":"Přepis vytvořen", + "textFileDescription":"Toto je textový přepis smazaného ticketu!", + "htmlProgress":"Prosím, počkejte, zatímco se zpracovává HTML přepis...", + + "createdChannel":"Na serveru byl vytvořen nový přepis {0}!", + "createdCreator":"Byl vytvořen nový přepis {0} pro jeden z tvých ticketů!", + "createdParticipant":"Byl vytvořen nový přepis {0} v jednom z ticketů, kterých ses účastnil!", + "createdActiveAdmin":"Byl vytvořen nový přepis {0} v jednom z ticketů, kterých ses účastnil jako admin!", + "createdEveryAdmin":"Byl vytvořen nový přepis {0} v jednom z ticketů, kde jsi byl adminem!", + "createdOther":"Byl vytvořen nový přepis {0}!" + }, + "errors":{ + "retry":"Zkusit Znovu", + "continue":"Vymazat bez přepisu", + "backup":"Vytvořit záložní přepis", + "error":"Při pokusu o vytvoření přepisu se něco pokazilo.\nCo chcete udělat?\n\nTento ticket nebude smazán, dokud nekliknete na jedno z těchto tlačítek." + } + }, + "errors":{ + "titles":{ + "internalError":"Interní chyba", + "optionMissing":"Chybějící možnost příkazu", + "optionInvalid":"Neplatná možnost příkazu", + "unknownCommand":"Neznámý příkaz", + "noPermissions":"Nemáš oprávnění", + "unknownTicket":"Neznámý ticket", + "deprecatedTicket":"Zastaralý ticket", + "unknownOption":"Neznámá možnost", + "unknownPanel":"Neznámý panel", + "notInGuild":"Nejsi na serveru", + "channelRename":"Nepodařilo se přejmenovat kanál", + "busy":"Ticket je zaneprázdněn" + }, + "descriptions":{ + "askForInfo":"Pro více informací kontaktuj vlastníka tohoto bota!", + "askForInfoResolve":"Kontaktuj vlastníka tohoto bota, pokud se tento problém nevyřeší po několika pokusech.", + "internalError":"Nepodařilo se odpovědět na tento {0} kvůli interní chybě!", + "optionMissing":"V tomto příkazu chybí požadovaný parametr!", + "optionInvalid":"V tomto příkazu je neplatný parametr!", + "optionInvalidChoose":"Vyber z", + "unknownCommand":"Zkus navštívit nabídku nápovědy pro více informací!", + "noPermissions":"Nemáš oprávnění k použití tohoto {0}!", + "noPermissionsList":"Požadovaná oprávnění: (jedno z nich)", + "noPermissionsCooldown":"Nemáš oprávnění k použití tohoto {0}, protože máš cooldown!", + "noPermissionsBlacklist":"Nemáš oprávnění k použití tohoto {0}, protože jsi byl přidán na blacklist!", + "noPermissionsLimitGlobal":"Nemáš oprávnění vytvořit ticket, protože server dosáhl maximálního limitu ticketů!", + "noPermissionsLimitGlobalUser":"Nemáš oprávnění vytvořit ticket, protože jsi dosáhl maximálního limitu ticketů!", + "noPermissionsLimitOption":"Nemáš oprávnění vytvořit ticket, protože server dosáhl maximálního limitu ticketů pro tuto možnost!", + "noPermissionsLimitOptionUser":"Nemáš oprávnění vytvořit ticket, protože jsi dosáhl maximálního limitu ticketů pro tuto možnost!", + "unknownTicket":"Zkus tento příkaz znovu v platném ticketu!", + "deprecatedTicket":"Aktuální kanál není platný ticket! Může se jednat o ticket ze starší verze Open Ticketu!", + "notInGuild":"Tento {0} nefunguje v DM! Zkus to znovu na serveru!", + "channelRename":"Kvůli ratelimitům Discordu není momentálně možné, aby bot přejmenoval kanál. Kanál bude automaticky přejmenován do 10 minut, pokud nedojde k restartu bota.", + "channelRenameSource":"Zdroj této chyby je: {0}", + "busy":"Nelze použít tento {0}!\nTicket je momentálně zpracováván botem.\n\nZkus to prosím znovu za několik sekund!" + }, + "optionInvalidReasons":{ + "stringRegex":"Hodnota neodpovídá vzoru!", + "stringMinLength":"Hodnota musí mít alespoň {0} znaků!", + "stringMaxLength":"Hodnota může mít maximálně {0} znaků!", + "numberInvalid":"Neplatné číslo!", + "numberMin":"Číslo musí být alespoň {0}!", + "numberMax":"Číslo může být maximálně {0}!", + "numberDecimal":"Číslo nesmí být desetinné!", + "numberNegative":"Číslo nesmí být záporné!", + "numberPositive":"Číslo nesmí být kladné!", + "numberZero":"Číslo nesmí být nula!", + "channelNotFound":"Nelze najít kanál!", + "userNotFound":"Nelze najít uživatele!", + "roleNotFound":"Nelze najít roli!", + "memberNotFound":"Nelze najít uživatele!", + "mentionableNotFound":"Nelze najít uživatele nebo roli!", + "channelType":"Neplatný typ kanálu!", + "notInGuild":"Tato možnost vyžaduje, abys byl na serveru!" + }, + "permissions":{ + "developer":"Musíš být vývojářem tohoto bota.", + "owner":"Musíš být vlastníkem serveru.", + "admin":"Musíš být adminem serveru.", + "moderator":"Musíš být moderátorem.", + "support":"Musíš být členem týmu podpory.", + "member":"Musíš být členem.", + "discord-administrator":"Musíš mít oprávnění `ADMINISTRATOR`." + }, + "actionInvalid":{ + "close":"Ticket je již uzavřený!", + "reopen":"Ticket ještě není uzavřený!", + "claim":"Ticket je již přidělený!", + "unclaim":"Ticket ještě není přidělený!", + "pin":"Ticket je již připnutý!", + "unpin":"Ticket ještě není připnutý!", + "add":"Tento uživatel už má k ticketu přístup!", + "remove":"Tohoto uživatele nelze z ticketu odstranit!" + } + }, + "params":{ + "uppercase":{ + "ticket":"Ticket", + "tickets":"Tickety", + "reason":"Důvod", + "creator":"Tvůrce", + "remaining":"Zbývající čas", + "added":"Přidáno", + "removed":"Odstraněno", + "filter":"Filtr", + "claimedBy":"Přiděleno uživatelem {0}", + "method":"Metoda", + "type":"Typ", + "blacklisted":"Na blacklistu", + "panel":"Panel", + "command":"Příkaz", + "system":"Systém", + "true":"true", + "false":"false", + "syntax":"Syntaxe", + "originalName":"Původní jméno", + "newName":"Nové jméno", + "until":"Do", + "validOptions":"Platné možnosti", + "validPanels":"Platné panely", + "autoclose":"Automatické uzavření", + "autodelete":"Automatické smazání", + "startupDate":"Datum spuštění", + "version":"Verze", + "name":"Jméno", + "role":"Role", + "status":"Stav", + "claimed":"Přiděleno", + "pinned":"Připnuto", + "creationDate":"Datum vytvoření" + }, + "lowercase":{ + "text":"text", + "html":"html", + "command":"příkaz", + "modal":"modal", + "button":"tlačítko", + "dropdown":"roletka", + "method":"metoda" + } + }, + "commands":{ + "reason":"Zadej nepovinný důvod, který bude viditelný v logu.", + "help":"Získej seznam všech dostupných příkazů.", + "panel":"Vytvoř zprávu s roletkou nebo tlačítky (pro vytvoření ticketu).", + "panelId":"Identifikátor panelu, který chceš vytvořit.", + "panelAutoUpdate":"Chceš, aby se tento panel automaticky aktualizoval při úpravách?", + "ticket":"Okamžitě vytvoř ticket.", + "ticketId":"Identifikátor ticketu, který chceš vytvořit.", + "close":"Uzavři ticket.", + "delete":"Smaž ticket.", + "deleteNoTranscript":"Smaž tento ticket bez vytvoření přepisu.", + "reopen":"Znovu otevři ticket.", + "claim":"Přiděl ticket.", + "claimUser":"Přiděl tento ticket někomu jinému místo sebe.", + "unclaim":"Odeber přidělení ticketu.", + "pin":"Připni ticket.", + "unpin":"Odepni ticket.", + "move":"Přesuň ticket.", + "moveId":"Identifikátor možnost, na který chceš přesunout.", + "rename":"Přejmenuj ticket.", + "renameName":"Nový název pro tento ticket.", + "add":"Přidej uživatele k ticketu.", + "addUser":"Uživatel k přidání.", + "remove":"Odstraň uživatele z ticketu.", + "removeUser":"Uživatel k odstranění.", + "blacklist":"Spravuj blacklist ticketů.", + "blacklistView":"Zobraz seznam aktuálního blacklistu.", + "blacklistAdd":"Přidej uživatele na blacklist.", + "blacklistRemove":"Odstraň uživatele z blacklistu.", + "blacklistGet":"Získej detaily o uživateli na blacklistu.", + "blacklistGetUser":"Uživatel, o kterém chceš získat detaily.", + "stats":"Zobraz statistiky bota, člena nebo ticketu.", + "statsReset":"Resetuj všechny statistiky bota (a začni počítat od nuly).", + "statsGlobal":"Zobraz globální statistiky.", + "statsUser":"Zobraz statistiky uživatele na serveru.", + "statsUserUser":"Uživatel, jehož statistiky chceš zobrazit.", + "statsTicket":"Zobraz statistiky ticketu na serveru.", + "statsTicketTicket":"Ticket, jehož statistiky chceš zobrazit.", + "clear":"Smaž více ticketů najednou. Lze zadat volitelný filtr.", + "clearFilter":"Filtr pro vymazání tiketů.", + "clearFilters":{ + "all":"Vše", + "open":"Otevřené", + "close":"Uzavřené", + "claim":"Přidělené", + "unclaim":"Nepřidělené", + "pin":"Připnuté", + "unpin":"Nepřipnuté", + "autoclose":"Automaticky uzavřené" + }, + "autoclose":"Spravuj automatické uzavření ticketu.", + "autocloseDisable":"Deaktivuj automatické uzavření v tomto ticketu.", + "autocloseEnable":"Aktivuj automatické uzavření v tomto ticketu.", + "autocloseEnableTime":"Počet hodin neaktivity, po kterých se ticket uzavře.", + "autodelete":"Spravuj automatické smazání ticketu.", + "autodeleteDisable":"Deaktivuj automatické smazání v tomto ticketu.", + "autodeleteEnable":"Aktivuj automatické smazání v tomto ticketu.", + "autodeleteEnableTime":"Počet dnů neaktivity, po kterých se ticket smaže." + }, + "helpMenu":{ + "help":"Získej seznam všech dostupných příkazů.", + "ticket":"Okamžitě vytvoř ticket.", + "close":"Uzavři ticket, čímž se zakáže psaní v tomto kanálu.", + "delete":"Smaž ticket, čímž se vytvoří přepis, pokud je to povoleno.", + "reopen":"Znovu otevři ticket, což opět umožní psaní v tomto kanálu.", + "pin":"Připni ticket. Tento ticket se přesune na začátek a bude mít ve jménu emoji '📌'.", + "unpin":"Odepni ticket. Ticket zůstane na svém místě, ale ztratí emoji '📌'.", + "move":"Přesuň ticket. Tím se změní typ tohoto ticketu.", + "rename":"Přejmenuj ticket. Tím se změní název kanálu tohoto ticketu.", + "claim":"Přiděl ticket. Tím dáš svému týmu najevo, že tento ticket řešíš.", + "unclaim":"Odeber přidělení ticketu. Tím dáš svému týmu najevo, že je tento ticket znovu volný.", + "add":"Přidej uživatele k ticketu. Tímto uživateli umožníš číst a psát v tomto ticketu.", + "remove":"Odstraň uživatele z ticketu. Tímto uživateli odebereš možnost číst a psát v tomto ticketu.", + "panel":"Vytvoř zprávu s roletkou nebo tlačítky (pro vytvoření ticketu).", + "blacklistView":"Zobraz seznam aktuálního blacklistu.", + "blacklistAdd":"Přidej uživatele na blacklist.", + "blacklistRemove":"Odstraň uživatele z blacklistu.", + "blacklistGet":"Získej detaily o uživateli na blacklistu.", + "statsGlobal":"Zobraz globální statistiky.", + "statsTicket":"Zobraz statistiky ticketu na serveru.", + "statsUser":"Zobraz statistiky uživatele na serveru.", + "statsReset":"Resetuj všechny statistiky bota (a začni počítat od nuly).", + "autocloseDisable":"Deaktivuj automatické uzavření v tomto ticketu.", + "autocloseEnable":"Aktivuj automatické uzavření v tomto ticketu.", + "autodeleteDisable":"Deaktivuj automatické smazání v tomto ticketu.", + "autodeleteEnable":"Aktivuj automatické smazání v tomto ticketu." + }, + "stats":{ + "scopes":{ + "global":"Globální statistiky", + "system":"Statistiky systému", + "user":"Statistiky uživatele", + "ticket":"Statistiky ticketu", + "participants":"Účastníci" + }, + "properties":{ + "ticketsCreated":"Vytvořené tickety", + "ticketsClosed":"Uzavřené tickety", + "ticketsDeleted":"Smazané tickety", + "ticketsReopened":"Znovu otevřené tickety", + "ticketsAutoclosed":"Automaticky uzavřené tickety", + "ticketsClaimed":"Přidělené tickety", + "ticketsPinned":"Připnuté tickety", + "ticketsMoved":"Přesunuté tickety", + "usersBlacklisted":"Uživatelé na blacklistu", + "transcriptsCreated":"Vytvořené přepisy" + } + } +} \ No newline at end of file diff --git a/languages/dutch.json b/languages/dutch.json new file mode 100644 index 0000000..fa3b14d --- /dev/null +++ b/languages/dutch.json @@ -0,0 +1,477 @@ +{ + "_TRANSLATION":{ + "otversion":"v4.0.0", + "translator":"DJj123dj", + "lastedited":"21/08/2024", + "language":"Dutch" + }, + "checker":{ + "system":{ + "typeError":"[ERROR]", + "headerOpenTicket":"OPEN TICKET", + "typeWarning":"[WAARSCHUWING]", + "typeInfo":"[INFO]", + "headerConfigChecker":"CONFIG CHECKER", + "headerDescription":"check voor errors in je config bestanden!", + "footerError":"de bot start niet tot alle {0}'s opgelost zijn!", + "footerWarning":"het is aangeraden om alle {0}'en op te lossen voor het starten!", + "footerSupport":"SUPPORT: {0} - DOCS: {1}", + "compactInformation":"gebruik {0} voor meer informatie!", + "dataPath":"pad", + "dataDocs":"docs", + "dataMessages":"bericht" + }, + "messages":{ + "stringTooShort":"Deze tekst kan niet korter dan {0} karakters zijn!", + "stringTooLong":"Deze tekst kan niet langer dan {0} karakters zijn!", + "stringLengthInvalid":"Deze tekst moet {0} karakters lang zijn!", + "stringStartsWith":"Deze tekst moet starten met {0}!", + "stringEndsWith":"Deze tekst moet eindigen met {0}!", + "stringContains":"Deze tekst moet {0} bevatten!", + "stringChoices":"Deze tekst kan alleen de volgende waardes bevatten: {0}!", + "stringRegex":"Deze tekst is ongeldig!", + + "numberTooShort":"Dit nummer kan niet korter dan {0} karakters zijn!", + "numberTooLong":"Dit nummer kan niet langer dan {0} karakters zijn!", + "numberLengthInvalid":"Dit nummer moet {0} karakters lang zijn!", + "numberTooSmall":"Dit nummer moet minimum {0} zijn!", + "numberTooLarge":"Dit nummer kan maximum {0} zijn!", + "numberNotEqual":"Dit nummer moet gelijk zijn aan {0}!", + "numberStep":"Dit nummer moet een meervoud zijn van {0}!", + "numberStepOffset":"Dit nummer moet een meervoud zijn van {0} startend bij {1}!", + "numberStartsWith":"Dit nummer moet starten met {0}!", + "numberEndsWith":"Dit nummer moet eindigen met {0}!", + "numberContains":"Dit nummer moet {0} bevatten!", + "numberChoices":"Dit nummer kan alleen een van de volgende waardes bevatten: {0}!", + "numberFloat":"Dit nummer kan geen kommagetal zijn!", + "numberNegative":"Dit nummer kan niet negatief zijn!", + "numberPositive":"Dit nummer kan niet positief zijn!", + "numberZero":"Dit nummer kan niet nul zijn!", + + "booleanTrue":"Deze boolean kan niet true zijn!", + "booleanFalse":"Deze boolean kan niet false zijn!", + + "arrayEmptyDisabled":"Deze array mag niet leeg zijn!", + "arrayEmptyRequired":"Deze array moet leeg zijn!", + "arrayTooShort":"Deze array moet een minimum lengte hebben van {0}!", + "arrayTooLong":"Deze array mag niet langer zijn dan {0}!", + "arrayLengthInvalid":"Deze array moet een lengte hebben van {0}!", + "arrayInvalidTypes":"Deze array kan alleen de volgende waardes bevatten: {0}!", + "arrayDouble":"Deze array staat dubbele waardes niet toe!", + + "discordInvalidId":"Dit is een ongeldig discord {0} id!", + "discordInvalidIdOptions":"Dit is an ongeldig discord {0} id! Je kan ook een van deze gebruiken: {1}!", + "discordInvalidToken":"Dit is een ongeldig discord token (syntactisch)!", + "colorInvalid":"Dit is een ongeldig hex kleur!", + "emojiTooShort":"Deze tekst moet minimum {0} emoji's hebben!", + "emojiTooLong":"Deze tekst mag maximum {0} emoji's hebben!", + "emojiCustom":"Deze emoji kan geen custom discord emoji zijn!", + "emojiInvalid":"Dit is een ongeldige emoji!", + "urlInvalid":"Deze url is ongeldig!", + "urlInvalidHttp":"Deze url kan alleen het https:// protocol gebruiken!", + "urlInvalidProtocol":"Deze url kan alleen de http:// & https:// protocolen gebruiken!", + "urlInvalidHostname":"Deze url heeft een verboden hostnaam (domein)!", + "urlInvalidExtension":"Deze url heeft een ongeldige extensie! Kies tussen: {0}!", + "urlInvalidPath":"Deze url heeft een ongeldig pad!", + "idNotUnique":"Dit id is niet uniek, gebruik een andere in de plaats!", + "idNonExistent":"Het id {0} bestaat niet!", + + "invalidType":"Het type van deze property moet gelijk zijn aan: {0}!", + "propertyMissing":"De property {0} mist in dit object!", + "propertyOptional":"De property {0} is optioneel in dit object!", + "objectDisabled":"Dit object is uitgeschakeld, gebruik het met behulp van {0}!", + "nullInvalid":"Deze property kan niet null zijn!", + "switchInvalidType":"Dit moet een van de volgende types zijn: {0}!", + "objectSwitchInvalid":"Dit object moet een van de volgende types zijn: {0}!", + + "invalidLanguage":"Dit is een ongeldige taal!", + "invalidButton":"Deze knop moet een {0} of {1} hebben!", + "unusedOption":"De optie {0} wordt nergens gebruikt!", + "unusedQuestion":"The vraag {0} wordt nergens gebruikt!", + "dropdownOption":"Een paneel met dropdown mag alleen opties bevaten van het 'ticket' type!" + } + }, + "actions":{ + "buttons":{ + "create":"Bekijk Ticket", + "close":"Sluit Ticket", + "delete":"Verwijder Ticket", + "reopen":"Heropen Ticket", + "claim":"Claim Ticket", + "unclaim":"Ontclaim Ticket", + "pin":"Pin Ticket", + "unpin":"Maak Ticket Los", + "clear":"Verwijder Tickets", + "helpSwitchSlash":"Bekijk Slash Commands", + "helpSwitchText":"Bekijk Text Commands", + "helpPage":"Pagina {0}", + "withReason":"Met Reden", + "withoutTranscript":"Zonder Transcript" + }, + "titles":{ + "created":"Ticket Gecreëerd", + "close":"Ticket Gesloten", + "delete":"Ticket Verwijderd", + "reopen":"Ticket Heropend", + "claim":"Ticket Geclaimd", + "unclaim":"Ticket Ontclaimd", + "pin":"Ticket Gepind", + "unpin":"Ticket Losgemaakt", + "rename":"Ticket Hernoemd", + "move":"Ticket Verplaatst", + "add":"Ticket Gebruiker Toegevoegd", + "remove":"Ticket Gebruiker Verwijderd", + + "help":"Beschikbare Commands", + "statsReset":"Reset Stats", + "blacklistAdd":"Gebruiker Geblacklisted", + "blacklistRemove":"Gebruiker Vrijgelaten", + "blacklistGet":"Geblackliste Gebruiker", + "blacklistView":"Huidige Blacklist", + "blacklistAddDm":"Toegevoegd Aan Blacklist", + "blacklistRemoveDm":"Verwijderd Van Blacklist", + "clear":"Tickets Opgeruimd", + "roles":"Rollen Bijgewerkt", + + "autoclose":"Ticket Automatisch Gesloten", + "autocloseEnabled":"Autoclose Ingeschakeld", + "autocloseDisabled":"Autoclose Uitgeschakeld", + "autodelete":"Ticket Automatisch Verwijderd", + "autodeleteEnabled":"Autodelete Ingeschakeld", + "autodeleteDisabled":"Autodelete Uitgeschakeld" + }, + "descriptions":{ + "create":"Je ticket is aangemaakt. Klik op de knop hier onder om er naar toe te gaan!", + "close":"Het ticket is succesvol gesloten!", + "delete":"Het ticket is succesvol verwijderd!", + "reopen":"Het ticket is succesvol heropend!", + "claim":"Het ticket is succesvol geclaimd!", + "unclaim":"Het ticket is succesvol ontclaimd!", + "pin":"Het ticket is succesvol gepind!", + "unpin":"Het ticket is succesvol losgemaakt!", + "rename":"Het ticket is succesvol hernoemd naar {0}!", + "move":"Het ticket is succesvol verplaatst naar {0}!", + "add":"{0} is succesvol toegevoegd aan het ticket!", + "remove":"{0} is succesvol verwijderd van het ticket!", + + "helpExplanation":"`` => verplichte parameter\n`[naam]` => optionele parameter", + "statsReset":"De bot stats zijn succesvol gereset!", + "statsError":"Kan ticket stats niet bekijken!\n{0} is geen ticket!", + "blacklistAdd":"{0} is succesvol geblacklist!", + "blacklistRemove":"{0} is succesvol vrijgelaten!", + "blacklistGetSuccess":"{0} is op dit moment geblacklist!", + "blacklistGetEmpty":"{0} is op dit moment niet geblacklist!", + "blacklistViewEmpty":"Nog niemand is geblacklist!", + "blacklistViewTip":"Gebruik \"/blacklist add\" om een gebruiker te blacklisten!", + "clearVerify":"Weet je zeker dat je meerdere tickets wilt verwijderen? Deze actie kan niet teruggedraaid worden!", + "clearReady":"{0} tickets zijn succesvol verwijderd!", + "rolesEmpty":"Geen enkele rollen zijn bijgewerkt!", + + "autocloseLeave":"Dit ticket is automatisch gesloten omdat de maker de server verlaten is!", + "autocloseTimeout":"Dit ticket is automatisch gesloten omdat het inactief was voor meer dan `{0} uur`!", + "autodeleteLeave":"Dit ticket is automatisch verwijderd omdat de maker de server verlaten is!", + "autodeleteTimeout":"Dit ticket is automatisch verwijderd omdat het inactief was voor meer dan `{0} dagen`!", + "autocloseEnabled":"Autoclose is ingeschakeld in dit ticket!\nHet wordt gesloten wanneer het inactief is voor meer dan `{0} uur`!", + "autocloseDisabled":"Autoclose is uitgeschakeld in dit ticket!\nHet wordt niet meer automatisch gesloten!", + "autodeleteEnabled":"Autodelete is ingeschakeld in dit ticket!\nHet wordt verwijderd wanneer het inactief is voor meer dan `{0} dagen`!", + "autodeleteDisabled":"Autodelete is uitgeschakeld in dit ticket!\nHet wordt niet meer automatisch verwijderd!", + + "ticketMessageLimit":"Je kan maar {0} ticket(s) op het zelfde moment aanmaken!", + "ticketMessageAutoclose":"Dit ticket wordt automatisch gesloten wanneer het inactief is voor {0} uur!", + "ticketMessageAutodelete":"Dit ticket wordt automatisch verwijderd wanneer het inactief is voor {0} dagen!", + "panelReady":"Je kan het paneel hier onder vinden!\nDit bericht kan nu verwijderd worden!" + }, + "modal":{ + "closePlaceholder":"Waarom heb je dit ticket gesloten?", + "deletePlaceholder":"Waarom heb je dit ticket verwijderd?", + "reopenPlaceholder":"Waarom heb je dit ticket heropend?", + "claimPlaceholder":"Waarom heb je dit ticket geclaimd?", + "unclaimPlaceholder":"Waarom heb je dit ticket ontclaimd?", + "pinPlaceholder":"Waarom heb je dit ticket gepind?", + "unpinPlaceholder":"Waarom heb je dit ticket losgemaakt?" + }, + "logs":{ + "createLog":"Een nieuw ticket is aangemaakt door {0}!", + "closeLog":"Dit ticket is gesloten door {0}!", + "closeDm":"Jouw ticket is gesloten in onze server!", + "deleteLog":"Dit ticket is verwijderd door {0}!", + "deleteDm":"Jouw ticket is verwijderd in onze server!", + "reopenLog":"Dit ticket is heropend door {0}!", + "reopenDm":"Jouw ticket is heropend in onze server!", + "claimLog":"Dit ticket is geclaimd door {0}!", + "claimDm":"Jouw ticket is geclaimd in onze server!", + "unclaimLog":"Dit ticket is ontclaimd door {0}!", + "unclaimDm":"Jouw ticket is ontclaimd in onze server!", + "pinLog":"Dit ticket is gepind door {0}!", + "pinDm":"Jouw ticket is gepind in onze server!", + "unpinLog":"Dit ticket is losgemaakt door {0}!", + "unpinDm":"Jouw ticket is losgemaakt in onze server!", + "renameLog":"Dit ticket is hernoemd naar {0} door {1}!", + "renameDm":"Jouw ticket is hernoemd naar {0} in onze server!", + "moveLog":"Dit ticket is verplaatst naar {0} door {1}!", + "moveDm":"Jouw ticket is verplaatst naar {0} in onze server!", + "addLog":"{0} is toegevoegd aan dit ticket door {1}!", + "addDm":"{0} is toegevoegd aan jouw ticket in onze server!", + "removeLog":"{0} is verwijderd van dit ticket door {1}!", + "removeDm":"{0} is verwijderd van jouw ticket in onze server!", + + "blacklistAddLog":"{0} is geblacklist door {1}!", + "blacklistRemoveLog":"{0} is verwijderd van de blacklist door {1}!", + "blacklistAddDm":"Je bent geblacklist in onze server!\nVanaf nu kan je geen tickets meer aanmaken!", + "blacklistRemoveDm":"Je bent verwijderd van de blacklist in onze server!\nNormaal zou je nu terug tickets kunnen aanmaken!", + "clearLog":"{0} tickets zijn verwijderd door by {1}!" + } + }, + "transcripts":{ + "success":{ + "visit":"Bekijk Transcript", + "ready":"Transcript Aangemaakt", + "textFileDescription":"Dit is een tekst transcript van een verwijderd ticket!", + "htmlProgress":"Gelieve te wachten terwijl dit html transcript verwerkt wordt...", + + "createdChannel":"Een nieuw {0} transcript is aangemaakt in de server!", + "createdCreator":"Een nieuw {0} transcript is aangemaakt voor een van jouw tickets!", + "createdParticipant":"Een nieuw {0} transcript is aangemaakt in een van de tickets waar jij lid van was!", + "createdActiveAdmin":"Een nieuw {0} transcript is aangemaakt in een van de tickets waar jij een actieve admin was!", + "createdEveryAdmin":"Een nieuw {0} transcript is aangemaakt in een van de tickets waar jij admin was!", + "createdOther":"Een nieuw {0} transcript is aangemaakt!" + }, + "errors":{ + "retry":"Herprobeer", + "continue":"Verwijder Zonder Transcript", + "backup":"Maak Backup Transcript", + "error":"Er is iets misgegaan bij het maken van het transcript.\nWat wil je doen?\n\nDit ticket wordt niet meer verwijderd to je een van de onderstaande knoppen gebruikt." + } + }, + "errors":{ + "titles":{ + "internalError":"Interne Error", + "optionMissing":"Missende Command Optie", + "optionInvalid":"Onjuiste Command Optie", + "unknownCommand":"Onbekende Command", + "noPermissions":"Geen Permissies", + "unknownTicket":"Onbekend Ticket", + "deprecatedTicket":"Verouderd Ticket", + "unknownOption":"Onbekende Optie", + "unknownPanel":"Onbekend Paneel", + "notInGuild":"Niet In Server", + "channelRename":"Kan Kanaal Niet Hernoemen", + "busy":"Ticket Is Bezig" + }, + "descriptions":{ + "askForInfo":"Contacteer the eigenaar van deze bot voor meer info!", + "askForInfoResolve":"Contacteer de eigenaar van deze bot als het probleem niet opgelost wordt na enkele keren.", + "internalError":"Gefaald om te reageren op deze {0} door een interne error!", + "optionMissing":"Een verplichte parameter mist in deze command!", + "optionInvalid":"Een parameter in deze command is ongeldig!", + "optionInvalidChoose":"Kies tussen", + "unknownCommand":"Probeer het help menu te bekijken voor meer info!", + "noPermissions":"Je bent niet toegestaan om deze {0} te gebruiken!", + "noPermissionsList":"Benodigde Permissies: (één van deze)", + "noPermissionsCooldown":"Je bent niet toegestaan om deze {0} te gebruiken omdat je een cooldown hebt!", + "noPermissionsBlacklist":"Je bent niet toegestaan om deze {0} te gebruiken omdat je geblacklist bent!", + "noPermissionsLimitGlobal":"Je bent niet toegestaan om een ticket aan te maken omdat de server het maximum tickets limiet heeft bereikt!", + "noPermissionsLimitGlobalUser":"Je bent niet toegestaan om een ticket aan te maken omdat je het maximum tickets limiet hebt bereikt!", + "noPermissionsLimitOption":"Je bent niet toegestaan om een ticket aan te maken omdat de server het maximum tickets limiet van deze optie heeft bereikt!", + "noPermissionsLimitOptionUser":"Je bent niet toegestaan om een ticket aan te maken omdat je het maximum tickets limiet van deze optie hebt bereikt!", + "unknownTicket":"Probeer deze command opnieuw in een geldig ticket!", + "deprecatedTicket":"Het huidige kanaal is geen geldig ticket! Het is misschien een ticket van een oudere Open Ticket versie!", + "notInGuild":"Deze {0} werkt niet in DM! Probeer het opnieuw in een server!", + "channelRename":"Door discord ratelimits is het op dit moment onmogelijk voor de bot om het kanaal te hernoemen. Het kanaal zal automatisch hernoemd worden over 10 min als de bot niet herstart wordt.", + "channelRenameSource":"De bron van deze error is: {0}", + "busy":"Kan deze {0} niet gebruiken!\nHet ticket wordt op dit moment verwerkt door de bot.\n\nProbeer het opnieuw binnen een paar seconden!" + }, + "optionInvalidReasons":{ + "stringRegex":"Waarde is niet gelijk aan het patroon!", + "stringMinLength":"Waarde moet minstens {0} karakters hebben!", + "stringMaxLength":"Waarde moet maximum {0} karakters hebben!", + "numberInvalid":"Ongeldig nummer!", + "numberMin":"Nummer moet minstens {0} zijn!", + "numberMax":"Nummer moet maximum {0} zijn!", + "numberDecimal":"Nummer kan geen kommagetal zijn!", + "numberNegative":"Nummer mag niet negatief zijn!", + "numberPositive":"Nummer mag niet positief zijn!", + "numberZero":"Nummer mag niet nul zijn!", + "channelNotFound":"Kan kanaal niet vinden!", + "userNotFound":"Kan gebruiker niet vinden!", + "roleNotFound":"Kan rol niet vinden!", + "memberNotFound":"Kan gebruiker niet vinden!", + "mentionableNotFound":"Kan gebruiker of rol niet vinden!", + "channelType":"Ongeldig kanaal type!", + "notInGuild":"Voor deze optie moet je in een server zitten!" + }, + "permissions":{ + "developer":"Je moet de ontwikkelaar van de bot zijn.", + "owner":"Je moet de server eigenaar zijn.", + "admin":"Je moet een server admin zijn.", + "moderator":"Je moet een server moderator zijn.", + "support":"Je moet in het support team zitten.", + "member":"Je moet een member van de server zijn.", + "discord-administrator":"Je hebt de `ADMINISTRATOR` permissie nodig." + }, + "actionInvalid":{ + "close":"Ticket is al gesloten!", + "reopen":"Ticket is nog niet gesloten!", + "claim":"Ticket is al geclaimd!", + "unclaim":"Ticket nog niet geclaimd!", + "pin":"Ticket is al gepind!", + "unpin":"Ticket nog niet gepind!", + "add":"Deze gebruiker heeft al toegang tot dit ticket!", + "remove":"Deze gebruiker kan niet verwijderd worden van dit ticket!" + } + }, + "params":{ + "uppercase":{ + "ticket":"Ticket", + "tickets":"Tickets", + "reason":"Reden", + "creator":"Maker", + "remaining":"Resterende Tijd", + "added":"Toegevoegd", + "removed":"Verwijderd", + "filter":"Filter", + "claimedBy":"Geclaimd Door {0}", + "method":"Methode", + "type":"Type", + "blacklisted":"Geblacklist", + "panel":"Paneel", + "command":"Command", + "system":"Systeem", + "true":"Waar", + "false":"Onwaar", + "syntax":"Syntax", + "originalName":"Originele Naam", + "newName":"Nieuwe Naam", + "until":"Tot", + "validOptions":"Geldige Opties", + "validPanels":"Geldige Panelen", + "autoclose":"Autoclose", + "autodelete":"Autodelete", + "startupDate":"Opstart Datum", + "version":"Versie", + "name":"Naam", + "role":"Rol", + "status":"Status", + "claimed":"Geclaimd", + "pinned":"Gepind", + "creationDate":"Aanmaakdatum" + }, + "lowercase":{ + "text":"tekst", + "html":"html", + "command":"command", + "modal":"form", + "button":"knop", + "dropdown":"dropdown", + "method":"methode" + } + }, + "commands":{ + "reason":"Specifieer een optionele reden die zichtbaar is in de logs.", + "help":"Verkrijg een lijst met alle beschikbare commands.", + "panel":"Spawn een bericht met een dropdown of knoppen (voor ticket creatie).", + "panelId":"Het id van het paneel dat je wilt spawnen.", + "panelAutoUpdate":"Wil je dat dit paneel automatisch bijwerkt wanneer het aangepast wordt?", + "ticket":"Maak een instant ticket.", + "ticketId":"Het id van het ticket dat je wilt aanmaken.", + "close":"Sluit een ticket, dit schakelt schrijven in het kanaal uit.", + "delete":"Verwijder een ticket, dit maakt een transcript waneer ingeschakeld.", + "deleteNoTranscript":"Verwijder dit ticket zonder een transcript aan te maken.", + "reopen":"Heropen een ticket, dit schakelt schrijven in het kanaal terug aan.", + "claim":"Claim een ticket.", + "claimUser":"Claim dit ticket voor iemand anders.", + "unclaim":"Ontclaim dit ticket.", + "pin":"Pin dit ticket.", + "unpin":"Maak dit ticket los.", + "move":"Verplaats een ticket.", + "moveId":"Het id van de optie waarnaar je wilt verplaatsen.", + "rename":"Hernoem een ticket.", + "renameName":"De nieuwe naam voor dit ticket.", + "add":"Voeg een gebruiker toe aan dit ticket.", + "addUser":"De gebruiker om toe te voegen.", + "remove":"Verwijder een user van dit ticket.", + "removeUser":"De gebruiker om te verwijderen.", + "blacklist":"Beheer de ticket blacklist.", + "blacklistView":"Bekijk de huidige blacklist.", + "blacklistAdd":"Voeg een gebruiker toe aan de blacklist.", + "blacklistRemove":"Verwijder een gebruiker van de blacklist.", + "blacklistGet":"Verkrijg de details van een geblackliste gebruiker.", + "blacklistGetUser":"De gebruiker om details van te verkrijgen.", + "stats":"Bekijk statistieken van de bot, een gebruiker of ticket.", + "statsReset":"Reset alle stats van de bot (en begin terug bij nul).", + "statsGlobal":"Bekijk de globale stats.", + "statsUser":"Bekijk de stats van een gebruiker in de server.", + "statsUserUser":"De gebruiker om te bekijken.", + "statsTicket":"Bekijk de stats van een ticket in de server.", + "statsTicketTicket":"Het ticket om te bekijken.", + "clear":"Verwijder meerdere tickets tegelijkertijd.", + "clearFilter":"De filter om tickets op te ruimen.", + "clearFilters":{ + "all":"Allemaal", + "open":"Open (niet gesloten)", + "close":"Gesloten", + "claim":"Geclaimd", + "unclaim":"Ontclaimd (niet geclaimd)", + "pin":"Gepind", + "unpin":"Losgemaakt (niet gepind)", + "autoclose":"Automatisch Gesloten" + }, + "autoclose":"Beheer autoclose in een ticket.", + "autocloseDisable":"Schakel autoclose in dit ticket uit.", + "autocloseEnable":"Schakel autoclose in dit ticket in.", + "autocloseEnableTime":"De hoeveelheid uren dat dit ticket inactief moet zijn voor het te sluiten.", + "autodelete":"Beheer autodelete in een ticket.", + "autodeleteDisable":"Schakel autodelete in dit ticket aan.", + "autodeleteEnable":"Schakel autodelete in dit ticket aan.", + "autodeleteEnableTime":"De hoeveelheid dagen dat dit ticket inactief moet zijn voor het te verwijderen." + }, + "helpMenu":{ + "help":"Krijg een lijst van alle beschikbare commands.", + "ticket":"Maak een instant ticket.", + "close":"Sluit een ticket, dit schakelt schrijven in het kanaal uit.", + "delete":"Verwijder een ticket, dit maakt een transcript wanneer ingeschakeld.", + "reopen":"Heropen een ticket, dit schakeld schrijven in het kanaal terug aan.", + "pin":"Pin een ticket. Dit verplaatst het ticket naar boven & voegt een '📌' emoij toe.", + "unpin":"Maak een ticket los. Het ticket zal blijven staan, maar de '📌' emoij verliezen.", + "move":"Verplaats een ticket. Dit verandert het soort van dit ticket.", + "rename":"Hernoem een ticket. Dit zal de kanaalnaam veranderen.", + "claim":"Claim een ticket. Hiermee kan je laten weten dat je bezig bent met dit ticket.", + "unclaim":"Ontclaim een ticket. Hiermee kan je laten weten dat het ticket terug vrij is.", + "add":"Voeg een user toe aan een ticket.", + "remove":"Verwijder een user van een ticket.", + "panel":"Spawn een bericht met dropdown of knoppen.", + "blacklistView":"Bekijk een lijst van de huidige blacklist.", + "blacklistAdd":"Voeg een user toe aan de blacklist.", + "blacklistRemove":"Verwijder een user van de blacklist.", + "blacklistGet":"Verkrijg de details van een geblacklisted user.", + "statsGlobal":"Bekijk de globale stats.", + "statsTicket":"Bekijk de stats van een ticket in de server.", + "statsUser":"Bekijk de stats van een user in de server.", + "statsReset":"Reset alle stats van de bot (en begin terug vanaf 0).", + "autocloseDisable":"Zet autoclose uit in dit ticket.", + "autocloseEnable":"Zet autoclose in aan dit ticket.", + "autodeleteDisable":"Zet autodelete uit in dit ticket.", + "autodeleteEnable":"Zet autodelete aan in dit ticket." + }, + "stats":{ + "scopes":{ + "global":"Globale Stats", + "system":"Systeem Stats", + "user":"User Stats", + "ticket":"Ticket Stats", + "participants":"Deelnemers" + }, + "properties":{ + "ticketsCreated":"Tickets Aangemaakt", + "ticketsClosed":"Tickets Gesloten", + "ticketsDeleted":"Tickets Verwijderd", + "ticketsReopened":"Tickets Heropend", + "ticketsAutoclosed":"Tickets Autoclosed", + "ticketsClaimed":"Tickets Geclaimd", + "ticketsPinned":"Tickets Gepind", + "ticketsMoved":"Tickets Verplaatst", + "usersBlacklisted":"Users Geblacklist", + "transcriptsCreated":"Transcripts Aangemaakt" + } + } +} \ No newline at end of file diff --git a/languages/english.json b/languages/english.json new file mode 100644 index 0000000..d73991b --- /dev/null +++ b/languages/english.json @@ -0,0 +1,477 @@ +{ + "_TRANSLATION":{ + "otversion":"v4.0.0", + "translator":"DJj123dj", + "lastedited":"21/08/2024", + "language":"English" + }, + "checker":{ + "system":{ + "typeError":"[ERROR]", + "headerOpenTicket":"OPEN TICKET", + "typeWarning":"[WARNING]", + "typeInfo":"[INFO]", + "headerConfigChecker":"CONFIG CHECKER", + "headerDescription":"check for errors in you config files!", + "footerError":"the bot won't start until all {0}'s are fixed!", + "footerWarning":"it's recommended to fix all {0}'s before starting!", + "footerSupport":"SUPPORT: {0} - DOCS: {1}", + "compactInformation":"use {0} for more information!", + "dataPath":"path", + "dataDocs":"docs", + "dataMessages":"message" + }, + "messages":{ + "stringTooShort":"This string can't be shorter than {0} characters!", + "stringTooLong":"This string can't be longer than {0} characters!", + "stringLengthInvalid":"This string needs to be {0} characters long!", + "stringStartsWith":"This string needs to start with {0}!", + "stringEndsWith":"This string needs to end with {0}!", + "stringContains":"This string needs to contain {0}!", + "stringChoices":"This string can only be one of the following values: {0}!", + "stringRegex":"This string is invalid!", + + "numberTooShort":"This number can't be shorter than {0} characters!", + "numberTooLong":"This number can't be longer than {0} characters!", + "numberLengthInvalid":"This number needs to be {0} characters long!", + "numberTooSmall":"This number needs to be at least {0}!", + "numberTooLarge":"This number needs to be at most {0}!", + "numberNotEqual":"This number needs to be {0}!", + "numberStep":"This number needs to be a multiple of {0}!", + "numberStepOffset":"This number needs to be a multiple of {0} starting with {1}!", + "numberStartsWith":"This number needs to start with {0}!", + "numberEndsWith":"This number needs to end with {0}!", + "numberContains":"This number needs to contain {0}!", + "numberChoices":"This number can only be one of the following values: {0}!", + "numberFloat":"This number can't be a decimal!", + "numberNegative":"This number can't be negative!", + "numberPositive":"This number can't be positive!", + "numberZero":"This number can't be zero!", + + "booleanTrue":"This boolean can't be true!", + "booleanFalse":"This boolean can't be false!", + + "arrayEmptyDisabled":"This array isn't allowed to be empty!", + "arrayEmptyRequired":"This array is required to be empty!", + "arrayTooShort":"This array needs to have a length of at least {0}!", + "arrayTooLong":"This array needs to have a length of at most {0}!", + "arrayLengthInvalid":"This array needs to have a length of {0}!", + "arrayInvalidTypes":"This array can only contain the following types: {0}!", + "arrayDouble":"This array doesn't allow the same value twice!", + + "discordInvalidId":"This is an invalid discord {0} id!", + "discordInvalidIdOptions":"This is an invalid discord {0} id! You can also use one of these: {1}!", + "discordInvalidToken":"This is an invalid discord token (syntactically)!", + "colorInvalid":"This is an invalid hex color!", + "emojiTooShort":"This string needs to have at least {0} emoji's!", + "emojiTooLong":"This string needs to have at most {0} emoji's!", + "emojiCustom":"This emoji can't be a custom discord emoji!", + "emojiInvalid":"This is an invalid emoji!", + "urlInvalid":"This url is invalid!", + "urlInvalidHttp":"This url can only use the https:// protocol!", + "urlInvalidProtocol":"This url can only use the http:// & https:// protocols!", + "urlInvalidHostname":"This url has a disallowed hostname!", + "urlInvalidExtension":"This url has an invalid extension! Choose between: {0}!", + "urlInvalidPath":"This url has an invalid path!", + "idNotUnique":"This id isn't unique, use another id instead!", + "idNonExistent":"The id {0} doesn't exist!", + + "invalidType":"This property needs to be the type: {0}!", + "propertyMissing":"The property {0} is missing from this object!", + "propertyOptional":"The property {0} is optional in this object!", + "objectDisabled":"This object is disabled, enable it using {0}!", + "nullInvalid":"This property can't be null!", + "switchInvalidType":"This needs to be one of the following types: {0}!", + "objectSwitchInvalid":"This object needs to be one of the following types: {0}!", + + "invalidLanguage":"This is an invalid language!", + "invalidButton":"This button needs to have at least an {0} or {1}!", + "unusedOption":"The option {0} isn't used anywhere!", + "unusedQuestion":"The question {0} isn't used anywhere!", + "dropdownOption":"A panel with dropdown enabled can only contain options of the 'ticket' type!" + } + }, + "actions":{ + "buttons":{ + "create":"Visit Ticket", + "close":"Close Ticket", + "delete":"Delete Ticket", + "reopen":"Reopen Ticket", + "claim":"Claim Ticket", + "unclaim":"Unclaim Ticket", + "pin":"Pin Ticket", + "unpin":"Unpin Ticket", + "clear":"Delete Tickets", + "helpSwitchSlash":"View Slash Commands", + "helpSwitchText":"View Text Commands", + "helpPage":"Page {0}", + "withReason":"With Reason", + "withoutTranscript":"Without Transcript" + }, + "titles":{ + "created":"Ticket Created", + "close":"Ticket Closed", + "delete":"Ticket Deleted", + "reopen":"Ticket Reopened", + "claim":"Ticket Claimed", + "unclaim":"Ticket Unclaimed", + "pin":"Ticket Pinned", + "unpin":"Ticket Unpinned", + "rename":"Ticket Renamed", + "move":"Ticket Moved", + "add":"Ticket User Added", + "remove":"Ticket User Removed", + + "help":"Available Commands", + "statsReset":"Reset Stats", + "blacklistAdd":"User Blacklisted", + "blacklistRemove":"User Released", + "blacklistGet":"Blacklisted User", + "blacklistView":"Current Blacklist", + "blacklistAddDm":"Added To Blacklist", + "blacklistRemoveDm":"Removed From Blacklist", + "clear":"Tickets Cleared", + "roles":"Roles Updated", + + "autoclose":"Ticket Autoclosed", + "autocloseEnabled":"Autoclose Enabled", + "autocloseDisabled":"Autoclose Disabled", + "autodelete":"Ticket Autodeleted", + "autodeleteEnabled":"Autodelete Enabled", + "autodeleteDisabled":"Autodelete Disabled" + }, + "descriptions":{ + "create":"Your ticket has been created. Click the button below to access it!", + "close":"The ticket has been closed succesfully!", + "delete":"The ticket has been deleted succesfully!", + "reopen":"The ticket has been reopened succesfully!", + "claim":"The ticket has been claimed succesfully!", + "unclaim":"The ticket has been unclaimed succesfully!", + "pin":"The ticket has been pinned succesfully!", + "unpin":"The ticket has been unpinned succesfully!", + "rename":"The ticket has been renamed to {0} succesfully!", + "move":"The ticket has been moved to {0} succesfully!", + "add":"{0} has been added to the ticket succesfully!", + "remove":"{0} has been removed from the ticket succesfully!", + + "helpExplanation":"`` => required parameter\n`[name]` => optional parameter", + "statsReset":"The bot stats have been reset successfully!", + "statsError":"Unable to view ticket stats!\n{0} is not a ticket!", + "blacklistAdd":"{0} has been blacklisted successfully!", + "blacklistRemove":"{0} has been released successfully!", + "blacklistGetSuccess":"{0} is currently blacklisted!", + "blacklistGetEmpty":"{0} is currently not blacklisted!", + "blacklistViewEmpty":"No-one has been blacklisted yet!", + "blacklistViewTip":"Use \"/blacklist add\" to blacklist a user!", + "clearVerify":"Are you sure you want to delete multiple tickets?\nThis action can't be undone!", + "clearReady":"{0} tickets have been deleted succesfully!", + "rolesEmpty":"No roles have been updated!", + + "autocloseLeave":"This ticket has been autoclosed because the creator left the server!", + "autocloseTimeout":"This ticket has been autoclosed because it has been inactive for more than `{0}h`!", + "autodeleteLeave":"This ticket has been autodeleted because the creator left the server!", + "autodeleteTimeout":"This ticket has been autodeleted because it has been inactive for more than `{0} days`!", + "autocloseEnabled":"Autoclose has been enabled in this ticket!\nIt will be closed when it is inactive for more than `{0}h`!", + "autocloseDisabled":"Autoclose has been disabled in this ticket!\nIt won't be closed automatically anymore!", + "autodeleteEnabled":"Autodelete has been enabled in this ticket!\nIt will be deleted when it is inactive for more than `{0} days`!", + "autodeleteDisabled":"Autodelete has been disabled in this ticket!\nIt won't be deleted automatically anymore!", + + "ticketMessageLimit":"You can only create {0} ticket(s) at the same time!", + "ticketMessageAutoclose":"This ticket will be autoclosed when inactive for {0}h!", + "ticketMessageAutodelete":"This ticket will be autodeleted when inactive for {0} days!", + "panelReady":"You can find the panel below!\nThis message can now be deleted!" + }, + "modal":{ + "closePlaceholder":"Why did you close this ticket?", + "deletePlaceholder":"Why did you delete this ticket?", + "reopenPlaceholder":"Why did you reopen this ticket?", + "claimPlaceholder":"Why did you claim this ticket?", + "unclaimPlaceholder":"Why did you unclaim this ticket?", + "pinPlaceholder":"Why did you pin this ticket?", + "unpinPlaceholder":"Why did you unpin this ticket?" + }, + "logs":{ + "createLog":"A new ticket got created by {0}!", + "closeLog":"This ticket has been closed by {0}!", + "closeDm":"Your ticket has been closed in our server!", + "deleteLog":"This ticket has been deleted by {0}!", + "deleteDm":"Your ticket has been deleted in our server!", + "reopenLog":"This ticket has been reopened by {0}!", + "reopenDm":"Your ticket has been reopened in our server!", + "claimLog":"This ticket has been claimed by {0}!", + "claimDm":"Your ticket has been claimed in our server!", + "unclaimLog":"This ticket has been unclaimed by {0}!", + "unclaimDm":"Your ticket has been unclaimed in our server!", + "pinLog":"This ticket has been pinned by {0}!", + "pinDm":"Your ticket has been pinned in our server!", + "unpinLog":"This ticket has been unpinned by {0}!", + "unpinDm":"Your ticket has been unpinned in our server!", + "renameLog":"This ticket has been renamed to {0} by {1}!", + "renameDm":"Your ticket has been renamed to {0} in our server!", + "moveLog":"This ticket has been moved to {0} by {1}!", + "moveDm":"Your ticket has been moved to {0} in our server!", + "addLog":"{0} has been added to this ticket by {1}!", + "addDm":"{0} has been added to your ticket in our server!", + "removeLog":"{0} has been removed from this ticket by {1}!", + "removeDm":"{0} has been removed from your ticket in our server!", + + "blacklistAddLog":"{0} was blacklisted by {1}!", + "blacklistRemoveLog":"{0} was removed from the blacklist by {1}!", + "blacklistAddDm":"You have been blacklisted in our server!\nFrom now on, you are unable to create a ticket!", + "blacklistRemoveDm":"You have been removed from the blacklist in our server!\nNow you can create tickets again!", + "clearLog":"{0} tickets have been deleted by {1}!" + } + }, + "transcripts":{ + "success":{ + "visit":"Visit Transcript", + "ready":"Transcript Created", + "textFileDescription":"This is the text transcript of a deleted ticket!", + "htmlProgress":"Please wait while this html transcript is getting processed...", + + "createdChannel":"A new {0} transcript has been created in the server!", + "createdCreator":"A new {0} transcript has been created for one of your tickets!", + "createdParticipant":"A new {0} transcript has been created in one of the tickets you participated in!", + "createdActiveAdmin":"A new {0} transcript has been created in one of the tickets you participated as admin!", + "createdEveryAdmin":"A new {0} transcript has been created in one of the tickets you were admin in!", + "createdOther":"A new {0} transcript has been created!" + }, + "errors":{ + "retry":"Retry", + "continue":"Delete Without Transcript", + "backup":"Create Backup Transcript", + "error":"Something went wrong while trying to create the transcript.\nWhat would you like to do?\n\nThis ticket won't be deleted until you click one of these buttons." + } + }, + "errors":{ + "titles":{ + "internalError":"Internal Error", + "optionMissing":"Command Option Missing", + "optionInvalid":"Command Option Invalid", + "unknownCommand":"Unknown Command", + "noPermissions":"No Permissions", + "unknownTicket":"Unknown Ticket", + "deprecatedTicket":"Deprecated Ticket", + "unknownOption":"Unknown Option", + "unknownPanel":"Unknown Panel", + "notInGuild":"Not In Server", + "channelRename":"Unable To Rename Channel", + "busy":"Ticket Is Busy" + }, + "descriptions":{ + "askForInfo":"Contact the owner of this bot for more info!", + "askForInfoResolve":"Contact the bot owner of this bot if this issue doesn't resolve after a few tries.", + "internalError":"Failed to respond to this {0} due to an internal error!", + "optionMissing":"A required parameter is missing in this command!", + "optionInvalid":"A parameter in this command is invalid!", + "optionInvalidChoose":"Choose between", + "unknownCommand":"Try visiting the help menu for more info!", + "noPermissions":"You are not allowed to use this {0}!", + "noPermissionsList":"Required Permissions: (one of them)", + "noPermissionsCooldown":"You are not allowed to use this {0} because you have a cooldown!", + "noPermissionsBlacklist":"You are not allowed to use this {0} because you have been blacklisted!", + "noPermissionsLimitGlobal":"You are not allowed to create a ticket because the server reached the max tickets limit!", + "noPermissionsLimitGlobalUser":"You are not allowed to create a ticket because you reached the max tickets limit!", + "noPermissionsLimitOption":"You are not allowed to create a ticket because the server reached the max tickets limit for this option!", + "noPermissionsLimitOptionUser":"You are not allowed to create a ticket because you reached the max tickets limit for this option!", + "unknownTicket":"Try this command again in a valid ticket!", + "deprecatedTicket":"The current channel is not a valid ticket! It might have been a ticket from an old Open Ticket version!", + "notInGuild":"This {0} doesn't work in DM! Please try it again in a server!", + "channelRename":"Due to discord ratelimits, it's currently impossible for the bot to rename the channel. The channel will automatically be renamed over 10 minutes if the bot isn't rebooted.", + "channelRenameSource":"The source of this error is: {0}", + "busy":"Unable to use this {0}!\nThe ticket is currently being processed by the bot.\n\nPlease try again in a few seconds!" + }, + "optionInvalidReasons":{ + "stringRegex":"Value doesn't match pattern!", + "stringMinLength":"Value needs to be at least {0} characters!", + "stringMaxLength":"Value needs to be at most {0} characters!", + "numberInvalid":"Invalid number!", + "numberMin":"Number needs to be at least {0}!", + "numberMax":"Number needs to be at most {0}!", + "numberDecimal":"Number is not allowed to be a decimal!", + "numberNegative":"Number is not allowed to be negative!", + "numberPositive":"Number is not allowed to be positive!", + "numberZero":"Number is not allowed to be zero!", + "channelNotFound":"Unable to find channel!", + "userNotFound":"Unable to find user!", + "roleNotFound":"Unable to find role!", + "memberNotFound":"Unable to find user!", + "mentionableNotFound":"Unable to find user or role!", + "channelType":"Invalid channel type!", + "notInGuild":"This option requires you to be in a server!" + }, + "permissions":{ + "developer":"You need to be the developer of the bot.", + "owner":"You need to be the server owner.", + "admin":"You need to be a server admin.", + "moderator":"You need to be a moderator.", + "support":"You need to be in the support team.", + "member":"You need to be a member.", + "discord-administrator":"You need to have the `ADMINISTRATOR` permission." + }, + "actionInvalid":{ + "close":"Ticket is already closed!", + "reopen":"Ticket is not closed yet!", + "claim":"Ticket is already claimed!", + "unclaim":"Ticket is not claimed yet!", + "pin":"Ticket is already pinned!", + "unpin":"Ticket is not pinned yet!", + "add":"This user is already able to access the ticket!", + "remove":"Unable to remove this user from the ticket!" + } + }, + "params":{ + "uppercase":{ + "ticket":"Ticket", + "tickets":"Tickets", + "reason":"Reason", + "creator":"Creator", + "remaining":"Time Remaining", + "added":"Added", + "removed":"Removed", + "filter":"Filter", + "claimedBy":"Claimed By {0}", + "method":"Method", + "type":"Type", + "blacklisted":"Blacklisted", + "panel":"Panel", + "command":"Command", + "system":"System", + "true":"True", + "false":"False", + "syntax":"Syntax", + "originalName":"Original Name", + "newName":"New Name", + "until":"Until", + "validOptions":"Valid Options", + "validPanels":"Valid Panels", + "autoclose":"Autoclose", + "autodelete":"Autodelete", + "startupDate":"Startup Date", + "version":"Version", + "name":"Name", + "role":"Role", + "status":"Status", + "claimed":"Claimed", + "pinned":"Pinned", + "creationDate":"Creation Date" + }, + "lowercase":{ + "text":"text", + "html":"html", + "command":"command", + "modal":"modal", + "button":"button", + "dropdown":"dropdown", + "method":"method" + } + }, + "commands":{ + "reason":"Specify an optional reason that will be visible in logs.", + "help":"Get a list of all the available commands.", + "panel":"Spawn a message with a dropdown or buttons (for ticket creation).", + "panelId":"The identifier of the panel that you want to spawn.", + "panelAutoUpdate":"Do you want this panel to automatically update when edited?", + "ticket":"Instantly create a ticket.", + "ticketId":"The identifier of the ticket that you want to create.", + "close":"Close a ticket.", + "delete":"Delete a ticket.", + "deleteNoTranscript":"Delete this ticket without creating a transcript.", + "reopen":"Reopen a ticket.", + "claim":"Claim a ticket.", + "claimUser":"Claim this ticket to someone else instead of yourself.", + "unclaim":"Unclaim a ticket.", + "pin":"Pin a ticket.", + "unpin":"Unpin a ticket.", + "move":"Move a ticket.", + "moveId":"The identifier of the option that you want to move to.", + "rename":"Rename a ticket.", + "renameName":"The new name for this ticket.", + "add":"Add a user to a ticket.", + "addUser":"The user to add.", + "remove":"Remove a user from a ticket.", + "removeUser":"The user to remove.", + "blacklist":"Manage the ticket blacklist.", + "blacklistView":"View a list of the current blacklist.", + "blacklistAdd":"Add a user to the blacklist.", + "blacklistRemove":"Remove a user from the blacklist.", + "blacklistGet":"Get the details from a blacklisted user.", + "blacklistGetUser":"The user to get details from.", + "stats":"View statistics from the bot, a member or a ticket.", + "statsReset":"Reset all the stats of the bot (and start counting from zero).", + "statsGlobal":"View the global stats.", + "statsUser":"View the stats from a user in the server.", + "statsUserUser":"The user to view.", + "statsTicket":"View the stats of a ticket in the server.", + "statsTicketTicket":"The ticket to view.", + "clear":"Delete multiple tickets at the same time.", + "clearFilter":"The filter for clearing tickets.", + "clearFilters":{ + "all":"All", + "open":"Open", + "close":"Closed", + "claim":"Claimed", + "unclaim":"Unclaimed", + "pin":"Pinned", + "unpin":"Unpinned", + "autoclose":"Autoclosed" + }, + "autoclose":"Manage autoclose in a ticket.", + "autocloseDisable":"Disable autoclose in this ticket.", + "autocloseEnable":"Enable autoclose in this ticket.", + "autocloseEnableTime":"The amount of hours this ticket needs to be inactive to close it.", + "autodelete":"Manage autodelete in a ticket.", + "autodeleteDisable":"Disable autodelete in this ticket.", + "autodeleteEnable":"Enable autodelete in this ticket.", + "autodeleteEnableTime":"The amount of days this ticket needs to be inactive to delete it." + }, + "helpMenu":{ + "help":"Get a list of all the available commands.", + "ticket":"Instantly create a ticket.", + "close":"Close a ticket, this disables writing in this channel.", + "delete":"Delete a ticket, this creates a transcript when enabled.", + "reopen":"Reopen a ticket, this enables writing in this channel again.", + "pin":"Pin a ticket. This will move the ticket to the top and will add a '📌' emoij to the name.", + "unpin":"Unpin a ticket. The ticket will stay on it's position but will lose the '📌' emoij.", + "move":"Move a ticket. This will change the type of this ticket.", + "rename":"Rename a ticket. This will change the channel name of this ticket.", + "claim":"Claim a ticket. With this, you can let your team know you are handling this ticket.", + "unclaim":"Unclaim a ticket. With this, you can let your team know that this ticket is free again.", + "add":"Add a user to a ticket. This will allow the user to read & write in this ticket.", + "remove":"Remove a user from a ticket. This will remove the ability to read & write for a user in this ticket.", + "panel":"Spawn a message with a dropdown or buttons (for ticket creation).", + "blacklistView":"View a list of the current blacklist.", + "blacklistAdd":"Add a user to the blacklist.", + "blacklistRemove":"Remove a user from the blacklist.", + "blacklistGet":"Get the details from a blacklisted user.", + "statsGlobal":"View the global stats.", + "statsTicket":"View the stats of a ticket in the server.", + "statsUser":"View the stats from a user in the server.", + "statsReset":"Reset all the stats of the bot (and start counting from zero).", + "autocloseDisable":"Disable autoclose in this ticket.", + "autocloseEnable":"Enable autoclose in this ticket.", + "autodeleteDisable":"Disable autodelete in this ticket.", + "autodeleteEnable":"Enable autodelete in this ticket." + }, + "stats":{ + "scopes":{ + "global":"Global Stats", + "system":"System Stats", + "user":"User Stats", + "ticket":"Ticket Stats", + "participants":"Participants" + }, + "properties":{ + "ticketsCreated":"Tickets Created", + "ticketsClosed":"Tickets Closed", + "ticketsDeleted":"Tickets Deleted", + "ticketsReopened":"Tickets Reopened", + "ticketsAutoclosed":"Tickets Autoclosed", + "ticketsClaimed":"Tickets Claimed", + "ticketsPinned":"Tickets Pinned", + "ticketsMoved":"Tickets Moved", + "usersBlacklisted":"Users Blacklisted", + "transcriptsCreated":"Transcripts Created" + } + } +} \ No newline at end of file diff --git a/languages/portuguese.json b/languages/portuguese.json new file mode 100644 index 0000000..181ea58 --- /dev/null +++ b/languages/portuguese.json @@ -0,0 +1,477 @@ +{ + "_TRANSLATION":{ + "otversion":"v4.0.0", + "translator":"quiradon", + "lastedited":"20/08/2024", + "language":"Portuguese" + }, + "checker":{ + "system":{ + "typeError": "[ERRO]", + "headerOpenTicket": "ABRIR TICKET", + "typeWarning": "[AVISO]", + "typeInfo": "[INFORMAÇÃO]", + "headerConfigChecker": "Verificador de Configuração", + "headerDescription": "verifique erros nos seus arquivos de configuração!", + "footerError": "o bot não iniciará até que todos os {0} sejam corrigidos!", + "footerWarning": "é recomendado corrigir todos os {0} antes de iniciar!", + "footerSupport": "SUPORTE: {0} - DOCS: {1}", + "compactInformation": "use {0} para mais informações!", + "dataPath": "caminho", + "dataDocs": "documentos", + "dataMessages": "mensagem" + }, + "messages":{ + "stringTooShort": "Esta string não pode ter menos de {0} caracteres!", + "stringTooLong": "Esta string não pode ter mais de {0} caracteres!", + "stringLengthInvalid": "Esta string precisa ter {0} caracteres!", + "stringStartsWith": "Esta string precisa começar com {0}!", + "stringEndsWith": "Esta string precisa terminar com {0}!", + "stringContains": "Esta string precisa conter {0}!", + "stringChoices": "Esta string só pode ser um dos seguintes valores: {0}!", + "stringRegex": "Esta string é inválida!", + + "numberTooShort": "Este número não pode ter menos de {0} caracteres!", + "numberTooLong": "Este número não pode ter mais de {0} caracteres!", + "numberLengthInvalid": "Este número precisa ter {0} caracteres!", + "numberTooSmall": "Este número precisa ser pelo menos {0}!", + "numberTooLarge": "Este número precisa ser no máximo {0}!", + "numberNotEqual": "Este número precisa ser {0}!", + "numberStep": "Este número precisa ser um múltiplo de {0}!", + "numberStepOffset": "Este número precisa ser um múltiplo de {0} começando com {1}!", + "numberStartsWith": "Este número precisa começar com {0}!", + "numberEndsWith": "Este número precisa terminar com {0}!", + "numberContains": "Este número precisa conter {0}!", + "numberChoices": "Este número só pode ser um dos seguintes valores: {0}!", + "numberFloat": "Este número não pode ser decimal!", + "numberNegative": "Este número não pode ser negativo!", + "numberPositive": "Este número não pode ser positivo!", + "numberZero": "Este número não pode ser zero!", + + "booleanTrue": "Este booleano não pode ser verdadeiro!", + "booleanFalse": "Este booleano não pode ser falso!", + + "arrayEmptyDisabled": "Este array não pode estar vazio!", + "arrayEmptyRequired": "Este array deve estar vazio!", + "arrayTooShort": "Este array precisa ter um comprimento de pelo menos {0}!", + "arrayTooLong": "Este array precisa ter um comprimento de no máximo {0}!", + "arrayLengthInvalid": "Este array precisa ter um comprimento de {0}!", + "arrayInvalidTypes": "Este array só pode conter os seguintes tipos: {0}!", + "arrayDouble": "Este array não permite o mesmo valor duas vezes!", + + "discordInvalidId": "Este é um ID do Discord inválido {0}!", + "discordInvalidIdOptions": "Este é um ID do Discord inválido {0}! Você também pode usar um destes: {1}!", + "discordInvalidToken": "Este é um token do Discord inválido (sintaticamente)!", + "colorInvalid": "Esta é uma cor hexadecimal inválida!", + "emojiTooShort": "Esta string precisa ter pelo menos {0} emojis!", + "emojiTooLong": "Esta string precisa ter no máximo {0} emojis!", + "emojiCustom": "Este emoji não pode ser um emoji personalizado do Discord!", + "emojiInvalid": "Este é um emoji inválido!", + "urlInvalid": "Esta URL é inválida!", + "urlInvalidHttp": "Esta URL só pode usar o protocolo https://!", + "urlInvalidProtocol": "Esta URL só pode usar os protocolos http:// e https://!", + "urlInvalidHostname": "Esta URL tem um hostname não permitido!", + "urlInvalidExtension": "Esta URL tem uma extensão inválida! Escolha entre: {0}!", + "urlInvalidPath": "Esta URL tem um caminho inválido!", + "idNotUnique": "Este ID não é único, use outro ID!", + "idNonExistent": "O ID {0} não existe!", + + "invalidType": "Esta propriedade precisa ser do tipo: {0}!", + "propertyMissing": "A propriedade {0} está faltando neste objeto!", + "propertyOptional": "A propriedade {0} é opcional neste objeto!", + "objectDisabled": "Este objeto está desabilitado, habilite-o usando {0}!", + "nullInvalid": "Esta propriedade não pode ser nula!", + "switchInvalidType": "Isso precisa ser um dos seguintes tipos: {0}!", + "objectSwitchInvalid": "Este objeto precisa ser um dos seguintes tipos: {0}!", + + "invalidLanguage": "Este é um idioma inválido!", + "invalidButton": "Este botão precisa ter pelo menos um {0} ou {1}!", + "unusedOption": "A opção {0} não é usada em nenhum lugar!", + "unusedQuestion": "A pergunta {0} não é usada em nenhum lugar!", + "dropdownOption": "Um painel com dropdown habilitado só pode conter opções do tipo 'ticket'!" + } + }, + "actions":{ + "buttons":{ + "create": "Visitar Ticket", + "close": "Fechar Ticket", + "delete": "Excluir Ticket", + "reopen": "Reabrir Ticket", + "claim": "Reivindicar Ticket", + "unclaim": "Desreivindicar Ticket", + "pin": "Fixar Ticket", + "unpin": "Desfixar Ticket", + "clear": "Excluir Tickets", + "helpSwitchSlash": "Ver Comandos Slash", + "helpSwitchText": "Ver Comandos de Texto", + "helpPage": "Página {0}", + "withReason": "Com Motivo", + "withoutTranscript": "Sem Transcrição" + }, + "titles":{ + "created":"Ticket Criado", + "close":"Ticket Fechado", + "delete":"Ticket Deletado", + "reopen":"Ticket Re-abeto", + "claim":"Ticket Resgatado", + "unclaim":"Ticket Não Resgatado", + "pin":"Ticket Fixado", + "unpin":"Ticket Desfixado", + "rename":"Ticket Renomeado", + "move":"Ticket Movido", + "add":"Ticket Usuário Adicionado", + "remove":"Ticket Usuário Removido", + + "help":"Comandos disponíveis", + "statsReset":"Redefinir Status", + "blacklistAdd":"User Blacklisted", + "blacklistRemove":"User Released", + "blacklistGet":"Usuário na Blacklisted", + "blacklistView":"Atualmente na Blacklist", + "blacklistAddDm":"Adicionado a Blacklist", + "blacklistRemoveDm":"Removido da Blacklist", + "clear":"Tickets Limpos", + "roles":"Cargos Atualizados", + + "autoclose": "Ticket Fechado Automaticamente", + "autocloseEnabled": "Fechamento Automático Habilitado", + "autocloseDisabled": "Fechamento Automático Desabilitado", + "autodelete": "Ticket Excluído Automaticamente", + "autodeleteEnabled": "Exclusão Automática Habilitada", + "autodeleteDisabled": "Exclusão Automática Desabilitada" + }, + "descriptions":{ + "create": "Seu ticket foi criado. Clique no botão abaixo para acessá-lo!", + "close": "O ticket foi fechado com sucesso!", + "delete": "O ticket foi excluído com sucesso!", + "reopen": "O ticket foi reaberto com sucesso!", + "claim": "O ticket foi reivindicado com sucesso!", + "unclaim": "O ticket foi desreivindicado com sucesso!", + "pin": "O ticket foi fixado com sucesso!", + "unpin": "O ticket foi desfixado com sucesso!", + "rename": "O ticket foi renomeado para {0} com sucesso!", + "move": "O ticket foi movido para {0} com sucesso!", + "add": "{0} foi adicionado ao ticket com sucesso!", + "remove": "{0} foi removido do ticket com sucesso!", + + "helpExplanation":"`` => parâmetro obrigatório\n`[nome]` => parâmetro opcional", + "statsReset":"As estatísticas do bot foram redefinidas com sucesso!", + "statsError":"Não foi possível visualizar as estatísticas do ticket!\n{0} não é um ticket!", + "blacklistAdd":"{0} foi adicionado à lista negra com sucesso!", + "blacklistRemove":"{0} foi removido da lista negra com sucesso!", + "blacklistGetSuccess":"{0} está atualmente na lista negra!", + "blacklistGetEmpty":"{0} não está atualmente na lista negra!", + "blacklistViewEmpty":"Ninguém foi adicionado à lista negra ainda!", + "blacklistViewTip":"Use \"/blacklist add\" para adicionar um usuário à lista negra!", + "clearVerify":"Você tem certeza de que deseja excluir vários tickets?\nEsta ação não pode ser desfeita!", + "clearReady":"{0} tickets foram excluídos com sucesso!", + "rolesEmpty":"Nenhum cargo foi atualizado!", + + "autocloseLeave": "Este ticket foi fechado automaticamente porque o criador saiu do servidor!", + "autocloseTimeout": "Este ticket foi fechado automaticamente porque ficou inativo por mais de `{0}h`!", + "autodeleteLeave": "Este ticket foi excluído automaticamente porque o criador saiu do servidor!", + "autodeleteTimeout": "Este ticket foi excluído automaticamente porque ficou inativo por mais de `{0} dias`!", + "autocloseEnabled": "O fechamento automático foi habilitado neste ticket!\nEle será fechado quando ficar inativo por mais de `{0}h`!", + "autocloseDisabled": "O fechamento automático foi desabilitado neste ticket!\nEle não será mais fechado automaticamente!", + "autodeleteEnabled": "A exclusão automática foi habilitada neste ticket!\nEle será excluído quando ficar inativo por mais de `{0} dias`!", + "autodeleteDisabled": "A exclusão automática foi desabilitada neste ticket!\nEle não será mais excluído automaticamente!", + + "ticketMessageLimit": "Você só pode criar {0} ticket(s) ao mesmo tempo!", + "ticketMessageAutoclose": "Este ticket será fechado automaticamente quando inativo por {0}h!", + "ticketMessageAutodelete": "Este ticket será excluído automaticamente quando inativo por {0} dias!", + "panelReady": "Você pode encontrar o painel abaixo!\nEsta mensagem agora pode ser excluída!" + }, + "modal":{ + "closePlaceholder": "Por que você fechou este ticket?", + "deletePlaceholder": "Por que você excluiu este ticket?", + "reopenPlaceholder": "Por que você reabriu este ticket?", + "claimPlaceholder": "Por que você reivindicou este ticket?", + "unclaimPlaceholder": "Por que você desreivindicou este ticket?", + "pinPlaceholder": "Por que você fixou este ticket?", + "unpinPlaceholder": "Por que você desfixou este ticket?" + }, + "logs":{ + "createLog": "Um novo ticket foi criado por {0}!", + "closeLog": "Este ticket foi fechado por {0}!", + "closeDm": "Seu ticket foi fechado em nosso servidor!", + "deleteLog": "Este ticket foi excluído por {0}!", + "deleteDm": "Seu ticket foi excluído em nosso servidor!", + "reopenLog": "Este ticket foi reaberto por {0}!", + "reopenDm": "Seu ticket foi reaberto em nosso servidor!", + "claimLog": "Este ticket foi reivindicado por {0}!", + "claimDm": "Seu ticket foi reivindicado em nosso servidor!", + "unclaimLog": "Este ticket foi desreivindicado por {0}!", + "unclaimDm": "Seu ticket foi desreivindicado em nosso servidor!", + "pinLog": "Este ticket foi fixado por {0}!", + "pinDm": "Seu ticket foi fixado em nosso servidor!", + "unpinLog": "Este ticket foi desfixado por {0}!", + "unpinDm": "Seu ticket foi desfixado em nosso servidor!", + "renameLog": "Este ticket foi renomeado para {0} por {1}!", + "renameDm": "Seu ticket foi renomeado para {0} em nosso servidor!", + "moveLog": "Este ticket foi movido para {0} por {1}!", + "moveDm": "Seu ticket foi movido para {0} em nosso servidor!", + "addLog": "{0} foi adicionado a este ticket por {1}!", + "addDm": "{0} foi adicionado ao seu ticket em nosso servidor!", + "removeLog": "{0} foi removido deste ticket por {1}!", + "removeDm": "{0} foi removido do seu ticket em nosso servidor!", + + "blacklistAddLog": "{0} foi adicionado à lista negra por {1}!", + "blacklistRemoveLog": "{0} foi removido da lista negra por {1}!", + "blacklistAddDm": "Você foi adicionado à lista negra em nosso servidor!\nA partir de agora, você não pode criar um ticket!", + "blacklistRemoveDm": "Você foi removido da lista negra em nosso servidor!\nAgora você pode criar tickets novamente!", + "clearLog": "{0} tickets foram excluídos por {1}!" + } + }, + "transcripts":{ + "success":{ + "visit": "Visitar Transcrição", + "ready": "Transcrição Criada", + "textFileDescription": "Esta é a transcrição em texto de um ticket excluído!", + "htmlProgress": "Por favor, aguarde enquanto esta transcrição em HTML está sendo processada...", + + "createdChannel": "Uma nova transcrição de {0} foi criada no servidor!", + "createdCreator": "Uma nova transcrição de {0} foi criada para um dos seus tickets!", + "createdParticipant": "Uma nova transcrição de {0} foi criada em um dos tickets em que você participou!", + "createdActiveAdmin": "Uma nova transcrição de {0} foi criada em um dos tickets em que você participou como administrador!", + "createdEveryAdmin": "Uma nova transcrição de {0} foi criada em um dos tickets em que você era administrador!", + "createdOther": "Uma nova transcrição de {0} foi criada!" + }, + "errors":{ + "retry": "Tentar Novamente", + "continue": "Excluir Sem Transcrição", + "backup": "Criar Transcrição de Backup", + "error": "Algo deu errado ao tentar criar a transcrição.\nO que você gostaria de fazer?\n\nEste ticket não será excluído até que você clique em um desses botões." + } + }, + "errors":{ + "titles":{ + "internalError": "Erro Interno", + "optionMissing": "Opção de Comando Ausente", + "optionInvalid": "Opção de Comando Inválida", + "unknownCommand": "Comando Desconhecido", + "noPermissions": "Sem Permissões", + "unknownTicket": "Ticket Desconhecido", + "deprecatedTicket": "Ticket Obsoleto", + "unknownOption": "Opção Desconhecida", + "unknownPanel": "Painel Desconhecido", + "notInGuild": "Não Está no Servidor", + "channelRename": "Incapaz de Renomear Canal", + "busy": "Ticket Está Ocupado" + }, + "descriptions":{ + "askForInfo": "Entre em contato com o proprietário deste bot para mais informações!", + "askForInfoResolve": "Entre em contato com o proprietário deste bot se este problema não for resolvido após algumas tentativas.", + "internalError": "Falha ao responder a este {0} devido a um erro interno!", + "optionMissing": "Um parâmetro obrigatório está faltando neste comando!", + "optionInvalid": "Um parâmetro neste comando é inválido!", + "optionInvalidChoose": "Escolha entre", + "unknownCommand": "Tente visitar o menu de ajuda para mais informações!", + "noPermissions": "Você não tem permissão para usar este {0}!", + "noPermissionsList": "Permissões Necessárias: (uma delas)", + "noPermissionsCooldown": "Você não tem permissão para usar este {0} porque você está em período de cooldown!", + "noPermissionsBlacklist": "Você não tem permissão para usar este {0} porque você foi colocado na lista negra!", + "noPermissionsLimitGlobal": "Você não tem permissão para criar um ticket porque o servidor atingiu o limite máximo de tickets!", + "noPermissionsLimitGlobalUser": "Você não tem permissão para criar um ticket porque você atingiu o limite máximo de tickets!", + "noPermissionsLimitOption": "Você não tem permissão para criar um ticket porque o servidor atingiu o limite máximo de tickets para esta opção!", + "noPermissionsLimitOptionUser": "Você não tem permissão para criar um ticket porque você atingiu o limite máximo de tickets para esta opção!", + "unknownTicket": "Tente este comando novamente em um ticket válido!", + "deprecatedTicket": "O canal atual não é um ticket válido! Pode ter sido um ticket de uma versão antiga do Open Ticket!", + "notInGuild": "Este {0} não funciona em DM! Por favor, tente novamente em um servidor!", + "channelRename": "Devido aos limites de taxa do Discord, atualmente é impossível para o bot renomear o canal. O canal será renomeado automaticamente em 10 minutos se o bot não for reiniciado.", + "channelRenameSource": "A fonte deste erro é: {0}", + "busy": "Não é possível usar este {0}!\nO ticket está sendo processado pelo bot no momento.\n\nPor favor, tente novamente em alguns segundos!" + }, + "optionInvalidReasons":{ + "stringRegex": "O valor não corresponde ao padrão!", + "stringMinLength": "O valor precisa ter pelo menos {0} caracteres!", + "stringMaxLength": "O valor precisa ter no máximo {0} caracteres!", + "numberInvalid": "Número inválido!", + "numberMin": "O número precisa ser pelo menos {0}!", + "numberMax": "O número precisa ser no máximo {0}!", + "numberDecimal": "Não é permitido que o número seja decimal!", + "numberNegative": "Não é permitido que o número seja negativo!", + "numberPositive": "Não é permitido que o número seja positivo!", + "numberZero": "Não é permitido que o número seja zero!", + "channelNotFound": "Não foi possível encontrar o canal!", + "userNotFound": "Não foi possível encontrar o usuário!", + "roleNotFound": "Não foi possível encontrar o cargo!", + "memberNotFound": "Não foi possível encontrar o usuário!", + "mentionableNotFound": "Não foi possível encontrar o usuário ou cargo!", + "channelType": "Tipo de canal inválido!", + "notInGuild": "Esta opção requer que você esteja em um servidor!" + }, + "permissions": { + "developer": "Você precisa ser o desenvolvedor do bot.", + "owner": "Você precisa ser o dono do servidor.", + "admin": "Você precisa ser um administrador do servidor.", + "moderator": "Você precisa ser um moderador.", + "support": "Você precisa estar na equipe de suporte.", + "member": "Você precisa ser um membro.", + "discord-administrator": "Você precisa ter a permissão `ADMINISTRATOR`." + }, + "actionInvalid":{ + "close": "O ticket já está fechado!", + "reopen": "O ticket ainda não está fechado!", + "claim": "O ticket já está reivindicado!", + "unclaim": "O ticket ainda não está reivindicado!", + "pin": "O ticket já está fixado!", + "unpin": "O ticket ainda não está fixado!", + "add": "Este usuário já pode acessar o ticket!", + "remove": "Não foi possível remover este usuário do ticket!" + } + }, + "params":{ + "uppercase":{ + "ticket":"Ticket", + "tickets":"Tickets", + "reason":"Motivo", + "creator":"Criador", + "remaining":"Tempo Restante", + "added":"Adicionado", + "removed":"Removido", + "filter":"Filtro", + "claimedBy":"Reivindicado por {0}", + "method":"Método", + "type":"Tipo", + "blacklisted":"Na Lista Negra", + "panel":"Painel", + "command":"Comando", + "system":"Sistema", + "true":"Verdadeiro", + "false":"Falso", + "syntax":"Sintaxe", + "originalName":"Nome Original", + "newName":"Novo Nome", + "until":"Até", + "validOptions":"Opções Válidas", + "validPanels":"Painéis Válidos", + "autoclose":"Auto-fechar", + "autodelete":"Auto-deletar", + "startupDate":"Data de Início", + "version":"Versão", + "name":"Nome", + "role":"Cargo", + "status":"Status", + "claimed":"Reivindicado", + "pinned":"Fixado", + "creationDate":"Data de Criação" + }, + "lowercase":{ + "text":"texto", + "html":"html", + "command":"comando", + "modal":"modal", + "button":"botão", + "dropdown":"dropdown", + "method":"método" + } + }, + "commands":{ + "reason": "Especifique um motivo opcional que será visível nos logs.", + "help": "Obtenha uma lista de todos os comandos disponíveis.", + "panel": "Gerar uma mensagem com um dropdown ou botões (para criação de tickets).", + "panelId": "O identificador do painel que você deseja gerar.", + "panelAutoUpdate": "Você quer que este painel atualize automaticamente quando editado?", + "ticket": "Crie um ticket instantaneamente.", + "ticketId": "O identificador do ticket que você deseja criar.", + "close": "Feche um ticket.", + "delete": "Exclua um ticket.", + "deleteNoTranscript": "Exclua este ticket sem criar uma transcrição.", + "reopen": "Reabra um ticket.", + "claim": "Assuma um ticket.", + "claimUser": "Assuma este ticket para outra pessoa em vez de você mesmo.", + "unclaim": "Desassuma um ticket.", + "pin": "Fixe um ticket.", + "unpin": "Desfixe um ticket.", + "move": "Mova um ticket.", + "moveId": "O identificador do opção para o qual você deseja mover.", + "rename": "Renomeie um ticket.", + "renameName": "O novo nome para este ticket.", + "add": "Adicione um usuário a um ticket.", + "addUser": "O usuário a ser adicionado.", + "remove": "Remover um usuário de um ticket.", + "removeUser": "O usuário a ser removido.", + "blacklist": "Gerenciar a lista negra de tickets.", + "blacklistView": "Ver uma lista da blacklist atual.", + "blacklistAdd": "Adicionar um usuário à blacklist.", + "blacklistRemove": "Remover um usuário da blacklist.", + "blacklistGet": "Obter os detalhes de um usuário na blacklist.", + "blacklistGetUser": "O usuário para obter detalhes.", + "stats": "Ver estatísticas do bot, de um membro ou de um ticket.", + "statsReset": "Redefinir todas as estatísticas do bot (e começar a contar do zero).", + "statsGlobal": "Ver as estatísticas globais.", + "statsUser": "Ver as estatísticas de um usuário no servidor.", + "statsUserUser": "O usuário para visualizar.", + "statsTicket": "Ver as estatísticas de um ticket no servidor.", + "statsTicketTicket": "O ticket para visualizar.", + "clear": "Excluir vários tickets ao mesmo tempo. Um filtro opcional pode ser especificado.", + "clearFilter": "O filtro para limpar tickets.", + "clearFilters": { + "all": "Todos", + "open": "Aberto", + "close": "Fechado", + "claim": "Reivindicado", + "unclaim": "Não Reivindicado", + "pin": "Fixado", + "unpin": "Desfixado", + "autoclose": "Fechado Automaticamente" + }, + "autoclose": "Gerenciar fechamento automático em um ticket.", + "autocloseDisable": "Desabilitar fechamento automático neste ticket.", + "autocloseEnable": "Habilitar fechamento automático neste ticket.", + "autocloseEnableTime": "A quantidade de horas que este ticket precisa estar inativo para ser fechado.", + "autodelete": "Gerenciar exclusão automática em um ticket.", + "autodeleteDisable": "Desabilitar exclusão automática neste ticket.", + "autodeleteEnable": "Habilitar exclusão automática neste ticket.", + "autodeleteEnableTime": "A quantidade de dias que este ticket precisa estar inativo para ser excluído." + }, + "helpMenu": { + "help": "Obtenha uma lista de todos os comandos disponíveis.", + "ticket": "Crie um ticket instantaneamente.", + "close": "Feche um ticket, isso desabilita a escrita neste canal.", + "delete": "Exclua um ticket, isso cria uma transcrição quando habilitado.", + "reopen": "Reabra um ticket, isso habilita a escrita neste canal novamente.", + "pin": "Fixe um ticket. Isso moverá o ticket para o topo e adicionará um emoji '📌' ao nome.", + "unpin": "Desfixe um ticket. O ticket permanecerá na sua posição, mas perderá o emoji '📌'.", + "move": "Mova um ticket. Isso mudará o tipo deste ticket.", + "rename": "Renomeie um ticket. Isso mudará o nome do canal deste ticket.", + "claim": "Assuma um ticket. Com isso, você pode informar à sua equipe que está lidando com este ticket.", + "unclaim": "Desassuma um ticket. Com isso, você pode informar à sua equipe que este ticket está livre novamente.", + "add": "Adicione um usuário a um ticket. Isso permitirá que o usuário leia e escreva neste ticket.", + "remove": "Remova um usuário de um ticket. Isso removerá a capacidade de ler e escrever para um usuário neste ticket.", + "panel": "Gere uma mensagem com um dropdown ou botões (para criação de tickets).", + "blacklistView": "Veja uma lista da blacklist atual.", + "blacklistAdd": "Adicione um usuário à blacklist.", + "blacklistRemove": "Remova um usuário da blacklist.", + "blacklistGet": "Obtenha os detalhes de um usuário na blacklist.", + "statsGlobal": "Veja as estatísticas globais.", + "statsTicket": "Veja as estatísticas de um ticket no servidor.", + "statsUser": "Veja as estatísticas de um usuário no servidor.", + "statsReset": "Redefina todas as estatísticas do bot (e comece a contar do zero).", + "autocloseDisable": "Desabilite o fechamento automático neste ticket.", + "autocloseEnable": "Habilite o fechamento automático neste ticket.", + "autodeleteDisable": "Desabilite a exclusão automática neste ticket.", + "autodeleteEnable": "Habilite a exclusão automática neste ticket." + }, + "stats":{ + "scopes": { + "global": "Status Global", + "system": "Status do Sistema", + "user": "Status do Usuário", + "ticket": "Status do Ticket", + "participants": "Participantes" + }, + "properties":{ + "ticketsCreated":"Tickets Criados", + "ticketsClosed":"Tickets Fechados", + "ticketsDeleted":"Tickets Deletados", + "ticketsReopened":"Tickets Re-Abertos", + "ticketsAutoclosed":"Tickets Auto Fechados", + "ticketsClaimed":"Tickets Claimados", + "ticketsPinned":"Tickets Fixados", + "ticketsMoved":"Tickets Movidos", + "usersBlacklisted":"Usuários na Blacklist", + "transcriptsCreated":"Transcrição Criada" + } + } +} \ No newline at end of file diff --git a/plugins/example-plugin/config.json b/plugins/example-plugin/config.json new file mode 100644 index 0000000..cf41d25 --- /dev/null +++ b/plugins/example-plugin/config.json @@ -0,0 +1,5 @@ +{ + "testVariable1":true, + "testVariable2":123, + "testVariable3":"abc" +} \ No newline at end of file diff --git a/plugins/example-plugin/index.ts b/plugins/example-plugin/index.ts new file mode 100644 index 0000000..94f18a3 --- /dev/null +++ b/plugins/example-plugin/index.ts @@ -0,0 +1,44 @@ +import {api, openticket, utilities} from "../../src/index" +import * as discord from "discord.js" + +///////////////////////////////////////////// +//// This plugin is not enabled yet! //// +//// Enable it in the plugin.json file! //// +///////////////////////////////////////////// + +if (utilities.project != "openticket") throw new api.ODPluginError("This plugin only works in Open Ticket!") +if (!utilities.isBeta) throw new api.ODPluginError("This plugin is made for the beta version of Open Ticket!") + +//Add Typescript autocomplete support for plugin data. (!!!OPTIONAL!!!) +declare module "../../src/core/api/api.ts" { + export interface ODConfigManagerIds_Default { + "example-plugin:config":api.ODJsonConfig + } +} + +//Let's register the example config. This way it's available for all plugins & systems. +openticket.events.get("onConfigLoad").listen((configManager) => { + configManager.add(new api.ODJsonConfig("example-plugin:config","config.json","./plugins/example-plugin/")) + /*===== What did we do? ===== + - "example-plugin:config" Is the ID of this config. You can use this id troughout the bot to access this config file. Even in other plugins. + - "config.json" Is the FILE of this config. It is just the filename. + - "./plugins/example-plugin/" Is the DIRECTORY of this config. By default it's "./config/", but we want to change it to point at the plugin directory. + */ + + //Let's also log it to the console to let us know it worked! + const ourConfig = configManager.get("example-plugin:config") + openticket.log("The example config loaded succesfully!","plugin",[ + {key:"var-1",value:ourConfig.data.testVariable1}, + {key:"var-2",value:ourConfig.data.testVariable2.toString()}, + {key:"var-3",value:ourConfig.data.testVariable3.toString()} + ]) +}) + +openticket.events.get("onTicketCreate").listen((creator) => { + //This is logged before the ticket is created (after the button is pressed) + openticket.log("Ticket is getting created...","plugin") +}) +openticket.events.get("afterTicketCreated").listen((ticket,creator,channel) => { + //This is logged after the ticket is created + openticket.log("Ticket ready!","plugin") +}) \ No newline at end of file diff --git a/plugins/example-plugin/plugin.json b/plugins/example-plugin/plugin.json new file mode 100644 index 0000000..76fd490 --- /dev/null +++ b/plugins/example-plugin/plugin.json @@ -0,0 +1,23 @@ +{ + "name":"Example Plugin", + "id":"example-plugin", + "version":"1.0.0", + "startFile":"index.ts", + + "enabled":false, + "priority":0, + "events":[], + + "npmDependencies":["discord.js"], + "requiredPlugins":[], + "incompatiblePlugins":[], + + "details":{ + "author":"DJj123dj", + "shortDescription":"A simple template for an Open Ticket v4 plugin!", + "longDescription":"A simple example of an Open Ticket v4 plugin!", + "imageUrl":"", + "projectUrl":"", + "tags":["template","example","default"] + } +} \ No newline at end of file diff --git a/src/actions/addTicketUser.ts b/src/actions/addTicketUser.ts new file mode 100644 index 0000000..8081b91 --- /dev/null +++ b/src/actions/addTicketUser.ts @@ -0,0 +1,87 @@ +/////////////////////////////////////// +//TICKET ADD USER SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:add-ticket-user")) + openticket.actions.get("openticket:add-ticket-user").workers.add([ + new api.ODWorker("openticket:add-ticket-user",2,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason,data} = params + if (channel.isThread()) throw new api.ODSystemError("Unable to add user to ticket! Open Ticket doesn't support threads!") + + await openticket.events.get("onTicketUserAdd").emit([ticket,user,data,channel,reason]) + + //update ticket + ticket.get("openticket:participants").value.push({type:"user",id:data.id}) + ticket.get("openticket:participants").refreshDatabase() + ticket.get("openticket:busy").value = true + + //update channel permissions + try{ + await channel.permissionOverwrites.create(data,{ + ViewChannel:true, + SendMessages:true, + AddReactions:true, + AttachFiles:true, + SendPolls:true, + ReadMessageHistory:true + }) + }catch{ + openticket.log("Failed to add channel permission overwrites on add-ticket-user","error") + } + + //update ticket message + const ticketMessage = await openticket.tickets.getTicketMessage(ticket) + if (ticketMessage){ + try{ + ticketMessage.edit((await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})).message) + }catch(e){ + openticket.log("Unable to edit ticket message on ticket user adding!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"messageid",value:ticketMessage.id}, + {key:"option",value:ticket.option.id.value,hidden:true} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + } + + //reply with new message + if (params.sendMessage) await channel.send((await openticket.builders.messages.getSafe("openticket:add-message").build(source,{guild,channel,user,ticket,reason,data})).message) + ticket.get("openticket:busy").value = false + await openticket.events.get("afterTicketUserAdded").emit([ticket,user,data,channel,reason]) + }), + new api.ODWorker("openticket:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason,data} = params + + //to logs + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.adding.logs){ + const logChannel = openticket.posts.get("openticket:logs") + if (logChannel) logChannel.send(await openticket.builders.messages.getSafe("openticket:ticket-action-logs").build(source,{guild,channel,user,ticket,mode:"add",reason,additionalData:data})) + } + + //to dm + if (generalConfig.data.system.messages.adding.dm) await openticket.client.sendUserDm(user,await openticket.builders.messages.getSafe("openticket:ticket-action-dm").build(source,{guild,channel,user,ticket,mode:"add",reason,additionalData:data})) + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {guild,channel,user,ticket,data} = params + + openticket.log(user.displayName+" added "+data.displayName+" to a ticket!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"reason",value:params.reason ?? "/"}, + {key:"method",value:source} + ]) + }) + ]) + openticket.actions.get("openticket:add-ticket-user").workers.backupWorker = new api.ODWorker("openticket:cancel-busy",0,(instance,params) => { + //set busy to false in case of crash or cancel + params.ticket.get("openticket:busy").value = false + }) +} \ No newline at end of file diff --git a/src/actions/claimTicket.ts b/src/actions/claimTicket.ts new file mode 100644 index 0000000..e3b6aa5 --- /dev/null +++ b/src/actions/claimTicket.ts @@ -0,0 +1,276 @@ +/////////////////////////////////////// +//TICKET CLAIMING SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:claim-ticket")) + openticket.actions.get("openticket:claim-ticket").workers.add([ + new api.ODWorker("openticket:claim-ticket",2,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason} = params + if (channel.isThread()) throw new api.ODSystemError("Unable to claim ticket! Open Ticket doesn't support threads!") + + await openticket.events.get("onTicketClaim").emit([ticket,user,channel,reason]) + + //update ticket + ticket.get("openticket:claimed").value = true + ticket.get("openticket:claimed-by").value = user.id + ticket.get("openticket:claimed-on").value = new Date().getTime() + ticket.get("openticket:busy").value = true + + //update stats + openticket.stats.get("openticket:global").setStat("openticket:tickets-claimed",1,"increase") + openticket.stats.get("openticket:user").setStat("openticket:tickets-claimed",user.id,1,"increase") + + //update category + const rawClaimCategory = ticket.option.get("openticket:channel-categories-claimed").value.find((c) => c.user == user.id) + const claimCategory = (rawClaimCategory) ? rawClaimCategory.category : null + if (claimCategory){ + try { + channel.setParent(claimCategory,{lockPermissions:false}) + ticket.get("openticket:category-mode").value = "claimed" + ticket.get("openticket:category").value = claimCategory + }catch(e){ + openticket.log("Unable to move ticket to 'claimed category'!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"categoryid",value:claimCategory} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + } + + //update ticket message + const ticketMessage = await openticket.tickets.getTicketMessage(ticket) + if (ticketMessage){ + try{ + ticketMessage.edit((await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})).message) + }catch(e){ + openticket.log("Unable to edit ticket message on ticket claiming!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"messageid",value:ticketMessage.id}, + {key:"option",value:ticket.option.id.value} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + } + + //reply with new message + if (params.sendMessage) await channel.send((await openticket.builders.messages.getSafe("openticket:claim-message").build(source,{guild,channel,user,ticket,reason})).message) + ticket.get("openticket:busy").value = false + await openticket.events.get("afterTicketClaimed").emit([ticket,user,channel,reason]) + }), + new api.ODWorker("openticket:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason} = params + + //to logs + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.claiming.logs){ + const logChannel = openticket.posts.get("openticket:logs") + if (logChannel) logChannel.send(await openticket.builders.messages.getSafe("openticket:ticket-action-logs").build(source,{guild,channel,user,ticket,mode:"claim",reason,additionalData:null})) + } + + //to dm + if (generalConfig.data.system.messages.claiming.dm) await openticket.client.sendUserDm(user,await openticket.builders.messages.getSafe("openticket:ticket-action-dm").build(source,{guild,channel,user,ticket,mode:"claim",reason,additionalData:null})) + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {guild,channel,user,ticket} = params + + openticket.log(user.displayName+" claimed a ticket!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"reason",value:params.reason ?? "/"}, + {key:"method",value:source} + ]) + }) + ]) + openticket.actions.get("openticket:claim-ticket").workers.backupWorker = new api.ODWorker("openticket:cancel-busy",0,(instance,params) => { + //set busy to false in case of crash or cancel + params.ticket.get("openticket:busy").value = false + }) +} + +export const registerVerifyBars = async () => { + //CLAIM TICKET TICKET MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:claim-ticket-ticket-message",openticket.builders.messages.getSafe("openticket:verifybar-ticket-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:claim-ticket-ticket-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.claim + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:claim-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when already claimed + if (ticket.get("openticket:claimed").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:openticket.languages.getTranslation("errors.actionInvalid.claim"),layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start claiming ticket + if (params.data == "reason"){ + //claim with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:claim-ticket-reason").build("ticket-message",{guild,channel,user,ticket})) + }else{ + //claim without reason + await instance.defer("update",false) + await openticket.actions.get("openticket:claim-ticket").run("ticket-message",{guild,channel,user,ticket,reason:null,sendMessage:true}) + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + } + }) + ]) + openticket.verifybars.get("openticket:claim-ticket-ticket-message").failure.add([ + new api.ODWorker("openticket:back-to-ticket-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + }) + ]) + + //CLAIM TICKET UNCLAIM MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:claim-ticket-unclaim-message",openticket.builders.messages.getSafe("openticket:verifybar-unclaim-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:claim-ticket-unclaim-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.claim + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:claim-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when already claimed + if (ticket.get("openticket:claimed").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:openticket.languages.getTranslation("errors.actionInvalid.claim"),layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start claiming ticket + if (params.data == "reason"){ + //claim with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:claim-ticket-reason").build("unclaim-message",{guild,channel,user,ticket})) + }else{ + //claim without reason + await instance.defer("update",false) + await openticket.actions.get("openticket:claim-ticket").run("unclaim-message",{guild,channel,user,ticket,reason:null,sendMessage:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:claim-message").build("unclaim-message",{guild,channel,user,ticket,reason:null})) + } + }) + ]) + openticket.verifybars.get("openticket:claim-ticket-unclaim-message").failure.add([ + new api.ODWorker("openticket:back-to-unclaim-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + const {verifybarMessage} = params + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + const rawReason = (verifybarMessage && verifybarMessage.embeds[0] && verifybarMessage.embeds[0].fields[0]) ? verifybarMessage.embeds[0].fields[0].value : null + const reason = (rawReason == null) ? null : rawReason.substring(3,rawReason.length-3) + + await instance.update(await openticket.builders.messages.getSafe("openticket:unclaim-message").build("other",{guild,channel,user,ticket,reason})) + }) + ]) +} \ No newline at end of file diff --git a/src/actions/clearTickets.ts b/src/actions/clearTickets.ts new file mode 100644 index 0000000..97ecbf4 --- /dev/null +++ b/src/actions/clearTickets.ts @@ -0,0 +1,45 @@ +/////////////////////////////////////// +//CLEAR TICKETS SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:clear-tickets")) + openticket.actions.get("openticket:clear-tickets").workers.add([ + new api.ODWorker("openticket:clear-tickets",2,async (instance,params,source,cancel) => { + const {guild,channel,user,filter,list} = params + + await openticket.events.get("onTicketsClear").emit([list,user,channel,filter]) + const nameList: string[] = [] + for (const ticket of list){ + const ticketChannel = await openticket.tickets.getTicketChannel(ticket) + if (!ticketChannel) return + nameList.push("#"+ticketChannel.name) + await openticket.actions.get("openticket:delete-ticket").run("clear",{guild,channel:ticketChannel,user,ticket,reason:"Cleared Ticket",sendMessage:true,withoutTranscript:false}) + } + instance.list = nameList + await openticket.events.get("afterTicketsCleared").emit([list,user,channel,filter]) + }), + new api.ODWorker("openticket:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user,filter,list} = params + + //to logs + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.deleting.logs){ + const logChannel = openticket.posts.get("openticket:logs") + if (logChannel) logChannel.send(await openticket.builders.messages.getSafe("openticket:clear-logs").build(source,{guild,channel,user,filter,list:instance.list ?? []})) + } + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {guild,user,filter,list} = params + openticket.log(user.displayName+" cleared "+list.length+" tickets!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"method",value:source}, + {key:"filter",value:filter} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/actions/closeTicket.ts b/src/actions/closeTicket.ts new file mode 100644 index 0000000..ded141e --- /dev/null +++ b/src/actions/closeTicket.ts @@ -0,0 +1,328 @@ +/////////////////////////////////////// +//TICKET CLOSING SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:close-ticket")) + openticket.actions.get("openticket:close-ticket").workers.add([ + new api.ODWorker("openticket:close-ticket",2,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason} = params + if (channel.isThread()) throw new api.ODSystemError("Unable to close ticket! Open Ticket doesn't support threads!") + + await openticket.events.get("onTicketClose").emit([ticket,user,channel,reason]) + + //update ticket + ticket.get("openticket:closed").value = true + if (source == "autoclose") ticket.get("openticket:autoclosed").value = true + ticket.get("openticket:open").value = false + ticket.get("openticket:closed-by").value = user.id + ticket.get("openticket:closed-on").value = new Date().getTime() + ticket.get("openticket:busy").value = true + + //update stats + openticket.stats.get("openticket:global").setStat("openticket:tickets-closed",1,"increase") + openticket.stats.get("openticket:user").setStat("openticket:tickets-closed",user.id,1,"increase") + + //update category + const closeCategory = ticket.option.get("openticket:channel-category-closed").value + if (closeCategory !== ""){ + try { + channel.setParent(closeCategory,{lockPermissions:false}) + ticket.get("openticket:category-mode").value = "closed" + ticket.get("openticket:category").value = closeCategory + }catch(e){ + openticket.log("Unable to move ticket to 'closed category'!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"categoryid",value:closeCategory} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + } + + //update permissions (non-staff => readonly) + const permissions: discord.OverwriteResolvable[] = [{ + type:discord.OverwriteType.Role, + id:guild.roles.everyone.id, + allow:[], + deny:["ViewChannel","SendMessages","ReadMessageHistory"] + }] + const globalAdmins = openticket.configs.get("openticket:general").data.globalAdmins + const optionAdmins = ticket.option.get("openticket:admins").value + const readonlyAdmins = ticket.option.get("openticket:admins-readonly").value + + globalAdmins.forEach((admin) => { + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","ManageMessages"], + deny:[] + }) + }) + optionAdmins.forEach((admin) => { + if (globalAdmins.includes(admin)) return + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","ManageMessages"], + deny:[] + }) + }) + readonlyAdmins.forEach((admin) => { + if (globalAdmins.includes(admin)) return + if (optionAdmins.includes(admin)) return + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","ReadMessageHistory"], + deny:["SendMessages","AddReactions","AttachFiles","SendPolls"] + }) + }) + ticket.get("openticket:participants").value.forEach((participant) => { + //all participants that aren't roles/admins => readonly + if (participant.type == "user"){ + permissions.push({ + type:discord.OverwriteType.Member, + id:user.id, + allow:["ViewChannel","ReadMessageHistory"], + deny:["SendMessages","AddReactions","AttachFiles","SendPolls"] + }) + } + }) + channel.permissionOverwrites.set(permissions) + + //update ticket message + const ticketMessage = await openticket.tickets.getTicketMessage(ticket) + if (ticketMessage){ + try{ + ticketMessage.edit((await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})).message) + }catch(e){ + openticket.log("Unable to edit ticket message on ticket closing!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"messageid",value:ticketMessage.id}, + {key:"option",value:ticket.option.id.value} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + } + + //reply with new message + if (params.sendMessage) await channel.send((await openticket.builders.messages.getSafe("openticket:close-message").build(source,{guild,channel,user,ticket,reason})).message) + ticket.get("openticket:busy").value = false + await openticket.events.get("afterTicketClosed").emit([ticket,user,channel,reason]) + }), + new api.ODWorker("openticket:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason} = params + + //to logs + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.closing.logs){ + const logChannel = openticket.posts.get("openticket:logs") + if (logChannel) logChannel.send(await openticket.builders.messages.getSafe("openticket:ticket-action-logs").build(source,{guild,channel,user,ticket,mode:"close",reason,additionalData:null})) + } + + //to dm + if (generalConfig.data.system.messages.closing.dm) await openticket.client.sendUserDm(user,await openticket.builders.messages.getSafe("openticket:ticket-action-dm").build(source,{guild,channel,user,ticket,mode:"close",reason,additionalData:null})) + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {guild,channel,user,ticket} = params + + openticket.log(user.displayName+" closed a ticket!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"reason",value:params.reason ?? "/"}, + {key:"method",value:source} + ]) + }) + ]) + openticket.actions.get("openticket:close-ticket").workers.backupWorker = new api.ODWorker("openticket:cancel-busy",0,(instance,params) => { + //set busy to false in case of crash or cancel + params.ticket.get("openticket:busy").value = false + }) +} + +export const registerVerifyBars = async () => { + //CLOSE TICKET TICKET MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:close-ticket-ticket-message",openticket.builders.messages.getSafe("openticket:verifybar-ticket-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:close-ticket-ticket-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.close + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:close-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when already closed + if (ticket.get("openticket:closed").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:openticket.languages.getTranslation("errors.actionInvalid.close"),layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start closing ticket + if (params.data == "reason"){ + //close with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:close-ticket-reason").build("ticket-message",{guild,channel,user,ticket})) + }else{ + //close without reason + await instance.defer("update",false) + await openticket.actions.get("openticket:close-ticket").run("ticket-message",{guild,channel,user,ticket,reason:null,sendMessage:true}) + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + } + }) + ]) + openticket.verifybars.get("openticket:close-ticket-ticket-message").failure.add([ + new api.ODWorker("openticket:back-to-ticket-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + }) + ]) + + //CLOSE TICKET REOPEN MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:close-ticket-reopen-message",openticket.builders.messages.getSafe("openticket:verifybar-reopen-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:close-ticket-reopen-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.close + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:close-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when already closed + if (ticket.get("openticket:closed").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:openticket.languages.getTranslation("errors.actionInvalid.close"),layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start closing ticket + if (params.data == "reason"){ + //close with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:close-ticket-reason").build("reopen-message",{guild,channel,user,ticket})) + }else{ + //close without reason + await instance.defer("update",false) + await openticket.actions.get("openticket:close-ticket").run("reopen-message",{guild,channel,user,ticket,reason:null,sendMessage:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:close-message").build("reopen-message",{guild,channel,user,ticket,reason:null})) + } + }) + ]) + openticket.verifybars.get("openticket:close-ticket-reopen-message").failure.add([ + new api.ODWorker("openticket:back-to-reopen-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + const {verifybarMessage} = params + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + const rawReason = (verifybarMessage && verifybarMessage.embeds[0] && verifybarMessage.embeds[0].fields[0]) ? verifybarMessage.embeds[0].fields[0].value : null + const reason = (rawReason == null) ? null : rawReason.substring(3,rawReason.length-3) + + await instance.update(await openticket.builders.messages.getSafe("openticket:reopen-message").build("other",{guild,channel,user,ticket,reason})) + }) + ]) +} \ No newline at end of file diff --git a/src/actions/createTicket.ts b/src/actions/createTicket.ts new file mode 100644 index 0000000..8bd8c36 --- /dev/null +++ b/src/actions/createTicket.ts @@ -0,0 +1,229 @@ +/////////////////////////////////////// +//TICKET CREATION SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:create-ticket")) + openticket.actions.get("openticket:create-ticket").workers.add([ + new api.ODWorker("openticket:create-ticket",3,async (instance,params,source,cancel) => { + const {guild,user,answers,option} = params + + await openticket.events.get("onTicketCreate").emit([user]) + await openticket.events.get("onTicketChannelCreation").emit([option,user]) + + //get channel properties + const channelPrefix = option.get("openticket:channel-prefix").value + const channelCategory = option.get("openticket:channel-category").value + const channelBackupCategory = option.get("openticket:channel-category-backup").value + const channelDescription = option.get("openticket:channel-description").value + const channelSuffix = openticket.options.suffix.getSuffixFromOption(option,user) + const channelName = channelPrefix+channelSuffix + + //handle category + let category: string|null = null + let categoryMode: "backup"|"normal"|null = null + if (channelCategory != ""){ + //category enabled + const normalCategory = await openticket.client.fetchGuildCategoryChannel(guild,channelCategory) + if (!normalCategory){ + //default category was not found + openticket.log("Ticket Creation Error: Unable to find category! #1","error",[ + {key:"categoryid",value:channelCategory}, + {key:"backup",value:"false"} + ]) + }else{ + //default category was found + if (normalCategory.children.cache.size >= 50 && channelBackupCategory != ""){ + //use backup category + const backupCategory = await openticket.client.fetchGuildCategoryChannel(guild,channelBackupCategory) + if (!backupCategory){ + //default category was not found + openticket.log("Ticket Creation Error: Unable to find category! #2","error",[ + {key:"categoryid",value:channelBackupCategory}, + {key:"backup",value:"true"} + ]) + }else{ + category = backupCategory.id + categoryMode = "backup" + } + }else{ + //use default category + category = normalCategory.id + categoryMode = "normal" + } + } + } + + //handle permissions + const permissions: discord.OverwriteResolvable[] = [{ + type:discord.OverwriteType.Role, + id:guild.roles.everyone.id, + allow:[], + deny:["ViewChannel","SendMessages","ReadMessageHistory"] + }] + const globalAdmins = openticket.configs.get("openticket:general").data.globalAdmins + const optionAdmins = option.get("openticket:admins").value + const readonlyAdmins = option.get("openticket:admins-readonly").value + + globalAdmins.forEach((admin) => { + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","ManageMessages"], + deny:[] + }) + }) + optionAdmins.forEach((admin) => { + if (globalAdmins.includes(admin)) return + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","ManageMessages"], + deny:[] + }) + }) + readonlyAdmins.forEach((admin) => { + if (globalAdmins.includes(admin)) return + if (optionAdmins.includes(admin)) return + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","ReadMessageHistory"], + deny:["SendMessages","AddReactions","AttachFiles","SendPolls"] + }) + }) + permissions.push({ + type:discord.OverwriteType.Member, + id:user.id, + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory"], + deny:[] + }) + + //create channel + const channel = await guild.channels.create({ + type:discord.ChannelType.GuildText, + name:channelName, + nsfw:false, + topic:channelDescription, + parent:category, + reason:"Ticket Created By "+user.displayName, + permissionOverwrites:permissions + }) + + await openticket.events.get("afterTicketChannelCreated").emit([option,channel,user]) + + //create participants + const participants: {type:"role"|"user",id:string}[] = [] + permissions.forEach((permission,index) => { + if (index == 0) return //don't include @everyone + const type = (permission.type == discord.OverwriteType.Role) ? "role" : "user" + const id = permission.id as string + participants.push({type,id}) + }) + + //create ticket + const ticket = new api.ODTicket(channel.id,option,[ + new api.ODTicketData("openticket:busy",false), + new api.ODTicketData("openticket:ticket-message",null), + new api.ODTicketData("openticket:participants",participants), + new api.ODTicketData("openticket:channel-suffix",channelSuffix), + + new api.ODTicketData("openticket:open",true), + new api.ODTicketData("openticket:opened-by",user.id), + new api.ODTicketData("openticket:opened-on",new Date().getTime()), + new api.ODTicketData("openticket:closed",false), + new api.ODTicketData("openticket:closed-by",null), + new api.ODTicketData("openticket:closed-on",null), + new api.ODTicketData("openticket:claimed",false), + new api.ODTicketData("openticket:claimed-by",null), + new api.ODTicketData("openticket:claimed-on",null), + new api.ODTicketData("openticket:pinned",false), + new api.ODTicketData("openticket:pinned-by",null), + new api.ODTicketData("openticket:pinned-on",null), + new api.ODTicketData("openticket:for-deletion",false), + + new api.ODTicketData("openticket:category",category), + new api.ODTicketData("openticket:category-mode",categoryMode), + + new api.ODTicketData("openticket:autoclose-enabled",option.get("openticket:autoclose-enable-hours").value), + new api.ODTicketData("openticket:autoclose-hours",(option.get("openticket:autoclose-enable-hours").value ? option.get("openticket:autoclose-hours").value : 0)), + new api.ODTicketData("openticket:autoclosed",false), + new api.ODTicketData("openticket:autodelete-enabled",option.get("openticket:autodelete-enable-days").value), + new api.ODTicketData("openticket:autodelete-days",(option.get("openticket:autodelete-enable-days").value ? option.get("openticket:autodelete-days").value : 0)), + + new api.ODTicketData("openticket:answers",answers) + ]) + + //manage stats + openticket.stats.get("openticket:global").setStat("openticket:tickets-created",1,"increase") + openticket.stats.get("openticket:user").setStat("openticket:tickets-created",user.id,1,"increase") + + //manage bot permissions + await openticket.events.get("onTicketPermissionsCreated").emit([option,openticket.permissions,channel,user]) + await (await import("../data/framework/permissionLoader.ts")).addTicketPermissions(ticket) + await openticket.events.get("afterTicketPermissionsCreated").emit([option,openticket.permissions,channel,user]) + + //export channel & ticket + instance.channel = channel + instance.ticket = ticket + openticket.tickets.add(ticket) + }), + new api.ODWorker("openticket:send-ticket-message",2,async (instance,params,source,cancel) => { + const {guild,user,answers,option} = params + const {ticket,channel} = instance + + if (!ticket || !channel) return openticket.log("Ticket Creation Error: Unable to send ticket message. Previous worker failed!","error") + + await openticket.events.get("onTicketMainMessageCreated").emit([ticket,channel,user]) + //check if ticket message is enabled + if (!option.get("openticket:ticket-message-enabled").value) return + try { + const msg = await channel.send((await openticket.builders.messages.getSafe("openticket:ticket-message").build(source,{guild,channel,user,ticket})).message) + + ticket.get("openticket:ticket-message").value = msg.id + + //manage stats + openticket.stats.get("openticket:ticket").setStat("openticket:messages-sent",ticket.id.value,1,"increase") + + await openticket.events.get("afterTicketMainMessageCreated").emit([ticket,msg,channel,user]) + }catch(err){ + process.emit("uncaughtException",err) + //something went wrong while sending the ticket message + channel.send((await openticket.builders.messages.getSafe("openticket:error").build("other",{guild,channel,user,error:"Ticket Message: Creation Error!\n=> Ticket Is Still Created Succesfully",layout:"advanced"})).message) + } + await openticket.events.get("afterTicketCreated").emit([ticket,user,channel]) + }), + new api.ODWorker("openticket:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,user,answers,option} = params + const {ticket,channel} = instance + + //to logs + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.creation.logs){ + const logChannel = openticket.posts.get("openticket:logs") + if (logChannel) logChannel.send(await openticket.builders.messages.getSafe("openticket:ticket-created-logs").build(source,{guild,channel,user,ticket})) + } + + //to dm + if (generalConfig.data.system.messages.creation.dm) await openticket.client.sendUserDm(user,await openticket.builders.messages.getSafe("openticket:ticket-created-dm").build(source,{guild,channel,user,ticket})) + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {guild,user,answers,option} = params + const {ticket,channel} = instance + + if (!ticket || !channel) return openticket.log("Ticket Creation Error: Unable to create logs. Previous worker failed!","error") + + openticket.log(user.displayName+" created a ticket!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"method",value:source}, + {key:"option",value:option.id.value} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/actions/createTicketPermissions.ts b/src/actions/createTicketPermissions.ts new file mode 100644 index 0000000..342cbb4 --- /dev/null +++ b/src/actions/createTicketPermissions.ts @@ -0,0 +1,106 @@ +/////////////////////////////////////// +//TICKET CREATION SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:create-ticket-permissions")) + openticket.actions.get("openticket:create-ticket-permissions").workers.add([ + new api.ODWorker("openticket:check-blacklist",4,(instance,params,source,cancel) => { + if (!params.option.get("openticket:allow-blacklisted-users").value && openticket.blacklist.exists(params.user.id)){ + instance.valid = false + instance.reason = "blacklist" + openticket.log(params.user.displayName+" tried to create a ticket but is blacklisted!","info",[ + {key:"user",value:params.user.username}, + {key:"userid",value:params.user.id,hidden:true}, + {key:"option",value:params.option.id.value} + ]) + return cancel() + } + }), + new api.ODWorker("openticket:check-cooldown",3,(instance,params,source,cancel) => { + const cooldown = openticket.cooldowns.get("openticket:option-cooldown_"+params.option.id.value) + if (cooldown && cooldown instanceof api.ODTimeoutCooldown && cooldown.use(params.user.id)){ + instance.valid = false + instance.reason = "cooldown" + const remaining = cooldown.remaining(params.user.id) ?? 0 + instance.cooldownUntil = new Date(new Date().getTime() + remaining) + + openticket.log(params.user.displayName+" tried to create a ticket but is on cooldown!","info",[ + {key:"user",value:params.user.username}, + {key:"userid",value:params.user.id,hidden:true}, + {key:"option",value:params.option.id.value}, + {key:"remaining",value:(remaining/1000).toString()+"sec"} + ]) + return cancel() + } + }), + new api.ODWorker("openticket:check-global-limits",2,(instance,params,source,cancel) => { + const generalConfig = openticket.configs.get("openticket:general") + if (!generalConfig.data.system.limits.enabled) return + + const allTickets = openticket.tickets.getAll() + const globalTicketCount = allTickets.length + const userTickets = openticket.tickets.getFiltered((ticket) => ticket.exists("openticket:opened-by") && (ticket.get("openticket:opened-by").value == params.user.id)) + const userTicketCount = userTickets.length + + if (globalTicketCount >= generalConfig.data.system.limits.globalMaximum){ + instance.valid = false + instance.reason = "global-limit" + openticket.log(params.user.displayName+" tried to create a ticket but reached the limit!","info",[ + {key:"user",value:params.user.username}, + {key:"userid",value:params.user.id,hidden:true}, + {key:"option",value:params.option.id.value}, + {key:"limit",value:"global"} + ]) + return cancel() + }else if (userTicketCount >= generalConfig.data.system.limits.userMaximum){ + instance.valid = false + instance.reason = "global-user-limit" + openticket.log(params.user.displayName+" tried to create a ticket, but reached the limit!","info",[ + {key:"user",value:params.user.username}, + {key:"userid",value:params.user.id,hidden:true}, + {key:"option",value:params.option.id.value}, + {key:"limit",value:"global-user"} + ]) + return cancel() + } + }), + new api.ODWorker("openticket:check-option-limits",1,(instance,params,source,cancel) => { + if (!params.option.exists("openticket:limits-enabled") || !params.option.get("openticket:limits-enabled").value) return + + const allTickets = openticket.tickets.getFiltered((ticket) => ticket.option.id.value == params.option.id.value) + const globalTicketCount = allTickets.length + const userTickets = openticket.tickets.getFiltered((ticket) => ticket.option.id.value == params.option.id.value && ticket.exists("openticket:opened-by") && (ticket.get("openticket:opened-by").value == params.user.id)) + const userTicketCount = userTickets.length + + if (globalTicketCount >= params.option.get("openticket:limits-maximum-global").value){ + instance.valid = false + instance.reason = "option-limit" + openticket.log(params.user.displayName+" tried to create a ticket, but reached the limit!","info",[ + {key:"user",value:params.user.username}, + {key:"userid",value:params.user.id,hidden:true}, + {key:"option",value:params.option.id.value}, + {key:"limit",value:"option"} + ]) + return cancel() + }else if (userTicketCount >= params.option.get("openticket:limits-maximum-user").value){ + instance.valid = false + instance.reason = "option-user-limit" + openticket.log(params.user.displayName+" tried to create a ticket, but reached the limit!","info",[ + {key:"user",value:params.user.username}, + {key:"userid",value:params.user.id,hidden:true}, + {key:"option",value:params.option.id.value}, + {key:"limit",value:"option-user"} + ]) + return cancel() + } + }), + new api.ODWorker("openticket:valid",0,(instance,params,source,cancel) => { + instance.valid = true + instance.reason = null + cancel() + }) + ]) +} \ No newline at end of file diff --git a/src/actions/createTranscript.ts b/src/actions/createTranscript.ts new file mode 100644 index 0000000..e95dc29 --- /dev/null +++ b/src/actions/createTranscript.ts @@ -0,0 +1,164 @@ +/////////////////////////////////////// +//TRANSCRIPT CREATION SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const transcriptConfig = openticket.configs.get("openticket:transcripts") + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:create-transcript")) + openticket.actions.get("openticket:create-transcript").workers.add([ + new api.ODWorker("openticket:select-compiler",4,async (instance,params,source,cancel) => { + const {channel,user,ticket} = params + if (channel.type != discord.ChannelType.GuildText) return cancel() + if (!transcriptConfig.data.general.enabled) return cancel() + + await openticket.events.get("onTranscriptCreate").emit([openticket.transcripts,ticket,channel,user]) + + instance.errorReason = null + const participants = await openticket.tickets.getAllTicketParticipants(params.ticket) + if (!participants){ + instance.pendingMessage = null + instance.errorReason = "Unable to fetch ticket channel participants!" + throw new api.ODSystemError("ODAction(ot:create-transcript) => Unable to fetch ticket channel participants!") + } + instance.participants = participants + + //select transcript compiler + if (transcriptConfig.data.general.mode == "text") instance.compiler = openticket.transcripts.get("openticket:text-compiler") + else if (transcriptConfig.data.general.mode == "html") instance.compiler = openticket.transcripts.get("openticket:html-compiler") + }), + new api.ODWorker("openticket:init-transcript",3,async (instance,params,source,cancel) => { + const {channel,user,ticket} = params + if (channel.type != discord.ChannelType.GuildText) return cancel() + if (!transcriptConfig.data.general.enabled) return cancel() + + //run transcript compiler init() + await openticket.events.get("onTranscriptInit").emit([openticket.transcripts,ticket,channel,user]) + if (instance.compiler.init){ + try{ + const result = await instance.compiler.init(ticket,channel,user) + if (result.success && result.pendingMessage && transcriptConfig.data.general.enableChannel){ + //send init message to channel + const post = openticket.posts.get("openticket:transcripts") + if (post){ + instance.pendingMessage = await post.send(result.pendingMessage) + } + }else if (!result.success){ + instance.pendingMessage = null + instance.errorReason = result.errorReason + throw new api.ODSystemError("ODAction(ot:create-transcript) => Known Init Error => "+result.errorReason) + }else instance.pendingMessage = null + }catch(err){ + instance.success = false + cancel() + process.emit("uncaughtException",err) + throw new api.ODSystemError("ODAction(ot:create-transcript) => Failed transcript compiler init()! (see error above)") + } + } + await openticket.events.get("afterTranscriptInitiated").emit([openticket.transcripts,ticket,channel,user]) + }), + new api.ODWorker("openticket:compile-transcript",2,async (instance,params,source,cancel) => { + const {channel,user,ticket} = params + if (channel.type != discord.ChannelType.GuildText) return cancel() + if (!instance.compiler){ + instance.success = false + cancel() + throw new api.ODSystemError("ODAction(ot:create-transcript):ODWorker(ot:compile-transcript) => Instance is missing transcript compiler!") + } + + //run transcript compiler compile() + await openticket.events.get("onTranscriptCompile").emit([openticket.transcripts,ticket,channel,user]) + if (instance.compiler.compile){ + try{ + const result = await instance.compiler.compile(ticket,channel,user) + if (!result.success){ + instance.errorReason = result.errorReason + throw new api.ODSystemError("ODAction(ot:create-transcript) => Known Compiler Error => "+result.errorReason) + }else{ + instance.result = result + instance.success = true + } + }catch(err){ + instance.success = false + cancel() + process.emit("uncaughtException",err) + throw new api.ODSystemError("ODAction(ot:create-transcript) => Failed transcript compiler compile()! (see error above)") + } + } + await openticket.events.get("afterTranscriptCompiled").emit([openticket.transcripts,ticket,channel,user]) + }), + new api.ODWorker("openticket:ready-transcript",1,async (instance,params,source,cancel) => { + if (!instance.result){ + instance.success = false + cancel() + throw new api.ODSystemError("ODAction(ot:create-transcript):ODWorker(ot:ready-transcript) => Instance is missing transcript result!") + } + + //run transcript compiler ready() + utilities.runAsync(async () => { + await openticket.events.get("onTranscriptReady").emit([openticket.transcripts,instance.result.ticket,instance.result.channel,instance.result.user]) + if (instance.compiler.ready){ + try{ + const {channelMessage,creatorDmMessage,participantDmMessage,activeAdminDmMessage,everyAdminDmMessage} = await instance.compiler.ready(instance.result) + + //send channel message + if (transcriptConfig.data.general.enableChannel && channelMessage){ + if (instance.pendingMessage && instance.pendingMessage.message && instance.pendingMessage.success){ + //edit "pending" message to be the "ready" message + instance.pendingMessage.message.edit(channelMessage.message) + }else{ + //send ready message to channel + const post = openticket.posts.get("openticket:transcripts") + if (post) await post.send(channelMessage) + } + } + + //send dm mesages + if (instance.participants){ + for (const p of instance.participants){ + if (p.role == "creator" && transcriptConfig.data.general.enableCreatorDM && creatorDmMessage){ + //send creator dm message + await openticket.client.sendUserDm(p.user,creatorDmMessage) + }else if (p.role == "participant" && transcriptConfig.data.general.enableParticipantDM && participantDmMessage){ + //send participant dm message + await openticket.client.sendUserDm(p.user,participantDmMessage) + }else if (p.role == "admin" && transcriptConfig.data.general.enableActiveAdminDM && instance.result.success && instance.result.messages && instance.result.messages.some((msg) => msg.author.id == p.user.id) && activeAdminDmMessage){ + //send active admin dm message + await openticket.client.sendUserDm(p.user,activeAdminDmMessage) + }else if (p.role == "admin" && transcriptConfig.data.general.enableEveryAdminDM && everyAdminDmMessage){ + //send every admin dm message + await openticket.client.sendUserDm(p.user,everyAdminDmMessage) + } + } + } + + }catch(err){ + instance.success = false + cancel() + process.emit("uncaughtException",err) + throw new api.ODSystemError("ODAction(ot:create-transcript) => Failed transcript compiler ready()! (see error above)") + } + } + await openticket.events.get("afterTranscriptReady").emit([openticket.transcripts,instance.result.ticket,instance.result.channel,instance.result.user]) + }) + + //update stats + openticket.stats.get("openticket:global").setStat("openticket:transcripts-created",1,"increase") + await openticket.events.get("afterTranscriptCreated").emit([openticket.transcripts,instance.result.ticket,instance.result.channel,instance.result.user]) + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {user,channel,ticket} = params + openticket.log(user.displayName+" created a transcript!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"option",value:ticket.option.id.value}, + {key:"method",value:source,hidden:true}, + {key:"compiler",value:instance.compiler.id.value}, + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/actions/deleteTicket.ts b/src/actions/deleteTicket.ts new file mode 100644 index 0000000..6988b5e --- /dev/null +++ b/src/actions/deleteTicket.ts @@ -0,0 +1,463 @@ +/////////////////////////////////////// +//TICKET DELETION SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:delete-ticket")) + openticket.actions.get("openticket:delete-ticket").workers.add([ + new api.ODWorker("openticket:delete-ticket",3,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason} = params + if (channel.isThread()) throw new api.ODSystemError("Unable to delete ticket! Open Ticket doesn't support threads!") + + await openticket.events.get("onTicketDelete").emit([ticket,user,channel,reason]) + + //update ticket + ticket.get("openticket:for-deletion").value = true + ticket.get("openticket:busy").value = true + + //update ticket message + const ticketMessage = await openticket.tickets.getTicketMessage(ticket) + if (ticketMessage){ + try{ + ticketMessage.edit((await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})).message) + }catch(e){ + openticket.log("Unable to edit ticket message on ticket deletion!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"messageid",value:ticketMessage.id}, + {key:"option",value:ticket.option.id.value} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + } + + if (params.sendMessage) await channel.send((await openticket.builders.messages.getSafe("openticket:delete-message").build(source,{guild,channel,user,ticket,reason})).message) + + //create transcript + if (!params.withoutTranscript){ + const transcriptRes = await openticket.actions.get("openticket:create-transcript").run(source,{guild,channel,user,ticket}) + if (typeof transcriptRes.success == "boolean" && !transcriptRes.success && transcriptRes.compiler){ + const {compiler} = transcriptRes + await channel.send((await openticket.builders.messages.getSafe("openticket:transcript-error").build(source,{guild,channel,user,ticket,compiler,reason:transcriptRes.errorReason ?? null})).message) + + //undo deletion + ticket.get("openticket:for-deletion").value = false + ticket.get("openticket:busy").value = false + openticket.log("Canceled ticket deletion because of transcript system malfunction!","warning",[ + {key:"compiler",value:compiler.id.value}, + {key:"reason",value:transcriptRes.errorReason ?? "/"}, + ]) + return cancel() + } + } + + //update stats + openticket.stats.get("openticket:global").setStat("openticket:tickets-deleted",1,"increase") + openticket.stats.get("openticket:user").setStat("openticket:tickets-deleted",user.id,1,"increase") + + //delete ticket from manager + openticket.tickets.remove(ticket.id) + + //delete permissions from manager + await (await import("../data/framework/permissionLoader.ts")).removeTicketPermissions(ticket) + }), + new api.ODWorker("openticket:discord-logs",2,async (instance,params,source,cancel) => { + //logs before channel deletion => channel might still be used in log embeds + const {guild,channel,user,ticket,reason} = params + + //to logs + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.deleting.logs){ + const logChannel = openticket.posts.get("openticket:logs") + if (logChannel) logChannel.send(await openticket.builders.messages.getSafe("openticket:ticket-action-logs").build(source,{guild,channel,user,ticket,mode:"delete",reason,additionalData:null})) + } + + //to dm + if (generalConfig.data.system.messages.deleting.dm) await openticket.client.sendUserDm(user,await openticket.builders.messages.getSafe("openticket:ticket-action-dm").build(source,{guild,channel,user,ticket,mode:"delete",reason,additionalData:null})) + }), + new api.ODWorker("openticket:delete-channel",1,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason} = params + //delete channel & events + await openticket.events.get("onTicketChannelDeletion").emit([ticket,channel,user]) + await channel.delete("Ticket Deleted") + await openticket.events.get("afterTicketChannelDeleted").emit([ticket,user]) + + //delete permissions from manager + await (await import("../data/framework/permissionLoader.ts")).removeTicketPermissions(ticket) + + await openticket.events.get("afterTicketDeleted").emit([ticket,user,reason]) + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {guild,channel,user,ticket} = params + + openticket.log(user.displayName+" deleted a ticket!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"reason",value:params.reason ?? "/"}, + {key:"method",value:source}, + {key:"transcript",value:(!params.withoutTranscript).toString()}, + ]) + }) + ]) + openticket.actions.get("openticket:delete-ticket").workers.backupWorker = new api.ODWorker("openticket:cancel-busy",0,(instance,params) => { + //set busy to false in case of crash or cancel + params.ticket.get("openticket:busy").value = false + params.ticket.get("openticket:for-deletion").value = false + }) +} + +export const registerVerifyBars = async () => { + //DELETE TICKET TICKET MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:delete-ticket-ticket-message",openticket.builders.messages.getSafe("openticket:verifybar-ticket-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:delete-ticket-ticket-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.delete + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:delete-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start deleting ticket + if (params.data == "reason"){ + //delete with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:delete-ticket-reason").build("ticket-message",{guild,channel,user,ticket})) + }else{ + //delete without reason + await instance.defer("update",false) + //don't await DELETE action => else it will update the message after the channel has been deleted + openticket.actions.get("openticket:delete-ticket").run("ticket-message",{guild,channel,user,ticket,reason:null,sendMessage:true,withoutTranscript:(params.data == "no-transcript")}) + //update ticket (for ticket message) => no-await doesn't wait for the action to set this variable + ticket.get("openticket:for-deletion").value = true + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + } + }) + ]) + openticket.verifybars.get("openticket:delete-ticket-ticket-message").failure.add([ + new api.ODWorker("openticket:back-to-ticket-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + }) + ]) + + //DELETE TICKET CLOSE MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:delete-ticket-close-message",openticket.builders.messages.getSafe("openticket:verifybar-close-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:delete-ticket-close-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.delete + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:delete-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start deleting ticket + if (params.data == "reason"){ + //delete with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:delete-ticket-reason").build("close-message",{guild,channel,user,ticket})) + }else{ + //delete without reason + await instance.defer("update",false) + //don't await DELETE action => else it will update the message after the channel has been deleted + openticket.actions.get("openticket:delete-ticket").run("close-message",{guild,channel,user,ticket,reason:null,sendMessage:false,withoutTranscript:(params.data == "no-transcript")}) + //update ticket (for ticket message) => no-await doesn't wait for the action to set this variable + ticket.get("openticket:for-deletion").value = true + await instance.update(await openticket.builders.messages.getSafe("openticket:delete-message").build("close-message",{guild,channel,user,ticket,reason:null})) + } + }) + ]) + openticket.verifybars.get("openticket:delete-ticket-close-message").failure.add([ + new api.ODWorker("openticket:back-to-close-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + const {verifybarMessage} = params + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + const rawReason = (verifybarMessage && verifybarMessage.embeds[0] && verifybarMessage.embeds[0].fields[0]) ? verifybarMessage.embeds[0].fields[0].value : null + const reason = (rawReason == null) ? null : rawReason.substring(3,rawReason.length-3) + + await instance.update(await openticket.builders.messages.getSafe("openticket:close-message").build("other",{guild,channel,user,ticket,reason})) + }) + ]) + + //DELETE TICKET REOPEN MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:delete-ticket-reopen-message",openticket.builders.messages.getSafe("openticket:verifybar-reopen-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:delete-ticket-reopen-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.delete + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:delete-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start deleting ticket + if (params.data == "reason"){ + //delete with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:delete-ticket-reason").build("reopen-message",{guild,channel,user,ticket})) + }else{ + //delete without reason + await instance.defer("update",false) + //don't await DELETE action => else it will update the message after the channel has been deleted + openticket.actions.get("openticket:delete-ticket").run("reopen-message",{guild,channel,user,ticket,reason:null,sendMessage:false,withoutTranscript:(params.data == "no-transcript")}) + //update ticket (for ticket message) => no-await doesn't wait for the action to set this variable + ticket.get("openticket:for-deletion").value = true + await instance.update(await openticket.builders.messages.getSafe("openticket:delete-message").build("reopen-message",{guild,channel,user,ticket,reason:null})) + } + }) + ]) + openticket.verifybars.get("openticket:delete-ticket-reopen-message").failure.add([ + new api.ODWorker("openticket:back-to-reopen-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + const {verifybarMessage} = params + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + const rawReason = (verifybarMessage && verifybarMessage.embeds[0] && verifybarMessage.embeds[0].fields[0]) ? verifybarMessage.embeds[0].fields[0].value : null + const reason = (rawReason == null) ? null : rawReason.substring(3,rawReason.length-3) + + await instance.update(await openticket.builders.messages.getSafe("openticket:reopen-message").build("other",{guild,channel,user,ticket,reason})) + }) + ]) + + //DELETE TICKET AUTOCLOSE MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:delete-ticket-autoclose-message",openticket.builders.messages.getSafe("openticket:verifybar-autoclose-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:delete-ticket-autoclose-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.delete + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:delete-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start deleting ticket + if (params.data == "reason"){ + //delete with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:delete-ticket-reason").build("autoclose-message",{guild,channel,user,ticket})) + }else{ + //delete without reason + await instance.defer("update",false) + //don't await DELETE action => else it will update the message after the channel has been deleted + openticket.actions.get("openticket:delete-ticket").run("autoclose-message",{guild,channel,user,ticket,reason:null,sendMessage:false,withoutTranscript:(params.data == "no-transcript")}) + //update ticket (for ticket message) => no-await doesn't wait for the action to set this variable + ticket.get("openticket:for-deletion").value = true + await instance.update(await openticket.builders.messages.getSafe("openticket:delete-message").build("autoclose-message",{guild,channel,user,ticket,reason:null})) + } + }) + ]) + openticket.verifybars.get("openticket:delete-ticket-autoclose-message").failure.add([ + new api.ODWorker("openticket:back-to-autoclose-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + const {verifybarMessage} = params + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + await instance.update(await openticket.builders.messages.getSafe("openticket:autoclose-message").build("other",{guild,channel,user,ticket})) + }) + ]) +} \ No newline at end of file diff --git a/src/actions/handleTranscriptErrors.ts b/src/actions/handleTranscriptErrors.ts new file mode 100644 index 0000000..e15eae6 --- /dev/null +++ b/src/actions/handleTranscriptErrors.ts @@ -0,0 +1,164 @@ +/////////////////////////////////////// +//TRANSCRIPT ERROR SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerButtonResponders = async () => { + //TRANSCRIPT ERROR RETRY + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:transcript-error-retry",/^od:transcript-error-retry_([^_]+)/)) + openticket.responders.buttons.get("openticket:transcript-error-retry").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.delete + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:delete-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + const originalSource = instance.interaction.customId.split("_")[1] + if (originalSource != "slash" && originalSource != "text" && originalSource != "ticket-message" && originalSource != "reopen-message" && originalSource != "close-message" && originalSource != "other") return + + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start deleting ticket (without reason) + await instance.defer("update",false) + //don't await DELETE action => else it will update the message after the channel has been deleted + openticket.actions.get("openticket:delete-ticket").run(originalSource,{guild,channel,user,ticket,reason:"Transcript Error (Retried)",sendMessage:false,withoutTranscript:false}) + //update ticket (for ticket message) => no-await doesn't wait for the action to set this variable + ticket.get("openticket:for-deletion").value = true + await instance.update(await openticket.builders.messages.getSafe("openticket:delete-message").build("other",{guild,channel,user,ticket,reason:"Transcript Error (Retried)"})) + + }), + new api.ODWorker("openticket:logs",-1,async (instance,params,source,cancel) => { + const {user,channel} = instance + if (channel.isDMBased()) return + openticket.log(user.displayName+" retried deleting a ticket with transcript!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) + + //TRANSCRIPT ERROR CONTINUE + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:transcript-error-continue",/^od:transcript-error-continue_([^_]+)/)) + openticket.responders.buttons.get("openticket:transcript-error-continue").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.delete + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:delete-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + const originalSource = instance.interaction.customId.split("_")[1] + if (originalSource != "slash" && originalSource != "text" && originalSource != "ticket-message" && originalSource != "reopen-message" && originalSource != "close-message" && originalSource != "other") return + + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start deleting ticket (without reason) + await instance.defer("update",false) + //don't await DELETE action => else it will update the message after the channel has been deleted + openticket.actions.get("openticket:delete-ticket").run(originalSource,{guild,channel,user,ticket,reason:"Transcript Error (Continued)",sendMessage:false,withoutTranscript:true}) + //update ticket (for ticket message) => no-await doesn't wait for the action to set this variable + ticket.get("openticket:for-deletion").value = true + await instance.update(await openticket.builders.messages.getSafe("openticket:delete-message").build("other",{guild,channel,user,ticket,reason:"Transcript Error (Continued)"})) + + }), + new api.ODWorker("openticket:logs",-1,async (instance,params,source,cancel) => { + const {user,channel} = instance + if (channel.isDMBased()) return + openticket.log(user.displayName+" continued deleting a ticket without transcript!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/actions/handleVerifyBar.ts b/src/actions/handleVerifyBar.ts new file mode 100644 index 0000000..4154a4b --- /dev/null +++ b/src/actions/handleVerifyBar.ts @@ -0,0 +1,32 @@ +/////////////////////////////////////// +//VERIFYBAR SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" + +export const registerButtonResponders = async () => { + //VERIFYBAR SUCCESS + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:verifybar-success",/^od:verifybar-success_([^_]+)/)) + openticket.responders.buttons.get("openticket:verifybar-success").workers.add( + new api.ODWorker("openticket:handle-verifybar",0,async (instance,params,source,cancel) => { + const id = instance.interaction.customId.split("_")[1] + const customData = instance.interaction.customId.split("_")[2] as string|undefined + + const verifybar = openticket.verifybars.get(id) + if (!verifybar) return + if (verifybar.success) await verifybar.success.executeWorkers(instance,"verifybar",{data:customData ?? null,verifybarMessage:instance.message}) + }) + ) + + //VERIFYBAR FAILURE + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:verifybar-failure",/^od:verifybar-failure_([^_]+)/)) + openticket.responders.buttons.get("openticket:verifybar-failure").workers.add( + new api.ODWorker("openticket:handle-verifybar",0,async (instance,params,source,cancel) => { + const id = instance.interaction.customId.split("_")[1] + const customData = instance.interaction.customId.split("_")[2] as string|undefined + + const verifybar = openticket.verifybars.get(id) + if (!verifybar) return + if (verifybar.failure) await verifybar.failure.executeWorkers(instance,"verifybar",{data:customData ?? null,verifybarMessage:instance.message}) + }) + ) +} \ No newline at end of file diff --git a/src/actions/moveTicket.ts b/src/actions/moveTicket.ts new file mode 100644 index 0000000..7d95b82 --- /dev/null +++ b/src/actions/moveTicket.ts @@ -0,0 +1,220 @@ +/////////////////////////////////////// +//TICKET MOVING SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:move-ticket")) + openticket.actions.get("openticket:move-ticket").workers.add([ + new api.ODWorker("openticket:move-ticket",2,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason,data} = params + if (channel.isThread()) throw new api.ODSystemError("Unable to move ticket! Open Ticket doesn't support threads!") + + await openticket.events.get("onTicketMove").emit([ticket,user,channel,reason]) + ticket.option = data + + //update stats + openticket.stats.get("openticket:global").setStat("openticket:tickets-moved",1,"increase") + openticket.stats.get("openticket:user").setStat("openticket:tickets-moved",user.id,1,"increase") + + //get new channel properties + const channelPrefix = ticket.option.get("openticket:channel-prefix").value + const channelSuffix = ticket.get("openticket:channel-suffix").value + const channelCategory = ticket.option.get("openticket:channel-category").value + const channelBackupCategory = ticket.option.get("openticket:channel-category-backup").value + const rawClaimCategory = ticket.option.get("openticket:channel-categories-claimed").value.find((c) => c.user == user.id) + const claimCategory = (rawClaimCategory) ? rawClaimCategory.category : null + const closeCategory = ticket.option.get("openticket:channel-category-closed").value + const channelDescription = ticket.option.get("openticket:channel-description").value + const channelName = channelPrefix+channelSuffix + + //handle category + let category: string|null = null + let categoryMode: "backup"|"normal"|"closed"|"claimed"|null = null + if (claimCategory){ + //use claim category + category = claimCategory + categoryMode = "claimed" + }else if (closeCategory != "" && ticket.get("openticket:closed").value){ + //use close category + category = closeCategory + categoryMode = "closed" + }else if (channelCategory != ""){ + //category enabled + const normalCategory = await openticket.client.fetchGuildCategoryChannel(guild,channelCategory) + if (!normalCategory){ + //default category was not found + openticket.log("Ticket Move Error: Unable to find category! #1","error",[ + {key:"categoryid",value:channelCategory}, + {key:"backup",value:"false"} + ]) + }else{ + //default category was found + if (normalCategory.children.cache.size >= 50 && channelBackupCategory != ""){ + //use backup category + const backupCategory = await openticket.client.fetchGuildCategoryChannel(guild,channelBackupCategory) + if (!backupCategory){ + //default category was not found + openticket.log("Ticket Move Error: Unable to find category! #2","error",[ + {key:"categoryid",value:channelBackupCategory}, + {key:"backup",value:"true"} + ]) + }else{ + category = backupCategory.id + categoryMode = "backup" + } + }else{ + //use default category + category = normalCategory.id + categoryMode = "normal" + } + } + } + + try { + //only move category when not the same. + if (channel.parentId != category) await utilities.timedAwait(channel.setParent(category,{lockPermissions:false}),2500,(err) => { + openticket.log("Failed to change channel category on ticket move","error") + }) + ticket.get("openticket:category-mode").value = categoryMode + ticket.get("openticket:category").value = category + }catch(e){ + openticket.log("Unable to move ticket to 'moved category'!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"categoryid",value:category ?? "/"} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + + //handle permissions + const permissions: discord.OverwriteResolvable[] = [{ + type:discord.OverwriteType.Role, + id:guild.roles.everyone.id, + allow:[], + deny:["ViewChannel","SendMessages","ReadMessageHistory"] + }] + const globalAdmins = openticket.configs.get("openticket:general").data.globalAdmins + const optionAdmins = ticket.option.get("openticket:admins").value + const readonlyAdmins = ticket.option.get("openticket:admins-readonly").value + + globalAdmins.forEach((admin) => { + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","ManageMessages"], + deny:[] + }) + }) + optionAdmins.forEach((admin) => { + if (globalAdmins.includes(admin)) return + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","ManageMessages"], + deny:[] + }) + }) + readonlyAdmins.forEach((admin) => { + if (globalAdmins.includes(admin)) return + if (optionAdmins.includes(admin)) return + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","ReadMessageHistory"], + deny:["SendMessages","AddReactions","AttachFiles","SendPolls"] + }) + }) + //transfer all old user-participants over to the new ticket (creator & participants) + ticket.get("openticket:participants").value.forEach((p) => { + if (p.type == "user") permissions.push({ + type:discord.OverwriteType.Member, + id:p.id, + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory"], + deny:[] + }) + }) + try{ + await channel.permissionOverwrites.set(permissions) + }catch{ + openticket.log("Failed to reset channel permissions on ticket move!","error") + } + + //handle participants + const participants: {type:"role"|"user",id:string}[] = [] + permissions.forEach((permission,index) => { + if (index == 0) return //don't include @everyone + const type = (permission.type == discord.OverwriteType.Role) ? "role" : "user" + const id = permission.id as string + participants.push({type,id}) + }) + ticket.get("openticket:participants").value = participants + ticket.get("openticket:participants").refreshDatabase() + + //rename channel (and give error when crashed) + const originalName = channel.name + try{ + await utilities.timedAwait(channel.setName(channelName),2500,(err) => { + openticket.log("Failed to rename channel on ticket move","error") + }) + }catch(err){ + await channel.send((await openticket.builders.messages.getSafe("openticket:error-channel-rename").build("ticket-move",{guild,channel,user,originalName,newName:channelName})).message) + } + try{ + if (channel.type == discord.ChannelType.GuildText) channel.setTopic(channelDescription) + }catch{} + + //update ticket message + const ticketMessage = await openticket.tickets.getTicketMessage(ticket) + if (ticketMessage){ + try{ + ticketMessage.edit((await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})).message) + }catch(e){ + openticket.log("Unable to edit ticket message on ticket renaming!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"messageid",value:ticketMessage.id}, + {key:"option",value:ticket.option.id.value} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + } + + //reply with new message + if (params.sendMessage) await channel.send((await openticket.builders.messages.getSafe("openticket:move-message").build(source,{guild,channel,user,ticket,reason,data})).message) + ticket.get("openticket:busy").value = false + await openticket.events.get("afterTicketMoved").emit([ticket,user,channel,reason]) + }), + new api.ODWorker("openticket:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason,data} = params + + //to logs + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.moving.logs){ + const logChannel = openticket.posts.get("openticket:logs") + if (logChannel) logChannel.send(await openticket.builders.messages.getSafe("openticket:ticket-action-logs").build(source,{guild,channel,user,ticket,mode:"move",reason,additionalData:data})) + } + + //to dm + if (generalConfig.data.system.messages.moving.dm) await openticket.client.sendUserDm(user,await openticket.builders.messages.getSafe("openticket:ticket-action-dm").build(source,{guild,channel,user,ticket,mode:"move",reason,additionalData:data})) + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {guild,channel,user,ticket} = params + + openticket.log(user.displayName+" moved a ticket!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"reason",value:params.reason ?? "/"}, + {key:"method",value:source} + ]) + }) + ]) + openticket.actions.get("openticket:move-ticket").workers.backupWorker = new api.ODWorker("openticket:cancel-busy",0,(instance,params) => { + //set busy to false in case of crash or cancel + params.ticket.get("openticket:busy").value = false + }) +} \ No newline at end of file diff --git a/src/actions/pinTicket.ts b/src/actions/pinTicket.ts new file mode 100644 index 0000000..3f06af4 --- /dev/null +++ b/src/actions/pinTicket.ts @@ -0,0 +1,274 @@ +/////////////////////////////////////// +//TICKET PINNING SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:pin-ticket")) + openticket.actions.get("openticket:pin-ticket").workers.add([ + new api.ODWorker("openticket:pin-ticket",2,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason} = params + if (channel.isThread()) throw new api.ODSystemError("Unable to pin ticket! Open Ticket doesn't support threads!") + + await openticket.events.get("onTicketPin").emit([ticket,user,channel,reason]) + + //update ticket + ticket.get("openticket:pinned").value = true + ticket.get("openticket:pinned-by").value = user.id + ticket.get("openticket:pinned-on").value = new Date().getTime() + ticket.get("openticket:busy").value = true + + //update stats + openticket.stats.get("openticket:global").setStat("openticket:tickets-pinned",1,"increase") + openticket.stats.get("openticket:user").setStat("openticket:tickets-pinned",user.id,1,"increase") + + //move to top of category + if (channel.parent){ + await channel.setPosition(0,{reason:"Ticket Pinned!"}) + } + + //rename channel (and give error when crashed) + const originalName = channel.name + const newName = "📌"+channel.name + try{ + await utilities.timedAwait(channel.setName(newName),2500,(err) => { + openticket.log("Failed to rename channel on ticket pin","error") + }) + }catch(err){ + await channel.send((await openticket.builders.messages.getSafe("openticket:error-channel-rename").build("ticket-pin",{guild,channel,user,originalName,newName})).message) + } + + //update ticket message + const ticketMessage = await openticket.tickets.getTicketMessage(ticket) + if (ticketMessage){ + try{ + ticketMessage.edit((await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})).message) + }catch(e){ + openticket.log("Unable to edit ticket message on ticket pinning!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"message",value:ticketMessage.id}, + {key:"option",value:ticket.option.id.value} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + } + + //reply with new message + if (params.sendMessage) await channel.send((await openticket.builders.messages.getSafe("openticket:pin-message").build(source,{guild,channel,user,ticket,reason})).message) + ticket.get("openticket:busy").value = false + await openticket.events.get("afterTicketPinned").emit([ticket,user,channel,reason]) + }), + new api.ODWorker("openticket:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason} = params + + //to logs + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.pinning.logs){ + const logChannel = openticket.posts.get("openticket:logs") + if (logChannel) logChannel.send(await openticket.builders.messages.getSafe("openticket:ticket-action-logs").build(source,{guild,channel,user,ticket,mode:"pin",reason,additionalData:null})) + } + + //to dm + if (generalConfig.data.system.messages.pinning.dm) await openticket.client.sendUserDm(user,await openticket.builders.messages.getSafe("openticket:ticket-action-dm").build(source,{guild,channel,user,ticket,mode:"pin",reason,additionalData:null})) + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {guild,channel,user,ticket} = params + + openticket.log(user.displayName+" pinned a ticket!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"reason",value:params.reason ?? "/"}, + {key:"method",value:source} + ]) + }) + ]) +} + +export const registerVerifyBars = async () => { + //PIN TICKET TICKET MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:pin-ticket-ticket-message",openticket.builders.messages.getSafe("openticket:verifybar-ticket-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:pin-ticket-ticket-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.pin + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:pin-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when already pinned + if (ticket.get("openticket:pinned").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:openticket.languages.getTranslation("errors.actionInvalid.pin"),layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start pinning ticket + if (params.data == "reason"){ + //pin with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:pin-ticket-reason").build("ticket-message",{guild,channel,user,ticket})) + }else{ + //pin without reason + await instance.defer("update",false) + await openticket.actions.get("openticket:pin-ticket").run("ticket-message",{guild,channel,user,ticket,reason:null,sendMessage:true}) + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + } + }) + ]) + openticket.verifybars.get("openticket:pin-ticket-ticket-message").failure.add([ + new api.ODWorker("openticket:back-to-ticket-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + }) + ]) + + //PIN TICKET UNPIN MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:pin-ticket-unpin-message",openticket.builders.messages.getSafe("openticket:verifybar-unpin-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:pin-ticket-unpin-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.pin + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:pin-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when already pinned + if (ticket.get("openticket:pinned").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:openticket.languages.getTranslation("errors.actionInvalid.pin"),layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start pinning ticket + if (params.data == "reason"){ + //pin with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:pin-ticket-reason").build("unpin-message",{guild,channel,user,ticket})) + }else{ + //pin without reason + await instance.defer("update",false) + await openticket.actions.get("openticket:pin-ticket").run("unpin-message",{guild,channel,user,ticket,reason:null,sendMessage:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:pin-message").build("unpin-message",{guild,channel,user,ticket,reason:null})) + } + }) + ]) + openticket.verifybars.get("openticket:pin-ticket-unpin-message").failure.add([ + new api.ODWorker("openticket:back-to-unpin-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + const {verifybarMessage} = params + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + const rawReason = (verifybarMessage && verifybarMessage.embeds[0] && verifybarMessage.embeds[0].fields[0]) ? verifybarMessage.embeds[0].fields[0].value : null + const reason = (rawReason == null) ? null : rawReason.substring(3,rawReason.length-3) + + await instance.update(await openticket.builders.messages.getSafe("openticket:unpin-message").build("other",{guild,channel,user,ticket,reason})) + }) + ]) + openticket.actions.get("openticket:pin-ticket").workers.backupWorker = new api.ODWorker("openticket:cancel-busy",0,(instance,params) => { + //set busy to false in case of crash or cancel + params.ticket.get("openticket:busy").value = false + }) +} \ No newline at end of file diff --git a/src/actions/reactionRole.ts b/src/actions/reactionRole.ts new file mode 100644 index 0000000..884a2cd --- /dev/null +++ b/src/actions/reactionRole.ts @@ -0,0 +1,94 @@ +/////////////////////////////////////// +//REACTION ROLE SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:reaction-role")) + openticket.actions.get("openticket:reaction-role").workers.add([ + new api.ODWorker("openticket:reaction-role",2,async (instance,params,source,cancel) => { + const {guild,user,option,overwriteMode} = params + const role = openticket.roles.get(option.id) + if (!role) throw new api.ODSystemError("ODAction(ot:reaction-role) => Unknown reaction role (ODRole)") + instance.role = role + const mode = (overwriteMode) ? overwriteMode : role.get("openticket:mode").value + + await openticket.events.get("onRoleUpdate").emit([user,role]) + + //get guild member + const member = await openticket.client.fetchGuildMember(guild,user.id) + if (!member) throw new api.ODSystemError("ODAction(ot:reaction-role) => User isn't a member of the server!") + + //get all roles + const roleIds = role.get("openticket:roles").value + const roles: discord.Role[] = [] + for (const id of roleIds){ + const r = await openticket.client.fetchGuildRole(guild,id) + if (r) roles.push(r) + else openticket.log("Unable to find role in server!","warning",[ + {key:"roleid",value:id} + ]) + } + + //update roles of user + const result: api.OTRoleUpdateResult[] = [] + for (const r of roles){ + try{ + if (r.members.has(user.id) && (mode == "add&remove" || mode == "remove")){ + //user has role (remove) + await member.roles.remove(r) + result.push({role:r,action:"removed"}) + }else if (!r.members.has(user.id) && (mode == "add&remove" || mode == "add")){ + //user doesn't have role (add) + await member.roles.add(r) + result.push({role:r,action:"added"}) + }else{ + //don't do anything + result.push({role:r,action:null}) + } + }catch{ + result.push({role:r,action:null}) + } + } + + //get roles to remove on add + if (result.find((r) => r.action == "added")){ + //get all remove roles + const removeRoleIds = role.get("openticket:remove-roles-on-add").value + const removeRoles: discord.Role[] = [] + for (const id of removeRoleIds){ + const r = await openticket.client.fetchGuildRole(guild,id) + if (r) removeRoles.push(r) + else openticket.log("Unable to find role in server!","warning",[ + {key:"roleid",value:id} + ]) + } + + //remove roles from user + for (const r of removeRoles){ + try{ + if (r.members.has(user.id)){ + //user has role (remove) + await member.roles.remove(r) + result.push({role:r,action:"removed"}) + } + }catch{} + } + } + + //update instance & finish event + instance.result = result + await openticket.events.get("afterRolesUpdated").emit([user,role]) + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {guild,user,option} = params + openticket.log(user.displayName+" updated his roles!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"method",value:source}, + {key:"option",value:option.id.value} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/actions/removeTicketUser.ts b/src/actions/removeTicketUser.ts new file mode 100644 index 0000000..b22b62b --- /dev/null +++ b/src/actions/removeTicketUser.ts @@ -0,0 +1,81 @@ +/////////////////////////////////////// +//TICKET REMOVE USER SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:remove-ticket-user")) + openticket.actions.get("openticket:remove-ticket-user").workers.add([ + new api.ODWorker("openticket:remove-ticket-user",2,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason,data} = params + if (channel.isThread()) throw new api.ODSystemError("Unable to remove user from ticket! Open Ticket doesn't support threads!") + + await openticket.events.get("onTicketUserRemove").emit([ticket,user,data,channel,reason]) + + //update ticket + const index = ticket.get("openticket:participants").value.findIndex((p) => p.type == "user" && p.id == data.id) + if (index < 0) return cancel() + ticket.get("openticket:participants").value.splice(index,1) + ticket.get("openticket:participants").refreshDatabase() + ticket.get("openticket:busy").value = true + + //update channel permissions + try{ + await channel.permissionOverwrites.delete(data) + }catch{ + openticket.log("Failed to remove channel permission overwrites on remove-ticket-user","error") + } + + //update ticket message + const ticketMessage = await openticket.tickets.getTicketMessage(ticket) + if (ticketMessage){ + try{ + ticketMessage.edit((await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})).message) + }catch(e){ + openticket.log("Unable to edit ticket message on ticket user removal!","error",[ + {key:"channel",value:channel.id}, + {key:"message",value:ticketMessage.id}, + {key:"option",value:ticket.option.id.value} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + } + + //reply with new message + if (params.sendMessage) await channel.send((await openticket.builders.messages.getSafe("openticket:remove-message").build(source,{guild,channel,user,ticket,reason,data})).message) + ticket.get("openticket:busy").value = false + await openticket.events.get("afterTicketUserRemoved").emit([ticket,user,data,channel,reason]) + }), + new api.ODWorker("openticket:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason,data} = params + + //to logs + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.removing.logs){ + const logChannel = openticket.posts.get("openticket:logs") + if (logChannel) logChannel.send(await openticket.builders.messages.getSafe("openticket:ticket-action-logs").build(source,{guild,channel,user,ticket,mode:"remove",reason,additionalData:data})) + } + + //to dm + if (generalConfig.data.system.messages.removing.dm) await openticket.client.sendUserDm(user,await openticket.builders.messages.getSafe("openticket:ticket-action-dm").build(source,{guild,channel,user,ticket,mode:"remove",reason,additionalData:data})) + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {guild,channel,user,ticket,data} = params + + openticket.log(user.displayName+" removed "+data.displayName+" from a ticket!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"reason",value:params.reason ?? "/"}, + {key:"method",value:source} + ]) + }) + ]) + openticket.actions.get("openticket:remove-ticket-user").workers.backupWorker = new api.ODWorker("openticket:cancel-busy",0,(instance,params) => { + //set busy to false in case of crash or cancel + params.ticket.get("openticket:busy").value = false + }) +} \ No newline at end of file diff --git a/src/actions/renameTicket.ts b/src/actions/renameTicket.ts new file mode 100644 index 0000000..312be72 --- /dev/null +++ b/src/actions/renameTicket.ts @@ -0,0 +1,78 @@ +/////////////////////////////////////// +//TICKET RENAMING SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:rename-ticket")) + openticket.actions.get("openticket:rename-ticket").workers.add([ + new api.ODWorker("openticket:rename-ticket",2,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason,data} = params + if (channel.isThread()) throw new api.ODSystemError("Unable to rename ticket! Open Ticket doesn't support threads!") + + await openticket.events.get("onTicketRename").emit([ticket,user,channel,reason]) + + //rename channel (and give error when crashed) + const originalName = channel.name + try{ + await utilities.timedAwait(channel.setName(data),2500,(err) => { + openticket.log("Failed to rename channel on ticket rename","error") + }) + }catch(err){ + await channel.send((await openticket.builders.messages.getSafe("openticket:error-channel-rename").build("ticket-rename",{guild,channel,user,originalName,newName:data})).message) + } + + //update ticket message + const ticketMessage = await openticket.tickets.getTicketMessage(ticket) + if (ticketMessage){ + try{ + ticketMessage.edit((await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})).message) + }catch(e){ + openticket.log("Unable to edit ticket message on ticket renaming!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"messageid",value:ticketMessage.id}, + {key:"option",value:ticket.option.id.value} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + } + + //reply with new message + if (params.sendMessage) await channel.send((await openticket.builders.messages.getSafe("openticket:rename-message").build(source,{guild,channel,user,ticket,reason,data})).message) + ticket.get("openticket:busy").value = false + await openticket.events.get("afterTicketRenamed").emit([ticket,user,channel,reason]) + }), + new api.ODWorker("openticket:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason,data} = params + + //to logs + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.renaming.logs){ + const logChannel = openticket.posts.get("openticket:logs") + if (logChannel) logChannel.send(await openticket.builders.messages.getSafe("openticket:ticket-action-logs").build(source,{guild,channel,user,ticket,mode:"rename",reason,additionalData:data})) + } + + //to dm + if (generalConfig.data.system.messages.renaming.dm) await openticket.client.sendUserDm(user,await openticket.builders.messages.getSafe("openticket:ticket-action-dm").build(source,{guild,channel,user,ticket,mode:"rename",reason,additionalData:data})) + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {guild,channel,user,ticket} = params + + openticket.log(user.displayName+" renamed a ticket!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"reason",value:params.reason ?? "/"}, + {key:"method",value:source} + ]) + }) + ]) + openticket.actions.get("openticket:rename-ticket").workers.backupWorker = new api.ODWorker("openticket:cancel-busy",0,(instance,params) => { + //set busy to false in case of crash or cancel + params.ticket.get("openticket:busy").value = false + }) +} \ No newline at end of file diff --git a/src/actions/reopenTicket.ts b/src/actions/reopenTicket.ts new file mode 100644 index 0000000..77bd5ab --- /dev/null +++ b/src/actions/reopenTicket.ts @@ -0,0 +1,448 @@ +/////////////////////////////////////// +//TICKET REOPENING SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:reopen-ticket")) + openticket.actions.get("openticket:reopen-ticket").workers.add([ + new api.ODWorker("openticket:reopen-ticket",2,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason} = params + if (channel.isThread()) throw new api.ODSystemError("Unable to reopen ticket! Open Ticket doesn't support threads!") + + await openticket.events.get("onTicketReopen").emit([ticket,user,channel,reason]) + + //update ticket + ticket.get("openticket:closed").value = false + ticket.get("openticket:open").value = true + ticket.get("openticket:autoclosed").value = false + ticket.get("openticket:closed-by").value = null + ticket.get("openticket:closed-on").value = null + ticket.get("openticket:busy").value = true + + //update stats + openticket.stats.get("openticket:global").setStat("openticket:tickets-reopened",1,"increase") + openticket.stats.get("openticket:user").setStat("openticket:tickets-reopened",user.id,1,"increase") + + //update category + const channelCategory = ticket.option.get("openticket:channel-category").value + const channelBackupCategory = ticket.option.get("openticket:channel-category-backup").value + if (channelCategory !== ""){ + //category enabled + try { + const normalCategory = await openticket.client.fetchGuildCategoryChannel(guild,channelCategory) + if (!normalCategory){ + //default category was not found + openticket.log("Ticket Reopening Error: Unable to find category! #1","error",[ + {key:"categoryid",value:channelCategory}, + {key:"backup",value:"false"} + ]) + }else{ + //default category was found + if (normalCategory.children.cache.size >= 49 && channelBackupCategory != ""){ + //use backup category + const backupCategory = await openticket.client.fetchGuildCategoryChannel(guild,channelBackupCategory) + if (!backupCategory){ + //default category was not found + openticket.log("Ticket Reopening Error: Unable to find category! #2","error",[ + {key:"categoryid",value:channelBackupCategory}, + {key:"backup",value:"true"} + ]) + }else{ + //use backup category + channel.setParent(backupCategory,{lockPermissions:false}) + ticket.get("openticket:category-mode").value = "backup" + ticket.get("openticket:category").value = backupCategory.id + } + }else{ + //use default category + channel.setParent(normalCategory,{lockPermissions:false}) + ticket.get("openticket:category-mode").value = "normal" + ticket.get("openticket:category").value = normalCategory.id + } + } + }catch(e){ + openticket.log("Unable to move ticket to 'reopened category'!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + }else{ + channel.setParent(null,{lockPermissions:false}) + ticket.get("openticket:category-mode").value = null + ticket.get("openticket:category").value = null + } + + //update permissions + const permissions: discord.OverwriteResolvable[] = [{ + type:discord.OverwriteType.Role, + id:guild.roles.everyone.id, + allow:[], + deny:["ViewChannel","SendMessages","ReadMessageHistory"] + }] + const globalAdmins = openticket.configs.get("openticket:general").data.globalAdmins + const optionAdmins = ticket.option.get("openticket:admins").value + const readonlyAdmins = ticket.option.get("openticket:admins-readonly").value + + globalAdmins.forEach((admin) => { + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","ManageMessages"], + deny:[] + }) + }) + optionAdmins.forEach((admin) => { + if (globalAdmins.includes(admin)) return + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","ManageMessages"], + deny:[] + }) + }) + readonlyAdmins.forEach((admin) => { + if (globalAdmins.includes(admin)) return + if (optionAdmins.includes(admin)) return + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","ReadMessageHistory"], + deny:["SendMessages","AddReactions","AttachFiles","SendPolls"] + }) + }) + ticket.get("openticket:participants").value.forEach((participant) => { + //all participants that aren't roles/admins + if (participant.type == "user"){ + permissions.push({ + type:discord.OverwriteType.Member, + id:user.id, + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory"], + deny:[] + }) + } + }) + channel.permissionOverwrites.set(permissions) + + //update ticket message + const ticketMessage = await openticket.tickets.getTicketMessage(ticket) + if (ticketMessage){ + try{ + ticketMessage.edit((await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})).message) + }catch(e){ + openticket.log("Unable to edit ticket message on ticket reopening!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"messageid",value:ticketMessage.id}, + {key:"option",value:ticket.option.id.value} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + } + + if (params.sendMessage) await channel.send((await openticket.builders.messages.getSafe("openticket:reopen-message").build(source,{guild,channel,user,ticket,reason})).message) + ticket.get("openticket:busy").value = false + await openticket.events.get("afterTicketReopened").emit([ticket,user,channel,reason]) + }), + new api.ODWorker("openticket:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason} = params + + //to logs + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.reopening.logs){ + const logChannel = openticket.posts.get("openticket:logs") + if (logChannel) logChannel.send(await openticket.builders.messages.getSafe("openticket:ticket-action-logs").build(source,{guild,channel,user,ticket,mode:"reopen",reason,additionalData:null})) + } + + //to dm + if (generalConfig.data.system.messages.reopening.dm) await openticket.client.sendUserDm(user,await openticket.builders.messages.getSafe("openticket:ticket-action-dm").build(source,{guild,channel,user,ticket,mode:"reopen",reason,additionalData:null})) + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {guild,channel,user,ticket} = params + + openticket.log(user.displayName+" reopened a ticket!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"reason",value:params.reason ?? "/"}, + {key:"method",value:source} + ]) + }) + ]) +} + +export const registerVerifyBars = async () => { + //REOPEN TICKET TICKET MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:reopen-ticket-ticket-message",openticket.builders.messages.getSafe("openticket:verifybar-ticket-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:reopen-ticket-ticket-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.reopen + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:reopen-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when not closed + if (!ticket.get("openticket:closed").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:openticket.languages.getTranslation("errors.actionInvalid.reopen"),layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start reopening ticket + if (params.data == "reason"){ + //reopen with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:reopen-ticket-reason").build("ticket-message",{guild,channel,user,ticket})) + }else{ + //reopen without reason + await instance.defer("update",false) + await openticket.actions.get("openticket:reopen-ticket").run("ticket-message",{guild,channel,user,ticket,reason:null,sendMessage:true}) + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + } + }) + ]) + openticket.verifybars.get("openticket:reopen-ticket-ticket-message").failure.add([ + new api.ODWorker("openticket:back-to-ticket-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + }) + ]) + + //REOPEN TICKET CLOSE MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:reopen-ticket-close-message",openticket.builders.messages.getSafe("openticket:verifybar-close-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:reopen-ticket-close-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.reopen + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:reopen-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when not closed + if (!ticket.get("openticket:closed").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:openticket.languages.getTranslation("errors.actionInvalid.reopen"),layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start reopening ticket + if (params.data == "reason"){ + //reopen with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:reopen-ticket-reason").build("close-message",{guild,channel,user,ticket})) + }else{ + //reopen without reason + await instance.defer("update",false) + await openticket.actions.get("openticket:reopen-ticket").run("close-message",{guild,channel,user,ticket,reason:null,sendMessage:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:reopen-message").build("close-message",{guild,channel,user,ticket,reason:null})) + } + }) + ]) + openticket.verifybars.get("openticket:reopen-ticket-close-message").failure.add([ + new api.ODWorker("openticket:back-to-close-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + const {verifybarMessage} = params + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + const rawReason = (verifybarMessage && verifybarMessage.embeds[0] && verifybarMessage.embeds[0].fields[0]) ? verifybarMessage.embeds[0].fields[0].value : null + const reason = (rawReason == null) ? null : rawReason.substring(3,rawReason.length-3) + + await instance.update(await openticket.builders.messages.getSafe("openticket:close-message").build("other",{guild,channel,user,ticket,reason})) + }) + ]) + + //REOPEN TICKET AUTOCLOSE MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:reopen-ticket-autoclose-message",openticket.builders.messages.getSafe("openticket:verifybar-autoclose-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:reopen-ticket-autoclose-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.reopen + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:reopen-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when not closed + if (!ticket.get("openticket:closed").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:openticket.languages.getTranslation("errors.actionInvalid.reopen"),layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start reopening ticket + if (params.data == "reason"){ + //reopen with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:reopen-ticket-reason").build("autoclose-message",{guild,channel,user,ticket})) + }else{ + //reopen without reason + await instance.defer("update",false) + await openticket.actions.get("openticket:reopen-ticket").run("autoclose-message",{guild,channel,user,ticket,reason:null,sendMessage:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:reopen-message").build("autoclose-message",{guild,channel,user,ticket,reason:null})) + } + }) + ]) + openticket.verifybars.get("openticket:reopen-ticket-autoclose-message").failure.add([ + new api.ODWorker("openticket:back-to-autoclose-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + const {verifybarMessage} = params + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + await instance.update(await openticket.builders.messages.getSafe("openticket:autoclose-message").build("other",{guild,channel,user,ticket})) + }) + ]) + openticket.actions.get("openticket:reopen-ticket").workers.backupWorker = new api.ODWorker("openticket:cancel-busy",0,(instance,params) => { + //set busy to false in case of crash or cancel + params.ticket.get("openticket:busy").value = false + }) +} \ No newline at end of file diff --git a/src/actions/unclaimTicket.ts b/src/actions/unclaimTicket.ts new file mode 100644 index 0000000..302fb3a --- /dev/null +++ b/src/actions/unclaimTicket.ts @@ -0,0 +1,305 @@ +/////////////////////////////////////// +//TICKET UNCLAIMING SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:unclaim-ticket")) + openticket.actions.get("openticket:unclaim-ticket").workers.add([ + new api.ODWorker("openticket:unclaim-ticket",2,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason} = params + if (channel.isThread()) throw new api.ODSystemError("Unable to unclaim ticket! Open Ticket doesn't support threads!") + + await openticket.events.get("onTicketUnclaim").emit([ticket,user,channel,reason]) + + //update ticket + ticket.get("openticket:claimed").value = false + ticket.get("openticket:claimed-by").value = null + ticket.get("openticket:claimed-on").value = null + ticket.get("openticket:busy").value = true + + //update category + const channelCategory = ticket.option.get("openticket:channel-category").value + const channelBackupCategory = ticket.option.get("openticket:channel-category-backup").value + if (channelCategory !== ""){ + //category enabled + try { + const normalCategory = await openticket.client.fetchGuildCategoryChannel(guild,channelCategory) + if (!normalCategory){ + //default category was not found + openticket.log("Ticket Unclaiming Error: Unable to find category! #1","error",[ + {key:"categoryid",value:channelCategory}, + {key:"backup",value:"false"} + ]) + }else{ + //default category was found + if (normalCategory.children.cache.size >= 49 && channelBackupCategory != ""){ + //use backup category + const backupCategory = await openticket.client.fetchGuildCategoryChannel(guild,channelBackupCategory) + if (!backupCategory){ + //default category was not found + openticket.log("Ticket Unclaiming Error: Unable to find category! #2","error",[ + {key:"categoryid",value:channelBackupCategory}, + {key:"backup",value:"true"} + ]) + }else{ + //use backup category + channel.setParent(backupCategory,{lockPermissions:false}) + ticket.get("openticket:category-mode").value = "backup" + ticket.get("openticket:category").value = backupCategory.id + } + }else{ + //use default category + channel.setParent(normalCategory,{lockPermissions:false}) + ticket.get("openticket:category-mode").value = "normal" + ticket.get("openticket:category").value = normalCategory.id + } + } + + }catch(e){ + openticket.log("Unable to move ticket to 'unclaimed category'!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + }else{ + channel.setParent(null,{lockPermissions:false}) + ticket.get("openticket:category-mode").value = null + ticket.get("openticket:category").value = null + } + + //update ticket message + const ticketMessage = await openticket.tickets.getTicketMessage(ticket) + if (ticketMessage){ + try{ + ticketMessage.edit((await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})).message) + }catch(e){ + openticket.log("Unable to edit ticket message on ticket unclaiming!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"messageid",value:ticketMessage.id}, + {key:"option",value:ticket.option.id.value} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + } + + //reply with new message + if (params.sendMessage) await channel.send((await openticket.builders.messages.getSafe("openticket:unclaim-message").build(source,{guild,channel,user,ticket,reason})).message) + ticket.get("openticket:busy").value = false + await openticket.events.get("afterTicketUnclaimed").emit([ticket,user,channel,reason]) + }), + new api.ODWorker("openticket:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason} = params + + //to logs + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.claiming.logs){ + const logChannel = openticket.posts.get("openticket:logs") + if (logChannel) logChannel.send(await openticket.builders.messages.getSafe("openticket:ticket-action-logs").build(source,{guild,channel,user,ticket,mode:"unclaim",reason,additionalData:null})) + } + + //to dm + if (generalConfig.data.system.messages.claiming.dm) await openticket.client.sendUserDm(user,await openticket.builders.messages.getSafe("openticket:ticket-action-dm").build(source,{guild,channel,user,ticket,mode:"unclaim",reason,additionalData:null})) + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {guild,channel,user,ticket} = params + + openticket.log(user.displayName+" unclaimed a ticket!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"reason",value:params.reason ?? "/"}, + {key:"method",value:source} + ]) + }) + ]) +} + +export const registerVerifyBars = async () => { + //UNCLAIM TICKET TICKET MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:unclaim-ticket-ticket-message",openticket.builders.messages.getSafe("openticket:verifybar-ticket-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:unclaim-ticket-ticket-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.unclaim + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:unclaim-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when not claimed + if (!ticket.get("openticket:claimed").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:openticket.languages.getTranslation("errors.actionInvalid.unclaim"),layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start unclaiming ticket + if (params.data == "reason"){ + //unclaim with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:unclaim-ticket-reason").build("ticket-message",{guild,channel,user,ticket})) + }else{ + //unclaim without reason + await instance.defer("update",false) + await openticket.actions.get("openticket:unclaim-ticket").run("ticket-message",{guild,channel,user,ticket,reason:null,sendMessage:true}) + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + } + }) + ]) + openticket.verifybars.get("openticket:unclaim-ticket-ticket-message").failure.add([ + new api.ODWorker("openticket:back-to-ticket-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + }) + ]) + + //UNCLAIM TICKET CLAIM MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:unclaim-ticket-claim-message",openticket.builders.messages.getSafe("openticket:verifybar-claim-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:unclaim-ticket-claim-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.unclaim + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:unclaim-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when not claimed + if (!ticket.get("openticket:claimed").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:openticket.languages.getTranslation("errors.actionInvalid.unclaim"),layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start unclaiming ticket + if (params.data == "reason"){ + //unclaim with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:unclaim-ticket-reason").build("claim-message",{guild,channel,user,ticket})) + }else{ + //unclaim without reason + await instance.defer("update",false) + await openticket.actions.get("openticket:unclaim-ticket").run("claim-message",{guild,channel,user,ticket,reason:null,sendMessage:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:unclaim-message").build("claim-message",{guild,channel,user,ticket,reason:null})) + } + }) + ]) + openticket.verifybars.get("openticket:unclaim-ticket-claim-message").failure.add([ + new api.ODWorker("openticket:back-to-claim-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + const {verifybarMessage} = params + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + const rawReason = (verifybarMessage && verifybarMessage.embeds[0] && verifybarMessage.embeds[0].fields[0]) ? verifybarMessage.embeds[0].fields[0].value : null + const reason = (rawReason == null) ? null : rawReason.substring(3,rawReason.length-3) + + await instance.update(await openticket.builders.messages.getSafe("openticket:claim-message").build("other",{guild,channel,user,ticket,reason})) + }) + ]) + openticket.actions.get("openticket:unclaim-ticket").workers.backupWorker = new api.ODWorker("openticket:cancel-busy",0,(instance,params) => { + //set busy to false in case of crash or cancel + params.ticket.get("openticket:busy").value = false + }) +} \ No newline at end of file diff --git a/src/actions/unpinTicket.ts b/src/actions/unpinTicket.ts new file mode 100644 index 0000000..e624c65 --- /dev/null +++ b/src/actions/unpinTicket.ts @@ -0,0 +1,267 @@ +/////////////////////////////////////// +//TICKET UNPINNING SYSTEM +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerActions = async () => { + openticket.actions.add(new api.ODAction("openticket:unpin-ticket")) + openticket.actions.get("openticket:unpin-ticket").workers.add([ + new api.ODWorker("openticket:unpin-ticket",2,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason} = params + if (channel.isThread()) throw new api.ODSystemError("Unable to unpin ticket! Open Ticket doesn't support threads!") + + await openticket.events.get("onTicketUnpin").emit([ticket,user,channel,reason]) + + //update ticket + ticket.get("openticket:pinned").value = false + ticket.get("openticket:pinned-by").value = null + ticket.get("openticket:pinned-on").value = null + ticket.get("openticket:busy").value = true + + //rename channel (and give error when crashed) + const originalName = channel.name + if (originalName.startsWith("📌")){ + const newName = originalName.replace("📌",""); + try{ + await utilities.timedAwait(channel.setName(newName),2500,(err) => { + openticket.log("Failed to rename channel on ticket unpin","error") + }) + }catch(err){ + await channel.send((await openticket.builders.messages.getSafe("openticket:error-channel-rename").build("ticket-unpin",{guild,channel,user,originalName,newName})).message) + } + } + + //update ticket message + const ticketMessage = await openticket.tickets.getTicketMessage(ticket) + if (ticketMessage){ + try{ + ticketMessage.edit((await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})).message) + }catch(e){ + openticket.log("Unable to edit ticket message on ticket unpinning!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"messageid",value:ticketMessage.id}, + {key:"option",value:ticket.option.id.value} + ]) + openticket.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + } + + //reply with new message + if (params.sendMessage) await channel.send((await openticket.builders.messages.getSafe("openticket:unpin-message").build(source,{guild,channel,user,ticket,reason})).message) + ticket.get("openticket:busy").value = false + await openticket.events.get("afterTicketUnpinned").emit([ticket,user,channel,reason]) + }), + new api.ODWorker("openticket:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason} = params + + //to logs + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.pinning.logs){ + const logChannel = openticket.posts.get("openticket:logs") + if (logChannel) logChannel.send(await openticket.builders.messages.getSafe("openticket:ticket-action-logs").build(source,{guild,channel,user,ticket,mode:"unpin",reason,additionalData:null})) + } + + //to dm + if (generalConfig.data.system.messages.pinning.dm) await openticket.client.sendUserDm(user,await openticket.builders.messages.getSafe("openticket:ticket-action-dm").build(source,{guild,channel,user,ticket,mode:"unpin",reason,additionalData:null})) + }), + new api.ODWorker("openticket:logs",0,(instance,params,source,cancel) => { + const {guild,channel,user,ticket} = params + + openticket.log(user.displayName+" unpinned a ticket!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"reason",value:params.reason ?? "/"}, + {key:"method",value:source} + ]) + }) + ]) +} + +export const registerVerifyBars = async () => { + //UNPIN TICKET TICKET MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:unpin-ticket-ticket-message",openticket.builders.messages.getSafe("openticket:verifybar-ticket-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:unpin-ticket-ticket-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.unpin + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:unpin-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when not pinned + if (!ticket.get("openticket:pinned").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:openticket.languages.getTranslation("errors.actionInvalid.unpin"),layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start unpining ticket + if (params.data == "reason"){ + //unpin with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:unpin-ticket-reason").build("ticket-message",{guild,channel,user,ticket})) + }else{ + //unpin without reason + await instance.defer("update",false) + await openticket.actions.get("openticket:unpin-ticket").run("ticket-message",{guild,channel,user,ticket,reason:null,sendMessage:true}) + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + } + }) + ]) + openticket.verifybars.get("openticket:unpin-ticket-ticket-message").failure.add([ + new api.ODWorker("openticket:back-to-ticket-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + }) + ]) + + //UNPIN TICKET PIN MESSAGE + openticket.verifybars.add(new api.ODVerifyBar("openticket:unpin-ticket-pin-message",openticket.builders.messages.getSafe("openticket:verifybar-pin-message"),!generalConfig.data.system.disableVerifyBars)) + openticket.verifybars.get("openticket:unpin-ticket-pin-message").success.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.unpin + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:unpin-ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when not pinned + if (!ticket.get("openticket:pinned").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:openticket.languages.getTranslation("errors.actionInvalid.unpin"),layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + //start unpinning ticket + if (params.data == "reason"){ + //unpin with reason + instance.modal(await openticket.builders.modals.getSafe("openticket:unpin-ticket-reason").build("pin-message",{guild,channel,user,ticket})) + }else{ + //unpin without reason + await instance.defer("update",false) + await openticket.actions.get("openticket:unpin-ticket").run("pin-message",{guild,channel,user,ticket,reason:null,sendMessage:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:unpin-message").build("pin-message",{guild,channel,user,ticket,reason:null})) + } + }) + ]) + openticket.verifybars.get("openticket:unpin-ticket-pin-message").failure.add([ + new api.ODWorker("openticket:back-to-pin-message",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + const {verifybarMessage} = params + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + + const rawReason = (verifybarMessage && verifybarMessage.embeds[0] && verifybarMessage.embeds[0].fields[0]) ? verifybarMessage.embeds[0].fields[0].value : null + const reason = (rawReason == null) ? null : rawReason.substring(3,rawReason.length-3) + + await instance.update(await openticket.builders.messages.getSafe("openticket:pin-message").build("other",{guild,channel,user,ticket,reason})) + }) + ]) + openticket.actions.get("openticket:unpin-ticket").workers.backupWorker = new api.ODWorker("openticket:cancel-busy",0,(instance,params) => { + //set busy to false in case of crash or cancel + params.ticket.get("openticket:busy").value = false + }) +} \ No newline at end of file diff --git a/src/builders/buttons.ts b/src/builders/buttons.ts new file mode 100644 index 0000000..78f088e --- /dev/null +++ b/src/builders/buttons.ts @@ -0,0 +1,351 @@ +/////////////////////////////////////// +//BUTTON BUILDERS +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const buttons = openticket.builders.buttons +const lang = openticket.languages +const generalConfig = openticket.configs.get("openticket:general") + +export const registerAllButtons = async () => { + verifybarButtons() + errorButtons() + helpMenuButtons() + panelButtons() + ticketButtons() + transcriptButtons() + clearButtons() +} + +const verifybarButtons = () => { + //VERIFYBAR SUCCESS + buttons.add(new api.ODButton("openticket:verifybar-success")) + buttons.get("openticket:verifybar-success").workers.add( + new api.ODWorker("openticket:verifybar-success",0,async (instance,params) => { + const {verifybar,customData,customColor,customLabel,customEmoji} = params + + if (customData && customData.length > 40) throw new api.ODSystemError("ODButton:openticket:verifybar-success => customData exceeds 40 characters limit!") + + const newData = (customData) ? "_"+customData : "" + const newColor = customColor ?? "gray" + const newLabel = customLabel ?? "" + const newEmoji = customEmoji ?? "✅" + + instance.setCustomId("od:verifybar-success_"+verifybar.id.value+newData) + instance.setMode("button") + instance.setColor(newColor) + if (newLabel) instance.setLabel(newLabel) + if (newEmoji) instance.setEmoji(newEmoji) + }) + ) + + //VERIFYBAR FAILURE + buttons.add(new api.ODButton("openticket:verifybar-failure")) + buttons.get("openticket:verifybar-failure").workers.add( + new api.ODWorker("openticket:verifybar-failure",0,async (instance,params) => { + const {verifybar,customData,customColor,customLabel,customEmoji} = params + + if (customData && customData.length > 40) throw new api.ODSystemError("ODButton:openticket:verifybar-success => customData exceeds 40 characters limit!") + + const newData = (customData) ? "_"+customData : "" + const newColor = customColor ?? "gray" + const newLabel = customLabel ?? "" + const newEmoji = customEmoji ?? "❌" + + instance.setCustomId("od:verifybar-failure_"+verifybar.id.value+newData) + instance.setMode("button") + instance.setColor(newColor) + if (newLabel) instance.setLabel(newLabel) + if (newEmoji) instance.setEmoji(newEmoji) + }) + ) +} + +const errorButtons = () => { + //ERROR TICKET DEPRECATED TRANSCRIPT + buttons.add(new api.ODButton("openticket:error-ticket-deprecated-transcript")) + buttons.get("openticket:error-ticket-deprecated-transcript").workers.add( + new api.ODWorker("openticket:error-ticket-deprecated-transcript",0,async (instance,params) => { + + instance.setCustomId("od:error-ticket-deprecated-transcript") + instance.setMode("button") + instance.setColor("gray") + instance.setLabel(lang.getTranslation("transcripts.errors.backup")) + instance.setEmoji("📄") + }) + ) +} + +const helpMenuButtons = () => { + //HELP MENU PAGE + buttons.add(new api.ODButton("openticket:help-menu-page")) + buttons.get("openticket:help-menu-page").workers.add( + new api.ODWorker("openticket:help-menu-page",0,async (instance,params) => { + const {mode,page} = params + const totalPages = (await openticket.helpmenu.render(mode)).length + const pageText = (page+1).toString()+"/"+totalPages.toString() + + instance.setCustomId("od:help-menu-page_"+page.toString()) + instance.setMode("button") + instance.setColor("gray") + instance.setLabel(lang.getTranslationWithParams("actions.buttons.helpPage",[pageText])) + instance.setDisabled(true) + }) + ) + + //HELP MENU SWITCH + buttons.add(new api.ODButton("openticket:help-menu-switch")) + buttons.get("openticket:help-menu-switch").workers.add( + new api.ODWorker("openticket:help-menu-switch",0,async (instance,params) => { + const {mode} = params + + instance.setCustomId("od:help-menu-switch_"+mode) + instance.setMode("button") + instance.setColor("gray") + if (mode == "slash") instance.setLabel(lang.getTranslation("actions.buttons.helpSwitchText")) + else instance.setLabel(lang.getTranslation("actions.buttons.helpSwitchSlash")) + }) + ) + + //HELP MENU PREVIOUS + buttons.add(new api.ODButton("openticket:help-menu-previous")) + buttons.get("openticket:help-menu-previous").workers.add( + new api.ODWorker("openticket:help-menu-previous",0,async (instance,params) => { + const {page} = params + + instance.setCustomId("od:help-menu-previous") + instance.setMode("button") + instance.setColor("gray") + instance.setEmoji("◀️") + if (page == 0) instance.setDisabled(true) + }) + ) + + //HELP MENU NEXT + buttons.add(new api.ODButton("openticket:help-menu-next")) + buttons.get("openticket:help-menu-next").workers.add( + new api.ODWorker("openticket:help-menu-next",0,async (instance,params) => { + const {mode,page} = params + const totalPages = (await openticket.helpmenu.render(mode)).length + + instance.setCustomId("od:help-menu-next") + instance.setMode("button") + instance.setColor("gray") + instance.setEmoji("▶️") + if (page == totalPages-1) instance.setDisabled(true) + }) + ) +} + +const panelButtons = () => { + //TICKET OPTION + buttons.add(new api.ODButton("openticket:ticket-option")) + buttons.get("openticket:ticket-option").workers.add( + new api.ODWorker("openticket:ticket-option",0,async (instance,params) => { + const {panel,option} = params + + instance.setCustomId("od:ticket-option_"+panel.id.value+"_"+option.id.value) + instance.setMode("button") + instance.setColor(option.get("openticket:button-color").value) + if (option.get("openticket:button-emoji").value) instance.setEmoji(option.get("openticket:button-emoji").value) + if (option.get("openticket:button-label").value) instance.setLabel(option.get("openticket:button-label").value) + if (!option.get("openticket:button-emoji").value && !option.get("openticket:button-label").value) instance.setLabel("<"+option.id.value+">") + }) + ) + + //WEBSITE OPTION + buttons.add(new api.ODButton("openticket:website-option")) + buttons.get("openticket:website-option").workers.add( + new api.ODWorker("openticket:website-option",0,async (instance,params) => { + const {panel,option} = params + + instance.setMode("url") + instance.setUrl(option.get("openticket:url").value) + if (option.get("openticket:button-emoji").value) instance.setEmoji(option.get("openticket:button-emoji").value) + if (option.get("openticket:button-label").value) instance.setLabel(option.get("openticket:button-label").value) + if (!option.get("openticket:button-emoji").value && !option.get("openticket:button-label").value) instance.setLabel("<"+option.id.value+">") + }) + ) + + //ROLE OPTION + buttons.add(new api.ODButton("openticket:role-option")) + buttons.get("openticket:role-option").workers.add( + new api.ODWorker("openticket:role-option",0,async (instance,params) => { + const {panel,option} = params + + instance.setCustomId("od:role-option_"+panel.id.value+"_"+option.id.value) + instance.setMode("button") + instance.setColor(option.get("openticket:button-color").value) + if (option.get("openticket:button-emoji").value) instance.setEmoji(option.get("openticket:button-emoji").value) + if (option.get("openticket:button-label").value) instance.setLabel(option.get("openticket:button-label").value) + if (!option.get("openticket:button-emoji").value && !option.get("openticket:button-label").value) instance.setLabel("<"+option.id.value+">") + }) + ) +} + +const ticketButtons = () => { + //VISIT TICKET + buttons.add(new api.ODButton("openticket:visit-ticket")) + buttons.get("openticket:visit-ticket").workers.add( + new api.ODWorker("openticket:visit-ticket",0,async (instance,params) => { + const {guild,channel,ticket} = params + + instance.setMode("url") + instance.setUrl(channel.url) + instance.setEmoji("🎫") + instance.setLabel(lang.getTranslation("actions.buttons.create")) + }) + ) + + //CLOSE TICKET + buttons.add(new api.ODButton("openticket:close-ticket")) + buttons.get("openticket:close-ticket").workers.add( + new api.ODWorker("openticket:close-ticket",0,async (instance,params,source) => { + const {guild,channel,ticket} = params + + instance.setMode("button") + instance.setCustomId("od:close-ticket_"+source) + instance.setColor("gray") + instance.setEmoji("🔒") + instance.setLabel(lang.getTranslation("actions.buttons.close")) + }) + ) + + //DELETE TICKET + buttons.add(new api.ODButton("openticket:delete-ticket")) + buttons.get("openticket:delete-ticket").workers.add( + new api.ODWorker("openticket:delete-ticket",0,async (instance,params,source) => { + const {guild,channel,ticket} = params + + instance.setMode("button") + instance.setCustomId("od:delete-ticket_"+source) + instance.setColor("red") + instance.setEmoji("✖") + instance.setLabel(lang.getTranslation("actions.buttons.delete")) + }) + ) + + //REOPEN TICKET + buttons.add(new api.ODButton("openticket:reopen-ticket")) + buttons.get("openticket:reopen-ticket").workers.add( + new api.ODWorker("openticket:reopen-ticket",0,async (instance,params,source) => { + const {guild,channel,ticket} = params + + instance.setMode("button") + instance.setCustomId("od:reopen-ticket_"+source) + instance.setColor("green") + instance.setEmoji("🔓") + instance.setLabel(lang.getTranslation("actions.buttons.reopen")) + }) + ) + + //CLAIM TICKET + buttons.add(new api.ODButton("openticket:claim-ticket")) + buttons.get("openticket:claim-ticket").workers.add( + new api.ODWorker("openticket:claim-ticket",0,async (instance,params,source) => { + const {guild,channel,ticket} = params + + instance.setMode("button") + instance.setCustomId("od:claim-ticket_"+source) + instance.setColor("green") + instance.setEmoji("👋") + instance.setLabel(lang.getTranslation("actions.buttons.claim")) + }) + ) + + //UNCLAIM TICKET + buttons.add(new api.ODButton("openticket:unclaim-ticket")) + buttons.get("openticket:unclaim-ticket").workers.add( + new api.ODWorker("openticket:unclaim-ticket",0,async (instance,params,source) => { + const {guild,channel,ticket} = params + + instance.setMode("button") + instance.setCustomId("od:unclaim-ticket_"+source) + instance.setColor("green") + instance.setEmoji("↩️") + instance.setLabel(lang.getTranslation("actions.buttons.unclaim")) + }) + ) + + //PIN TICKET + buttons.add(new api.ODButton("openticket:pin-ticket")) + buttons.get("openticket:pin-ticket").workers.add( + new api.ODWorker("openticket:pin-ticket",0,async (instance,params,source) => { + const {guild,channel,ticket} = params + + instance.setMode("button") + instance.setCustomId("od:pin-ticket_"+source) + instance.setColor("gray") + instance.setEmoji("📌") + instance.setLabel(lang.getTranslation("actions.buttons.pin")) + }) + ) + + //UNPIN TICKET + buttons.add(new api.ODButton("openticket:unpin-ticket")) + buttons.get("openticket:unpin-ticket").workers.add( + new api.ODWorker("openticket:unpin-ticket",0,async (instance,params,source) => { + const {guild,channel,ticket} = params + + instance.setMode("button") + instance.setCustomId("od:unpin-ticket_"+source) + instance.setColor("gray") + instance.setEmoji("📌") + instance.setLabel(lang.getTranslation("actions.buttons.unpin")) + }) + ) +} + +const transcriptButtons = () => { + //TRANSCRIPT HTML VISIT + buttons.add(new api.ODButton("openticket:transcript-html-visit")) + buttons.get("openticket:transcript-html-visit").workers.add( + new api.ODWorker("openticket:transcript-html-visit",0,async (instance,params,source) => { + const {result} = params + instance.setMode("url") + if (result.data) instance.setUrl(result.data.url) + else throw new api.ODSystemError("ODButton:openticket:transcript-html-visit => Missing Transcript Result Data!") + instance.setEmoji("📄") + instance.setLabel(lang.getTranslation("transcripts.success.visit")) + }) + ) + + //TRANSCRIPT ERROR RETRY + buttons.add(new api.ODButton("openticket:transcript-error-retry")) + buttons.get("openticket:transcript-error-retry").workers.add( + new api.ODWorker("openticket:transcript-error-retry",0,async (instance,params,source) => { + instance.setMode("button") + instance.setCustomId("od:transcript-error-retry_"+source) + instance.setColor("gray") + instance.setEmoji("🔄") + instance.setLabel(lang.getTranslation("transcripts.errors.retry")) + }) + ) + + //TRANSCRIPT ERROR CONTINUE + buttons.add(new api.ODButton("openticket:transcript-error-continue")) + buttons.get("openticket:transcript-error-continue").workers.add( + new api.ODWorker("openticket:transcript-error-continue",0,async (instance,params,source) => { + instance.setMode("button") + instance.setCustomId("od:transcript-error-continue_"+source) + instance.setColor("red") + instance.setEmoji("✖") + instance.setLabel(lang.getTranslation("transcripts.errors.continue")) + }) + ) +} + +const clearButtons = () => { + //CLEAR CONTINUE + buttons.add(new api.ODButton("openticket:clear-continue")) + buttons.get("openticket:clear-continue").workers.add( + new api.ODWorker("openticket:clear-continue",0,async (instance,params,source) => { + instance.setMode("button") + instance.setCustomId("od:clear-continue_"+source+"_"+params.filter) + instance.setColor("red") + instance.setEmoji("✖") + instance.setLabel(lang.getTranslation("actions.buttons.clear")) + }) + ) +} \ No newline at end of file diff --git a/src/builders/dropdowns.ts b/src/builders/dropdowns.ts new file mode 100644 index 0000000..d2f9b60 --- /dev/null +++ b/src/builders/dropdowns.ts @@ -0,0 +1,37 @@ +/////////////////////////////////////// +//DROPDOWN BUILDERS +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const dropdowns = openticket.builders.dropdowns + +export const registerAllDropdowns = async () => { + panelDropdowns() +} + +const panelDropdowns = () => { + //TICKET OPTION + dropdowns.add(new api.ODDropdown("openticket:panel-dropdown-tickets")) + dropdowns.get("openticket:panel-dropdown-tickets").workers.add( + new api.ODWorker("openticket:panel-dropdown-tickets",0,async (instance,params) => { + const {panel,options} = params + + const parsedOptions = options.map((option) => { + return { + label:option.get("openticket:button-label").value.substring(0,100), + value:"od:ticket-option_"+panel.id.value+"_"+option.id.value, + emoji:option.get("openticket:button-emoji").value, + description:option.get("openticket:description").value.substring(0,100) + } + }) + + instance.setCustomId("od:panel-dropdown_"+panel.id.value) + instance.setType("string") + instance.setMaxValues(1) + instance.setMinValues(0) + instance.setPlaceholder(panel.get("openticket:dropdown-placeholder").value) + instance.setOptions(parsedOptions) + }) + ) +} \ No newline at end of file diff --git a/src/builders/embeds.ts b/src/builders/embeds.ts new file mode 100644 index 0000000..4eb4d0e --- /dev/null +++ b/src/builders/embeds.ts @@ -0,0 +1,1272 @@ +/////////////////////////////////////// +//EMBED BUILDERS +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" +import nodepath from "path" + +const embeds = openticket.builders.embeds +const lang = openticket.languages +const generalConfig = openticket.configs.get("openticket:general") + +export const registerAllEmbeds = async () => { + errorEmbeds() + helpMenuEmbeds() + statsEmbeds() + panelEmbeds() + ticketEmbeds() + blacklistEmbeds() + transcriptEmbeds() + roleEmbeds() + clearEmbeds() + autoEmbeds() +} + +/**Utility function to get the translated "method" from the source. Mostly used in error embeds. */ +const getMethodFromSource = (source:"slash"|"text"|"button"|"dropdown"|"modal"|"other"): string => { + if (source == "slash" || source == "text") return lang.getTranslation("params.lowercase.command") + else if (source == "button") return lang.getTranslation("params.lowercase.button") + else if (source == "dropdown") return lang.getTranslation("params.lowercase.dropdown") + else if (source == "modal") return lang.getTranslation("params.lowercase.modal") + else return lang.getTranslation("params.lowercase.method") +} +//lang.getTranslation() + +const errorEmbeds = () => { + //ERROR + embeds.add(new api.ODEmbed("openticket:error")) + embeds.get("openticket:error").workers.add( + new api.ODWorker("openticket:error",0,async (instance,params,source) => { + const {user,error,layout} = params + + const method = getMethodFromSource(source) + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.internalError"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslationWithParams("errors.descriptions.internalError",[method]) + (layout == "simple") ? "\n"+error : "") + if (layout == "advanced" && error) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+error+"```"}) + instance.setFooter(lang.getTranslation("errors.descriptions.askForInfo")) + }) + ) + + //ERROR OPTION MISSING + embeds.add(new api.ODEmbed("openticket:error-option-missing")) + embeds.get("openticket:error-option-missing").workers.add( + new api.ODWorker("openticket:error-option-missing",0,async (instance,params) => { + const {user,error} = params + + const options = error.command.builder.options ?? [] + const optionSyntax = options.map((opt,index) => { + if (index == error.location){ + if (opt.required){ + return "`<"+opt.name+":"+opt.type.replace("guildmember","user")+">`" + }else{ + return "`["+opt.name+":"+opt.type.replace("guildmember","user")+"]`" + } + }else{ + if (opt.required){ + return "<"+opt.name+":"+opt.type.replace("guildmember","user")+">" + }else{ + return "["+opt.name+":"+opt.type.replace("guildmember","user")+"]" + } + } + }) + const commandSyntax = "**"+error.prefix+error.name+" "+optionSyntax.join(" ")+"**" + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.optionMissing"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslation("errors.descriptions.optionMissing")) + instance.addFields({name:lang.getTranslation("params.uppercase.syntax")+":",value:commandSyntax}) + }) + ) + + //ERROR OPTION INVALID + embeds.add(new api.ODEmbed("openticket:error-option-invalid")) + embeds.get("openticket:error-option-invalid").workers.add( + new api.ODWorker("openticket:error-option-invalid",0,async (instance,params) => { + const {user,error} = params + + const options = error.command.builder.options ?? [] + const optionSyntax = options.map((opt,index) => { + if (index == error.location){ + if (opt.required){ + return "`<"+opt.name+":"+opt.type.replace("guildmember","user")+">`" + }else{ + return "`["+opt.name+":"+opt.type.replace("guildmember","user")+"]`" + } + }else{ + if (opt.required){ + return "<"+opt.name+":"+opt.type.replace("guildmember","user")+">" + }else{ + return "["+opt.name+":"+opt.type.replace("guildmember","user")+"]" + } + } + }) + const commandSyntax = "**"+error.prefix+error.name+" "+optionSyntax.join(" ")+"**" + + let reasonTitle = (error.reason == "boolean" || error.reason == "string_choice") ? lang.getTranslation("errors.descriptions.optionInvalidChoose") : lang.getTranslation("params.uppercase.reason") + let reasonValue = "" + + if (error.reason == "boolean" && error.option.type == "boolean") reasonValue = (error.option.falseValue ?? "false")+" OR "+(error.option.trueValue ?? "true") + else if (error.reason == "string_choice" && error.option.type == "string" && error.option.choices) reasonValue = error.option.choices.join(" OR ") + else if (error.reason == "string_regex" && error.option.type == "string" && error.option.regex) reasonValue = lang.getTranslation("errors.optionInvalidReasons.stringRegex") + else if (error.reason == "string_min_length" && error.option.type == "string" && error.option.minLength) reasonValue = lang.getTranslationWithParams("errors.optionInvalidReasons.stringMinLength",[error.option.minLength.toString()]) + else if (error.reason == "string_max_length" && error.option.type == "string" && error.option.maxLength) reasonValue = lang.getTranslationWithParams("errors.optionInvalidReasons.stringMaxLength",[error.option.maxLength.toString()]) + else if (error.reason == "number_invalid" && error.option.type == "number") reasonValue = lang.getTranslation("errors.optionInvalidReasons.numberInvalid") + else if (error.reason == "number_min" && error.option.type == "number" && error.option.min) reasonValue = lang.getTranslationWithParams("errors.optionInvalidReasons.numberMin",[error.option.min.toString()]) + else if (error.reason == "number_max" && error.option.type == "number" && error.option.max) reasonValue = lang.getTranslationWithParams("errors.optionInvalidReasons.numberMax",[error.option.max.toString()]) + else if (error.reason == "number_decimal" && error.option.type == "number") reasonValue = lang.getTranslation("errors.optionInvalidReasons.numberDecimal") + else if (error.reason == "number_negative" && error.option.type == "number") reasonValue = lang.getTranslation("errors.optionInvalidReasons.numberNegative") + else if (error.reason == "number_positive" && error.option.type == "number") reasonValue = lang.getTranslation("errors.optionInvalidReasons.numberPositive") + else if (error.reason == "number_zero" && error.option.type == "number") reasonValue = lang.getTranslation("errors.optionInvalidReasons.numberZero") + else if (error.reason == "channel_not_found" && error.option.type == "channel") reasonValue = lang.getTranslation("errors.optionInvalidReasons.channelNotFound") + else if (error.reason == "user_not_found" && error.option.type == "user") reasonValue = lang.getTranslation("errors.optionInvalidReasons.userNotFound") + else if (error.reason == "role_not_found" && error.option.type == "role") reasonValue = lang.getTranslation("errors.optionInvalidReasons.roleNotFound") + else if (error.reason == "member_not_found" && error.option.type == "guildmember") reasonValue = lang.getTranslation("errors.optionInvalidReasons.memberNotFound") + else if (error.reason == "mentionable_not_found" && error.option.type == "mentionable") reasonValue = lang.getTranslation("errors.optionInvalidReasons.mentionableNotFound") + else if (error.reason == "channel_type" && error.option.type == "channel") reasonValue = lang.getTranslation("errors.optionInvalidReasons.channelType") + else if (error.reason == "not_in_guild") reasonValue = lang.getTranslation("errors.optionInvalidReasons.notInGuild") + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.optionInvalid"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslation("errors.descriptions.optionInvalid")+"\n"+reasonTitle+": `"+reasonValue+"`") + instance.addFields({name:lang.getTranslation("params.uppercase.syntax")+":",value:commandSyntax}) + }) + ) + + //ERROR UNKNOWN COMMAND + embeds.add(new api.ODEmbed("openticket:error-unknown-command")) + embeds.get("openticket:error-unknown-command").workers.add( + new api.ODWorker("openticket:error-unknown-command",0,async (instance,params) => { + const {user} = params + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.unknownCommand"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslation("errors.descriptions.unknownCommand")) + }) + ) + + //ERROR NO PERMISSIONS + embeds.add(new api.ODEmbed("openticket:error-no-permissions")) + embeds.get("openticket:error-no-permissions").workers.add( + new api.ODWorker("openticket:error-no-permissions",0,async (instance,params,source) => { + const {user,permissions} = params + + const method = getMethodFromSource(source) + + const renderedPerms = permissions.map((perm) => { + if (perm == "developer") return "- "+lang.getTranslation("errors.permissions.developer") + else if (perm == "owner") return "- "+lang.getTranslation("errors.permissions.owner") + else if (perm == "admin") return "- "+lang.getTranslation("errors.permissions.admin") + else if (perm == "moderator") return "- "+lang.getTranslation("errors.permissions.moderator") + else if (perm == "support") return "- "+lang.getTranslation("errors.permissions.support") + else if (perm == "member") return "- "+lang.getTranslation("errors.permissions.member") + else if (perm == "discord-administrator") return "- "+lang.getTranslation("errors.permissions.discord-administrator") + }).join("\n") + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.noPermissions"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslationWithParams("errors.descriptions.noPermissions",[method])) + if (renderedPerms) instance.addFields({name:lang.getTranslation("errors.descriptions.noPermissionsList"),value:renderedPerms}) + }) + ) + + //ERROR NO PERMISSIONS COOLDOWN + embeds.add(new api.ODEmbed("openticket:error-no-permissions-cooldown")) + embeds.get("openticket:error-no-permissions-cooldown").workers.add( + new api.ODWorker("openticket:error-no-permissions-cooldown",0,async (instance,params,source) => { + const {user} = params + + const method = getMethodFromSource(source) + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.noPermissions"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslationWithParams("errors.descriptions.noPermissionsCooldown",[method])) + instance.addFields({name:lang.getTranslation("params.uppercase.until")+":",value:discord.time(params.until ?? new Date(),"R")}) + }) + ) + + //ERROR NO PERMISSIONS BLACKLISTED + embeds.add(new api.ODEmbed("openticket:error-no-permissions-blacklisted")) + embeds.get("openticket:error-no-permissions-blacklisted").workers.add( + new api.ODWorker("openticket:error-no-permissions-blacklisted",0,async (instance,params,source) => { + const {user} = params + + const method = getMethodFromSource(source) + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.noPermissions"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslation("errors.descriptions.noPermissionsBlacklist")) + }) + ) + + //ERROR NO PERMISSIONS LIMITS + embeds.add(new api.ODEmbed("openticket:error-no-permissions-limits")) + embeds.get("openticket:error-no-permissions-limits").workers.add( + new api.ODWorker("openticket:error-no-permissions-limits",0,async (instance,params,source) => { + const {user,limit} = params + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.noPermissions"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + if (limit == "global") instance.setDescription(lang.getTranslation("errors.descriptions.noPermissionsLimitGlobal")) + else if (limit == "global-user") instance.setDescription(lang.getTranslation("errors.descriptions.noPermissionsLimitGlobalUser")) + else if (limit == "option") instance.setDescription(lang.getTranslation("errors.descriptions.noPermissionsLimitOption")) + else if (limit == "option-user") instance.setDescription(lang.getTranslation("errors.descriptions.noPermissionsLimitOptionUser")) + }) + ) + + //ERROR RESPONDER TIMEOUT + embeds.add(new api.ODEmbed("openticket:error-responder-timeout")) + embeds.get("openticket:error-responder-timeout").workers.add( + new api.ODWorker("openticket:error-responder-timeout",0,async (instance,params,source) => { + const {user} = params + + const method = getMethodFromSource(source) + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.internalError"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslationWithParams("errors.descriptions.internalError",[method])) + instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```Responder Timeout```"}) + instance.setFooter(lang.getTranslation("errors.descriptions.askForInfo")) + }) + ) + + //ERROR TICKET UNKNOWN + embeds.add(new api.ODEmbed("openticket:error-ticket-unknown")) + embeds.get("openticket:error-ticket-unknown").workers.add( + new api.ODWorker("openticket:error-ticket-unknown",0,async (instance,params,source) => { + const {user} = params + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.unknownTicket"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslation("errors.descriptions.unknownTicket")) + instance.setFooter(lang.getTranslation("errors.descriptions.askForInfo")) + }) + ) + + //ERROR TICKET DEPRECATED + embeds.add(new api.ODEmbed("openticket:error-ticket-deprecated")) + embeds.get("openticket:error-ticket-deprecated").workers.add( + new api.ODWorker("openticket:error-ticket-deprecated",0,async (instance,params,source) => { + const {user} = params + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.deprecatedTicket"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslation("errors.descriptions.deprecatedTicket")) + instance.setFooter(lang.getTranslation("errors.descriptions.askForInfo")) + }) + ) + + //ERROR OPTION UNKNOWN + embeds.add(new api.ODEmbed("openticket:error-option-unknown")) + embeds.get("openticket:error-option-unknown").workers.add( + new api.ODWorker("openticket:error-option-unknown",0,async (instance,params,source) => { + const {user} = params + + const renderedTicketOptions = openticket.options.getAll().map((option) => { + if (option instanceof api.ODTicketOption && option.exists("openticket:name")){ + return "- **"+option.get("openticket:name").value+":** `"+option.id.value+"`" + }else return "- `"+option.id.value+"`" + }).join("\n") + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.unknownOption"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setFooter(lang.getTranslation("errors.descriptions.askForInfo")) + instance.addFields({name:lang.getTranslation("params.uppercase.validOptions")+":",value:renderedTicketOptions}) + }) + ) + + //ERROR PANEL UNKNOWN + embeds.add(new api.ODEmbed("openticket:error-panel-unknown")) + embeds.get("openticket:error-panel-unknown").workers.add( + new api.ODWorker("openticket:error-panel-unknown",0,async (instance,params,source) => { + const {user} = params + + const renderedPanels = openticket.panels.getAll().map((panel) => { + if (panel.exists("openticket:name")){ + return "- **"+panel.get("openticket:name").value+":** `"+panel.id.value+"`" + }else return "- `"+panel.id.value+"`" + }).join("\n") + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.unknownPanel"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.addFields({name:lang.getTranslation("params.uppercase.validPanels")+":",value:renderedPanels}) + instance.setFooter(lang.getTranslation("errors.descriptions.askForInfo")) + }) + ) + + //ERROR NOT IN GUILD + embeds.add(new api.ODEmbed("openticket:error-not-in-guild")) + embeds.get("openticket:error-not-in-guild").workers.add( + new api.ODWorker("openticket:error-not-in-guild",0,async (instance,params,source) => { + const {user} = params + + const method = getMethodFromSource(source) + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.notInGuild"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslationWithParams("errors.descriptions.notInGuild",[method])) + }) + ) + + //ERROR CHANNEL RENAME + embeds.add(new api.ODEmbed("openticket:error-channel-rename")) + embeds.get("openticket:error-channel-rename").workers.add( + new api.ODWorker("openticket:error-channel-rename",0,async (instance,params,source) => { + const {channel,user,originalName,newName} = params + + const method = (source == "ticket-move" || source == "ticket-pin" || source == "ticket-rename" || source == "ticket-unpin") ? source : getMethodFromSource(source) + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.channelRename"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslation("errors.descriptions.channelRename")) + instance.setFooter(lang.getTranslationWithParams("errors.descriptions.channelRenameSource",[method])) + instance.addFields( + {name:lang.getTranslation("params.uppercase.originalName")+":",value:"```#"+originalName+"```",inline:false}, + {name:lang.getTranslation("params.uppercase.newName")+":",value:"```#"+newName+"```",inline:false} + ) + }) + ) + + //ERROR TICKET BUSY + embeds.add(new api.ODEmbed("openticket:error-ticket-busy")) + embeds.get("openticket:error-ticket-busy").workers.add( + new api.ODWorker("openticket:error-ticket-busy",0,async (instance,params,source) => { + const {user} = params + + const method = getMethodFromSource(source) + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.busy"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslationWithParams("errors.descriptions.busy",[method])) + instance.setFooter(lang.getTranslation("errors.descriptions.askForInfoResolve")) + }) + ) +} + +const helpMenuEmbeds = () => { + //HELP MENU + embeds.add(new api.ODEmbed("openticket:help-menu")) + embeds.get("openticket:help-menu").workers.add( + new api.ODWorker("openticket:help-menu",0,async (instance,params) => { + const {mode,page} = params + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("ℹ️",lang.getTranslation("actions.titles.help"))) + instance.setDescription(lang.getTranslation("actions.descriptions.helpExplanation")) + instance.setThumbnail(openticket.client.client.user.displayAvatarURL()) + + const data = await openticket.helpmenu.render(mode) + const currentData = data[page] ?? [] + instance.setFields(currentData) + }) + ) +} + +const statsEmbeds = () => { + //STATS GLOBAL + embeds.add(new api.ODEmbed("openticket:stats-global")) + embeds.get("openticket:stats-global").workers.add( + new api.ODWorker("openticket:stats-global",0,async (instance,params) => { + const {guild,channel,user} = params + + const scope = openticket.stats.get("openticket:global") + if (!scope) return + const data = await scope.render("GLOBAL",guild,channel,user) + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(scope.name) + instance.setDescription(data) + + if (openticket.permissions.hasPermissions("owner",await openticket.permissions.getPermissions(user,channel,guild))){ + //show system data when owner or developer + const systemScope = openticket.stats.get("openticket:system") + if (!systemScope) return + const systemData = await systemScope.render("GLOBAL",guild,channel,user) + instance.addFields({name:systemScope.name,value:systemData,inline:false}) + } + }) + ) + + //STATS TICKET + embeds.add(new api.ODEmbed("openticket:stats-ticket")) + embeds.get("openticket:stats-ticket").workers.add( + new api.ODWorker("openticket:stats-ticket",0,async (instance,params) => { + const {guild,channel,user,scopeData} = params + + const scope = openticket.stats.get("openticket:ticket") + const participantsScope = openticket.stats.get("openticket:participants") + if (!scope || !participantsScope) return + const data = await scope.render(scopeData.id.value,guild,channel,user) + const participantsData = await participantsScope.render(scopeData.id.value,guild,channel,user) + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(scope.name) + instance.setDescription(data) + instance.addFields({name:participantsScope.name,value:participantsData,inline:false}) + }) + ) + + //STATS USER + embeds.add(new api.ODEmbed("openticket:stats-user")) + embeds.get("openticket:stats-user").workers.add([ + new api.ODWorker("openticket:stats-user",0,async (instance,params) => { + const {guild,channel,user,scopeData} = params + + const scope = openticket.stats.get("openticket:user") + if (!scope) return + const data = await scope.render(scopeData.id,guild,channel,user) + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(scope.name) + instance.setDescription(data) + instance.setThumbnail(scopeData.displayAvatarURL()) + }), + new api.ODWorker("openticket:easter-egg",-1,async (instance,params) => { + if (!openticket.flags.exists("openticket:no-easter")) return + const easterFlag = openticket.flags.get("openticket:no-easter") + if (!easterFlag.value){ + //🥚 add easter egg 🥚 + const {user} = params + if (user.id == utilities.easterEggs.creator){ + instance.setFooter("💻 Open Ticket Developer") + }else if (utilities.easterEggs.translators.includes(user.id)){ + instance.setFooter("💬 Open Ticket Translator") + } + } + }) + ]) + + //STATS RESET + embeds.add(new api.ODEmbed("openticket:stats-reset")) + embeds.get("openticket:stats-reset").workers.add( + new api.ODWorker("openticket:stats-reset",0,async (instance,params) => { + const {user,reason} = params + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🗑️",lang.getTranslation("actions.titles.statsReset"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslation("actions.descriptions.statsReset")) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //STATS TICKET UNKNOWN + embeds.add(new api.ODEmbed("openticket:stats-ticket-unknown")) + embeds.get("openticket:stats-ticket-unknown").workers.add( + new api.ODWorker("openticket:stats-ticket-unknown",0,async (instance,params) => { + const {user,id} = params + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.unknownTicket"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.statsError",[discord.channelMention(id)])) + }) + ) +} + +const panelEmbeds = () => { + //PANEL + embeds.add(new api.ODEmbed("openticket:panel")) + embeds.get("openticket:panel").workers.add( + new api.ODWorker("openticket:panel",0,async (instance,params) => { + const {panel} = params + if (!panel.exists("openticket:embed")) return + const embedOptions = panel.get("openticket:embed").value + + instance.setColor(embedOptions.customColor ? (embedOptions.customColor as discord.ColorResolvable) : generalConfig.data.mainColor) + instance.setTitle(embedOptions.title) + if (embedOptions.thumbnail) instance.setThumbnail(embedOptions.thumbnail) + if (embedOptions.image) instance.setImage(embedOptions.image) + if (embedOptions.url) instance.setUrl(embedOptions.url) + if (embedOptions.footer) instance.setFooter(embedOptions.footer) + if (embedOptions.timestamp) instance.setTimestamp(new Date()) + + if (panel.get("openticket:describe-options-in-embed-description").value){ + //describe options in description + const text = (await import("../data/openticket/panelLoader.ts")).describePanelOptions("text",panel) + instance.setDescription(embedOptions.description+"\n\n"+text) + }else if (embedOptions.description){ + instance.setDescription(embedOptions.description) + } + + if (panel.get("openticket:enable-max-tickets-warning-embed").value && generalConfig.data.system.limits.enabled){ + instance.setDescription(instance.data.description+"\n\n*"+lang.getTranslationWithParams("actions.descriptions.ticketMessageLimit",[generalConfig.data.system.limits.userMaximum.toString()])+"*") + } + + if (panel.get("openticket:describe-options-in-embed-fields").value){ + //describe options in fields + const fields = (await import("../data/openticket/panelLoader.ts")).describePanelOptions("fields",panel) + instance.setFields(fields) + }else if(embedOptions.fields.length > 0){ + instance.setFields(embedOptions.fields) + } + }) + ) +} + +const ticketEmbeds = () => { + //TICKET CREATED + embeds.add(new api.ODEmbed("openticket:ticket-created")) + embeds.get("openticket:ticket-created").workers.add( + new api.ODWorker("openticket:ticket-created",0,async (instance,params,source) => { + const {user} = params + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🎫",lang.getTranslation("actions.titles.created"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setTimestamp(new Date()) + instance.setDescription(lang.getTranslation("actions.descriptions.create")) + }) + ) + + //TICKET CREATED DM + embeds.add(new api.ODEmbed("openticket:ticket-created-dm")) + embeds.get("openticket:ticket-created-dm").workers.add( + new api.ODWorker("openticket:ticket-created-dm",0,async (instance,params,source) => { + const {user,ticket} = params + const embedOptions = ticket.option.get("openticket:dm-message-embed").value + + instance.setColor(embedOptions.customColor ? (embedOptions.customColor as discord.ColorResolvable) : generalConfig.data.mainColor) + instance.setTitle(embedOptions.title) + if (embedOptions.thumbnail) instance.setThumbnail(embedOptions.thumbnail) + if (embedOptions.image) instance.setImage(embedOptions.image) + if (embedOptions.timestamp) instance.setTimestamp(new Date()) + if (embedOptions.description) instance.setDescription(embedOptions.description) + if (embedOptions.fields) instance.setFields(embedOptions.fields) + + if (ticket.get("openticket:closed").value && ticket.get("openticket:autodelete-enabled").value) instance.setFooter("⏱️ "+lang.getTranslationWithParams("actions.descriptions.ticketMessageAutodelete",[ticket.option.get("openticket:autodelete-days").value.toString()])) + else if (!ticket.get("openticket:closed").value && ticket.get("openticket:autoclose-enabled").value) instance.setFooter("⏱️ "+lang.getTranslationWithParams("actions.descriptions.ticketMessageAutoclose",[ticket.option.get("openticket:autoclose-hours").value.toString()])) + }) + ) + + //TICKET CREATED LOGS + embeds.add(new api.ODEmbed("openticket:ticket-created-logs")) + embeds.get("openticket:ticket-created-logs").workers.add( + new api.ODWorker("openticket:ticket-created-logs",0,async (instance,params,source) => { + const {user,ticket} = params + + const method = (source == "panel-button" || source == "panel-dropdown") ? lang.getTranslation("params.uppercase.panel") : (source == "slash" || source == "text") ? lang.getTranslation("params.uppercase.command") : lang.getTranslation("params.uppercase.system") + const blacklisted = openticket.blacklist.exists(user.id) ? lang.getTranslation("params.uppercase.true") : lang.getTranslation("params.uppercase.false") + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🎫",lang.getTranslation("actions.titles.created"))) + instance.setThumbnail(user.displayAvatarURL()) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setTimestamp(new Date()) + instance.setDescription(lang.getTranslationWithParams("actions.logs.createLog",[discord.userMention(user.id)])) + instance.addFields( + {name:lang.getTranslation("params.uppercase.type")+":",value:"```"+ticket.option.get("openticket:name").value+"```",inline:false}, + {name:lang.getTranslation("params.uppercase.method")+":",value:"```"+method+"```",inline:true}, + {name:lang.getTranslation("params.uppercase.blacklisted")+":",value:"```"+blacklisted+"```", inline:true} + ) + }) + ) + + //TICKET MESSAGE + embeds.add(new api.ODEmbed("openticket:ticket-message")) + embeds.get("openticket:ticket-message").workers.add( + new api.ODWorker("openticket:ticket-message",0,async (instance,params,source) => { + const {user,ticket} = params + const embedOptions = ticket.option.get("openticket:ticket-message-embed").value + + instance.setColor(embedOptions.customColor ? (embedOptions.customColor as discord.ColorResolvable) : generalConfig.data.mainColor) + if (embedOptions.title) instance.setTitle(embedOptions.title) + if (embedOptions.thumbnail) instance.setThumbnail(embedOptions.thumbnail) + if (embedOptions.image) instance.setImage(embedOptions.image) + if (embedOptions.timestamp) instance.setTimestamp(new Date()) + if (embedOptions.description) instance.setDescription(embedOptions.description) + + if (ticket.option.get("openticket:questions").value.length > 0){ + const answers = ticket.get("openticket:answers").value + answers.forEach((answer) => { + if (!answer.value || answer.value.length == 0) return + if (generalConfig.data.system.questionFieldsInCodeBlock) instance.addFields({name:answer.name,value:"```"+answer.value+"```",inline:false}) + else instance.addFields({name:answer.name,value:answer.value,inline:false}) + }) + }else if (embedOptions.fields){ + instance.setFields(embedOptions.fields) + } + + if (ticket.get("openticket:closed").value && ticket.get("openticket:autodelete-enabled").value) instance.setFooter("⏱️ "+lang.getTranslationWithParams("actions.descriptions.ticketMessageAutodelete",[ticket.option.get("openticket:autodelete-days").value.toString()])) + else if (!ticket.get("openticket:closed").value && ticket.get("openticket:autoclose-enabled").value) instance.setFooter("⏱️ "+lang.getTranslationWithParams("actions.descriptions.ticketMessageAutoclose",[ticket.option.get("openticket:autoclose-hours").value.toString()])) + + if (ticket.get("openticket:claimed").value){ + const claimUser = await openticket.tickets.getTicketUser(ticket,"claimer") + if (!claimUser) return + instance.setAuthor(lang.getTranslationWithParams("params.uppercase.claimedBy",[claimUser.displayName]),claimUser.displayAvatarURL()) + } + }) + ) + + //TICKET CLOSED + embeds.add(new api.ODEmbed("openticket:close-message")) + embeds.get("openticket:close-message").workers.add( + new api.ODWorker("openticket:close-message",0,async (instance,params,source) => { + const {user,ticket,reason} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🔒",lang.getTranslation("actions.titles.close"))) + instance.setDescription(lang.getTranslation("actions.descriptions.close")) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //TICKET REOPENED + embeds.add(new api.ODEmbed("openticket:reopen-message")) + embeds.get("openticket:reopen-message").workers.add( + new api.ODWorker("openticket:reopen-message",0,async (instance,params,source) => { + const {user,ticket,reason} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🔓",lang.getTranslation("actions.titles.reopen"))) + instance.setDescription(lang.getTranslation("actions.descriptions.reopen")) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //TICKET DELETED + embeds.add(new api.ODEmbed("openticket:delete-message")) + embeds.get("openticket:delete-message").workers.add( + new api.ODWorker("openticket:delete-message",0,async (instance,params,source) => { + const {user,ticket,reason} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🗑️",lang.getTranslation("actions.titles.delete"))) + instance.setDescription(lang.getTranslation("actions.descriptions.delete")) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //TICKET CLAIMED + embeds.add(new api.ODEmbed("openticket:claim-message")) + embeds.get("openticket:claim-message").workers.add( + new api.ODWorker("openticket:claim-message",0,async (instance,params,source) => { + const {user,ticket,reason} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("👋",lang.getTranslation("actions.titles.claim"))) + instance.setDescription(lang.getTranslation("actions.descriptions.claim")) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //TICKET UNCLAIMED + embeds.add(new api.ODEmbed("openticket:unclaim-message")) + embeds.get("openticket:unclaim-message").workers.add( + new api.ODWorker("openticket:unclaim-message",0,async (instance,params,source) => { + const {user,ticket,reason} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("↩️",lang.getTranslation("actions.titles.unclaim"))) + instance.setDescription(lang.getTranslation("actions.descriptions.unclaim")) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //TICKET PINNED + embeds.add(new api.ODEmbed("openticket:pin-message")) + embeds.get("openticket:pin-message").workers.add( + new api.ODWorker("openticket:pin-message",0,async (instance,params,source) => { + const {user,ticket,reason} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.pin"))) + instance.setDescription(lang.getTranslation("actions.descriptions.pin")) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //TICKET UNPINNED + embeds.add(new api.ODEmbed("openticket:unpin-message")) + embeds.get("openticket:unpin-message").workers.add( + new api.ODWorker("openticket:unpin-message",0,async (instance,params,source) => { + const {user,ticket,reason} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.unpin"))) + instance.setDescription(lang.getTranslation("actions.descriptions.unpin")) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //TICKET RENAMED + embeds.add(new api.ODEmbed("openticket:rename-message")) + embeds.get("openticket:rename-message").workers.add( + new api.ODWorker("openticket:rename-message",0,async (instance,params,source) => { + const {user,ticket,reason,data} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🔄",lang.getTranslation("actions.titles.rename"))) + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.rename",["`#"+data+"`"])) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //TICKET MOVED + embeds.add(new api.ODEmbed("openticket:move-message")) + embeds.get("openticket:move-message").workers.add( + new api.ODWorker("openticket:move-message",0,async (instance,params,source) => { + const {user,ticket,reason,data} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🔀",lang.getTranslation("actions.titles.move"))) + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.move",["`"+data.get("openticket:name").value+"`"])) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //TICKET USER ADDED + embeds.add(new api.ODEmbed("openticket:add-message")) + embeds.get("openticket:add-message").workers.add( + new api.ODWorker("openticket:add-message",0,async (instance,params,source) => { + const {user,ticket,reason,data} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("👤",lang.getTranslation("actions.titles.add"))) + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.add",[discord.userMention(data.id)])) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //TICKET USER REMOVED + embeds.add(new api.ODEmbed("openticket:remove-message")) + embeds.get("openticket:remove-message").workers.add( + new api.ODWorker("openticket:remove-message",0,async (instance,params,source) => { + const {user,ticket,reason,data} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("👤",lang.getTranslation("actions.titles.remove"))) + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.remove",[discord.userMention(data.id)])) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //TICKET ACTION DM + embeds.add(new api.ODEmbed("openticket:ticket-action-dm")) + embeds.get("openticket:ticket-action-dm").workers.add( + new api.ODWorker("openticket:ticket-action-dm",0,async (instance,params,source) => { + const {user,mode,ticket,reason,additionalData} = params + const channel = await openticket.tickets.getTicketChannel(ticket) + + instance.setColor(generalConfig.data.mainColor) + instance.setTimestamp(new Date()) + instance.addFields({name:lang.getTranslation("params.uppercase.ticket")+":",value:"```#"+(channel ? channel.name : "")+"```",inline:false}) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```",inline:false}) + + if (mode == "close"){ + instance.setTitle(utilities.emojiTitle("🔒",lang.getTranslation("actions.titles.close"))) + instance.setDescription(lang.getTranslation("actions.logs.closeDm")) + }else if (mode == "reopen"){ + instance.setTitle(utilities.emojiTitle("🔓",lang.getTranslation("actions.titles.reopen"))) + instance.setDescription(lang.getTranslation("actions.logs.reopenDm")) + }else if (mode == "delete"){ + instance.setTitle(utilities.emojiTitle("🗑️",lang.getTranslation("actions.titles.delete"))) + instance.setDescription(lang.getTranslation("actions.logs.deleteDm")) + }else if (mode == "claim"){ + instance.setTitle(utilities.emojiTitle("👋",lang.getTranslation("actions.titles.claim"))) + instance.setDescription(lang.getTranslation("actions.logs.claimDm")) + }else if (mode == "unclaim"){ + instance.setTitle(utilities.emojiTitle("↩️",lang.getTranslation("actions.titles.unclaim"))) + instance.setDescription(lang.getTranslation("actions.logs.unclaimDm")) + }else if (mode == "pin"){ + instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.pin"))) + instance.setDescription(lang.getTranslation("actions.logs.pinDm")) + }else if (mode == "unpin"){ + instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.unpin"))) + instance.setDescription(lang.getTranslation("actions.logs.unpinDm")) + }else if (mode == "rename"){ + instance.setTitle(utilities.emojiTitle("🔄",lang.getTranslation("actions.titles.rename"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.renameDm",["`#"+(typeof additionalData == "string" ? additionalData : "")+"`"])) + }else if (mode == "move"){ + instance.setTitle(utilities.emojiTitle("🔀",lang.getTranslation("actions.titles.move"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.moveDm",["`"+(additionalData instanceof api.ODTicketOption ? additionalData.get("openticket:name").value : "")+"`"])) + }else if (mode == "add"){ + instance.setTitle(utilities.emojiTitle("👤",lang.getTranslation("actions.titles.add"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.addDm",[(additionalData instanceof discord.User ? discord.userMention(additionalData.id) : "")])) + }else if (mode == "remove"){ + instance.setTitle(utilities.emojiTitle("👤",lang.getTranslation("actions.titles.remove"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.removeDm",[(additionalData instanceof discord.User ? discord.userMention(additionalData.id) : "")])) + } + }) + ) + + //TICKET ACTION LOGS + embeds.add(new api.ODEmbed("openticket:ticket-action-logs")) + embeds.get("openticket:ticket-action-logs").workers.add( + new api.ODWorker("openticket:ticket-action-logs",0,async (instance,params,source) => { + const {user,mode,ticket,reason,additionalData} = params + const channel = await openticket.tickets.getTicketChannel(ticket) + + instance.setColor(generalConfig.data.mainColor) + instance.setThumbnail(user.displayAvatarURL()) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setTimestamp(new Date()) + instance.addFields( + {name:lang.getTranslation("params.uppercase.ticket")+":",value:"```#"+(channel ? channel.name : "")+"```",inline:false}, + //TODO TRANSLATION!!! + {name:"Option"+":",value:"```"+(ticket.option.get("openticket:name").value)+"```",inline:false}, + ) + if (reason) instance.addFields({name:"Reason:",value:"```"+reason+"```",inline:false}) + + if (mode == "close"){ + instance.setTitle(utilities.emojiTitle("🔒",lang.getTranslation("actions.titles.close"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.closeLog",[discord.userMention(user.id)])) + }else if (mode == "reopen"){ + instance.setTitle(utilities.emojiTitle("🔓",lang.getTranslation("actions.titles.reopen"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.reopenLog",[discord.userMention(user.id)])) + }else if (mode == "delete"){ + instance.setTitle(utilities.emojiTitle("🗑️",lang.getTranslation("actions.titles.delete"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.deleteLog",[discord.userMention(user.id)])) + }else if (mode == "claim"){ + instance.setTitle(utilities.emojiTitle("👋",lang.getTranslation("actions.titles.claim"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.claimLog",[discord.userMention(user.id)])) + }else if (mode == "unclaim"){ + instance.setTitle(utilities.emojiTitle("↩️",lang.getTranslation("actions.titles.unclaim"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.unclaimLog",[discord.userMention(user.id)])) + }else if (mode == "pin"){ + instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.pin"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.pinLog",[discord.userMention(user.id)])) + }else if (mode == "unpin"){ + instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.unpin"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.unpinLog",[discord.userMention(user.id)])) + }else if (mode == "rename"){ + instance.setTitle(utilities.emojiTitle("🔄",lang.getTranslation("actions.titles.rename"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.renameLog",["`#"+(typeof additionalData == "string" ? additionalData : "")+"`",discord.userMention(user.id)])) + }else if (mode == "move"){ + instance.setTitle(utilities.emojiTitle("🔀",lang.getTranslation("actions.titles.move"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.moveLog",["`"+(additionalData instanceof api.ODTicketOption ? additionalData.get("openticket:name").value : "")+"`",discord.userMention(user.id)])) + }else if (mode == "add"){ + instance.setTitle(utilities.emojiTitle("👤",lang.getTranslation("actions.titles.add"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.addLog",[(additionalData instanceof discord.User ? discord.userMention(additionalData.id) : ""),discord.userMention(user.id)])) + }else if (mode == "remove"){ + instance.setTitle(utilities.emojiTitle("👤",lang.getTranslation("actions.titles.remove"))) + instance.setDescription(lang.getTranslationWithParams("actions.logs.removeLog",[(additionalData instanceof discord.User ? discord.userMention(additionalData.id) : ""),discord.userMention(user.id)])) + } + }) + ) +} + +const blacklistEmbeds = () => { + //BLACKLIST VIEW + embeds.add(new api.ODEmbed("openticket:blacklist-view")) + embeds.get("openticket:blacklist-view").workers.add( + new api.ODWorker("openticket:blacklist-view",0,async (instance,params,source) => { + const {user} = params + + const renderedUsers: string[] = [] + openticket.blacklist.getAll().forEach((blacklist) => renderedUsers.push(discord.userMention(blacklist.id.value)+" - "+(blacklist.reason ? blacklist.reason : "/"))) + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🚫",lang.getTranslation("actions.titles.blacklistView"))) + + if (renderedUsers.length > 0) instance.setDescription(renderedUsers.join("\n")) + else{ + instance.setDescription("*"+lang.getTranslation("actions.descriptions.blacklistViewEmpty")+"*") + instance.setFooter(lang.getTranslation("actions.descriptions.blacklistViewTip")) + } + }) + ) + //BLACKLIST GET + embeds.add(new api.ODEmbed("openticket:blacklist-get")) + embeds.get("openticket:blacklist-get").workers.add( + new api.ODWorker("openticket:blacklist-get",0,async (instance,params,source) => { + const {user,data} = params + const blacklist = openticket.blacklist.get(data.id) + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🚫",lang.getTranslation("actions.titles.blacklistGet"))) + + if (blacklist){ + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.blacklistGetSuccess",[discord.userMention(data.id)])) + if (blacklist.reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+blacklist.reason+"```"}) + + }else instance.setDescription("*"+lang.getTranslationWithParams("actions.descriptions.blacklistGetEmpty",[discord.userMention(data.id)])+"*") + }) + ) + + //BLACKLIST ADD + embeds.add(new api.ODEmbed("openticket:blacklist-add")) + embeds.get("openticket:blacklist-add").workers.add( + new api.ODWorker("openticket:blacklist-add",0,async (instance,params,source) => { + const {user,data,reason} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🚫",lang.getTranslation("actions.titles.blacklistAdd"))) + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.blacklistAdd",[discord.userMention(data.id)])) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //BLACKLIST REMOVE + embeds.add(new api.ODEmbed("openticket:blacklist-remove")) + embeds.get("openticket:blacklist-remove").workers.add( + new api.ODWorker("openticket:blacklist-remove",0,async (instance,params,source) => { + const {user,data,reason} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🆓",lang.getTranslation("actions.titles.blacklistRemove"))) + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.blacklistRemove",[discord.userMention(data.id)])) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //BLACKLIST DM + embeds.add(new api.ODEmbed("openticket:blacklist-dm")) + embeds.get("openticket:blacklist-dm").workers.add( + new api.ODWorker("openticket:blacklist-dm",0,async (instance,params,source) => { + const {user,mode,data,reason} = params + + const title = (mode == "add") ? lang.getTranslation("actions.titles.blacklistAddDm") : lang.getTranslation("actions.titles.blacklistRemoveDm") + const text = (mode == "add") ? lang.getTranslation("actions.logs.blacklistAddDm") : lang.getTranslation("actions.logs.blacklistRemoveDm") + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle((mode == "add") ? "🚫" : "🆓",title)) + instance.setTimestamp(new Date()) + instance.setDescription(text) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```",inline:false}) + }) + ) + + //BLACKLIST LOGS + embeds.add(new api.ODEmbed("openticket:blacklist-logs")) + embeds.get("openticket:blacklist-logs").workers.add( + new api.ODWorker("openticket:blacklist-logs",0,async (instance,params,source) => { + const {user,mode,data,reason} = params + + const title = (mode == "add") ? lang.getTranslation("actions.titles.blacklistAdd") : lang.getTranslation("actions.titles.blacklistRemove") + const text = (mode == "add") ? lang.getTranslationWithParams("actions.logs.blacklistAddLog",[discord.userMention(data.id),discord.userMention(user.id)]) : lang.getTranslationWithParams("actions.logs.blacklistRemoveLog",[discord.userMention(data.id),discord.userMention(user.id)]) + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle((mode == "add") ? "🚫" : "🆓",title)) + instance.setThumbnail(data.displayAvatarURL()) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setTimestamp(new Date()) + instance.setDescription(text) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```",inline:false}) + }) + ) +} + +const transcriptEmbeds = () => { + //TRANSCRIPT TEXT READY + embeds.add(new api.ODEmbed("openticket:transcript-text-ready")) + embeds.get("openticket:transcript-text-ready").workers.add( + new api.ODWorker("openticket:transcript-text-ready",0,async (instance,params,source) => { + const {guild,channel,user,ticket,compiler} = params + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("📄",lang.getTranslation("transcripts.success.ready"))) + instance.setTimestamp(new Date()) + instance.addFields({name:lang.getTranslation("params.uppercase.ticket")+":",value:"#"+channel.name,inline:false}) + + const creatorId = ticket.get("openticket:opened-by").value + if (creatorId){ + try{ + const creator = await channel.client.users.fetch(creatorId) + instance.addFields({name:lang.getTranslation("params.uppercase.creator")+":",value:creator.username+" ("+discord.userMention(creator.id)+")"}) + instance.setThumbnail(creator.displayAvatarURL()) + }catch{} + } + + if (source == "channel") instance.setDescription(lang.getTranslationWithParams("transcripts.success.createdChannel",[lang.getTranslation("params.lowercase.text")])) + else if (source == "creator-dm") instance.setDescription(lang.getTranslationWithParams("transcripts.success.createdCreator",[lang.getTranslation("params.lowercase.text")])) + else if (source == "participant-dm") instance.setDescription(lang.getTranslationWithParams("transcripts.success.createdParticipant",[lang.getTranslation("params.lowercase.text")])) + else if (source == "active-admin-dm") instance.setDescription(lang.getTranslationWithParams("transcripts.success.createdActiveAdmin",[lang.getTranslation("params.lowercase.text")])) + else if (source == "every-admin-dm") instance.setDescription(lang.getTranslationWithParams("transcripts.success.createdEveryAdmin",[lang.getTranslation("params.lowercase.text")])) + else instance.setDescription(lang.getTranslationWithParams("transcripts.success.createdOther",[lang.getTranslation("params.lowercase.text")])) + }) + ) + + //TRANSCRIPT HTML READY + embeds.add(new api.ODEmbed("openticket:transcript-html-ready")) + embeds.get("openticket:transcript-html-ready").workers.add( + new api.ODWorker("openticket:transcript-html-ready",0,async (instance,params,source) => { + const {guild,channel,user,ticket,compiler,result} = params + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("📄",lang.getTranslation("transcripts.success.ready"))) + instance.setTimestamp(new Date()) + instance.addFields({name:lang.getTranslation("params.uppercase.ticket")+":",value:"#"+channel.name,inline:false}) + if (result.data) instance.setUrl(result.data.url) + + const creatorId = ticket.get("openticket:opened-by").value + if (creatorId){ + try{ + const creator = await channel.client.users.fetch(creatorId) + instance.addFields({name:lang.getTranslation("params.uppercase.creator")+":",value:creator.username+" ("+discord.userMention(creator.id)+")"}) + instance.setThumbnail(creator.displayAvatarURL()) + }catch{} + } + + if (source == "channel") instance.setDescription(lang.getTranslationWithParams("transcripts.success.createdChannel",[lang.getTranslation("params.lowercase.html")])) + else if (source == "creator-dm") instance.setDescription(lang.getTranslationWithParams("transcripts.success.createdCreator",[lang.getTranslation("params.lowercase.html")])) + else if (source == "participant-dm") instance.setDescription(lang.getTranslationWithParams("transcripts.success.createdParticipant",[lang.getTranslation("params.lowercase.html")])) + else if (source == "active-admin-dm") instance.setDescription(lang.getTranslationWithParams("transcripts.success.createdActiveAdmin",[lang.getTranslation("params.lowercase.html")])) + else if (source == "every-admin-dm") instance.setDescription(lang.getTranslationWithParams("transcripts.success.createdEveryAdmin",[lang.getTranslation("params.lowercase.html")])) + else instance.setDescription(lang.getTranslationWithParams("transcripts.success.createdOther",[lang.getTranslation("params.lowercase.html")])) + }) + ) + + //TRANSCRIPT HTML PROGRESS + embeds.add(new api.ODEmbed("openticket:transcript-html-progress")) + embeds.get("openticket:transcript-html-progress").workers.add( + new api.ODWorker("openticket:transcript-html-progress",0,async (instance,params,source) => { + const {guild,channel,user,ticket,compiler,remaining} = params + + const remainingDate = new Date(new Date().getTime()+remaining) + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("📄",lang.getTranslation("transcripts.success.ready"))) + instance.setDescription(lang.getTranslation("transcripts.success.htmlProgress")) + instance.setTimestamp(new Date()) + instance.addFields({name:lang.getTranslation("params.uppercase.remaining")+":",value:discord.time(remainingDate,"R"),inline:false}) + instance.addFields({name:lang.getTranslation("params.uppercase.ticket")+":",value:"#"+channel.name,inline:false}) + + const creatorId = ticket.get("openticket:opened-by").value + if (creatorId){ + try{ + const creator = await channel.client.users.fetch(creatorId) + instance.addFields({name:lang.getTranslation("params.uppercase.creator")+":",value:creator.username+" ("+discord.userMention(creator.id)+")"}) + instance.setThumbnail(creator.displayAvatarURL()) + }catch{} + } + }) + ) + + //TRANSCRIPT ERROR + embeds.add(new api.ODEmbed("openticket:transcript-error")) + embeds.get("openticket:transcript-error").workers.add( + new api.ODWorker("openticket:transcript-error",0,async (instance,params,source) => { + const {guild,channel,user,ticket,compiler,reason} = params + + instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("❌","Transcript Error")) //TODO TRANSLATION!!! + instance.setTimestamp(new Date()) + instance.setDescription(lang.getTranslation("transcripts.errors.error")) + instance.setFooter(lang.getTranslation("errors.descriptions.askForInfo")) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```",inline:false}) + }) + ) +} + +const roleEmbeds = () => { + //REACTION ROLE + embeds.add(new api.ODEmbed("openticket:reaction-role")) + embeds.get("openticket:reaction-role").workers.add( + new api.ODWorker("openticket:reaction-role",0,async (instance,params,source) => { + const {guild,user,role,result} = params + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🎨",lang.getTranslation("actions.titles.roles"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setTimestamp(new Date()) + + const newResult = result.filter((r) => r.action != null).sort((a,b) => { + if (a.action == "added" && b.action == "removed") return -1 + else if (a.action == "removed" && b.action == "added") return 1 + else return 0 + }).map((r) => { + return (r.action == "added") ? "🟢 "+lang.getTranslation("params.uppercase.added")+" "+discord.roleMention(r.role.id) : "🔴 "+lang.getTranslation("params.uppercase.removed")+" "+discord.roleMention(r.role.id) + }) + + if (newResult.length > 0) instance.setDescription(newResult.join("\n")) + else instance.setDescription(lang.getTranslation("actions.descriptions.rolesEmpty")) + }) + ) +} + +const clearEmbeds = () => { + //CLEAR VERIFY MESSAGE + embeds.add(new api.ODEmbed("openticket:clear-verify-message")) + embeds.get("openticket:clear-verify-message").workers.add( + new api.ODWorker("openticket:clear-verify-message",0,async (instance,params,source) => { + const {guild,channel,user,filter,list} = params + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("⚠️","Clear Tickets")) //TODO TRANSLATION!!! + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setTimestamp(new Date()) + instance.setDescription(lang.getTranslation("actions.descriptions.clearVerify")) + instance.addFields( + {name:lang.getTranslation("params.uppercase.filter")+":",value:filter,inline:false}, + {name:lang.getTranslation("params.uppercase.tickets")+":",value:"`"+list.join("`\n`")+"`",inline:false} + ) + }) + ) + + //CLEAR MESSAGE + embeds.add(new api.ODEmbed("openticket:clear-message")) + embeds.get("openticket:clear-message").workers.add( + new api.ODWorker("openticket:clear-message",0,async (instance,params,source) => { + const {guild,channel,user,filter,list} = params + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("⚠️",lang.getTranslation("actions.titles.clear"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setTimestamp(new Date()) + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.clearReady",[list.length.toString()])) + instance.addFields( + {name:lang.getTranslation("params.uppercase.tickets")+":",value:"`"+list.join("`\n`")+"`",inline:false} + ) + }) + ) + + //CLEAR LOGS + embeds.add(new api.ODEmbed("openticket:clear-logs")) + embeds.get("openticket:clear-logs").workers.add( + new api.ODWorker("openticket:clear-logs",0,async (instance,params,source) => { + const {guild,channel,user,filter,list} = params + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("⚠️",lang.getTranslation("actions.titles.clear"))) + instance.setThumbnail(user.displayAvatarURL()) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setTimestamp(new Date()) + instance.setDescription(lang.getTranslationWithParams("actions.logs.clearLog",[list.length.toString(),discord.userMention(user.id)])) + instance.addFields( + {name:lang.getTranslation("params.uppercase.tickets")+":",value:"`"+list.join("`\n`")+"`",inline:false} + ) + }) + ) +} + +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE +//TRANSLATION FINISHED UNTIL HERE + +const autoEmbeds = () => { + //AUTOCLOSE MESSAGE + embeds.add(new api.ODEmbed("openticket:autoclose-message")) + embeds.get("openticket:autoclose-message").workers.add( + new api.ODWorker("openticket:autoclose-message",0,async (instance,params,source) => { + const {user,ticket} = params + const hours: number = ticket.get("openticket:autoclose-hours").value + const description = (source == "leave") ? lang.getTranslation("actions.descriptions.autocloseLeave") : lang.getTranslationWithParams("actions.descriptions.autocloseTimeout",[hours.toString()]) + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("⏱️",lang.getTranslation("actions.titles.autoclose"))) + instance.setDescription(description) + instance.setTimestamp(new Date()) + }) + ) + + //AUTODELETE MESSAGE + embeds.add(new api.ODEmbed("openticket:autodelete-message")) + embeds.get("openticket:autodelete-message").workers.add( + new api.ODWorker("openticket:autodelete-message",0,async (instance,params,source) => { + const {user,ticket} = params + const days: number = ticket.get("openticket:autodelete-days").value + const description = (source == "leave") ? lang.getTranslation("actions.descriptions.autodeleteLeave") : lang.getTranslationWithParams("actions.descriptions.autodeleteTimeout",[days.toString()]) + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("⏱️",lang.getTranslation("actions.titles.autodelete"))) + instance.setDescription(description) + instance.setTimestamp(new Date()) + }) + ) + + //AUTOCLOSE ENABLE + embeds.add(new api.ODEmbed("openticket:autoclose-enable")) + embeds.get("openticket:autoclose-enable").workers.add( + new api.ODWorker("openticket:autoclose-enable",0,async (instance,params,source) => { + const {user,ticket,reason,time} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("⏱️",lang.getTranslation("actions.titles.autocloseEnabled"))) + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.autocloseEnabled",[time.toString()])) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //AUTODELETE ENABLE + embeds.add(new api.ODEmbed("openticket:autodelete-enable")) + embeds.get("openticket:autodelete-enable").workers.add( + new api.ODWorker("openticket:autodelete-enable",0,async (instance,params,source) => { + const {user,ticket,reason,time} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("⏱️",lang.getTranslation("actions.titles.autodeleteEnabled"))) + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.autodeleteEnabled",[time.toString()])) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //AUTOCLOSE DISABLE + embeds.add(new api.ODEmbed("openticket:autoclose-disable")) + embeds.get("openticket:autoclose-disable").workers.add( + new api.ODWorker("openticket:autoclose-disable",0,async (instance,params,source) => { + const {user,ticket,reason} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("⏱️",lang.getTranslation("actions.titles.autocloseDisabled"))) + instance.setDescription(lang.getTranslation("actions.descriptions.autocloseDisabled")) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //AUTODELETE DISABLE + embeds.add(new api.ODEmbed("openticket:autodelete-disable")) + embeds.get("openticket:autodelete-disable").workers.add( + new api.ODWorker("openticket:autodelete-disable",0,async (instance,params,source) => { + const {user,ticket,reason} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("⏱️",lang.getTranslation("actions.titles.autodeleteDisabled"))) + instance.setDescription(lang.getTranslation("actions.descriptions.autodeleteDisabled")) + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) +} \ No newline at end of file diff --git a/src/builders/files.ts b/src/builders/files.ts new file mode 100644 index 0000000..ae00fca --- /dev/null +++ b/src/builders/files.ts @@ -0,0 +1,45 @@ +/////////////////////////////////////// +//FILE BUILDERS +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const files = openticket.builders.files +const lang = openticket.languages +const transcriptConfig = openticket.configs.get("openticket:transcripts") + +export const registerAllFiles = async () => { + transcriptFiles() +} + + +const transcriptFiles = () => { + //TEXT TRANSCRIPT + files.add(new api.ODFile("openticket:text-transcript")) + files.get("openticket:text-transcript").workers.add( + new api.ODWorker("openticket:text-transcript",0,async (instance,params,source) => { + const {guild,channel,user,ticket,compiler,result} = params + + const fileMode = transcriptConfig.data.textTranscriptStyle.fileMode + const customName = transcriptConfig.data.textTranscriptStyle.customFileName + + const creatorId = ticket.get("openticket:opened-by").value ?? "unknown-creator-id" + const creator = (await openticket.tickets.getTicketUser(ticket,"creator")) + + if (fileMode == "custom") instance.setName(customName.split(".")[0]+".txt") + else if (fileMode == "user-id") instance.setName(creatorId+".txt") + else if (fileMode == "user-name") instance.setName((creator ? creator.username : "unknown-creator-name")+".txt") + else if (fileMode == "channel-id") instance.setName(channel.id+".txt") + else if (fileMode == "channel-name") instance.setName(channel.name+".txt") + else instance.setName("transcript.txt") + + instance.setDescription(lang.getTranslation("transcripts.success.textFileDescription")) + + if (compiler.id.value != "openticket:text-compiler" || !result.data || typeof result.data.contents != "string"){ + instance.setContents("") + return + } + instance.setContents(result.data.contents) + }) + ) +} \ No newline at end of file diff --git a/src/builders/messages.ts b/src/builders/messages.ts new file mode 100644 index 0000000..25dd7a3 --- /dev/null +++ b/src/builders/messages.ts @@ -0,0 +1,1055 @@ +/////////////////////////////////////// +//MESSAGE BUILDERS +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const messages = openticket.builders.messages +const buttons = openticket.builders.buttons +const dropdowns = openticket.builders.dropdowns +const files = openticket.builders.files +const embeds = openticket.builders.embeds +const lang = openticket.languages +const generalConfig = openticket.configs.get("openticket:general") + +export const registerAllMessages = async () => { + verifyBarMessages() + errorMessages() + helpMenuMessages() + statsMessages() + panelMessages() + ticketMessages() + blacklistMessages() + transcriptMessages() + roleMessages() + clearMessages() + autoMessages() +} + +const verifyBarMessages = () => { + //VERIFYBAR TICKET MESSAGE + messages.add(new api.ODMessage("openticket:verifybar-ticket-message")) + messages.get("openticket:verifybar-ticket-message").workers.add( + new api.ODWorker("openticket:verifybar-ticket-message",0,async (instance,params,source) => { + const {guild,channel,user,verifybar} = params + if (!guild){ + instance.setContent("ODError: Not In Guild => `openticket:verifybar-ticket-message`") + return + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket){ + instance.setContent("ODError: Unknown Ticket => `openticket:verifybar-ticket-message`") + return + } + const option = ticket.option + + //add pings + const pingOptions = option.get("openticket:ticket-message-ping").value + const pings: string[] = [discord.userMention(user.id)] + if (pingOptions["@everyone"]) pings.push("@everyone") + if (pingOptions["@here"]) pings.push("@here") + pingOptions.custom.forEach((ping) => pings.push(discord.roleMention(ping))) + const pingText = (pings.length > 0) ? pings.join(" ")+"\n" : "" + + //add text + const text = option.get("openticket:ticket-message-text").value + if (text !== "") instance.setContent(pingText+text) + else instance.setContent(pingText) + + //add embed + if (option.get("openticket:ticket-message-embed").value.enabled) instance.addEmbed(await embeds.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + + //add verifybar components + if (verifybar.id.value == "openticket:claim-ticket-ticket-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + + }else if (verifybar.id.value == "openticket:unclaim-ticket-ticket-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + + }else if (verifybar.id.value == "openticket:pin-ticket-ticket-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + + }else if (verifybar.id.value == "openticket:unpin-ticket-ticket-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + + }else if (verifybar.id.value == "openticket:close-ticket-ticket-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + + }else if (verifybar.id.value == "openticket:reopen-ticket-ticket-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + + }else if (verifybar.id.value == "openticket:delete-ticket-ticket-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + if (generalConfig.data.system.enableDeleteWithoutTranscript) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"no-transcript",customEmoji:"📄",customLabel:"Without Transcript",customColor:"red"})) + } + }) + ) + + //TICKET CLOSED + messages.add(new api.ODMessage("openticket:verifybar-close-message")) + messages.get("openticket:verifybar-close-message").workers.add( + new api.ODWorker("openticket:verifybar-close-message",0,async (instance,params,source) => { + const {guild,channel,user,verifybar,originalMessage} = params + if (!guild){ + instance.setContent("ODError: Not In Guild => `openticket:verifybar-close-message`") + return + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket){ + instance.setContent("ODError: Unknown Ticket => `openticket:verifybar-close-message`") + return + } + + const rawReason = (originalMessage.embeds[0] && originalMessage.embeds[0].fields[0]) ? originalMessage.embeds[0].fields[0].value : null + const reason = (rawReason == null) ? null : rawReason.substring(3,rawReason.length-3) + + //add embed + instance.addEmbed(await embeds.getSafe("openticket:close-message").build("other",{guild,channel,user,ticket,reason})) + + //add verifybar components + if (verifybar.id.value == "openticket:reopen-ticket-close-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + + }else if (verifybar.id.value == "openticket:delete-ticket-close-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + if (generalConfig.data.system.enableDeleteWithoutTranscript) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"no-transcript",customEmoji:"📄",customLabel:"Without Transcript",customColor:"red"})) + } + }) + ) + + //TICKET REOPENED + messages.add(new api.ODMessage("openticket:verifybar-reopen-message")) + messages.get("openticket:verifybar-reopen-message").workers.add( + new api.ODWorker("openticket:verifybar-reopen-message",0,async (instance,params,source) => { + const {guild,channel,user,verifybar,originalMessage} = params + if (!guild){ + instance.setContent("ODError: Not In Guild => `openticket:verifybar-reopen-message`") + return + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket){ + instance.setContent("ODError: Unknown Ticket => `openticket:verifybar-reopen-message`") + return + } + + const rawReason = (originalMessage.embeds[0] && originalMessage.embeds[0].fields[0]) ? originalMessage.embeds[0].fields[0].value : null + const reason = (rawReason == null) ? null : rawReason.substring(3,rawReason.length-3) + + //add embed + instance.addEmbed(await embeds.getSafe("openticket:reopen-message").build("other",{guild,channel,user,ticket,reason})) + + //add verifybar components + if (verifybar.id.value == "openticket:close-ticket-reopen-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + + }else if (verifybar.id.value == "openticket:delete-ticket-reopen-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + if (generalConfig.data.system.enableDeleteWithoutTranscript) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"no-transcript",customEmoji:"📄",customLabel:"Without Transcript",customColor:"red"})) + } + }) + ) + + //TICKET CLAIM + messages.add(new api.ODMessage("openticket:verifybar-claim-message")) + messages.get("openticket:verifybar-claim-message").workers.add( + new api.ODWorker("openticket:verifybar-claim-message",0,async (instance,params,source) => { + const {guild,channel,user,verifybar,originalMessage} = params + if (!guild){ + instance.setContent("ODError: Not In Guild => `openticket:verifybar-claim-message`") + return + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket){ + instance.setContent("ODError: Unknown Ticket => `openticket:verifybar-claim-message`") + return + } + + const rawReason = (originalMessage.embeds[0] && originalMessage.embeds[0].fields[0]) ? originalMessage.embeds[0].fields[0].value : null + const reason = (rawReason == null) ? null : rawReason.substring(3,rawReason.length-3) + + //add embed + instance.addEmbed(await embeds.getSafe("openticket:claim-message").build("other",{guild,channel,user,ticket,reason})) + + //add verifybar components + if (verifybar.id.value == "openticket:unclaim-ticket-claim-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + } + }) + ) + + //TICKET UNCLAIM + messages.add(new api.ODMessage("openticket:verifybar-unclaim-message")) + messages.get("openticket:verifybar-unclaim-message").workers.add( + new api.ODWorker("openticket:verifybar-unclaim-message",0,async (instance,params,source) => { + const {guild,channel,user,verifybar,originalMessage} = params + if (!guild){ + instance.setContent("ODError: Not In Guild => `openticket:verifybar-unclaim-message`") + return + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket){ + instance.setContent("ODError: Unknown Ticket => `openticket:verifybar-unclaim-message`") + return + } + + const rawReason = (originalMessage.embeds[0] && originalMessage.embeds[0].fields[0]) ? originalMessage.embeds[0].fields[0].value : null + const reason = (rawReason == null) ? null : rawReason.substring(3,rawReason.length-3) + + //add embed + instance.addEmbed(await embeds.getSafe("openticket:unclaim-message").build("other",{guild,channel,user,ticket,reason})) + + //add verifybar components + if (verifybar.id.value == "openticket:claim-ticket-unclaim-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + } + }) + ) + + //TICKET PIN + messages.add(new api.ODMessage("openticket:verifybar-pin-message")) + messages.get("openticket:verifybar-pin-message").workers.add( + new api.ODWorker("openticket:verifybar-pin-message",0,async (instance,params,source) => { + const {guild,channel,user,verifybar,originalMessage} = params + if (!guild){ + instance.setContent("ODError: Not In Guild => `openticket:verifybar-pin-message`") + return + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket){ + instance.setContent("ODError: Unknown Ticket => `openticket:verifybar-pin-message`") + return + } + + const rawReason = (originalMessage.embeds[0] && originalMessage.embeds[0].fields[0]) ? originalMessage.embeds[0].fields[0].value : null + const reason = (rawReason == null) ? null : rawReason.substring(3,rawReason.length-3) + + //add embed + instance.addEmbed(await embeds.getSafe("openticket:pin-message").build("other",{guild,channel,user,ticket,reason})) + + //add verifybar components + if (verifybar.id.value == "openticket:unpin-ticket-pin-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + } + }) + ) + + //TICKET UNPIN + messages.add(new api.ODMessage("openticket:verifybar-unpin-message")) + messages.get("openticket:verifybar-unpin-message").workers.add( + new api.ODWorker("openticket:verifybar-unpin-message",0,async (instance,params,source) => { + const {guild,channel,user,verifybar,originalMessage} = params + if (!guild){ + instance.setContent("ODError: Not In Guild => `openticket:verifybar-unpin-message`") + return + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket){ + instance.setContent("ODError: Unknown Ticket => `openticket:verifybar-unpin-message`") + return + } + + const rawReason = (originalMessage.embeds[0] && originalMessage.embeds[0].fields[0]) ? originalMessage.embeds[0].fields[0].value : null + const reason = (rawReason == null) ? null : rawReason.substring(3,rawReason.length-3) + + //add embed + instance.addEmbed(await embeds.getSafe("openticket:unpin-message").build("other",{guild,channel,user,ticket,reason})) + + //add verifybar components + if (verifybar.id.value == "openticket:pin-ticket-unpin-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + } + }) + ) + + //TICKET AUTOCLOSED + messages.add(new api.ODMessage("openticket:verifybar-autoclose-message")) + messages.get("openticket:verifybar-autoclose-message").workers.add( + new api.ODWorker("openticket:verifybar-autoclose-message",0,async (instance,params,source) => { + const {guild,channel,user,verifybar,originalMessage} = params + if (!guild || channel.isDMBased()){ + instance.setContent("ODError: Not In Guild => `openticket:verifybar-autoclose-message`") + return + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket){ + instance.setContent("ODError: Unknown Ticket => `openticket:verifybar-autoclose-message`") + return + } + + //add embed + instance.addEmbed(await embeds.getSafe("openticket:autoclose-message").build("other",{guild,channel,user,ticket})) + + //add verifybar components + if (verifybar.id.value == "openticket:reopen-ticket-autoclose-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + + }else if (verifybar.id.value == "openticket:delete-ticket-autoclose-message"){ + instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar})) + instance.addComponent(await buttons.getSafe("openticket:verifybar-failure").build("verifybar",{guild,channel,user,verifybar})) + if (generalConfig.data.system.enableTicketActionWithReason) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"reason",customEmoji:"✏️",customLabel:lang.getTranslation("actions.buttons.withReason"),customColor:"blue"})) + if (generalConfig.data.system.enableDeleteWithoutTranscript) instance.addComponent(await buttons.getSafe("openticket:verifybar-success").build("verifybar",{guild,channel,user,verifybar,customData:"no-transcript",customEmoji:"📄",customLabel:"Without Transcript",customColor:"red"})) + } + }) + ) +} + +const errorMessages = () => { + //ERROR + messages.add(new api.ODMessage("openticket:error")) + messages.get("openticket:error").workers.add( + new api.ODWorker("openticket:error",0,async (instance,params,source) => { + const {guild,channel,user,error,layout} = params + instance.addEmbed(await embeds.getSafe("openticket:error").build(source,{guild,channel,user,error,layout})) + instance.setEphemeral(true) + }) + ) + + //ERROR OPTION MISSING + messages.add(new api.ODMessage("openticket:error-option-missing")) + messages.get("openticket:error-option-missing").workers.add( + new api.ODWorker("openticket:error-option-missing",0,async (instance,params,source) => { + const {guild,channel,user,error} = params + instance.addEmbed(await embeds.getSafe("openticket:error-option-missing").build(source,{guild,channel,user,error})) + instance.setEphemeral(true) + }) + ) + + //ERROR OPTION INVALID + messages.add(new api.ODMessage("openticket:error-option-invalid")) + messages.get("openticket:error-option-invalid").workers.add( + new api.ODWorker("openticket:error-option-invalid",0,async (instance,params,source) => { + const {guild,channel,user,error} = params + instance.addEmbed(await embeds.getSafe("openticket:error-option-invalid").build(source,{guild,channel,user,error})) + instance.setEphemeral(true) + }) + ) + + //ERROR UNKNOWN COMMAND + messages.add(new api.ODMessage("openticket:error-unknown-command")) + messages.get("openticket:error-unknown-command").workers.add( + new api.ODWorker("openticket:error-unknown-command",0,async (instance,params,source) => { + const {guild,channel,user,error} = params + instance.addEmbed(await embeds.getSafe("openticket:error-unknown-command").build(source,{guild,channel,user,error})) + instance.setEphemeral(true) + }) + ) + + //ERROR NO PERMISSIONS + messages.add(new api.ODMessage("openticket:error-no-permissions")) + messages.get("openticket:error-no-permissions").workers.add( + new api.ODWorker("openticket:error-no-permissions",0,async (instance,params,source) => { + const {guild,channel,user,permissions} = params + instance.addEmbed(await embeds.getSafe("openticket:error-no-permissions").build(source,{guild,channel,user,permissions})) + instance.setEphemeral(true) + }) + ) + + //ERROR NO PERMISSIONS COOLDOWN + messages.add(new api.ODMessage("openticket:error-no-permissions-cooldown")) + messages.get("openticket:error-no-permissions-cooldown").workers.add( + new api.ODWorker("openticket:error-no-permissions-cooldown",0,async (instance,params,source) => { + const {guild,channel,user,until} = params + instance.addEmbed(await embeds.getSafe("openticket:error-no-permissions-cooldown").build(source,{guild,channel,user,until})) + instance.setEphemeral(true) + }) + ) + + //ERROR NO PERMISSIONS BLACKLISTED + messages.add(new api.ODMessage("openticket:error-no-permissions-blacklisted")) + messages.get("openticket:error-no-permissions-blacklisted").workers.add( + new api.ODWorker("openticket:error-no-permissions-blacklisted",0,async (instance,params,source) => { + const {guild,channel,user} = params + instance.addEmbed(await embeds.getSafe("openticket:error-no-permissions-blacklisted").build(source,{guild,channel,user})) + instance.setEphemeral(true) + }) + ) + + //ERROR NO PERMISSIONS LIMITS + messages.add(new api.ODMessage("openticket:error-no-permissions-limits")) + messages.get("openticket:error-no-permissions-limits").workers.add( + new api.ODWorker("openticket:error-no-permissions-limits",0,async (instance,params,source) => { + const {guild,channel,user,limit} = params + instance.addEmbed(await embeds.getSafe("openticket:error-no-permissions-limits").build(source,{guild,channel,user,limit})) + instance.setEphemeral(true) + }) + ) + + //ERROR RESPONDER TIMEOUT + messages.add(new api.ODMessage("openticket:error-responder-timeout")) + messages.get("openticket:error-responder-timeout").workers.add( + new api.ODWorker("openticket:error-responder-timeout",0,async (instance,params,source) => { + const {guild,channel,user} = params + instance.addEmbed(await embeds.getSafe("openticket:error-responder-timeout").build(source,{guild,channel,user})) + instance.setEphemeral(true) + }) + ) + + //ERROR TICKET UNKNOWN + messages.add(new api.ODMessage("openticket:error-ticket-unknown")) + messages.get("openticket:error-ticket-unknown").workers.add( + new api.ODWorker("openticket:error-ticket-unknown",0,async (instance,params,source) => { + const {guild,channel,user} = params + instance.addEmbed(await embeds.getSafe("openticket:error-ticket-unknown").build(source,{guild,channel,user})) + instance.setEphemeral(true) + }) + ) + + //ERROR TICKET DEPRECATED + messages.add(new api.ODMessage("openticket:error-ticket-deprecated")) + messages.get("openticket:error-ticket-deprecated").workers.add( + new api.ODWorker("openticket:error-ticket-deprecated",0,async (instance,params,source) => { + const {guild,channel,user} = params + instance.addEmbed(await embeds.getSafe("openticket:error-ticket-deprecated").build(source,{guild,channel,user})) + instance.addComponent(await buttons.getSafe("openticket:error-ticket-deprecated-transcript").build(source,{})) + instance.setEphemeral(true) + }) + ) + + //ERROR OPTION UNKNOWN + messages.add(new api.ODMessage("openticket:error-option-unknown")) + messages.get("openticket:error-option-unknown").workers.add( + new api.ODWorker("openticket:error-option-unknown",0,async (instance,params,source) => { + const {guild,channel,user} = params + instance.addEmbed(await embeds.getSafe("openticket:error-option-unknown").build(source,{guild,channel,user})) + instance.setEphemeral(true) + }) + ) + + //ERROR PANEL UNKNOWN + messages.add(new api.ODMessage("openticket:error-panel-unknown")) + messages.get("openticket:error-panel-unknown").workers.add( + new api.ODWorker("openticket:error-panel-unknown",0,async (instance,params,source) => { + const {guild,channel,user} = params + instance.addEmbed(await embeds.getSafe("openticket:error-panel-unknown").build(source,{guild,channel,user})) + instance.setEphemeral(true) + }) + ) + + //ERROR NOD IN GUILD + messages.add(new api.ODMessage("openticket:error-not-in-guild")) + messages.get("openticket:error-not-in-guild").workers.add( + new api.ODWorker("openticket:error-not-in-guild",0,async (instance,params,source) => { + const {channel,user} = params + instance.addEmbed(await embeds.getSafe("openticket:error-not-in-guild").build(source,{channel,user})) + instance.setEphemeral(true) + }) + ) + + //ERROR CHANNEL RENAME + messages.add(new api.ODMessage("openticket:error-channel-rename")) + messages.get("openticket:error-channel-rename").workers.add( + new api.ODWorker("openticket:error-channel-rename",0,async (instance,params,source) => { + const {guild,channel,user,originalName,newName} = params + instance.addEmbed(await embeds.getSafe("openticket:error-channel-rename").build(source,{guild,channel,user,originalName,newName})) + instance.setEphemeral(true) + }) + ) + + //ERROR TICKET BUSY + messages.add(new api.ODMessage("openticket:error-ticket-busy")) + messages.get("openticket:error-ticket-busy").workers.add( + new api.ODWorker("openticket:error-ticket-busy",0,async (instance,params,source) => { + const {guild,channel,user} = params + instance.addEmbed(await embeds.getSafe("openticket:error-ticket-busy").build(source,{guild,channel,user})) + instance.setEphemeral(true) + }) + ) +} + +const helpMenuMessages = () => { + //HELP MENU + messages.add(new api.ODMessage("openticket:help-menu")) + messages.get("openticket:help-menu").workers.add( + new api.ODWorker("openticket:help-menu",0,async (instance,params,source) => { + const {mode,page} = params + const totalPages = (await openticket.helpmenu.render(mode)).length + + const embed = await embeds.getSafe("openticket:help-menu").build(source,{mode,page}) + instance.addEmbed(embed) + if (totalPages > 1){ + //when more than 1 page + instance.addComponent(await buttons.getSafe("openticket:help-menu-previous").build(source,{mode,page})) + instance.addComponent(await buttons.getSafe("openticket:help-menu-page").build(source,{mode,page})) + instance.addComponent(await buttons.getSafe("openticket:help-menu-next").build(source,{mode,page})) + instance.addComponent(buttons.getNewLine("openticket:help-menu-divider")) + } + if (generalConfig.data.textCommands && generalConfig.data.slashCommands) instance.addComponent(await buttons.get("openticket:help-menu-switch").build(source,{mode,page})) + }) + ) +} + +const statsMessages = () => { + //STATS GLOBAL + messages.add(new api.ODMessage("openticket:stats-global")) + messages.get("openticket:stats-global").workers.add( + new api.ODWorker("openticket:stats-global",0,async (instance,params,source) => { + const {guild,channel,user} = params + instance.addEmbed(await embeds.getSafe("openticket:stats-global").build(source,{guild,channel,user})) + }) + ) + + //STATS TICKET + messages.add(new api.ODMessage("openticket:stats-ticket")) + messages.get("openticket:stats-ticket").workers.add( + new api.ODWorker("openticket:stats-ticket",0,async (instance,params,source) => { + const {guild,channel,user,scopeData} = params + instance.addEmbed(await embeds.getSafe("openticket:stats-ticket").build(source,{guild,channel,user,scopeData})) + }) + ) + + //STATS USER + messages.add(new api.ODMessage("openticket:stats-user")) + messages.get("openticket:stats-user").workers.add( + new api.ODWorker("openticket:stats-user",0,async (instance,params,source) => { + const {guild,channel,user,scopeData} = params + instance.addEmbed(await embeds.getSafe("openticket:stats-user").build(source,{guild,channel,user,scopeData})) + }) + ) + + //STATS RESET + messages.add(new api.ODMessage("openticket:stats-reset")) + messages.get("openticket:stats-reset").workers.add( + new api.ODWorker("openticket:stats-reset",0,async (instance,params,source) => { + const {guild,channel,user,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:stats-reset").build(source,{guild,channel,user,reason})) + }) + ) + + //STATS TICKET UNKNOWN + messages.add(new api.ODMessage("openticket:stats-ticket-unknown")) + messages.get("openticket:stats-ticket-unknown").workers.add( + new api.ODWorker("openticket:stats-ticket-unknown",0,async (instance,params,source) => { + const {guild,channel,user,id} = params + instance.addEmbed(await embeds.getSafe("openticket:stats-ticket-unknown").build(source,{guild,channel,user,id})) + instance.setEphemeral(true) + }) + ) +} + +const panelMessages = () => { + //PANEL + messages.add(new api.ODMessage("openticket:panel")) + messages.get("openticket:panel").workers.add([ + new api.ODWorker("openticket:panel-layout",1,async (instance,params,source) => { + const {guild,channel,user,panel} = params + + //add text + const text = panel.get("openticket:text").value + if (panel.get("openticket:describe-options-in-text").value){ + //describe options in text + const describeText = (await import("../data/openticket/panelLoader.ts")).describePanelOptions("text",panel) + instance.setContent(text+"\n\n"+describeText) + }else if (text){ + instance.setContent(text) + } + + if (panel.get("openticket:enable-max-tickets-warning-text").value && generalConfig.data.system.limits.enabled){ + instance.setContent(instance.data.content+"\n\n*"+lang.getTranslationWithParams("actions.descriptions.ticketMessageLimit",[generalConfig.data.system.limits.userMaximum.toString()])+"*") + } + + //add embed + const embedOptions = panel.get("openticket:embed").value + if (embedOptions.enabled) instance.addEmbed(await embeds.getSafe("openticket:panel").build(source,{guild,channel,user,panel})) + }), + new api.ODWorker("openticket:panel-components",0,async (instance,params,source) => { + const {guild,channel,user,panel} = params + const options: api.ODOption[] = [] + panel.get("openticket:options").value.forEach((id) => { + const opt = openticket.options.get(id) + if (opt) options.push(opt) + }) + + if (panel.get("openticket:dropdown").value){ + //dropdown + const ticketOptions: api.ODTicketOption[] = [] + options.forEach((option) => { + if (option instanceof api.ODTicketOption) ticketOptions.push(option) + }) + instance.addComponent(await dropdowns.getSafe("openticket:panel-dropdown-tickets").build(source,{guild,channel,user,panel,options:ticketOptions})) + }else{ + //buttons + for (const option of options){ + if (option instanceof api.ODTicketOption) instance.addComponent(await buttons.getSafe("openticket:ticket-option").build(source,{guild,channel,user,panel,option})) + else if (option instanceof api.ODWebsiteOption) instance.addComponent(await buttons.getSafe("openticket:website-option").build(source,{guild,channel,user,panel,option})) + else if (option instanceof api.ODRoleOption) instance.addComponent(await buttons.getSafe("openticket:role-option").build(source,{guild,channel,user,panel,option})) + } + } + }) + ]) + + //PANEL READY + messages.add(new api.ODMessage("openticket:panel-ready")) + messages.get("openticket:panel-ready").workers.add( + new api.ODWorker("openticket:panel-ready",0,async (instance,params,source) => { + instance.setContent("## "+lang.getTranslation("actions.descriptions.panelReady")) + instance.setEphemeral(true) + }) + ) +} + +const ticketMessages = () => { + //TICKET CREATED + messages.add(new api.ODMessage("openticket:ticket-created")) + messages.get("openticket:ticket-created").workers.add( + new api.ODWorker("openticket:ticket-created",0,async (instance,params,source) => { + const {guild,channel,user,ticket} = params + + instance.addEmbed(await embeds.getSafe("openticket:ticket-created").build(source,{guild,channel,user,ticket})) + instance.addComponent(await buttons.getSafe("openticket:visit-ticket").build("ticket-created",{guild,channel,user,ticket})) + instance.setEphemeral(true) + }) + ) + + //TICKET CREATED DM + messages.add(new api.ODMessage("openticket:ticket-created-dm")) + messages.get("openticket:ticket-created-dm").workers.add( + new api.ODWorker("openticket:ticket-created-dm",0,async (instance,params,source) => { + const {guild,channel,user,ticket} = params + + //add text + const text = ticket.option.get("openticket:dm-message-text").value + if (text !== "") instance.setContent(text) + + //add embed + if (ticket.option.get("openticket:dm-message-embed").value.enabled) instance.addEmbed(await embeds.getSafe("openticket:ticket-created-dm").build(source,{guild,channel,user,ticket})) + + //add components + instance.addComponent(await buttons.getSafe("openticket:visit-ticket").build("ticket-created",{guild,channel,user,ticket})) + }) + ) + + //TICKET CREATED LOGS + messages.add(new api.ODMessage("openticket:ticket-created-logs")) + messages.get("openticket:ticket-created-logs").workers.add( + new api.ODWorker("openticket:ticket-created-logs",0,async (instance,params,source) => { + const {guild,channel,user,ticket} = params + + instance.addEmbed(await embeds.getSafe("openticket:ticket-created-logs").build(source,{guild,channel,user,ticket})) + instance.addComponent(await buttons.getSafe("openticket:visit-ticket").build("ticket-created",{guild,channel,user,ticket})) + }) + ) + + //TICKET MESSAGE + messages.add(new api.ODMessage("openticket:ticket-message")) + messages.get("openticket:ticket-message").workers.add([ + new api.ODWorker("openticket:ticket-message-layout",0,async (instance,params,source) => { + const {guild,channel,user,ticket} = params + + //add pings + const pingOptions = ticket.option.get("openticket:ticket-message-ping").value + const pings: string[] = [discord.userMention(user.id)] + if (pingOptions["@everyone"]) pings.push("@everyone") + if (pingOptions["@here"]) pings.push("@here") + pingOptions.custom.forEach((ping) => pings.push(discord.roleMention(ping))) + const pingText = (pings.length > 0) ? pings.join(" ")+"\n" : "" + + //add text + const text = ticket.option.get("openticket:ticket-message-text").value + if (text !== "") instance.setContent(pingText+text) + else instance.setContent(pingText) + + //add embed + if (ticket.option.get("openticket:ticket-message-embed").value.enabled) instance.addEmbed(await embeds.getSafe("openticket:ticket-message").build(source,{guild,channel,user,ticket})) + + + }), + new api.ODWorker("openticket:ticket-message-components",1,async (instance,params,source) => { + const {guild,channel,user,ticket} = params + //add components + if (generalConfig.data.system.enableTicketClaimButtons && !ticket.get("openticket:closed").value){ + //enable ticket claiming + if (ticket.get("openticket:claimed").value){ + instance.addComponent(await buttons.getSafe("openticket:unclaim-ticket").build("ticket-message",{guild,channel,user,ticket})) + }else{ + instance.addComponent(await buttons.getSafe("openticket:claim-ticket").build("ticket-message",{guild,channel,user,ticket})) + } + } + if (generalConfig.data.system.enableTicketPinButtons && !ticket.get("openticket:closed").value){ + //enable ticket pinning + if (ticket.get("openticket:pinned").value){ + instance.addComponent(await buttons.getSafe("openticket:unpin-ticket").build("ticket-message",{guild,channel,user,ticket})) + }else{ + instance.addComponent(await buttons.getSafe("openticket:pin-ticket").build("ticket-message",{guild,channel,user,ticket})) + } + } + if (generalConfig.data.system.enableTicketCloseButtons){ + //enable ticket closing + if (ticket.get("openticket:closed").value){ + instance.addComponent(await buttons.getSafe("openticket:reopen-ticket").build("ticket-message",{guild,channel,user,ticket})) + }else{ + instance.addComponent(await buttons.getSafe("openticket:close-ticket").build("ticket-message",{guild,channel,user,ticket})) + } + } + //enable ticket deletion + if (generalConfig.data.system.enableTicketDeleteButtons) instance.addComponent(await buttons.getSafe("openticket:delete-ticket").build("ticket-message",{guild,channel,user,ticket})) + }), + new api.ODWorker("openticket:ticket-message-disable-components",2,async (instance,params,source) => { + const {ticket} = params + if (ticket.get("openticket:for-deletion").value){ + //disable all buttons when ticket is being prepared for deletion + instance.data.components.forEach((component) => { + if ((component.component instanceof discord.ButtonBuilder) || (component.component instanceof discord.BaseSelectMenuBuilder)){ + component.component.setDisabled(true) + } + }) + } + }) + ]) + + //TICKET CLOSED + messages.add(new api.ODMessage("openticket:close-message")) + messages.get("openticket:close-message").workers.add( + new api.ODWorker("openticket:close-message",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:close-message").build(source,{guild,channel,user,ticket,reason})) + if (generalConfig.data.system.enableTicketCloseButtons) instance.addComponent(await buttons.getSafe("openticket:reopen-ticket").build("close-message",{guild,channel,user,ticket})) + if (generalConfig.data.system.enableTicketDeleteButtons) instance.addComponent(await buttons.getSafe("openticket:delete-ticket").build("close-message",{guild,channel,user,ticket})) + }) + ) + + //TICKET REOPENED + messages.add(new api.ODMessage("openticket:reopen-message")) + messages.get("openticket:reopen-message").workers.add( + new api.ODWorker("openticket:reopen-message",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:reopen-message").build(source,{guild,channel,user,ticket,reason})) + if (generalConfig.data.system.enableTicketCloseButtons) instance.addComponent(await buttons.getSafe("openticket:close-ticket").build("reopen-message",{guild,channel,user,ticket})) + if (generalConfig.data.system.enableTicketDeleteButtons) instance.addComponent(await buttons.getSafe("openticket:delete-ticket").build("reopen-message",{guild,channel,user,ticket})) + }) + ) + + //TICKET DELETED + messages.add(new api.ODMessage("openticket:delete-message")) + messages.get("openticket:delete-message").workers.add( + new api.ODWorker("openticket:delete-message",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:delete-message").build(source,{guild,channel,user,ticket,reason})) + }) + ) + + //TICKET CLAIMED + messages.add(new api.ODMessage("openticket:claim-message")) + messages.get("openticket:claim-message").workers.add( + new api.ODWorker("openticket:claim-message",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:claim-message").build(source,{guild,channel,user,ticket,reason})) + if (generalConfig.data.system.enableTicketClaimButtons) instance.addComponent(await buttons.getSafe("openticket:unclaim-ticket").build("claim-message",{guild,channel,user,ticket})) + }) + ) + + //TICKET UNCLAIMED + messages.add(new api.ODMessage("openticket:unclaim-message")) + messages.get("openticket:unclaim-message").workers.add( + new api.ODWorker("openticket:unclaim-message",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:unclaim-message").build(source,{guild,channel,user,ticket,reason})) + if (generalConfig.data.system.enableTicketClaimButtons) instance.addComponent(await buttons.getSafe("openticket:claim-ticket").build("unclaim-message",{guild,channel,user,ticket})) + }) + ) + + //TICKET PINNED + messages.add(new api.ODMessage("openticket:pin-message")) + messages.get("openticket:pin-message").workers.add( + new api.ODWorker("openticket:pin-message",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:pin-message").build(source,{guild,channel,user,ticket,reason})) + if (generalConfig.data.system.enableTicketPinButtons) instance.addComponent(await buttons.getSafe("openticket:unpin-ticket").build("pin-message",{guild,channel,user,ticket})) + }) + ) + + //TICKET UNPINNED + messages.add(new api.ODMessage("openticket:unpin-message")) + messages.get("openticket:unpin-message").workers.add( + new api.ODWorker("openticket:unpin-message",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:unpin-message").build(source,{guild,channel,user,ticket,reason})) + if (generalConfig.data.system.enableTicketPinButtons) instance.addComponent(await buttons.getSafe("openticket:pin-ticket").build("unpin-message",{guild,channel,user,ticket})) + }) + ) + + //TICKET RENAMED + messages.add(new api.ODMessage("openticket:rename-message")) + messages.get("openticket:rename-message").workers.add( + new api.ODWorker("openticket:rename-message",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason,data} = params + instance.addEmbed(await embeds.getSafe("openticket:rename-message").build(source,{guild,channel,user,ticket,reason,data})) + }) + ) + + //TICKET MOVED + messages.add(new api.ODMessage("openticket:move-message")) + messages.get("openticket:move-message").workers.add( + new api.ODWorker("openticket:move-message",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason,data} = params + instance.addEmbed(await embeds.getSafe("openticket:move-message").build(source,{guild,channel,user,ticket,reason,data})) + }) + ) + + //TICKET USER ADDED + messages.add(new api.ODMessage("openticket:add-message")) + messages.get("openticket:add-message").workers.add( + new api.ODWorker("openticket:add-message",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason,data} = params + instance.addEmbed(await embeds.getSafe("openticket:add-message").build(source,{guild,channel,user,ticket,reason,data})) + }) + ) + + //TICKET USER REMOVED + messages.add(new api.ODMessage("openticket:remove-message")) + messages.get("openticket:remove-message").workers.add( + new api.ODWorker("openticket:remove-message",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason,data} = params + instance.addEmbed(await embeds.getSafe("openticket:remove-message").build(source,{guild,channel,user,ticket,reason,data})) + }) + ) + + //TICKET ACTION DM + messages.add(new api.ODMessage("openticket:ticket-action-dm")) + messages.get("openticket:ticket-action-dm").workers.add( + new api.ODWorker("openticket:ticket-action-dm",0,async (instance,params,source) => { + const {guild,channel,user,mode,ticket,reason,additionalData} = params + instance.addEmbed(await embeds.getSafe("openticket:ticket-action-dm").build(source,{guild,channel,user,mode,ticket,reason,additionalData})) + }) + ) + + //TICKET ACTION LOGS + messages.add(new api.ODMessage("openticket:ticket-action-logs")) + messages.get("openticket:ticket-action-logs").workers.add( + new api.ODWorker("openticket:ticket-action-logs",0,async (instance,params,source) => { + const {guild,channel,user,mode,ticket,reason,additionalData} = params + instance.addEmbed(await embeds.getSafe("openticket:ticket-action-logs").build(source,{guild,channel,user,mode,ticket,reason,additionalData})) + }) + ) +} + +const blacklistMessages = () => { + //BLACKLIST VIEW + messages.add(new api.ODMessage("openticket:blacklist-view")) + messages.get("openticket:blacklist-view").workers.add( + new api.ODWorker("openticket:blacklist-view",0,async (instance,params,source) => { + const {guild,channel,user} = params + instance.addEmbed(await embeds.getSafe("openticket:blacklist-view").build(source,{guild,channel,user})) + }) + ) + + //BLACKLIST GET + messages.add(new api.ODMessage("openticket:blacklist-get")) + messages.get("openticket:blacklist-get").workers.add( + new api.ODWorker("openticket:blacklist-get",0,async (instance,params,source) => { + const {guild,channel,user,data} = params + instance.addEmbed(await embeds.getSafe("openticket:blacklist-get").build(source,{guild,channel,user,data})) + }) + ) + + //BLACKLIST ADD + messages.add(new api.ODMessage("openticket:blacklist-add")) + messages.get("openticket:blacklist-add").workers.add( + new api.ODWorker("openticket:blacklist-add",0,async (instance,params,source) => { + const {guild,channel,user,data,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:blacklist-add").build(source,{guild,channel,user,data,reason})) + }) + ) + + //BLACKLIST REMOVE + messages.add(new api.ODMessage("openticket:blacklist-remove")) + messages.get("openticket:blacklist-remove").workers.add( + new api.ODWorker("openticket:blacklist-remove",0,async (instance,params,source) => { + const {guild,channel,user,data,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:blacklist-remove").build(source,{guild,channel,user,data,reason})) + }) + ) + + //BLACKLIST DM + messages.add(new api.ODMessage("openticket:blacklist-dm")) + messages.get("openticket:blacklist-dm").workers.add( + new api.ODWorker("openticket:blacklist-dm",0,async (instance,params,source) => { + const {guild,channel,user,mode,data,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:blacklist-dm").build(source,{guild,channel,user,mode,data,reason})) + }) + ) + + //BLACKLIST LOGS + messages.add(new api.ODMessage("openticket:blacklist-logs")) + messages.get("openticket:blacklist-logs").workers.add( + new api.ODWorker("openticket:blacklist-logs",0,async (instance,params,source) => { + const {guild,channel,user,mode,data,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:blacklist-logs").build(source,{guild,channel,user,mode,data,reason})) + }) + ) +} + +const transcriptMessages = () => { + //TRANSCRIPT TEXT READY + messages.add(new api.ODMessage("openticket:transcript-text-ready")) + messages.get("openticket:transcript-text-ready").workers.add( + new api.ODWorker("openticket:transcript-text-ready",0,async (instance,params,source) => { + const {guild,channel,user,ticket,compiler,result} = params + instance.addEmbed(await embeds.getSafe("openticket:transcript-text-ready").build(source,{guild,channel,user,ticket,compiler,result})) + instance.addFile(await files.getSafe("openticket:text-transcript").build(source,{guild,channel,user,ticket,compiler,result})) + }) + ) + + //TRANSCRIPT HTML READY + messages.add(new api.ODMessage("openticket:transcript-html-ready")) + messages.get("openticket:transcript-html-ready").workers.add( + new api.ODWorker("openticket:transcript-html-ready",0,async (instance,params,source) => { + const {guild,channel,user,ticket,compiler,result} = params + instance.addEmbed(await embeds.getSafe("openticket:transcript-html-ready").build(source,{guild,channel,user,ticket,compiler,result})) + instance.addComponent(await buttons.getSafe("openticket:transcript-html-visit").build(source,{guild,channel,user,ticket,compiler,result})) + }) + ) + + //TRANSCRIPT HTML PROGRESS + messages.add(new api.ODMessage("openticket:transcript-html-progress")) + messages.get("openticket:transcript-html-progress").workers.add( + new api.ODWorker("openticket:transcript-html-progress",0,async (instance,params,source) => { + const {guild,channel,user,ticket,compiler,remaining} = params + instance.addEmbed(await embeds.getSafe("openticket:transcript-html-progress").build(source,{guild,channel,user,ticket,compiler,remaining})) + }) + ) + + //TRANSCRIPT ERROR + messages.add(new api.ODMessage("openticket:transcript-error")) + messages.get("openticket:transcript-error").workers.add( + new api.ODWorker("openticket:transcript-error",0,async (instance,params,source) => { + const {guild,channel,user,ticket,compiler,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:transcript-error").build(source,{guild,channel,user,ticket,compiler,reason})) + instance.addComponent(await buttons.getSafe("openticket:transcript-error-retry").build(source,{guild,channel,user,ticket,compiler,reason})) + instance.addComponent(await buttons.getSafe("openticket:transcript-error-continue").build(source,{guild,channel,user,ticket,compiler,reason})) + }) + ) +} + +const roleMessages = () => { + //REACTION ROLE + messages.add(new api.ODMessage("openticket:reaction-role")) + messages.get("openticket:reaction-role").workers.add( + new api.ODWorker("openticket:reaction-role",0,async (instance,params,source) => { + const {guild,user,role,result} = params + instance.addEmbed(await embeds.getSafe("openticket:reaction-role").build(source,{guild,user,role,result})) + instance.setEphemeral(true) + }) + ) +} + +const clearMessages = () => { + //CLEAR VERIFY MESSAGE + messages.add(new api.ODMessage("openticket:clear-verify-message")) + messages.get("openticket:clear-verify-message").workers.add( + new api.ODWorker("openticket:clear-verify-message",0,async (instance,params,source) => { + const {guild,channel,user,filter,list} = params + instance.addEmbed(await embeds.getSafe("openticket:clear-verify-message").build(source,{guild,channel,user,filter,list})) + instance.addComponent(await buttons.getSafe("openticket:clear-continue").build(source,{guild,channel,user,filter,list})) + instance.setEphemeral(true) + }) + ) + + //CLEAR MESSAGE + messages.add(new api.ODMessage("openticket:clear-message")) + messages.get("openticket:clear-message").workers.add( + new api.ODWorker("openticket:clear-message",0,async (instance,params,source) => { + const {guild,channel,user,filter,list} = params + instance.addEmbed(await embeds.getSafe("openticket:clear-message").build(source,{guild,channel,user,filter,list})) + instance.setEphemeral(true) + }) + ) + + //CLEAR LOGS + messages.add(new api.ODMessage("openticket:clear-logs")) + messages.get("openticket:clear-logs").workers.add( + new api.ODWorker("openticket:clear-logs",0,async (instance,params,source) => { + const {guild,channel,user,filter,list} = params + instance.addEmbed(await embeds.getSafe("openticket:clear-logs").build(source,{guild,channel,user,filter,list})) + }) + ) +} + +const autoMessages = () => { + //AUTOCLOSE MESSAGE + messages.add(new api.ODMessage("openticket:autoclose-message")) + messages.get("openticket:autoclose-message").workers.add( + new api.ODWorker("openticket:autoclose-message",0,async (instance,params,source) => { + const {guild,channel,user,ticket} = params + instance.addEmbed(await embeds.getSafe("openticket:autoclose-message").build(source,{guild,channel,user,ticket})) + if (generalConfig.data.system.enableTicketCloseButtons) instance.addComponent(await buttons.getSafe("openticket:reopen-ticket").build("autoclose-message",{guild,channel,user,ticket})) + if (generalConfig.data.system.enableTicketDeleteButtons) instance.addComponent(await buttons.getSafe("openticket:delete-ticket").build("autoclose-message",{guild,channel,user,ticket})) + }) + ) + + //AUTODELETE MESSAGE + messages.add(new api.ODMessage("openticket:autodelete-message")) + messages.get("openticket:autodelete-message").workers.add( + new api.ODWorker("openticket:autodelete-message",0,async (instance,params,source) => { + const {guild,channel,user,ticket} = params + instance.addEmbed(await embeds.getSafe("openticket:autodelete-message").build(source,{guild,channel,user,ticket})) + }) + ) + + //AUTOCLOSE ENABLE + messages.add(new api.ODMessage("openticket:autoclose-enable")) + messages.get("openticket:autoclose-enable").workers.add( + new api.ODWorker("openticket:autoclose-enable",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason,time} = params + instance.addEmbed(await embeds.getSafe("openticket:autoclose-enable").build(source,{guild,channel,user,ticket,reason,time})) + }) + ) + + //AUTODELETE ENABLE + messages.add(new api.ODMessage("openticket:autodelete-enable")) + messages.get("openticket:autodelete-enable").workers.add( + new api.ODWorker("openticket:autodelete-enable",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason,time} = params + instance.addEmbed(await embeds.getSafe("openticket:autodelete-enable").build(source,{guild,channel,user,ticket,reason,time})) + }) + ) + + //AUTOCLOSE DISABLE + messages.add(new api.ODMessage("openticket:autoclose-disable")) + messages.get("openticket:autoclose-disable").workers.add( + new api.ODWorker("openticket:autoclose-disable",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:autoclose-disable").build(source,{guild,channel,user,ticket,reason})) + }) + ) + + //AUTODELETE DISABLE + messages.add(new api.ODMessage("openticket:autodelete-disable")) + messages.get("openticket:autodelete-disable").workers.add( + new api.ODWorker("openticket:autodelete-disable",0,async (instance,params,source) => { + const {guild,channel,user,ticket,reason} = params + instance.addEmbed(await embeds.getSafe("openticket:autodelete-disable").build(source,{guild,channel,user,ticket,reason})) + }) + ) +} \ No newline at end of file diff --git a/src/builders/modals.ts b/src/builders/modals.ts new file mode 100644 index 0000000..6412467 --- /dev/null +++ b/src/builders/modals.ts @@ -0,0 +1,173 @@ +/////////////////////////////////////// +//MODAL BUILDERS +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const modals = openticket.builders.modals +const lang = openticket.languages + +export const registerAllModals = async () => { + ticketModals() +} +const ticketModals = () => { + //TICKET QUESTIONS + modals.add(new api.ODModal("openticket:ticket-questions")) + modals.get("openticket:ticket-questions").workers.add( + new api.ODWorker("openticket:ticket-questions",0,async (instance,params,source) => { + const {option} = params + + instance.setCustomId("od:ticket-questions_"+option.id.value+"_"+source) + instance.setTitle(option.exists("openticket:name") ? option.get("openticket:name").value : option.id.value) + const questionIds = option.get("openticket:questions").value + questionIds.forEach((id) => { + const question = openticket.questions.get(id) + if (!question) return + if (question instanceof api.ODShortQuestion) instance.addQuestion({ + customId:question.id.value, + label:question.get("openticket:name").value, + style:"short", + required:question.get("openticket:required").value, + placeholder:(question.get("openticket:placeholder").value) ? question.get("openticket:placeholder").value : undefined, + minLength:(question.get("openticket:length-enabled").value) ? question.get("openticket:length-min").value : undefined, + maxLength:(question.get("openticket:length-enabled").value) ? question.get("openticket:length-max").value : undefined + }) + else if (question instanceof api.ODParagraphQuestion) instance.addQuestion({ + customId:question.id.value, + label:question.get("openticket:name").value, + style:"paragraph", + required:question.get("openticket:required").value, + placeholder:(question.get("openticket:placeholder").value) ? question.get("openticket:placeholder").value : undefined, + minLength:(question.get("openticket:length-enabled").value) ? question.get("openticket:length-min").value : undefined, + maxLength:(question.get("openticket:length-enabled").value) ? question.get("openticket:length-max").value : undefined + }) + }) + }) + ) + + //CLOSE TICKET REASON + modals.add(new api.ODModal("openticket:close-ticket-reason")) + modals.get("openticket:close-ticket-reason").workers.add( + new api.ODWorker("openticket:close-ticket-reason",0,async (instance,params,source) => { + const {ticket} = params + + instance.setCustomId("od:close-ticket-reason_"+ticket.id.value+"_"+source) + instance.setTitle(lang.getTranslation("actions.buttons.close")) + instance.addQuestion({ + customId:"reason", + label:lang.getTranslation("params.uppercase.reason"), + style:"paragraph", + required:true, + placeholder:lang.getTranslation("actions.modal.closePlaceholder") + }) + }) + ) + + //REOPEN TICKET REASON + modals.add(new api.ODModal("openticket:reopen-ticket-reason")) + modals.get("openticket:reopen-ticket-reason").workers.add( + new api.ODWorker("openticket:reopen-ticket-reason",0,async (instance,params,source) => { + const {ticket} = params + + instance.setCustomId("od:reopen-ticket-reason_"+ticket.id.value+"_"+source) + instance.setTitle(lang.getTranslation("actions.buttons.reopen")) + instance.addQuestion({ + customId:"reason", + label:lang.getTranslation("params.uppercase.reason"), + style:"paragraph", + required:true, + placeholder:lang.getTranslation("actions.modal.reopenPlaceholder") + }) + }) + ) + + //DELETE TICKET REASON + modals.add(new api.ODModal("openticket:delete-ticket-reason")) + modals.get("openticket:delete-ticket-reason").workers.add( + new api.ODWorker("openticket:delete-ticket-reason",0,async (instance,params,source) => { + const {ticket} = params + + instance.setCustomId("od:delete-ticket-reason_"+ticket.id.value+"_"+source) + instance.setTitle(lang.getTranslation("actions.buttons.delete")) + instance.addQuestion({ + customId:"reason", + label:lang.getTranslation("params.uppercase.reason"), + style:"paragraph", + required:true, + placeholder:lang.getTranslation("actions.modal.deletePlaceholder") + }) + }) + ) + + //CLAIM TICKET REASON + modals.add(new api.ODModal("openticket:claim-ticket-reason")) + modals.get("openticket:claim-ticket-reason").workers.add( + new api.ODWorker("openticket:claim-ticket-reason",0,async (instance,params,source) => { + const {ticket} = params + + instance.setCustomId("od:claim-ticket-reason_"+ticket.id.value+"_"+source) + instance.setTitle(lang.getTranslation("actions.buttons.claim")) + instance.addQuestion({ + customId:"reason", + label:lang.getTranslation("params.uppercase.reason"), + style:"paragraph", + required:true, + placeholder:lang.getTranslation("actions.modal.claimPlaceholder") + }) + }) + ) + + //UNCLAIM TICKET REASON + modals.add(new api.ODModal("openticket:unclaim-ticket-reason")) + modals.get("openticket:unclaim-ticket-reason").workers.add( + new api.ODWorker("openticket:unclaim-ticket-reason",0,async (instance,params,source) => { + const {ticket} = params + + instance.setCustomId("od:unclaim-ticket-reason_"+ticket.id.value+"_"+source) + instance.setTitle(lang.getTranslation("actions.buttons.unclaim")) + instance.addQuestion({ + customId:"reason", + label:lang.getTranslation("params.uppercase.reason"), + style:"paragraph", + required:true, + placeholder:lang.getTranslation("actions.modal.unclaimPlaceholder") + }) + }) + ) + + //PIN TICKET REASON + modals.add(new api.ODModal("openticket:pin-ticket-reason")) + modals.get("openticket:pin-ticket-reason").workers.add( + new api.ODWorker("openticket:pin-ticket-reason",0,async (instance,params,source) => { + const {ticket} = params + + instance.setCustomId("od:pin-ticket-reason_"+ticket.id.value+"_"+source) + instance.setTitle(lang.getTranslation("actions.buttons.pin")) + instance.addQuestion({ + customId:"reason", + label:lang.getTranslation("params.uppercase.reason"), + style:"paragraph", + required:true, + placeholder:lang.getTranslation("actions.modal.pinPlaceholder") + }) + }) + ) + + //UNPIN TICKET REASON + modals.add(new api.ODModal("openticket:unpin-ticket-reason")) + modals.get("openticket:unpin-ticket-reason").workers.add( + new api.ODWorker("openticket:unpin-ticket-reason",0,async (instance,params,source) => { + const {ticket} = params + + instance.setCustomId("od:unpin-ticket-reason_"+ticket.id.value+"_"+source) + instance.setTitle(lang.getTranslation("actions.buttons.unpin")) + instance.addQuestion({ + customId:"reason", + label:lang.getTranslation("params.uppercase.reason"), + style:"paragraph", + required:true, + placeholder:lang.getTranslation("actions.modal.unpinPlaceholder") + }) + }) + ) +} \ No newline at end of file diff --git a/src/commands/add.ts b/src/commands/add.ts new file mode 100644 index 0000000..4ad48b9 --- /dev/null +++ b/src/commands/add.ts @@ -0,0 +1,87 @@ +/////////////////////////////////////// +//ADD COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //ADD COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:add",generalConfig.data.prefix,"add")) + openticket.responders.commands.get("openticket:add").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.add + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:add",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const data = instance.options.getUser("user",true) + const reason = instance.options.getString("reason",false) + + //return when user already added to ticket + const participants = await openticket.tickets.getAllTicketParticipants(ticket) + if (!participants || participants.find((p) => p.user.id == data.id)){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:"This user is already able to access the ticket!",layout:"simple"})) + return cancel() + } + + //start adding user to ticket + await instance.defer(false) + await openticket.actions.get("openticket:add-ticket-user").run(source,{guild,channel,user,ticket,reason,sendMessage:false,data}) + await instance.reply(await openticket.builders.messages.getSafe("openticket:add-message").build(source,{guild,channel,user,ticket,reason,data})) + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'add' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/commands/autoclose.ts b/src/commands/autoclose.ts new file mode 100644 index 0000000..e731e96 --- /dev/null +++ b/src/commands/autoclose.ts @@ -0,0 +1,98 @@ +/////////////////////////////////////// +//AUTOCLOSE COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //AUTOCLOSE COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:autoclose",generalConfig.data.prefix,/^autoclose/)) + openticket.responders.commands.get("openticket:autoclose").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.autoclose + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:autoclose",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel:instance.channel,user:instance.user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return + } + //return when already closed + if (ticket.get("openticket:closed").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:"Ticket is already closed!",layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const scope = instance.options.getSubCommand() + if (!scope || (scope != "disable" && scope != "enable")) return + + if (scope == "disable"){ + const reason = instance.options.getString("reason",false) + ticket.get("openticket:autoclose-enabled").value = false + ticket.get("openticket:autoclose-hours").value = 0 + await instance.reply(await openticket.builders.messages.getSafe("openticket:autoclose-disable").build(source,{guild,channel,user,ticket,reason})) + + }else if (scope == "enable"){ + const time = instance.options.getNumber("time",true) + const reason = instance.options.getString("reason",false) + ticket.get("openticket:autoclose-enabled").value = true + ticket.get("openticket:autoclose-hours").value = time + await instance.reply(await openticket.builders.messages.getSafe("openticket:autoclose-enable").build(source,{guild,channel,user,ticket,reason,time})) + } + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + const scope = instance.options.getSubCommand() + const reason = instance.options.getString("reason",false) + openticket.log(instance.user.displayName+" used the 'autoclose "+scope+"' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source}, + {key:"reason",value:reason ?? "/"}, + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/commands/autodelete.ts b/src/commands/autodelete.ts new file mode 100644 index 0000000..4744804 --- /dev/null +++ b/src/commands/autodelete.ts @@ -0,0 +1,93 @@ +/////////////////////////////////////// +//AUTODELETE COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //AUTODELETE COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:autodelete",generalConfig.data.prefix,/^autodelete/)) + openticket.responders.commands.get("openticket:autodelete").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.autodelete + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:autodelete",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel:instance.channel,user:instance.user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const scope = instance.options.getSubCommand() + if (!scope || (scope != "disable" && scope != "enable")) return + + if (scope == "disable"){ + const reason = instance.options.getString("reason",false) + ticket.get("openticket:autodelete-enabled").value = false + ticket.get("openticket:autodelete-days").value = 0 + await instance.reply(await openticket.builders.messages.getSafe("openticket:autodelete-disable").build(source,{guild,channel,user,ticket,reason})) + + }else if (scope == "enable"){ + const time = instance.options.getNumber("time",true) + const reason = instance.options.getString("reason",false) + ticket.get("openticket:autodelete-enabled").value = true + ticket.get("openticket:autodelete-days").value = time + await instance.reply(await openticket.builders.messages.getSafe("openticket:autodelete-enable").build(source,{guild,channel,user,ticket,reason,time})) + } + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + const scope = instance.options.getSubCommand() + const reason = instance.options.getString("reason",false) + openticket.log(instance.user.displayName+" used the 'autodelete "+scope+"' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source}, + {key:"reason",value:reason ?? "/"}, + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/commands/blacklist.ts b/src/commands/blacklist.ts new file mode 100644 index 0000000..f0d3a6e --- /dev/null +++ b/src/commands/blacklist.ts @@ -0,0 +1,127 @@ +/////////////////////////////////////// +//BLACKLIST COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //BLACKLIST COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:blacklist",generalConfig.data.prefix,/^blacklist/)) + openticket.responders.commands.get("openticket:blacklist").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.blacklist + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:blacklist",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel:instance.channel,user:instance.user})) + return cancel() + } + const scope = instance.options.getSubCommand() + if (!scope || (scope != "add" && scope != "get" && scope != "remove" && scope != "view")) return + + if (scope == "view"){ + await instance.reply(await openticket.builders.messages.getSafe("openticket:blacklist-view").build(source,{guild,channel,user})) + + }else if (scope == "get"){ + const data = instance.options.getUser("user",true) + await instance.reply(await openticket.builders.messages.getSafe("openticket:blacklist-get").build(source,{guild,channel,user,data})) + + }else if (scope == "add"){ + const data = instance.options.getUser("user",true) + const reason = instance.options.getString("reason",false) + + openticket.blacklist.add(new api.ODBlacklist(data.id,reason),true) + openticket.log(instance.user.displayName+" added "+data.displayName+" to blacklist!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"method",value:source}, + {key:"reason",value:reason ?? "/"} + ]) + + //manage stats + openticket.stats.get("openticket:global").setStat("openticket:users-blacklisted",1,"increase") + openticket.stats.get("openticket:user").setStat("openticket:users-blacklisted",user.id,1,"increase") + + await instance.reply(await openticket.builders.messages.getSafe("openticket:blacklist-add").build(source,{guild,channel,user,data,reason})) + + }else if (scope == "remove"){ + const data = instance.options.getUser("user",true) + const reason = instance.options.getString("reason",false) + + openticket.blacklist.remove(data.id) + openticket.log(instance.user.displayName+" removed "+data.displayName+" from blacklist!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"method",value:source}, + {key:"reason",value:reason ?? "/"} + ]) + + await instance.reply(await openticket.builders.messages.getSafe("openticket:blacklist-remove").build(source,{guild,channel,user,data,reason})) + } + }), + new api.ODWorker("openticket:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild) return + + const scope = instance.options.getSubCommand() + if (!scope || (scope != "add" && scope != "remove")) return + + const data = instance.options.getUser("user",true) + const reason = instance.options.getString("reason",false) + + //to logs + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.blacklisting.logs){ + const logChannel = openticket.posts.get("openticket:logs") + if (logChannel) logChannel.send(await openticket.builders.messages.getSafe("openticket:blacklist-logs").build(source,{guild,channel,user,mode:scope,data,reason})) + } + + //to dm + if (generalConfig.data.system.messages.blacklisting.dm) await openticket.client.sendUserDm(user,await openticket.builders.messages.getSafe("openticket:blacklist-dm").build(source,{guild,channel,user,mode:scope,data,reason})) + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + const scope = instance.options.getSubCommand() + openticket.log(instance.user.displayName+" used the 'blacklist "+scope+"' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/commands/claim.ts b/src/commands/claim.ts new file mode 100644 index 0000000..84316f7 --- /dev/null +++ b/src/commands/claim.ts @@ -0,0 +1,138 @@ +/////////////////////////////////////// +//CLAIM COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //CLAIM COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:claim",generalConfig.data.prefix,"claim")) + openticket.responders.commands.get("openticket:claim").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.claim + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:claim",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when already claimed + if (ticket.get("openticket:claimed").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:"Ticket is already claimed!",layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const claimUser = instance.options.getUser("user",false) ?? user + const reason = instance.options.getString("reason",false) + + //start claiming ticket + await instance.defer(false) + await openticket.actions.get("openticket:claim-ticket").run(source,{guild,channel,user:claimUser,ticket,reason,sendMessage:false}) + await instance.reply(await openticket.builders.messages.getSafe("openticket:claim-message").build(source,{guild,channel,user:claimUser,ticket,reason})) + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'claim' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} + +export const registerButtonResponders = async () => { + //CLAIM TICKET BUTTON RESPONDER + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:claim-ticket",/^od:claim-ticket/)) + openticket.responders.buttons.get("openticket:claim-ticket").workers.add( + new api.ODWorker("openticket:claim-ticket",0,async (instance,params,source,cancel) => { + const originalSource = instance.interaction.customId.split("_")[1] + if (originalSource != "ticket-message" && originalSource != "unclaim-message") return + + if (originalSource == "ticket-message") await openticket.verifybars.get("openticket:claim-ticket-ticket-message").activate(instance) + else await openticket.verifybars.get("openticket:claim-ticket-unclaim-message").activate(instance) + }) + ) +} + +export const registerModalResponders = async () => { + //CLAIM WITH REASON MODAL RESPONDER + openticket.responders.modals.add(new api.ODModalResponder("openticket:claim-ticket-reason",/^od:claim-ticket-reason_/)) + openticket.responders.modals.get("openticket:claim-ticket-reason").workers.add([ + new api.ODWorker("openticket:claim-ticket-reason",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!channel) return + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel,user:instance.user})) + return cancel() + } + const ticket = openticket.tickets.get(instance.interaction.customId.split("_")[1]) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return + } + + const originalSource = instance.interaction.customId.split("_")[2] as ("ticket-message"|"unclaim-message"|"other") + const reason = instance.values.getTextField("reason",true) + + //claim with reason + if (originalSource == "ticket-message"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:claim-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:true}) + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + }else if (originalSource == "unclaim-message"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:claim-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:claim-message").build("other",{guild,channel,user,ticket,reason})) + }else if (originalSource == "other"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:claim-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:true}) + } + + }) + ]) +} \ No newline at end of file diff --git a/src/commands/clear.ts b/src/commands/clear.ts new file mode 100644 index 0000000..61a0e7f --- /dev/null +++ b/src/commands/clear.ts @@ -0,0 +1,122 @@ +/////////////////////////////////////// +//CLEAR COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //CLEAR COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:clear",generalConfig.data.prefix,"clear")) + openticket.responders.commands.get("openticket:clear").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.clear + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + //only allow global admins (ticket admins aren't allowed to clear tickets) + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild,{allowChannelUserScope:false,allowChannelRoleScope:false}))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:clear",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + + const tempFilter = instance.options.getString("filter",false) + const filter = (tempFilter) ? tempFilter.toLowerCase() as api.ODTicketClearFilter : "all" + const list: string[] = [] + const ticketList = openticket.tickets.getAll().filter((ticket) => { + if (filter == "all") return true + else if (filter == "open" && ticket.get("openticket:open").value) return true + else if (filter == "closed" && ticket.get("openticket:closed").value) return true + else if (filter == "claimed" && ticket.get("openticket:claimed").value) return true + else if (filter == "pinned" && ticket.get("openticket:pinned").value) return true + else if (filter == "unclaimed" && !ticket.get("openticket:claimed").value) return true + else if (filter == "unpinned" && !ticket.get("openticket:pinned").value) return true + else if (filter == "autoclosed" && ticket.get("openticket:closed").value) return true + else return false + }) + for (const ticket of ticketList){ + const ticketChannel = await openticket.tickets.getTicketChannel(ticket) + if (ticketChannel) list.push("#"+ticketChannel.name) + } + + //reply with clear verify + await instance.defer(true) + await instance.reply(await openticket.builders.messages.getSafe("openticket:clear-verify-message").build(source,{guild,channel,user,filter,list})) + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'clear' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} + +export const registerButtonResponders = async () => { + //CLEAR CONTINUE BUTTON RESPONDER + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:clear-continue",/^od:clear-continue_/)) + openticket.responders.buttons.get("openticket:clear-continue").workers.add( + new api.ODWorker("openticket:clear-continue",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild || channel.isDMBased()) return + const originalSource = instance.interaction.customId.split("_")[1] + if (originalSource != "slash" && originalSource != "text" && originalSource != "other") return + const filter = instance.interaction.customId.split("_")[2] as api.ODTicketClearFilter + + //start ticket clear + instance.defer("update",true) + const list: string[] = [] + const ticketList = openticket.tickets.getAll().filter((ticket) => { + if (filter == "all") return true + else if (filter == "open" && ticket.get("openticket:open").value) return true + else if (filter == "closed" && ticket.get("openticket:closed").value) return true + else if (filter == "claimed" && ticket.get("openticket:claimed").value) return true + else if (filter == "pinned" && ticket.get("openticket:pinned").value) return true + else if (filter == "unclaimed" && !ticket.get("openticket:claimed").value) return true + else if (filter == "unpinned" && !ticket.get("openticket:pinned").value) return true + else if (filter == "autoclosed" && ticket.get("openticket:closed").value) return true + else return false + }) + for (const ticket of ticketList){ + const ticketChannel = await openticket.tickets.getTicketChannel(ticket) + if (ticketChannel) list.push("#"+ticketChannel.name) + } + + await openticket.actions.get("openticket:clear-tickets").run(originalSource,{guild,channel,user,filter,list:ticketList}) + await instance.update(await openticket.builders.messages.getSafe("openticket:clear-message").build(originalSource,{guild,channel,user,filter,list})) + }) + ) +} \ No newline at end of file diff --git a/src/commands/close.ts b/src/commands/close.ts new file mode 100644 index 0000000..dc39dc6 --- /dev/null +++ b/src/commands/close.ts @@ -0,0 +1,137 @@ +/////////////////////////////////////// +//CLOSE COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //CLOSE COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:close",generalConfig.data.prefix,"close")) + openticket.responders.commands.get("openticket:close").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.close + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:close",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when already closed + if (ticket.get("openticket:closed").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:"Ticket is already closed!",layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const reason = instance.options.getString("reason",false) + + //start closing ticket + await instance.defer(false) + await openticket.actions.get("openticket:close-ticket").run(source,{guild,channel,user,ticket,reason,sendMessage:false}) + await instance.reply(await openticket.builders.messages.getSafe("openticket:close-message").build(source,{guild,channel,user,ticket,reason})) + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'close' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} + +export const registerButtonResponders = async () => { + //CLOSE TICKET BUTTON RESPONDER + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:close-ticket",/^od:close-ticket_/)) + openticket.responders.buttons.get("openticket:close-ticket").workers.add( + new api.ODWorker("openticket:close-ticket",0,async (instance,params,source,cancel) => { + const originalSource = instance.interaction.customId.split("_")[1] + if (originalSource != "ticket-message" && originalSource != "reopen-message") return + + if (originalSource == "ticket-message") await openticket.verifybars.get("openticket:close-ticket-ticket-message").activate(instance) + else await openticket.verifybars.get("openticket:close-ticket-reopen-message").activate(instance) + }) + ) +} + +export const registerModalResponders = async () => { + //CLOSE WITH REASON MODAL RESPONDER + openticket.responders.modals.add(new api.ODModalResponder("openticket:close-ticket-reason",/^od:close-ticket-reason_/)) + openticket.responders.modals.get("openticket:close-ticket-reason").workers.add([ + new api.ODWorker("openticket:close-ticket-reason",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!channel) return + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel,user:instance.user})) + return cancel() + } + const ticket = openticket.tickets.get(instance.interaction.customId.split("_")[1]) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return + } + + const originalSource = instance.interaction.customId.split("_")[2] as ("ticket-message"|"reopen-message"|"other") + const reason = instance.values.getTextField("reason",true) + + //close with reason + if (originalSource == "ticket-message"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:close-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:true}) + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + }else if (originalSource == "reopen-message"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:close-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:close-message").build("other",{guild,channel,user,ticket,reason})) + }else if (originalSource == "other"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:close-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:true}) + } + + }) + ]) +} \ No newline at end of file diff --git a/src/commands/delete.ts b/src/commands/delete.ts new file mode 100644 index 0000000..e80f677 --- /dev/null +++ b/src/commands/delete.ts @@ -0,0 +1,149 @@ +/////////////////////////////////////// +//DELETE COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //DELETE COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:delete",generalConfig.data.prefix,"delete")) + openticket.responders.commands.get("openticket:delete").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.delete + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:delete",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const reason = instance.options.getString("reason",false) + const withoutTranscript = instance.options.getBoolean("notranscript",false) ?? false + + //start deleting ticket + await instance.defer(false) + await instance.reply(await openticket.builders.messages.getSafe("openticket:delete-message").build(source,{guild,channel,user,ticket,reason})) + await openticket.actions.get("openticket:delete-ticket").run(source,{guild,channel,user,ticket,reason,sendMessage:false,withoutTranscript}) + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'delete' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} + +export const registerButtonResponders = async () => { + //DELETE TICKET BUTTON RESPONDER + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:delete-ticket",/^od:delete-ticket/)) + openticket.responders.buttons.get("openticket:delete-ticket").workers.add( + new api.ODWorker("openticket:delete-ticket",0,async (instance,params,source,cancel) => { + const originalSource = instance.interaction.customId.split("_")[1] + if (originalSource != "ticket-message" && originalSource != "reopen-message" && originalSource != "close-message" && originalSource != "autoclose-message") return + + if (originalSource == "ticket-message") await openticket.verifybars.get("openticket:delete-ticket-ticket-message").activate(instance) + else if (originalSource == "close-message") await openticket.verifybars.get("openticket:delete-ticket-close-message").activate(instance) + else if (originalSource == "reopen-message") await openticket.verifybars.get("openticket:delete-ticket-reopen-message").activate(instance) + else if (originalSource == "autoclose-message") await openticket.verifybars.get("openticket:delete-ticket-autoclose-message").activate(instance) + }) + ) +} + +export const registerModalResponders = async () => { + //REOPEN WITH REASON MODAL RESPONDER + openticket.responders.modals.add(new api.ODModalResponder("openticket:delete-ticket-reason",/^od:delete-ticket-reason_/)) + openticket.responders.modals.get("openticket:delete-ticket-reason").workers.add([ + new api.ODWorker("openticket:delete-ticket-reason",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!channel) return + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel,user:instance.user})) + return cancel() + } + const ticket = openticket.tickets.get(instance.interaction.customId.split("_")[1]) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return + } + + const originalSource = instance.interaction.customId.split("_")[2] as ("ticket-message"|"reopen-message"|"close-message"|"autoclose-message"|"other") + const reason = instance.values.getTextField("reason",true) + + //delete with reason + if (originalSource == "ticket-message"){ + await instance.defer("update",false) + //don't await DELETE action => else it will update the message after the channel has been deleted + openticket.actions.get("openticket:delete-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:true,withoutTranscript:false}) + //update ticket (for ticket message) => no-await doesn't wait for the action to set this variable + ticket.get("openticket:for-deletion").value = true + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + }else if (originalSource == "close-message"){ + await instance.defer("update",false) + //don't await DELETE action => else it will update the message after the channel has been deleted + openticket.actions.get("openticket:delete-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:false,withoutTranscript:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:delete-message").build("other",{guild,channel,user,ticket,reason})) + }else if (originalSource == "reopen-message"){ + await instance.defer("update",false) + //don't await DELETE action => else it will update the message after the channel has been deleted + openticket.actions.get("openticket:delete-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:false,withoutTranscript:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:delete-message").build("other",{guild,channel,user,ticket,reason})) + }else if (originalSource == "autoclose-message"){ + await instance.defer("update",false) + //don't await DELETE action => else it will update the message after the channel has been deleted + openticket.actions.get("openticket:delete-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:false,withoutTranscript:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:delete-message").build("other",{guild,channel,user,ticket,reason})) + }else if (originalSource == "other"){ + await instance.defer("update",false) + //don't await DELETE action => else it will update the message after the channel has been deleted + openticket.actions.get("openticket:delete-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:true,withoutTranscript:false}) + } + }) + ]) +} \ No newline at end of file diff --git a/src/commands/help.ts b/src/commands/help.ts new file mode 100644 index 0000000..36b7ec1 --- /dev/null +++ b/src/commands/help.ts @@ -0,0 +1,112 @@ +/////////////////////////////////////// +//HELP COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //HELP COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:help",generalConfig.data.prefix,"help")) + openticket.responders.commands.get("openticket:help").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.help + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:help",0,async (instance,params,source,cancel) => { + const preferSlashOverText = generalConfig.data.system.preferSlashOverText + + let mode: "slash"|"text" + if (generalConfig.data.slashCommands && generalConfig.data.textCommands){ + mode = (preferSlashOverText) ? "slash" : "text" + + }else if (!generalConfig.data.slashCommands) mode = "text" + else mode = "slash" + + await instance.reply(await openticket.builders.messages.getSafe("openticket:help-menu").build(source,{mode,page:0})) + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'help' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} + +export const registerButtonResponders = async () => { + //HELP MENU SWITCH BUTTON RESPONDER + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:help-menu-switch",/^od:help-menu-switch_(slash|text)/)) + openticket.responders.buttons.get("openticket:help-menu-switch").workers.add( + new api.ODWorker("openticket:update-help-menu",0,async (instance,params,source,cancel) => { + const mode = instance.interaction.customId.split("_")[1] as "slash"|"text" + const pageButton = instance.getMessageComponent("button",/^od:help-menu-page_([0-9]+)/) + const currentPage = (pageButton && pageButton.customId) ? Number(pageButton.customId.split("_")[1]) : 0 + + const newMode = (mode == "slash") ? "text" : "slash" + + const msg = await openticket.builders.messages.getSafe("openticket:help-menu").build("button",{mode:newMode,page:currentPage}) + await instance.update(msg) + }) + ) + + //HELP MENU PREVIOUS BUTTON RESPONDER + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:help-menu-previous",/^od:help-menu-previous/)) + openticket.responders.buttons.get("openticket:help-menu-previous").workers.add( + new api.ODWorker("openticket:update-help-menu",0,async (instance,params,source,cancel) => { + const switchButton = instance.getMessageComponent("button",/^od:help-menu-switch_(slash|text)/) + const pageButton = instance.getMessageComponent("button",/^od:help-menu-page_([0-9]+)/) + const currentPage = (pageButton && pageButton.customId) ? Number(pageButton.customId.split("_")[1]) : 0 + const currentMode = (switchButton && switchButton.customId) ? switchButton.customId.split("_")[1] as "text"|"slash" : "slash" + + const msg = await openticket.builders.messages.getSafe("openticket:help-menu").build("button",{mode:currentMode,page:currentPage-1}) + await instance.update(msg) + }) + ) + + //HELP MENU NEXT BUTTON RESPONDER + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:help-menu-next",/^od:help-menu-next/)) + openticket.responders.buttons.get("openticket:help-menu-next").workers.add( + new api.ODWorker("openticket:update-help-menu",0,async (instance,params,source,cancel) => { + const switchButton = instance.getMessageComponent("button",/^od:help-menu-switch_(slash|text)/) + const pageButton = instance.getMessageComponent("button",/^od:help-menu-page_([0-9]+)/) + const currentPage = (pageButton && pageButton.customId) ? Number(pageButton.customId.split("_")[1]) : 0 + const currentMode = (switchButton && switchButton.customId) ? switchButton.customId.split("_")[1] as "text"|"slash" : "slash" + + const msg = await openticket.builders.messages.getSafe("openticket:help-menu").build("button",{mode:currentMode,page:currentPage+1}) + await instance.update(msg) + }) + ) +} \ No newline at end of file diff --git a/src/commands/move.ts b/src/commands/move.ts new file mode 100644 index 0000000..a33cb55 --- /dev/null +++ b/src/commands/move.ts @@ -0,0 +1,92 @@ +/////////////////////////////////////// +//MOVE COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //MOVE COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:move",generalConfig.data.prefix,"move")) + openticket.responders.commands.get("openticket:move").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.move + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:move",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const id = instance.options.getString("id",true) + const reason = instance.options.getString("reason",false) + + const option = openticket.options.get(id) + //return if unknown option + if (!option || !(option instanceof api.ODTicketOption)){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:"Unknown Option",layout:"simple"})) + return cancel() + } + //return if option is the same + if (ticket.option.id.value == option.id.value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:"This ticket is already the same as the chosen option!",layout:"simple"})) + return cancel() + } + + //start moving ticket + await instance.defer(false) + await openticket.actions.get("openticket:move-ticket").run(source,{guild,channel,user,ticket,reason,sendMessage:false,data:option}) + await instance.reply(await openticket.builders.messages.getSafe("openticket:move-message").build(source,{guild,channel,user,ticket,reason,data:option})) + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'move' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/commands/panel.ts b/src/commands/panel.ts new file mode 100644 index 0000000..72cf050 --- /dev/null +++ b/src/commands/panel.ts @@ -0,0 +1,82 @@ +/////////////////////////////////////// +//STATS COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //PANEL COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:panel",generalConfig.data.prefix,/^panel/)) + openticket.responders.commands.get("openticket:panel").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.panel + + //command is disabled + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:panel",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel:instance.channel,user:instance.user})) + return cancel() + } + + const id = instance.options.getString("id",true) + const panel = openticket.panels.get(id) + + if (!panel){ + //invalid panel + instance.reply(await openticket.builders.messages.getSafe("openticket:error-panel-unknown").build(source,{guild,channel,user})) + return cancel() + } + + await instance.reply(await openticket.builders.messages.getSafe("openticket:panel-ready").build(source,{guild,channel,user,panel})) + const panelMessage = await instance.channel.send((await openticket.builders.messages.getSafe("openticket:panel").build(source,{guild,channel,user,panel})).message) + + //add panel to database on auto-update + if (instance.options.getBoolean("auto-update",false)){ + const globalDatabase = openticket.databases.get("openticket:global") + globalDatabase.set("openticket:panel-update",panelMessage.channel.id+"_"+panelMessage.id,panel.id.value) + } + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'panel' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/commands/pin.ts b/src/commands/pin.ts new file mode 100644 index 0000000..64d5c2b --- /dev/null +++ b/src/commands/pin.ts @@ -0,0 +1,137 @@ +/////////////////////////////////////// +//PIN COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //PIN COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:pin",generalConfig.data.prefix,"pin")) + openticket.responders.commands.get("openticket:pin").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.pin + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:pin",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when already pinned + if (ticket.get("openticket:pinned").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:"Ticket is already pinned!",layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const reason = instance.options.getString("reason",false) + + //start pinning ticket + await instance.defer(false) + await openticket.actions.get("openticket:pin-ticket").run(source,{guild,channel,user,ticket,reason,sendMessage:false}) + await instance.reply(await openticket.builders.messages.getSafe("openticket:pin-message").build(source,{guild,channel,user,ticket,reason})) + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'pin' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} + +export const registerButtonResponders = async () => { + //PIN TICKET BUTTON RESPONDER + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:pin-ticket",/^od:pin-ticket/)) + openticket.responders.buttons.get("openticket:pin-ticket").workers.add( + new api.ODWorker("openticket:pin-ticket",0,async (instance,params,source,cancel) => { + const originalSource = instance.interaction.customId.split("_")[1] + if (originalSource != "ticket-message" && originalSource != "unpin-message") return + + if (originalSource == "ticket-message") await openticket.verifybars.get("openticket:pin-ticket-ticket-message").activate(instance) + else await openticket.verifybars.get("openticket:pin-ticket-unpin-message").activate(instance) + }) + ) +} + +export const registerModalResponders = async () => { + //PIN WITH REASON MODAL RESPONDER + openticket.responders.modals.add(new api.ODModalResponder("openticket:pin-ticket-reason",/^od:pin-ticket-reason_/)) + openticket.responders.modals.get("openticket:pin-ticket-reason").workers.add([ + new api.ODWorker("openticket:pin-ticket-reason",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!channel) return + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel,user:instance.user})) + return cancel() + } + const ticket = openticket.tickets.get(instance.interaction.customId.split("_")[1]) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return + } + + const originalSource = instance.interaction.customId.split("_")[2] as ("ticket-message"|"unpin-message"|"other") + const reason = instance.values.getTextField("reason",true) + + //pin with reason + if (originalSource == "ticket-message"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:pin-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:true}) + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + }else if (originalSource == "unpin-message"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:pin-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:pin-message").build("other",{guild,channel,user,ticket,reason})) + }else if (originalSource == "other"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:pin-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:true}) + } + + }) + ]) +} \ No newline at end of file diff --git a/src/commands/remove.ts b/src/commands/remove.ts new file mode 100644 index 0000000..f984372 --- /dev/null +++ b/src/commands/remove.ts @@ -0,0 +1,87 @@ +/////////////////////////////////////// +//REMOVE COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //REMOVE COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:remove",generalConfig.data.prefix,"remove")) + openticket.responders.commands.get("openticket:remove").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.remove + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:remove",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const data = instance.options.getUser("user",true) + const reason = instance.options.getString("reason",false) + + //return when user is not a participant of the ticket (admins & creator can't be removed) + const participants = await openticket.tickets.getAllTicketParticipants(ticket) + if (!participants || !participants.find((p) => p.user.id == data.id && p.role == "participant")){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:"Unable to remove this user from the ticket!",layout:"simple"})) + return cancel() + } + + //start removing user from ticket + await instance.defer(false) + await openticket.actions.get("openticket:remove-ticket-user").run(source,{guild,channel,user,ticket,reason,sendMessage:false,data}) + await instance.reply(await openticket.builders.messages.getSafe("openticket:remove-message").build(source,{guild,channel,user,ticket,reason,data})) + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'remove' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/commands/rename.ts b/src/commands/rename.ts new file mode 100644 index 0000000..e5fafb5 --- /dev/null +++ b/src/commands/rename.ts @@ -0,0 +1,80 @@ +/////////////////////////////////////// +//RENAME COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //RENAME COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:rename",generalConfig.data.prefix,"rename")) + openticket.responders.commands.get("openticket:rename").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.rename + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:rename",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const name = instance.options.getString("name",true) + const reason = instance.options.getString("reason",false) + + //start renaming ticket + await instance.defer(false) + await openticket.actions.get("openticket:rename-ticket").run(source,{guild,channel,user,ticket,reason,sendMessage:false,data:name}) + await instance.reply(await openticket.builders.messages.getSafe("openticket:rename-message").build(source,{guild,channel,user,ticket,reason,data:name})) + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'rename' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/commands/reopen.ts b/src/commands/reopen.ts new file mode 100644 index 0000000..82ccb36 --- /dev/null +++ b/src/commands/reopen.ts @@ -0,0 +1,142 @@ +/////////////////////////////////////// +//REOPEN COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //REOPEN COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:reopen",generalConfig.data.prefix,"reopen")) + openticket.responders.commands.get("openticket:reopen").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.reopen + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:reopen",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when not closed + if (!ticket.get("openticket:closed").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:"Ticket is not closed yet!",layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const reason = instance.options.getString("reason",false) + + //start reopening ticket + await instance.defer(false) + await openticket.actions.get("openticket:reopen-ticket").run(source,{guild,channel,user,ticket,reason,sendMessage:false}) + await instance.reply(await openticket.builders.messages.getSafe("openticket:reopen-message").build(source,{guild,channel,user,ticket,reason})) + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'reopen' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} + +export const registerButtonResponders = async () => { + //REOPEN TICKET BUTTON RESPONDER + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:reopen-ticket",/^od:reopen-ticket/)) + openticket.responders.buttons.get("openticket:reopen-ticket").workers.add( + new api.ODWorker("openticket:reopen-ticket",0,async (instance,params,source,cancel) => { + const originalSource = instance.interaction.customId.split("_")[1] + if (originalSource != "ticket-message" && originalSource != "close-message" && originalSource != "autoclose-message") return + + if (originalSource == "ticket-message") await openticket.verifybars.get("openticket:reopen-ticket-ticket-message").activate(instance) + else if (originalSource == "close-message") await openticket.verifybars.get("openticket:reopen-ticket-close-message").activate(instance) + else await openticket.verifybars.get("openticket:reopen-ticket-autoclose-message").activate(instance) + }) + ) +} + +export const registerModalResponders = async () => { + //REOPEN WITH REASON MODAL RESPONDER + openticket.responders.modals.add(new api.ODModalResponder("openticket:reopen-ticket-reason",/^od:reopen-ticket-reason_/)) + openticket.responders.modals.get("openticket:reopen-ticket-reason").workers.add([ + new api.ODWorker("openticket:reopen-ticket-reason",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!channel) return + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel,user:instance.user})) + return cancel() + } + const ticket = openticket.tickets.get(instance.interaction.customId.split("_")[1]) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return + } + + const originalSource = instance.interaction.customId.split("_")[2] as ("ticket-message"|"close-message"|"autoclose-message"|"other") + const reason = instance.values.getTextField("reason",true) + + //reopen with reason + if (originalSource == "ticket-message"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:reopen-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:true}) + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + }else if (originalSource == "close-message"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:reopen-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:reopen-message").build("other",{guild,channel,user,ticket,reason})) + }else if (originalSource == "autoclose-message"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:reopen-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:reopen-message").build("other",{guild,channel,user,ticket,reason})) + }else if (originalSource == "other"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:reopen-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:true}) + } + + }) + ]) +} \ No newline at end of file diff --git a/src/commands/role.ts b/src/commands/role.ts new file mode 100644 index 0000000..88ea4f6 --- /dev/null +++ b/src/commands/role.ts @@ -0,0 +1,41 @@ +/////////////////////////////////////// +//ROLE BUTTON (not command) +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerButtonResponders = async () => { + //ROLE OPTION BUTTON RESPONDER + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:role-option",/^od:role-option_/)) + openticket.responders.buttons.get("openticket:role-option").workers.add( + new api.ODWorker("openticket:role-option",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel:instance.channel,user:instance.user})) + return cancel() + } + + //get option + const optionId = instance.interaction.customId.split("_")[2] + const option = openticket.options.get(optionId) + if (!option || !(option instanceof api.ODRoleOption)){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-option-unknown").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user})) + return cancel() + } + + //reaction role + await instance.defer("reply",true) + const res = await openticket.actions.get("openticket:reaction-role").run("panel-button",{guild,user,option,overwriteMode:null}) + if (!res.result || !res.role){ + //error + await instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild,channel:instance.channel,user,error:"Unable to receive role update data from worker!",layout:"advanced"})) + return cancel() + } + if (generalConfig.data.system.replyOnReactionRole) await instance.reply(await openticket.builders.messages.getSafe("openticket:reaction-role").build("panel-button",{guild,user,role:res.role,result:res.result})) + }) + ) +} \ No newline at end of file diff --git a/src/commands/stats.ts b/src/commands/stats.ts new file mode 100644 index 0000000..1c35760 --- /dev/null +++ b/src/commands/stats.ts @@ -0,0 +1,107 @@ +/////////////////////////////////////// +//STATS COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //STATS COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:stats",generalConfig.data.prefix,/^stats/)) + openticket.responders.commands.get("openticket:stats").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.stats + + //command is disabled + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + } + + //reset subcommand is owner/developer only + if (instance.options.getSubCommand() == "reset"){ + if (!openticket.permissions.hasPermissions("owner",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["owner","developer"]})) + return cancel() + }else return + } + + //permissions for normal scopes + if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:stats",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel:instance.channel,user:instance.user})) + return cancel() + } + const scope = instance.options.getSubCommand() + if (!scope || (scope != "global" && scope != "ticket" && scope != "user" && scope != "reset")) return + + if (scope == "global"){ + await instance.reply(await openticket.builders.messages.getSafe("openticket:stats-global").build(source,{guild,channel,user})) + + }else if (scope == "ticket"){ + const id = instance.options.getChannel("ticket",false)?.id ?? channel.id + const ticket = openticket.tickets.get(id) + + if (ticket) await instance.reply(await openticket.builders.messages.getSafe("openticket:stats-ticket").build(source,{guild,channel,user,scopeData:ticket})) + else await instance.reply(await openticket.builders.messages.getSafe("openticket:stats-ticket-unknown").build(source,{guild,channel,user,id})) + + }else if (scope == "user"){ + const statsUser = instance.options.getUser("user",false) ?? user + await instance.reply(await openticket.builders.messages.getSafe("openticket:stats-user").build(source,{guild,channel,user,scopeData:statsUser})) + + }else if (scope == "reset"){ + const reason = instance.options.getString("reason",false) + openticket.stats.reset() + await instance.reply(await openticket.builders.messages.getSafe("openticket:stats-reset").build(source,{guild,channel,user,reason})) + + } + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + const scope = instance.options.getSubCommand() + let data: string + if (scope == "ticket"){ + data = instance.options.getChannel("ticket",false)?.id ?? instance.channel.id + }else if (scope == "user"){ + data = instance.options.getUser("user",false)?.id ?? instance.user.id + }else data = "/" + openticket.log(instance.user.displayName+" used the 'stats "+scope+"' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source}, + {key:"data",value:data}, + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/commands/ticket.ts b/src/commands/ticket.ts new file mode 100644 index 0000000..ffa00ea --- /dev/null +++ b/src/commands/ticket.ts @@ -0,0 +1,269 @@ +/////////////////////////////////////// +//TICKET COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //TICKET COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:ticket",generalConfig.data.prefix,/^ticket/)) + openticket.responders.commands.get("openticket:ticket").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.ticket + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:ticket",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel:instance.channel,user:instance.user})) + return cancel() + } + + //get option + const optionId = instance.options.getString("id",true) + const option = openticket.options.get(optionId) + if (!option || !(option instanceof api.ODTicketOption)){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-option-unknown").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user})) + return cancel() + } + + //check ticket permissions + const res = await openticket.actions.get("openticket:create-ticket-permissions").run(source,{guild,user,option}) + if (!res.valid){ + //error + if (res.reason == "blacklist") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-blacklisted").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user})) + else if (res.reason == "cooldown") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-cooldown").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,until:res.cooldownUntil})) + else if (res.reason == "global-limit") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"global"})) + else if (res.reason == "global-user-limit") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"global-user"})) + else if (res.reason == "option-limit") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option"})) + else if (res.reason == "option-user-limit") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option-user"})) + else instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Unknown invalid_permission reason => calculation failed #1",layout:"advanced"})) + return cancel() + } + + //start ticket creation + if (option.exists("openticket:questions") && option.get("openticket:questions").value.length > 0){ + //send modal + instance.modal(await openticket.builders.modals.getSafe("openticket:ticket-questions").build(source,{guild,channel,user,option})) + }else{ + //create ticket + await instance.defer(true) + const res = await openticket.actions.get("openticket:create-ticket").run(source,{guild,user,answers:[],option}) + if (!res.channel || !res.ticket){ + //error + await instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild,channel:instance.channel,user,error:"Unable to receive ticket or channel from callback! #1",layout:"advanced"})) + return cancel() + } + await instance.reply(await openticket.builders.messages.getSafe("openticket:ticket-created").build(source,{guild,channel:res.channel,user,ticket:res.ticket})) + } + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'ticket' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} + +export const registerButtonResponders = async () => { + //TICKET OPTION BUTTON RESPONDER + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:ticket-option",/^od:ticket-option_/)) + openticket.responders.buttons.get("openticket:ticket-option").workers.add( + new api.ODWorker("openticket:ticket-option",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel:instance.channel,user:instance.user})) + return cancel() + } + + //get option + const optionId = instance.interaction.customId.split("_")[2] + const option = openticket.options.get(optionId) + if (!option || !(option instanceof api.ODTicketOption)){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-option-unknown").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user})) + return cancel() + } + + //check ticket permissions + const res = await openticket.actions.get("openticket:create-ticket-permissions").run("panel-button",{guild,user,option}) + if (!res.valid){ + //error + if (res.reason == "blacklist") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-blacklisted").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user})) + else if (res.reason == "cooldown") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-cooldown").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,until:res.cooldownUntil})) + else if (res.reason == "global-limit") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"global"})) + else if (res.reason == "global-user-limit") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"global-user"})) + else if (res.reason == "option-limit") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option"})) + else if (res.reason == "option-user-limit") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option-user"})) + else instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Unknown invalid_permission reason => calculation failed #1",layout:"advanced"})) + return cancel() + } + + //start ticket creation + if (option.exists("openticket:questions") && option.get("openticket:questions").value.length > 0){ + //send modal + instance.modal(await openticket.builders.modals.getSafe("openticket:ticket-questions").build("panel-button",{guild,channel,user,option})) + }else{ + //create ticket + await instance.defer("reply",true) + const res = await openticket.actions.get("openticket:create-ticket").run("panel-button",{guild,user,answers:[],option}) + if (!res.channel || !res.ticket){ + //error + await instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild,channel:instance.channel,user,error:"Unable to receive ticket or channel from callback! #1",layout:"advanced"})) + return cancel() + } + if (generalConfig.data.system.replyOnTicketCreation) await instance.reply(await openticket.builders.messages.getSafe("openticket:ticket-created").build("panel-button",{guild,channel:res.channel,user,ticket:res.ticket})) + } + }) + ) +} + +export const registerDropdownResponders = async () => { + //PANEL DROPDOWN TICKETS DROPDOWN RESPONDER + openticket.responders.dropdowns.add(new api.ODDropdownResponder("openticket:panel-dropdown-tickets",/^od:panel-dropdown_/)) + openticket.responders.dropdowns.get("openticket:panel-dropdown-tickets").workers.add( + new api.ODWorker("openticket:panel-dropdown-tickets",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel:instance.channel,user:instance.user})) + return cancel() + } + + //get option + const optionId = instance.values.getStringValues()[0].split("_")[2] + const option = openticket.options.get(optionId) + if (!option || !(option instanceof api.ODTicketOption)){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-option-unknown").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user})) + return cancel() + } + + //check ticket permissions + const res = await openticket.actions.get("openticket:create-ticket-permissions").run("panel-dropdown",{guild,user,option}) + if (!res.valid){ + //error + if (res.reason == "blacklist") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-blacklisted").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user})) + else if (res.reason == "cooldown") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-cooldown").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,until:res.cooldownUntil})) + else if (res.reason == "global-limit") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"global"})) + else if (res.reason == "global-user-limit") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"global-user"})) + else if (res.reason == "option-limit") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option"})) + else if (res.reason == "option-user-limit") instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option-user"})) + else instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Unknown invalid_permission reason => calculation failed #1",layout:"advanced"})) + return cancel() + } + + //start ticket creation + if (option.exists("openticket:questions") && option.get("openticket:questions").value.length > 0){ + //send modal + instance.modal(await openticket.builders.modals.getSafe("openticket:ticket-questions").build("panel-dropdown",{guild,channel,user,option})) + }else{ + //create ticket + await instance.defer("reply",true) + const res = await openticket.actions.get("openticket:create-ticket").run("panel-dropdown",{guild,user,answers:[],option}) + if (!res.channel || !res.ticket){ + //error + await instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild,channel:instance.channel,user,error:"Unable to receive ticket or channel from callback! #1",layout:"advanced"})) + return cancel() + } + if (generalConfig.data.system.replyOnTicketCreation) await instance.reply(await openticket.builders.messages.getSafe("openticket:ticket-created").build("panel-dropdown",{guild,channel:res.channel,user,ticket:res.ticket})) + } + }) + ) +} + +export const registerModalResponders = async () => { + //TICKET QUESTIONS RESPONDER + openticket.responders.modals.add(new api.ODModalResponder("openticket:ticket-questions",/^od:ticket-questions_/)) + openticket.responders.modals.get("openticket:ticket-questions").workers.add([ + new api.ODWorker("openticket:ticket-questions",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!channel) throw new api.ODSystemError("The 'Ticket Questions' modal Requires a channel for responding!") + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel,user:instance.user})) + return cancel() + } + + const originalSource = instance.interaction.customId.split("_")[2] as ("panel-button"|"panel-dropdown"|"slash"|"text"|"other") + + //get option + const optionId = instance.interaction.customId.split("_")[1] + const option = openticket.options.get(optionId) + if (!option || !(option instanceof api.ODTicketOption)){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-option-unknown").build(source,{guild:instance.guild,channel,user:instance.user})) + return cancel() + } + + //get answers + const answers: {id:string,name:string,type:"short"|"paragraph",value:string|null}[] = [] + option.get("openticket:questions").value.forEach((id) => { + const question = openticket.questions.get(id) + if (!question) return + if (question instanceof api.ODShortQuestion){ + answers.push({ + id, + name:question.exists("openticket:name") ? question.get("openticket:name")?.value : id, + type:"short", + value:instance.values.getTextField(id,false) + }) + }else if (question instanceof api.ODParagraphQuestion){ + answers.push({ + id, + name:question.exists("openticket:name") ? question.get("openticket:name")?.value : id, + type:"paragraph", + value:instance.values.getTextField(id,false) + }) + } + }) + + //create ticket + await instance.defer("reply",true) + const res = await openticket.actions.get("openticket:create-ticket").run(originalSource,{guild,user,answers,option}) + if (!res.channel || !res.ticket){ + //error + await instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild,channel,user,error:"Unable to receive ticket or channel from callback! #2",layout:"advanced"})) + return cancel() + } + if (generalConfig.data.system.replyOnTicketCreation) await instance.reply(await openticket.builders.messages.getSafe("openticket:ticket-created").build(originalSource,{guild,channel:res.channel,user,ticket:res.ticket})) + }) + ]) +} \ No newline at end of file diff --git a/src/commands/unclaim.ts b/src/commands/unclaim.ts new file mode 100644 index 0000000..784fa8a --- /dev/null +++ b/src/commands/unclaim.ts @@ -0,0 +1,136 @@ +/////////////////////////////////////// +//UNCLAIM COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //UNCLAIM COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:unclaim",generalConfig.data.prefix,"unclaim")) + openticket.responders.commands.get("openticket:unclaim").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.unclaim + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:unclaim",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when not claimed + if (!ticket.get("openticket:claimed").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:"Ticket is not claimed yet!",layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const reason = instance.options.getString("reason",false) + + //start unclaiming ticket + await instance.defer(false) + await openticket.actions.get("openticket:unclaim-ticket").run(source,{guild,channel,user,ticket,reason,sendMessage:false}) + await instance.reply(await openticket.builders.messages.getSafe("openticket:unclaim-message").build(source,{guild,channel,user,ticket,reason})) + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'unclaim' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} + +export const registerButtonResponders = async () => { + //UNCLAIM TICKET BUTTON RESPONDER + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:unclaim-ticket",/^od:unclaim-ticket/)) + openticket.responders.buttons.get("openticket:unclaim-ticket").workers.add( + new api.ODWorker("openticket:unclaim-ticket",0,async (instance,params,source,cancel) => { + const originalSource = instance.interaction.customId.split("_")[1] + if (originalSource != "ticket-message" && originalSource != "claim-message") return + + if (originalSource == "ticket-message") await openticket.verifybars.get("openticket:unclaim-ticket-ticket-message").activate(instance) + else await openticket.verifybars.get("openticket:unclaim-ticket-claim-message").activate(instance) + }) + ) +} + +export const registerModalResponders = async () => { + //UNCLAIM WITH REASON MODAL RESPONDER + openticket.responders.modals.add(new api.ODModalResponder("openticket:unclaim-ticket-reason",/^od:unclaim-ticket-reason_/)) + openticket.responders.modals.get("openticket:unclaim-ticket-reason").workers.add([ + new api.ODWorker("openticket:unclaim-ticket-reason",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!channel) return + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel,user:instance.user})) + return cancel() + } + const ticket = openticket.tickets.get(instance.interaction.customId.split("_")[1]) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return + } + + const originalSource = instance.interaction.customId.split("_")[2] as ("ticket-message"|"claim-message"|"other") + const reason = instance.values.getTextField("reason",true) + + //unclaim with reason + if (originalSource == "ticket-message"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:unclaim-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:true}) + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + }else if (originalSource == "claim-message"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:unclaim-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:unclaim-message").build("other",{guild,channel,user,ticket,reason})) + }else if (originalSource == "other"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:unclaim-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:true}) + } + }) + ]) +} \ No newline at end of file diff --git a/src/commands/unpin.ts b/src/commands/unpin.ts new file mode 100644 index 0000000..c39daf2 --- /dev/null +++ b/src/commands/unpin.ts @@ -0,0 +1,136 @@ +/////////////////////////////////////// +//UNPIN COMMAND +/////////////////////////////////////// +import {openticket, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") + +export const registerCommandResponders = async () => { + //UNPIN COMMAND RESPONDER + openticket.responders.commands.add(new api.ODCommandResponder("openticket:unpin",generalConfig.data.prefix,"unpin")) + openticket.responders.commands.get("openticket:unpin").workers.add([ + new api.ODWorker("openticket:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.unpin + + if (permissionMode == "none"){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!openticket.permissions.hasPermissions("support",await openticket.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await openticket.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await openticket.builders.messages.getSafe("openticket:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("openticket:unpin",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = openticket.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when not pinned yet + if (!ticket.get("openticket:pinned").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error").build("button",{guild,channel,user,error:"Ticket is not pinned yet!",layout:"simple"})) + return cancel() + } + //return when busy + if (ticket.get("openticket:busy").value){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const reason = instance.options.getString("reason",false) + + //start unpinning ticket + await instance.defer(false) + await openticket.actions.get("openticket:unpin-ticket").run(source,{guild,channel,user,ticket,reason,sendMessage:false}) + await instance.reply(await openticket.builders.messages.getSafe("openticket:unpin-message").build(source,{guild,channel,user,ticket,reason})) + }), + new api.ODWorker("openticket:logs",-1,(instance,params,source,cancel) => { + openticket.log(instance.user.displayName+" used the 'unpin' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} + +export const registerButtonResponders = async () => { + //UNPIN TICKET BUTTON RESPONDER + openticket.responders.buttons.add(new api.ODButtonResponder("openticket:unpin-ticket",/^od:unpin-ticket/)) + openticket.responders.buttons.get("openticket:unpin-ticket").workers.add( + new api.ODWorker("openticket:unpin-ticket",0,async (instance,params,source,cancel) => { + const originalSource = instance.interaction.customId.split("_")[1] + if (originalSource != "ticket-message" && originalSource != "pin-message") return + + if (originalSource == "ticket-message") await openticket.verifybars.get("openticket:unpin-ticket-ticket-message").activate(instance) + else await openticket.verifybars.get("openticket:unpin-ticket-pin-message").activate(instance) + }) + ) +} + +export const registerModalResponders = async () => { + //UNPIN WITH REASON MODAL RESPONDER + openticket.responders.modals.add(new api.ODModalResponder("openticket:unpin-ticket-reason",/^od:unpin-ticket-reason_/)) + openticket.responders.modals.get("openticket:unpin-ticket-reason").workers.add([ + new api.ODWorker("openticket:unpin-ticket-reason",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!channel) return + if (!guild){ + //error + instance.reply(await openticket.builders.messages.getSafe("openticket:error-not-in-guild").build(source,{channel,user:instance.user})) + return cancel() + } + const ticket = openticket.tickets.get(instance.interaction.customId.split("_")[1]) + if (!ticket || channel.isDMBased()){ + instance.reply(await openticket.builders.messages.getSafe("openticket:error-ticket-unknown").build("button",{guild,channel,user})) + return + } + + const originalSource = instance.interaction.customId.split("_")[2] as ("ticket-message"|"pin-message"|"other") + const reason = instance.values.getTextField("reason",true) + + //unpin with reason + if (originalSource == "ticket-message"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:unpin-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:true}) + await instance.update(await openticket.builders.messages.getSafe("openticket:ticket-message").build("other",{guild,channel,user,ticket})) + }else if (originalSource == "pin-message"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:unpin-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:false}) + await instance.update(await openticket.builders.messages.getSafe("openticket:unpin-message").build("other",{guild,channel,user,ticket,reason})) + }else if (originalSource == "other"){ + await instance.defer("update",false) + await openticket.actions.get("openticket:unpin-ticket").run(originalSource,{guild,channel,user,ticket,reason,sendMessage:true}) + } + }) + ]) +} \ No newline at end of file diff --git a/src/core/api/api.ts b/src/core/api/api.ts new file mode 100644 index 0000000..387de94 --- /dev/null +++ b/src/core/api/api.ts @@ -0,0 +1,60 @@ +//MAIN MODULE +export * from "./main" + +//BASE MODULES +export * from "./modules/base" +export * from "./modules/event" +export * from "./modules/config" +export * from "./modules/database" +export * from "./modules/language" +export * from "./modules/flag" +export * from "./modules/console" +export * from "./modules/defaults" +export * from "./modules/plugin" +export * from "./modules/checker" +export * from "./modules/client" +export * from "./modules/worker" +export * from "./modules/builder" +export * from "./modules/responder" +export * from "./modules/action" +export * from "./modules/permission" +export * from "./modules/helpmenu" +export * from "./modules/session" +export * from "./modules/stat" +export * from "./modules/code" +export * from "./modules/cooldown" +export * from "./modules/post" +export * from "./modules/verifybar" +export * from "./modules/startscreen" + +//OPENTICKET DEFAULT MODULES +export * from "./defaults/base" +export * from "./defaults/event" +export * from "./defaults/config" +export * from "./defaults/database" +export * from "./defaults/checker" +export * from "./defaults/client" +export * from "./defaults/language" +export * from "./defaults/builder" +export * from "./defaults/responder" +export * from "./defaults/action" +export * from "./defaults/flag" +export * from "./defaults/permission" +export * from "./defaults/helpmenu" +export * from "./defaults/session" +export * from "./defaults/stat" +export * from "./defaults/worker" +export * from "./defaults/code" +export * from "./defaults/cooldown" +export * from "./defaults/post" +export * from "./defaults/startscreen" +export * from "./defaults/console" + +//OPENTICKET MODULES +export * from "./openticket/question" +export * from "./openticket/option" +export * from "./openticket/panel" +export * from "./openticket/ticket" +export * from "./openticket/blacklist" +export * from "./openticket/transcript" +export * from "./openticket/role" \ No newline at end of file diff --git a/src/core/api/defaults/action.ts b/src/core/api/defaults/action.ts new file mode 100644 index 0000000..c8d68a8 --- /dev/null +++ b/src/core/api/defaults/action.ts @@ -0,0 +1,154 @@ +/////////////////////////////////////// +//DEFAULT ACTION MODULE +/////////////////////////////////////// +import { ODValidId } from "../modules/base" +import { ODAction, ODActionManager } from "../modules/action" +import { ODWorkerManager_Default } from "./worker" +import * as discord from "discord.js" +import { ODRoleOption, ODTicketOption } from "../openticket/option" +import { ODTicket, ODTicketClearFilter } from "../openticket/ticket" +import { ODTranscriptCompiler, ODTranscriptCompilerCompileResult } from "../openticket/transcript" +import { ODMessageBuildSentResult } from "../modules/builder" +import { ODRole, OTRoleUpdateMode, OTRoleUpdateResult } from "../openticket/role" + +/**## ODActionManagerIds_Default `type` + * This type is an array of ids available in the `ODActionManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODActionManagerIds_Default { + "openticket:create-ticket-permissions":{ + source:"panel-button"|"panel-dropdown"|"slash"|"text"|"other", + params:{guild:discord.Guild,user:discord.User,option:ODTicketOption}, + result:{valid:boolean,reason:"blacklist"|"cooldown"|"global-limit"|"global-user-limit"|"option-limit"|"option-user-limit"|null,cooldownUntil?:Date}, + workers:"openticket:check-blacklist"|"openticket:check-cooldown"|"openticket:check-global-limits"|"openticket:check-option-limits"|"openticket:valid" + }, + "openticket:create-transcript":{ + source:"slash"|"text"|"ticket-message"|"reopen-message"|"close-message"|"autoclose-message"|"autodelete"|"clear"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket}, + result:{compiler:ODTranscriptCompiler, success:boolean, result:ODTranscriptCompilerCompileResult, errorReason:string|null, pendingMessage:ODMessageBuildSentResult|null, participants:{user:discord.User,role:"creator"|"participant"|"admin"}[]}, + workers:"openticket:select-compiler"|"openticket:init-transcript"|"openticket:compile-transcript"|"openticket:ready-transcript"|"openticket:logs" + }, + "openticket:create-ticket":{ + source:"panel-button"|"panel-dropdown"|"slash"|"text"|"other", + params:{guild:discord.Guild,user:discord.User,option:ODTicketOption,answers:{id:string,name:string,type:"short"|"paragraph",value:string|null}[]}, + result:{channel:discord.GuildTextBasedChannel,ticket:ODTicket}, + workers:"openticket:create-ticket"|"openticket:send-ticket-message"|"openticket:discord-logs"|"openticket:logs" + }, + "openticket:close-ticket":{ + source:"slash"|"text"|"ticket-message"|"reopen-message"|"autoclose"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,sendMessage:boolean}, + result:{}, + workers:"openticket:close-ticket"|"openticket:discord-logs"|"openticket:logs" + }, + "openticket:delete-ticket":{ + source:"slash"|"text"|"ticket-message"|"reopen-message"|"close-message"|"autoclose-message"|"autodelete"|"clear"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,sendMessage:boolean,withoutTranscript:boolean}, + result:{}, + workers:"openticket:delete-ticket"|"openticket:discord-logs"|"openticket:delete-channel"|"openticket:logs" + }, + "openticket:reopen-ticket":{ + source:"slash"|"text"|"ticket-message"|"close-message"|"autoclose-message"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,sendMessage:boolean}, + result:{}, + workers:"openticket:reopen-ticket"|"openticket:discord-logs"|"openticket:logs" + }, + "openticket:claim-ticket":{ + source:"slash"|"text"|"ticket-message"|"unclaim-message"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,sendMessage:boolean}, + result:{}, + workers:"openticket:claim-ticket"|"openticket:discord-logs"|"openticket:logs" + }, + "openticket:unclaim-ticket":{ + source:"slash"|"text"|"ticket-message"|"claim-message"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,sendMessage:boolean}, + result:{}, + workers:"openticket:unclaim-ticket"|"openticket:discord-logs"|"openticket:logs" + }, + "openticket:pin-ticket":{ + source:"slash"|"text"|"ticket-message"|"unpin-message"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,sendMessage:boolean}, + result:{}, + workers:"openticket:pin-ticket"|"openticket:discord-logs"|"openticket:logs" + }, + "openticket:unpin-ticket":{ + source:"slash"|"text"|"ticket-message"|"pin-message"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,sendMessage:boolean}, + result:{}, + workers:"openticket:unpin-ticket"|"openticket:discord-logs"|"openticket:logs" + }, + "openticket:rename-ticket":{ + source:"slash"|"text"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,sendMessage:boolean,data:string}, + result:{}, + workers:"openticket:rename-ticket"|"openticket:discord-logs"|"openticket:logs" + }, + "openticket:move-ticket":{ + source:"slash"|"text"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,sendMessage:boolean,data:ODTicketOption}, + result:{}, + workers:"openticket:move-ticket"|"openticket:discord-logs"|"openticket:logs" + }, + "openticket:add-ticket-user":{ + source:"slash"|"text"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,sendMessage:boolean,data:discord.User}, + result:{}, + workers:"openticket:add-ticket-user"|"openticket:discord-logs"|"openticket:logs" + }, + "openticket:remove-ticket-user":{ + source:"slash"|"text"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,sendMessage:boolean,data:discord.User}, + result:{}, + workers:"openticket:remove-ticket-user"|"openticket:discord-logs"|"openticket:logs" + }, + "openticket:reaction-role":{ + source:"panel-button"|"other", + params:{guild:discord.Guild,user:discord.User,option:ODRoleOption,overwriteMode:OTRoleUpdateMode|null}, + result:{result:OTRoleUpdateResult[],role:ODRole}, + workers:"openticket:reaction-role"|"openticket:logs" + }, + "openticket:clear-tickets":{ + source:"slash"|"text"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:ODTicket[]}, + result:{list:string[]}, + workers:"openticket:clear-tickets"|"openticket:discord-logs"|"openticket:logs" + } +} + +/**## ODActionManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODActionManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.actions`! + */ +export class ODActionManager_Default extends ODActionManager { + get(id:ActionId): ODAction_Default + get(id:ODValidId): ODAction|null + + get(id:ODValidId): ODAction|null { + return super.get(id) + } + + remove(id:ActionId): ODAction_Default + remove(id:ODValidId): ODAction|null + + remove(id:ODValidId): ODAction|null { + return super.remove(id) + } + + exists(id:keyof ODActionManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODAction_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODAction class. + * It doesn't add any extra features! + * + * This default class is made for the default `ODAction`'s! + */ +export class ODAction_Default extends ODAction { + declare workers: ODWorkerManager_Default +} \ No newline at end of file diff --git a/src/core/api/defaults/base.ts b/src/core/api/defaults/base.ts new file mode 100644 index 0000000..fceda6e --- /dev/null +++ b/src/core/api/defaults/base.ts @@ -0,0 +1,45 @@ +/////////////////////////////////////// +//BASE MODULE +/////////////////////////////////////// +import { ODVersion, ODVersionManager, ODValidId } from "../modules/base" + +/**## ODVersionManagerIds_Default `type` + * This type is an array of ids available in the `ODVersionManager` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODVersionManagerIds_Default { + "openticket:version":ODVersion, + "openticket:last-version":ODVersion, + "openticket:api":ODVersion, + "openticket:transcripts":ODVersion, + "openticket:livestatus":ODVersion +} + +/**## ODFlagManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODFlagManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.flags`! + */ +export class ODVersionManager_Default extends ODVersionManager { + get(id:VersionId): ODVersionManagerIds_Default[VersionId] + get(id:ODValidId): ODVersion|null + + get(id:ODValidId): ODVersion|null { + return super.get(id) + } + + remove(id:VersionId): ODVersionManagerIds_Default[VersionId] + remove(id:ODValidId): ODVersion|null + + remove(id:ODValidId): ODVersion|null { + return super.remove(id) + } + + exists(id:keyof ODVersionManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/builder.ts b/src/core/api/defaults/builder.ts new file mode 100644 index 0000000..0620071 --- /dev/null +++ b/src/core/api/defaults/builder.ts @@ -0,0 +1,532 @@ +/////////////////////////////////////// +//DEFAULT BUILDER MODULE +/////////////////////////////////////// +import { ODValidButtonColor, ODValidId } from "../modules/base" +import { ODBuilderManager, ODButton, ODButtonInstance, ODButtonManager, ODDropdown, ODDropdownInstance, ODDropdownManager, ODEmbed, ODEmbedInstance, ODEmbedManager, ODFile, ODFileInstance, ODFileManager, ODMessage, ODMessageInstance, ODMessageManager, ODModal, ODModalInstance, ODModalManager } from "../modules/builder" +import { ODWorkerManager_Default } from "./worker" +import { ODTicket, ODTicketClearFilter } from "../openticket/ticket" +import { ODPermissionEmbedType } from "../defaults/permission" +import { ODTextCommandErrorInvalidOption, ODTextCommandErrorMissingOption, ODTextCommandErrorUnknownCommand } from "../modules/client" +import { ODPanel } from "../openticket/panel" +import { ODRoleOption, ODTicketOption, ODWebsiteOption } from "../openticket/option" +import { ODVerifyBar } from "../modules/verifybar" +import * as discord from "discord.js" +import { ODTranscriptCompiler, ODTranscriptCompilerCompileResult } from "../openticket/transcript" +import { ODRole, OTRoleUpdateResult } from "../openticket/role" + +/**## ODBuilderManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODBuilderManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.builders`! + */ +export class ODBuilderManager_Default extends ODBuilderManager { + declare buttons: ODButtonManager_Default + declare dropdowns: ODDropdownManager_Default + declare files: ODFileManager_Default + declare embeds: ODEmbedManager_Default + declare messages: ODMessageManager_Default + declare modals: ODModalManager_Default +} + +/**## ODButtonManagerIds_Default `type` + * This type is an array of ids available in the `ODButtonManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODButtonManagerIds_Default { + "openticket:verifybar-success":{source:"verifybar"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,verifybar:ODVerifyBar,customData?:string,customColor?:ODValidButtonColor,customLabel?:string,customEmoji?:string},workers:"openticket:verifybar-success"}, + "openticket:verifybar-failure":{source:"verifybar"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,verifybar:ODVerifyBar,customData?:string,customColor?:ODValidButtonColor,customLabel?:string,customEmoji?:string},workers:"openticket:verifybar-failure"}, + + "openticket:error-ticket-deprecated-transcript":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{},workers:"openticket:error-ticket-deprecated-transcript"}, + + "openticket:help-menu-previous":{source:"text"|"slash"|"button"|"other",params:{mode:"slash"|"text",page:number},workers:"openticket:help-menu-previous"}, + "openticket:help-menu-next":{source:"text"|"slash"|"button"|"other",params:{mode:"slash"|"text",page:number},workers:"openticket:help-menu-next"}, + "openticket:help-menu-page":{source:"text"|"slash"|"button"|"other",params:{mode:"slash"|"text",page:number},workers:"openticket:help-menu-page"} + "openticket:help-menu-switch":{source:"text"|"slash"|"button"|"other",params:{mode:"slash"|"text",page:number},workers:"openticket:help-menu-switch"}, + + "openticket:ticket-option":{source:"slash"|"text"|"auto-update"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,panel:ODPanel,option:ODTicketOption},workers:"openticket:ticket-option"}, + "openticket:website-option":{source:"slash"|"text"|"auto-update"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,panel:ODPanel,option:ODWebsiteOption},workers:"openticket:website-option"}, + "openticket:role-option":{source:"slash"|"text"|"auto-update"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,panel:ODPanel,option:ODRoleOption},workers:"openticket:role-option"} + + "openticket:visit-ticket":{source:"ticket-created"|"dm"|"logs"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:visit-ticket"}, + + "openticket:close-ticket":{source:"ticket-message"|"reopen-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:close-ticket"}, + "openticket:delete-ticket":{source:"ticket-message"|"close-message"|"autoclose-message"|"reopen-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:delete-ticket"}, + "openticket:reopen-ticket":{source:"ticket-message"|"close-message"|"autoclose-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:reopen-ticket"}, + "openticket:claim-ticket":{source:"ticket-message"|"unclaim-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:claim-ticket"}, + "openticket:unclaim-ticket":{source:"ticket-message"|"claim-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:unclaim-ticket"}, + "openticket:pin-ticket":{source:"ticket-message"|"unpin-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:pin-ticket"}, + "openticket:unpin-ticket":{source:"ticket-message"|"pin-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:unpin-ticket"}, + + "openticket:transcript-html-visit":{source:"channel"|"creator-dm"|"participant-dm"|"active-admin-dm"|"every-admin-dm"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,compiler:ODTranscriptCompiler<{url:string}>,result:ODTranscriptCompilerCompileResult<{url:string}>},workers:"openticket:transcript-html-visit"}, + "openticket:transcript-error-retry":{source:"slash"|"text"|"ticket-message"|"reopen-message"|"close-message"|"autoclose-message"|"autodelete"|"clear"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,compiler:ODTranscriptCompiler,reason:string|null},workers:"openticket:transcript-error-retry"}, + "openticket:transcript-error-continue":{source:"slash"|"text"|"ticket-message"|"reopen-message"|"close-message"|"autoclose-message"|"autodelete"|"clear"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,compiler:ODTranscriptCompiler,reason:string|null},workers:"openticket:transcript-error-continue"}, + + "openticket:clear-continue":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:string[]},workers:"openticket:clear-continue"}, +} + +/**## ODButtonManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODButtonManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.builders.buttons`! + */ +export class ODButtonManager_Default extends ODButtonManager { + get(id:ButtonId): ODButton_Default + get(id:ODValidId): ODButton|null + + get(id:ODValidId): ODButton|null { + return super.get(id) + } + + remove(id:ButtonId): ODButton_Default + remove(id:ODValidId): ODButton|null + + remove(id:ODValidId): ODButton|null { + return super.remove(id) + } + + exists(id:keyof ODButtonManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + getSafe(id:ButtonId): ODButton_Default + getSafe(id:ODValidId): ODButton + + getSafe(id:ODValidId): ODButton { + return super.getSafe(id) + } +} + +/**## ODButton_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODButton class. + * It doesn't add any extra features! + * + * This default class is made for the default `ODButton`'s! + */ +export class ODButton_Default extends ODButton { + declare workers: ODWorkerManager_Default +} + +/**## ODDropdownManagerIds_Default `type` + * This type is an array of ids available in the `ODDropdownManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODDropdownManagerIds_Default { + "openticket:panel-dropdown-tickets":{source:"slash"|"text"|"auto-update"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,panel:ODPanel,options:ODTicketOption[]},workers:"openticket:panel-dropdown-tickets"} +} + +/**## ODDropdownManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODDropdownManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.builders.dropdowns`! + */ +export class ODDropdownManager_Default extends ODDropdownManager { + get(id:DropdownId): ODDropdown_Default + get(id:ODValidId): ODDropdown|null + + get(id:ODValidId): ODDropdown|null { + return super.get(id) + } + + remove(id:DropdownId): ODDropdown_Default + remove(id:ODValidId): ODDropdown|null + + remove(id:ODValidId): ODDropdown|null { + return super.remove(id) + } + + exists(id:keyof ODDropdownManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + getSafe(id:DropdownId): ODDropdown_Default + getSafe(id:ODValidId): ODDropdown + + getSafe(id:ODValidId): ODDropdown { + return super.getSafe(id) + } +} + +/**## ODDropdown_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODDropdown class. + * It doesn't add any extra features! + * + * This default class is made for the default `ODDropdown`'s! + */ +export class ODDropdown_Default extends ODDropdown { + declare workers: ODWorkerManager_Default +} + +/**## ODFileManagerIds_Default `type` + * This type is an array of ids available in the `ODFileManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODFileManagerIds_Default { + "openticket:text-transcript":{source:"channel"|"creator-dm"|"participant-dm"|"active-admin-dm"|"every-admin-dm"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,compiler:ODTranscriptCompiler,result:ODTranscriptCompilerCompileResult},workers:"openticket:text-transcript"} +} + +/**## ODFileManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODFileManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.builders.files`! + */ +export class ODFileManager_Default extends ODFileManager { + get(id:FileId): ODFile_Default + get(id:ODValidId): ODFile|null + + get(id:ODValidId): ODFile|null { + return super.get(id) + } + + remove(id:FileId): ODFile_Default + remove(id:ODValidId): ODFile|null + + remove(id:ODValidId): ODFile|null { + return super.remove(id) + } + + exists(id:keyof ODFileManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + getSafe(id:FileId): ODFile_Default + getSafe(id:ODValidId): ODFile + + getSafe(id:ODValidId): ODFile { + return super.getSafe(id) + } +} + +/**## ODFile_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODFile class. + * It doesn't add any extra features! + * + * This default class is made for the default `ODFile`'s! + */ +export class ODFile_Default extends ODFile { + declare workers: ODWorkerManager_Default +} + +/**## ODEmbedManagerIds_Default `type` + * This type is an array of ids available in the `ODEmbedManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODEmbedManagerIds_Default { + "openticket:error":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,error:string,layout:"simple"|"advanced"},workers:"openticket:error"}, + "openticket:error-option-missing":{source:"slash"|"text"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,error:ODTextCommandErrorMissingOption},workers:"openticket:error-option-missing"}, + "openticket:error-option-invalid":{source:"slash"|"text"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,error:ODTextCommandErrorInvalidOption},workers:"openticket:error-option-invalid"}, + "openticket:error-unknown-command":{source:"slash"|"text"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,error:ODTextCommandErrorUnknownCommand},workers:"openticket:error-unknown-command"}, + "openticket:error-no-permissions":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,permissions:ODPermissionEmbedType[]},workers:"openticket:error-no-permissions"}, + "openticket:error-no-permissions-cooldown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,until?:Date},workers:"openticket:error-no-permissions-cooldown"}, + "openticket:error-no-permissions-blacklisted":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-no-permissions-blacklisted"}, + "openticket:error-no-permissions-limits":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,limit:"global"|"global-user"|"option"|"option-user"},workers:"openticket:error-no-permissions-limits"}, + "openticket:error-responder-timeout":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-responder-timeout"}, + "openticket:error-ticket-unknown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-ticket-unknown"}, + "openticket:error-ticket-deprecated":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-ticket-deprecated"}, + "openticket:error-option-unknown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-option-unknown"}, + "openticket:error-panel-unknown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-panel-unknown"}, + "openticket:error-not-in-guild":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-not-in-guild"}, + "openticket:error-channel-rename":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"ticket-pin"|"ticket-unpin"|"ticket-rename"|"ticket-move"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,originalName:string,newName:string},workers:"openticket:error-channel-rename"}, + "openticket:error-ticket-busy":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-ticket-busy"}, + + "openticket:help-menu":{source:"text"|"slash"|"button"|"other",params:{mode:"slash"|"text",page:number},workers:"openticket:help-menu"}, + + "openticket:stats-global":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:stats-global"}, + "openticket:stats-ticket":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,scopeData:ODTicket},workers:"openticket:stats-ticket"}, + "openticket:stats-user":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,scopeData:discord.User},workers:"openticket:stats-user"|"openticket:easter-egg"}, + "openticket:stats-reset":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,reason:string|null},workers:"openticket:stats-reset"}, + "openticket:stats-ticket-unknown":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,id:string},workers:"openticket:stats-ticket-unknown"}, + + "openticket:panel":{source:"slash"|"text"|"auto-update"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,panel:ODPanel},workers:"openticket:panel"}, + "openticket:ticket-created":{source:"panel-button"|"panel-dropdown"|"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:ticket-created"}, + "openticket:ticket-created-dm":{source:"panel-button"|"panel-dropdown"|"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:ticket-created-dm"}, + "openticket:ticket-created-logs":{source:"panel-button"|"panel-dropdown"|"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:ticket-created-logs"}, + "openticket:ticket-message":{source:"panel-button"|"panel-dropdown"|"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:ticket-message"}, + "openticket:close-message":{source:"slash"|"text"|"ticket-message"|"reopen-message"|"autoclose"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:close-message"}, + "openticket:reopen-message":{source:"slash"|"text"|"ticket-message"|"close-message"|"autoclose-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:reopen-message"}, + "openticket:delete-message":{source:"slash"|"text"|"ticket-message"|"reopen-message"|"close-message"|"autoclose-message"|"autodelete"|"clear"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:delete-message"}, + "openticket:claim-message":{source:"slash"|"text"|"ticket-message"|"unclaim-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:claim-message"}, + "openticket:unclaim-message":{source:"slash"|"text"|"ticket-message"|"claim-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:unclaim-message"}, + "openticket:pin-message":{source:"slash"|"text"|"ticket-message"|"unpin-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:pin-message"}, + "openticket:unpin-message":{source:"slash"|"text"|"ticket-message"|"pin-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:unpin-message"}, + "openticket:rename-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,data:string},workers:"openticket:rename-message"}, + "openticket:move-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,data:ODTicketOption},workers:"openticket:move-message"}, + "openticket:add-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,data:discord.User},workers:"openticket:add-message"}, + "openticket:remove-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,data:discord.User},workers:"openticket:remove-message"}, + "openticket:ticket-action-dm":{source:"slash"|"text"|"ticket-message"|"close-message"|"reopen-message"|"delete-message"|"claim-message"|"unclaim-message"|"pin-message"|"unpin-message"|"autoclose-message"|"autoclose"|"autodelete"|"clear"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,mode:"close"|"reopen"|"delete"|"claim"|"unclaim"|"pin"|"unpin"|"rename"|"move"|"add"|"remove",ticket:ODTicket,reason:string|null,additionalData:null|string|discord.User|ODTicketOption},workers:"openticket:ticket-action-dm"}, + "openticket:ticket-action-logs":{source:"slash"|"text"|"ticket-message"|"close-message"|"reopen-message"|"delete-message"|"claim-message"|"unclaim-message"|"pin-message"|"unpin-message"|"autoclose-message"|"autoclose"|"autodelete"|"clear"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,mode:"close"|"reopen"|"delete"|"claim"|"unclaim"|"pin"|"unpin"|"rename"|"move"|"add"|"remove",ticket:ODTicket,reason:string|null,additionalData:null|string|discord.User|ODTicketOption},workers:"openticket:ticket-action-logs"}, + + "openticket:blacklist-view":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:blacklist-view"}, + "openticket:blacklist-get":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,data:discord.User},workers:"openticket:blacklist-get"}, + "openticket:blacklist-add":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,data:discord.User,reason:string|null},workers:"openticket:blacklist-add"}, + "openticket:blacklist-remove":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,data:discord.User,reason:string|null},workers:"openticket:blacklist-remove"} + "openticket:blacklist-dm":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,mode:"add"|"remove",data:discord.User,reason:string|null},workers:"openticket:blacklist-dm"}, + "openticket:blacklist-logs":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,mode:"add"|"remove",data:discord.User,reason:string|null},workers:"openticket:blacklist-logs"}, + + "openticket:transcript-text-ready":{source:"channel"|"creator-dm"|"participant-dm"|"active-admin-dm"|"every-admin-dm"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,compiler:ODTranscriptCompiler<{contents:string}>,result:ODTranscriptCompilerCompileResult<{contents:string}>},workers:"openticket:transcript-text-ready"}, + "openticket:transcript-html-ready":{source:"channel"|"creator-dm"|"participant-dm"|"active-admin-dm"|"every-admin-dm"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,compiler:ODTranscriptCompiler<{url:string}>,result:ODTranscriptCompilerCompileResult<{url:string}>},workers:"openticket:transcript-html-ready"}, + "openticket:transcript-html-progress":{source:"channel"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,compiler:ODTranscriptCompiler<{url:string}>,remaining:number},workers:"openticket:transcript-html-progress"}, + "openticket:transcript-error":{source:"slash"|"text"|"ticket-message"|"reopen-message"|"close-message"|"autoclose-message"|"autodelete"|"clear"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,compiler:ODTranscriptCompiler,reason:string|null},workers:"openticket:transcript-error"}, + + "openticket:reaction-role":{source:"panel-button"|"other",params:{guild:discord.Guild,user:discord.User,role:ODRole,result:OTRoleUpdateResult[]},workers:"openticket:reaction-role"}, + "openticket:clear-verify-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:string[]},workers:"openticket:clear-verify-message"}, + "openticket:clear-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:string[]},workers:"openticket:clear-message"}, + "openticket:clear-logs":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:string[]},workers:"openticket:clear-logs"}, + + "openticket:autoclose-message":{source:"timeout"|"leave"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:autoclose-message"}, + "openticket:autodelete-message":{source:"timeout"|"leave"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:autodelete-message"}, + "openticket:autoclose-enable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,time:number,reason:string|null},workers:"openticket:autoclose-enable"}, + "openticket:autodelete-enable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,time:number,reason:string|null},workers:"openticket:autodelete-enable"}, + "openticket:autoclose-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:autoclose-disable"}, + "openticket:autodelete-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:autodelete-disable"}, +} + +/**## ODEmbedManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODEmbedManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.builders.embeds`! + */ +export class ODEmbedManager_Default extends ODEmbedManager { + get(id:EmbedId): ODEmbed_Default + get(id:ODValidId): ODEmbed|null + + get(id:ODValidId): ODEmbed|null { + return super.get(id) + } + + remove(id:EmbedId): ODEmbed_Default + remove(id:ODValidId): ODEmbed|null + + remove(id:ODValidId): ODEmbed|null { + return super.remove(id) + } + + exists(id:keyof ODEmbedManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + getSafe(id:EmbedId): ODEmbed_Default + getSafe(id:ODValidId): ODEmbed + + getSafe(id:ODValidId): ODEmbed { + return super.getSafe(id) + } +} + +/**## ODEmbed_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODEmbed class. + * It doesn't add any extra features! + * + * This default class is made for the default `ODEmbed`'s! + */ +export class ODEmbed_Default extends ODEmbed { + declare workers: ODWorkerManager_Default +} + +/**## ODMessageManagerIds_Default `type` + * This type is an array of ids available in the `ODMessageManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODMessageManagerIds_Default { + "openticket:verifybar-ticket-message":{source:"verifybar",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,verifybar:ODVerifyBar,originalMessage:discord.Message},workers:"openticket:verifybar-ticket-message"}, + "openticket:verifybar-close-message":{source:"verifybar",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,verifybar:ODVerifyBar,originalMessage:discord.Message},workers:"openticket:verifybar-close-message"}, + "openticket:verifybar-reopen-message":{source:"verifybar",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,verifybar:ODVerifyBar,originalMessage:discord.Message},workers:"openticket:verifybar-reopen-message"}, + "openticket:verifybar-claim-message":{source:"verifybar",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,verifybar:ODVerifyBar,originalMessage:discord.Message},workers:"openticket:verifybar-claim-message"}, + "openticket:verifybar-unclaim-message":{source:"verifybar",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,verifybar:ODVerifyBar,originalMessage:discord.Message},workers:"openticket:verifybar-unclaim-message"}, + "openticket:verifybar-pin-message":{source:"verifybar",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,verifybar:ODVerifyBar,originalMessage:discord.Message},workers:"openticket:verifybar-pin-message"}, + "openticket:verifybar-unpin-message":{source:"verifybar",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,verifybar:ODVerifyBar,originalMessage:discord.Message},workers:"openticket:verifybar-unpin-message"} + "openticket:verifybar-autoclose-message":{source:"verifybar",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,verifybar:ODVerifyBar,originalMessage:discord.Message},workers:"openticket:verifybar-autoclose-message"} + + "openticket:error":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,error:string,layout:"simple"|"advanced"},workers:"openticket:error"}, + "openticket:error-option-missing":{source:"slash"|"text"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,error:ODTextCommandErrorMissingOption},workers:"openticket:error-option-missing"}, + "openticket:error-option-invalid":{source:"slash"|"text"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,error:ODTextCommandErrorInvalidOption},workers:"openticket:error-option-invalid"}, + "openticket:error-unknown-command":{source:"slash"|"text"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,error:ODTextCommandErrorUnknownCommand},workers:"openticket:error-unknown-command"}, + "openticket:error-no-permissions":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,permissions:ODPermissionEmbedType[]},workers:"openticket:error-no-permissions"}, + "openticket:error-no-permissions-cooldown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,until?:Date},workers:"openticket:error-no-permissions-cooldown"}, + "openticket:error-no-permissions-blacklisted":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-no-permissions-blacklisted"}, + "openticket:error-no-permissions-limits":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,limit:"global"|"global-user"|"option"|"option-user"},workers:"openticket:error-no-permissions-limits"}, + "openticket:error-responder-timeout":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-responder-timeout"}, + "openticket:error-ticket-unknown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-ticket-unknown"}, + "openticket:error-ticket-deprecated":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-ticket-deprecated"}, + "openticket:error-option-unknown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-option-unknown"}, + "openticket:error-panel-unknown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-panel-unknown"}, + "openticket:error-not-in-guild":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-not-in-guild"}, + "openticket:error-channel-rename":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"ticket-pin"|"ticket-unpin"|"ticket-rename"|"ticket-move"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,originalName:string,newName:string},workers:"openticket:error-channel-rename"}, + "openticket:error-ticket-busy":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:error-ticket-busy"}, + + "openticket:help-menu":{source:"slash"|"text"|"button"|"other",params:{mode:"slash"|"text",page:number},workers:"openticket:help-menu"}, + + "openticket:stats-global":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:stats-global"}, + "openticket:stats-ticket":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,scopeData:ODTicket},workers:"openticket:stats-ticket"}, + "openticket:stats-user":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,scopeData:discord.User},workers:"openticket:stats-user"|"openticket:easter-egg"}, + "openticket:stats-reset":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,reason:string|null},workers:"openticket:stats-reset"}, + "openticket:stats-ticket-unknown":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,id:string},workers:"openticket:stats-ticket-unknown"}, + + "openticket:panel":{source:"slash"|"text"|"auto-update"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,panel:ODPanel},workers:"openticket:panel-layout"|"openticket:panel-components"}, + "openticket:panel-ready":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,panel:ODPanel},workers:"openticket:panel-ready"}, + + "openticket:ticket-created":{source:"panel-button"|"panel-dropdown"|"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:ticket-created"}, + "openticket:ticket-created-dm":{source:"panel-button"|"panel-dropdown"|"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:ticket-created-dm"}, + "openticket:ticket-created-logs":{source:"panel-button"|"panel-dropdown"|"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:ticket-created-logs"}, + "openticket:ticket-message":{source:"panel-button"|"panel-dropdown"|"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:ticket-message-layout"|"openticket:ticket-message-components"|"openticket:ticket-message-disable-components"}, + "openticket:close-message":{source:"slash"|"text"|"ticket-message"|"reopen-message"|"autoclose"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:close-message"}, + "openticket:reopen-message":{source:"slash"|"text"|"ticket-message"|"close-message"|"autoclose-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:reopen-message"}, + "openticket:delete-message":{source:"slash"|"text"|"ticket-message"|"reopen-message"|"close-message"|"autoclose-message"|"autodelete"|"clear"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:delete-message"}, + "openticket:claim-message":{source:"slash"|"text"|"ticket-message"|"unclaim-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:claim-message"}, + "openticket:unclaim-message":{source:"slash"|"text"|"ticket-message"|"claim-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:unclaim-message"}, + "openticket:pin-message":{source:"slash"|"text"|"ticket-message"|"unpin-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:pin-message"}, + "openticket:unpin-message":{source:"slash"|"text"|"ticket-message"|"pin-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:unpin-message"}, + "openticket:rename-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,data:string},workers:"openticket:rename-message"}, + "openticket:move-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,data:ODTicketOption},workers:"openticket:move-message"}, + "openticket:add-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,data:discord.User},workers:"openticket:add-message"}, + "openticket:remove-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null,data:discord.User},workers:"openticket:remove-message"}, + "openticket:ticket-action-dm":{source:"slash"|"text"|"ticket-message"|"close-message"|"reopen-message"|"delete-message"|"claim-message"|"unclaim-message"|"pin-message"|"unpin-message"|"autoclose-message"|"autoclose"|"autodelete"|"clear"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,mode:"close"|"reopen"|"delete"|"claim"|"unclaim"|"pin"|"unpin"|"rename"|"move"|"add"|"remove",ticket:ODTicket,reason:string|null,additionalData:null|string|discord.User|ODTicketOption},workers:"openticket:ticket-action-dm"}, + "openticket:ticket-action-logs":{source:"slash"|"text"|"ticket-message"|"close-message"|"reopen-message"|"delete-message"|"claim-message"|"unclaim-message"|"pin-message"|"unpin-message"|"autoclose-message"|"autoclose"|"autodelete"|"clear"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,mode:"close"|"reopen"|"delete"|"claim"|"unclaim"|"pin"|"unpin"|"rename"|"move"|"add"|"remove",ticket:ODTicket,reason:string|null,additionalData:null|string|discord.User|ODTicketOption},workers:"openticket:ticket-action-logs"}, + + "openticket:blacklist-view":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User},workers:"openticket:blacklist-view"}, + "openticket:blacklist-get":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,data:discord.User},workers:"openticket:blacklist-get"}, + "openticket:blacklist-add":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,data:discord.User,reason:string|null},workers:"openticket:blacklist-add"}, + "openticket:blacklist-remove":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,data:discord.User,reason:string|null},workers:"openticket:blacklist-remove"}, + "openticket:blacklist-dm":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,mode:"add"|"remove",data:discord.User,reason:string|null},workers:"openticket:blacklist-dm"}, + "openticket:blacklist-logs":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,mode:"add"|"remove",data:discord.User,reason:string|null},workers:"openticket:blacklist-logs"}, + + "openticket:transcript-text-ready":{source:"channel"|"creator-dm"|"participant-dm"|"active-admin-dm"|"every-admin-dm"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,compiler:ODTranscriptCompiler<{contents:string}>,result:ODTranscriptCompilerCompileResult<{contents:string}>},workers:"openticket:transcript-text-ready"}, + "openticket:transcript-html-ready":{source:"channel"|"creator-dm"|"participant-dm"|"active-admin-dm"|"every-admin-dm"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,compiler:ODTranscriptCompiler<{url:string}>,result:ODTranscriptCompilerCompileResult<{url:string}>},workers:"openticket:transcript-html-ready"}, + "openticket:transcript-html-progress":{source:"channel"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,compiler:ODTranscriptCompiler<{url:string}>,remaining:number},workers:"openticket:transcript-html-progress"}, + "openticket:transcript-error":{source:"slash"|"text"|"ticket-message"|"reopen-message"|"close-message"|"autoclose-message"|"autodelete"|"clear"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,compiler:ODTranscriptCompiler,reason:string|null},workers:"openticket:transcript-error"}, + + "openticket:reaction-role":{source:"panel-button"|"other",params:{guild:discord.Guild,user:discord.User,role:ODRole,result:OTRoleUpdateResult[]},workers:"openticket:reaction-role"}, + "openticket:clear-verify-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:string[]},workers:"openticket:clear-verify-message"}, + "openticket:clear-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:string[]},workers:"openticket:clear-message"}, + "openticket:clear-logs":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:string[]},workers:"openticket:clear-logs"}, + + "openticket:autoclose-message":{source:"timeout"|"leave"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:autoclose-message"}, + "openticket:autodelete-message":{source:"timeout"|"leave"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:autodelete-message"}, + "openticket:autoclose-enable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,time:number,reason:string|null},workers:"openticket:autoclose-enable"}, + "openticket:autodelete-enable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,time:number,reason:string|null},workers:"openticket:autodelete-enable"}, + "openticket:autoclose-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:autoclose-disable"}, + "openticket:autodelete-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"openticket:autodelete-disable"}, +} + +/**## ODMessageManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODMessageManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.builders.messages`! + */ +export class ODMessageManager_Default extends ODMessageManager { + get(id:MessageId): ODMessage_Default + get(id:ODValidId): ODMessage|null + + get(id:ODValidId): ODMessage|null { + return super.get(id) + } + + remove(id:MessageId): ODMessage_Default + remove(id:ODValidId): ODMessage|null + + remove(id:ODValidId): ODMessage|null { + return super.remove(id) + } + + exists(id:keyof ODMessageManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + getSafe(id:MessageId): ODMessage_Default + getSafe(id:ODValidId): ODMessage + + getSafe(id:ODValidId): ODMessage { + return super.getSafe(id) + } +} + +/**## ODMessage_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODMessage class. + * It doesn't add any extra features! + * + * This default class is made for the default `ODMessage`'s! + */ +export class ODMessage_Default extends ODMessage { + declare workers: ODWorkerManager_Default +} + +/**## ODModalManagerIds_Default `type` + * This type is an array of ids available in the `ODModalManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODModalManagerIds_Default { + "openticket:ticket-questions":{source:"panel-button"|"panel-dropdown"|"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,option:ODTicketOption},workers:"openticket:ticket-questions"} + "openticket:close-ticket-reason":{source:"ticket-message"|"reopen-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:close-ticket-reason"} + "openticket:reopen-ticket-reason":{source:"ticket-message"|"close-message"|"autoclose-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:reopen-ticket-reason"} + "openticket:delete-ticket-reason":{source:"ticket-message"|"reopen-message"|"close-message"|"autoclose-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:delete-ticket-reason"} + "openticket:claim-ticket-reason":{source:"ticket-message"|"unclaim-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:claim-ticket-reason"} + "openticket:unclaim-ticket-reason":{source:"ticket-message"|"claim-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:unclaim-ticket-reason"} + "openticket:pin-ticket-reason":{source:"ticket-message"|"unpin-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:pin-ticket-reason"} + "openticket:unpin-ticket-reason":{source:"ticket-message"|"pin-message"|"other",params:{guild:discord.Guild,channel:discord.TextBasedChannel,user:discord.User,ticket:ODTicket},workers:"openticket:unpin-ticket-reason"} +} + +/**## ODModalManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODModalManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.builders.modals`! + */ +export class ODModalManager_Default extends ODModalManager { + get(id:ModalId): ODModal_Default + get(id:ODValidId): ODModal|null + + get(id:ODValidId): ODModal|null { + return super.get(id) + } + + remove(id:ModalId): ODModal_Default + remove(id:ODValidId): ODModal|null + + remove(id:ODValidId): ODModal|null { + return super.remove(id) + } + + exists(id:keyof ODModalManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + getSafe(id:ModalId): ODModal_Default + getSafe(id:ODValidId): ODModal + + getSafe(id:ODValidId): ODModal { + return super.getSafe(id) + } +} + +/**## ODModal_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODModal class. + * It doesn't add any extra features! + * + * This default class is made for the default `ODModal`'s! + */ +export class ODModal_Default extends ODModal { + declare workers: ODWorkerManager_Default +} \ No newline at end of file diff --git a/src/core/api/defaults/checker.ts b/src/core/api/defaults/checker.ts new file mode 100644 index 0000000..69e6d53 --- /dev/null +++ b/src/core/api/defaults/checker.ts @@ -0,0 +1,378 @@ +/////////////////////////////////////// +//DEFAULT CONFIG CHECKER MODULE +/////////////////////////////////////// +import { ODLanguageManager_Default } from "../api" +import { ODValidId } from "../modules/base" +import { ODCheckerManager, ODChecker, ODCheckerTranslationRegister, ODCheckerRenderer, ODCheckerFunctionManager, ODCheckerResult, ODCheckerFunction } from "../modules/checker" +import ansis from "ansis" + +/**## ODCheckerManagerIds_Default `type` + * This type is an array of ids available in the `ODCheckerManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODCheckerManagerIds_Default { + "openticket:general":ODChecker, + "openticket:options":ODChecker, + "openticket:messages":ODChecker, + "openticket:transcripts":ODChecker +} + +/**## ODCheckerManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODCheckerManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.checkers`! + */ +export class ODCheckerManager_Default extends ODCheckerManager { + declare translation: ODCheckerTranslationRegister_Default + declare renderer: ODCheckerRenderer_Default + declare functions: ODCheckerFunctionManager_Default + + get(id:CheckerId): ODCheckerManagerIds_Default[CheckerId] + get(id:ODValidId): ODChecker|null + + get(id:ODValidId): ODChecker|null { + return super.get(id) + } + + remove(id:CheckerId): ODCheckerManagerIds_Default[CheckerId] + remove(id:ODValidId): ODChecker|null + + remove(id:ODValidId): ODChecker|null { + return super.remove(id) + } + + exists(id:keyof ODCheckerManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODCheckerRenderer_Default `default_class` + * This is a special class that adds type definitions & features to the ODCheckerRenderer class. + * It contains the code that renders the default config checker. + * + * This default class is made for the global variable `openticket.checkers.renderer`! + */ +export class ODCheckerRenderer_Default extends ODCheckerRenderer { + extraHeaderText: string[] = [] + extraFooterText: string[] = [] + extraTopText: string[] = [] + extraBottomText: string[] = [] + + horizontalFiller: string = "=" + verticalFiller: string = "|" + descriptionSeparator: string = " => " + footerTipPrefix: string = "=> " + + disableHeader: boolean = false + disableFooter: boolean = false + + getComponents(compact:boolean, renderEmpty:boolean, translation:ODCheckerTranslationRegister_Default, data:ODCheckerResult): string[] { + const tm = translation + const t = { + headerOpenticket:tm.get("other","openticket:header-openticket") ?? "OPEN TICKET", + headerConfigchecker:tm.get("other","openticket:header-configchecker") ?? "CONFIG CHECKER", + headerDescription:tm.get("other","openticket:header-description") ?? "check for errors in you config files!", + footerError:tm.get("other","openticket:footer-error") ?? "the bot won't start until all {0}'s are fixed!", + footerWarning:tm.get("other","openticket:footer-warning") ?? "it's recommended to fix all {0}'s before starting!", + footerSupport:tm.get("other","openticket:footer-support") ?? "SUPPORT: {0} - DOCS: {1}", + error:tm.get("other","openticket:type-error") ?? "[ERROR]", + warning:tm.get("other","openticket:type-warning") ?? "[WARNING]", + info:tm.get("other","openticket:type-info") ?? "[INFO]", + compactInfo:tm.get("other","openticket:compact-information") ?? "use {0} for more information!", + dataPath:tm.get("other","openticket:data-path") ?? "path", + dataDocs:tm.get("other","openticket:data-docs") ?? "docs", + dataMessage:tm.get("other","openticket:data-message") ?? "message" + } + const hasErrors = data.messages.filter((m) => m.type == "error").length > 0 + const hasWarnings = data.messages.filter((m) => m.type == "warning").length > 0 + const hasInfo = data.messages.filter((m) => m.type == "info").length > 0 + + if (!renderEmpty && !hasErrors && !hasWarnings && (!hasInfo || compact)) return [] + + const headerText = ansis.bold.hex("#f8ba00")(t.headerOpenticket)+" "+t.headerConfigchecker+" => "+ansis.hex("#f8ba00")(t.headerDescription) + const footerErrorText = (hasErrors) ? this.footerTipPrefix+ansis.gray(tm.insertTranslationParams(t.footerError,[ansis.bold.red(t.error)])) : "" + const footerWarningText = (hasWarnings) ? this.footerTipPrefix+ansis.gray(tm.insertTranslationParams(t.footerWarning,[ansis.bold.yellow(t.warning)])) : "" + const footerSupportText = tm.insertTranslationParams(t.footerSupport,[ansis.green("https://discord.dj-dj.be"),ansis.green("https://otdocs.dj-dj.be")]) + const bottomCompactInfo = (compact) ? ansis.gray(tm.insertTranslationParams(t.compactInfo,[ansis.bold.green("npm start -- --checker")])) : "" + + const finalHeader = [headerText,...this.extraHeaderText] + const finalFooter = [footerErrorText,footerWarningText,footerSupportText,...this.extraFooterText] + const finalTop = [...this.extraTopText] + const finalBottom = [bottomCompactInfo,...this.extraBottomText] + const borderLength = this.#getLongestLength([...finalHeader,...finalFooter]) + + const finalComponents: string[] = [] + + //header + if (!this.disableHeader){ + finalHeader.forEach((text) => { + if (text.length < 1) return + finalComponents.push(this.#createBlockFromText(text,borderLength)) + }) + } + finalComponents.push(this.#getHorizontalDivider(borderLength+4)) + + //top + finalTop.forEach((text) => { + if (text.length < 1) return + finalComponents.push(this.verticalFiller+" "+text) + }) + finalComponents.push(this.verticalFiller) + + //messages + if (compact){ + //use compact messages + data.messages.forEach((msg,index) => { + //compact mode doesn't render info + if (msg.type == "info") return + + //check if translation available & use it if possible + const rawTranslation = tm.get("message",msg.messageId.value) + const translatedMessage = (rawTranslation) ? tm.insertTranslationParams(rawTranslation,msg.translationParams) : msg.message + + if (msg.type == "error") finalComponents.push(this.verticalFiller+" "+ansis.bold.red(`${t.error} ${translatedMessage}`)) + else if (msg.type == "warning") finalComponents.push(this.verticalFiller+" "+ansis.bold.yellow(`${t.warning} ${translatedMessage}`)) + + const pathSplitter = msg.path ? ":" : "" + finalComponents.push(this.verticalFiller+ansis.bold(this.descriptionSeparator)+ansis.cyan(`${ansis.magenta(msg.filepath+pathSplitter)} ${msg.path}`)) + if (index != data.messages.length-1) finalComponents.push(this.verticalFiller) + }) + }else{ + //use full messages + data.messages.forEach((msg,index) => { + //check if translation available & use it if possible + const rawTranslation = tm.get("message",msg.messageId.value) + const translatedMessage = (rawTranslation) ? tm.insertTranslationParams(rawTranslation,msg.translationParams) : msg.message + + if (msg.type == "error") finalComponents.push(this.verticalFiller+" "+ansis.bold.red(`${t.error} ${translatedMessage}`)) + else if (msg.type == "warning") finalComponents.push(this.verticalFiller+" "+ansis.bold.yellow(`${t.warning} ${translatedMessage}`)) + else if (msg.type == "info") finalComponents.push(this.verticalFiller+" "+ansis.bold.blue(`${t.info} ${translatedMessage}`)) + + const pathSplitter = msg.path ? ":" : "" + finalComponents.push(this.verticalFiller+" "+ansis.bold((t.dataPath)+this.descriptionSeparator)+ansis.cyan(`${ansis.magenta(msg.filepath+pathSplitter)} ${msg.path}`)) + if (msg.locationDocs) finalComponents.push(this.verticalFiller+" "+ansis.bold(t.dataDocs+this.descriptionSeparator)+ansis.italic.gray(msg.locationDocs)) + if (msg.messageDocs) finalComponents.push(this.verticalFiller+" "+ansis.bold(t.dataMessage+this.descriptionSeparator)+ansis.italic.gray(msg.messageDocs)) + if (index != data.messages.length-1) finalComponents.push(this.verticalFiller) + }) + } + + //bottom + finalComponents.push(this.verticalFiller) + finalBottom.forEach((text) => { + if (text.length < 1) return + finalComponents.push(this.verticalFiller+" "+text) + }) + + //footer + finalComponents.push(this.#getHorizontalDivider(borderLength+4)) + if (!this.disableFooter){ + finalFooter.forEach((text) => { + if (text.length < 1) return + finalComponents.push(this.#createBlockFromText(text,borderLength)) + }) + finalComponents.push(this.#getHorizontalDivider(borderLength+4)) + } + + //return all components + return finalComponents + } + /**Get the length of the longest string in the array. */ + #getLongestLength(text:string[]): number { + let finalLength = 0 + text.forEach((t) => { + const l = ansis.strip(t).length + if (l > finalLength) finalLength = l + }) + + return finalLength + } + /**Get a horizontal divider used between different parts of the config checker result. */ + #getHorizontalDivider(width:number): string { + if (width > 2) width = width-2 + else return this.verticalFiller+this.verticalFiller + let divider = this.verticalFiller + this.horizontalFiller.repeat(width) + this.verticalFiller + return divider + } + /**Create a block of text with a vertical divider on the left & right side. */ + #createBlockFromText(text:string,width:number): string { + if (width < 3) return this.verticalFiller+this.verticalFiller + let newWidth = width-ansis.strip(text).length+1 + let final = this.verticalFiller+" "+text+" ".repeat(newWidth)+this.verticalFiller + return final + } +} + +/**## ODCheckerTranslationRegisterOtherIds_Default `type` + * This type is an array of ids available in the `ODCheckerTranslationRegister_Default` class. + * It's used to generate typescript declarations for this class. + */ +export type ODCheckerTranslationRegisterOtherIds_Default = ( + "openticket:header-openticket"| + "openticket:header-configchecker"| + "openticket:header-description"| + "openticket:type-error"| + "openticket:type-warning"| + "openticket:type-info"| + "openticket:data-path"| + "openticket:data-docs"| + "openticket:data-message"| + "openticket:compact-information"| + "openticket:footer-error"| + "openticket:footer-warning"| + "openticket:footer-support" +) + +/**## ODCheckerTranslationRegisterMessageIds_Default `type` + * This type is an array of ids available in the `ODCheckerTranslationRegister_Default` class. + * It's used to generate typescript declarations for this class. + */ +export type ODCheckerTranslationRegisterMessageIds_Default = ( + "openticket:invalid-type"| + "openticket:property-missing"| + "openticket:property-optional"| + "openticket:object-disabled"| + "openticket:null-invalid"| + "openticket:switch-invalid-type"| + "openticket:object-switch-invalid-type"| + + "openticket:string-too-short"| + "openticket:string-too-long"| + "openticket:string-length-invalid"| + "openticket:string-starts-with"| + "openticket:string-ends-with"| + "openticket:string-contains"| + "openticket:string-choices"| + "openticket:string-regex"| + + "openticket:number-too-short"| + "openticket:number-too-long"| + "openticket:number-length-invalid"| + "openticket:number-too-small"| + "openticket:number-too-large"| + "openticket:number-not-equal"| + "openticket:number-step"| + "openticket:number-step-offset"| + "openticket:number-starts-with"| + "openticket:number-ends-with"| + "openticket:number-contains"| + "openticket:number-choices"| + "openticket:number-float"| + "openticket:number-negative"| + "openticket:number-positive"| + "openticket:number-zero"| + + "openticket:boolean-true"| + "openticket:boolean-false"| + + "openticket:array-empty-disabled"| + "openticket:array-empty-required"| + "openticket:array-too-short"| + "openticket:array-too-long"| + "openticket:array-length-invalid"| + "openticket:array-invalid-types"| + "openticket:array-double"| + + "openticket:discord-invalid-id"| + "openticket:discord-invalid-id-options"| + "openticket:discord-invalid-token"| + "openticket:color-invalid"| + "openticket:emoji-too-short"| + "openticket:emoji-too-long"| + "openticket:emoji-custom"| + "openticket:emoji-invalid"| + "openticket:url-invalid"| + "openticket:url-invalid-http"| + "openticket:url-invalid-protocol"| + "openticket:url-invalid-hostname"| + "openticket:url-invalid-extension"| + "openticket:url-invalid-path"| + "openticket:id-not-unique"| + "openticket:id-non-existent"| + + "openticket:invalid-language"| + "openticket:invalid-button"| + "openticket:unused-option"| + "openticket:unused-question"| + "openticket:dropdown-option" +) + +/**## ODCheckerTranslationRegister_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODCheckerTranslationRegister class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.checkers.translation`! + */ +export class ODCheckerTranslationRegister_Default extends ODCheckerTranslationRegister { + get(type:"other", id:ODCheckerTranslationRegisterOtherIds_Default): string + get(type:"message", id:ODCheckerTranslationRegisterMessageIds_Default): string + get(type:"message"|"other", id:string): string|null + + get(type:"message"|"other", id:string): string|null { + return super.get(type,id) + } + + set(type:"other", id:ODCheckerTranslationRegisterOtherIds_Default, translation:string): boolean + set(type:"message", id:ODCheckerTranslationRegisterMessageIds_Default, translation:string): boolean + set(type:"message"|"other", id:string, translation:string): boolean + + set(type:"message"|"other", id:string, translation:string): boolean { + return super.set(type,id,translation) + } + + delete(type:"other", id:ODCheckerTranslationRegisterOtherIds_Default): boolean + delete(type:"message", id:ODCheckerTranslationRegisterMessageIds_Default): boolean + delete(type:"message"|"other", id:string): boolean + + delete(type:"message"|"other", id:string): boolean { + return super.delete(type,id) + } + + quickTranslate(manager:ODLanguageManager_Default, translationId:string, type:"other"|"message", id:ODCheckerTranslationRegisterOtherIds_Default|ODCheckerTranslationRegisterMessageIds_Default) + quickTranslate(manager:ODLanguageManager_Default, translationId:string, type:"other"|"message", id:string) + + quickTranslate(manager:ODLanguageManager_Default, translationId:string, type:"other"|"message", id:ODCheckerTranslationRegisterOtherIds_Default|ODCheckerTranslationRegisterMessageIds_Default|string){ + super.quickTranslate(manager,translationId,type,id) + } +} + +/**## ODCheckerFunctionManagerIds_Default `type` + * This type is an array of ids available in the `ODCheckerFunctionManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODCheckerFunctionManagerIds_Default { + "openticket:unused-options":ODCheckerFunction, + "openticket:dropdown-options":ODCheckerFunction +} + +/**## ODCheckerFunctionManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODCheckerFunctionManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.checkers.functions`! + */ +export class ODCheckerFunctionManager_Default extends ODCheckerFunctionManager { + get(id:CheckerFunctionId): ODCheckerManagerIds_Default[CheckerFunctionId] + get(id:ODValidId): ODCheckerFunction|null + + get(id:ODValidId): ODCheckerFunction|null { + return super.get(id) + } + + remove(id:CheckerFunctionId): ODCheckerManagerIds_Default[CheckerFunctionId] + remove(id:ODValidId): ODCheckerFunction|null + + remove(id:ODValidId): ODCheckerFunction|null { + return super.remove(id) + } + + exists(id:keyof ODCheckerFunctionManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/client.ts b/src/core/api/defaults/client.ts new file mode 100644 index 0000000..d03d26f --- /dev/null +++ b/src/core/api/defaults/client.ts @@ -0,0 +1,147 @@ +/////////////////////////////////////// +//DEFAULT CLIENT MODULE +/////////////////////////////////////// +import { ODValidId } from "../modules/base" +import { ODClientManager, ODSlashCommand, ODTextCommand, ODSlashCommandManager, ODTextCommandManager, ODSlashCommandInteractionCallback, ODTextCommandInteractionCallback } from "../modules/client" + +/**## ODClientManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODClientManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.client`! + */ +export class ODClientManager_Default extends ODClientManager { + declare slashCommands: ODSlashCommandManager_Default + declare textCommands: ODTextCommandManager_Default +} + +/**## ODSlashCommandManagerIds_Default `type` + * This type is an array of ids available in the `ODSlashCommandManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODSlashCommandManagerIds_Default { + "openticket:help":ODSlashCommand, + "openticket:panel":ODSlashCommand, + "openticket:ticket":ODSlashCommand, + "openticket:close":ODSlashCommand, + "openticket:delete":ODSlashCommand, + "openticket:reopen":ODSlashCommand, + "openticket:claim":ODSlashCommand, + "openticket:unclaim":ODSlashCommand, + "openticket:pin":ODSlashCommand, + "openticket:unpin":ODSlashCommand, + "openticket:move":ODSlashCommand, + "openticket:rename":ODSlashCommand, + "openticket:add":ODSlashCommand, + "openticket:remove":ODSlashCommand, + "openticket:blacklist":ODSlashCommand, + "openticket:stats":ODSlashCommand, + "openticket:clear":ODSlashCommand, + "openticket:autoclose":ODSlashCommand, + "openticket:autodelete":ODSlashCommand, +} + +/**## ODSlashCommandManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODSlashCommandManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.client.slashCommands`! + */ +export class ODSlashCommandManager_Default extends ODSlashCommandManager { + get(id:SlashCommandId): ODSlashCommandManagerIds_Default[SlashCommandId] + get(id:ODValidId): ODSlashCommand|null + + get(id:ODValidId): ODSlashCommand|null { + return super.get(id) + } + + remove(id:SlashCommandId): ODSlashCommandManagerIds_Default[SlashCommandId] + remove(id:ODValidId): ODSlashCommand|null + + remove(id:ODValidId): ODSlashCommand|null { + return super.remove(id) + } + + exists(id:keyof ODSlashCommandManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + onInteraction(commandName:keyof ODSlashCommandManagerIds_Default, callback:ODSlashCommandInteractionCallback): void + onInteraction(commandName:string|RegExp, callback:ODSlashCommandInteractionCallback): void + + onInteraction(commandName:string|RegExp, callback:ODSlashCommandInteractionCallback): void { + return super.onInteraction(commandName,callback) + } +} + +/**## ODTextCommandManagerIds_Default `type` + * This type is an array of ids available in the `ODTextCommandManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODTextCommandManagerIds_Default { + "openticket:help":ODTextCommand, + "openticket:panel":ODTextCommand, + "openticket:close":ODTextCommand, + "openticket:delete":ODTextCommand, + "openticket:reopen":ODTextCommand, + "openticket:claim":ODTextCommand, + "openticket:unclaim":ODTextCommand, + "openticket:pin":ODTextCommand, + "openticket:unpin":ODTextCommand, + "openticket:move":ODTextCommand, + "openticket:rename":ODTextCommand, + "openticket:add":ODTextCommand, + "openticket:remove":ODTextCommand, + "openticket:blacklist-view":ODTextCommand, + "openticket:blacklist-add":ODTextCommand, + "openticket:blacklist-remove":ODTextCommand, + "openticket:blacklist-get":ODTextCommand, + "openticket:stats-global":ODTextCommand, + "openticket:stats-reset":ODTextCommand, + "openticket:stats-ticket":ODTextCommand, + "openticket:stats-user":ODTextCommand, + "openticket:clear":ODTextCommand, + "openticket:autoclose-disable":ODTextCommand, + "openticket:autoclose-enable":ODTextCommand, + "openticket:autodelete-disable":ODTextCommand, + "openticket:autodelete-enable":ODTextCommand +} + +/**## ODTextCommandManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODTextCommandManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.client.textCommands`! + */ +export class ODTextCommandManager_Default extends ODTextCommandManager { + get(id:TextCommandId): ODTextCommandManagerIds_Default[TextCommandId] + get(id:ODValidId): ODTextCommand|null + + get(id:ODValidId): ODTextCommand|null { + return super.get(id) + } + + remove(id:TextCommandId): ODTextCommandManagerIds_Default[TextCommandId] + remove(id:ODValidId): ODTextCommand|null + + remove(id:ODValidId): ODTextCommand|null { + return super.remove(id) + } + + exists(id:keyof ODTextCommandManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + onInteraction(prefix:string, id:keyof ODTextCommandManagerIds_Default, callback:ODTextCommandInteractionCallback): void + onInteraction(commandPrefix:string, commandName:string|RegExp, callback:ODTextCommandInteractionCallback): void + + onInteraction(commandPrefix:string, commandName:string|RegExp, callback:ODTextCommandInteractionCallback): void { + return super.onInteraction(commandPrefix,commandName,callback) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/code.ts b/src/core/api/defaults/code.ts new file mode 100644 index 0000000..4275298 --- /dev/null +++ b/src/core/api/defaults/code.ts @@ -0,0 +1,56 @@ +/////////////////////////////////////// +//DEFAULT CODE MODULE +/////////////////////////////////////// +import { ODValidId } from "../modules/base" +import { ODCode, ODCodeManager } from "../modules/code" + +/**## ODCodeManagerIds_Default `type` + * This type is an array of ids available in the `ODCodeManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODCodeManagerIds_Default { + "openticket:command-error-handling":ODCode, + "openticket:start-listening-interactions":ODCode, + "openticket:panel-database-cleaner":ODCode, + "openticket:suffix-database-cleaner":ODCode, + "openticket:option-database-cleaner":ODCode, + "openticket:user-database-cleaner":ODCode, + "openticket:ticket-database-cleaner":ODCode, + "openticket:panel-auto-update":ODCode, + "openticket:ticket-saver":ODCode, + "openticket:blacklist-saver":ODCode, + "openticket:auto-role-on-join":ODCode, + "openticket:autoclose-timeout":ODCode, + "openticket:autoclose-leave":ODCode, + "openticket:autodelete-timeout":ODCode, + "openticket:autodelete-leave":ODCode +} + +/**## ODCodeManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODCodeManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.code`! + */ +export class ODCodeManager_Default extends ODCodeManager { + get(id:CodeId): ODCodeManagerIds_Default[CodeId] + get(id:ODValidId): ODCode|null + + get(id:ODValidId): ODCode|null { + return super.get(id) + } + + remove(id:CodeId): ODCodeManagerIds_Default[CodeId] + remove(id:ODValidId): ODCode|null + + remove(id:ODValidId): ODCode|null { + return super.remove(id) + } + + exists(id:keyof ODCodeManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/config.ts b/src/core/api/defaults/config.ts new file mode 100644 index 0000000..e4db760 --- /dev/null +++ b/src/core/api/defaults/config.ts @@ -0,0 +1,470 @@ +/////////////////////////////////////// +//DEFAULT CONFIG MODULE +/////////////////////////////////////// +import { ODValidButtonColor, ODValidId } from "../modules/base" +import * as discord from "discord.js" +import { ODConfigManager, ODConfig, ODJsonConfig } from "../modules/config" +import { ODClientActivityStatus, ODClientActivityType } from "../modules/client" +import { OTRoleUpdateMode } from "../openticket/role" + +/**## ODConfigManagerIds_Default `type` + * This type is an array of ids available in the `ODConfigManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODConfigManagerIds_Default { + "openticket:general":ODJsonConfig_DefaultGeneral, + "openticket:options":ODJsonConfig_DefaultOptions, + "openticket:panels":ODJsonConfig_DefaultPanels, + "openticket:questions":ODJsonConfig_DefaultQuestions, + "openticket:transcripts":ODJsonConfig_DefaultTranscripts +} + +/**## ODConfigManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODConfigManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.configs`! + */ +export class ODConfigManager_Default extends ODConfigManager { + get(id:ConfigId): ODConfigManagerIds_Default[ConfigId] + get(id:ODValidId): ODConfig|null + + get(id:ODValidId): ODConfig|null { + return super.get(id) + } + + remove(id:ConfigId): ODConfigManagerIds_Default[ConfigId] + remove(id:ODValidId): ODConfig|null + + remove(id:ODValidId): ODConfig|null { + return super.remove(id) + } + + exists(id:keyof ODConfigManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODJsonConfig_DefaultStatusType `interface` + * This interface is an object which has all properties for the status object in the `general.json` config! + */ +export interface ODJsonConfig_DefaultStatusType { + enabled:boolean, + type:Exclude, + text:string, + status:ODClientActivityStatus +} + +/**## ODJsonConfig_DefaultMessageSettingsType `interface` + * This interface is an object which has all properties for the system/messages/... object in the `general.json` config! + */ +export interface ODJsonConfig_DefaultMessageSettingsType { + dm:boolean, + logs:boolean +} + +/**## ODJsonConfig_DefaultCmdPermissionSettingsType `type` + * This type is a collection of command permission settings for the system/permissions/... object in the `general.json` config! + */ +export type ODJsonConfig_DefaultCmdPermissionSettingsType = "admin"|"everyone"|"none"|string + +/**## ODJsonConfig_DefaultGeneral `default_class` + * This is a special class that adds type definitions & typescript to the ODJsonConfig class. + * It doesn't add any extra features! + * + * This default class is made for the `general.json` config! + */ +export class ODJsonConfig_DefaultGeneral extends ODJsonConfig { + declare data: { + _INFO:{ + support:string, + discord:string, + version:string + }, + + token:string, + tokenFromENV:boolean, + + mainColor:discord.ColorResolvable, + language:string, + prefix:string, + serverId:string, + globalAdmins:string[], + + slashCommands:boolean, + textCommands:boolean, + + status:ODJsonConfig_DefaultStatusType, + + system:{ + removeParticipantsOnClose:boolean, + replyOnTicketCreation:boolean, + replyOnReactionRole:boolean, + useTranslatedConfigChecker:boolean, + preferSlashOverText:boolean, + sendErrorOnUnknownCommand:boolean, + questionFieldsInCodeBlock:boolean, + disableVerifyBars:boolean, + useRedErrorEmbeds:boolean, + emojiStyle:"before"|"after"|"double"|"disabled", + + enableTicketClaimButtons:boolean, + enableTicketCloseButtons:boolean, + enableTicketPinButtons:boolean, + enableTicketDeleteButtons:boolean, + enableTicketActionWithReason:boolean, + enableDeleteWithoutTranscript:boolean, + + logs:{ + enabled:boolean, + channel:string + }, + + limits:{ + enabled:boolean, + globalMaximum:number, + userMaximum:number + }, + + permissions:{ + help:ODJsonConfig_DefaultCmdPermissionSettingsType, + panel:ODJsonConfig_DefaultCmdPermissionSettingsType, + ticket:ODJsonConfig_DefaultCmdPermissionSettingsType, + close:ODJsonConfig_DefaultCmdPermissionSettingsType, + delete:ODJsonConfig_DefaultCmdPermissionSettingsType, + reopen:ODJsonConfig_DefaultCmdPermissionSettingsType, + claim:ODJsonConfig_DefaultCmdPermissionSettingsType, + unclaim:ODJsonConfig_DefaultCmdPermissionSettingsType, + pin:ODJsonConfig_DefaultCmdPermissionSettingsType, + unpin:ODJsonConfig_DefaultCmdPermissionSettingsType, + move:ODJsonConfig_DefaultCmdPermissionSettingsType, + rename:ODJsonConfig_DefaultCmdPermissionSettingsType, + add:ODJsonConfig_DefaultCmdPermissionSettingsType, + remove:ODJsonConfig_DefaultCmdPermissionSettingsType, + blacklist:ODJsonConfig_DefaultCmdPermissionSettingsType, + stats:ODJsonConfig_DefaultCmdPermissionSettingsType, + clear:ODJsonConfig_DefaultCmdPermissionSettingsType, + autoclose:ODJsonConfig_DefaultCmdPermissionSettingsType, + autodelete:ODJsonConfig_DefaultCmdPermissionSettingsType + }, + + messages:{ + creation:ODJsonConfig_DefaultMessageSettingsType, + closing:ODJsonConfig_DefaultMessageSettingsType, + deleting:ODJsonConfig_DefaultMessageSettingsType, + reopening:ODJsonConfig_DefaultMessageSettingsType, + claiming:ODJsonConfig_DefaultMessageSettingsType, + pinning:ODJsonConfig_DefaultMessageSettingsType, + adding:ODJsonConfig_DefaultMessageSettingsType, + removing:ODJsonConfig_DefaultMessageSettingsType, + renaming:ODJsonConfig_DefaultMessageSettingsType, + moving:ODJsonConfig_DefaultMessageSettingsType, + blacklisting:ODJsonConfig_DefaultMessageSettingsType, + roleAdding:ODJsonConfig_DefaultMessageSettingsType, + roleRemoving:ODJsonConfig_DefaultMessageSettingsType + } + } + } +} + +/**## ODJsonConfig_DefaultOptionType `interface` + * This interface is an object which has all basic properties for options in the `options.json` config! + */ +export interface ODJsonConfig_DefaultOptionType { + id:string, + name:string, + description:string, + type:"ticket"|"website"|"role", + button:{ + emoji:string, + label:string + } +} + +/**## ODJsonConfig_DefaultOptionButtonSettingsType `interface` + * This interface is an object which has all button settings for ticket options in the `options.json` config! + */ +export interface ODJsonConfig_DefaultOptionButtonSettingsType { + emoji:string, + label:string, + color:ODValidButtonColor +} + +/**## ODJsonConfig_DefaultOptionEmbedSettingsType `interface` + * This interface is an object which has all message embed settings for ticket options in the `options.json` config! + */ +export interface ODJsonConfig_DefaultOptionEmbedSettingsType { + enabled:boolean, + title:string, + description:string, + customColor:string, + + image:string, + thumbnail:string, + fields:{name:string, value:string, inline:boolean}[], + timestamp:boolean +} + +/**## ODJsonConfig_DefaultOptionPingSettingsType `interface` + * This interface is an object which has all message ping settings for ticket options in the `options.json` config! + */ +export interface ODJsonConfig_DefaultOptionPingSettingsType { + "@here":boolean, + "@everyone":boolean, + custom:string[] +} + +/**## ODJsonConfig_DefaultOptionTicketType `interface` + * This interface is an object which has all ticket properties for options in the `options.json` config! + */ +export interface ODJsonConfig_DefaultOptionTicketType extends ODJsonConfig_DefaultOptionType { + type:"ticket", + button:ODJsonConfig_DefaultOptionButtonSettingsType, + ticketAdmins:string[], + readonlyAdmins:string[], + allowCreationByBlacklistedUsers:boolean, + questions:string[], + channel:{ + prefix:string, + suffix:"user-name"|"user-id"|"random-number"|"random-hex"|"counter-dynamic"|"counter-fixed", + category:string, + closedCategory:string, + backupCategory:string, + claimedCategory:{user:string, category:string}[], + description:string + }, + dmMessage:{ + enabled:boolean, + text:string, + embed:ODJsonConfig_DefaultOptionEmbedSettingsType + }, + ticketMessage:{ + enabled:boolean, + text:string, + embed:ODJsonConfig_DefaultOptionEmbedSettingsType, + ping:ODJsonConfig_DefaultOptionPingSettingsType + }, + autoclose:{ + enableInactiveHours:boolean, + inactiveHours:number, + enableUserLeave:boolean, + disableOnClaim:boolean + }, + autodelete:{ + enableInactiveDays:boolean, + inactiveDays:number, + enableUserLeave:boolean, + disableOnClaim:boolean + }, + cooldown:{ + enabled:boolean, + cooldownMinutes:number + }, + limits:{ + enabled:boolean, + globalMaximum:number, + userMaximum:number + } +} + +/**## ODJsonConfig_DefaultOptionWebsiteType `interface` + * This interface is an object which has all website properties for options in the `options.json` config! + */ +export interface ODJsonConfig_DefaultOptionWebsiteType extends ODJsonConfig_DefaultOptionType { + type:"website", + url:string +} + +/**## ODJsonConfig_DefaultOptionRoleType `interface` + * This interface is an object which has all reaction role properties for options in the `options.json` config! + */ +export interface ODJsonConfig_DefaultOptionRoleType extends ODJsonConfig_DefaultOptionType { + type:"role", + button:ODJsonConfig_DefaultOptionButtonSettingsType, + roles:string[], + mode:OTRoleUpdateMode, + removeRolesOnAdd:string[], + addOnMemberJoin:boolean +} + +/**## ODJsonConfig_DefaultOptions `default_class` + * This is a special class that adds type definitions & typescript to the ODJsonConfig class. + * It doesn't add any extra features! + * + * This default class is made for the `options.json` config! + */ +export class ODJsonConfig_DefaultOptions extends ODJsonConfig { + declare data: ( + ODJsonConfig_DefaultOptionTicketType| + ODJsonConfig_DefaultOptionWebsiteType| + ODJsonConfig_DefaultOptionRoleType + )[] +} + +/**## ODJsonConfig_DefaultPanelEmbedSettingsType `interface` + * This interface is an object which has all message embed settings for panels in the `panels.json` config! + */ +export interface ODJsonConfig_DefaultPanelEmbedSettingsType { + enabled:boolean, + title:string, + description:string, + + customColor:string, + url:string, + + image:string, + thumbnail:string, + + footer:string, + fields:{name:string,value:string,inline:boolean}[], + timestamp:boolean +} + +/**## ODJsonConfig_DefaultPanelType `interface` + * This interface is an object which has all properties for panels in the `panels.json` config! + */ +export interface ODJsonConfig_DefaultPanelType { + id:string, + name:string, + dropdown:boolean, + options:string[], + + text:string, + embed:ODJsonConfig_DefaultPanelEmbedSettingsType, + settings:{ + dropdownPlaceholder:string, + + enableMaxTicketsWarningInText:boolean, + enableMaxTicketsWarningInEmbed:boolean, + + describeOptionsLayout:"simple"|"normal"|"detailed", + describeOptionsCustomTitle:string, + describeOptionsInText:boolean, + describeOptionsInEmbedFields:boolean, + describeOptionsInEmbedDescription:boolean + } +} + +/**## ODJsonConfig_DefaultPanels `default_class` + * This is a special class that adds type definitions & typescript to the ODJsonConfig class. + * It doesn't add any extra features! + * + * This default class is made for the `panels.json` config! + */ +export class ODJsonConfig_DefaultPanels extends ODJsonConfig { + declare data: ODJsonConfig_DefaultPanelType[] +} + +/**## ODJsonConfig_DefaultShortQuestionType `interface` + * This interface is an object which has all properties for short questions in the `questions.json` config! + */ +export interface ODJsonConfig_DefaultShortQuestionType { + id:string, + name:string, + type:"short", + + required:boolean, + placeholder:string, + length:{ + enabled:boolean, + min:number, + max:number + } +} + +/**## ODJsonConfig_DefaultParagraphQuestionType `interface` + * This interface is an object which has all properties for paragraph questions in the `questions.json` config! + */ +export interface ODJsonConfig_DefaultParagraphQuestionType { + id:string, + name:string, + type:"paragraph", + + required:boolean, + placeholder:string, + length:{ + enabled:boolean, + min:number, + max:number + } +} + +/**## ODJsonConfig_DefaultQuestions `default_class` + * This is a special class that adds type definitions & typescript to the ODJsonConfig class. + * It doesn't add any extra features! + * + * This default class is made for the `questions.json` config! + */ +export class ODJsonConfig_DefaultQuestions extends ODJsonConfig { + declare data: ( + ODJsonConfig_DefaultShortQuestionType| + ODJsonConfig_DefaultParagraphQuestionType + )[] +} + +/**## ODJsonConfig_DefaultTranscripts `default_class` + * This is a special class that adds type definitions & typescript to the ODJsonConfig class. + * It doesn't add any extra features! + * + * This default class is made for the `transcripts.json` config! + */ +export class ODJsonConfig_DefaultTranscripts extends ODJsonConfig { + declare data: { + general:{ + enabled:boolean, + + enableChannel:boolean, + enableCreatorDM:boolean, + enableParticipantDM:boolean, + enableActiveAdminDM:boolean, + enableEveryAdminDM:boolean, + + channel:string, + mode:"html"|"text" + }, + embedSettings:{ + customColor:string, + listAllParticipants:boolean, + includeTicketStats:boolean + }, + textTranscriptStyle:{ + layout:"simple"|"normal"|"detailed", + includeStats:boolean, + includeIds:boolean, + includeEmbeds:boolean, + includeFiles:boolean, + includeBotMessages:boolean, + + fileMode:"custom"|"channel-name"|"channel-id"|"user-name"|"user-id", + customFileName:string + }, + htmlTranscriptStyle:{ + background:{ + enableCustomBackground:boolean, + backgroundColor:string, + backgroundImage:string + }, + header:{ + enableCustomHeader:boolean, + backgroundColor:string, + decoColor:string, + textColor:string + }, + stats:{ + enableCustomStats:false, + backgroundColor:string, + keyTextColor:string, + valueTextColor:string, + hideBackgroundColor:string, + hideTextColor:string + }, + favicon:{ + enableCustomFavicon:boolean, + imageUrl:string + } + } + } +} \ No newline at end of file diff --git a/src/core/api/defaults/console.ts b/src/core/api/defaults/console.ts new file mode 100644 index 0000000..58f6feb --- /dev/null +++ b/src/core/api/defaults/console.ts @@ -0,0 +1,42 @@ +/////////////////////////////////////// +//DEFAULT CONSOLE MODULE +/////////////////////////////////////// +import { ODValidId } from "../modules/base" +import { ODLiveStatusUrlSource, ODLiveStatusManager, ODLiveStatusSource } from "../modules/console" + +/**## ODLiveStatusManagerIds_Default `type` + * This type is an array of ids available in the `ODLiveStatusManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODLiveStatusManagerIds_Default { + "openticket:default-djdj-dev":ODLiveStatusUrlSource +} + +/**## ODLiveStatusManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODLiveStatusManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.livestatus`! + */ +export class ODLiveStatusManager_Default extends ODLiveStatusManager { + get(id:LiveStatusId): ODLiveStatusManagerIds_Default[LiveStatusId] + get(id:ODValidId): ODLiveStatusSource|null + + get(id:ODValidId): ODLiveStatusSource|null { + return super.get(id) + } + + remove(id:LiveStatusId): ODLiveStatusManagerIds_Default[LiveStatusId] + remove(id:ODValidId): ODLiveStatusSource|null + + remove(id:ODValidId): ODLiveStatusSource|null { + return super.remove(id) + } + + exists(id:keyof ODLiveStatusManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/cooldown.ts b/src/core/api/defaults/cooldown.ts new file mode 100644 index 0000000..0a0c1a2 --- /dev/null +++ b/src/core/api/defaults/cooldown.ts @@ -0,0 +1,42 @@ +/////////////////////////////////////// +//DEFAULT COOLDOWN MODULE +/////////////////////////////////////// +import { ODValidId } from "../modules/base" +import { ODCooldown, ODCooldownManager } from "../modules/cooldown" + +/**## ODCooldownManagerIds_Default `type` + * This type is an array of ids available in the `ODCooldownManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODCooldownManagerIds_Default { + +} + +/**## ODCooldownManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODCooldownManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.cooldowns`! + */ +export class ODCooldownManager_Default extends ODCooldownManager { + get(id:CooldownId): ODCooldownManagerIds_Default[CooldownId] + get(id:ODValidId): ODCooldown|null + + get(id:ODValidId): ODCooldown|null { + return super.get(id) + } + + remove(id:CooldownId): ODCooldownManagerIds_Default[CooldownId] + remove(id:ODValidId): ODCooldown|null + + remove(id:ODValidId): ODCooldown|null { + return super.remove(id) + } + + exists(id:keyof ODCooldownManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/database.ts b/src/core/api/defaults/database.ts new file mode 100644 index 0000000..1805d90 --- /dev/null +++ b/src/core/api/defaults/database.ts @@ -0,0 +1,256 @@ +/////////////////////////////////////// +//DEFAULT DATABASE MODULE +/////////////////////////////////////// +import { ODValidId, ODValidJsonType } from "../modules/base" +import { ODDatabaseManager, ODDatabase, ODFormattedJsonDatabase } from "../modules/database" +import { ODTicketJson } from "../openticket/ticket" +import { ODOptionJson } from "../openticket/option" + +/**## ODDatabaseManagerIds_Default `type` + * This type is an array of ids available in the `ODDatabaseManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODDatabaseManagerIds_Default { + "openticket:global":ODFormattedJsonDatabase_DefaultGlobal, + "openticket:stats":ODFormattedJsonDatabase, + "openticket:tickets":ODFormattedJsonDatabase_DefaultTickets, + "openticket:users":ODFormattedJsonDatabase_DefaultUsers, + "openticket:options":ODFormattedJsonDatabase_DefaultOptions, +} + +/**## ODDatabaseManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODDatabaseManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.databases`! + */ +export class ODDatabaseManager_Default extends ODDatabaseManager { + get(id:DatabaseId): ODDatabaseManagerIds_Default[DatabaseId] + get(id:ODValidId): ODDatabase|null + + get(id:ODValidId): ODDatabase|null { + return super.get(id) + } + + remove(id:DatabaseId): ODDatabaseManagerIds_Default[DatabaseId] + remove(id:ODValidId): ODDatabase|null + + remove(id:ODValidId): ODDatabase|null { + return super.remove(id) + } + + exists(id:keyof ODDatabaseManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODFormattedJsonDatabaseIds_DefaultGlobal `type` + * This type is an array of ids available in the `ODFormattedJsonDatabase_DefaultGlobal` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODFormattedJsonDatabaseIds_DefaultGlobal { + "openticket:panel-update":string, + "openticket:option-suffix-counter":number, + "openticket:option-suffix-history":string[], + "openticket:last-version":string +} + +/**## ODFormattedJsonDatabase_DefaultGlobal `default_class` + * This is a special class that adds type definitions & typescript to the ODFormattedJsonDatabase class. + * It doesn't add any extra features! + * + * This default class is made for the `global.json` database! + */ +export class ODFormattedJsonDatabase_DefaultGlobal extends ODFormattedJsonDatabase { + set(category:CategoryId, key:string, value:ODFormattedJsonDatabaseIds_DefaultGlobal[CategoryId]): boolean + set(category:string, key:string, value:ODValidJsonType): boolean + + set(category:string, key:string, value:ODValidJsonType): boolean { + return super.set(category,key,value) + } + + get(category:CategoryId, key:string): ODFormattedJsonDatabaseIds_DefaultGlobal[CategoryId]|undefined + get(category:string, key:string): ODValidJsonType|undefined + + get(category:string, key:string): ODValidJsonType|undefined { + return super.get(category,key) + } + + delete(category:CategoryId, key:string): boolean + delete(category:string, key:string): boolean + + delete(category:string, key:string): boolean { + return super.delete(category,key) + } + + exists(category:keyof ODFormattedJsonDatabaseIds_DefaultGlobal, key:string): boolean + exists(category:string, key:string): boolean + + exists(category:string, key:string): boolean { + return super.exists(category,key) + } + + getCategory(category:CategoryId): {key:string, value:ODFormattedJsonDatabaseIds_DefaultGlobal[CategoryId]}[]|undefined + getCategory(category:string): {key:string, value:ODValidJsonType}[]|undefined + + getCategory(category:string): {key:string, value:ODValidJsonType}[]|undefined { + return super.getCategory(category) + } +} + +/**## ODFormattedJsonDatabaseIds_DefaultTickets `type` + * This type is an array of ids available in the `ODDatabaseManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODFormattedJsonDatabaseIds_DefaultTickets { + "openticket:ticket":ODTicketJson +} + +/**## ODFormattedJsonDatabase_DefaultTickets `default_class` + * This is a special class that adds type definitions & typescript to the ODFormattedJsonDatabase class. + * It doesn't add any extra features! + * + * This default class is made for the `tickets.json` database! + */ +export class ODFormattedJsonDatabase_DefaultTickets extends ODFormattedJsonDatabase { + set(category:CategoryId, key:string, value:ODFormattedJsonDatabaseIds_DefaultTickets[CategoryId]): boolean + set(category:string, key:string, value:ODValidJsonType): boolean + + set(category:string, key:string, value:ODValidJsonType): boolean { + return super.set(category,key,value) + } + + get(category:CategoryId, key:string): ODFormattedJsonDatabaseIds_DefaultTickets[CategoryId]|undefined + get(category:string, key:string): ODValidJsonType|undefined + + get(category:string, key:string): ODValidJsonType|undefined { + return super.get(category,key) + } + + delete(category:CategoryId, key:string): boolean + delete(category:string, key:string): boolean + + delete(category:string, key:string): boolean { + return super.delete(category,key) + } + + exists(category:keyof ODFormattedJsonDatabaseIds_DefaultTickets, key:string): boolean + exists(category:string, key:string): boolean + + exists(category:string, key:string): boolean { + return super.exists(category,key) + } + + getCategory(category:CategoryId): {key:string, value:ODFormattedJsonDatabaseIds_DefaultTickets[CategoryId]}[]|undefined + getCategory(category:string): {key:string, value:ODValidJsonType}[]|undefined + + getCategory(category:string): {key:string, value:ODValidJsonType}[]|undefined { + return super.getCategory(category) + } +} + +/**## ODFormattedJsonDatabaseIds_DefaultUsers `type` + * This type is an array of ids available in the `ODDatabaseManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODFormattedJsonDatabaseIds_DefaultUsers { + "openticket:blacklist":ODTicketJson +} + +/**## ODFormattedJsonDatabase_DefaultUsers `default_class` + * This is a special class that adds type definitions & typescript to the ODFormattedJsonDatabase class. + * It doesn't add any extra features! + * + * This default class is made for the `users.json` database! + */ +export class ODFormattedJsonDatabase_DefaultUsers extends ODFormattedJsonDatabase { + set(category:CategoryId, key:string, value:ODFormattedJsonDatabaseIds_DefaultUsers[CategoryId]): boolean + set(category:string, key:string, value:ODValidJsonType): boolean + + set(category:string, key:string, value:ODValidJsonType): boolean { + return super.set(category,key,value) + } + + get(category:CategoryId, key:string): ODFormattedJsonDatabaseIds_DefaultUsers[CategoryId]|undefined + get(category:string, key:string): ODValidJsonType|undefined + + get(category:string, key:string): ODValidJsonType|undefined { + return super.get(category,key) + } + + delete(category:CategoryId, key:string): boolean + delete(category:string, key:string): boolean + + delete(category:string, key:string): boolean { + return super.delete(category,key) + } + + exists(category:keyof ODFormattedJsonDatabaseIds_DefaultUsers, key:string): boolean + exists(category:string, key:string): boolean + + exists(category:string, key:string): boolean { + return super.exists(category,key) + } + + getCategory(category:CategoryId): {key:string, value:ODFormattedJsonDatabaseIds_DefaultUsers[CategoryId]}[]|undefined + getCategory(category:string): {key:string, value:ODValidJsonType}[]|undefined + + getCategory(category:string): {key:string, value:ODValidJsonType}[]|undefined { + return super.getCategory(category) + } +} + + +/**## ODFormattedJsonDatabaseIds_DefaultOptions `type` + * This type is an array of ids available in the `ODDatabaseManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODFormattedJsonDatabaseIds_DefaultOptions { + "openticket:used-option":ODOptionJson +} + +/**## ODFormattedJsonDatabase_DefaultOptions `default_class` + * This is a special class that adds type definitions & typescript to the ODFormattedJsonDatabase class. + * It doesn't add any extra features! + * + * This default class is made for the `options.json` database! + */ +export class ODFormattedJsonDatabase_DefaultOptions extends ODFormattedJsonDatabase { + set(category:CategoryId, key:string, value:ODFormattedJsonDatabaseIds_DefaultOptions[CategoryId]): boolean + set(category:string, key:string, value:ODValidJsonType): boolean + + set(category:string, key:string, value:ODValidJsonType): boolean { + return super.set(category,key,value) + } + + get(category:CategoryId, key:string): ODFormattedJsonDatabaseIds_DefaultOptions[CategoryId]|undefined + get(category:string, key:string): ODValidJsonType|undefined + + get(category:string, key:string): ODValidJsonType|undefined { + return super.get(category,key) + } + + delete(category:CategoryId, key:string): boolean + delete(category:string, key:string): boolean + + delete(category:string, key:string): boolean { + return super.delete(category,key) + } + + exists(category:keyof ODFormattedJsonDatabaseIds_DefaultOptions, key:string): boolean + exists(category:string, key:string): boolean + + exists(category:string, key:string): boolean { + return super.exists(category,key) + } + + getCategory(category:CategoryId): {key:string, value:ODFormattedJsonDatabaseIds_DefaultOptions[CategoryId]}[]|undefined + getCategory(category:string): {key:string, value:ODValidJsonType}[]|undefined + + getCategory(category:string): {key:string, value:ODValidJsonType}[]|undefined { + return super.getCategory(category) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/event.ts b/src/core/api/defaults/event.ts new file mode 100644 index 0000000..c9b02b1 --- /dev/null +++ b/src/core/api/defaults/event.ts @@ -0,0 +1,331 @@ +/////////////////////////////////////// +//DEFAULT EVENT MODULE +/////////////////////////////////////// +//BASE MODULES +import { ODPromiseVoid, ODValidId } from "../modules/base" +import { ODPluginClassManager, ODPluginEventManager, ODPluginManager } from "../modules/plugin" +import { ODConsoleManager, ODError } from "../modules/console" +import { ODCheckerResult, ODCheckerStorage } from "../modules/checker" +import { ODDefaultsManager } from "../modules/defaults" +import { ODLanguage } from "../modules/language" +import { ODClientActivityManager } from "../modules/client" +import { ODEvent, ODEventManager } from "../modules/event" +import * as discord from "discord.js" + +//DEFAULT MODULES +import { ODVersionManager_Default } from "./base" +import { ODConfigManager_Default} from "./config" +import { ODDatabaseManager_Default } from "./database" +import { ODFlagManager_Default } from "./flag" +import { ODSessionManager_Default } from "./session" +import { ODLanguageManager_Default } from "./language" +import { ODCheckerFunctionManager_Default, ODCheckerManager_Default, ODCheckerRenderer_Default, ODCheckerTranslationRegister_Default } from "./checker" +import { ODClientManager_Default, ODSlashCommandManager_Default, ODTextCommandManager_Default } from "./client" +import { ODBuilderManager_Default, ODButtonManager_Default, ODDropdownManager_Default, ODEmbedManager_Default, ODFileManager_Default, ODMessageManager_Default, ODModalManager_Default } from "./builder" +import { ODButtonResponderManager_Default, ODCommandResponderManager_Default, ODDropdownResponderManager_Default, ODModalResponderManager_Default, ODResponderManager_Default } from "./responder" +import { ODActionManager_Default } from "./action" +import { ODPermissionManager_Default } from "./permission" +import { ODHelpMenuManager_Default } from "./helpmenu" +import { ODStatsManager_Default } from "./stat" +import { ODCodeManager_Default } from "./code" +import { ODCooldownManager_Default } from "./cooldown" +import { ODPostManager_Default } from "./post" +import { ODVerifyBarManager_Default } from "./verifybar" +import { ODStartScreenManager_Default } from "./startscreen" +import { ODLiveStatusManager_Default } from "./console" + +//OPEN TICKET MODULES +import { ODOptionManager, ODTicketOption } from "../openticket/option" +import { ODPanel, ODPanelManager } from "../openticket/panel" +import { ODTicket, ODTicketClearFilter, ODTicketManager } from "../openticket/ticket" +import { ODQuestionManager } from "../openticket/question" +import { ODBlacklistManager } from "../openticket/blacklist" +import { ODTranscriptManager_Default } from "../openticket/transcript" +import { ODRole, ODRoleManager } from "../openticket/role" + +/**## ODEventIds_Default `type` + * This type is an array of ids available in the `ODEvent_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODEventIds_Default { + //error handling + "onErrorHandling": ODEvent_Default<(error:Error, origin:NodeJS.UncaughtExceptionOrigin) => ODPromiseVoid> + "afterErrorHandling": ODEvent_Default<(error:Error, origin:NodeJS.UncaughtExceptionOrigin, message:ODError) => ODPromiseVoid> + + //plugins + "afterPluginsLoaded": ODEvent_Default<(plugins:ODPluginManager) => ODPromiseVoid> + "onPluginClassLoad": ODEvent_Default<(classes:ODPluginClassManager, plugins:ODPluginManager) => ODPromiseVoid> + "afterPluginClassesLoaded": ODEvent_Default<(classes:ODPluginClassManager, plugins:ODPluginManager) => ODPromiseVoid> + "onPluginEventLoad": ODEvent_Default<(classes:ODPluginEventManager, plugins:ODPluginManager) => ODPromiseVoid> + "afterPluginEventsLoaded": ODEvent_Default<(classes:ODPluginEventManager, plugins:ODPluginManager) => ODPromiseVoid> + + + "onFlagLoad": ODEvent_Default<(flags:ODFlagManager_Default) => ODPromiseVoid> + "afterFlagsLoaded": ODEvent_Default<(flags:ODFlagManager_Default) => ODPromiseVoid> + "onFlagInit": ODEvent_Default<(flags:ODFlagManager_Default) => ODPromiseVoid> + "afterFlagsInitiated": ODEvent_Default<(flags:ODFlagManager_Default) => ODPromiseVoid> + + //configs + "onConfigLoad": ODEvent_Default<(configs:ODConfigManager_Default) => ODPromiseVoid> + "afterConfigsLoaded": ODEvent_Default<(configs:ODConfigManager_Default) => ODPromiseVoid> + + //databases + "onDatabaseLoad": ODEvent_Default<(databases:ODDatabaseManager_Default) => ODPromiseVoid> + "afterDatabasesLoaded": ODEvent_Default<(databases:ODDatabaseManager_Default) => ODPromiseVoid> + + //languages + "onLanguageLoad": ODEvent_Default<(languages:ODLanguageManager_Default) => ODPromiseVoid> + "afterLanguagesLoaded": ODEvent_Default<(languages:ODLanguageManager_Default) => ODPromiseVoid> + "onLanguageSelect": ODEvent_Default<(languages:ODLanguageManager_Default) => ODPromiseVoid> + "afterLanguagesSelected": ODEvent_Default<(main:ODLanguage|null, backup:ODLanguage|null, languages:ODLanguageManager_Default) => ODPromiseVoid> + + //sessions + "onSessionLoad": ODEvent_Default<(languages:ODSessionManager_Default) => ODPromiseVoid> + "afterSessionsLoaded": ODEvent_Default<(languages:ODSessionManager_Default) => ODPromiseVoid> + + //config checkers + "onCheckerLoad": ODEvent_Default<(checkers:ODCheckerManager_Default) => ODPromiseVoid> + "afterCheckersLoaded": ODEvent_Default<(checkers:ODCheckerManager_Default) => ODPromiseVoid> + "onCheckerFunctionLoad": ODEvent_Default<(functions:ODCheckerFunctionManager_Default, checkers:ODCheckerManager_Default) => ODPromiseVoid> + "afterCheckerFunctionsLoaded": ODEvent_Default<(functions:ODCheckerFunctionManager_Default, checkers:ODCheckerManager_Default) => ODPromiseVoid> + "onCheckerExecute": ODEvent_Default<(checkers:ODCheckerManager_Default) => ODPromiseVoid> + "afterCheckersExecuted": ODEvent_Default<(result:ODCheckerResult, checkers:ODCheckerManager_Default) => ODPromiseVoid> + "onCheckerTranslationLoad": ODEvent_Default<(translations:ODCheckerTranslationRegister_Default, enabled:boolean, checkers:ODCheckerManager_Default) => ODPromiseVoid> + "afterCheckerTranslationsLoaded": ODEvent_Default<(translations:ODCheckerTranslationRegister_Default, checkers:ODCheckerManager_Default) => ODPromiseVoid> + "onCheckerRender": ODEvent_Default<(renderer:ODCheckerRenderer_Default, checkers:ODCheckerManager_Default) => ODPromiseVoid> + "afterCheckersRendered": ODEvent_Default<(renderer:ODCheckerRenderer_Default, checkers:ODCheckerManager_Default) => ODPromiseVoid> + "onCheckerQuit": ODEvent_Default<(checkers:ODCheckerManager_Default) => ODPromiseVoid> + + //client configuration + "onClientLoad": ODEvent_Default<(client:ODClientManager_Default) => ODPromiseVoid> + "afterClientLoaded": ODEvent_Default<(client:ODClientManager_Default) => ODPromiseVoid> + "onClientInit": ODEvent_Default<(client:ODClientManager_Default) => ODPromiseVoid> + "afterClientInitiated": ODEvent_Default<(client:ODClientManager_Default) => ODPromiseVoid> + "onClientReady": ODEvent_Default<(client:ODClientManager_Default) => ODPromiseVoid> + "afterClientReady": ODEvent_Default<(client:ODClientManager_Default) => ODPromiseVoid> + "onClientActivityLoad": ODEvent_Default<(activity:ODClientActivityManager, client:ODClientManager_Default) => ODPromiseVoid> + "afterClientActivityLoaded": ODEvent_Default<(activity:ODClientActivityManager, client:ODClientManager_Default) => ODPromiseVoid> + "onClientActivityInit": ODEvent_Default<(activity:ODClientActivityManager, client:ODClientManager_Default) => ODPromiseVoid> + "afterClientActivityInitiated": ODEvent_Default<(activity:ODClientActivityManager, client:ODClientManager_Default) => ODPromiseVoid> + + //client slash commands + "onSlashCommandLoad": ODEvent_Default<(slash:ODSlashCommandManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + "afterSlashCommandsLoaded": ODEvent_Default<(slash:ODSlashCommandManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + "onSlashCommandRegister": ODEvent_Default<(slash:ODSlashCommandManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + "afterSlashCommandsRegistered": ODEvent_Default<(slash:ODSlashCommandManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + + //client text commands + "onTextCommandLoad": ODEvent_Default<(text:ODTextCommandManager_Default, client:ODClientManager_Default,) => ODPromiseVoid> + "afterTextCommandsLoaded": ODEvent_Default<(text:ODTextCommandManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + + //questions + "onQuestionLoad": ODEvent_Default<(questions:ODQuestionManager) => ODPromiseVoid> + "afterQuestionsLoaded": ODEvent_Default<(questions:ODQuestionManager) => ODPromiseVoid> + + //options + "onOptionLoad": ODEvent_Default<(options:ODOptionManager) => ODPromiseVoid> + "afterOptionsLoaded": ODEvent_Default<(options:ODOptionManager) => ODPromiseVoid> + + //panels + "onPanelLoad": ODEvent_Default<(panels:ODPanelManager) => ODPromiseVoid> + "afterPanelsLoaded": ODEvent_Default<(panels:ODPanelManager) => ODPromiseVoid> + "onPanelSpawn": ODEvent_Default<(panel:ODPanel) => ODPromiseVoid> + "afterPanelSpawned": ODEvent_Default<(panel:ODPanel) => ODPromiseVoid> + + //tickets + "onTicketLoad": ODEvent_Default<(tickets:ODTicketManager) => ODPromiseVoid> + "afterTicketsLoaded": ODEvent_Default<(tickets:ODTicketManager) => ODPromiseVoid> + + //ticket creation + "onTicketChannelCreation": ODEvent_Default<(option:ODTicketOption, user:discord.User) => ODPromiseVoid> + "afterTicketChannelCreated": ODEvent_Default<(option:ODTicketOption, channel:discord.GuildTextBasedChannel, user:discord.User) => ODPromiseVoid> + "onTicketChannelDeletion": ODEvent_Default<(ticket:ODTicket, channel:discord.GuildTextBasedChannel, user:discord.User) => ODPromiseVoid> + "afterTicketChannelDeleted": ODEvent_Default<(ticket:ODTicket, user:discord.User) => ODPromiseVoid> + "onTicketPermissionsCreated": ODEvent_Default<(option:ODTicketOption, permissions:ODPermissionManager_Default, channel:discord.GuildTextBasedChannel, user:discord.User) => ODPromiseVoid> + "afterTicketPermissionsCreated": ODEvent_Default<(option:ODTicketOption, permissions:ODPermissionManager_Default, channel:discord.GuildTextBasedChannel, user:discord.User) => ODPromiseVoid> + "onTicketMainMessageCreated": ODEvent_Default<(ticket:ODTicket, channel:discord.GuildTextBasedChannel, user:discord.User) => ODPromiseVoid> + "afterTicketMainMessageCreated": ODEvent_Default<(ticket:ODTicket, message:discord.Message, channel:discord.GuildTextBasedChannel, user:discord.User) => ODPromiseVoid> + + //ticket actions + "onTicketCreate": ODEvent_Default<(creator:discord.User) => ODPromiseVoid> + "afterTicketCreated": ODEvent_Default<(ticket:ODTicket, creator:discord.User, channel:discord.GuildTextBasedChannel) => ODPromiseVoid> + "onTicketClose": ODEvent_Default<(ticket:ODTicket, closer:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "afterTicketClosed": ODEvent_Default<(ticket:ODTicket, closer:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "onTicketReopen": ODEvent_Default<(ticket:ODTicket, reopener:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "afterTicketReopened": ODEvent_Default<(ticket:ODTicket, reopener:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "onTicketDelete": ODEvent_Default<(ticket:ODTicket, deleter:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "afterTicketDeleted": ODEvent_Default<(ticket:ODTicket, deleter:discord.User, reason:string|null) => ODPromiseVoid> + "onTicketMove": ODEvent_Default<(ticket:ODTicket, mover:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "afterTicketMoved": ODEvent_Default<(ticket:ODTicket, mover:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "onTicketClaim": ODEvent_Default<(ticket:ODTicket, claimer:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "afterTicketClaimed": ODEvent_Default<(ticket:ODTicket, claimer:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "onTicketUnclaim": ODEvent_Default<(ticket:ODTicket, unclaimer:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "afterTicketUnclaimed": ODEvent_Default<(ticket:ODTicket, unclaimer:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "onTicketPin": ODEvent_Default<(ticket:ODTicket, pinner:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "afterTicketPinned": ODEvent_Default<(ticket:ODTicket, pinner:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "onTicketUnpin": ODEvent_Default<(ticket:ODTicket, unpinner:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "afterTicketUnpinned": ODEvent_Default<(ticket:ODTicket, unpinner:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "onTicketUserAdd": ODEvent_Default<(ticket:ODTicket, adder:discord.User, user:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "afterTicketUserAdded": ODEvent_Default<(ticket:ODTicket, adder:discord.User, user:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "onTicketUserRemove": ODEvent_Default<(ticket:ODTicket, remover:discord.User, user:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "afterTicketUserRemoved": ODEvent_Default<(ticket:ODTicket, remover:discord.User, user:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "onTicketRename": ODEvent_Default<(ticket:ODTicket, renamer:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "afterTicketRenamed": ODEvent_Default<(ticket:ODTicket, renamer:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> + "onTicketsClear": ODEvent_Default<(tickets:ODTicket[], clearer:discord.User, channel:discord.GuildTextBasedChannel, filter:ODTicketClearFilter) => ODPromiseVoid> + "afterTicketsCleared": ODEvent_Default<(tickets:ODTicket[], clearer:discord.User, channel:discord.GuildTextBasedChannel, filter:ODTicketClearFilter) => ODPromiseVoid> + + //roles + "onRoleLoad": ODEvent_Default<(roles:ODRoleManager) => ODPromiseVoid> + "afterRolesLoaded": ODEvent_Default<(roles:ODRoleManager) => ODPromiseVoid> + "onRoleUpdate": ODEvent_Default<(user:discord.User,role:ODRole) => ODPromiseVoid> + "afterRolesUpdated": ODEvent_Default<(user:discord.User,role:ODRole) => ODPromiseVoid> + + //blacklist + "onBlacklistLoad": ODEvent_Default<(blacklist:ODBlacklistManager) => ODPromiseVoid> + "afterBlacklistLoaded": ODEvent_Default<(blacklist:ODBlacklistManager) => ODPromiseVoid> + + //transcripts + "onTranscriptCompilerLoad": ODEvent_Default<(transcripts:ODTranscriptManager_Default) => ODPromiseVoid> + "afterTranscriptCompilersLoaded": ODEvent_Default<(transcripts:ODTranscriptManager_Default) => ODPromiseVoid> + "onTranscriptHistoryLoad": ODEvent_Default<(transcripts:ODTranscriptManager_Default) => ODPromiseVoid> + "afterTranscriptHistoryLoaded": ODEvent_Default<(transcripts:ODTranscriptManager_Default) => ODPromiseVoid> + + //transcript creation + "onTranscriptCreate": ODEvent_Default<(transcripts:ODTranscriptManager_Default,ticket:ODTicket,channel:discord.TextChannel,user:discord.User) => ODPromiseVoid> + "afterTranscriptCreated": ODEvent_Default<(transcripts:ODTranscriptManager_Default,ticket:ODTicket,channel:discord.TextChannel,user:discord.User) => ODPromiseVoid> + "onTranscriptInit": ODEvent_Default<(transcripts:ODTranscriptManager_Default,ticket:ODTicket,channel:discord.TextChannel,user:discord.User) => ODPromiseVoid> + "afterTranscriptInitiated": ODEvent_Default<(transcripts:ODTranscriptManager_Default,ticket:ODTicket,channel:discord.TextChannel,user:discord.User) => ODPromiseVoid> + "onTranscriptCompile": ODEvent_Default<(transcripts:ODTranscriptManager_Default,ticket:ODTicket,channel:discord.TextChannel,user:discord.User) => ODPromiseVoid> + "afterTranscriptCompiled": ODEvent_Default<(transcripts:ODTranscriptManager_Default,ticket:ODTicket,channel:discord.TextChannel,user:discord.User) => ODPromiseVoid> + "onTranscriptReady": ODEvent_Default<(transcripts:ODTranscriptManager_Default,ticket:ODTicket,channel:discord.TextChannel,user:discord.User) => ODPromiseVoid> + "afterTranscriptReady": ODEvent_Default<(transcripts:ODTranscriptManager_Default,ticket:ODTicket,channel:discord.TextChannel,user:discord.User) => ODPromiseVoid> + + //builders + "onButtonBuilderLoad": ODEvent_Default<(buttons:ODButtonManager_Default, builders:ODBuilderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "afterButtonBuildersLoaded": ODEvent_Default<(buttons:ODButtonManager_Default, builders:ODBuilderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "onDropdownBuilderLoad": ODEvent_Default<(dropdowns:ODDropdownManager_Default, builders:ODBuilderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "afterDropdownBuildersLoaded": ODEvent_Default<(dropdowns:ODDropdownManager_Default, builders:ODBuilderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "onFileBuilderLoad": ODEvent_Default<(files:ODFileManager_Default, builders:ODBuilderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "afterFileBuildersLoaded": ODEvent_Default<(files:ODFileManager_Default, builders:ODBuilderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "onEmbedBuilderLoad": ODEvent_Default<(embeds:ODEmbedManager_Default, builders:ODBuilderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "afterEmbedBuildersLoaded": ODEvent_Default<(embeds:ODEmbedManager_Default, builders:ODBuilderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "onMessageBuilderLoad": ODEvent_Default<(messages:ODMessageManager_Default, builders:ODBuilderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "afterMessageBuildersLoaded": ODEvent_Default<(messages:ODMessageManager_Default, builders:ODBuilderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "onModalBuilderLoad": ODEvent_Default<(modals:ODModalManager_Default, builders:ODBuilderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "afterModalBuildersLoaded": ODEvent_Default<(modals:ODModalManager_Default, builders:ODBuilderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + + //responders + "onCommandResponderLoad": ODEvent_Default<(commands:ODCommandResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "afterCommandRespondersLoaded": ODEvent_Default<(commands:ODCommandResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "onButtonResponderLoad": ODEvent_Default<(buttons:ODButtonResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "afterButtonRespondersLoaded": ODEvent_Default<(buttons:ODButtonResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "onDropdownResponderLoad": ODEvent_Default<(dropdowns:ODDropdownResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "afterDropdownRespondersLoaded": ODEvent_Default<(dropdowns:ODDropdownResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "onModalResponderLoad": ODEvent_Default<(modals:ODModalResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "afterModalRespondersLoaded": ODEvent_Default<(modals:ODModalResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + + //actions + "onActionLoad": ODEvent_Default<(actions:ODActionManager_Default) => ODPromiseVoid> + "afterActionsLoaded": ODEvent_Default<(actions:ODActionManager_Default) => ODPromiseVoid> + + //verifybars + "onVerifyBarLoad": ODEvent_Default<(verifybars:ODVerifyBarManager_Default) => ODPromiseVoid> + "afterVerifyBarsLoaded": ODEvent_Default<(verifybars:ODVerifyBarManager_Default) => ODPromiseVoid> + + //permissions + "onPermissionLoad": ODEvent_Default<(permissions:ODPermissionManager_Default) => ODPromiseVoid> + "afterPermissionsLoaded": ODEvent_Default<(permissions:ODPermissionManager_Default) => ODPromiseVoid> + + //posts + "onPostLoad": ODEvent_Default<(posts:ODPostManager_Default) => ODPromiseVoid> + "afterPostsLoaded": ODEvent_Default<(posts:ODPostManager_Default) => ODPromiseVoid> + "onPostInit": ODEvent_Default<(posts:ODPostManager_Default) => ODPromiseVoid> + "afterPostsInitiated": ODEvent_Default<(posts:ODPostManager_Default) => ODPromiseVoid> + + //cooldowns + "onCooldownLoad": ODEvent_Default<(cooldowns:ODCooldownManager_Default) => ODPromiseVoid> + "afterCooldownsLoaded": ODEvent_Default<(cooldowns:ODCooldownManager_Default) => ODPromiseVoid> + "onCooldownInit": ODEvent_Default<(cooldowns:ODCooldownManager_Default) => ODPromiseVoid> + "afterCooldownsInitiated": ODEvent_Default<(cooldowns:ODCooldownManager_Default) => ODPromiseVoid> + + //help menu + "onHelpMenuCategoryLoad": ODEvent_Default<(menu:ODHelpMenuManager_Default) => ODPromiseVoid> + "afterHelpMenuCategoriesLoaded": ODEvent_Default<(menu:ODHelpMenuManager_Default) => ODPromiseVoid> + "onHelpMenuComponentLoad": ODEvent_Default<(menu:ODHelpMenuManager_Default) => ODPromiseVoid> + "afterHelpMenuComponentsLoaded": ODEvent_Default<(menu:ODHelpMenuManager_Default) => ODPromiseVoid> + + //stats + "onStatScopeLoad": ODEvent_Default<(stats:ODStatsManager_Default) => ODPromiseVoid> + "afterStatScopesLoaded": ODEvent_Default<(stats:ODStatsManager_Default) => ODPromiseVoid> + "onStatLoad": ODEvent_Default<(stats:ODStatsManager_Default) => ODPromiseVoid> + "afterStatsLoaded": ODEvent_Default<(stats:ODStatsManager_Default) => ODPromiseVoid> + "onStatInit": ODEvent_Default<(stats:ODStatsManager_Default) => ODPromiseVoid> + "afterStatsInitiated": ODEvent_Default<(stats:ODStatsManager_Default) => ODPromiseVoid> + + //code + "onCodeLoad": ODEvent_Default<(code:ODCodeManager_Default) => ODPromiseVoid> + "afterCodeLoaded": ODEvent_Default<(code:ODCodeManager_Default) => ODPromiseVoid> + "onCodeExecute": ODEvent_Default<(code:ODCodeManager_Default) => ODPromiseVoid> + "afterCodeExecuted": ODEvent_Default<(code:ODCodeManager_Default) => ODPromiseVoid> + + //livestatus + "onLiveStatusSourceLoad": ODEvent_Default<(livestatus:ODLiveStatusManager_Default) => ODPromiseVoid> + "afterLiveStatusSourcesLoaded": ODEvent_Default<(livestatus:ODLiveStatusManager_Default) => ODPromiseVoid> + + //startscreen + "onStartScreenLoad": ODEvent_Default<(startscreen:ODStartScreenManager_Default) => ODPromiseVoid> + "afterStartScreensLoaded": ODEvent_Default<(startscreen:ODStartScreenManager_Default) => ODPromiseVoid> + "onStartScreenRender": ODEvent_Default<(startscreen:ODStartScreenManager_Default) => ODPromiseVoid> + "afterStartScreensRendered": ODEvent_Default<(startscreen:ODStartScreenManager_Default) => ODPromiseVoid> +} + +/**## ODEventManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODEvent class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.events`! + */ +export class ODEventManager_Default extends ODEventManager { + get(id:StartScreenId): ODEventIds_Default[StartScreenId] + get(id:ODValidId): ODEvent|null + + get(id:ODValidId): ODEvent|null { + return super.get(id) + } + + remove(id:StartScreenId): ODEventIds_Default[StartScreenId] + remove(id:ODValidId): ODEvent|null + + remove(id:ODValidId): ODEvent|null { + return super.remove(id) + } + + exists(id:keyof ODEventIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODEventManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODEvent class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.events`! + */ +export class ODEvent_Default ODPromiseVoid)> extends ODEvent { + listen(callback:Callback): void { + return super.listen(callback) + } + listenOnce(callback:Callback): void { + return super.listenOnce(callback) + } + wait(): Promise> + wait(): Promise { + return super.wait() + } + emit(params:Parameters): Promise { + return super.emit(params) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/flag.ts b/src/core/api/defaults/flag.ts new file mode 100644 index 0000000..0d4b835 --- /dev/null +++ b/src/core/api/defaults/flag.ts @@ -0,0 +1,53 @@ +/////////////////////////////////////// +//DEFAULT PROCESS MODULE +/////////////////////////////////////// +import { ODValidId } from "../modules/base" +import { ODFlagManager, ODFlag } from "../modules/flag" + +/**## ODFlagManagerIds_Default `type` + * This type is an array of ids available in the `ODFlagManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODFlagManagerIds_Default { + "openticket:no-migration":ODFlag, + "openticket:dev-config":ODFlag, + "openticket:dev-database":ODFlag, + "openticket:debug":ODFlag, + "openticket:crash":ODFlag, + "openticket:no-transcripts":ODFlag, + "openticket:no-checker":ODFlag, + "openticket:checker":ODFlag, + "openticket:no-easter":ODFlag, + "openticket:no-plugins":ODFlag, + "openticket:soft-plugins":ODFlag, + "openticket:force-slash-update":ODFlag, +} + +/**## ODFlagManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODFlagManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.flags`! + */ +export class ODFlagManager_Default extends ODFlagManager { + get(id:FlagId): ODFlagManagerIds_Default[FlagId] + get(id:ODValidId): ODFlag|null + + get(id:ODValidId): ODFlag|null { + return super.get(id) + } + + remove(id:FlagId): ODFlagManagerIds_Default[FlagId] + remove(id:ODValidId): ODFlag|null + + remove(id:ODValidId): ODFlag|null { + return super.remove(id) + } + + exists(id:keyof ODFlagManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/helpmenu.ts b/src/core/api/defaults/helpmenu.ts new file mode 100644 index 0000000..7add16f --- /dev/null +++ b/src/core/api/defaults/helpmenu.ts @@ -0,0 +1,323 @@ +/////////////////////////////////////// +//DEFAULT HELP MODULE +/////////////////////////////////////// +import { ODValidId } from "../modules/base" +import { ODHelpMenuCategory, ODHelpMenuCommandComponent, ODHelpMenuComponent, ODHelpMenuManager } from "../modules/helpmenu" + +/**## ODHelpMenuManagerIds_Default `type` + * This type is an array of ids available in the `ODHelpMenuManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODHelpMenuManagerIds_Default { + "openticket:general":ODHelpMenuCategory_DefaultGeneral, + "openticket:ticket-basic":ODHelpMenuCategory_DefaultTicketBasic, + "openticket:ticket-advanced":ODHelpMenuCategory_DefaultTicketAdvanced, + "openticket:ticket-user":ODHelpMenuCategory_DefaultTicketUser, + "openticket:admin":ODHelpMenuCategory_DefaultAdmin, + "openticket:advanced":ODHelpMenuCategory_DefaultAdvanced, + "openticket:extra":ODHelpMenuCategory_DefaultExtra +} + +/**## ODHelpMenuManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODHelpMenuManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.helpmenu`! + */ +export class ODHelpMenuManager_Default extends ODHelpMenuManager { + get(id:HelpMenuCategoryId): ODHelpMenuManagerIds_Default[HelpMenuCategoryId] + get(id:ODValidId): ODHelpMenuCategory|null + + get(id:ODValidId): ODHelpMenuCategory|null { + return super.get(id) + } + + remove(id:HelpMenuCategoryId): ODHelpMenuManagerIds_Default[HelpMenuCategoryId] + remove(id:ODValidId): ODHelpMenuCategory|null + + remove(id:ODValidId): ODHelpMenuCategory|null { + return super.remove(id) + } + + exists(id:keyof ODHelpMenuManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODHelpMenuManagerCategoryIds_DefaultGeneral `type` + * This type is an array of ids available in the `ODHelpMenuCategory_DefaultGeneral` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODHelpMenuManagerCategoryIds_DefaultGeneral { + "openticket:help":ODHelpMenuCommandComponent, + "openticket:ticket":ODHelpMenuCommandComponent|null +} + +/**## ODHelpMenuCategory_DefaultGeneral `default_class` + * This is a special class that adds type definitions & typescript to the ODHelpMenuManager class. + * It doesn't add any extra features! + * + * This default class is made for the `openticket:general` category in `openticket.helpmenu`! + */ +export class ODHelpMenuCategory_DefaultGeneral extends ODHelpMenuCategory { + get(id:HelpMenuCategoryId): ODHelpMenuManagerCategoryIds_DefaultGeneral[HelpMenuCategoryId] + get(id:ODValidId): ODHelpMenuComponent|null + + get(id:ODValidId): ODHelpMenuComponent|null { + return super.get(id) + } + + remove(id:HelpMenuCategoryId): ODHelpMenuManagerCategoryIds_DefaultGeneral[HelpMenuCategoryId] + remove(id:ODValidId): ODHelpMenuComponent|null + + remove(id:ODValidId): ODHelpMenuComponent|null { + return super.remove(id) + } + + exists(id:keyof ODHelpMenuManagerCategoryIds_DefaultGeneral): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODHelpMenuManagerCategoryIds_DefaultTicketBasic `type` + * This type is an array of ids available in the `ODHelpMenuCategory_DefaultTicketBasic` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODHelpMenuManagerCategoryIds_DefaultTicketBasic { + "openticket:close":ODHelpMenuCommandComponent, + "openticket:delete":ODHelpMenuCommandComponent, + "openticket:reopen":ODHelpMenuCommandComponent +} + +/**## ODHelpMenuCategory_DefaultTicketBasic `default_class` + * This is a special class that adds type definitions & typescript to the ODHelpMenuManager class. + * It doesn't add any extra features! + * + * This default class is made for the `openticket:ticket` category in `openticket.helpmenu`! + */ +export class ODHelpMenuCategory_DefaultTicketBasic extends ODHelpMenuCategory { + get(id:HelpMenuCategoryId): ODHelpMenuManagerCategoryIds_DefaultTicketBasic[HelpMenuCategoryId] + get(id:ODValidId): ODHelpMenuComponent|null + + get(id:ODValidId): ODHelpMenuComponent|null { + return super.get(id) + } + + remove(id:HelpMenuCategoryId): ODHelpMenuManagerCategoryIds_DefaultTicketBasic[HelpMenuCategoryId] + remove(id:ODValidId): ODHelpMenuComponent|null + + remove(id:ODValidId): ODHelpMenuComponent|null { + return super.remove(id) + } + + exists(id:keyof ODHelpMenuManagerCategoryIds_DefaultTicketBasic): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODHelpMenuManagerCategoryIds_DefaultTicketAdvanced `type` + * This type is an array of ids available in the `ODHelpMenuCategory_DefaultTicketAdvanced` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODHelpMenuManagerCategoryIds_DefaultTicketAdvanced { + "openticket:pin":ODHelpMenuCommandComponent, + "openticket:unpin":ODHelpMenuCommandComponent, + "openticket:move":ODHelpMenuCommandComponent, + "openticket:rename":ODHelpMenuCommandComponent +} + +/**## ODHelpMenuCategory_DefaultTicketAdvanced `default_class` + * This is a special class that adds type definitions & typescript to the ODHelpMenuManager class. + * It doesn't add any extra features! + * + * This default class is made for the `openticket:ticket` category in `openticket.helpmenu`! + */ +export class ODHelpMenuCategory_DefaultTicketAdvanced extends ODHelpMenuCategory { + get(id:HelpMenuCategoryId): ODHelpMenuManagerCategoryIds_DefaultTicketAdvanced[HelpMenuCategoryId] + get(id:ODValidId): ODHelpMenuComponent|null + + get(id:ODValidId): ODHelpMenuComponent|null { + return super.get(id) + } + + remove(id:HelpMenuCategoryId): ODHelpMenuManagerCategoryIds_DefaultTicketAdvanced[HelpMenuCategoryId] + remove(id:ODValidId): ODHelpMenuComponent|null + + remove(id:ODValidId): ODHelpMenuComponent|null { + return super.remove(id) + } + + exists(id:keyof ODHelpMenuManagerCategoryIds_DefaultTicketAdvanced): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODHelpMenuManagerCategoryIds_DefaultTicketUser `type` + * This type is an array of ids available in the `ODHelpMenuCategory_DefaultTicketUser` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODHelpMenuManagerCategoryIds_DefaultTicketUser { + "openticket:claim":ODHelpMenuCommandComponent, + "openticket:unclaim":ODHelpMenuCommandComponent, + "openticket:add":ODHelpMenuCommandComponent, + "openticket:remove":ODHelpMenuCommandComponent +} + +/**## ODHelpMenuCategory_DefaultTicketUser `default_class` + * This is a special class that adds type definitions & typescript to the ODHelpMenuManager class. + * It doesn't add any extra features! + * + * This default class is made for the `openticket:ticket` category in `openticket.helpmenu`! + */ +export class ODHelpMenuCategory_DefaultTicketUser extends ODHelpMenuCategory { + get(id:HelpMenuCategoryId): ODHelpMenuManagerCategoryIds_DefaultTicketUser[HelpMenuCategoryId] + get(id:ODValidId): ODHelpMenuComponent|null + + get(id:ODValidId): ODHelpMenuComponent|null { + return super.get(id) + } + + remove(id:HelpMenuCategoryId): ODHelpMenuManagerCategoryIds_DefaultTicketUser[HelpMenuCategoryId] + remove(id:ODValidId): ODHelpMenuComponent|null + + remove(id:ODValidId): ODHelpMenuComponent|null { + return super.remove(id) + } + + exists(id:keyof ODHelpMenuManagerCategoryIds_DefaultTicketUser): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODHelpMenuManagerCategoryIds_DefaultAdmin `type` + * This type is an array of ids available in the `ODHelpMenuCategory_DefaultAdmin` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODHelpMenuManagerCategoryIds_DefaultAdmin { + "openticket:panel":ODHelpMenuCommandComponent, + "openticket:blacklist-view":ODHelpMenuCommandComponent, + "openticket:blacklist-add":ODHelpMenuCommandComponent, + "openticket:blacklist-remove":ODHelpMenuCommandComponent, + "openticket:blacklist-get":ODHelpMenuCommandComponent +} + +/**## ODHelpMenuCategory_DefaultAdmin `default_class` + * This is a special class that adds type definitions & typescript to the ODHelpMenuManager class. + * It doesn't add any extra features! + * + * This default class is made for the `openticket:admin` category in `openticket.helpmenu`! + */ +export class ODHelpMenuCategory_DefaultAdmin extends ODHelpMenuCategory { + get(id:HelpMenuCategoryId): ODHelpMenuManagerCategoryIds_DefaultAdmin[HelpMenuCategoryId] + get(id:ODValidId): ODHelpMenuComponent|null + + get(id:ODValidId): ODHelpMenuComponent|null { + return super.get(id) + } + + remove(id:HelpMenuCategoryId): ODHelpMenuManagerCategoryIds_DefaultAdmin[HelpMenuCategoryId] + remove(id:ODValidId): ODHelpMenuComponent|null + + remove(id:ODValidId): ODHelpMenuComponent|null { + return super.remove(id) + } + + exists(id:keyof ODHelpMenuManagerCategoryIds_DefaultAdmin): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODHelpMenuManagerCategoryIds_DefaultAdvanced `type` + * This type is an array of ids available in the `ODHelpMenuCategory_DefaultAdvanced` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODHelpMenuManagerCategoryIds_DefaultAdvanced { + "openticket:stats-global":ODHelpMenuCommandComponent, + "openticket:stats-reset":ODHelpMenuCommandComponent, + "openticket:stats-ticket":ODHelpMenuCommandComponent, + "openticket:stats-user":ODHelpMenuCommandComponent, + "openticket:autoclose-disable":ODHelpMenuCommandComponent, + "openticket:autoclose-enable":ODHelpMenuCommandComponent +} + +/**## ODHelpMenuCategory_DefaultAdvanced `default_class` + * This is a special class that adds type definitions & typescript to the ODHelpMenuManager class. + * It doesn't add any extra features! + * + * This default class is made for the `openticket:advanced` category in `openticket.helpmenu`! + */ +export class ODHelpMenuCategory_DefaultAdvanced extends ODHelpMenuCategory { + get(id:HelpMenuCategoryId): ODHelpMenuManagerCategoryIds_DefaultAdvanced[HelpMenuCategoryId] + get(id:ODValidId): ODHelpMenuComponent|null + + get(id:ODValidId): ODHelpMenuComponent|null { + return super.get(id) + } + + remove(id:HelpMenuCategoryId): ODHelpMenuManagerCategoryIds_DefaultAdvanced[HelpMenuCategoryId] + remove(id:ODValidId): ODHelpMenuComponent|null + + remove(id:ODValidId): ODHelpMenuComponent|null { + return super.remove(id) + } + + exists(id:keyof ODHelpMenuManagerCategoryIds_DefaultAdvanced): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODHelpMenuManagerCategoryIds_DefaultExtra `type` + * This type is an array of ids available in the `ODHelpMenuCategory_DefaultExtra` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODHelpMenuManagerCategoryIds_DefaultExtra {} + +/**## ODHelpMenuCategory_DefaultExtra `default_class` + * This is a special class that adds type definitions & typescript to the ODHelpMenuManager class. + * It doesn't add any extra features! + * + * This default class is made for the `openticket:general` category in `openticket.helpmenu`! + */ +export class ODHelpMenuCategory_DefaultExtra extends ODHelpMenuCategory { + get(id:HelpMenuCategoryId): ODHelpMenuManagerCategoryIds_DefaultExtra[HelpMenuCategoryId] + get(id:ODValidId): ODHelpMenuComponent|null + + get(id:ODValidId): ODHelpMenuComponent|null { + return super.get(id) + } + + remove(id:HelpMenuCategoryId): ODHelpMenuManagerCategoryIds_DefaultExtra[HelpMenuCategoryId] + remove(id:ODValidId): ODHelpMenuComponent|null + + remove(id:ODValidId): ODHelpMenuComponent|null { + return super.remove(id) + } + + exists(id:keyof ODHelpMenuManagerCategoryIds_DefaultExtra): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/language.ts b/src/core/api/defaults/language.ts new file mode 100644 index 0000000..d8ad7ab --- /dev/null +++ b/src/core/api/defaults/language.ts @@ -0,0 +1,510 @@ +/////////////////////////////////////// +//DEFAULT LANGUAGE MODULE +/////////////////////////////////////// +import { ODValidId } from "../modules/base" +import { ODLanguageManager, ODLanguage } from "../modules/language" + +/**## ODLanguageManagerTranslations_Default `type` + * This type is an array of ids available in the `ODLanguageManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export type ODLanguageManagerTranslations_Default = ( + "checker.system.headerOpenTicket"| + "checker.system.typeError"| + "checker.system.typeWarning"| + "checker.system.typeInfo"| + "checker.system.headerConfigChecker"| + "checker.system.headerDescription"| + "checker.system.footerError"| + "checker.system.footerWarning"| + "checker.system.footerSupport"| + "checker.system.compactInformation"| + "checker.system.dataPath"| + "checker.system.dataDocs"| + "checker.system.dataMessages"| + "checker.messages.invalidType"| + "checker.messages.propertyMissing"| + "checker.messages.propertyOptional"| + "checker.messages.objectDisabled"| + "checker.messages.nullInvalid"| + "checker.messages.switchInvalidType"| + "checker.messages.objectSwitchInvalid"| + "checker.messages.stringTooShort"| + "checker.messages.stringTooLong"| + "checker.messages.stringLengthInvalid"| + "checker.messages.stringStartsWith"| + "checker.messages.stringEndsWith"| + "checker.messages.stringContains"| + "checker.messages.stringChoices"| + "checker.messages.stringRegex"| + "checker.messages.numberTooShort"| + "checker.messages.numberTooLong"| + "checker.messages.numberLengthInvalid"| + "checker.messages.numberTooSmall"| + "checker.messages.numberTooLarge"| + "checker.messages.numberNotEqual"| + "checker.messages.numberStep"| + "checker.messages.numberStepOffset"| + "checker.messages.numberStartsWith"| + "checker.messages.numberEndsWith"| + "checker.messages.numberContains"| + "checker.messages.numberChoices"| + "checker.messages.numberFloat"| + "checker.messages.numberNegative"| + "checker.messages.numberPositive"| + "checker.messages.numberZero"| + "checker.messages.booleanTrue"| + "checker.messages.booleanFalse"| + "checker.messages.arrayEmptyDisabled"| + "checker.messages.arrayEmptyRequired"| + "checker.messages.arrayTooShort"| + "checker.messages.arrayTooLong"| + "checker.messages.arrayLengthInvalid"| + "checker.messages.arrayInvalidTypes"| + "checker.messages.arrayDouble"| + "checker.messages.discordInvalidId"| + "checker.messages.discordInvalidIdOptions"| + "checker.messages.discordInvalidToken"| + "checker.messages.colorInvalid"| + "checker.messages.emojiTooShort"| + "checker.messages.emojiTooLong"| + "checker.messages.emojiCustom"| + "checker.messages.emojiInvalid"| + "checker.messages.urlInvalid"| + "checker.messages.urlInvalidHttp"| + "checker.messages.urlInvalidProtocol"| + "checker.messages.urlInvalidHostname"| + "checker.messages.urlInvalidExtension"| + "checker.messages.urlInvalidPath"| + "checker.messages.idNotUnique"| + "checker.messages.idNonExistent"| + "checker.messages.invalidLanguage"| + "checker.messages.invalidButton"| + "checker.messages.unusedOption"| + "checker.messages.unusedQuestion"| + "checker.messages.dropdownOption"| + "actions.buttons.create"| + "actions.buttons.close"| + "actions.buttons.delete"| + "actions.buttons.reopen"| + "actions.buttons.claim"| + "actions.buttons.unclaim"| + "actions.buttons.pin"| + "actions.buttons.unpin"| + "actions.buttons.clear"| + "actions.buttons.helpSwitchSlash"| + "actions.buttons.helpSwitchText"| + "actions.buttons.helpPage"| + "actions.buttons.withReason"| + "actions.buttons.withoutTranscript"| + + "actions.titles.created"| + "actions.titles.close"| + "actions.titles.delete"| + "actions.titles.reopen"| + "actions.titles.claim"| + "actions.titles.unclaim"| + "actions.titles.pin"| + "actions.titles.unpin"| + "actions.titles.rename"| + "actions.titles.move"| + "actions.titles.add"| + "actions.titles.remove"| + + "actions.titles.help"| + "actions.titles.statsReset"| + "actions.titles.blacklistAdd"| + "actions.titles.blacklistRemove"| + "actions.titles.blacklistGet"| + "actions.titles.blacklistView"| + "actions.titles.blacklistAddDm"| + "actions.titles.blacklistRemoveDm"| + "actions.titles.clear"| + "actions.titles.roles"| + + "actions.titles.autoclose"| + "actions.titles.autocloseEnabled"| + "actions.titles.autocloseDisabled"| + "actions.titles.autodelete"| + "actions.titles.autodeleteEnabled"| + "actions.titles.autodeleteDisabled"| + + "actions.descriptions.create"| + "actions.descriptions.close"| + "actions.descriptions.delete"| + "actions.descriptions.reopen"| + "actions.descriptions.claim"| + "actions.descriptions.unclaim"| + "actions.descriptions.pin"| + "actions.descriptions.unpin"| + "actions.descriptions.rename"| + "actions.descriptions.move"| + "actions.descriptions.add"| + "actions.descriptions.remove"| + + "actions.descriptions.helpExplanation"| + "actions.descriptions.statsReset"| + "actions.descriptions.statsError"| + "actions.descriptions.blacklistAdd"| + "actions.descriptions.blacklistRemove"| + "actions.descriptions.blacklistGetSuccess"| + "actions.descriptions.blacklistGetEmpty"| + "actions.descriptions.blacklistViewEmpty"| + "actions.descriptions.blacklistViewTip"| + "actions.descriptions.clearVerify"| + "actions.descriptions.clearReady"| + "actions.descriptions.rolesEmpty"| + + "actions.descriptions.autocloseLeave"| + "actions.descriptions.autocloseTimeout"| + "actions.descriptions.autodeleteLeave"| + "actions.descriptions.autodeleteTimeout"| + "actions.descriptions.autocloseEnabled"| + "actions.descriptions.autocloseDisabled"| + "actions.descriptions.autodeleteEnabled"| + "actions.descriptions.autodeleteDisabled"| + + "actions.descriptions.ticketMessageLimit"| + "actions.descriptions.ticketMessageAutoclose"| + "actions.descriptions.ticketMessageAutodelete"| + "actions.descriptions.panelReady"| + + "actions.modal.closePlaceholder"| + "actions.modal.deletePlaceholder"| + "actions.modal.reopenPlaceholder"| + "actions.modal.claimPlaceholder"| + "actions.modal.unclaimPlaceholder"| + "actions.modal.pinPlaceholder"| + "actions.modal.unpinPlaceholder"| + + "actions.logs.createLog"| + "actions.logs.closeLog"| + "actions.logs.closeDm"| + "actions.logs.deleteLog"| + "actions.logs.deleteDm"| + "actions.logs.reopenLog"| + "actions.logs.reopenDm"| + "actions.logs.claimLog"| + "actions.logs.claimDm"| + "actions.logs.unclaimLog"| + "actions.logs.unclaimDm"| + "actions.logs.pinLog"| + "actions.logs.pinDm"| + "actions.logs.unpinLog"| + "actions.logs.unpinDm"| + "actions.logs.renameLog"| + "actions.logs.renameDm"| + "actions.logs.moveLog"| + "actions.logs.moveDm"| + "actions.logs.addLog"| + "actions.logs.addDm"| + "actions.logs.removeLog"| + "actions.logs.removeDm"| + + "actions.logs.blacklistAddLog"| + "actions.logs.blacklistRemoveLog"| + "actions.logs.blacklistAddDm"| + "actions.logs.blacklistRemoveDm"| + "actions.logs.clearLog"| + + "transcripts.success.visit"| + "transcripts.success.ready"| + "transcripts.success.textFileDescription"| + "transcripts.success.htmlProgress"| + + "transcripts.success.createdChannel"| + "transcripts.success.createdCreator"| + "transcripts.success.createdParticipant"| + "transcripts.success.createdActiveAdmin"| + "transcripts.success.createdEveryAdmin"| + "transcripts.success.createdOther"| + + "transcripts.errors.retry"| + "transcripts.errors.continue"| + "transcripts.errors.backup"| + "transcripts.errors.error"| + + "errors.titles.internalError"| + "errors.titles.optionMissing"| + "errors.titles.optionInvalid"| + "errors.titles.unknownCommand"| + "errors.titles.noPermissions"| + "errors.titles.unknownTicket"| + "errors.titles.deprecatedTicket"| + "errors.titles.unknownOption"| + "errors.titles.unknownPanel"| + "errors.titles.notInGuild"| + "errors.titles.channelRename"| + "errors.titles.busy"| + + "errors.descriptions.askForInfo"| + "errors.descriptions.askForInfoResolve"| + "errors.descriptions.internalError"| + "errors.descriptions.optionMissing"| + "errors.descriptions.optionInvalid"| + "errors.descriptions.optionInvalidChoose"| + "errors.descriptions.unknownCommand"| + "errors.descriptions.noPermissions"| + "errors.descriptions.noPermissionsList"| + "errors.descriptions.noPermissionsCooldown"| + "errors.descriptions.noPermissionsBlacklist"| + "errors.descriptions.noPermissionsLimitGlobal"| + "errors.descriptions.noPermissionsLimitGlobalUser"| + "errors.descriptions.noPermissionsLimitOption"| + "errors.descriptions.noPermissionsLimitOptionUser"| + "errors.descriptions.unknownTicket"| + "errors.descriptions.deprecatedTicket"| + "errors.descriptions.notInGuild"| + "errors.descriptions.channelRename"| + "errors.descriptions.channelRenameSource"| + "errors.descriptions.busy"| + + "errors.optionInvalidReasons.stringRegex"| + "errors.optionInvalidReasons.stringMinLength"| + "errors.optionInvalidReasons.stringMaxLength"| + "errors.optionInvalidReasons.numberInvalid"| + "errors.optionInvalidReasons.numberMin"| + "errors.optionInvalidReasons.numberMax"| + "errors.optionInvalidReasons.numberDecimal"| + "errors.optionInvalidReasons.numberNegative"| + "errors.optionInvalidReasons.numberPositive"| + "errors.optionInvalidReasons.numberZero"| + "errors.optionInvalidReasons.channelNotFound"| + "errors.optionInvalidReasons.userNotFound"| + "errors.optionInvalidReasons.roleNotFound"| + "errors.optionInvalidReasons.memberNotFound"| + "errors.optionInvalidReasons.mentionableNotFound"| + "errors.optionInvalidReasons.channelType"| + "errors.optionInvalidReasons.notInGuild"| + + "errors.permissions.developer"| + "errors.permissions.owner"| + "errors.permissions.admin"| + "errors.permissions.moderator"| + "errors.permissions.support"| + "errors.permissions.member"| + "errors.permissions.discord-administrator"| + + "errors.actionInvalid.close"| + "errors.actionInvalid.reopen"| + "errors.actionInvalid.claim"| + "errors.actionInvalid.unclaim"| + "errors.actionInvalid.pin"| + "errors.actionInvalid.unpin"| + "errors.actionInvalid.add"| + "errors.actionInvalid.remove"| + + "params.uppercase.ticket"| + "params.uppercase.tickets"| + "params.uppercase.reason"| + "params.uppercase.creator"| + "params.uppercase.remaining"| + "params.uppercase.added"| + "params.uppercase.removed"| + "params.uppercase.filter"| + "params.uppercase.claimedBy"| + "params.uppercase.method"| + "params.uppercase.type"| + "params.uppercase.blacklisted"| + "params.uppercase.panel"| + "params.uppercase.command"| + "params.uppercase.system"| + "params.uppercase.true"| + "params.uppercase.false"| + "params.uppercase.syntax"| + "params.uppercase.originalName"| + "params.uppercase.newName"| + "params.uppercase.until"| + "params.uppercase.validOptions"| + "params.uppercase.validPanels"| + "params.uppercase.autoclose"| + "params.uppercase.autodelete"| + "params.uppercase.startupDate"| + "params.uppercase.version"| + "params.uppercase.name"| + "params.uppercase.role"| + "params.uppercase.status"| + "params.uppercase.claimed"| + "params.uppercase.pinned"| + "params.uppercase.creationDate"| + + "params.lowercase.text"| + "params.lowercase.html"| + "params.lowercase.command"| + "params.lowercase.modal"| + "params.lowercase.button"| + "params.lowercase.dropdown"| + "params.lowercase.method"| + + "commands.reason"| + "commands.help"| + "commands.panel"| + "commands.panelId"| + "commands.panelAutoUpdate"| + "commands.ticket"| + "commands.ticketId"| + "commands.close"| + "commands.delete"| + "commands.deleteNoTranscript"| + "commands.reopen"| + "commands.claim"| + "commands.claimUser"| + "commands.unclaim"| + "commands.pin"| + "commands.unpin"| + "commands.move"| + "commands.moveId"| + "commands.rename"| + "commands.renameName"| + "commands.add"| + "commands.addUser"| + "commands.remove"| + "commands.removeUser"| + "commands.blacklist"| + "commands.blacklistView"| + "commands.blacklistAdd"| + "commands.blacklistRemove"| + "commands.blacklistGet"| + "commands.blacklistGetUser"| + "commands.stats"| + "commands.statsReset"| + "commands.statsGlobal"| + "commands.statsUser"| + "commands.statsUserUser"| + "commands.statsTicket"| + "commands.statsTicketTicket"| + "commands.clear"| + "commands.clearFilter"| + + "commands.clearFilters.all"| + "commands.clearFilters.open"| + "commands.clearFilters.close"| + "commands.clearFilters.claim"| + "commands.clearFilters.unclaim"| + "commands.clearFilters.pin"| + "commands.clearFilters.unpin"| + "commands.clearFilters.autoclose"| + + "commands.autoclose"| + "commands.autocloseDisable"| + "commands.autocloseEnable"| + "commands.autocloseEnableTime"| + "commands.autodelete"| + "commands.autodeleteDisable"| + "commands.autodeleteEnable"| + "commands.autodeleteEnableTime"| + + "helpMenu.help"| + "helpMenu.ticket"| + "helpMenu.close"| + "helpMenu.delete"| + "helpMenu.reopen"| + "helpMenu.pin"| + "helpMenu.unpin"| + "helpMenu.move"| + "helpMenu.rename"| + "helpMenu.claim"| + "helpMenu.unclaim"| + "helpMenu.add"| + "helpMenu.remove"| + "helpMenu.panel"| + "helpMenu.blacklistView"| + "helpMenu.blacklistAdd"| + "helpMenu.blacklistRemove"| + "helpMenu.blacklistGet"| + "helpMenu.statsGlobal"| + "helpMenu.statsTicket"| + "helpMenu.statsUser"| + "helpMenu.statsReset"| + "helpMenu.autocloseDisable"| + "helpMenu.autocloseEnable"| + "helpMenu.autodeleteDisable"| + "helpMenu.autodeleteEnable"| + + "stats.scopes.global"| + "stats.scopes.system"| + "stats.scopes.user"| + "stats.scopes.ticket"| + "stats.scopes.participants"| + + "stats.properties.ticketsCreated"| + "stats.properties.ticketsClosed"| + "stats.properties.ticketsDeleted"| + "stats.properties.ticketsReopened"| + "stats.properties.ticketsAutoclosed"| + "stats.properties.ticketsClaimed"| + "stats.properties.ticketsPinned"| + "stats.properties.ticketsMoved"| + "stats.properties.usersBlacklisted"| + "stats.properties.transcriptsCreated" +) + +/**## ODLanguageManagerIds_Default `type` + * This type is an array of ids available in the `ODLanguageManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODLanguageManagerIds_Default { + "openticket:custom":ODLanguage, + "openticket:english":ODLanguage, + "openticket:dutch":ODLanguage, + "openticket:portuguese":ODLanguage, + "openticket:czech":ODLanguage, + //ADD NEW LANGUAGES HERE!!! +} + +/**## ODLanguageManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODLanguageManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.languages`! + */ +export class ODLanguageManager_Default extends ODLanguageManager { + get(id:LanguageId): ODLanguageManagerIds_Default[LanguageId] + get(id:ODValidId): ODLanguage|null + + get(id:ODValidId): ODLanguage|null { + return super.get(id) + } + + remove(id:LanguageId): ODLanguageManagerIds_Default[LanguageId] + remove(id:ODValidId): ODLanguage|null + + remove(id:ODValidId): ODLanguage|null { + return super.remove(id) + } + + exists(id:keyof ODLanguageManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + getTranslation(id:ODLanguageManagerTranslations_Default): string + getTranslation(id:string): string|null + + getTranslation(id:string): string|null { + return super.getTranslation(id) + } + + setCurrentLanguage(id:keyof ODLanguageManagerIds_Default): void + setCurrentLanguage(id:ODValidId): void + + setCurrentLanguage(id:ODValidId): void { + return super.setCurrentLanguage(id) + } + + setBackupLanguage(id:keyof ODLanguageManagerIds_Default): void + setBackupLanguage(id:ODValidId): void + + setBackupLanguage(id:ODValidId): void { + return super.setBackupLanguage(id) + } + + getTranslationWithParams(id:ODLanguageManagerTranslations_Default, params:string[]): string + getTranslationWithParams(id:string, params:string[]): string|null + + getTranslationWithParams(id:string, params:string[]): string|null { + return super.getTranslationWithParams(id,params) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/permission.ts b/src/core/api/defaults/permission.ts new file mode 100644 index 0000000..0f01536 --- /dev/null +++ b/src/core/api/defaults/permission.ts @@ -0,0 +1,30 @@ +/////////////////////////////////////// +//DEFAULT PERMISSION MODULE +/////////////////////////////////////// +import { ODDebugger } from "../modules/console" +import { ODPermissionManager } from "../modules/permission" + +/**## ODPermissionManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODPermissionManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.permissions`! + */ +export class ODPermissionManager_Default extends ODPermissionManager { + constructor(debug:ODDebugger){ + super(debug,true) + } +} + +/**## ODPermissionEmbedType `type` + * This type contains all types available in the `openticket:no-permissions` embed. + */ +export type ODPermissionEmbedType = ( + "developer"| + "owner"| + "admin"| + "moderator"| + "support"| + "member"| + "discord-administrator" +) \ No newline at end of file diff --git a/src/core/api/defaults/post.ts b/src/core/api/defaults/post.ts new file mode 100644 index 0000000..0b0dc27 --- /dev/null +++ b/src/core/api/defaults/post.ts @@ -0,0 +1,44 @@ +/////////////////////////////////////// +//DEFAULT POST MODULE +/////////////////////////////////////// +import { ODValidId } from "../modules/base" +import { ODPost, ODPostManager } from "../modules/post" +import * as discord from "discord.js" + +/**## ODPostManagerIds_Default `type` + * This type is an array of ids available in the `ODPostManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODPostManagerIds_Default { + "openticket:logs":ODPost|null, + "openticket:transcripts":ODPost|null +} + +/**## ODPostManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODPostManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.code`! + */ +export class ODPostManager_Default extends ODPostManager { + get(id:PostId): ODPostManagerIds_Default[PostId] + get(id:ODValidId): ODPost|null + + get(id:ODValidId): ODPost|null { + return super.get(id) + } + + remove(id:PostId): ODPostManagerIds_Default[PostId] + remove(id:ODValidId): ODPost|null + + remove(id:ODValidId): ODPost|null { + return super.remove(id) + } + + exists(id:keyof ODPostManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/responder.ts b/src/core/api/defaults/responder.ts new file mode 100644 index 0000000..ffe944f --- /dev/null +++ b/src/core/api/defaults/responder.ts @@ -0,0 +1,255 @@ +/////////////////////////////////////// +//DEFAULT RESPONDER MODULE +/////////////////////////////////////// +import { ODValidId } from "../modules/base" +import { ODButtonResponder, ODButtonResponderInstance, ODButtonResponderManager, ODCommandResponder, ODCommandResponderInstance, ODCommandResponderManager, ODDropdownResponder, ODDropdownResponderInstance, ODDropdownResponderManager, ODModalResponder, ODModalResponderInstance, ODModalResponderManager, ODResponderManager } from "../modules/responder" +import { ODWorkerManager_Default } from "./worker" + +/**## ODResponderManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODResponderManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.responders`! + */ +export class ODResponderManager_Default extends ODResponderManager { + declare commands: ODCommandResponderManager_Default + declare buttons: ODButtonResponderManager_Default + declare dropdowns: ODDropdownResponderManager_Default + declare modals: ODModalResponderManager_Default +} + +/**## ODCommandResponderManagerIds_Default `type` + * This type is an array of ids available in the `ODCommandResponderManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODCommandResponderManagerIds_Default { + "openticket:help":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:help"|"openticket:logs"}, + "openticket:stats":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:stats"|"openticket:logs"}, + "openticket:panel":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:panel"|"openticket:logs"}, + "openticket:ticket":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:ticket"|"openticket:logs"}, + "openticket:blacklist":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:blacklist"|"openticket:discord-logs"|"openticket:logs"}, + + "openticket:close":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:close"|"openticket:logs"}, + "openticket:reopen":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:reopen"|"openticket:logs"}, + "openticket:delete":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:delete"|"openticket:logs"}, + "openticket:claim":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:claim"|"openticket:logs"}, + "openticket:unclaim":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:unclaim"|"openticket:logs"}, + "openticket:pin":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:pin"|"openticket:logs"}, + "openticket:unpin":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:unpin"|"openticket:logs"}, + + "openticket:rename":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:rename"|"openticket:logs"}, + "openticket:move":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:move"|"openticket:logs"}, + "openticket:add":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:add"|"openticket:logs"}, + "openticket:remove":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:remove"|"openticket:logs"}, + "openticket:clear":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:clear"|"openticket:logs"}, + + "openticket:autoclose":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:autoclose"|"openticket:logs"}, + "openticket:autodelete":{source:"slash"|"text",params:{},workers:"openticket:permissions"|"openticket:autodelete"|"openticket:logs"}, +} + +/**## ODCommandResponderManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODCommandResponderManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.responders.commands`! + */ +export class ODCommandResponderManager_Default extends ODCommandResponderManager { + get(id:CommandResponderId): ODCommandResponder_Default + get(id:ODValidId): ODCommandResponder<"slash"|"text",any>|null + + get(id:ODValidId): ODCommandResponder<"slash"|"text",any>|null { + return super.get(id) + } + + remove(id:CommandResponderId): ODCommandResponder_Default + remove(id:ODValidId): ODCommandResponder<"slash"|"text",any>|null + + remove(id:ODValidId): ODCommandResponder<"slash"|"text",any>|null { + return super.remove(id) + } + + exists(id:keyof ODCommandResponderManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODCommandResponder_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODCommandResponder class. + * It doesn't add any extra features! + * + * This default class is made for the default `ODCommandResponder`'s! + */ +export class ODCommandResponder_Default extends ODCommandResponder { + declare workers: ODWorkerManager_Default +} + +/**## ODButtonResponderManagerIds_Default `type` + * This type is an array of ids available in the `ODButtonResponderManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODButtonResponderManagerIds_Default { + "openticket:verifybar-success":{source:"button",params:{},workers:"openticket:handle-verifybar"}, + "openticket:verifybar-failure":{source:"button",params:{},workers:"openticket:handle-verifybar"}, + + "openticket:help-menu-switch":{source:"button",params:{},workers:"openticket:update-help-menu"}, + "openticket:help-menu-previous":{source:"button",params:{},workers:"openticket:update-help-menu"}, + "openticket:help-menu-next":{source:"button",params:{},workers:"openticket:update-help-menu"}, + + "openticket:ticket-option":{source:"button",params:{},workers:"openticket:ticket-option"}, + "openticket:role-option":{source:"button",params:{},workers:"openticket:role-option"}, + + "openticket:claim-ticket":{source:"button",params:{},workers:"openticket:claim-ticket"}, + "openticket:unclaim-ticket":{source:"button",params:{},workers:"openticket:unclaim-ticket"}, + "openticket:pin-ticket":{source:"button",params:{},workers:"openticket:pin-ticket"}, + "openticket:unpin-ticket":{source:"button",params:{},workers:"openticket:unpin-ticket"}, + "openticket:close-ticket":{source:"button",params:{},workers:"openticket:close-ticket"}, + "openticket:reopen-ticket":{source:"button",params:{},workers:"openticket:reopen-ticket"}, + "openticket:delete-ticket":{source:"button",params:{},workers:"openticket:delete-ticket"}, + + "openticket:transcript-error-retry":{source:"button",params:{},workers:"openticket:permissions"|"openticket:delete-ticket"|"openticket:logs"}, + "openticket:transcript-error-continue":{source:"button",params:{},workers:"openticket:permissions"|"openticket:delete-ticket"|"openticket:logs"}, + "openticket:clear-continue":{source:"button",params:{},workers:"openticket:clear-continue"}, +} + +/**## ODButtonResponderManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODButtonResponderManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.responders.buttons`! + */ +export class ODButtonResponderManager_Default extends ODButtonResponderManager { + get(id:ButtonResponderId): ODButtonResponder_Default + get(id:ODValidId): ODButtonResponder<"button",any>|null + + get(id:ODValidId): ODButtonResponder<"button",any>|null { + return super.get(id) + } + + remove(id:ButtonResponderId): ODButtonResponder_Default + remove(id:ODValidId): ODButtonResponder<"button",any>|null + + remove(id:ODValidId): ODButtonResponder<"button",any>|null { + return super.remove(id) + } + + exists(id:keyof ODButtonResponderManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODButtonResponder_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODButtonResponder class. + * It doesn't add any extra features! + * + * This default class is made for the default `ODButtonResponder`'s! + */ +export class ODButtonResponder_Default extends ODButtonResponder { + declare workers: ODWorkerManager_Default +} + +/**## ODDropdownResponderManagerIds_Default `type` + * This type is an array of ids available in the `ODDropdownResponderManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODDropdownResponderManagerIds_Default { + "openticket:panel-dropdown-tickets":{source:"dropdown",params:{},workers:"openticket:panel-dropdown-tickets"}, +} + +/**## ODDropdownResponderManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODDropdownResponderManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.responders.dropdowns`! + */ +export class ODDropdownResponderManager_Default extends ODDropdownResponderManager { + get(id:DropdownResponderId): ODDropdownResponder_Default + get(id:ODValidId): ODDropdownResponder<"dropdown",any>|null + + get(id:ODValidId): ODDropdownResponder<"dropdown",any>|null { + return super.get(id) + } + + remove(id:DropdownResponderId): ODDropdownResponder_Default + remove(id:ODValidId): ODDropdownResponder<"dropdown",any>|null + + remove(id:ODValidId): ODDropdownResponder<"dropdown",any>|null { + return super.remove(id) + } + + exists(id:keyof ODDropdownResponderManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODDropdownResponder_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODDropdownResponder class. + * It doesn't add any extra features! + * + * This default class is made for the default `ODDropdownResponder`'s! + */ +export class ODDropdownResponder_Default extends ODDropdownResponder { + declare workers: ODWorkerManager_Default +} + +/**## ODModalResponderManagerIds_Default `type` + * This type is an array of ids available in the `ODModalResponderManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODModalResponderManagerIds_Default { + "openticket:ticket-questions":{source:"modal",params:{},workers:"openticket:ticket-questions"}, + "openticket:close-ticket-reason":{source:"modal",params:{},workers:"openticket:close-ticket-reason"}, + "openticket:reopen-ticket-reason":{source:"modal",params:{},workers:"openticket:reopen-ticket-reason"}, + "openticket:delete-ticket-reason":{source:"modal",params:{},workers:"openticket:delete-ticket-reason"}, + "openticket:claim-ticket-reason":{source:"modal",params:{},workers:"openticket:claim-ticket-reason"}, + "openticket:unclaim-ticket-reason":{source:"modal",params:{},workers:"openticket:unclaim-ticket-reason"}, + "openticket:pin-ticket-reason":{source:"modal",params:{},workers:"openticket:pin-ticket-reason"}, + "openticket:unpin-ticket-reason":{source:"modal",params:{},workers:"openticket:unpin-ticket-reason"}, +} + +/**## ODModalResponderManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODModalResponderManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.responders.dropdowns`! + */ +export class ODModalResponderManager_Default extends ODModalResponderManager { + get(id:ModalResponderId): ODModalResponder_Default + get(id:ODValidId): ODModalResponder<"modal",any>|null + + get(id:ODValidId): ODModalResponder<"modal",any>|null { + return super.get(id) + } + + remove(id:ModalResponderId): ODModalResponder_Default + remove(id:ODValidId): ODModalResponder<"modal",any>|null + + remove(id:ODValidId): ODModalResponder<"modal",any>|null { + return super.remove(id) + } + + exists(id:keyof ODModalResponderManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODModalResponder_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODModalResponder class. + * It doesn't add any extra features! + * + * This default class is made for the default `ODModalResponder`'s! + */ +export class ODModalResponder_Default extends ODModalResponder { + declare workers: ODWorkerManager_Default +} \ No newline at end of file diff --git a/src/core/api/defaults/session.ts b/src/core/api/defaults/session.ts new file mode 100644 index 0000000..cce95ec --- /dev/null +++ b/src/core/api/defaults/session.ts @@ -0,0 +1,42 @@ +/////////////////////////////////////// +//DEFAULT SESSION MODULE +/////////////////////////////////////// +import { ODValidId } from "../modules/base" +import { ODSession, ODSessionManager } from "../modules/session" + +/**## ODSessionManagerIds_Default `type` + * This type is an array of ids available in the `ODSessionManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODSessionManagerIds_Default { + "test-session":ODSession +} + +/**## ODSessionManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODSessionManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.sessions`! + */ +export class ODSessionManager_Default extends ODSessionManager { + get(id:SessionId): ODSessionManagerIds_Default[SessionId] + get(id:ODValidId): ODSession|null + + get(id:ODValidId): ODSession|null { + return super.get(id) + } + + remove(id:SessionId): ODSessionManagerIds_Default[SessionId] + remove(id:ODValidId): ODSession|null + + remove(id:ODValidId): ODSession|null { + return super.remove(id) + } + + exists(id:keyof ODSessionManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/startscreen.ts b/src/core/api/defaults/startscreen.ts new file mode 100644 index 0000000..4d49c07 --- /dev/null +++ b/src/core/api/defaults/startscreen.ts @@ -0,0 +1,48 @@ +/////////////////////////////////////// +//DEFAULT STARTSCREEN MODULE +/////////////////////////////////////// +import { ODValidId } from "../modules/base" +import { ODStartScreenCategoryComponent, ODStartScreenComponent, ODStartScreenFlagsCategoryComponent, ODStartScreenHeaderComponent, ODStartScreenLiveStatusCategoryComponent, ODStartScreenLogoComponent, ODStartScreenManager, ODStartScreenPluginsCategoryComponent, ODStartScreenPropertiesCategoryComponent } from "../modules/startscreen" + +/**## ODStartScreenManagerIds_Default `type` + * This type is an array of ids available in the `ODStartScreenManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODStartScreenManagerIds_Default { + "openticket:logo":ODStartScreenLogoComponent, + "openticket:header":ODStartScreenHeaderComponent, + "openticket:flags":ODStartScreenFlagsCategoryComponent, + "openticket:plugins":ODStartScreenPluginsCategoryComponent, + "openticket:stats":ODStartScreenPropertiesCategoryComponent, + "openticket:livestatus":ODStartScreenLiveStatusCategoryComponent, + "openticket:logs":ODStartScreenCategoryComponent +} + +/**## ODStartScreenManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODStartScreenManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.startscreen`! + */ +export class ODStartScreenManager_Default extends ODStartScreenManager { + get(id:StartScreenId): ODStartScreenManagerIds_Default[StartScreenId] + get(id:ODValidId): ODStartScreenComponent|null + + get(id:ODValidId): ODStartScreenComponent|null { + return super.get(id) + } + + remove(id:StartScreenId): ODStartScreenManagerIds_Default[StartScreenId] + remove(id:ODValidId): ODStartScreenComponent|null + + remove(id:ODValidId): ODStartScreenComponent|null { + return super.remove(id) + } + + exists(id:keyof ODStartScreenManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/stat.ts b/src/core/api/defaults/stat.ts new file mode 100644 index 0000000..3366d7d --- /dev/null +++ b/src/core/api/defaults/stat.ts @@ -0,0 +1,363 @@ +/////////////////////////////////////// +//DEFAULT SESSION MODULE +/////////////////////////////////////// +import { Guild, TextBasedChannel, User } from "discord.js" +import { ODValidId } from "../modules/base" +import { ODStatScope, ODStatGlobalScope, ODStatsManager, ODStat, ODBasicStat, ODDynamicStat, ODValidStatValue, ODStatScopeSetMode } from "../modules/stat" + +/**## ODStatsManagerIds_Default `type` + * This type is an array of ids available in the `ODStatsManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODStatsManagerIds_Default { + "openticket:global":ODStatGlobalScope_DefaultGlobal, + "openticket:system":ODStatGlobalScope_DefaultSystem, + "openticket:user":ODStatScope_DefaultUser, + "openticket:ticket":ODStatScope_DefaultTicket, + "openticket:participants":ODStatScope_DefaultParticipants +} + +/**## ODStatsManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODStatsManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.stats`! + */ +export class ODStatsManager_Default extends ODStatsManager { + get(id:StatsId): ODStatsManagerIds_Default[StatsId] + get(id:ODValidId): ODStatScope|null + + get(id:ODValidId): ODStatScope|null { + return super.get(id) + } + + remove(id:StatsId): ODStatsManagerIds_Default[StatsId] + remove(id:ODValidId): ODStatScope|null + + remove(id:ODValidId): ODStatScope|null { + return super.remove(id) + } + + exists(id:keyof ODStatsManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODStatGlobalScopeIds_DefaultGlobal `type` + * This type is an array of ids available in the `ODStatGlobalScope_DefaultGlobal` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODStatGlobalScopeIds_DefaultGlobal { + "openticket:tickets-created":ODBasicStat, + "openticket:tickets-closed":ODBasicStat, + "openticket:tickets-deleted":ODBasicStat, + "openticket:tickets-reopened":ODBasicStat, + "openticket:tickets-autoclosed":ODBasicStat, + "openticket:tickets-autodeleted":ODBasicStat, + "openticket:tickets-claimed":ODBasicStat, + "openticket:tickets-pinned":ODBasicStat, + "openticket:tickets-moved":ODBasicStat, + "openticket:users-blacklisted":ODBasicStat, + "openticket:transcripts-created":ODBasicStat +} + +/**## ODStatGlobalScope_DefaultGlobal `default_class` + * This is a special class that adds type definitions & typescript to the ODStatsManager class. + * It doesn't add any extra features! + * + * This default class is made for the `openticket:global` category in `openticket.stats`! + */ +export class ODStatGlobalScope_DefaultGlobal extends ODStatGlobalScope { + get(id:StatsId): ODStatGlobalScopeIds_DefaultGlobal[StatsId] + get(id:ODValidId): ODStat|null + + get(id:ODValidId): ODStat|null { + return super.get(id) + } + + remove(id:StatsId): ODStatGlobalScopeIds_DefaultGlobal[StatsId] + remove(id:ODValidId): ODStat|null + + remove(id:ODValidId): ODStat|null { + return super.remove(id) + } + + exists(id:keyof ODStatGlobalScopeIds_DefaultGlobal): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + getStat(id:StatsId): ODValidStatValue|null + getStat(id:ODValidId): ODValidStatValue|null + + getStat(id:ODValidId): ODValidStatValue|null { + return super.getStat(id) + } + + setStat(id:StatsId, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean + setStat(id:ODValidId, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean + + setStat(id:ODValidId, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean { + return super.setStat(id,value,mode) + } + + resetStat(id:ODValidId): ODValidStatValue|null + resetStat(id:ODValidId): ODValidStatValue|null + + resetStat(id:ODValidId): ODValidStatValue|null { + return super.resetStat(id) + } +} + +/**## ODStatGlobalScopeIds_DefaultSystem `type` + * This type is an array of ids available in the `ODStatScope_DefaultSystem` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODStatGlobalScopeIds_DefaultSystem { + "openticket:startup-date":ODDynamicStat, + "openticket:version":ODDynamicStat +} + +/**## ODStatGlobalScope_DefaultSystem `default_class` + * This is a special class that adds type definitions & typescript to the ODStatsManager class. + * It doesn't add any extra features! + * + * This default class is made for the `openticket:system` category in `openticket.stats`! + */ +export class ODStatGlobalScope_DefaultSystem extends ODStatGlobalScope { + get(id:StatsId): ODStatGlobalScopeIds_DefaultSystem[StatsId] + get(id:ODValidId): ODStat|null + + get(id:ODValidId): ODStat|null { + return super.get(id) + } + + remove(id:StatsId): ODStatGlobalScopeIds_DefaultSystem[StatsId] + remove(id:ODValidId): ODStat|null + + remove(id:ODValidId): ODStat|null { + return super.remove(id) + } + + exists(id:keyof ODStatGlobalScopeIds_DefaultSystem): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + getStat(id:StatsId): ODValidStatValue|null + getStat(id:ODValidId): ODValidStatValue|null + + getStat(id:ODValidId): ODValidStatValue|null { + return super.getStat(id) + } + + setStat(id:StatsId, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean + setStat(id:ODValidId, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean + + setStat(id:ODValidId, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean { + return super.setStat(id,value,mode) + } + + resetStat(id:ODValidId): ODValidStatValue|null + resetStat(id:ODValidId): ODValidStatValue|null + + resetStat(id:ODValidId): ODValidStatValue|null { + return super.resetStat(id) + } +} + +/**## ODStatScopeIds_DefaultUser `type` + * This type is an array of ids available in the `ODStatScope_DefaultUser` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODStatScopeIds_DefaultUser { + "openticket:name":ODDynamicStat, + "openticket:role":ODDynamicStat, + "openticket:tickets-created":ODBasicStat, + "openticket:tickets-closed":ODBasicStat, + "openticket:tickets-deleted":ODBasicStat, + "openticket:tickets-reopened":ODBasicStat, + "openticket:tickets-claimed":ODBasicStat, + "openticket:tickets-pinned":ODBasicStat, + "openticket:tickets-moved":ODBasicStat, + "openticket:users-blacklisted":ODBasicStat, + "openticket:transcripts-created":ODBasicStat +} + +/**## ODStatScope_DefaultUser `default_class` + * This is a special class that adds type definitions & typescript to the ODStatsManager class. + * It doesn't add any extra features! + * + * This default class is made for the `openticket:user` category in `openticket.stats`! + */ +export class ODStatScope_DefaultUser extends ODStatScope { + get(id:StatsId): ODStatScopeIds_DefaultUser[StatsId] + get(id:ODValidId): ODStat|null + + get(id:ODValidId): ODStat|null { + return super.get(id) + } + + remove(id:StatsId): ODStatScopeIds_DefaultUser[StatsId] + remove(id:ODValidId): ODStat|null + + remove(id:ODValidId): ODStat|null { + return super.remove(id) + } + + exists(id:keyof ODStatScopeIds_DefaultUser): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + getStat(id:StatsId, scopeId:string): ODValidStatValue|null + getStat(id:ODValidId, scopeId:string): ODValidStatValue|null + + getStat(id:ODValidId, scopeId:string): ODValidStatValue|null { + return super.getStat(id,scopeId) + } + + setStat(id:StatsId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean + setStat(id:ODValidId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean + + setStat(id:ODValidId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean { + return super.setStat(id,scopeId,value,mode) + } + + resetStat(id:ODValidId, scopeId:string): ODValidStatValue|null + resetStat(id:ODValidId, scopeId:string): ODValidStatValue|null + + resetStat(id:ODValidId, scopeId:string): ODValidStatValue|null { + return super.resetStat(id,scopeId) + } +} + +/**## ODStatScopeIds_DefaultTicket `type` + * This type is an array of ids available in the `ODStatScope_DefaultTicket` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODStatScopeIds_DefaultTicket { + "openticket:name":ODDynamicStat, + "openticket:status":ODDynamicStat, + "openticket:claimed":ODDynamicStat, + "openticket:pinned":ODDynamicStat, + "openticket:creation-date":ODDynamicStat, + "openticket:creator":ODDynamicStat +} + +/**## ODStatScope_DefaultTicket `default_class` + * This is a special class that adds type definitions & typescript to the ODStatsManager class. + * It doesn't add any extra features! + * + * This default class is made for the `openticket:ticket` category in `openticket.stats`! + */ +export class ODStatScope_DefaultTicket extends ODStatScope { + get(id:StatsId): ODStatScopeIds_DefaultTicket[StatsId] + get(id:ODValidId): ODStat|null + + get(id:ODValidId): ODStat|null { + return super.get(id) + } + + remove(id:StatsId): ODStatScopeIds_DefaultTicket[StatsId] + remove(id:ODValidId): ODStat|null + + remove(id:ODValidId): ODStat|null { + return super.remove(id) + } + + exists(id:keyof ODStatScopeIds_DefaultTicket): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + getStat(id:StatsId, scopeId:string): ODValidStatValue|null + getStat(id:ODValidId, scopeId:string): ODValidStatValue|null + + getStat(id:ODValidId, scopeId:string): ODValidStatValue|null { + return super.getStat(id,scopeId) + } + + setStat(id:StatsId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean + setStat(id:ODValidId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean + + setStat(id:ODValidId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean { + return super.setStat(id,scopeId,value,mode) + } + + resetStat(id:ODValidId, scopeId:string): ODValidStatValue|null + resetStat(id:ODValidId, scopeId:string): ODValidStatValue|null + + resetStat(id:ODValidId, scopeId:string): ODValidStatValue|null { + return super.resetStat(id,scopeId) + } +} + +/**## ODStatScopeIds_DefaultParticipants `type` + * This type is an array of ids available in the `ODStatScope_DefaultParticipants` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODStatScopeIds_DefaultParticipants { + "openticket:participants":ODDynamicStat +} + +/**## ODStatScope_DefaultParticipants `default_class` + * This is a special class that adds type definitions & typescript to the ODStatsManager class. + * It doesn't add any extra features! + * + * This default class is made for the `openticket:participants` category in `openticket.stats`! + */ +export class ODStatScope_DefaultParticipants extends ODStatScope { + get(id:StatsId): ODStatScopeIds_DefaultParticipants[StatsId] + get(id:ODValidId): ODStat|null + + get(id:ODValidId): ODStat|null { + return super.get(id) + } + + remove(id:StatsId): ODStatScopeIds_DefaultParticipants[StatsId] + remove(id:ODValidId): ODStat|null + + remove(id:ODValidId): ODStat|null { + return super.remove(id) + } + + exists(id:keyof ODStatScopeIds_DefaultParticipants): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + getStat(id:StatsId, scopeId:string): ODValidStatValue|null + getStat(id:ODValidId, scopeId:string): ODValidStatValue|null + + getStat(id:ODValidId, scopeId:string): ODValidStatValue|null { + return super.getStat(id,scopeId) + } + + setStat(id:StatsId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean + setStat(id:ODValidId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean + + setStat(id:ODValidId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean { + return super.setStat(id,scopeId,value,mode) + } + + resetStat(id:ODValidId, scopeId:string): ODValidStatValue|null + resetStat(id:ODValidId, scopeId:string): ODValidStatValue|null + + resetStat(id:ODValidId, scopeId:string): ODValidStatValue|null { + return super.resetStat(id,scopeId) + } +} \ No newline at end of file diff --git a/src/core/api/defaults/verifybar.ts b/src/core/api/defaults/verifybar.ts new file mode 100644 index 0000000..3df5f8f --- /dev/null +++ b/src/core/api/defaults/verifybar.ts @@ -0,0 +1,72 @@ +/////////////////////////////////////// +//DEFAULT VERIFYBAR MODULE +/////////////////////////////////////// +import { ODValidId } from "../modules/base" +import { ODButtonResponderInstance } from "../modules/responder" +import { ODWorkerManager_Default } from "../defaults/worker" +import { ODVerifyBarManager, ODVerifyBar } from "../modules/verifybar" +import * as discord from "discord.js" + +/**## ODVerifyBarManagerIds_Default `type` + * This type is an array of ids available in the `ODVerifyBarManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODVerifyBarManagerIds_Default { + "openticket:claim-ticket-ticket-message":{successWorkerIds:"openticket:permissions"|"openticket:claim-ticket",failureWorkerIds:"openticket:back-to-ticket-message"}, + "openticket:claim-ticket-unclaim-message":{successWorkerIds:"openticket:permissions"|"openticket:claim-ticket",failureWorkerIds:"openticket:back-to-unclaim-message"}, + "openticket:unclaim-ticket-ticket-message":{successWorkerIds:"openticket:permissions"|"openticket:unclaim-ticket",failureWorkerIds:"openticket:back-to-ticket-message"}, + "openticket:unclaim-ticket-claim-message":{successWorkerIds:"openticket:permissions"|"openticket:unclaim-ticket",failureWorkerIds:"openticket:back-to-claim-message"}, + "openticket:pin-ticket-ticket-message":{successWorkerIds:"openticket:permissions"|"openticket:pin-ticket",failureWorkerIds:"openticket:back-to-ticket-message"}, + "openticket:pin-ticket-unpin-message":{successWorkerIds:"openticket:permissions"|"openticket:pin-ticket",failureWorkerIds:"openticket:back-to-unpin-message"}, + "openticket:unpin-ticket-ticket-message":{successWorkerIds:"openticket:permissions"|"openticket:unpin-ticket",failureWorkerIds:"openticket:back-to-ticket-message"}, + "openticket:unpin-ticket-pin-message":{successWorkerIds:"openticket:permissions"|"openticket:unpin-ticket",failureWorkerIds:"openticket:back-to-pin-message"}, + "openticket:close-ticket-ticket-message":{successWorkerIds:"openticket:permissions"|"openticket:close-ticket",failureWorkerIds:"openticket:back-to-ticket-message"}, + "openticket:close-ticket-reopen-message":{successWorkerIds:"openticket:permissions"|"openticket:close-ticket",failureWorkerIds:"openticket:back-to-reopen-message"}, + "openticket:reopen-ticket-ticket-message":{successWorkerIds:"openticket:permissions"|"openticket:reopen-ticket",failureWorkerIds:"openticket:back-to-ticket-message"}, + "openticket:reopen-ticket-close-message":{successWorkerIds:"openticket:permissions"|"openticket:reopen-ticket",failureWorkerIds:"openticket:back-to-close-message"}, + "openticket:reopen-ticket-autoclose-message":{successWorkerIds:"openticket:permissions"|"openticket:reopen-ticket",failureWorkerIds:"openticket:back-to-autoclose-message"}, + "openticket:delete-ticket-ticket-message":{successWorkerIds:"openticket:permissions"|"openticket:delete-ticket",failureWorkerIds:"openticket:back-to-ticket-message"} + "openticket:delete-ticket-close-message":{successWorkerIds:"openticket:permissions"|"openticket:delete-ticket",failureWorkerIds:"openticket:back-to-close-message"} + "openticket:delete-ticket-reopen-message":{successWorkerIds:"openticket:permissions"|"openticket:delete-ticket",failureWorkerIds:"openticket:back-to-reopen-message"} + "openticket:delete-ticket-autoclose-message":{successWorkerIds:"openticket:permissions"|"openticket:delete-ticket",failureWorkerIds:"openticket:back-to-autoclose-message"} +} + +/**## ODVerifyBarManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODVerifyBarManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.verifybars`! + */ +export class ODVerifyBarManager_Default extends ODVerifyBarManager { + get(id:VerifyBarId): ODVerifyBar_Default + get(id:ODValidId): ODVerifyBar|null + + get(id:ODValidId): ODVerifyBar|null { + return super.get(id) + } + + remove(id:VerifyBarId): ODVerifyBar_Default + remove(id:ODValidId): ODVerifyBar|null + + remove(id:ODValidId): ODVerifyBar|null { + return super.remove(id) + } + + exists(id:keyof ODVerifyBarManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODVerifyBar_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODVerifyBar class. + * It doesn't add any extra features! + * + * This default class is made for the default `ODVerifyBar`'s! + */ +export class ODVerifyBar_Default extends ODVerifyBar { + declare success: ODWorkerManager_Default|null},SuccessWorkerIds> + declare failure: ODWorkerManager_Default|null},FailureWorkerIds> +} \ No newline at end of file diff --git a/src/core/api/defaults/worker.ts b/src/core/api/defaults/worker.ts new file mode 100644 index 0000000..3642aee --- /dev/null +++ b/src/core/api/defaults/worker.ts @@ -0,0 +1,35 @@ +/////////////////////////////////////// +//DEFAULT WORKER MODULE +/////////////////////////////////////// +import { ODValidId } from "../modules/base" +import { ODWorker, ODWorkerManager } from "../modules/worker" + + +/**## ODWorkerManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODWorkerManager class. + * It doesn't add any extra features! + * + * This default class is made for the worker manager in actions, builders & responders! + */ +export class ODWorkerManager_Default extends ODWorkerManager { + get(id:WorkerIds): ODWorker + get(id:ODValidId): ODWorker|null + + get(id:ODValidId): ODWorker|null { + return super.get(id) + } + + remove(id:WorkerIds): ODWorker + remove(id:ODValidId): ODWorker|null + + remove(id:ODValidId): ODWorker|null { + return super.remove(id) + } + + exists(id:WorkerIds): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} \ No newline at end of file diff --git a/src/core/api/main.ts b/src/core/api/main.ts new file mode 100644 index 0000000..84a3873 --- /dev/null +++ b/src/core/api/main.ts @@ -0,0 +1,180 @@ +//BASE MODULES +import { ODEnvHelper, ODVersion } from "./modules/base" +import { ODPluginManager } from "./modules/plugin" +import { ODConsoleManager, ODConsoleMessage, ODConsoleMessageParam, ODConsoleMessageTypes, ODDebugFileManager, ODDebugger, ODError } from "./modules/console" +import { ODCheckerStorage } from "./modules/checker" +import { ODDefaultsManager } from "./modules/defaults" + +//DEFAULT MODULES +import { ODVersionManager_Default } from "./defaults/base" +import { ODEventManager_Default } from "./defaults/event" +import { ODConfigManager_Default} from "./defaults/config" +import { ODDatabaseManager_Default } from "./defaults/database" +import { ODFlagManager_Default } from "./defaults/flag" +import { ODSessionManager_Default } from "./defaults/session" +import { ODLanguageManager_Default } from "./defaults/language" +import { ODCheckerFunctionManager_Default, ODCheckerManager_Default, ODCheckerRenderer_Default, ODCheckerTranslationRegister_Default } from "./defaults/checker" +import { ODClientManager_Default } from "./defaults/client" +import { ODBuilderManager_Default } from "./defaults/builder" +import { ODResponderManager_Default } from "./defaults/responder" +import { ODActionManager_Default } from "./defaults/action" +import { ODPermissionManager_Default } from "./defaults/permission" +import { ODHelpMenuManager_Default } from "./defaults/helpmenu" +import { ODStatsManager_Default } from "./defaults/stat" +import { ODCodeManager_Default } from "./defaults/code" +import { ODCooldownManager_Default } from "./defaults/cooldown" +import { ODPostManager_Default } from "./defaults/post" +import { ODVerifyBarManager_Default } from "./defaults/verifybar" +import { ODStartScreenManager_Default } from "./defaults/startscreen" +import { ODLiveStatusManager_Default } from "./defaults/console" + +//OPEN TICKET MODULES +import { ODOptionManager } from "./openticket/option" +import { ODPanelManager } from "./openticket/panel" +import { ODTicketManager } from "./openticket/ticket" +import { ODQuestionManager } from "./openticket/question" +import { ODBlacklistManager } from "./openticket/blacklist" +import { ODTranscriptManager_Default } from "./openticket/transcript" +import { ODRoleManager } from "./openticket/role" + +/**## ODMain `class` + * This is the main Open Ticket class. + * It contains all managers from the entire bot & has shortcuts to the event & logging system. + * + * This class can't be overwritten or extended & is available as the global variable `openticket`! + */ +export class ODMain { + /**The manager that handles all versions in the bot. */ + versions: ODVersionManager_Default + + /**The manager responsible for the debug file. (`otdebug.txt`) */ + debugfile: ODDebugFileManager + /**The manager responsible for the console system. (logs, errors, etc) */ + console: ODConsoleManager + /**The manager responsible for sending debug logs to the debug file. (`otdebug.txt`) */ + debug: ODDebugger + /**The manager containing all Open Ticket events. */ + events: ODEventManager_Default + + /**The manager that handles & executes all plugins in the bot. */ + plugins: ODPluginManager + /**The manager that manages & checks all the console flags of the bot. (like `--debug`) */ + flags: ODFlagManager_Default + /**The manager that manages & contains all the config files of the bot. (like `config/general.json`) */ + configs: ODConfigManager_Default + /**The manager that manages & contains all the databases of the bot. (like `database/global.json`) */ + databases: ODDatabaseManager_Default + /**The manager that manages all the data sessions of the bot. (it's a temporary database) */ + sessions: ODSessionManager_Default + /**The manager that manages all languages & translations of the bot. (but not for plugins) */ + languages: ODLanguageManager_Default + + /**The manager that handles & executes all config checkers in the bot. (the code that checks if you have something wrong in your config) */ + checkers: ODCheckerManager_Default + /**The manager that manages all builders in the bot. (e.g. buttons, dropdowns, messages, modals, etc) */ + builders: ODBuilderManager_Default + /**The manager that manages all responders in the bot. (e.g. commands, buttons, dropdowns, modals) */ + responders: ODResponderManager_Default + /**The manager that manages all actions or procedures in the bot. (e.g. ticket-creation, ticket-deletion, ticket-claiming, etc) */ + actions: ODActionManager_Default + /**The manager that manages all verify bars in the bot. (the ✅ ❌ buttons) */ + verifybars: ODVerifyBarManager_Default + /**The manager that contains all permissions for commands & actions in the bot. (use it to check if someone has admin perms or not) */ + permissions: ODPermissionManager_Default + /**The manager that contains all cooldowns of the bot. (e.g. ticket-cooldowns) */ + cooldowns: ODCooldownManager_Default + /**The manager that manages & renders the Open Ticket help menu. (not the embed, but the text) */ + helpmenu: ODHelpMenuManager_Default + /**The manager that manages, saves & renders the Open Ticket statistics. (not the embed, but the text & database) */ + stats: ODStatsManager_Default + /**This manager is a place where you can put code that executes when the bot almost finishes the setup. (can be used for less important stuff that doesn't require an exact time-order) */ + code: ODCodeManager_Default + /**The manager that manages all posts (static discord channels) in the bot. (e.g. (transcript) logs, etc) */ + posts: ODPostManager_Default + + /**The manager responsible for everything related to the client. (e.g. status, login, slash & text commands, etc) */ + client: ODClientManager_Default + /**This manager contains A LOD of booleans. With these switches, you can turn off "default behaviours" from the bot. This is used if you want to replace the default Open Ticket code. */ + defaults: ODDefaultsManager + /**This manager manages all the variables in the ENV. It reads from both the `.env` file & the `process.env`. (these 2 will be combined) */ + env: ODEnvHelper + + /**The manager responsible for the livestatus system. (remote console logs) */ + livestatus: ODLiveStatusManager_Default + /**The manager responsible for the livestatus system. (remote console logs) */ + startscreen: ODStartScreenManager_Default + + //OPEN TICKET + /**The manager that manages all the data of questions in the bot. (these are used in options & tickets) */ + questions: ODQuestionManager + /**The manager that manages all the data of options in the bot. (these are used for panels, ticket creation, reaction roles) */ + options: ODOptionManager + /**The manager that manages all the data of panels in the bot. (panels contain the options) */ + panels: ODPanelManager + /**The manager that manages all tickets in the bot. (here, you can get & edit a lot of data from tickets) */ + tickets: ODTicketManager + /**The manager that manages the ticket blacklist. (people who are blacklisted can't create a ticket) */ + blacklist: ODBlacklistManager + /**The manager that manages the ticket transcripts. (both the history & compilers) */ + transcripts: ODTranscriptManager_Default + /**The manager that manages all reaction roles in the bot. (here, you can add additional data to roles) */ + roles: ODRoleManager + + constructor(){ + this.versions = new ODVersionManager_Default() + this.versions.add(ODVersion.fromString("openticket:version","v4.0.0")) + this.versions.add(ODVersion.fromString("openticket:api","v1.0.0")) + this.versions.add(ODVersion.fromString("openticket:transcripts","v2.0.0")) + this.versions.add(ODVersion.fromString("openticket:livestatus","v2.0.0")) + + this.debugfile = new ODDebugFileManager("./","otdebug.txt",5000,this.versions.get("openticket:version")) + this.console = new ODConsoleManager(100,this.debugfile) + this.debug = new ODDebugger(this.console) + this.events = new ODEventManager_Default(this.debug) + + this.plugins = new ODPluginManager(this.debug) + this.flags = new ODFlagManager_Default(this.debug) + this.configs = new ODConfigManager_Default(this.debug) + this.databases = new ODDatabaseManager_Default(this.debug) + this.sessions = new ODSessionManager_Default(this.debug) + this.languages = new ODLanguageManager_Default(this.debug,false) + + this.checkers = new ODCheckerManager_Default(this.debug,new ODCheckerStorage(),new ODCheckerRenderer_Default(),new ODCheckerTranslationRegister_Default(),new ODCheckerFunctionManager_Default(this.debug)) + this.builders = new ODBuilderManager_Default(this.debug) + this.client = new ODClientManager_Default(this.debug) + this.responders = new ODResponderManager_Default(this.debug,this.client) + this.actions = new ODActionManager_Default(this.debug) + this.verifybars = new ODVerifyBarManager_Default(this.debug) + this.permissions = new ODPermissionManager_Default(this.debug) + this.cooldowns = new ODCooldownManager_Default(this.debug) + this.helpmenu = new ODHelpMenuManager_Default(this.debug) + this.stats = new ODStatsManager_Default(this.debug) + this.code = new ODCodeManager_Default(this.debug) + this.posts = new ODPostManager_Default(this.debug) + + this.defaults = new ODDefaultsManager() + this.env = new ODEnvHelper() + + this.livestatus = new ODLiveStatusManager_Default(this.debug,this) + this.startscreen = new ODStartScreenManager_Default(this.debug,this.livestatus) + + //OPEN TICKET + this.questions = new ODQuestionManager(this.debug) + this.options = new ODOptionManager(this.debug) + this.panels = new ODPanelManager(this.debug) + this.tickets = new ODTicketManager(this.debug,this.client) + this.blacklist = new ODBlacklistManager(this.debug) + this.transcripts = new ODTranscriptManager_Default(this.debug,this.tickets) + this.roles = new ODRoleManager(this.debug) + } + + /**Log a message to the console. But in the Open Ticket style :) */ + log(message:ODConsoleMessage): void + log(message:ODError): void + log(message:string, type?:ODConsoleMessageTypes, params?:ODConsoleMessageParam[]): void + log(message:ODConsoleMessage|ODError|string, type?:ODConsoleMessageTypes, params?:ODConsoleMessageParam[]){ + if (message instanceof ODConsoleMessage) this.console.log(message) + else if (message instanceof ODError) this.console.log(message) + else if (["string","number","boolean","object"].includes(typeof message)) this.console.log(message,type,params) + } +} \ No newline at end of file diff --git a/src/core/api/modules/action.ts b/src/core/api/modules/action.ts new file mode 100644 index 0000000..dd9a46f --- /dev/null +++ b/src/core/api/modules/action.ts @@ -0,0 +1,58 @@ +/////////////////////////////////////// +//ACTION MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODValidId, ODSystemError, ODManagerData } from "./base" +import { ODWorkerManager, ODWorkerCallback, ODWorker } from "./worker" +import { ODDebugger } from "./console" + +/**## ODActionImplementation `class` + * This is an open ticket action implementation. + * + * It is a basic implementation of the `ODWorkerManager` used by all `ODAction` classes. + * + * This class can't be used stand-alone & needs to be extended from! + */ +export class ODActionImplementation extends ODManagerData { + /**The manager that has all workers of this implementation */ + workers: ODWorkerManager + + constructor(id:ODValidId, callback?:ODWorkerCallback, priority?:number, callbackId?:ODValidId){ + super(id) + this.workers = new ODWorkerManager("descending") + if (callback) this.workers.add(new ODWorker(callbackId ? callbackId : id,priority ?? 0,callback)) + } + /**Execute all workers & return the result. */ + async run(source:Source, params:Params): Promise> { + throw new ODSystemError("Tried to build an unimplemented ODResponderImplementation") + } +} + +/**## ODActionManager `class` + * This is an open ticket action manager. + * + * It contains all Open Ticket actions. You can compare actions with some sort of "procedure". + * It's a complicated task that is divided into multiple functions. + * + * Some examples are `ticket-creation`, `ticket-closing`, `ticket-claiming`, ... + * + * It's recommended to use this system in combination with Open Ticket responders! + */ +export class ODActionManager extends ODManager> { + constructor(debug:ODDebugger){ + super(debug,"action") + } +} + +export class ODAction extends ODActionImplementation { + /**Run this action */ + async run(source:Source, params:Params): Promise> { + //create instance + const instance = {} + + //wait for workers to finish + await this.workers.executeWorkers(instance,source,params) + + //return data generated by workers + return instance + } +} \ No newline at end of file diff --git a/src/core/api/modules/base.ts b/src/core/api/modules/base.ts new file mode 100644 index 0000000..20eda1f --- /dev/null +++ b/src/core/api/modules/base.ts @@ -0,0 +1,749 @@ +/////////////////////////////////////// +//BASE MODULE +/////////////////////////////////////// +import * as fs from "fs" +import { ODConsoleWarningMessage, ODDebugger } from "./console" + +/**## ODPromiseVoid `type` + * This is a simple type to represent a callback return value that could be a promise or not. + */ +export type ODPromiseVoid = void|Promise + +/**## ODValidButtonColor `type` + * This is a collection of all the possible button colors. + */ +export type ODValidButtonColor = "gray"|"red"|"green"|"blue" + +/**## ODValidId `type` + * This is a valid open ticket identifier. It can be an `ODId` or `string`! + * + * You will see this type in many functions from open ticket. + */ +export type ODValidId = string|ODId + +/**## ODValidJsonType `type` + * This is a collection of all types that can be stored in a JSON file! + * + * list: `string`, `number`, `boolean`, `array`, `object`, `null` + */ +export type ODValidJsonType = string|number|boolean|object|ODValidJsonType[]|null + + +/**## ODInterfaceWithPartialProperty `type` + * This is a utility type to create an interface where some properties are optional! + */ +export type ODInterfaceWithPartialProperty = Omit & Partial> + +/**## ODId `class` + * This is an open ticket identifier. + * + * It can only contain the following characters: `a-z`, `A-Z`, `0-9`, `:`, `-` & `_` + * + * You will use this class to assign a unique id when creating configs, databases, languages & more! + * @example + * const id = new api.ODId("openticket:test-id") //this is valid + * const id = new api.ODId("example%id?") //this is invalid + */ +export class ODId { + /**The full value of this `ODId` as a `string` */ + value: string + /**The source of the id (part before `:`). For example `openticket` for all built-in ids! */ + source: string + /**The identifier of the id (part after `:`). */ + identifier: string + + constructor(id:ODValidId){ + if (typeof id != "string" && !(id instanceof ODId)) throw new ODSystemError("Invalid constructor parameter => id:ODValidId") + + if (typeof id == "string"){ + //id is string + const result: string[] = [] + const charregex = /[a-zA-Z0-9éèçàêâôûî\:\-\_]/ + + id.split("").forEach((char) => { + if (charregex.test(char)){ + result.push(char) + } + }) + + if (result.length > 0) this.value = result.join("") + else throw new ODSystemError("invalid ID at 'new ODID(id: "+id+")'") + + const splitted = this.value.split(":") + if (splitted.length > 1){ + this.source = splitted[0] + splitted.shift() + this.identifier = splitted.join(":") + }else{ + this.identifier = splitted.join(":") + this.source = "" + } + }else{ + //id is ODId + this.value = id.value + this.source = id.source + this.identifier = id.identifier + } + } + + /**Returns a string representation of this id. (same as `this.value`) */ + toString(){ + return this.value + } +} + +/**## ODManagerChangeHelper `class` + * This is an open ticket manager change helper. + * + * It is used to let the "onChange" event in the `ODManager` class work. + * You will probably use this class when extending your own `ODManager` + */ +export class ODManagerChangeHelper { + #change: (() => void)|null = null + + protected _change(){ + if (this.#change){ + try{ + this.#change() + }catch(err){ + process.emit("uncaughtException",err) + throw new ODSystemError("Failed to execute _change() callback!") + } + } + } + /****(❌ SYSTEM ONLY!!)** Set the callback executed when a value inside this class changes */ + changed(callback:(() => void)|null){ + this.#change = callback + } +} + +/**## ODManagerRedirectHelper `class` + * This is open ticket ticket manager redirect helper. + * + * It is used to redirect a source to another source when the id isn't found. + * + * It will be used in **Open Discord** to allow plugins from all projects to work seamlessly! + * ## **(❌ SYSTEM ONLY!!)** + */ +export class ODManagerRedirectHelper { + #data: {fromSource:string,toSource:string}[] = [] + + /****(❌ SYSTEM ONLY!!)** Add a redirect to this manager. Returns `true` when overwritten. */ + add(fromSource:string, toSource:string){ + const index = this.#data.findIndex((data) => data.fromSource === fromSource) + if (index > -1){ + //already exists + this.#data[index] = {fromSource,toSource} + return true + }else{ + //doesn't exist + this.#data.push({fromSource,toSource}) + return false + } + } + /****(❌ SYSTEM ONLY!!)** Remove a redirect from this manager. Returns `true` when it existed. */ + remove(fromSource:string, toSource:string){ + const index = this.#data.findIndex((data) => data.fromSource === fromSource && data.toSource == toSource) + if (index > -1){ + //already exists + this.#data.splice(index,1) + return true + }else return false + } + /**List all redirects from this manager. */ + list(){ + return [...this.#data] + } +} + +/**## ODManagerData `class` + * This is open ticket manager data. + * + * It provides a template for all classes that are used in the `ODManager`. + * + * There is an `id:ODId` property & also some events used in the manager. + */ +export class ODManagerData extends ODManagerChangeHelper { + /**The id of this data */ + id: ODId + + constructor(id:ODValidId){ + if (typeof id != "string" && !(id instanceof ODId)) throw new ODSystemError("Invalid constructor parameter => id:ODValidId") + super() + this.id = new ODId(id) + } +} + +/**## ODManagerCallback `type` + * This is a callback for the `onChange` and `onRemove` events in the `ODManager` + */ +export type ODManagerCallback = (data:DataType) => void +/**## ODManagerAddCallback `type` + * This is a callback for the `onAdd` event in the `ODManager` + */ +export type ODManagerAddCallback = (data:DataType, overwritten:boolean) => void + +/**## ODManager `class` + * This is an open ticket manager. + * + * It can be used to store & manage different aspects of the bot! + * You will probably to extend this class when creating your own classes & managers. + * + * This class has many useful functions based on `ODId` (add, get, remove, getAll, getFiltered, exists) + */ +export class ODManager extends ODManagerChangeHelper { + /**Alias to open ticket debugger. */ + #debug?: ODDebugger + /**The message to send when debugging this manager. */ + #debugname?: string + /**An array storing all data classes ❌ **(don't edit unless really needed!)***/ + #data: DataType[] = [] + /**An array storing all listeners when data is added. */ + #addListeners: ODManagerAddCallback[] = [] + /**An array storing all listeners when data has changed. */ + #changeListeners: ODManagerCallback[] = [] + /**An array storing all listeners when data is removed. */ + #removeListeners: ODManagerCallback[] = [] + /**Handle all redirects in this `ODManager` */ + redirects: ODManagerRedirectHelper = new ODManagerRedirectHelper() + + constructor(debug?:ODDebugger, debugname?:string){ + super() + this.#debug = debug + this.#debugname = debugname + } + + /**Add data to the manager. The id will be fetched from the data class! You can optionally select to overwrite existing data!*/ + add(data:DataType|DataType[], overwrite?:boolean): boolean { + //repeat same command when data is an array + if (Array.isArray(data)){ + data.forEach((arrayData) => { + this.add(arrayData,overwrite) + }) + return false + } + + //add data + const existIndex = this.#data.findIndex((d) => d.id.value === data.id.value) + if (existIndex < 0){ + this.#data.push(data) + if (this.#debug) this.#debug.debug("Added new "+this.#debugname+" to manager",[{key:"id",value:data.id.value},{key:"overwrite",value:"false"}]) + + //change listeners + data.changed(() => { + //notify change in upper-manager (because data in this manager changed) + this._change() + this.#changeListeners.forEach((cb) => { + try{ + cb(data) + }catch(err){ + throw new ODSystemError("Failed to run manager onChange() listener.\n"+err) + } + }) + }) + + //add listeners + this.#addListeners.forEach((cb) => { + try{ + cb(data,false) + }catch(err){ + throw new ODSystemError("Failed to run manager onAdd() listener.\n"+err) + } + }) + + //notify change in upper-manager (because data added) + this._change() + + return false + }else{ + if (!overwrite) throw new ODSystemError("Id '"+data.id.value+"' already exists in "+this.#debugname+" manager. Use 'overwrite:true' to allow overwriting!") + this.#data[existIndex] = data + if (this.#debug) this.#debug.debug("Added new "+this.#debugname+" to manager",[{key:"id",value:data.id.value},{key:"overwrite",value:"true"}]) + + //change listeners + data.changed(() => { + //notify change in upper-manager (because data in this manager changed) + this._change() + this.#changeListeners.forEach((cb) => { + try{ + cb(data) + }catch(err){ + throw new ODSystemError("Failed to run manager onChange() listener.\n"+err) + } + }) + }) + + //add listeners + this.#addListeners.forEach((cb) => { + try{ + cb(data,true) + }catch(err){ + throw new ODSystemError("Failed to run manager onAdd() listener.\n"+err) + } + }) + + //notify change in upper-manager (because data added) + this._change() + + return true + } + } + /**Get data that matches the `ODId`. Returns the found data.*/ + get(id:ODValidId): DataType|null { + const newId = new ODId(id) + const d = this.#data.find((a) => a.id.value == newId.value) + if (d) return d + else{ + const redirect = this.redirects.list().find((redirect) => redirect.fromSource === newId.source) + if (!redirect) return null + else{ + const redirectId = new ODId(redirect.toSource+":"+newId.identifier) + return this.get(redirectId) + } + } + } + /**Remove data that matches the `ODId`. Returns the removed data.*/ + remove(id:ODValidId): DataType|null { + const newId = new ODId(id) + const index = this.#data.findIndex((a) => a.id.value == newId.value) + if (index < 0){ + const redirect = this.redirects.list().find((redirect) => redirect.fromSource === newId.source) + if (!redirect){ + if (this.#debug) this.#debug.debug("Removed "+this.#debugname+" from manager",[{key:"id",value:newId.value},{key:"found",value:"false"}]) + return null + }else{ + const redirectId = new ODId(redirect.toSource+":"+newId.identifier) + return this.remove(redirectId) + } + } + if (this.#debug) this.#debug.debug("Removed "+this.#debugname+" from manager",[{key:"id",value:newId.value},{key:"found",value:"true"}]) + const data = this.#data.splice(index,1)[0] + + //change listeners + data.changed(null) + + //remove listeners + this.#removeListeners.forEach((cb) => { + try{ + cb(data) + }catch(err){ + throw new ODSystemError("Failed to run manager onRemove() listener.\n"+err) + } + }) + + //notify change in upper-manager (because data removed) + this._change() + + return data + } + /**Check if data that matches the `ODId` exists. Returns a boolean.*/ + exists(id:ODValidId): boolean { + const newId = new ODId(id) + const d = this.#data.find((a) => a.id.value == newId.value) + if (d) return true + else{ + const redirect = this.redirects.list().find((redirect) => redirect.fromSource === newId.source) + if (!redirect) return false + else{ + const redirectId = new ODId(redirect.toSource+":"+newId.identifier) + return this.exists(redirectId) + } + } + } + /**Get all data inside this manager*/ + getAll(): DataType[] { + return [...this.#data] + } + /**Get all data that matches inside the filter function*/ + getFiltered(predicate:(value:DataType, index:number, array:DataType[]) => unknown): DataType[] { + return this.#data.filter(predicate) + } + /**Get all data that matches the regex*/ + getRegex(regex:RegExp): DataType[] { + return this.#data.filter((data) => regex.test(data.id.value)) + } + /**Get the length of the data inside this manager*/ + getLength(){ + return this.#data.length + } + /**Get a list of all the ids inside this manager*/ + getIds(): ODId[] { + return this.#data.map((d) => d.id) + } + /**Use the open ticket debugger in this manager for logs*/ + useDebug(debug?:ODDebugger, debugname?:string){ + this.#debug = debug + this.#debugname = debugname + } + /**Listen for when data is added to this manager. */ + onAdd(callback:ODManagerAddCallback){ + this.#addListeners.push(callback) + } + /**Listen for when data is changed in this manager. */ + onChange(callback:ODManagerCallback){ + this.#changeListeners.push(callback) + } + /**Listen for when data is removed from this manager. */ + onRemove(callback:ODManagerCallback){ + this.#removeListeners.push(callback) + } +} + +/**## ODManagerWithSafety `class` + * This is an open ticket safe manager. + * + * It functions exactly the same as a normal `ODManager`, but it has 1 function extra! + * The `getSafe()` function will always return data, because when it doesn't find an id, it returns pre-configured backup data. + */ +export class ODManagerWithSafety extends ODManager { + /**The function that creates backup data returned in `getSafe()` when an id is missing in this manager. */ + #backupCreator: () => DataType + /** Temporary storage for manager debug name. */ + #debugname: string + + constructor(backupCreator:() => DataType, debug?:ODDebugger, debugname?:string){ + super(debug,debugname) + this.#backupCreator = backupCreator + this.#debugname = debugname ?? "unknown" + } + + /**Get data that matches the `ODId`. Returns the backup data when not found. + * + * ### ⚠️ This should only be used when the data doesn't need to be written/edited + */ + getSafe(id:ODValidId): DataType { + const data = super.get(id) + if (!data){ + process.emit("uncaughtException",new ODSystemError("ODManagerWithSafety:getSafe(\""+id+"\") => Unknown Id => Used backup data ("+this.#debugname+" manager)")) + return this.#backupCreator() + } + else return data + } +} + +/**## ODVersionManager `class` + * A open ticket version manager. + * + * It is used to manage different `ODVersion`'s from the bot. You will use it to check which version of the bot is used. + */ +export class ODVersionManager extends ODManager { + constructor(){ + super() + } +} + +/**## ODVersion `class` + * This is an open ticket version. + * + * It has many features like comparing versions & checking if they are compatible. + * + * You can use it in your own plugin, but most of the time you will use it to check the open ticket version! + */ +export class ODVersion extends ODManagerData { + /**The first number of the version (example: `v1.2.3` => `1`) */ + primary: number + /**The second number of the version (example: `v1.2.3` => `2`) */ + secondary: number + /**The third number of the version (example: `v1.2.3` => `3`) */ + tertiary: number + + constructor(id:ODValidId, primary:number, secondary:number, tertiary:number){ + super(id) + if (typeof primary != "number") throw new ODSystemError("Invalid constructor parameter => primary:number") + if (typeof secondary != "number") throw new ODSystemError("Invalid constructor parameter => secondary:number") + if (typeof tertiary != "number") throw new ODSystemError("Invalid constructor parameter => tertiary:number") + + this.primary = primary + this.secondary = secondary + this.tertiary = tertiary + } + + /**Get the version from a string (also possible with `v` prefix) + * @example const version = api.ODVersion.fromString("id","v1.2.3") //creates version 1.2.3 + */ + static fromString(id:ODValidId, version:string){ + if (typeof id != "string" && !(id instanceof ODId)) throw new ODSystemError("Invalid function parameter => id:ODValidId") + if (typeof version != "string") throw new ODSystemError("Invalid function parameter => version:string") + + const versionCheck = (version.startsWith("v")) ? version.substring(1) : version + const splittedVersion = versionCheck.split(".") + + return new this(id,Number(splittedVersion[0]),Number(splittedVersion[1]),Number(splittedVersion[2])) + } + /**Get the version as a string (`noprefix:true` => with `v` prefix) + * @example + * new api.ODVersion(1,0,0).toString(false) //returns "v1.0.0" + * new api.ODVersion(1,0,0).toString(true) //returns "1.0.0" + */ + toString(noprefix?:boolean){ + const prefix = noprefix ? "" : "v" + return prefix+[this.primary,this.secondary,this.tertiary].join(".") + } + /**Compare this version with another version and returns the result: `higher`, `lower` or `equal` + * @example + * new api.ODVersion(1,0,0).compare(new api.ODVersion(1,2,0)) //returns "lower" + * new api.ODVersion(1,3,0).compare(new api.ODVersion(1,2,0)) //returns "higher" + * new api.ODVersion(1,2,0).compare(new api.ODVersion(1,2,0)) //returns "equal" + */ + compare(comparator:ODVersion): "higher"|"lower"|"equal" { + if (!(comparator instanceof ODVersion)) throw new ODSystemError("Invalid function parameter => comparator:ODVersion") + + if (this.primary < comparator.primary) return "lower" + else if (this.primary > comparator.primary) return "higher" + else { + if (this.secondary < comparator.secondary) return "lower" + else if (this.secondary > comparator.secondary) return "higher" + else { + if (this.tertiary < comparator.tertiary) return "lower" + else if (this.tertiary > comparator.tertiary) return "higher" + else return "equal" + } + } + } + /**Check if this version is included in the list + * @example + * const list = [ + * new api.ODVersion(1,0,0), + * new api.ODVersion(1,0,1), + * new api.ODVersion(1,0,2) + * ] + * new api.ODVersion(1,0,0).compatible(list) //returns true + * new api.ODVersion(1,0,1).compatible(list) //returns true + * new api.ODVersion(1,0,3).compatible(list) //returns false + */ + compatible(list:ODVersion[]): boolean { + if (!Array.isArray(list)) throw new ODSystemError("Invalid function parameter => list:ODVersion[]") + if (!list.every((v) => (v instanceof ODVersion))) throw new ODSystemError("Invalid function parameter => list:ODVersion[]") + + return list.some((v) => { + return (v.toString() === this.toString()) + }) + } +} + +/**## ODHTTPGetRequest `class` + * This is a class that can help you with creating simple HTTP GET requests. + * + * It works using the native node.js fetch() method. You can configure all options in the constructor! + * @example + * const request = new api.ODHTTPGetRequest("https://www.example.com/abc.txt",false,{}) + * + * const result = await request.run() + * result.body //the response body (string) + * result.status //the response code (number) + * result.response //the full response (object) + */ +export class ODHTTPGetRequest { + /**The url used in the request */ + url: string + /**The request config for additional options */ + config: RequestInit + /**Throw on error OR return http code 500 */ + throwOnError: boolean + + constructor(url:string,throwOnError:boolean,config?:RequestInit){ + if (typeof url != "string") throw new ODSystemError("Invalid constructor parameter => url:string") + if (typeof throwOnError != "boolean") throw new ODSystemError("Invalid constructor parameter => throwOnError:boolean") + if (typeof config != "undefined" && typeof config != "object") throw new ODSystemError("Invalid constructor parameter => config?:RequestInit") + + this.url = url + this.throwOnError = throwOnError + const newConfig = config ?? {} + newConfig.method = "GET" + this.config = newConfig + } + + /**Execute the GET request.*/ + run(): Promise<{status:number, body:string, response?:Response}> { + return new Promise(async (resolve,reject) => { + try{ + const response = await fetch(this.url,this.config) + resolve({ + status:response.status, + body:(await response.text()), + response:response + }) + }catch(err){ + if (this.throwOnError) return reject("[OPENTICKET ERROR]: ODHTTPGetRequest => Unknown fetch() error: "+err) + else return resolve({ + status:500, + body:"Open Ticket Error: Unknown fetch() error: "+err, + }) + } + }) + } +} + +/**## ODHTTPPostRequest `class` + * This is a class that can help you with creating simple HTTP POST requests. + * + * It works using the native node.js fetch() method. You can configure all options in the constructor! + * @example + * const request = new api.ODHTTPPostRequest("https://www.example.com/abc.txt",false,{}) + * + * const result = await request.run() + * result.body //the response body (string) + * result.status //the response code (number) + * result.response //the full response (object) + */ +export class ODHTTPPostRequest { + /**The url used in the request */ + url: string + /**The request config for additional options */ + config: RequestInit + /**Throw on error OR return http code 500 */ + throwOnError: boolean + + constructor(url:string,throwOnError:boolean,config?:RequestInit){ + if (typeof url != "string") throw new ODSystemError("Invalid constructor parameter => url:string") + if (typeof throwOnError != "boolean") throw new ODSystemError("Invalid constructor parameter => throwOnError:boolean") + if (typeof config != "undefined" && typeof config != "object") throw new ODSystemError("Invalid constructor parameter => config?:RequestInit") + + this.url = url + this.throwOnError = throwOnError + const newConfig = config ?? {} + newConfig.method = "POST" + this.config = newConfig + } + + /**Execute the POST request.*/ + run(): Promise<{status:number, body:string, response?:Response}> { + return new Promise(async (resolve,reject) => { + try{ + const response = await fetch(this.url,this.config) + resolve({ + status:response.status, + body:(await response.text()), + response:response + }) + }catch(err){ + if (this.throwOnError) return reject("[OPENTICKET ERROR]: ODHTTPPostRequest => Unknown fetch() error: "+err) + else return resolve({ + status:500, + body:"Open Ticket Error: Unknown fetch() error!", + }) + } + }) + } +} + +/**## ODEnvHelper `class` + * This is a utility class that helps you with reading the ENV. + * + * It has support for the built-in `process.env` & `.env` file + * @example + * const envHelper = new api.ODEnvHelper() + * + * const variableA = envHelper.getVariable("value-a") + * const variableB = envHelper.getVariable("value-b","dotenv") //only get from .env + * const variableA = envHelper.getVariable("value-c","env") //only get from process.env + */ +export class ODEnvHelper { + /**All variables found in the `.env` file */ + dotenv: object + /**All variables found in `process.env` */ + env: object + + constructor(customEnvPath?:string){ + if (typeof customEnvPath != "undefined" && typeof customEnvPath != "string") throw new ODSystemError("Invalid constructor parameter => customEnvPath?:string") + + const path = customEnvPath ? customEnvPath : ".env" + this.dotenv = fs.existsSync(path) ? this.#readDotEnv(fs.readFileSync(path)) : {} + this.env = process.env + } + + /**Get a variable from the env */ + getVariable(name:string,source?:"dotenv"|"env"): any|undefined { + if (typeof name != "string") throw new ODSystemError("Invalid function parameter => name:string") + if ((typeof source != "undefined" && typeof source != "string") || (source && !["env","dotenv"].includes(source))) throw new ODSystemError("Invalid function parameter => source:'dotenv'|'env'") + + if (source == "dotenv"){ + return this.dotenv[name] + }else if (source == "env"){ + return this.env[name] + }else{ + //when no source specified => .env has priority over process.env + if (this.dotenv[name]) return this.dotenv[name] + else return this.env[name] + } + } + + //THIS CODE IS COPIED FROM THE DODENV-LIB + //Repo: https://github.com/motdotla/dotenv + //Source: https://github.com/motdotla/dotenv/blob/master/lib/main.js#L12 + #readDotEnv(src:Buffer){ + const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg + const obj = {} + + // Convert buffer to string + let lines = src.toString() + + // Convert line breaks to same format + lines = lines.replace(/\r\n?/mg, '\n') + + let match + while ((match = LINE.exec(lines)) != null) { + const key = match[1] + + // Default undefined or null to empty string + let value = (match[2] || '') + + // Remove whitespace + value = value.trim() + + // Check if double quoted + const maybeQuote = value[0] + + // Remove surrounding quotes + value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2') + + // Expand newlines if double quoted + if (maybeQuote === '"') { + value = value.replace(/\\n/g, '\n') + value = value.replace(/\\r/g, '\r') + } + + // Add to object + obj[key] = value + } + return obj + } +} + +/**## ODSystemError `class` + * A wrapper for the node.js `Error` class that makes the error look better in the console! + * + * This wrapper is made for open ticket system errors! **It can only be used by open ticket itself!** + */ +export class ODSystemError extends Error { + /**This variable gets detected by the error handling system to know how to render it */ + _ODErrorType = "system" + + /**Create an `ODSystemError` directly from an `Error` class */ + static fromError(err:Error){ + err["_ODErrorType"] = "system" + return err as ODSystemError + } +} + +/**## ODPluginError `class` + * A wrapper for the node.js `Error` class that makes the error look better in the console! + * + * This wrapper is made for open ticket plugin errors! **It can only be used by plugins!** + */ +export class ODPluginError extends Error { + /**This variable gets detected by the error handling system to know how to render it */ + _ODErrorType = "plugin" + + /**Create an `ODPluginError` directly from an `Error` class */ + static fromError(err:Error){ + err["_ODErrorType"] = "plugin" + return err as ODPluginError + } +} + +/**Oh, what could this be `¯\_(ツ)_/¯` */ +export interface ODEasterEggs { + creator:string, + translators:string[] +} \ No newline at end of file diff --git a/src/core/api/modules/builder.ts b/src/core/api/modules/builder.ts new file mode 100644 index 0000000..9fb9f4a --- /dev/null +++ b/src/core/api/modules/builder.ts @@ -0,0 +1,1245 @@ +/////////////////////////////////////// +//BUILDER MODULE +/////////////////////////////////////// +import { ODId, ODValidButtonColor, ODValidId, ODSystemError, ODInterfaceWithPartialProperty, ODManagerWithSafety, ODManagerData } from "./base" +import * as discord from "discord.js" +import { ODWorkerManager, ODWorkerCallback, ODWorker } from "./worker" +import { ODDebugger } from "./console" + +/**## ODBuilderImplementation `class` + * This is an open ticket builder implementation. + * + * It is a basic implementation of the `ODWorkerManager` used by all `ODBuilder` classes. + * + * This class can't be used stand-alone & needs to be extended from! + */ +export class ODBuilderImplementation extends ODManagerData { + /**The manager that has all workers of this implementation */ + workers: ODWorkerManager + /**Cache a build or create it every time from scratch when this.build() gets executed. */ + allowCache: boolean = false + /**Did the build already got created/cached? */ + didCache: boolean = false + /**The cache of this build. */ + cache:BuildType|null = null + + constructor(id:ODValidId, callback?:ODWorkerCallback, priority?:number, callbackId?:ODValidId){ + super(id) + this.workers = new ODWorkerManager("ascending") + if (callback) this.workers.add(new ODWorker(callbackId ? callbackId : id,priority ?? 0,callback)) + } + + /**Set if caching is allowed */ + setCacheMode(allowed:boolean){ + this.allowCache = allowed + this.resetCache() + return this + } + /**Reset the current cache */ + resetCache(){ + this.cache = null + this.didCache = false + return this + } + /**Execute all workers & return the result. */ + async build(source:Source, params:Params): Promise { + throw new ODSystemError("Tried to build an unimplemented ODBuilderImplementation") + } +} + +/**## ODBuilderManager `class` + * This is an open ticket builder manager. + * + * It contains all Open Ticket builders. You can find messages, embeds, files & dropdowns, buttons & modals all here! + * + * Using the Open Ticket builder system has a few advantages compared to vanilla discord.js: + * - plugins can extend/edit messages + * - automatically reply on error + * - independent workers (with priority) + * - fail-safe design using try-catch + * - cache frequently used objects + * - get to know the source of the build request for a specific message, button, etc + * - And so much more! + */ +export class ODBuilderManager { + /**The manager for all button builders */ + buttons: ODButtonManager + /**The manager for all dropdown builders */ + dropdowns: ODDropdownManager + /**The manager for all file/attachment builders */ + files: ODFileManager + /**The manager for all embed builders */ + embeds: ODEmbedManager + /**The manager for all message builders */ + messages: ODMessageManager + /**The manager for all modal builders */ + modals: ODModalManager + + constructor(debug:ODDebugger){ + this.buttons = new ODButtonManager(debug) + this.dropdowns = new ODDropdownManager(debug) + this.files = new ODFileManager(debug) + this.embeds = new ODEmbedManager(debug) + this.messages = new ODMessageManager(debug) + this.modals = new ODModalManager(debug) + } +} + +/**## ODComponentBuildResult `interface` + * This interface contains the result from a built component (button/dropdown). This can be used in the `ODMessage` builder! + */ +export interface ODComponentBuildResult { + /**The id of this component (button or dropdown) */ + id:ODId, + /**The discord component or `\n` when it is a spacer between action rows */ + component:discord.MessageActionRowComponentBuilder|"\n"|null +} + +/**## ODButtonManager `class` + * This is an open ticket button manager. + * + * It contains all Open Ticket button builders. Here, you can add your own buttons or edit existing ones! + * + * It's recommended to use this system in combination with all the other Open Ticket builders! + */ +export class ODButtonManager extends ODManagerWithSafety> { + constructor(debug:ODDebugger){ + super(() => { + return new ODButton("openticket:unknown-button",(instance,params,source,cancel) => { + instance.setCustomId("od:unknown-button") + instance.setMode("button") + instance.setColor("red") + instance.setLabel("") + instance.setEmoji("✖") + instance.setDisabled(true) + cancel() + }) + },debug,"button") + } + + /**Get a newline component for buttons & dropdowns! */ + getNewLine(id:ODValidId): ODComponentBuildResult { + return { + id:new ODId(id), + component:"\n" + } + } +} + +/**## ODButtonData `interface` + * This interface contains the data to build a button. + */ +export interface ODButtonData { + /**The custom id of this button */ + customId:string, + /**The mode of this button */ + mode:"button"|"url", + /**The url for when the mode is set to "url" */ + url:string|null, + /**The button color */ + color:ODValidButtonColor|null, + /**The button label */ + label:string|null, + /**The button emoji */ + emoji:string|null, + /**Is the button disabled? */ + disabled:boolean +} + +/**## ODButtonInstance `class` + * This is an open ticket button instance. + * + * It contains all properties & functions to build a button! + */ +export class ODButtonInstance { + /**The current data of this button */ + data: ODButtonData = { + customId:"", + mode:"button", + url:null, + color:null, + label:null, + emoji:null, + disabled:false + } + + /**Set the custom id of this button */ + setCustomId(id:ODButtonData["customId"]){ + this.data.customId = id + return this + } + /**Set the mode of this button */ + setMode(mode:ODButtonData["mode"]){ + this.data.mode = mode + return this + } + /**Set the url of this button */ + setUrl(url:ODButtonData["url"]){ + this.data.url = url + return this + } + /**Set the color of this button */ + setColor(color:ODButtonData["color"]){ + this.data.color = color + return this + } + /**Set the label of this button */ + setLabel(label:ODButtonData["label"]){ + this.data.label = label + return this + } + /**Set the emoji of this button */ + setEmoji(emoji:ODButtonData["emoji"]){ + this.data.emoji = emoji + return this + } + /**Disable this button */ + setDisabled(disabled:ODButtonData["disabled"]){ + this.data.disabled = disabled + return this + } +} + +/**## ODButton `class` + * This is an open ticket button builder. + * + * With this class, you can create a button to use in a message. + * The only difference with normal buttons is that this one can be edited by Open Ticket plugins! + * + * This is possible by using "workers" or multiple functions that will be executed in priority order! + */ +export class ODButton extends ODBuilderImplementation { + /**Build this button & compile it for discord.js */ + async build(source:Source, params:Params){ + if (this.didCache && this.cache && this.allowCache) return this.cache + + try { + //create instance + const instance = new ODButtonInstance() + + //wait for workers to finish + await this.workers.executeWorkers(instance,source,params) + + //create the discord.js button + const button = new discord.ButtonBuilder() + if (instance.data.mode == "button") button.setCustomId(instance.data.customId) + if (instance.data.mode == "url") button.setStyle(discord.ButtonStyle.Link) + else if (instance.data.color == "gray") button.setStyle(discord.ButtonStyle.Secondary) + else if (instance.data.color == "blue") button.setStyle(discord.ButtonStyle.Primary) + else if (instance.data.color == "green") button.setStyle(discord.ButtonStyle.Success) + else if (instance.data.color == "red") button.setStyle(discord.ButtonStyle.Danger) + if (instance.data.url) button.setURL(instance.data.url) + if (instance.data.label) button.setLabel(instance.data.label) + if (instance.data.emoji) button.setEmoji(instance.data.emoji) + if (instance.data.disabled) button.setDisabled(instance.data.disabled) + if (!instance.data.emoji && !instance.data.label) button.setLabel(instance.data.customId) + + this.cache = {id:this.id,component:button} + this.didCache = true + return {id:this.id,component:button} + }catch(err){ + process.emit("uncaughtException",new ODSystemError("ODButton:build(\""+this.id.value+"\") => Major Error (see next error)")) + process.emit("uncaughtException",err) + return {id:this.id,component:null} + } + } +} + +/**## ODDropdownManager `class` + * This is an open ticket dropdown manager. + * + * It contains all Open Ticket dropdown builders. Here, you can add your own dropdowns or edit existing ones! + * + * It's recommended to use this system in combination with all the other Open Ticket builders! + */ +export class ODDropdownManager extends ODManagerWithSafety> { + constructor(debug:ODDebugger){ + super(() => { + return new ODDropdown("openticket:unknown-dropdown",(instance,params,source,cancel) => { + instance.setCustomId("od:unknown-dropdown") + instance.setType("string") + instance.setPlaceholder("❌ ") + instance.setDisabled(true) + instance.setOptions([ + {emoji:"❌",label:"",value:"error"} + ]) + cancel() + }) + },debug,"dropdown") + } + + /**Get a newline component for buttons & dropdowns! */ + getNewLine(id:ODValidId): ODComponentBuildResult { + return { + id:new ODId(id), + component:"\n" + } + } +} + +/**## ODDropdownData `interface` + * This interface contains the data to build a dropdown. + */ +export interface ODDropdownData { + /**The custom id of this dropdown */ + customId:string, + /**The type of this dropdown */ + type:"string"|"role"|"channel"|"user"|"mentionable", + /**The placeholder of this dropdown */ + placeholder:string|null, + /**The minimum amount of items to be selected in this dropdown */ + minValues:number|null, + /**The maximum amount of items to be selected in this dropdown */ + maxValues:number|null, + /**Is this dropdown disabled? */ + disabled:boolean, + /**Allowed channel types when the type is "channel" */ + channelTypes:discord.ChannelType[] + + /**The options when the type is "string" */ + options:discord.SelectMenuComponentOptionData[], + /**The options when the type is "user" */ + users:discord.User[], + /**The options when the type is "role" */ + roles:discord.Role[], + /**The options when the type is "channel" */ + channels:discord.Channel[], + /**The options when the type is "mentionable" */ + mentionables:(discord.User|discord.Role)[], +} + +/**## ODDropdownInstance `class` + * This is an open ticket dropdown instance. + * + * It contains all properties & functions to build a dropdown! + */ +export class ODDropdownInstance { + /**The current data of this dropdown */ + data: ODDropdownData = { + customId:"", + type:"string", + placeholder:null, + minValues:null, + maxValues:null, + disabled:false, + channelTypes:[], + + options:[], + users:[], + roles:[], + channels:[], + mentionables:[] + } + + /**Set the custom id of this dropdown */ + setCustomId(id:ODDropdownData["customId"]){ + this.data.customId = id + return this + } + /**Set the type of this dropdown */ + setType(type:ODDropdownData["type"]){ + this.data.type = type + return this + } + /**Set the placeholder of this dropdown */ + setPlaceholder(placeholder:ODDropdownData["placeholder"]){ + this.data.placeholder = placeholder + return this + } + /**Set the minimum amount of values in this dropdown */ + setMinValues(minValues:ODDropdownData["minValues"]){ + this.data.minValues = minValues + return this + } + /**Set the maximum amount of values ax this dropdown */ + setMaxValues(maxValues:ODDropdownData["maxValues"]){ + this.data.maxValues = maxValues + return this + } + /**Set the disabled of this dropdown */ + setDisabled(disabled:ODDropdownData["disabled"]){ + this.data.disabled = disabled + return this + } + /**Set the channel types of this dropdown */ + setChannelTypes(channelTypes:ODDropdownData["channelTypes"]){ + this.data.channelTypes = channelTypes + return this + } + /**Set the options of this dropdown (when `type == "string"`) */ + setOptions(options:ODDropdownData["options"]){ + this.data.options = options + return this + } + /**Set the users of this dropdown (when `type == "user"`) */ + setUsers(users:ODDropdownData["users"]){ + this.data.users = users + return this + } + /**Set the roles of this dropdown (when `type == "role"`) */ + setRoles(roles:ODDropdownData["roles"]){ + this.data.roles = roles + return this + } + /**Set the channels of this dropdown (when `type == "channel"`) */ + setChannels(channels:ODDropdownData["channels"]){ + this.data.channels = channels + return this + } + /**Set the mentionables of this dropdown (when `type == "mentionable"`) */ + setMentionables(mentionables:ODDropdownData["mentionables"]){ + this.data.mentionables = mentionables + return this + } +} + +/**## ODDropdown `class` + * This is an open ticket dropdown builder. + * + * With this class, you can create a dropdown to use in a message. + * The only difference with normal dropdowns is that this one can be edited by Open Ticket plugins! + * + * This is possible by using "workers" or multiple functions that will be executed in priority order! + */ +export class ODDropdown extends ODBuilderImplementation { + /**Build this dropdown & compile it for discord.js */ + async build(source:Source, params:Params){ + if (this.didCache && this.cache && this.allowCache) return this.cache + + try{ + //create instance + const instance = new ODDropdownInstance() + + //wait for workers to finish + await this.workers.executeWorkers(instance,source,params) + + //create the discord.js dropdown + if (instance.data.type == "string"){ + const dropdown = new discord.StringSelectMenuBuilder() + dropdown.setCustomId(instance.data.customId) + dropdown.setOptions(...instance.data.options) + if (instance.data.placeholder) dropdown.setPlaceholder(instance.data.placeholder) + if (instance.data.minValues) dropdown.setMinValues(instance.data.minValues) + if (instance.data.maxValues) dropdown.setMaxValues(instance.data.maxValues) + if (instance.data.disabled) dropdown.setDisabled(instance.data.disabled) + + this.cache = {id:this.id,component:dropdown} + this.didCache = true + return {id:this.id,component:dropdown} + + }else if (instance.data.type == "user"){ + const dropdown = new discord.UserSelectMenuBuilder() + dropdown.setCustomId(instance.data.customId) + if (instance.data.users.length > 0) dropdown.setDefaultUsers(...instance.data.users.map((u) => u.id)) + if (instance.data.placeholder) dropdown.setPlaceholder(instance.data.placeholder) + if (instance.data.minValues) dropdown.setMinValues(instance.data.minValues) + if (instance.data.maxValues) dropdown.setMaxValues(instance.data.maxValues) + if (instance.data.disabled) dropdown.setDisabled(instance.data.disabled) + + this.cache = {id:this.id,component:dropdown} + this.didCache = true + return {id:this.id,component:dropdown} + + }else if (instance.data.type == "role"){ + const dropdown = new discord.RoleSelectMenuBuilder() + dropdown.setCustomId(instance.data.customId) + if (instance.data.roles.length > 0) dropdown.setDefaultRoles(...instance.data.roles.map((r) => r.id)) + if (instance.data.placeholder) dropdown.setPlaceholder(instance.data.placeholder) + if (instance.data.minValues) dropdown.setMinValues(instance.data.minValues) + if (instance.data.maxValues) dropdown.setMaxValues(instance.data.maxValues) + if (instance.data.disabled) dropdown.setDisabled(instance.data.disabled) + + this.cache = {id:this.id,component:dropdown} + this.didCache = true + return {id:this.id,component:dropdown} + + }else if (instance.data.type == "channel"){ + const dropdown = new discord.ChannelSelectMenuBuilder() + dropdown.setCustomId(instance.data.customId) + if (instance.data.channels.length > 0) dropdown.setDefaultChannels(...instance.data.channels.map((c) => c.id)) + if (instance.data.placeholder) dropdown.setPlaceholder(instance.data.placeholder) + if (instance.data.minValues) dropdown.setMinValues(instance.data.minValues) + if (instance.data.maxValues) dropdown.setMaxValues(instance.data.maxValues) + if (instance.data.disabled) dropdown.setDisabled(instance.data.disabled) + + this.cache = {id:this.id,component:dropdown} + this.didCache = true + return {id:this.id,component:dropdown} + + }else if (instance.data.type == "mentionable"){ + const dropdown = new discord.MentionableSelectMenuBuilder() + + const values: ({type:discord.SelectMenuDefaultValueType.User,id:string}|{type:discord.SelectMenuDefaultValueType.Role,id:string})[] = [] + instance.data.mentionables.forEach((m) => { + if (m instanceof discord.User){ + values.push({type:discord.SelectMenuDefaultValueType.User,id:m.id}) + }else{ + values.push({type:discord.SelectMenuDefaultValueType.Role,id:m.id}) + } + }) + + dropdown.setCustomId(instance.data.customId) + if (instance.data.mentionables.length > 0) dropdown.setDefaultValues(...values) + if (instance.data.placeholder) dropdown.setPlaceholder(instance.data.placeholder) + if (instance.data.minValues) dropdown.setMinValues(instance.data.minValues) + if (instance.data.maxValues) dropdown.setMaxValues(instance.data.maxValues) + if (instance.data.disabled) dropdown.setDisabled(instance.data.disabled) + + this.cache = {id:this.id,component:dropdown} + this.didCache = true + return {id:this.id,component:dropdown} + }else{ + throw new Error("Tried to build an ODDropdown with unknown type!") + } + }catch(err){ + process.emit("uncaughtException",new ODSystemError("ODDropdown:build(\""+this.id.value+"\") => Major Error (see next error)")) + process.emit("uncaughtException",err) + return {id:this.id,component:null} + } + } +} + +/**## ODFileManager `class` + * This is an open ticket file manager. + * + * It contains all Open Ticket file builders. Here, you can add your own files or edit existing ones! + * + * It's recommended to use this system in combination with all the other Open Ticket builders! + */ +export class ODFileManager extends ODManagerWithSafety> { + constructor(debug:ODDebugger){ + super(() => { + return new ODFile("openticket:unknown-file",(instance,params,source,cancel) => { + instance.setName("openticket_unknown-file.txt") + instance.setDescription("❌ ") + instance.setContents("Couldn't find file in registery `openticket.builders.files`") + cancel() + }) + },debug,"file") + } +} + +/**## ODFileData `interface` + * This interface contains the data to build a file. + */ +export interface ODFileData { + /**The file buffer, string or raw data */ + file:discord.BufferResolvable + /**The name of the file */ + name:string, + /**The description of the file */ + description:string|null, + /**Set the file to be a spoiler */ + spoiler:boolean +} + +/**## ODFileBuildResult `interface` + * This interface contains the result from a built file (attachment). This can be used in the `ODMessage` builder! + */ +export interface ODFileBuildResult { + /**The id of this file */ + id:ODId, + /**The discord file */ + file:discord.AttachmentBuilder|null +} + +/**## ODFileInstance `class` + * This is an open ticket file instance. + * + * It contains all properties & functions to build a file! + */ +export class ODFileInstance { + /**The current data of this file */ + data: ODFileData = { + file:"", + name:"file.txt", + description:null, + spoiler:false + } + + /**Set the file path of this attachment */ + setFile(file:string){ + this.data.file = file + return this + } + /**Set the file contents of this attachment */ + setContents(contents:string|Buffer){ + this.data.file = (typeof contents == "string") ? Buffer.from(contents) : contents + return this + } + /**Set the name of this attachment */ + setName(name:ODFileData["name"]){ + this.data.name = name + return this + } + /**Set the description of this attachment */ + setDescription(description:ODFileData["description"]){ + this.data.description = description + return this + } + /**Set this attachment to show as a spoiler */ + setSpoiler(spoiler:ODFileData["spoiler"]){ + this.data.spoiler = spoiler + return this + } +} + +/**## ODFile `class` + * This is an open ticket file builder. + * + * With this class, you can create a file to use in a message. + * The only difference with normal files is that this one can be edited by Open Ticket plugins! + * + * This is possible by using "workers" or multiple functions that will be executed in priority order! + */ +export class ODFile extends ODBuilderImplementation { + /**Build this attachment & compile it for discord.js */ + async build(source:Source, params:Params){ + if (this.didCache && this.cache && this.allowCache) return this.cache + + try{ + //create instance + const instance = new ODFileInstance() + + //wait for workers to finish + await this.workers.executeWorkers(instance,source,params) + + //create the discord.js attachment + const file = new discord.AttachmentBuilder(instance.data.file) + file.setName(instance.data.name ? instance.data.name : "file.txt") + if (instance.data.description) file.setDescription(instance.data.description) + if (instance.data.spoiler) file.setSpoiler(instance.data.spoiler) + + + this.cache = {id:this.id,file} + this.didCache = true + return {id:this.id,file} + }catch(err){ + process.emit("uncaughtException",new ODSystemError("ODFile:build(\""+this.id.value+"\") => Major Error (see next error)")) + process.emit("uncaughtException",err) + return {id:this.id,file:null} + } + } +} + +/**## ODEmbedManager `class` + * This is an open ticket embed manager. + * + * It contains all Open Ticket embed builders. Here, you can add your own embeds or edit existing ones! + * + * It's recommended to use this system in combination with all the other Open Ticket builders! + */ +export class ODEmbedManager extends ODManagerWithSafety> { + constructor(debug:ODDebugger){ + super(() => { + return new ODEmbed("openticket:unknown-embed",(instance,params,source,cancel) => { + instance.setFooter("openticket:unknown-embed") + instance.setColor("#ff0000") + instance.setTitle("❌ ") + instance.setDescription("Couldn't find embed in registery `openticket.builders.embeds`") + cancel() + }) + },debug,"embed") + } +} + +/**## ODEmbedData `interface` + * This interface contains the data to build an embed. + */ +export interface ODEmbedData { + /**The title of the embed */ + title:string|null, + /**The color of the embed */ + color:discord.ColorResolvable|null, + /**The url of the embed */ + url:string|null, + /**The description of the embed */ + description:string|null, + /**The author text of the embed */ + authorText:string|null, + /**The author image of the embed */ + authorImage:string|null, + /**The author url of the embed */ + authorUrl:string|null, + /**The footer text of the embed */ + footerText:string|null, + /**The footer image of the embed */ + footerImage:string|null, + /**The image of the embed */ + image:string|null, + /**The thumbnail of the embed */ + thumbnail:string|null, + /**The fields of the embed */ + fields:ODInterfaceWithPartialProperty[], + /**The timestamp of the embed */ + timestamp:number|Date|null +} + +/**## ODEmbedBuildResult `interface` + * This interface contains the result from a built embed. This can be used in the `ODMessage` builder! + */ +export interface ODEmbedBuildResult { + /**The id of this embed */ + id:ODId, + /**The discord embed */ + embed:discord.EmbedBuilder|null +} + +/**## ODEmbedInstance `class` + * This is an open ticket embed instance. + * + * It contains all properties & functions to build an embed! + */ +export class ODEmbedInstance { + /**The current data of this embed */ + data: ODEmbedData = { + title:null, + color:null, + url:null, + description:null, + authorText:null, + authorImage:null, + authorUrl:null, + footerText:null, + footerImage:null, + image:null, + thumbnail:null, + fields:[], + timestamp:null + } + + /**Set the title of this embed */ + setTitle(title:ODEmbedData["title"]){ + this.data.title = title + return this + } + /**Set the color of this embed */ + setColor(color:ODEmbedData["color"]){ + this.data.color = color + return this + } + /**Set the url of this embed */ + setUrl(url:ODEmbedData["url"]){ + this.data.url = url + return this + } + /**Set the description of this embed */ + setDescription(description:ODEmbedData["description"]){ + this.data.description = description + return this + } + /**Set the author of this embed */ + setAuthor(text:ODEmbedData["authorText"], image?:ODEmbedData["authorImage"], url?:ODEmbedData["authorUrl"]){ + this.data.authorText = text + this.data.authorImage = image ?? null + this.data.authorUrl = url ?? null + return this + } + /**Set the footer of this embed */ + setFooter(text:ODEmbedData["footerText"], image?:ODEmbedData["footerImage"]){ + this.data.footerText = text + this.data.footerImage = image ?? null + return this + } + /**Set the image of this embed */ + setImage(image:ODEmbedData["image"]){ + this.data.image = image + return this + } + /**Set the thumbnail of this embed */ + setThumbnail(thumbnail:ODEmbedData["thumbnail"]){ + this.data.thumbnail = thumbnail + return this + } + /**Set the fields of this embed */ + setFields(fields:ODEmbedData["fields"]){ + //TEMP CHECKS + fields.forEach((field,index) => { + if (field.value.length >= 1024) throw new ODSystemError("ODEmbed:setFields() => field "+index+" reached 1024 character limit!") + if (field.name.length >= 256) throw new ODSystemError("ODEmbed:setFields() => field "+index+" reached 256 name character limit!") + }) + + this.data.fields = fields + return this + } + /**Add fields to this embed */ + addFields(...fields:ODEmbedData["fields"]){ + //TEMP CHECKS + fields.forEach((field,index) => { + if (field.value.length >= 1024) throw new ODSystemError("ODEmbed:addFields() => field "+index+" reached 1024 character limit!") + if (field.name.length >= 256) throw new ODSystemError("ODEmbed:addFields() => field "+index+" reached 256 name character limit!") + }) + + this.data.fields.push(...fields) + return this + } + /**Clear all fields from this embed */ + clearFields(){ + this.data.fields = [] + return this + } + /**Set the timestamp of this embed */ + setTimestamp(timestamp:ODEmbedData["timestamp"]){ + this.data.timestamp = timestamp + return this + } +} + +/**## ODEmbed `class` + * This is an open ticket embed builder. + * + * With this class, you can create a embed to use in a message. + * The only difference with normal embeds is that this one can be edited by Open Ticket plugins! + * + * This is possible by using "workers" or multiple functions that will be executed in priority order! + */ +export class ODEmbed extends ODBuilderImplementation { + /**Build this embed & compile it for discord.js */ + async build(source:Source, params:Params){ + if (this.didCache && this.cache && this.allowCache) return this.cache + + try{ + //create instance + const instance = new ODEmbedInstance() + + //wait for workers to finish + await this.workers.executeWorkers(instance,source,params) + + //create the discord.js embed + const embed = new discord.EmbedBuilder() + if (instance.data.title) embed.setTitle(instance.data.title) + if (instance.data.color) embed.setColor(instance.data.color) + if (instance.data.url) embed.setURL(instance.data.url) + if (instance.data.description) embed.setDescription(instance.data.description) + if (instance.data.authorText) embed.setAuthor({ + name:instance.data.authorText, + iconURL:instance.data.authorImage ?? undefined, + url:instance.data.authorUrl ?? undefined + }) + if (instance.data.footerText) embed.setFooter({ + text:instance.data.footerText, + iconURL:instance.data.footerImage ?? undefined, + }) + if (instance.data.image) embed.setImage(instance.data.image) + if (instance.data.thumbnail) embed.setThumbnail(instance.data.thumbnail) + if (instance.data.timestamp) embed.setTimestamp(instance.data.timestamp) + if (instance.data.fields.length > 0) embed.setFields(instance.data.fields) + + this.cache = {id:this.id,embed} + this.didCache = true + return {id:this.id,embed} + }catch(err){ + process.emit("uncaughtException",new ODSystemError("ODEmbed:build(\""+this.id.value+"\") => Major Error (see next error)")) + process.emit("uncaughtException",err) + return {id:this.id,embed:null} + } + } +} + +/**## ODMessageManager `class` + * This is an open ticket message manager. + * + * It contains all Open Ticket message builders. Here, you can add your own messages or edit existing ones! + * + * It's recommended to use this system in combination with all the other Open Ticket builders! + */ +export class ODMessageManager extends ODManagerWithSafety> { + constructor(debug:ODDebugger){ + super(() => { + return new ODMessage("openticket:unknown-message",(instance,params,source,cancel) => { + instance.setContent("**❌ **\nCouldn't find message in registery `openticket.builders.messages`") + cancel() + }) + },debug,"message") + } +} + +/**## ODMessageData `interface` + * This interface contains the data to build a message. + */ +export interface ODMessageData { + /**The content of this message. `null` when no content */ + content:string|null, + /**Poll data for this message */ + poll:discord.PollData|null, + /**Try to make this message ephemeral when available */ + ephemeral:boolean, + + /**Embeds from this message */ + embeds:ODEmbedBuildResult[], + /**Components from this message */ + components:ODComponentBuildResult[], + /**Files from this message */ + files:ODFileBuildResult[], + + /**Additional options that aren't covered by the Open Ticket api!*/ + additionalOptions:Omit +} + +/**## ODMessageBuildResult `interface` + * This interface contains the result from a built message. This can be sent in a discord channel! + */ +export interface ODMessageBuildResult { + /**The id of this message */ + id:ODId, + /**The discord message */ + message:Omit, + /**When enabled, the bot will try to send this as an ephemeral message */ + ephemeral:boolean +} + +/**## ODMessageBuildSentResult `interface` + * This interface contains the result from a sent built message. This can be used to edit, view & save the message that got created. + */ +export interface ODMessageBuildSentResult { + /**Did the message get sent succesfully? */ + success:boolean, + /**The message that got sent. */ + message:discord.Message|null +} + +/**## ODMessageInstance `class` + * This is an open ticket message instance. + * + * It contains all properties & functions to build a message! + */ +export class ODMessageInstance { + /**The current data of this message */ + data: ODMessageData = { + content:null, + poll:null, + ephemeral:false, + + embeds:[], + components:[], + files:[], + + additionalOptions:{ + } + } + + /**Set the content of this message */ + setContent(content:ODMessageData["content"]){ + this.data.content = content + return this + } + /**Set the poll of this message */ + setPoll(poll:ODMessageData["poll"]){ + this.data.poll = poll + return this + } + /**Make this message ephemeral when possible */ + setEphemeral(ephemeral:ODMessageData["ephemeral"]){ + this.data.ephemeral = ephemeral + return this + } + /**Set the embeds of this message */ + setEmbeds(...embeds:ODEmbedBuildResult[]){ + this.data.embeds = embeds + return this + } + /**Add an embed to this message! */ + addEmbed(embed:ODEmbedBuildResult){ + this.data.embeds.push(embed) + return this + } + /**Remove an embed from this message */ + removeEmbed(id:ODValidId){ + const index = this.data.embeds.findIndex((embed) => embed.id.value === new ODId(id).value) + if (index > -1) this.data.embeds.splice(index,1) + return this + } + /**Get an embed from this message */ + getEmbed(id:ODValidId){ + const embed = this.data.embeds.find((embed) => embed.id.value === new ODId(id).value) + if (embed) return embed.embed + else return null + } + /**Set the components of this message */ + setComponents(...components:ODComponentBuildResult[]){ + this.data.components = components + return this + } + /**Add a component to this message! */ + addComponent(component:ODComponentBuildResult){ + this.data.components.push(component) + return this + } + /**Remove a component from this message */ + removeComponent(id:ODValidId){ + const index = this.data.components.findIndex((component) => component.id.value === new ODId(id).value) + if (index > -1) this.data.components.splice(index,1) + return this + } + /**Get a component from this message */ + getComponent(id:ODValidId){ + const component = this.data.components.find((component) => component.id.value === new ODId(id).value) + if (component) return component.component + else return null + } + /**Set the files of this message */ + setFiles(...files:ODFileBuildResult[]){ + this.data.files = files + return this + } + /**Add a file to this message! */ + addFile(file:ODFileBuildResult){ + this.data.files.push(file) + return this + } + /**Remove a file from this message */ + removeFile(id:ODValidId){ + const index = this.data.files.findIndex((file) => file.id.value === new ODId(id).value) + if (index > -1) this.data.files.splice(index,1) + return this + } + /**Get a file from this message */ + getFile(id:ODValidId){ + const file = this.data.files.find((file) => file.id.value === new ODId(id).value) + if (file) return file.file + else return null + } +} + +/**## ODMessage `class` + * This is an open ticket message builder. + * + * With this class, you can create a message to send in a discord channel. + * The only difference with normal messages is that this one can be edited by Open Ticket plugins! + * + * This is possible by using "workers" or multiple functions that will be executed in priority order! + */ +export class ODMessage extends ODBuilderImplementation { + /**Build this message & compile it for discord.js */ + async build(source:Source, params:Params){ + if (this.didCache && this.cache && this.allowCache) return this.cache + + //create instance + const instance = new ODMessageInstance() + + //wait for workers to finish + await this.workers.executeWorkers(instance,source,params) + + //create the discord.js message + const componentArray: discord.ActionRowBuilder[] = [] + let currentRow: discord.ActionRowBuilder = new discord.ActionRowBuilder() + instance.data.components.forEach((c) => { + //return when component crashed + if (c.component == null) return + else if (c.component == "\n"){ + //create new current row when required + if (currentRow.components.length > 0){ + componentArray.push(currentRow) + currentRow = new discord.ActionRowBuilder() + } + }else if (c.component instanceof discord.BaseSelectMenuBuilder){ + //push current row when not empty + if (currentRow.components.length > 0){ + componentArray.push(currentRow) + currentRow = new discord.ActionRowBuilder() + } + currentRow.addComponents(c.component) + //create new current row after dropdown + componentArray.push(currentRow) + currentRow = new discord.ActionRowBuilder() + }else{ + //push button to current row + currentRow.addComponents(c.component) + } + + //create new row when 5 rows in length + if (currentRow.components.length == 5){ + componentArray.push(currentRow) + currentRow = new discord.ActionRowBuilder() + } + }) + //push final row to array + if (currentRow.components.length > 0) componentArray.push(currentRow) + + const filteredEmbeds = instance.data.embeds.map((e) => e.embed).filter((e) => e instanceof discord.EmbedBuilder) as discord.EmbedBuilder[] + const filteredFiles = instance.data.files.map((f) => f.file).filter((f) => f instanceof discord.AttachmentBuilder) as discord.AttachmentBuilder[] + + const message : discord.MessageCreateOptions = { + content:instance.data.content ?? "", + poll:instance.data.poll ?? undefined, + embeds:filteredEmbeds, + components:componentArray, + files:filteredFiles + } + + let result = {id:this.id,message,ephemeral:instance.data.ephemeral} + + Object.assign(result.message,instance.data.additionalOptions) + + this.cache = result + this.didCache = true + return result + } +} + +/**## ODModalManager `class` + * This is an open ticket modal manager. + * + * It contains all Open Ticket modal builders. Here, you can add your own modals or edit existing ones! + * + * It's recommended to use this system in combination with all the other Open Ticket builders! + */ +export class ODModalManager extends ODManagerWithSafety> { + constructor(debug:ODDebugger){ + super(() => { + return new ODModal("openticket:unknown-modal",(instance,params,source,cancel) => { + instance.setCustomId("od:unknown-modal") + instance.setTitle("❌ ") + instance.setQuestions( + { + style:"short", + customId:"error", + label:"error", + placeholder:"Contact the bot creator for more info!" + } + ) + cancel() + }) + },debug,"modal") + } +} + +/**## ODModalDataQuestion `interface` + * This interface contains the data to build a modal question. + */ +export interface ODModalDataQuestion { + /**The style of this modal question */ + style:"short"|"paragraph", + /**The custom id of this modal question */ + customId:string + /**The label of this modal question */ + label?:string, + /**The min length of this modal question */ + minLength?:number, + /**The max length of this modal question */ + maxLength?:number, + /**Is this modal question required? */ + required?:boolean, + /**The placeholder of this modal question */ + placeholder?:string, + /**The initial value of this modal question */ + value?:string +} + +/**## ODModalData `interface` + * This interface contains the data to build a modal. + */ +export interface ODModalData { + /**The custom id of this modal */ + customId:string, + /**The title of this modal */ + title:string|null, + /**The collection of questions in this modal */ + questions:ODModalDataQuestion[], +} + +/**## ODModalBuildResult `interface` + * This interface contains the result from a built modal (form). This can be used in the `ODMessage` builder! + */ +export interface ODModalBuildResult { + /**The id of this modal */ + id:ODId, + /**The discord modal */ + modal:discord.ModalBuilder +} + +/**## ODModalInstance `class` + * This is an open ticket modal instance. + * + * It contains all properties & functions to build a modal! + */ +export class ODModalInstance { + /**The current data of this modal */ + data: ODModalData = { + customId:"", + title:null, + questions:[] + } + + /**Set the custom id of this modal */ + setCustomId(customId:ODModalData["customId"]){ + this.data.customId = customId + return this + } + /**Set the title of this modal */ + setTitle(title:ODModalData["title"]){ + this.data.title = title + return this + } + /**Set the questions of this modal */ + setQuestions(...questions:ODModalData["questions"]){ + this.data.questions = questions + return this + } + /**Add a question to this modal! */ + addQuestion(question:ODModalDataQuestion){ + this.data.questions.push(question) + return this + } + /**Remove a question from this modal */ + removeQuestion(customId:string){ + const index = this.data.questions.findIndex((question) => question.customId === customId) + if (index > -1) this.data.questions.splice(index,1) + return this + } + /**Get a question from this modal */ + getQuestion(customId:string){ + const question = this.data.questions.find((question) => question.customId === customId) + if (question) return question + else return null + } +} + +/**## ODModal `class` + * This is an open ticket modal builder. + * + * With this class, you can create a modal to use as response in interactions. + * The only difference with normal modals is that this one can be edited by Open Ticket plugins! + * + * This is possible by using "workers" or multiple functions that will be executed in priority order! + */ +export class ODModal extends ODBuilderImplementation { + /**Build this modal & compile it for discord.js */ + async build(source:Source, params:Params){ + if (this.didCache && this.cache && this.allowCache) return this.cache + + //create instance + const instance = new ODModalInstance() + + //wait for workers to finish + await this.workers.executeWorkers(instance,source,params) + + //create the discord.js modal + const modal = new discord.ModalBuilder() + modal.setCustomId(instance.data.customId) + if (instance.data.title) modal.setTitle(instance.data.title) + else modal.setTitle(instance.data.customId) + + instance.data.questions.forEach((question) => { + const input = new discord.TextInputBuilder() + .setStyle(question.style == "paragraph" ? discord.TextInputStyle.Paragraph : discord.TextInputStyle.Short) + .setCustomId(question.customId) + .setLabel(question.label ? question.label : question.customId) + .setRequired(question.required ? true : false) + + if (question.minLength) input.setMinLength(question.minLength) + if (question.maxLength) input.setMaxLength(question.maxLength) + if (question.value) input.setValue(question.value) + if (question.placeholder) input.setPlaceholder(question.placeholder) + + modal.addComponents( + new discord.ActionRowBuilder() + .addComponents(input) + ) + }) + + this.cache = {id:this.id,modal} + this.didCache = true + return {id:this.id,modal} + } +} \ No newline at end of file diff --git a/src/core/api/modules/checker.ts b/src/core/api/modules/checker.ts new file mode 100644 index 0000000..5f5f727 --- /dev/null +++ b/src/core/api/modules/checker.ts @@ -0,0 +1,1465 @@ +/////////////////////////////////////// +//CONFIG CHECKER MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODManagerData, ODValidId, ODValidJsonType } from "./base" +import { ODConfig } from "./config" +import { ODLanguageManager } from "./language" +import { ODDebugger } from "./console" + +/**## ODCheckerResult `interface` + * This interface is the result from a config checker check() function. + */ +export interface ODCheckerResult { + valid:boolean + messages:ODCheckerMessage[] +} + +/**## ODCheckerManager `class` + * This is an open ticket checker manager. + * + * It manages all config checkers in the bot and allows plugins to access config checkers from open ticket & other plugins! + * + * You will use this class to get/add a config checker (`ODChecker`) in your plugin! + * @example + * //get checker for ./config/general.json => ODChecker class + * const testChecker = openticket.checkers.get("openticket:general") + * + * //add a new checker with id "test" => ./config/test.json + * const testConfig = new api.ODConfig("test","test.json") + * const testChecker = new api.ODChecker("test",openticket.checkers.storage,0,testConfig,[]) + * openticket.checkers.add(testChecker) + */ +export class ODCheckerManager extends ODManager { + /**The global temporary storage shared between all config checkers. */ + storage: ODCheckerStorage + /**The class responsible for rendering the config checker report. */ + renderer: ODCheckerRenderer + /**The class responsible for translating the config checker report. */ + translation: ODCheckerTranslationRegister + /**Final functions are global functions executed just before the report is created. */ + functions: ODCheckerFunctionManager + /**A variable containing the last result returned from `checkAll()` */ + lastResult: ODCheckerResult|null = null + + constructor(debug:ODDebugger, storage:ODCheckerStorage, renderer:ODCheckerRenderer, translation:ODCheckerTranslationRegister, functions:ODCheckerFunctionManager){ + super(debug,"config checker") + this.storage = storage + this.renderer = renderer + this.translation = translation + this.functions = functions + } + /**Check all config checkers registered in this manager.*/ + checkAll(sort:boolean): ODCheckerResult { + this.storage.reset() + + let isValid = true + const final: ODCheckerMessage[] = [] + + const checkers = this.getAll() + checkers.sort((a,b) => b.priority-a.priority) + + checkers.forEach((checker) => { + const res = checker.check() + final.push(...res.messages) + + if (!res.valid) isValid = false + }) + + this.functions.getAll().forEach((func) => { + const res = func.func(this,this.functions) + final.push(...res.messages) + + if (!res.valid) isValid = false + }) + + //sort messages => (info, warning, error) + if (sort) final.sort((a,b) => { + const typeA = (a.type == "error") ? 2 : (a.type == "warning") ? 1 : 0 + const typeB = (b.type == "error") ? 2 : (b.type == "warning") ? 1 : 0 + + return typeA-typeB + }) + + this.lastResult = { + valid:isValid, + messages:final + } + + return { + valid:isValid, + messages:final + } + } +} + +/**## ODCheckerStorage `class` + * This is an open ticket checker storage. + * + * It stores temporary data to share between config checkers! + * (e.g. The `messages.json` needs to access the `"id"` from `options.json`) + * + * + * You will probably use this class when you create your own config checker! + */ +export class ODCheckerStorage { + /**This is the array that stores all the data. ❌ **(don't edit unless really needed!)***/ + storage: {source:ODId, key:string, value:any}[] = [] + + /**Get data from the database (`source` => id of `ODChecker`) */ + get(source:ODValidId, key:string): any|null { + const result = this.storage.find(d => (d.source.value == new ODId(source).value) && (d.key == key)) + return (result) ? result.value : null + } + /**Add data to the database (`source` => id of `ODChecker`). This function also overwrites existing data!*/ + set(source:ODValidId, key:string, value:any){ + const index = this.storage.findIndex(d => (d.source.value == new ODId(source).value) && (d.key == key)) + if (index > -1){ + //overwrite + this.storage[index] = { + source:new ODId(source), + key,value + } + return true + }else{ + this.storage.push({ + source:new ODId(source), + key,value + }) + return false + } + } + /**Delete data from the database (`source` => id of `ODChecker`) */ + delete(source:ODValidId, key:string){ + const index = this.storage.findIndex(d => (d.source.value == new ODId(source).value) && (d.key == key)) + if (index > -1){ + //delete + this.storage.splice(index,1) + return true + }else return false + } + + /**Reset the entire database */ + reset(){ + this.storage = [] + } +} + +/**## ODCheckerRenderer `class` + * This is an open ticket checker renderer. + * + * It's responsible for rendering the config checker result in the console. + * This class doesn't provide any components! You need to create them by extending this class + * + * You will only use this class if you want to change how the config checker looks! + */ +export class ODCheckerRenderer { + /**Get all components */ + getComponents(compact:boolean, renderEmpty:boolean, translation:ODCheckerTranslationRegister, data:ODCheckerResult): string[] { + return [] + } + /**Render all components */ + render(components:string[]){ + if (components.length < 1) return + console.log("\n") + components.forEach((c) => { + console.log(c) + }) + console.log("\n") + } +} + +/**## ODCheckerTranslationRegister `class` + * This is an open ticket checker translation register. + * + * It's used to store & manage the translation for each message from the config checker! + * Most translations are stored by message id, but there are some exceptions like the additional text on the checker report. + * + * You will use this class if you want to translate your config checker messages! **This is optional & isn't required for the checker to work!** + */ +export class ODCheckerTranslationRegister { + /**This is the array that stores all the data. ❌ **(don't edit unless really needed!)***/ + #translations: {type:"message"|"other", id:string, translation:string}[] = [] + + /**Get the translation from a config checker message/sentence */ + get(type:"message"|"other", id:string): string|null { + const result = this.#translations.find(d => (d.id == id) && (d.type == type)) + return (result) ? result.translation : null + } + /**Set the translation for a config checker message/sentence. This function also overwrites existing translations!*/ + set(type:"message"|"other", id:string, translation:string){ + const index = this.#translations.findIndex(d => (d.id == id) && (d.type == type)) + if (index > -1){ + //overwrite + this.#translations[index] = {type,id,translation} + return true + }else{ + this.#translations.push({type,id,translation}) + return false + } + } + /**Delete the translation for a config checker message/sentence. */ + delete(type:"message"|"other", id:string){ + const index = this.#translations.findIndex(d => (d.id == id) && (d.type == type)) + if (index > -1){ + //delete + this.#translations.splice(index,1) + return true + }else return false + } + + /**Get all translations */ + getAll(){ + return this.#translations + } + + /**Insert the translation params into the text. */ + insertTranslationParams(text:string, translationParams:string[]){ + translationParams.forEach((value,index) => { + text = text.replace(`{${index}}`,value) + }) + return text + } + /**A shortcut to copy translations from the `ODLanguageManager` to `ODCheckerTranslationRegister` */ + quickTranslate(manager:ODLanguageManager, translationId:string, type:"other"|"message", id:string){ + const translation = manager.getTranslation(translationId) + if (translation) this.set(type,id,translation) + } +} + +/**## ODCheckerFunction `class` + * This is an open ticket config checker function. + * + * It is a global function that will be executed after all config checkers. It can do additional checks for invalid/missing configurations. + * It's mostly used for things that need to be checked globally! + */ +export class ODCheckerFunction extends ODManagerData { + /**The function itself :) */ + func: (manager:ODCheckerManager, functions:ODCheckerFunctionManager) => ODCheckerResult + + constructor(id:ODValidId, func:(manager:ODCheckerManager, functions:ODCheckerFunctionManager) => ODCheckerResult){ + super(id) + this.func = func + } +} + +/**## ODCheckerFunctionManager `class` + * This is an open ticket config checker function manager. + * + * It manages all `ODCheckerFunction`'s and it has some extra shortcuts for frequently used methods. + */ +export class ODCheckerFunctionManager extends ODManager { + constructor(debug:ODDebugger){ + super(debug,"config checker function") + } + + /**A shortcut to create a warning, info or error message */ + createMessage(checkerId:ODValidId, id:ODValidId, filepath:string, type:"info"|"warning"|"error", message:string, locationTrace:ODCheckerLocationTrace, docs:string|null, translationParams:string[], locationId:ODId, locationDocs:string|null): ODCheckerMessage { + return { + checkerId:new ODId(checkerId), + messageId:new ODId(id), + locationId, + + type,message, + path:this.locationTraceToString(locationTrace), + filepath, + translationParams, + + messageDocs:docs, + locationDocs + } + } + /**Create a string from the location trace (path)*/ + locationTraceToString(trace:ODCheckerLocationTrace){ + const final: ODCheckerLocationTrace = [] + trace.forEach((t) => { + if (typeof t == "number"){ + final.push(`:${t}`) + }else{ + final.push(`."${t}"`) + } + }) + return final.join("").substring(1) + } + /**De-reference the locationTrace array. Use this before adding a value to the array*/ + locationTraceDeref(trace:ODCheckerLocationTrace): ODCheckerLocationTrace { + return JSON.parse(JSON.stringify(trace)) + } +} + +/**## ODCheckerLocationTrace `type` + * This type is an array of strings & numbers which represents the location trace from the config checker. + * It's used to generate a path to the error (e.g. `"abc"."efg".1."something"`) + */ +export type ODCheckerLocationTrace = (string|number)[] + +/**## ODChecker `class` + * This is an open ticket config checker. + * + * It checks a specific config file for invalid/missing configurations. This data can then be used to show to the user what's wrong! + * You can check for example if a string is longer/shorter than a certain amount of characters & more! + * + * You will use this class when you create your own custom config file & you want to check it for syntax errors. + * @example + * //create a new checker with id "test" => ./config/test.json + * const testConfig = new api.ODConfig("test","test.json") + * const testChecker = new api.ODChecker("test",openticket.checkers.storage,0,testConfig,new api.ODCheckerStructure()) + * openticket.checkers.add(testChecker) + * + * //you will still need to build the structure & insert it in the constructor + */ +export class ODChecker extends ODManagerData { + /**The storage of this checker (reference for `ODCheckerManager.storage`) */ + storage: ODCheckerStorage + /**The higher the priority, the faster it gets checked! */ + priority: number + /**The config file that needs to be checked */ + config: ODConfig + /**The structure of the config file */ + structure: ODCheckerStructure + /**Temporary storage for all error messages from the check() method (not recommended to use) */ + messages: ODCheckerMessage[] = [] + /**Temporary storage for the quit status from the check() method (not recommended to use) */ + quit: boolean = false + + constructor(id:ODValidId, storage: ODCheckerStorage, priority:number, config:ODConfig, structure: ODCheckerStructure){ + super(id) + this.storage = storage + this.priority = priority + this.config = config + this.structure = structure + } + + /**Run this checker. Returns all errors*/ + check(): ODCheckerResult { + this.messages = [] + this.quit = false + + this.structure.check(this,this.config.data,[]) + return { + valid:!this.quit, + messages:this.messages + } + } + /**Create a string from the location trace (path)*/ + locationTraceToString(trace:ODCheckerLocationTrace){ + const final: ODCheckerLocationTrace = [] + trace.forEach((t) => { + if (typeof t == "number"){ + final.push(`:${t}`) + }else{ + final.push(`."${t}"`) + } + }) + return final.join("").substring(1) + } + /**De-reference the locationTrace array. Use this before adding a value to the array*/ + locationTraceDeref(trace:ODCheckerLocationTrace): ODCheckerLocationTrace { + return JSON.parse(JSON.stringify(trace)) + } + + /**A shortcut to create a warning, info or error message */ + createMessage(id:ODValidId, type:"info"|"warning"|"error", message:string, locationTrace:ODCheckerLocationTrace, docs:string|null, translationParams:string[], locationId:ODId, locationDocs:string|null){ + if (type == "error") this.quit = true + this.messages.push({ + checkerId:this.id, + messageId:new ODId(id), + locationId, + + type,message, + path:this.locationTraceToString(locationTrace), + filepath:this.config.file, + translationParams, + + messageDocs:docs, + locationDocs + }) + } +} + +/**## ODCheckerMessage `interface` + * This interface is an object which has all variables required for a config checker message! + */ +export interface ODCheckerMessage { + checkerId:ODId, + messageId:ODId, + locationId:ODId, + + type:"info"|"warning"|"error", + message:string, + path:string, + filepath:string, + translationParams:string[], + + messageDocs:string|null, + locationDocs:string|null +} + +/**## ODCheckerStructureOptions `interface` + * This interface has the basic options for the `ODCheckerStructure`! + */ +export interface ODCheckerStructureOptions { + /**Add a custom checker function */ + custom?:(checker:ODChecker, value:ODValidJsonType, locationTrace:ODCheckerLocationTrace, locationId:ODId, locationDocs:string|null) => boolean, + /**Set the url to the documentation of this variable. */ + docs?:string +} + +/**## ODCheckerStructure `class` + * This is an open ticket config checker structure. + * + * This class will check for a single variable in a config file, customise it in the settings! + * If you want prebuilt checkers (for strings, booleans, numbers, ...), check the other `ODCheckerStructure`'s! + * + * You will almost never use this class! It's better if you extend on another `ODConfigCheckerStructure`! + */ +export class ODCheckerStructure { + /**The id of this checker structure */ + id: ODId + /**The options for this checker structure */ + options: ODCheckerStructureOptions + + constructor(id:ODValidId, options:ODCheckerStructureOptions){ + this.id = new ODId(id) + this.options = options + } + + /**Check a variable if it matches all settings in this checker. This function is automatically executed by open ticket! */ + check(checker:ODChecker, value:ODValidJsonType, locationTrace:ODCheckerLocationTrace): boolean { + if (typeof this.options.custom != "undefined"){ + return this.options.custom(checker,value,locationTrace,this.id,(this.options.docs ?? null)) + }else return true + } +} + +/**## ODCheckerObjectStructureOptions `interface` + * This interface has the options for `ODCheckerObjectStructure`! + */ +export interface ODCheckerObjectStructureOptions extends ODCheckerStructureOptions { + /**Add a checker for a property in an object (can also be optional) */ + children?:{key:string, checker:ODCheckerStructure, priority:number, optional:boolean}[] +} + +/**## ODCheckerObjectStructure `class` + * This is an open ticket config checker structure. + * + * This class will check for an object variable in a config file, customise it in the settings! + * A checker for the children can be set in the settings. + */ +export class ODCheckerObjectStructure extends ODCheckerStructure { + declare options: ODCheckerObjectStructureOptions + + constructor(id:ODValidId, options:ODCheckerObjectStructureOptions){ + super(id,options) + } + + check(checker:ODChecker, value:object, locationTrace:ODCheckerLocationTrace): boolean { + const lt = checker.locationTraceDeref(locationTrace) + + //check type & options + if (typeof value != "object"){ + checker.createMessage("openticket:invalid-type","error","This property needs to be the type: object!",lt,null,["object"],this.id,(this.options.docs ?? null)) + return false + } + + //sort children + if (typeof this.options.children == "undefined") return super.check(checker,value,locationTrace) + const sortedChildren = this.options.children.sort((a,b) => { + if (a.priority < b.priority) return -1 + else if (a.priority > b.priority) return 1 + else return 0 + }) + + //check children + let localQuit = false + sortedChildren.forEach((child) => { + const localLt = checker.locationTraceDeref(lt) + localLt.push(child.key) + + if (typeof value[child.key] == "undefined"){ + if (!child.optional){ + localQuit = true + checker.createMessage("openticket:property-missing","error",`The property "${child.key}" is mising from this object!`,lt,null,[`"${child.key}"`],this.id,(this.options.docs ?? null)) + }else{ + checker.createMessage("openticket:property-optional","info",`The property "${child.key}" is optional in this object!`,lt,null,[`"${child.key}"`],this.id,(this.options.docs ?? null)) + } + }else if (!child.checker.check(checker,value[child.key],localLt)) localQuit = true + }) + + //do local quit or check custom function + if (localQuit) return false + else return super.check(checker,value,locationTrace) + } +} + +/**## ODCheckerStringStructureOptions `interface` + * This interface has the options for `ODCheckerStringStructure`! + */ +export interface ODCheckerStringStructureOptions extends ODCheckerStructureOptions { + /**The minimum length of this string */ + minLength?:number, + /**The maximum length of this string */ + maxLength?:number, + /**Set the required length of this string */ + length?:number, + /**This string needs to start with ... */ + startsWith?:string, + /**This string needs to end with ... */ + endsWith?:string, + /**This string needs to contain ... */ + contains?:string, + /**You need to choose between ... */ + choices?:string[], + /**The string needs to match this regex */ + regex?:RegExp +} + +/**## ODCheckerStringStructure `class` + * This is an open ticket config checker structure. + * + * This class will check for a string variable in a config file, customise it in the settings! + */ +export class ODCheckerStringStructure extends ODCheckerStructure { + declare options: ODCheckerStringStructureOptions + + constructor(id:ODValidId, options:ODCheckerStringStructureOptions){ + super(id,options) + } + + check(checker:ODChecker, value:string, locationTrace:ODCheckerLocationTrace): boolean { + const lt = checker.locationTraceDeref(locationTrace) + + //check type & options + if (typeof value != "string"){ + checker.createMessage("openticket:invalid-type","error","This property needs to be the type: string!",lt,null,["string"],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.minLength != "undefined" && value.length < this.options.minLength){ + checker.createMessage("openticket:string-too-short","error",`This string can't be shorter than ${this.options.minLength} characters!`,lt,null,[this.options.minLength.toString()],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.maxLength != "undefined" && value.length > this.options.maxLength){ + checker.createMessage("openticket:string-too-long","error",`This string can't be longer than ${this.options.maxLength} characters!`,lt,null,[this.options.maxLength.toString()],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.length != "undefined" && value.length !== this.options.length){ + checker.createMessage("openticket:string-length-invalid","error",`This string needs to be ${this.options.length} characters long!`,lt,null,[this.options.length.toString()],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.startsWith != "undefined" && !value.startsWith(this.options.startsWith)){ + checker.createMessage("openticket:string-starts-with","error",`This string needs to start with "${this.options.startsWith}"!`,lt,null,[`"${this.options.startsWith}"`],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.endsWith != "undefined" && !value.endsWith(this.options.endsWith)){ + checker.createMessage("openticket:string-ends-with","error",`This string needs to end with "${this.options.endsWith}"!`,lt,null,[`"${this.options.endsWith}"`],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.contains != "undefined" && !value.includes(this.options.contains)){ + checker.createMessage("openticket:string-contains","error",`This string needs to contain "${this.options.contains}"!`,lt,null,[`"${this.options.contains}"`],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.choices != "undefined" && !this.options.choices.includes(value)){ + checker.createMessage("openticket:string-choices","error",`This string can only be one of the following values: "${this.options.choices.join(`", "`)}"!`,lt,null,[`"${this.options.choices.join(`", "`)}"`],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.regex != "undefined" && !this.options.regex.test(value)){ + checker.createMessage("openticket:string-regex","error","This string is invalid!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else return super.check(checker,value,locationTrace) + } +} + +/**## ODCheckerNumberStructureOptions `interface` + * This interface has the options for `ODCheckerNumberStructure`! + */ +export interface ODCheckerNumberStructureOptions extends ODCheckerStructureOptions { + /**The minimum length of this number */ + minLength?:number, + /**The maximum length of this number */ + maxLength?:number, + /**Set the required length of this number */ + length?:number, + /**The minimum value of this number */ + min?:number, + /**The maximum value of this number */ + max?:number, + /**This number is required to match the value */ + is?:number, + /**Only allow a multiple of ... starting at `this.offset` or 0 */ + step?:number, + /**The offset for the step function. */ + offset?:number, + /**This number needs to start with ... */ + startsWith?:string, + /**This number needs to end with ... */ + endsWith?:string, + /**This number needs to contain ... */ + contains?:string, + /**You need to choose between ... */ + choices?:number[], + /**Are numbers with a decimal value allowed? */ + floatAllowed?:boolean, + /**Are negative numbers allowed (without zero) */ + negativeAllowed?:boolean, + /**Are positive numers allowed (without zero) */ + positiveAllowed?:boolean, + /**Is zero allowed? */ + zeroAllowed?:boolean +} + +/**## ODCheckerNumberStructure `class` + * This is an open ticket config checker structure. + * + * This class will check for a number variable in a config file, customise it in the settings! + */ +export class ODCheckerNumberStructure extends ODCheckerStructure { + declare options: ODCheckerNumberStructureOptions + + constructor(id:ODValidId, options:ODCheckerNumberStructureOptions){ + super(id,options) + } + + check(checker:ODChecker, value:number, locationTrace:ODCheckerLocationTrace): boolean { + const lt = checker.locationTraceDeref(locationTrace) + + //offset for step + const stepOffset = (typeof this.options.offset != "undefined") ? this.options.offset : 0 + + //check type & options + if (typeof value != "number"){ + checker.createMessage("openticket:invalid-type","error","This property needs to be the type: number!",lt,null,["number"],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.minLength != "undefined" && value.toString().length < this.options.minLength){ + checker.createMessage("openticket:number-too-short","error",`This number can't be shorter than ${this.options.minLength} characters!`,lt,null,[this.options.minLength.toString()],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.maxLength != "undefined" && value.toString().length > this.options.maxLength){ + checker.createMessage("openticket:number-too-long","error",`This number can't be longer than ${this.options.maxLength} characters!`,lt,null,[this.options.maxLength.toString()],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.length != "undefined" && value.toString().length !== this.options.length){ + checker.createMessage("openticket:number-length-invalid","error",`This number needs to be ${this.options.length} characters long!`,lt,null,[this.options.length.toString()],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.min != "undefined" && value < this.options.min){ + checker.createMessage("openticket:number-too-small","error",`This number needs to be at least ${this.options.min}!`,lt,null,[this.options.min.toString()],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.max != "undefined" && value > this.options.max){ + checker.createMessage("openticket:number-too-large","error",`This number needs to be at most ${this.options.max}!`,lt,null,[this.options.max.toString()],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.is != "undefined" && value == this.options.is){ + checker.createMessage("openticket:number-not-equal","error",`This number needs to be ${this.options.is}!`,lt,null,[this.options.is.toString()],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.step != "undefined" && ((value - stepOffset) % this.options.step) !== 0){ + if (stepOffset > 0) checker.createMessage("openticket:number-step-offset","error",`This number needs to be a multiple of ${this.options.step} starting with ${stepOffset}!`,lt,null,[this.options.step.toString(),stepOffset.toString()],this.id,(this.options.docs ?? null)) + else checker.createMessage("openticket:number-step","error",`This number needs to be a multiple of ${this.options.step}!`,lt,null,[this.options.step.toString()],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.startsWith != "undefined" && !value.toString().startsWith(this.options.startsWith)){ + checker.createMessage("openticket:number-starts-with","error",`This number needs to start with "${this.options.startsWith}"!`,lt,null,[`"${this.options.startsWith}"`],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.endsWith != "undefined" && !value.toString().endsWith(this.options.endsWith)){ + checker.createMessage("openticket:number-ends-with","error",`This number needs to end with "${this.options.endsWith}"!`,lt,null,[`"${this.options.endsWith}"`],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.contains != "undefined" && !value.toString().includes(this.options.contains)){ + checker.createMessage("openticket:number-contains","error",`This number needs to contain "${this.options.contains}"!`,lt,null,[`"${this.options.contains}"`],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.choices != "undefined" && !this.options.choices.includes(value)){ + checker.createMessage("openticket:number-choices","error",`This number can only be one of the following values: "${this.options.choices.join(`", "`)}"!`,lt,null,[`"${this.options.choices.join(`", "`)}"`],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.floatAllowed != "undefined" && !this.options.floatAllowed && (value % 1) !== 0){ + checker.createMessage("openticket:number-float","error","This number can't be a decimal!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.negativeAllowed != "undefined" && value < 0){ + checker.createMessage("openticket:number-negative","error","This number can't be negative!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.positiveAllowed != "undefined" && value > 0){ + checker.createMessage("openticket:number-positive","error","This number can't be positive!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.zeroAllowed != "undefined" && value === 0){ + checker.createMessage("openticket:number-zero","error","This number can't be zero!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else return super.check(checker,value,locationTrace) + } +} + +/**## ODCheckerBooleanStructureOptions `interface` + * This interface has the options for `ODCheckerBooleanStructure`! + */ +export interface ODCheckerBooleanStructureOptions extends ODCheckerStructureOptions { + /**Is `true` allowed? */ + trueAllowed?:boolean, + /**Is `false` allowed? */ + falseAllowed?:boolean +} + +/**## ODCheckerBooleanStructure `class` + * This is an open ticket config checker structure. + * + * This class will check for a boolean variable in a config file, customise it in the settings! + */ +export class ODCheckerBooleanStructure extends ODCheckerStructure { + declare options: ODCheckerBooleanStructureOptions + + constructor(id:ODValidId, options:ODCheckerBooleanStructureOptions){ + super(id,options) + } + + check(checker:ODChecker, value:boolean, locationTrace:ODCheckerLocationTrace): boolean { + const lt = checker.locationTraceDeref(locationTrace) + + //check type & options + if (typeof value != "boolean"){ + checker.createMessage("openticket:invalid-type","error","This property needs to be the type: boolean!",lt,null,["boolean"],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.trueAllowed != "undefined" && !this.options.trueAllowed && value == true){ + checker.createMessage("openticket:boolean-true","error","This boolean can't be true!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.falseAllowed != "undefined" && !this.options.falseAllowed && value == false){ + checker.createMessage("openticket:boolean-false","error","This boolean can't be false!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else return super.check(checker,value,locationTrace) + } +} + +/**## ODCheckerArrayStructureOptions `interface` + * This interface has the options for `ODCheckerArrayStructure`! + */ +export interface ODCheckerArrayStructureOptions extends ODCheckerStructureOptions { + /**The checker for all the properties in this array */ + propertyChecker?:ODCheckerStructure, + /**Don't allow this array to be empty */ + disableEmpty?:boolean, + /**This array is required to be empty */ + emptyRequired?:boolean, + /**The minimum length of this array */ + minLength?:number, + /**The maximum length of this array */ + maxLength?:number, + /**The length of the array needs to be the same as this value */ + length?:number, + /**Allow double values (only for `string`, `number` & `boolean`) */ + allowDoubles?:boolean + /**Only allow these types in the array (for multi-type propertyCheckers) */ + allowedTypes?:("string"|"number"|"boolean"|"null"|"array"|"object"|"other")[] +} + +/**## ODCheckerArrayStructure `class` + * This is an open ticket config checker structure. + * + * This class will check for an array variable in a config file, customise it in the settings! + */ +export class ODCheckerArrayStructure extends ODCheckerStructure { + declare options: ODCheckerArrayStructureOptions + + constructor(id:ODValidId, options:ODCheckerArrayStructureOptions){ + super(id,options) + } + + check(checker:ODChecker, value:Array, locationTrace:ODCheckerLocationTrace): boolean { + const lt = checker.locationTraceDeref(locationTrace) + + if (!Array.isArray(value)){ + checker.createMessage("openticket:invalid-type","error","This property needs to be the type: array!",lt,null,["array"],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.disableEmpty != "undefined" && this.options.disableEmpty && value.length == 0){ + checker.createMessage("openticket:array-empty-disabled","error","This array isn't allowed to be empty!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.emptyRequired != "undefined" && this.options.emptyRequired && value.length != 0){ + checker.createMessage("openticket:array-empty-required","error","This array is required to be empty!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.minLength != "undefined" && value.length < this.options.minLength){ + checker.createMessage("openticket:array-too-short","error",`This array needs to have a length of at least ${this.options.minLength}!`,lt,null,[this.options.minLength.toString()],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.maxLength != "undefined" && value.length > this.options.maxLength){ + checker.createMessage("openticket:array-too-long","error",`This array needs to have a length of at most ${this.options.maxLength}!`,lt,null,[this.options.maxLength.toString()],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.length != "undefined" && value.length == this.options.length){ + checker.createMessage("openticket:array-length-invalid","error",`This array needs to have a length of ${this.options.length}!`,lt,null,[this.options.length.toString()],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.allowedTypes != "undefined" && !this.#arrayAllowedTypesCheck(value,this.options.allowedTypes)){ + checker.createMessage("openticket:array-invalid-types","error",`This array can only contain the following types: ${this.options.allowedTypes.join(", ")}!`,lt,null,[this.options.allowedTypes.join(", ").toString()],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.options.allowDoubles != "undefined" && !this.options.allowDoubles && this.#arrayHasDoubles(value)){ + checker.createMessage("openticket:array-double","error","This array doesn't allow the same value twice!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else{ + //check all properties + let localQuit = false + if (this.options.propertyChecker) value.forEach((property,index) => { + if (!this.options.propertyChecker) return + + const localLt = checker.locationTraceDeref(lt) + localLt.push(index) + + if (!this.options.propertyChecker.check(checker,property,localLt)) localQuit = true + }) + + //return false if invalid properties + if (localQuit){ + checker.quit = true + return false + }else return super.check(checker,value,locationTrace) + } + } + + /**Check this array for the allowed types */ + #arrayAllowedTypesCheck(array:any[],allowedTypes:("string"|"number"|"boolean"|"null"|"array"|"object"|"other")[]): boolean { + //return TRUE if ALL values are valid + return !array.some((value) => { + if (allowedTypes.includes("string") && typeof value == "string"){ + return false //this value is valid + }else if (allowedTypes.includes("number") && typeof value == "number"){ + return false //this value is valid + }else if (allowedTypes.includes("boolean") && typeof value == "boolean"){ + return false //this value is valid + }else if (allowedTypes.includes("object") && typeof value == "object"){ + return false //this value is valid + }else if (allowedTypes.includes("array") && Array.isArray(value)){ + return false //this value is valid + }else if (allowedTypes.includes("null") && value === null){ + return false //this value is valid + }else if (allowedTypes.includes("other")){ + return false //this value is valid + }else{ + return true //this value is invalid + } + }) + } + /**Check this array for doubles */ + #arrayHasDoubles(array:any[]): boolean { + const alreadyFound: string[] = [] + let hasDoubles = false + array.forEach((value) => { + if (alreadyFound.includes(value)) hasDoubles = true + else alreadyFound.push(value) + }) + + return hasDoubles + } +} + +/**## ODCheckerNullStructureOptions `interface` + * This interface has the options for `ODCheckerNullStructure`! + */ +export interface ODCheckerNullStructureOptions extends ODCheckerStructureOptions { + /**Is the value allowed to be null */ + nullAllowed?:boolean +} + +/**## ODCheckerNullStructure `class` + * This is an open ticket config checker structure. + * + * This class will check for a null variable in a config file, customise it in the settings! + */ +export class ODCheckerNullStructure extends ODCheckerStructure { + declare options: ODCheckerNullStructureOptions + + constructor(id:ODValidId, options:ODCheckerNullStructureOptions){ + super(id,options) + } + + check(checker:ODChecker, value:null, locationTrace:ODCheckerLocationTrace): boolean { + const lt = checker.locationTraceDeref(locationTrace) + + //check type & options + if (typeof this.options.nullAllowed != "undefined" && !this.options.nullAllowed && value == null){ + checker.createMessage("openticket:null-invalid","error","This property can't be null!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (value !== null){ + checker.createMessage("openticket:invalid-type","error","This property needs to be the type: null!",lt,null,["null"],this.id,(this.options.docs ?? null)) + return false + }else return super.check(checker,value,locationTrace) + } +} + +/**## ODCheckerTypeSwitchStructureOptions `interface` + * This interface has the options for `ODCheckerTypeSwitchStructure`! + */ +export interface ODCheckerTypeSwitchStructureOptions extends ODCheckerStructureOptions { + /**A checker that will always run (replaces all other checkers) */ + all?:ODCheckerStructure, + /**A checker when the property is a string */ + string?:ODCheckerStringStructure, + /**A checker when the property is a number */ + number?:ODCheckerNumberStructure, + /**A checker when the property is a boolean */ + boolean?:ODCheckerBooleanStructure, + /**A checker when the property is null */ + null?:ODCheckerStructure, + /**A checker when the property is an array */ + array?:ODCheckerStructure, + /**A checker when the property is an object */ + object?:ODCheckerObjectStructure, + /**A checker when the property is something else */ + other?:ODCheckerStructure, + /**A list of allowed types */ + allowedTypes?:("string"|"number"|"boolean"|"null"|"array"|"object"|"other")[] +} + +/**## ODCheckerTypeSwitchStructure `class` + * This is an open ticket config checker structure. + * + * This class will switch checkers based on the type of the variable in a config file, customise it in the settings! + */ +export class ODCheckerTypeSwitchStructure extends ODCheckerStructure { + declare options: ODCheckerTypeSwitchStructureOptions + + constructor(id:ODValidId, options:ODCheckerTypeSwitchStructureOptions){ + super(id,options) + } + + check(checker:ODChecker, value:any, locationTrace:ODCheckerLocationTrace): boolean { + const lt = checker.locationTraceDeref(locationTrace) + + if (this.options.all){ + return this.options.all.check(checker,value,lt) + + }else if (this.options.string && typeof value == "string"){ + return this.options.string.check(checker,value,lt) + + }else if (this.options.number && typeof value == "number"){ + return this.options.number.check(checker,value,lt) + + }else if (this.options.boolean && typeof value == "boolean"){ + return this.options.boolean.check(checker,value,lt) + + }else if (this.options.array && Array.isArray(value)){ + return this.options.array.check(checker,value,lt) + + }else if (this.options.null && value === null){ + return this.options.null.check(checker,value,lt) + + }else if (this.options.object && typeof value == "object"){ + return this.options.object.check(checker,value,lt) + + }else if (this.options.other){ + return this.options.other.check(checker,value,lt) + + }else if (this.options.allowedTypes && this.options.allowedTypes.length > 0){ + checker.createMessage("openticket:switch-invalid-type","error",`This needs to be one of the following types: ${this.options.allowedTypes.join(", ")}!`,lt,null,[this.options.allowedTypes.join(", ")],this.id,(this.options.docs ?? null)) + return false + }else return super.check(checker,value,locationTrace) + } +} + +/**## ODCheckerObjectSwitchStructureOptions `interface` + * This interface has the options for `ODCheckerObjectSwitchStructure`! + */ +export interface ODCheckerObjectSwitchStructureOptions extends ODCheckerStructureOptions { + /**An array of object checkers with their name, properties & priority. */ + objects?:{ + /**The properties to match for this checker to be used. */ + properties:{key:string, value:any}[], + /**The name for this object type (used in rendering) */ + name:string, + /**The higher the priority, the earlier this checker will be tested. */ + priority:number, + /**The object checker used once the properties have been matched. */ + checker:ODCheckerObjectStructure + }[] +} + +/**## ODCheckerObjectSwitchStructure `class` + * This is an open ticket config checker structure. + * + * This class will switch object checkers based on a variable match in one of the objects, customise it in the settings! + */ +export class ODCheckerObjectSwitchStructure extends ODCheckerStructure { + declare options: ODCheckerObjectSwitchStructureOptions + + constructor(id:ODValidId, options:ODCheckerObjectSwitchStructureOptions){ + super(id,options) + } + + check(checker:ODChecker, value:object, locationTrace:ODCheckerLocationTrace): boolean { + const lt = checker.locationTraceDeref(locationTrace) + + if (this.options.objects){ + //check type & options + if (typeof value != "object"){ + checker.createMessage("openticket:invalid-type","error","This property needs to be the type: object!",lt,null,["object"],this.id,(this.options.docs ?? null)) + return false + } + + //sort objects + const sortedObjects = this.options.objects.sort((a,b) => { + if (a.priority < b.priority) return -1 + else if (a.priority > b.priority) return 1 + else return 0 + }) + + + //check objects + let localQuit = false + let didSelectObject = false + sortedObjects.forEach((obj) => { + if (!obj.properties.some((p) => value[p.key] !== p.value)){ + didSelectObject = true + if (!obj.checker.check(checker,value,lt)) localQuit = true + } + }) + + //do local quit or check custom function + if (!didSelectObject){ + checker.createMessage("openticket:object-switch-invalid-type","error",`This object needs to be one of the following types: ${this.options.objects.map((obj) => obj.name).join(", ")}!`,lt,null,[this.options.objects.map((obj) => obj.name).join(", ")],this.id,(this.options.docs ?? null)) + return false + }else if (localQuit){ + return false + }else return super.check(checker,value,locationTrace) + }else return super.check(checker,value,locationTrace) + } +} + +/**## ODCheckerEnabledObjectStructureOptions `interface` + * This interface has the options for `ODCheckerEnabledObjectStructure`! + */ +export interface ODCheckerEnabledObjectStructureOptions extends ODCheckerStructureOptions { + /**The name of the property to match the `enabledValue`. */ + property?:string, + /**The value of the property to be enabled. Defaults to `true` */ + enabledValue?:any, + /**The object checker to use once the property has been matched. */ + checker?:ODCheckerObjectStructure +} + +/**## ODCheckerEnabledObjectStructure `class` + * This is an open ticket config checker structure. + * + * This class will enable an object checker based on a variable match in the object, customise it in the settings! + */ +export class ODCheckerEnabledObjectStructure extends ODCheckerStructure { + declare options: ODCheckerEnabledObjectStructureOptions + + constructor(id:ODValidId, options:ODCheckerEnabledObjectStructureOptions){ + super(id,options) + } + + check(checker:ODChecker, value:object, locationTrace:ODCheckerLocationTrace): boolean { + const lt = checker.locationTraceDeref(locationTrace) + + if (typeof value != "object"){ + //value isn't an object + checker.createMessage("openticket:invalid-type","error","This property needs to be the type: object!",lt,null,["object"],this.id,(this.options.docs ?? null)) + return false + + }else if (this.options.property && typeof value[this.options.property] == "undefined"){ + //property doesn't exist + checker.createMessage("openticket:property-missing","error",`The property "${this.options.property}" is mising from this object!`,lt,null,[`"${this.options.property}"`],this.id,(this.options.docs ?? null)) + return false + + }else if (this.options.property && value[this.options.property] === (typeof this.options.enabledValue == "undefined" ? true : this.options.enabledValue)){ + //this object is enabled + if (this.options.checker) return this.options.checker.check(checker,value,lt) + else return super.check(checker,value,locationTrace) + + }else{ + //this object is disabled + if (this.options.property) checker.createMessage("openticket:object-disabled","info",`This object is disabled, enable it using "${this.options.property}"!`,lt,null,[`"${this.options.property}"`],this.id,(this.options.docs ?? null)) + return super.check(checker,value,locationTrace) + } + } +} + +/**## ODCheckerCustomStructure_DiscordId `class` + * This is an open ticket custom checker structure. + * + * This class extends a primitive config checker & adds another layer of checking in the `custom` function. + * You can compare it to a blueprint for a specific checker. + * + * **This custom checker is made for discord ids (channel, user, role, ...)** + */ +export class ODCheckerCustomStructure_DiscordId extends ODCheckerStringStructure { + /**The type of id (used in rendering) */ + readonly type: "role"|"server"|"channel"|"category"|"user"|"member"|"interaction"|"message" + /**Is this id allowed to be empty */ + readonly emptyAllowed: boolean + /**Extra matches (value will also be valid when one of these options match) */ + readonly extraOptions: string[] + + constructor(id:ODValidId, type:"role"|"server"|"channel"|"category"|"user"|"member"|"interaction"|"message", emptyAllowed:boolean, extraOptions:string[], options?:ODCheckerStringStructureOptions){ + //add premade custom structure checker + const newOptions = options ?? {} + newOptions.custom = (checker,value,locationTrace,locationId,locationDocs) => { + const lt = checker.locationTraceDeref(locationTrace) + + if (typeof value != "string") return false + else if ((!emptyAllowed && value.length < 15) || value.length > 50 || !/^[0-9]*$/.test(value)){ + if (!(extraOptions.length > 0 && extraOptions.some((opt) => opt == value))){ + //value is not an id & not one of the extra options + if (extraOptions.length > 0) checker.createMessage("openticket:discord-invalid-id-options","error",`This is an invalid discord ${type} id! You can also use one of these: ${extraOptions.join(", ")}!`,lt,null,[type,extraOptions.join(", ")],this.id,(this.options.docs ?? null)) + else checker.createMessage("openticket:discord-invalid-id","error",`This is an invalid discord ${type} id!`,lt,null,[type],this.id,(this.options.docs ?? null)) + return false + }else return true + } + return true + } + super(id,newOptions) + this.type = type + this.emptyAllowed = emptyAllowed + this.extraOptions = extraOptions + } +} + +/**## ODCheckerCustomStructure_DiscordIdArray `class` + * This is an open ticket custom checker structure. + * + * This class extends a primitive config checker & adds another layer of checking in the `custom` function. + * You can compare it to a blueprint for a specific checker. + * + * **This custom checker is made for discord id arrays (channel, user, role, ...)** + */ +export class ODCheckerCustomStructure_DiscordIdArray extends ODCheckerArrayStructure { + /**The type of id (used in rendering) */ + readonly type: "role"|"server"|"channel"|"category"|"user"|"member"|"interaction"|"message" + /**Extra matches (value will also be valid when one of these options match) */ + readonly extraOptions: string[] + + constructor(id:ODValidId, type:"role"|"server"|"channel"|"category"|"user"|"member"|"interaction"|"message", extraOptions:string[], options?:ODCheckerArrayStructureOptions, idOptions?:ODCheckerStringStructureOptions){ + //add premade custom structure checker + const newOptions = options ?? {} + newOptions.propertyChecker = new ODCheckerCustomStructure_DiscordId(id,type,false,extraOptions,idOptions) + super(id,newOptions) + this.type = type + this.extraOptions = extraOptions + } +} + +/**## ODCheckerCustomStructure_DiscordToken `class` + * This is an open ticket custom checker structure. + * + * This class extends a primitive config checker & adds another layer of checking in the `custom` function. + * You can compare it to a blueprint for a specific checker. + * + * **This custom checker is made for a discord (auth) token** + */ +export class ODCheckerCustomStructure_DiscordToken extends ODCheckerStringStructure { + constructor(id:ODValidId, options?:ODCheckerStringStructureOptions){ + //add premade custom structure checker + const newOptions = options ?? {} + newOptions.custom = (checker,value,locationTrace,locationId,locationDocs) => { + const lt = checker.locationTraceDeref(locationTrace) + + if (typeof value != "string" || !/^[A-Za-z0-9-_\.]+$/.test(value)){ + checker.createMessage("openticket:discord-invalid-token","error","This is an invalid discord token (syntactically)!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + } + return true + } + super(id,newOptions) + } +} + +/**## ODCheckerCustomStructure_DiscordToken `class` + * This is an open ticket custom checker structure. + * + * This class extends a primitive config checker & adds another layer of checking in the `custom` function. + * You can compare it to a blueprint for a specific checker. + * + * **This custom checker is made for a hex color** + */ +export class ODCheckerCustomStructure_HexColor extends ODCheckerStringStructure { + /**When enabled, you are also allowed to use `#fff` instead of `#ffffff` */ + readonly allowShortForm: boolean + /**Allow this hex color to be empty. */ + readonly emptyAllowed: boolean + + constructor(id:ODValidId, allowShortForm:boolean, emptyAllowed:boolean, options?:ODCheckerStringStructureOptions){ + //add premade custom structure checker + const newOptions = options ?? {} + newOptions.custom = (checker,value,locationTrace,locationId,locationDocs) => { + const lt = checker.locationTraceDeref(locationTrace) + + if (typeof value != "string") return false + else if (emptyAllowed && value.length == 0){ + return true + }else if ((!allowShortForm && !/^#[a-f0-9]{6}$/.test(value)) || (allowShortForm && !/^#[a-f0-9]{6}$/.test(value) && !/^#[a-f0-9]{3}$/.test(value))){ + checker.createMessage("openticket:color-invalid","error","This is an invalid hex color!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else return true + } + super(id,newOptions) + this.allowShortForm = allowShortForm + this.emptyAllowed = emptyAllowed + } +} + +/**## ODCheckerCustomStructure_EmojiString `class` + * This is an open ticket custom checker structure. + * + * This class extends a primitive config checker & adds another layer of checking in the `custom` function. + * You can compare it to a blueprint for a specific checker. + * + * **This custom checker is made for an emoji (string)** + */ +export class ODCheckerCustomStructure_EmojiString extends ODCheckerStringStructure { + /**The minimum amount of emojis required (0 to allow empty) */ + readonly minLength: number + /**The maximum amount of emojis allowed */ + readonly maxLength: number + /**Allow custom discord emoji ids (`<:12345678910:emoji_name>`) */ + readonly allowCustomDiscordEmoji: boolean + + constructor(id:ODValidId, minLength:number, maxLength:number, allowCustomDiscordEmoji:boolean, options?:ODCheckerStringStructureOptions){ + //add premade custom structure checker + const newOptions = options ?? {} + newOptions.custom = (checker,value,locationTrace,locationId,locationDocs) => { + const lt = checker.locationTraceDeref(locationTrace) + + if (typeof value != "string") return false + else if ([...value].length < minLength){ + checker.createMessage("openticket:emoji-too-short","error",`This string needs to have at least ${minLength} emoji's!`,lt,null,[maxLength.toString()],this.id,(this.options.docs ?? null)) + return false + }else if ([...value].length > maxLength){ + checker.createMessage("openticket:emoji-too-long","error",`This string needs to have at most ${maxLength} emoji's!`,lt,null,[maxLength.toString()],this.id,(this.options.docs ?? null)) + return false + }else if (!allowCustomDiscordEmoji && //.test(value)){ + checker.createMessage("openticket:emoji-custom","error",`This emoji can't be a custom discord emoji!`,lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (!/^(?:(?:\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])|(?:))*$/.test(value)){ + checker.createMessage("openticket:emoji-invalid","error","This is an invalid emoji!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + } + return true + } + super(id,newOptions) + this.minLength = minLength + this.maxLength = maxLength + this.allowCustomDiscordEmoji = allowCustomDiscordEmoji + } +} + +/**## ODCheckerCustomStructureOptions_UrlString `interface` + * This interface has the options for `ODCheckerCustomStructure_UrlString`! + */ +export interface ODCheckerCustomStructureOptions_UrlString { + /**Allow urls with `http://` instead of `https://` */ + allowHttp?:boolean + /**Allowed hostnames (string or regex) => will match domain + subdomain */ + allowedHostnames?: (string|RegExp)[] + /**Allowed extentions (string) => will match the end of the url (`.png`,`.svg`,...) */ + allowedExtensions?: string[] + /**Allowed paths (string or regex) => will match path + extension (not domain + subdomain) */ + allowedPaths?: (string|RegExp)[], + /**A regex that will be executed on the entire url (including search params, protcol, domain, ...) */ + regex?:RegExp +} + +/**## ODCheckerCustomStructure_UrlString `class` + * This is an open ticket custom checker structure. + * + * This class extends a primitive config checker & adds another layer of checking in the `custom` function. + * You can compare it to a blueprint for a specific checker. + * + * **This custom checker is made for a URL (string)** + */ +export class ODCheckerCustomStructure_UrlString extends ODCheckerStringStructure { + /**The settings for this url */ + readonly urlSettings: ODCheckerCustomStructureOptions_UrlString + /**Is this url allowed to be empty? */ + readonly emptyAllowed: boolean + + constructor(id:ODValidId, emptyAllowed:boolean, urlSettings:ODCheckerCustomStructureOptions_UrlString, options?:ODCheckerStringStructureOptions){ + //add premade custom structure checker + const newOptions = options ?? {} + newOptions.custom = (checker,value,locationTrace,locationId,locationDocs) => { + const lt = checker.locationTraceDeref(locationTrace) + + if (typeof value != "string") return false + else if (emptyAllowed && value.length == 0){ + return true + }else if (!this.#urlIsValid(value)){ + checker.createMessage("openticket:url-invalid","error","This url is invalid!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.urlSettings.allowHttp != "undefined" && !this.urlSettings.allowHttp && !/^(https:\/\/)/.test(value)){ + checker.createMessage("openticket:url-invalid-http","error","This url can only use the https:// protocol!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (!/^(http(s)?:\/\/)/.test(value)){ + checker.createMessage("openticket:url-invalid-protocol","error","This url can only use the http:// & https:// protocols!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.urlSettings.allowedHostnames != "undefined" && !this.#urlHasValidHostname(value,this.urlSettings.allowedHostnames)){ + checker.createMessage("openticket:url-invalid-hostname","error","This url has a disallowed hostname!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.urlSettings.allowedExtensions != "undefined" && !this.#urlHasValidExtension(value,this.urlSettings.allowedExtensions)){ + checker.createMessage("openticket:url-invalid-extension","error",`This url has an invalid extension! Choose between: ${this.urlSettings.allowedExtensions.join(", ")}!"`,lt,null,[this.urlSettings.allowedExtensions.join(", ")],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.urlSettings.allowedPaths != "undefined" && !this.#urlHasValidPath(value,this.urlSettings.allowedPaths)){ + checker.createMessage("openticket:url-invalid-path","error","This url has an invalid path!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (typeof this.urlSettings.regex != "undefined" && !this.urlSettings.regex.test(value)){ + checker.createMessage("openticket:url-invalid","error","This url is invalid!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else return true + } + super(id,newOptions) + this.urlSettings = urlSettings + this.emptyAllowed = emptyAllowed + } + + /**Check for the hostname */ + #urlHasValidHostname(url:string,hostnames:(string|RegExp)[]): boolean { + try { + const hostname = new URL(url).hostname + return hostnames.some((rule) => { + if (typeof rule == "string"){ + return rule == hostname + }else{ + return rule.test(hostname) + } + }) + + }catch{ + return false + } + } + /**Check for the extension */ + #urlHasValidExtension(url:string,extensions:string[]): boolean { + try { + const path = new URL(url).pathname + return extensions.some((rule) => { + return path.endsWith(rule) + }) + }catch{ + return false + } + } + /**Check for the path */ + #urlHasValidPath(url:string,paths:(string|RegExp)[]): boolean { + try { + const path = new URL(url).pathname + return paths.some((rule) => { + if (typeof rule == "string"){ + return rule == path + }else{ + return rule.test(path) + } + }) + }catch{ + return false + } + } + /**Do general syntax check on url */ + #urlIsValid(url:string){ + try { + new URL(url) + return true + }catch{ + return false + } + } +} + +/**## ODCheckerCustomStructure_UniqueId `class` + * This is an open ticket custom checker structure. + * + * This class extends a primitive config checker & adds another layer of checking in the `custom` function. + * You can compare it to a blueprint for a specific checker. + * + * **This custom checker is made for a unique id (per source & scope)** + */ +export class ODCheckerCustomStructure_UniqueId extends ODCheckerStringStructure { + /**The source of this unique id (generally the plugin name or `openticket`) */ + readonly source: string + /**The scope of this unique id (id needs to be unique in this scope) */ + readonly scope: string + + constructor(id:ODValidId, source:string, scope:string, options?:ODCheckerStringStructureOptions){ + //add premade custom structure checker + const newOptions = options ?? {} + newOptions.custom = (checker,value,locationTrace,locationId,locationDocs) => { + const lt = checker.locationTraceDeref(locationTrace) + + if (typeof value != "string") return false + const uniqueArray: string[] = (checker.storage.get(source,scope) === null) ? [] : checker.storage.get(source,scope) + if (uniqueArray.includes(value)){ + //unique id already exists => throw error + checker.createMessage("openticket:id-not-unique","error","This id isn't unique, use another id instead!",lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else{ + //unique id doesn't exists => add to list + uniqueArray.push(value) + checker.storage.set(source,scope,uniqueArray) + return true + } + } + super(id,newOptions) + this.source = source + this.scope = scope + } +} + +/**## ODCheckerCustomStructure_UniqueIdArray `class` + * This is an open ticket custom checker structure. + * + * This class extends a primitive config checker & adds another layer of checking in the `custom` function. + * You can compare it to a blueprint for a specific checker. + * + * **This custom checker is made for a unique id array (per source & scope)** + */ +export class ODCheckerCustomStructure_UniqueIdArray extends ODCheckerArrayStructure { + /**The source to read unique ids (generally the plugin name or `openticket`) */ + readonly source: string + /**The scope to read unique ids (id needs to be unique in this scope) */ + readonly scope: string + /**The scope to push unique ids when used in this array! */ + readonly usedScope: string|null + + constructor(id:ODValidId, source:string, scope:string, usedScope?:string, options?:ODCheckerArrayStructureOptions){ + //add premade custom structure checker + const newOptions = options ?? {} + newOptions.custom = (checker,value,locationTrace,locationId,locationDocs) => { + const lt = checker.locationTraceDeref(locationTrace) + + if (!Array.isArray(value)) return false + const uniqueArray: string[] = (checker.storage.get(source,scope) === null) ? [] : checker.storage.get(source,scope) + + let localQuit = false + value.forEach((id,index) => { + if (typeof id != "string") return + const localLt = checker.locationTraceDeref(lt) + localLt.push(index) + if (uniqueArray.includes(id)){ + //exists + if (usedScope){ + const current: string[] = checker.storage.get(source,usedScope) ?? [] + current.push(id) + checker.storage.set(source,usedScope,current) + } + }else{ + //doesn't exist + checker.createMessage("openticket:id-non-existent","error",`The id "${id}" doesn't exist!`,localLt,null,[`"${id}"`],this.id,(this.options.docs ?? null)) + localQuit = true + } + }) + return !localQuit + } + super(id,newOptions) + this.source = source + this.scope = scope + this.usedScope = usedScope ?? null + } +} + +/*TEMPLATE!!!! +export interface ODCheckerTemplateStructureOptions extends ODCheckerStructureOptions { + +} +export class ODCheckerTemplateStructure extends ODCheckerStructure { + declare options: ODCheckerTemplateStructureOptions + + constructor(id:ODValidId, options:ODCheckerTemplateStructureOptions){ + super(id,options) + } + + check(checker:ODChecker, value:any, locationTrace:ODCheckerLocationTrace): boolean { + const lt = checker.locationTraceDeref(locationTrace) + + return super.check(checker,value,locationTrace) + } +} +*/ +/*CUSTOM TEMPLATE!!!! +export class ODCheckerCustomStructure_Template extends ODCheckerTemplateStructure { + idk: string + + constructor(id:ODValidId, idk:string, options?:ODCheckerStringStructureOptions){ + //add premade custom structure checker + const newOptions = options ?? {} + newOptions.custom = (checker,value,locationTrace,locationId,locationDocs) => { + const lt = checker.locationTraceDeref(locationTrace) + + //do custom check & push error message. Return true if correct + return boolean + } + super(id,newOptions) + this.idk = idk + } +} +*/ \ No newline at end of file diff --git a/src/core/api/modules/client.ts b/src/core/api/modules/client.ts new file mode 100644 index 0000000..c7ca8dc --- /dev/null +++ b/src/core/api/modules/client.ts @@ -0,0 +1,1470 @@ +/////////////////////////////////////// +//DISCORD CLIENT MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODManagerData, ODSystemError, ODValidId } from "./base" +import * as discord from "discord.js" +import { ODConsoleWarningMessage, ODDebugger } from "./console" +import { ODMessageBuildResult, ODMessageBuildSentResult } from "./builder" + +/**## ODClientIntents `type` + * A list of intents required when inviting the bot. + */ +export type ODClientIntents = ("Guilds"|"GuildMembers"|"GuildModeration"|"GuildEmojisAndStickers"|"GuildIntegrations"|"GuildWebhooks"|"GuildInvites"|"GuildVoiceStates"|"GuildPresences"|"GuildMessages"|"GuildMessageReactions"|"GuildMessageTyping"|"DirectMessages"|"DirectMessageReactions"|"DirectMessageTyping"|"MessageContent"|"GuildScheduledEvents"|"AutoModerationConfiguration"|"AutoModerationExecution") +/**## ODClientPriviligedIntents `type` + * A list of priviliged intents required to be enabled in the developer portal. + */ +export type ODClientPriviligedIntents = ("GuildMembers"|"MessageContent"|"Presence") +/**## ODClientPartials `type` + * A list of partials required for the bot to work. (`Message` & `Channel` are for receiving DM messages from uncached channels) + */ +export type ODClientPartials = ("User"|"Channel"|"GuildMember"|"Message"|"Reaction"|"GuildScheduledEvent"|"ThreadMember") +/**## ODClientPermissions `type` + * A list of permissions required in the server that the bot is active in. + */ +export type ODClientPermissions = ("CreateInstantInvite"|"KickMembers"|"BanMembers"|"Administrator"|"ManageChannels"|"ManageGuild"|"AddReactions"|"ViewAuditLog"|"PrioritySpeaker"|"Stream"|"ViewChannel"|"SendMessages"|"SendTTSMessages"|"ManageMessages"|"EmbedLinks"|"AttachFiles"|"ReadMessageHistory"|"MentionEveryone"|"UseExternalEmojis"|"ViewGuildInsights"|"Connect"|"Speak"|"MuteMembers"|"DeafenMembers"|"MoveMembers"|"UseVAD"|"ChangeNickname"|"ManageNicknames"|"ManageRoles"|"ManageWebhooks"|"ManageGuildExpressions"|"UseApplicationCommands"|"RequestToSpeak"|"ManageEvents"|"ManageThreads"|"CreatePublicThreads"|"CreatePrivateThreads"|"UseExternalStickers"|"SendMessagesInThreads"|"UseEmbeddedActivities"|"ModerateMembers"|"ViewCreatorMonetizationAnalytics"|"UseSoundboard"|"UseExternalSounds"|"SendVoiceMessages") + +/**## ODClientManager `class` + * This is an open ticket client manager. + * + * It is responsible for managing the discord.js client. Here, you can set the status, register slash commands and much more! + * + * If you want, you can also listen for custom events on the `ODClientManager.client` variable (`discord.Client`) + */ +export class ODClientManager { + /**Alias to open ticket debugger. */ + #debug: ODDebugger + + /**List of required bot intents. Add intents to this list using the `onClientConfiguration` event. */ + intents: ODClientIntents[] = [] + /**List of required bot privileged intents. Add intents to this list using the `onClientConfiguration` event. */ + privileges: ODClientPriviligedIntents[] = [] + /**List of required bot partials. Add intents to this list using the `onClientConfiguration` event. **❌ Only use when neccessery!** */ + partials: ODClientPartials[] = [] + /**List of required bot permissions. Add permissions to this list using the `onClientConfiguration` event. */ + permissions: ODClientPermissions[] = [] + /**The bot token, empty by default. */ + token: string = "" + + /**The discord.js `discord.Client`. Only use it when initiated! */ + client: discord.Client = new discord.Client({intents:[]}) //temporary client + /**Is the bot initiated? */ + initiated: boolean = false + /**Is the bot logged in? */ + loggedIn: boolean = false + /**Is the bot ready? */ + ready: boolean = false + + /**The main server of the bot. Provided by serverId in the config */ + mainServer: discord.Guild|null = null + /**(❌ DO NOD OVERWRITE ❌) Internal open ticket function to continue the startup when the client is ready! */ + readyListener: (() => Promise)|null = null + /**The status manager is responsible for setting the bot status. */ + activity: ODClientActivityManager + /**The slash command manager is responsible for all slash commands & their events inside the bot. */ + slashCommands: ODSlashCommandManager + /**The text command manager is responsible for all text commands & their events inside the bot. */ + textCommands: ODTextCommandManager + + constructor(debug:ODDebugger){ + this.#debug = debug + this.activity = new ODClientActivityManager(this.#debug,this) + this.slashCommands = new ODSlashCommandManager(this.#debug,this) + this.textCommands = new ODTextCommandManager(this.#debug,this) + } + + /**Initiate the `client` variable & add the intents & partials to the bot. */ + initClient(){ + if (!this.intents.every((value) => typeof discord.GatewayIntentBits[value] != "undefined")) throw new ODSystemError("Client has non-existing intents!") + if (!this.privileges.every((value) => typeof {GuildMembers:true,MessageContent:true,Presence:true}[value] != "undefined")) throw new ODSystemError("Client has non-existing privileged intents!") + if (!this.partials.every((value) => typeof discord.Partials[value] != "undefined")) throw new ODSystemError("Client has non-existing partials!") + if (!this.permissions.every((value) => typeof discord.PermissionFlagsBits[value] != "undefined")) throw new ODSystemError("Client has non-existing partials!") + + const intents = this.intents.map((value) => discord.GatewayIntentBits[value]) + const partials = this.partials.map((value) => discord.Partials[value]) + + const oldClient = this.client + this.client = new discord.Client({intents,partials}) + + //@ts-ignore + oldClient.eventNames().forEach((event:keyof discord.ClientEvents) => { + //@ts-ignore + const callbacks = oldClient.rawListeners(event) + callbacks.forEach((cb:() => void) => { + this.client.on(event,cb) + }) + }) + + this.initiated = true + + this.#debug.debug("Created client with intents: "+this.intents.join(", ")) + this.#debug.debug("Created client with privileged intents: "+this.privileges.join(", ")) + this.#debug.debug("Created client with partials: "+this.partials.join(", ")) + this.#debug.debug("Created client with permissions: "+this.permissions.join(", ")) + } + /**Get all servers the bot is part of. */ + getGuilds(){ + if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!") + if (!this.ready) throw new ODSystemError("Client isn't ready yet!") + + return this.client.guilds.cache.map((guild) => guild) + } + /**Check if the bot is in a specific guild */ + checkBotInGuild(guild:discord.Guild){ + return (guild.members.me) ? true : false + } + /**Check if a specific guild has all required permissions (or `Administrator`) */ + checkGuildPerms(guild:discord.Guild){ + if (!guild.members.me) throw new ODSystemError("Client isn't a member in this server!") + const perms = guild.members.me.permissions + if (perms.has("Administrator")) return true + else{ + return this.permissions.every((perm) => { + return perms.has(perm) + }) + } + } + /**Log-in with a discord auth token. */ + login(): Promise { + return new Promise(async (resolve,reject) => { + if (!this.initiated) reject("Client isn't initiated yet!") + if (!this.token) reject("Client doesn't have a token!") + + try { + this.client.once("ready",async () => { + this.ready = true + + //set slashCommandManager to client applicationCommandManager + if (!this.client.application) throw new ODSystemError("Couldn't get client application! Unable to register slash commands!") + this.slashCommands.commandManager = this.client.application.commands + + if (this.readyListener) await this.readyListener() + resolve(true) + }) + + this.#debug.debug("Actual discord.js client.login()") + await this.client.login(this.token) + this.#debug.debug("Finished discord.js client.login()") + this.loggedIn = true + }catch(err){ + if (err.message.toLowerCase() == "used disallowed intents"){ + process.emit("uncaughtException",new ODSystemError("Used disallowed intents")) + }else reject("login error: "+err) + } + }) + } + /**A simplified shortcut to get a `discord.User` :) */ + async fetchUser(id:string): Promise { + if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!") + if (!this.ready) throw new ODSystemError("Client isn't ready yet!") + + try{ + return await this.client.users.fetch(id) + }catch{ + return null + } + } + /**A simplified shortcut to get a `discord.Guild` :) */ + async fetchGuild(id:string): Promise { + if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!") + if (!this.ready) throw new ODSystemError("Client isn't ready yet!") + + try{ + return await this.client.guilds.fetch(id) + }catch{ + return null + } + } + /**A simplified shortcut to get a `discord.Channel` :) */ + async fetchChannel(id:string): Promise { + if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!") + if (!this.ready) throw new ODSystemError("Client isn't ready yet!") + + try{ + return await this.client.channels.fetch(id) + }catch{ + return null + } + } + /**A simplified shortcut to get a `discord.GuildBasedChannel` :) */ + async fetchGuildChannel(guildId:string|discord.Guild, id:string): Promise { + if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!") + if (!this.ready) throw new ODSystemError("Client isn't ready yet!") + + try{ + const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId) + if (!guild) return null + const channel = await guild.channels.fetch(id) + return channel + }catch{ + return null + } + } + /**A simplified shortcut to get a `discord.TextChannel` :) */ + async fetchGuildTextChannel(guildId:string|discord.Guild, id:string): Promise { + if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!") + if (!this.ready) throw new ODSystemError("Client isn't ready yet!") + + try{ + const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId) + if (!guild) return null + const channel = await guild.channels.fetch(id) + if (!channel || channel.type != discord.ChannelType.GuildText) return null + return channel + }catch{ + return null + } + } + /**A simplified shortcut to get a `discord.CategoryChannel` :) */ + async fetchGuildCategoryChannel(guildId:string|discord.Guild, id:string): Promise { + if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!") + if (!this.ready) throw new ODSystemError("Client isn't ready yet!") + + try{ + const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId) + if (!guild) return null + const channel = await guild.channels.fetch(id) + if (!channel || channel.type != discord.ChannelType.GuildCategory) return null + return channel + }catch{ + return null + } + } + /**A simplified shortcut to get a `discord.GuildMember` :) */ + async fetchGuildMember(guildId:string|discord.Guild, id:string): Promise { + if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!") + if (!this.ready) throw new ODSystemError("Client isn't ready yet!") + if (typeof id != "string") throw new ODSystemError("TEMP ERROR => ODClientManager.fetchGuildMember() => id param isn't string") + + try{ + const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId) + if (!guild) return null + return await guild.members.fetch(id) + }catch{ + return null + } + } + /**A simplified shortcut to get a `discord.Role` :) */ + async fetchGuildRole(guildId:string|discord.Guild, id:string): Promise { + if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!") + if (!this.ready) throw new ODSystemError("Client isn't ready yet!") + if (typeof id != "string") throw new ODSystemError("TEMP ERROR => ODClientManager.fetchGuildRole() => id param isn't string") + + try{ + const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId) + if (!guild) return null + return await guild.roles.fetch(id) + }catch{ + return null + } + } + /**A simplified shortcut to get a `discord.Message` :) */ + async fetchGuildChannelMessage(guildId:string|discord.Guild, channelId:string|discord.TextChannel, id:string): Promise|null> + async fetchGuildChannelMessage(channelId:discord.TextChannel, id:string): Promise|null> + async fetchGuildChannelMessage(guildId:string|discord.Guild|discord.TextChannel, channelId:string|discord.TextChannel|string, id?:string): Promise|null> { + if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!") + if (!this.ready) throw new ODSystemError("Client isn't ready yet!") + + try{ + if (guildId instanceof discord.TextChannel && typeof channelId == "string"){ + const channel = guildId + return await channel.messages.fetch(channelId) + }else if (!(guildId instanceof discord.TextChannel) && id){ + const channel = (channelId instanceof discord.TextChannel) ? channelId : await this.fetchGuildTextChannel(guildId,channelId) + if (!channel) return null + return await channel.messages.fetch(id) + }else return null + }catch{ + return null + } + } + /**A simplified shortcut to send a DM to a user :) */ + async sendUserDm(user:string|discord.User, message:ODMessageBuildResult): Promise> { + if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!") + if (!this.ready) throw new ODSystemError("Client isn't ready yet!") + + try{ + if (user instanceof discord.User){ + if (user.bot) return {success:false,message:null} + const channel = await user.createDM() + const msg = await channel.send(message.message) + return {success:true,message:msg} + }else{ + const newUser = await this.fetchUser(user) + if (!newUser) throw new Error() + if (newUser.bot) return {success:false,message:null} + const channel = await newUser.createDM() + const msg = await channel.send(message.message) + return {success:true,message:msg} + } + }catch{ + try{ + this.#debug.console.log("Failed to send DM to user! ","warning",[ + {key:"id",value:(user instanceof discord.User ? user.id : user)}, + {key:"message",value:message.id.value} + ]) + }catch{} + return {success:false,message:null} + } + } +} + +/**## ODClientActivityType `type` + * Possible activity types for the bot. + */ +export type ODClientActivityType = ("playing"|"listening"|"watching"|"custom"|false) +/**## ODClientPermissions `type` + * Possible activity statuses for the bot. + */ +export type ODClientActivityStatus = ("online"|"invisible"|"idle"|"dnd") + + +/**## ODClientActivityManager `class` + * This is an open ticket client activity manager. + * + * It's responsible for managing the client status. Here, you can set the activity & status of the bot. + * + * It also has a built-in refresh function, so the status will refresh every 10 minutes to keep it visible. + */ +export class ODClientActivityManager { + /**Alias to open ticket debugger. */ + #debug: ODDebugger + + /**Copy of discord.js client */ + manager: ODClientManager + /**The current status type */ + type: ODClientActivityType = false + /**The current status text */ + text: string = "" + /**The current status status */ + status: ODClientActivityStatus = "online" + + /**The timer responsible for refreshing the status. Stop it using `clearInterval(interval)` */ + interval?: NodeJS.Timeout + /**status refresh interval in seconds*/ + refreshInterval: number = 600 + /**Is the status already initiated? */ + initiated: boolean = false + + constructor(debug:ODDebugger, manager:ODClientManager){ + this.#debug = debug + this.manager = manager + } + + /**Update the status. When already initiated, it can take up to 10min to see the updated status in discord. */ + setStatus(type:ODClientActivityType,text:string,status:ODClientActivityStatus){ + this.type = type + this.text = text + this.status = status + } + + /**When initiating the status, the bot starts updating the status using `discord.js`. Returns if succesfull or not. */ + initStatus(): boolean { + if (this.initiated || !this.manager.ready) return false + this.#updateClientActivity(this.type,this.text) + this.interval = setInterval(() => { + this.#updateClientActivity(this.type,this.text) + this.#debug.debug("Client status update cycle") + },this.refreshInterval*1000) + this.initiated = true + this.#debug.debug("Client status initiated") + return true + } + + /**Update the client status */ + #updateClientActivity(type:ODClientActivityType,text:string){ + if (!this.manager.client.user) throw new ODSystemError("Couldn't set client status: client.user == undefined") + if (type == false){ + this.manager.client.user.setActivity() + return + } + this.manager.client.user.setPresence({ + activities:[{ + type:this.#getStatusTypeEnum(type), + state:text, + name:text, + }], + status:this.status + }) + } + /**Get the enum that links to the correct type */ + #getStatusTypeEnum(type:Exclude){ + if (type == "playing") return discord.ActivityType.Playing + else if (type == "listening") return discord.ActivityType.Listening + else if (type == "watching") return discord.ActivityType.Watching + else if (type == "custom") return discord.ActivityType.Custom + else return discord.ActivityType.Listening + } + /**Get the status type (for displaying the status) */ + getStatusType(): "listening "|"playing "|"watching "|"" { + if (this.type == "listening" || this.type == "playing" || this.type == "watching") return this.type+" " as "listening "|"playing "|"watching "|"" + else return "" + } +} + +/**## ODSlashCommandInteractionCallback `type` + * Callback for the slash command interaction listener. + */ +export type ODSlashCommandInteractionCallback = (interaction:discord.ChatInputCommandInteraction,cmd:ODSlashCommand) => void + +/**## ODSlashCommandManager `class` + * This is an open ticket client slash manager. + * + * It's responsible for managing all the slash commands from the client. + * + * Here, you can add & remove slash commands & the bot will do the (de)registering. + */ +export class ODSlashCommandManager extends ODManager { + /**Alias to open ticket debugger. */ + #debug: ODDebugger + + /**Copy of discord.js client. */ + manager: ODClientManager + /**Discord.js application commands manager. */ + commandManager: discord.ApplicationCommandManager|null + /**Collection of all interaction listeners. */ + #interactionListeners: {name:string|RegExp, callback:ODSlashCommandInteractionCallback}[] = [] + /**Set the soft limit for maximum amount of listeners. A warning will be shown when there are more listeners than this limit. */ + listenerLimit: number = 100 + + constructor(debug:ODDebugger, manager:ODClientManager){ + super(debug,"slash command") + this.#debug = debug + this.manager = manager + this.commandManager = (manager.client.application) ? manager.client.application.commands : null + } + + /**Create all commands that don't exist yet. (global by default OR guildId for per-server) */ + async createNewCommands(guildId?:string){ + if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register slash commands!") + const cmds = await this.#existsCmd(guildId) + + let i = 0 + while (i < cmds.nonExisting.length){ + await this.#createCmd(cmds.nonExisting[i]) + this.#debug.debug("Created new slash command",[{key:"id",value:cmds.nonExisting[i].id.value},{key:"name",value:cmds.nonExisting[i].name}]) + i++ + } + } + /**Update all commands that already exist. (global by default OR guildId for per-server) */ + async updateExistingCommands(guildId?:string,force?:boolean){ + if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register slash commands!") + const cmds = await this.#existsCmd(guildId) + const filtered = cmds.existing.filter((cmd) => (cmd.requiresUpdate == true || force)) + + for (const cmd of filtered){ + await this.#createCmd(cmd.cmd) + this.#debug.debug("Updated existing slash command",[ + {key:"id",value:cmd.cmd.id.value}, + {key:"name",value:cmd.cmd.name}, + {key:"force",value:force ? "true" : "false"} + ]) + } + } + /**Remove all commands that exist but aren't used by open ticket. (global by default OR guildId for per-server) */ + async removeUnusedCommands(guildId?:string){ + if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register slash commands!") + const cmds = (await this.#unusedCmd(guildId)).map((cmd) => cmd.name) + await this.#removeCmd(cmds,guildId) + } + /**Create a slash command from an `ODSlashCommand` class */ + async #createCmd(cmd:ODSlashCommand){ + if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register slash commands!") + try { + await this.commandManager.create(cmd.builder,(cmd.guildId ?? undefined)) + }catch(err){ + process.emit("uncaughtException",err) + throw new ODSystemError("Failed to register slash command '/"+cmd.name+"'!") + } + } + /**Remove slash cmds by name */ + async #removeCmd(names:string[],guildId?:string){ + if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register slash commands!") + + const cmds = await this.commandManager.fetch({guildId}) + + cmds?.forEach((cmd) => { + if (!names.includes(cmd.name)) return + cmd.delete() + this.#debug.debug("Removed existing slash command",[{key:"name",value:cmd.name}]) + }) + } + /**Get all existing & non-existing slash commands. */ + async #existsCmd(guildId?:string){ + if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register slash commands!") + + const cmds = await this.commandManager.fetch({guildId}) + const existing: {cmd:ODSlashCommand, requiresUpdate:boolean}[] = [] + const nonExisting: ODSlashCommand[] = [] + + this.getAll().forEach((cmd) => { + if (guildId && cmd.guildId != guildId) return + const result = cmds.find((cmddata) => cmddata.name == cmd.name) + if (result){ + //command already exists (and may need to be updated) + const requiresUpdate = cmd.requiresUpdate ? cmd.requiresUpdate({ + name:result.name, + description:result.description, + options:result.options as readonly discord.ApplicationCommandOptionData[], + nsfw:result.nsfw, + dmPermission:result.dmPermission ?? undefined, + defaultMemberPermissions:result.defaultMemberPermissions, + nameLocalizations:result.nameLocalizations ?? undefined, + descriptionLocalizations:result.descriptionLocalizations ?? undefined + }) : false + existing.push({cmd,requiresUpdate}) + + //command doesn't exists yet + }else nonExisting.push(cmd) + }) + + return {existing,nonExisting} + } + /**Get all unused slash commands. */ + async #unusedCmd(guildId?:string){ + if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register slash commands!") + + const cmds = await this.commandManager.fetch({guildId}) + const unused: discord.ApplicationCommand[] = [] + + const allCommands = this.getAll() + cmds.forEach((cmddata) => { + if (guildId && cmddata.guildId != guildId) return + + if (!allCommands.find((cmd) => { + if (cmd.name != cmddata.name) return false + if (cmd.builder.description != cmddata.description) return false + return this.#checkUnusedOptions(cmddata.options,cmd.builder.options ?? []) + + })) unused.push(cmddata) + }) + + return unused + } + /**Returns `true` if the options are the same in both arrays. It will work recursively! */ + #checkUnusedOptions(currentOptions:readonly discord.ApplicationCommandOption[], newOptions:readonly discord.ApplicationCommandOptionData[]){ + if ((!newOptions || newOptions.length == 0) && currentOptions.length == 0) return true + else if (newOptions && newOptions.length != currentOptions.length) return false + + if (!currentOptions.every((currentOpt,index) => { + const newOpt = newOptions[index] + if (newOpt.name != currentOpt.name) return false + if (newOpt.type != currentOpt.type) return false + if (newOpt.description != currentOpt.description) return false + + if (newOpt.type == discord.ApplicationCommandOptionType.Number && currentOpt.type == discord.ApplicationCommandOptionType.Number){ + //check for min & max values when type is number + if (newOpt.minValue != currentOpt.minValue) return false + if (newOpt.maxValue != currentOpt.maxValue) return false + } + if (newOpt.type == discord.ApplicationCommandOptionType.String && currentOpt.type == discord.ApplicationCommandOptionType.String){ + //check for min & max length values when type is string + if (newOpt.minLength != currentOpt.minLength) return false + if (newOpt.maxLength != currentOpt.maxLength) return false + } + + //check for required normal options + if (currentOpt.type != discord.ApplicationCommandOptionType.SubcommandGroup && currentOpt.type != discord.ApplicationCommandOptionType.Subcommand && newOpt.type != discord.ApplicationCommandOptionType.SubcommandGroup && newOpt.type != discord.ApplicationCommandOptionType.Subcommand){ + if (currentOpt.required != newOpt.required) return false + + //check for sub-options in subcommands + }else if ((currentOpt.type == discord.ApplicationCommandOptionType.SubcommandGroup && newOpt.type == discord.ApplicationCommandOptionType.SubcommandGroup) || (currentOpt.type == discord.ApplicationCommandOptionType.Subcommand && newOpt.type == discord.ApplicationCommandOptionType.Subcommand)){ + if (currentOpt.options && newOpt.options){ + return this.#checkUnusedOptions(currentOpt.options,newOpt.options) + } + } + + return true + })) return false + return true + } + /**Start listening to the discord.js client `interactionCreate` event. */ + startListeningToInteractions(){ + this.manager.client.on("interactionCreate",(interaction) => { + //return when not in main server or DM + if (!this.manager.mainServer || (interaction.guild && interaction.guild.id != this.manager.mainServer.id)) return + + if (!interaction.isChatInputCommand()) return + const cmd = this.getFiltered((cmd) => cmd.name == interaction.commandName)[0] + if (!cmd) return + + this.#interactionListeners.forEach((listener) => { + if (typeof listener.name == "string" && (interaction.commandName != listener.name)) return + else if (listener.name instanceof RegExp && !listener.name.test(interaction.commandName)) return + + //this is a valid listener + listener.callback(interaction,cmd) + }) + }) + } + /**Callback on interaction from one or multiple slash commands. */ + onInteraction(commandName:string|RegExp, callback:ODSlashCommandInteractionCallback){ + this.#interactionListeners.push({ + name:commandName, + callback + }) + + if (this.#interactionListeners.length > this.listenerLimit){ + this.#debug.console.log(new ODConsoleWarningMessage("Possible slash command interaction memory leak detected!",[ + {key:"listeners",value:this.#interactionListeners.length.toString()} + ])) + } + } +} + +/**## ODSlashCommandBuilder `interface` + * The builder for slash commands. Here you can add options to the command. + */ +export interface ODSlashCommandBuilder extends discord.ChatInputApplicationCommandData {} + +/**## ODSlashCommandUpdateFunction `type` + * The function responsible for updating slash commands when they already exist. + */ +export type ODSlashCommandUpdateFunction = (current:ODSlashCommandBuilder) => boolean + +/**## ODSlashCommand `class` + * This is an open ticket slash command. + * + * When registered, you can listen for this command using the `ODCommandResponder`. The advantages of using this class for creating a slash command are: + * - automatic option parsing (even for channels, users, roles & mentions)! + * - automatic registration in discord.js + * - error reporting to the user when the bot fails to respond + * - plugins can extend this command + * - the bot won't re-register the command when it already exists (except when requested)! + * + * And more! + */ +export class ODSlashCommand extends ODManagerData { + /**The discord.js builder for this slash command. */ + builder: ODSlashCommandBuilder + /**The name of this slash command. */ + name: string + /**The id of the guild this command is for. Null when not set. */ + guildId: string|null + /**Function to check if the slash command requires to be updated (when it already exists). */ + requiresUpdate: ODSlashCommandUpdateFunction|null = null + + constructor(id:ODValidId, builder:ODSlashCommandBuilder, requiresUpdate?:ODSlashCommandUpdateFunction, guildId?:string){ + super(id) + if (builder.type != discord.ApplicationCommandType.ChatInput) throw new ODSystemError("ApplicationCommandData is required to be the 'ChatInput' type!") + + this.builder = builder + this.name = builder.name + this.guildId = guildId ?? null + this.requiresUpdate = requiresUpdate ?? null + } +} + +/**## ODTextCommandBuilderBaseOptionType `type` + * The types available in the text command option builder. + */ +export type ODTextCommandBuilderBaseOptionType = "string"|"number"|"boolean"|"user"|"guildmember"|"role"|"mentionable"|"channel" + +/**## ODTextCommandBuilderBaseOption `interface` + * The default option builder for text commands. + */ +export interface ODTextCommandBuilderBaseOption { + /**The name of this option */ + name:string, + /**The type of this option */ + type:ODTextCommandBuilderBaseOptionType, + /**Is this option required? (optional options can only exist at the end of the command!) */ + required?:boolean +} + +/**## ODTextCommandBuilderStringOption `interface` + * The string option builder for text commands. + */ +export interface ODTextCommandBuilderStringOption extends ODTextCommandBuilderBaseOption { + type:"string", + /**Set the maximum length of this string */ + maxLength?:number, + /**Set the minimum length of this string */ + minLength?:number, + /**The string needs to match this regex or it will be invalid */ + regex?:RegExp, + /**The string needs to match one of these choices or it will be invalid */ + choices?:string[], + /**When this is the last option, allow this string to contain spaces */ + allowSpaces?:boolean +} + +/**## ODTextCommandBuilderNumberOption `interface` + * The number option builder for text commands. + */ +export interface ODTextCommandBuilderNumberOption extends ODTextCommandBuilderBaseOption { + type:"number", + /**The number can't be higher than this value */ + max?:number, + /**The number can't be lower than this value */ + min?:number, + /**Allow the number to be negative */ + allowNegative?:boolean, + /**Allow the number to be positive */ + allowPositive?:boolean, + /**Allow the number to be zero */ + allowZero?:boolean, + /**Allow a number with decimal */ + allowDecimal?:boolean +} + +/**## ODTextCommandBuilderBooleanOption `interface` + * The boolean option builder for text commands. + */ +export interface ODTextCommandBuilderBooleanOption extends ODTextCommandBuilderBaseOption { + type:"boolean", + /**The value when `true` */ + trueValue?:string, + /**The value when `false` */ + falseValue?:string +} + +/**## ODTextCommandBuilderChannelOption `interface` + * The channel option builder for text commands. + */ +export interface ODTextCommandBuilderChannelOption extends ODTextCommandBuilderBaseOption { + type:"channel", + /**When specified, only allow the following channel types */ + channelTypes?:discord.GuildChannelType[] +} + +/**## ODTextCommandBuilderRoleOption `interface` + * The role option builder for text commands. + */ +export interface ODTextCommandBuilderRoleOption extends ODTextCommandBuilderBaseOption { + type:"role" +} + +/**## ODTextCommandBuilderUserOption `interface` + * The user option builder for text commands. + */ +export interface ODTextCommandBuilderUserOption extends ODTextCommandBuilderBaseOption { + type:"user" +} + +/**## ODTextCommandBuilderGuildMemberOption `interface` + * The guild member option builder for text commands. + */ +export interface ODTextCommandBuilderGuildMemberOption extends ODTextCommandBuilderBaseOption { + type:"guildmember" +} + +/**## ODTextCommandBuilderMentionableOption `interface` + * The mentionable option builder for text commands. + */ +export interface ODTextCommandBuilderMentionableOption extends ODTextCommandBuilderBaseOption { + type:"mentionable" +} + +/**## ODTextCommandBuilderOption `type` + * The option builder for text commands. + */ +export type ODTextCommandBuilderOption = ( + ODTextCommandBuilderStringOption| + ODTextCommandBuilderBooleanOption| + ODTextCommandBuilderNumberOption| + ODTextCommandBuilderChannelOption| + ODTextCommandBuilderRoleOption| + ODTextCommandBuilderUserOption| + ODTextCommandBuilderGuildMemberOption| + ODTextCommandBuilderMentionableOption +) + +/**## ODTextCommandBuilder `interface` + * The builder for text commands. Here you can add options to the command. + */ +export interface ODTextCommandBuilder { + /**The prefix of this command */ + prefix:string, + /**The name of this command (can include spaces for subcommands) */ + name:string, + /**Is this command allowed in dm? */ + dmPermission?:boolean, + /**Is this command allowed in guilds? */ + guildPermission?:boolean, + /**When specified, only allow this command to be executed in the following guilds */ + allowedGuildIds?:string[], + /**Are bots allowed to execute this command? */ + allowBots?:boolean + /**The options for this text command (like slash commands) */ + options?:ODTextCommandBuilderOption[] +} + +/**## ODTextCommand `class` + * This is an open ticket text command. + * + * When registered, you can listen for this command using the `ODCommandResponder`. The advantages of using this class for creating a text command are: + * - automatic option parsing (even for channels, users, roles & mentions)! + * - automatic errors on invalid parameters + * - error reporting to the user when the bot fails to respond + * - plugins can extend this command + * + * And more! + */ +export class ODTextCommand extends ODManagerData { + /**The builder for this slash command. */ + builder: ODTextCommandBuilder + /**The name of this slash command. */ + name: string + + constructor(id:ODValidId, builder:ODTextCommandBuilder){ + super(id) + this.builder = builder + this.name = builder.name + } +} + +/**## ODTextCommandInteractionOptionBase `interface` + * The object returned for options from a text command interaction. + */ +export interface ODTextCommandInteractionOptionBase { + /**The name of this option */ + name:string, + /**The type of this option */ + type:Name, + /**The value of this option */ + value:Type +} + +/**## ODTextCommandInteractionOption `type` + * A list of types returned for options from a text command interaction. + */ +export type ODTextCommandInteractionOption = ( + ODTextCommandInteractionOptionBase<"string",string>| + ODTextCommandInteractionOptionBase<"number",number>| + ODTextCommandInteractionOptionBase<"boolean",boolean>| + ODTextCommandInteractionOptionBase<"channel",discord.GuildBasedChannel>| + ODTextCommandInteractionOptionBase<"role",discord.Role>| + ODTextCommandInteractionOptionBase<"user",discord.User>| + ODTextCommandInteractionOptionBase<"guildmember",discord.GuildMember>| + ODTextCommandInteractionOptionBase<"mentionable",discord.Role|discord.User> +) + +/**## ODTextCommandInteractionCallback `type` + * Callback for the text command interaction listener. + */ +export type ODTextCommandInteractionCallback = (msg:discord.Message, cmd:ODTextCommand, options:ODTextCommandInteractionOption[]) => void + +/**## ODTextCommandErrorBase `interface` + * The object returned from a text command error callback. + */ +export interface ODTextCommandErrorBase { + /**The type of text command error */ + type:"unknown_prefix"|"unknown_command"|"invalid_option"|"missing_option", + /**The message this error originates from */ + msg:discord.Message +} + +/**## ODTextCommandErrorUnknownPrefix `interface` + * The object returned from a text command unknown prefix error callback. + */ +export interface ODTextCommandErrorUnknownPrefix extends ODTextCommandErrorBase { + type:"unknown_prefix" +} + +/**## ODTextCommandErrorUnknownCommand `interface` + * The object returned from a text command unknown command error callback. + */ +export interface ODTextCommandErrorUnknownCommand extends ODTextCommandErrorBase { + type:"unknown_command" +} + +/**## ODTextCommandErrorInvalidOptionReason `type` + * A list of reasons for the invalid_option error to be thrown. + */ +export type ODTextCommandErrorInvalidOptionReason = ( + "boolean"| + "number_max"| + "number_min"| + "number_decimal"| + "number_negative"| + "number_positive"| + "number_zero"| + "number_invalid"| + "string_max_length"| + "string_min_length"| + "string_regex"| + "string_choice"| + "not_in_guild"| + "channel_not_found"| + "channel_type"| + "user_not_found"| + "member_not_found"| + "role_not_found"| + "mentionable_not_found" +) + +/**## ODTextCommandErrorInvalidOption `interface` + * The object returned from a text command invalid option error callback. + */ +export interface ODTextCommandErrorInvalidOption extends ODTextCommandErrorBase { + type:"invalid_option", + /**The command this error originates from */ + command:ODTextCommand, + /**The command prefix this error originates from */ + prefix:string, + /**The command name this error originates from (can include spaces for subcommands) */ + name:string, + /**The option that this error originates from */ + option:ODTextCommandBuilderOption + /**The location that this option was found */ + location:number, + /**The current value of this invalid option */ + value:string, + /**The reason for this invalid option */ + reason:ODTextCommandErrorInvalidOptionReason +} + +/**## ODTextCommandErrorMissingOption `interface` + * The object returned from a text command missing option error callback. + */ +export interface ODTextCommandErrorMissingOption extends ODTextCommandErrorBase { + type:"missing_option", + /**The command this error originates from */ + command:ODTextCommand, + /**The command prefix this error originates from */ + prefix:string, + /**The command name this error originates from (can include spaces for subcommands) */ + name:string, + /**The option that this error originates from */ + option:ODTextCommandBuilderOption + /**The location that this option was found */ + location:number +} + +/**## ODTextCommandError `type` + * A list of types returned for errors from a text command interaction. + */ +export type ODTextCommandError = ( + ODTextCommandErrorUnknownPrefix| + ODTextCommandErrorUnknownCommand| + ODTextCommandErrorInvalidOption| + ODTextCommandErrorMissingOption +) + +/**## ODTextCommandErrorCallback `type` + * Callback for the text command error listener. + */ +export type ODTextCommandErrorCallback = (error:ODTextCommandError) => void + +/**## ODTextCommandManager `class` + * This is an open ticket client text manager. + * + * It's responsible for managing all the text commands from the client. + * + * Here, you can add & remove text commands & the bot will do the (de)registering. + */ +export class ODTextCommandManager extends ODManager { + /**Alias to open ticket debugger. */ + #debug: ODDebugger + /**Copy of discord.js client. */ + manager: ODClientManager + /**Collection of all interaction listeners. */ + #interactionListeners: {prefix:string, name:string|RegExp, callback:ODTextCommandInteractionCallback}[] = [] + /**Collection of all error listeners. */ + #errorListeners: ODTextCommandErrorCallback[] = [] + /**Set the soft limit for maximum amount of listeners. A warning will be shown when there are more listeners than this limit. */ + listenerLimit: number = 100 + + constructor(debug:ODDebugger, manager:ODClientManager){ + super(debug,"text command") + this.#debug = debug + this.manager = manager + } + + /*Check if a message is a registered command. */ + async #checkMessage(msg:discord.Message){ + if (this.manager.client.user && msg.author.id == this.manager.client.user.id) return false + + //filter commands for correct prefix + const validPrefixCommands: {cmd:ODTextCommand,newContent:string}[] = [] + this.getAll().forEach((cmd) => { + if (msg.content.startsWith(cmd.builder.prefix)) validPrefixCommands.push({ + cmd:cmd, + newContent:msg.content.substring(cmd.builder.prefix.length) + }) + }) + + //return when no command with prefix + if (validPrefixCommands.length == 0){ + this.#errorListeners.forEach((cb) => cb({ + type:"unknown_prefix", + msg:msg + })) + return false + } + + //filter commands for correct name + const validNameCommands: {cmd:ODTextCommand,newContent:string}[] = [] + validPrefixCommands.forEach((cmd) => { + if (cmd.newContent.startsWith(cmd.cmd.builder.name)) validNameCommands.push({ + cmd:cmd.cmd, + newContent:cmd.newContent.substring(cmd.cmd.builder.name.length+1) //+1 because of space after command name + }) + }) + + //return when no command with name + if (validNameCommands.length == 0){ + this.#errorListeners.forEach((cb) => cb({ + type:"unknown_command", + msg:msg + })) + return false + } + + //the final command + const command = validNameCommands[0] + const builder = command.cmd.builder + + //check additional options + if (typeof builder.allowBots != "undefined" && !builder.allowBots && msg.author.bot) return false + else if (typeof builder.dmPermission != "undefined" && !builder.dmPermission && msg.channel.type == discord.ChannelType.DM) return false + else if (typeof builder.guildPermission != "undefined" && !builder.guildPermission && msg.guild) return false + else if (typeof builder.allowedGuildIds != "undefined" && msg.guild && !builder.allowedGuildIds.includes(msg.guild.id)) return false + + //check all command options & return when incorrect + const options = await this.#checkOptions(command.cmd,command.newContent,msg) + if (!options.valid) return false + + //a command matched this message => emit event + this.#interactionListeners.forEach((listener) => { + if (typeof listener.prefix == "string" && (command.cmd.builder.prefix != listener.prefix)) return + if (typeof listener.name == "string" && (command.cmd.name.split(" ")[0] != listener.name)) return + else if (listener.name instanceof RegExp && !listener.name.test(command.cmd.name.split(" ")[0])) return + + //this is a valid listener + listener.callback(msg,command.cmd,options.data) + }) + return true + } + /**Check if all options of a command are correct. */ + async #checkOptions(cmd:ODTextCommand, newContent:string, msg:discord.Message){ + const options = cmd.builder.options + if (!options) return {valid:true,data:[]} + + let tempContent = newContent + let optionInvalid = false + const optionData: ODTextCommandInteractionOption[] = [] + + const optionError = (type:"invalid_option"|"missing_option", option:ODTextCommandBuilderOption, location:number, value?:string, reason?:ODTextCommandErrorInvalidOptionReason) => { + //ERROR INVALID + if (type == "invalid_option" && value && reason){ + this.#errorListeners.forEach((cb) => cb({ + type:"invalid_option", + msg:msg, + prefix:cmd.builder.prefix, + command:cmd, + name:cmd.builder.name, + option, + location, + value, + reason + })) + }else if (type == "missing_option"){ + this.#errorListeners.forEach((cb) => cb({ + type:"missing_option", + msg:msg, + prefix:cmd.builder.prefix, + command:cmd, + name:cmd.builder.name, + option, + location + })) + } + optionInvalid = true + } + + for (let location = 0;location < options.length;location++){ + const option = options[location] + if (optionInvalid) break + + //CHECK BOOLEAN + if (option.type == "boolean"){ + const falseValue = option.falseValue ?? "false" + const trueValue = option.trueValue ?? "true" + + if (tempContent.startsWith(falseValue+" ")){ + //FALSE VALUE + optionData.push({ + name:option.name, + type:"boolean", + value:false + }) + tempContent = tempContent.substring(falseValue.length+1) + + }else if (tempContent.startsWith(trueValue+" ")){ + //TRUE VALUE + optionData.push({ + name:option.name, + type:"boolean", + value:true + }) + tempContent = tempContent.substring(trueValue.length+1) + + }else if (option.required){ + //REQUIRED => ERROR IF NOD EXISTING + const invalidregex = /^[^ ]+/ + const invalidRes = invalidregex.exec(tempContent) + if (invalidRes) optionError("invalid_option",option,location,invalidRes[0],"boolean") + else optionError("missing_option",option,location) + } + + //CHECK NUMBER + }else if (option.type == "number"){ + const numRegex = /^[0-9\.\,]+/ + const res = numRegex.exec(tempContent) + if (res){ + const value = res[0].replace(/\,/g,".") + tempContent = tempContent.substring(value.length+1) + const numValue = Number(value) + + if (isNaN(numValue)){ + optionError("invalid_option",option,location,value,"number_invalid") + + }else if (typeof option.allowDecimal == "boolean" && !option.allowDecimal && (numValue % 1) !== 0){ + optionError("invalid_option",option,location,value,"number_decimal") + + }else if (typeof option.allowNegative == "boolean" && !option.allowNegative && numValue < 0){ + optionError("invalid_option",option,location,value,"number_negative") + + }else if (typeof option.allowPositive == "boolean" && !option.allowPositive && numValue > 0){ + optionError("invalid_option",option,location,value,"number_positive") + + }else if (typeof option.allowZero == "boolean" && !option.allowZero && numValue == 0){ + optionError("invalid_option",option,location,value,"number_zero") + + }else if (typeof option.max == "number" && numValue > option.max){ + optionError("invalid_option",option,location,value,"number_max") + + }else if (typeof option.min == "number" && numValue < option.min){ + optionError("invalid_option",option,location,value,"number_min") + + }else{ + //VALID NUMBER + optionData.push({ + name:option.name, + type:"number", + value:numValue + }) + } + }else if (option.required){ + //REQUIRED => ERROR IF NOD EXISTING + const invalidRegex = /^[^ ]+/ + const invalidRes = invalidRegex.exec(tempContent) + if (invalidRes) optionError("invalid_option",option,location,invalidRes[0],"number_invalid") + else optionError("missing_option",option,location) + } + //CHECK STRING + }else if (option.type == "string"){ + if (option.allowSpaces){ + //STRING WITH SPACES + const value = tempContent + tempContent = "" + + if (typeof option.minLength == "number" && value.length < option.minLength){ + optionError("invalid_option",option,location,value,"string_min_length") + + }else if (typeof option.maxLength == "number" && value.length > option.maxLength){ + optionError("invalid_option",option,location,value,"string_max_length") + + }else if (option.regex && !option.regex.test(value)){ + optionError("invalid_option",option,location,value,"string_regex") + + }else if (option.choices && !option.choices.includes(value)){ + optionError("invalid_option",option,location,value,"string_choice") + + }else if (option.required && value === ""){ + //REQUIRED => ERROR IF NOD EXISTING + optionError("missing_option",option,location) + + }else{ + //VALID STRING + optionData.push({ + name:option.name, + type:"string", + value + }) + } + }else{ + //STRING WITHOUT SPACES + const stringRegex = /^[^ ]+/ + const res = stringRegex.exec(tempContent) + if (res){ + const value = res[0] + tempContent = tempContent.substring(value.length+1) + + if (typeof option.minLength == "number" && value.length < option.minLength){ + optionError("invalid_option",option,location,value,"string_min_length") + + }else if (typeof option.maxLength == "number" && value.length > option.maxLength){ + optionError("invalid_option",option,location,value,"string_max_length") + + }else if (option.regex && !option.regex.test(value)){ + optionError("invalid_option",option,location,value,"string_regex") + + }else if (option.choices && !option.choices.includes(value)){ + optionError("invalid_option",option,location,value,"string_choice") + + }else{ + //VALID STRING + optionData.push({ + name:option.name, + type:"string", + value + }) + } + }else if (option.required){ + //REQUIRED => ERROR IF NOD EXISTING + optionError("missing_option",option,location) + } + } + //CHECK CHANNEL + }else if (option.type == "channel"){ + const channelRegex = /^(?:<#)?([0-9]+)>?/ + const res = channelRegex.exec(tempContent) + if (res){ + const value = res[0] + tempContent = tempContent.substring(value.length+1) + const channelId = res[1] + + if (!msg.guild){ + optionError("invalid_option",option,location,value,"not_in_guild") + }else{ + try{ + const channel = await msg.guild.channels.fetch(channelId) + if (!channel){ + optionError("invalid_option",option,location,value,"channel_not_found") + + }else if (option.channelTypes && !option.channelTypes.includes(channel.type)){ + optionError("invalid_option",option,location,value,"channel_type") + + }else{ + //VALID CHANNEL + optionData.push({ + name:option.name, + type:"channel", + value:channel + }) + } + }catch{ + optionError("invalid_option",option,location,value,"channel_not_found") + } + } + }else if (option.required){ + //REQUIRED => ERROR IF NOD EXISTING + const invalidRegex = /^[^ ]+/ + const invalidRes = invalidRegex.exec(tempContent) + if (invalidRes) optionError("invalid_option",option,location,invalidRes[0],"channel_not_found") + else optionError("missing_option",option,location) + } + //CHECK ROLE + }else if (option.type == "role"){ + const roleRegex = /^(?:<@&)?([0-9]+)>?/ + const res = roleRegex.exec(tempContent) + if (res){ + const value = res[0] + tempContent = tempContent.substring(value.length+1) + const roleId = res[1] + + if (!msg.guild){ + optionError("invalid_option",option,location,value,"not_in_guild") + }else{ + try{ + const role = await msg.guild.roles.fetch(roleId) + if (!role){ + optionError("invalid_option",option,location,value,"role_not_found") + }else{ + //VALID ROLE + optionData.push({ + name:option.name, + type:"role", + value:role + }) + } + }catch{ + optionError("invalid_option",option,location,value,"role_not_found") + } + } + }else if (option.required){ + //REQUIRED => ERROR IF NOD EXISTING + const invalidRegex = /^[^ ]+/ + const invalidRes = invalidRegex.exec(tempContent) + if (invalidRes) optionError("invalid_option",option,location,invalidRes[0],"role_not_found") + else optionError("missing_option",option,location) + } + //CHECK GUILD MEMBER + }else if (option.type == "guildmember"){ + const memberRegex = /^(?:<@)?([0-9]+)>?/ + const res = memberRegex.exec(tempContent) + if (res){ + const value = res[0] + tempContent = tempContent.substring(value.length+1) + const memberId = res[1] + + if (!msg.guild){ + optionError("invalid_option",option,location,value,"not_in_guild") + }else{ + try{ + const member = await msg.guild.members.fetch(memberId) + if (!member){ + optionError("invalid_option",option,location,value,"member_not_found") + }else{ + //VALID GUILD MEMBER + optionData.push({ + name:option.name, + type:"guildmember", + value:member + }) + } + }catch{ + optionError("invalid_option",option,location,value,"member_not_found") + } + } + }else if (option.required){ + //REQUIRED => ERROR IF NOD EXISTING + const invalidRegex = /^[^ ]+/ + const invalidRes = invalidRegex.exec(tempContent) + if (invalidRes) optionError("invalid_option",option,location,invalidRes[0],"member_not_found") + else optionError("missing_option",option,location) + } + //CHECK USER + }else if (option.type == "user"){ + const userRegex = /^(?:<@)?([0-9]+)>?/ + const res = userRegex.exec(tempContent) + if (res){ + const value = res[0] + tempContent = tempContent.substring(value.length+1) + const userId = res[1] + + try{ + const user = await this.manager.client.users.fetch(userId) + if (!user){ + optionError("invalid_option",option,location,value,"user_not_found") + }else{ + //VALID USER + optionData.push({ + name:option.name, + type:"user", + value:user + }) + } + }catch{ + optionError("invalid_option",option,location,value,"user_not_found") + } + }else if (option.required){ + //REQUIRED => ERROR IF NOD EXISTING + const invalidRegex = /^[^ ]+/ + const invalidRes = invalidRegex.exec(tempContent) + if (invalidRes) optionError("invalid_option",option,location,invalidRes[0],"user_not_found") + else optionError("missing_option",option,location) + } + //CHECK MENTIONABLE + }else if (option.type == "mentionable"){ + const mentionableRegex = /^<(@&?)([0-9]+)>/ + const res = mentionableRegex.exec(tempContent) + if (res){ + const value = res[0] + const type = (res[1] == "@&") ? "role" : "user" + tempContent = tempContent.substring(value.length+1) + const mentionableId = res[2] + + if (!msg.guild){ + optionError("invalid_option",option,location,value,"not_in_guild") + }else if (type == "role"){ + try { + const role = await msg.guild.roles.fetch(mentionableId) + if (!role){ + optionError("invalid_option",option,location,value,"mentionable_not_found") + }else{ + //VALID ROLE + optionData.push({ + name:option.name, + type:"mentionable", + value:role + }) + } + }catch{ + optionError("invalid_option",option,location,value,"mentionable_not_found") + } + }else if (type == "user"){ + try{ + const user = await this.manager.client.users.fetch(mentionableId) + if (!user){ + optionError("invalid_option",option,location,value,"mentionable_not_found") + }else{ + //VALID USER + optionData.push({ + name:option.name, + type:"mentionable", + value:user + }) + } + }catch{ + optionError("invalid_option",option,location,value,"mentionable_not_found") + } + } + }else if (option.required){ + //REQUIRED => ERROR IF NOD EXISTING + const invalidRegex = /^[^ ]+/ + const invalidRes = invalidRegex.exec(tempContent) + if (invalidRes) optionError("invalid_option",option,location,invalidRes[0],"mentionable_not_found") + else optionError("missing_option",option,location) + } + } + } + return {valid:!optionInvalid,data:optionData} + } + /**Start listening to the discord.js client `messageCreate` event. */ + startListeningToInteractions(){ + this.manager.client.on("messageCreate",(msg) => { + //return when not in main server or DM + if (!this.manager.mainServer || (msg.guild && msg.guild.id != this.manager.mainServer.id)) return + this.#checkMessage(msg) + }) + } + /**Check if optional values are only present at the end of the command. */ + #checkBuilderOptions(builder:ODTextCommandBuilder): {valid:boolean,reason:"required_after_optional"|"allowspaces_not_last"|null} { + let optionalVisited = false + let valid = true + let reason: "required_after_optional"|"allowspaces_not_last"|null = null + if (!builder.options) return {valid:true,reason:null} + builder.options.forEach((opt,index,list) => { + if (!opt.required) optionalVisited = true + if (optionalVisited && opt.required){ + valid = false + reason = "required_after_optional" + } + + if (opt.type == "string" && opt.allowSpaces && ((index+1) != list.length)){ + valid = false + reason = "allowspaces_not_last" + } + }) + + return {valid,reason} + } + /**Callback on interaction from one of the registered text commands */ + onInteraction(commandPrefix:string,commandName:string|RegExp, callback:ODTextCommandInteractionCallback){ + this.#interactionListeners.push({ + prefix:commandPrefix, + name:commandName, + callback + }) + + if (this.#interactionListeners.length > this.listenerLimit){ + this.#debug.console.log(new ODConsoleWarningMessage("Possible text command interaction memory leak detected!",[ + {key:"listeners",value:this.#interactionListeners.length.toString()} + ])) + } + } + /**Callback on error from all the registered text commands */ + onError(callback:ODTextCommandErrorCallback){ + this.#errorListeners.push(callback) + } + + add(data:ODTextCommand, overwrite?:boolean): boolean { + const checkResult = this.#checkBuilderOptions(data.builder) + if (!checkResult.valid && checkResult.reason == "required_after_optional") throw new ODSystemError("Invalid text command '"+data.id.value+"' => optional options are only allowed at the end of a command!") + else if (!checkResult.valid && checkResult.reason == "allowspaces_not_last") throw new ODSystemError("Invalid text command '"+data.id.value+"' => string option with 'allowSpaces' is only allowed at the end of a command!") + else return super.add(data,overwrite) + } +} \ No newline at end of file diff --git a/src/core/api/modules/code.ts b/src/core/api/modules/code.ts new file mode 100644 index 0000000..08cb395 --- /dev/null +++ b/src/core/api/modules/code.ts @@ -0,0 +1,55 @@ +/////////////////////////////////////// +//CODE MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODManagerData, ODValidId } from "./base" +import { ODDebugger } from "./console" + + +/**## ODCode `class` + * This is an open ticket code runner. + * + * It is just a function that will run just before the bot has started completely! + * This can be used for code that needs to run at startup, but isn't really time dependent. + * + * - It has an `id` for identification of the function + * - A `priority` to know when to execute this function (related to others) + */ +export class ODCode extends ODManagerData { + /**The priority of this code */ + priority: number + /**The main function of this code */ + func: () => void|Promise + + constructor(id:ODValidId, priority:number, func:() => void|Promise){ + super(id) + this.priority = priority + this.func = func + } +} + +/**## ODCodeManager `class` + * This is an open ticket code manager. + * + * It manages & executes `ODCode`'s in the correct order. + * + * You will probably register a function/code in this class for something that doesn't really need to be timed. + */ +export class ODCodeManager extends ODManager { + constructor(debug:ODDebugger){ + super(debug,"code") + } + + /**Execute all functions or code. */ + async execute(){ + const derefArray = [...this.getAll()] + const workers = derefArray.sort((a,b) => b.priority-a.priority) + + for (const worker of workers){ + try { + await worker.func() + }catch(err){ + process.emit("uncaughtException",err) + } + } + } +} \ No newline at end of file diff --git a/src/core/api/modules/config.ts b/src/core/api/modules/config.ts new file mode 100644 index 0000000..95edbb2 --- /dev/null +++ b/src/core/api/modules/config.ts @@ -0,0 +1,96 @@ +/////////////////////////////////////// +//CONFIG MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODManagerData, ODSystemError, ODValidId } from "./base" +import nodepath from "path" +import { ODDebugger } from "./console" + +/**## ODConfigManager `class` + * This is an open ticket config manager. + * + * It manages all config files in the bot and allows plugins to access config files from open ticket & other plugins! + * + * You will use this class to get/add a config file (`ODConfig`) in your plugin! + * @example + * //get ./config/general.json => ODConfig class + * const generalConfig = openticket.configs.get("openticket:general") + * + * //add a new config with id "test" => ./config/test.json + * const testConfig = new api.ODConfig("test","test.json") + * openticket.configs.add(testConfig) + */ +export class ODConfigManager extends ODManager { + constructor(debug:ODDebugger){ + super(debug,"config") + } + + /**Add data to the manager. The id will be fetched from the data class! You can optionally select to overwrite existing data! + * @example + * //add a new config with id "test" => ./config/test.json + * const testConfig = new api.ODConfig("test","test.json") + * openticket.configs.add(testConfig) + */ + add(data:ODConfig, overwrite?:boolean): boolean { + return super.add(data,overwrite) + } + /**Get data that matches the `ODId`. Returns the found data. + * @example + * //get "./config/general.json" (ot-general) => ODConfig class + * const generalConfig = openticket.configs.get("openticket:general") + */ + get(id:ODValidId): ODConfig|null { + return super.get(id) + } + /**Remove data that matches the `ODId`. Returns the removed data. + * @example + * //remove the "test" config + * openticket.configs.remove("test") //returns null if non-existing + */ + remove(id:ODValidId): ODConfig|null { + return super.remove(id) + } + /**Check if data that matches the `ODId` exists. Returns a boolean. + * @example + * //check if "./config/general.json" (ot-general) exists => boolean + * const exists = openticket.configs.exists("openticket:general") + */ + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODConfig `class` + * This is an open ticket config helper. + * This class doesn't do anything at all, it just gives a template & basic methods for a config. Use `ODJsonConfig` instead! + * + * You will only use this class if you want to create your own config implementation (e.g. `yml`, `xml`,...)! + */ +export class ODConfig extends ODManagerData { + /**The full path to this config with extension */ + file: string = "" + /**An object/array of the entire config file! Variables inside it can be edited while the bot is running! */ + data: any = undefined +} + +/**## ODJsonConfig `class` + * This is an open ticket config helper. + * You will use this class to get & edit variables from the config files or to create your own config! + * @example + * //create a config from: ./config/test.json with the id "some-config" + * const config = new api.ODJsonConfig("some-config","test.json") + * + * //create a config with custom dir: ./plugins/testplugin/test.json + * const config = new api.ODJsonConfig("plugin-config","test.json","./plugins/testplugin/") + */ +export class ODJsonConfig extends ODConfig { + constructor(id:ODValidId, file:string, customPath?:string){ + super(id) + try { + const filename = (file.endsWith(".json")) ? file : file+".json" + this.file = customPath ? nodepath.join("./",customPath,filename) : nodepath.join("./config/",filename) + this.data = customPath ? require(nodepath.join("../../../../",customPath,filename)) : require(nodepath.join("../../../../config",filename)) + }catch{ + throw new ODSystemError("config \""+nodepath.join("./",customPath ?? "",file)+"\" doesn't exist!") + } + } +} \ No newline at end of file diff --git a/src/core/api/modules/console.ts b/src/core/api/modules/console.ts new file mode 100644 index 0000000..6c1afc3 --- /dev/null +++ b/src/core/api/modules/console.ts @@ -0,0 +1,663 @@ +/////////////////////////////////////// +//CONSOLE MODULE +/////////////////////////////////////// +import { ODHTTPGetRequest, ODVersion, ODSystemError, ODPluginError, ODManager, ODManagerData, ODValidId } from "./base" +import { ODMain } from "../main" +import nodepath from "path" +import fs from "fs" +import ansis from "ansis" + +/**## ODValidConsoleColor `type` + * This is a collection of all the supported console colors within Open Ticket. + */ +export type ODValidConsoleColor = "white"|"red"|"yellow"|"green"|"blue"|"gray"|"cyan"|"magenta" + +/**## ODConsoleMessageParam `type` + * This interface contains all data required for a console log parameter within Open Ticket. + */ +export interface ODConsoleMessageParam { + /**The key of this parameter. */ + key:string, + /**The value of this parameter. */ + value:string, + /**When enabled, this parameter will only be shown in the debug file. */ + hidden?:boolean +} + +/**## ODConsoleMessage `class` + * This is an open ticket console message. + * + * It is used to create beautiful & styled logs in the console with a prefix, message & parameters. + * It also has full color support using `ansis` and parameters are parsed for you! + */ +export class ODConsoleMessage { + /**The main message sent in the console */ + message: string + /**An array of all the parameters in this message */ + params: ODConsoleMessageParam[] + /**The prefix of this message (!uppercase recommended!) */ + prefix: string + /**The color of the prefix of this message */ + color: ODValidConsoleColor + + constructor(message:string, prefix:string, color:ODValidConsoleColor, params?:ODConsoleMessageParam[]){ + this.message = message + this.params = params ? params : [] + this.prefix = prefix + + if (["white","red","yellow","green","blue","gray","cyan","magenta"].includes(color)){ + this.color = color + }else{ + this.color = "white" + } + } + /**Render this message to the console using `console.log`! Returns `false` when something went wrong. */ + render(){ + try { + const prefixcolor = ansis[this.color] + + const paramsstring = " "+this.createParamsString("gray") + const message = prefixcolor("["+this.prefix+"] ")+this.message + + console.log(message+paramsstring) + return true + }catch{ + return false + } + } + /**Create a more-detailed, non-colored version of this message to store it in the `otdebug.txt` file! */ + toDebugString(){ + const pstrings: string[] = [] + this.params.forEach((p) => { + pstrings.push(p.key+": "+p.value) + }) + const pstring = (pstrings.length > 0) ? " ("+pstrings.join(", ")+")" : "" + const date = new Date() + const dstring = `${date.getDate()}/${date.getMonth()}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}` + return `[${dstring} ${this.prefix}] ${this.message}${pstring}` + } + /**Render the parameters of this message in a specific color. */ + createParamsString(color:ODValidConsoleColor){ + let validcolor: ODValidConsoleColor = "white" + if (["white","red","yellow","green","blue","gray","cyan","magenta"].includes(color)){ + validcolor = color + } + + const pstrings: string[] = [] + this.params.forEach((p) => { + if (!p.hidden) pstrings.push(p.key+": "+p.value) + }) + + return (pstrings.length > 0) ? ansis[validcolor](" ("+pstrings.join(", ")+")") : "" + } + /**Set the message */ + setMessage(message:string){ + this.message = message + return this + } + /**Set the params */ + setParams(params:ODConsoleMessageParam[]){ + this.params = params + return this + } + /**Set the prefix */ + setPrefix(prefix:string){ + this.prefix = prefix + return this + } + /**Set the prefix color */ + setColor(color:ODValidConsoleColor){ + if (["white","red","yellow","green","blue","gray","cyan","magenta"].includes(color)){ + this.color = color + }else{ + this.color = "white" + } + return this + } +} + +/**## ODConsoleInfoMessage `class` + * This is an open ticket console info message. + * + * It is the same as a normal `ODConsoleMessage`, but it has a predefined prefix & color scheme for the "INFO" messages! + */ +export class ODConsoleInfoMessage extends ODConsoleMessage { + constructor(message:string,params?:ODConsoleMessageParam[]){ + super(message,"INFO","blue",params) + } +} + +/**## ODConsoleSystemMessage `class` + * This is an open ticket console system message. + * + * It is the same as a normal `ODConsoleMessage`, but it has a predefined prefix & color scheme for the "SYSTEM" messages! + */ +export class ODConsoleSystemMessage extends ODConsoleMessage { + constructor(message:string,params?:ODConsoleMessageParam[]){ + super(message,"SYSTEM","green",params) + } +} + +/**## ODConsolePluginMessage `class` + * This is an open ticket console plugin message. + * + * It is the same as a normal `ODConsoleMessage`, but it has a predefined prefix & color scheme for the "PLUGIN" messages! + */ +export class ODConsolePluginMessage extends ODConsoleMessage { + constructor(message:string,params?:ODConsoleMessageParam[]){ + super(message,"PLUGIN","magenta",params) + } +} + +/**## ODConsoleDebugMessage `class` + * This is an open ticket console debug message. + * + * It is the same as a normal `ODConsoleMessage`, but it has a predefined prefix & color scheme for the "DEBUG" messages! + */ +export class ODConsoleDebugMessage extends ODConsoleMessage { + constructor(message:string,params?:ODConsoleMessageParam[]){ + super(message,"DEBUG","cyan",params) + } +} + +/**## ODConsoleWarningMessage `class` + * This is an open ticket console warning message. + * + * It is the same as a normal `ODConsoleMessage`, but it has a predefined prefix & color scheme for the "WARNING" messages! + */ +export class ODConsoleWarningMessage extends ODConsoleMessage { + constructor(message:string,params?:ODConsoleMessageParam[]){ + super(message,"WARNING","yellow",params) + } +} + +/**## ODConsoleErrorMessage `class` + * This is an open ticket console error message. + * + * It is the same as a normal `ODConsoleMessage`, but it has a predefined prefix & color scheme for the "ERROR" messages! + */ +export class ODConsoleErrorMessage extends ODConsoleMessage { + constructor(message:string,params?:ODConsoleMessageParam[]){ + super(message,"ERROR","red",params) + } +} + +/**## ODError `class` + * This is an open ticket error. + * + * It is used to render and log Node.js errors & crashes in a styled way to the console & `otdebug.txt` file! + */ +export class ODError { + /**The original error that this class wraps around */ + error: Error|ODSystemError|ODPluginError + /**The origin of the original error */ + origin: NodeJS.UncaughtExceptionOrigin + + constructor(error:Error|ODSystemError|ODPluginError, origin:NodeJS.UncaughtExceptionOrigin){ + this.error = error + this.origin = origin + } + + /**Render this error to the console using `console.log`! Returns `false` when something went wrong. */ + render(){ + try { + let prefix = (this.error["_ODErrorType"] == "plugin") ? "PLUGIN ERROR" : ((this.error["_ODErrorType"] == "system") ? "OPENTICKET ERROR" : "UNKNOWN ERROR") + //title + console.log(ansis.red("["+prefix+"]: ")+this.error.message+" | origin: "+this.origin) + //stack trace + if (this.error.stack) console.log(ansis.gray(this.error.stack)) + //additional message + if (this.error["_ODErrorType"] == "plugin") console.log(ansis.red.bold("\nPlease report this error to the plugin developer and help us create a more stable plugin!")) + else console.log(ansis.red.bold("\nPlease report this error to our discord server and help us create a more stable ticket bot!")) + console.log(ansis.red("Also send the "+ansis.cyan.bold("otdebug.txt")+" file! It would help a lot!\n")) + return true + }catch{ + return false + } + } + /**Create a more-detailed, non-colored version of this error to store it in the `otdebug.txt` file! */ + toDebugString(){ + return "[UNKNOWN OD ERROR]: "+this.error.message+" | origin: "+this.origin+"\n"+this.error.stack + } +} + +/**## ODConsoleMessageTypes `type` + * This is a collection of all the default console message types within Open Ticket. + */ +export type ODConsoleMessageTypes = "info"|"system"|"plugin"|"debug"|"warning"|"error" + +/**## ODConsoleManager `class` + * This is the open ticket console manager. + * + * It handles the entire console system of Open Ticket. It's also the place where you need to log `ODConsoleMessage`'s. + * This manager keeps a short history of messages sent to the console which is configurable by plugins. + * + * The debug file (`otdebug.txt`) is handled in a sub-manager! + */ +export class ODConsoleManager { + /**The history of `ODConsoleMessage`'s and `ODError`'s since startup */ + history: (ODConsoleMessage|ODError)[] = [] + /**The max length of the history. The oldest messages will be removed when over the limit */ + historylength = 100 + /**An alias to the debugfile manager. (`otdebug.txt`) */ + debugfile: ODDebugFileManager + + constructor(historylength:number, debugfile:ODDebugFileManager){ + this.historylength = historylength + this.debugfile = debugfile + } + + /**Log a message to the console ... But in the Open Ticket way :) */ + log(message:ODConsoleMessage): void + log(message:ODError): void + log(message:string, type?:ODConsoleMessageTypes, params?:ODConsoleMessageParam[]): void + log(message:ODConsoleMessage|ODError|string, type?:ODConsoleMessageTypes, params?:ODConsoleMessageParam[]){ + if (message instanceof ODConsoleMessage){ + message.render() + if (this.debugfile) this.debugfile.writeConsoleMessage(message) + this.history.push(message) + + }else if (message instanceof ODError){ + message.render() + if (this.debugfile) this.debugfile.writeErrorMessage(message) + this.history.push(message) + + }else if (["string","number","boolean","object"].includes(typeof message)){ + let newMessage: ODConsoleMessage + if (type == "info") newMessage = new ODConsoleInfoMessage(message,params) + else if (type == "system") newMessage = new ODConsoleSystemMessage(message,params) + else if (type == "plugin") newMessage = new ODConsolePluginMessage(message,params) + else if (type == "debug") newMessage = new ODConsoleDebugMessage(message,params) + else if (type == "warning") newMessage = new ODConsoleWarningMessage(message,params) + else if (type == "error") newMessage = new ODConsoleErrorMessage(message,params) + else newMessage = new ODConsoleSystemMessage(message,params) + + newMessage.render() + if (this.debugfile) this.debugfile.writeConsoleMessage(newMessage) + this.history.push(newMessage) + } + this.#purgeHistory() + } + /**Shorten the history when it exceeds the max history length! */ + #purgeHistory(){ + if (this.history.length > this.historylength) this.history.shift() + } +} + +/**## ODDebugFileManager `class` + * This is the open ticket debug file manager. + * + * It manages the Open Ticket debug file (`otdebug.txt`) which keeps a history of all system logs. + * There are even internal logs that aren't logged to the console which are available in this file! + * + * Using this class, you can change the max length of this file and some other cool things! + */ +export class ODDebugFileManager { + /**The path to the debugfile (`./otdebug.txt` by default) */ + path: string + /**The filename of the debugfile (`otdebug.txt` by default) */ + filename: string + /**The current version of the bot used in the debug file. */ + version: ODVersion + /**The max length of the debug file. */ + maxlines: number + + constructor(path:string, filename:string, maxlines:number, version:ODVersion){ + this.path = nodepath.join(path,filename) + this.filename = filename + this.version = version + this.maxlines = maxlines + + this.#writeStartupStats() + } + + /**Check if the debug file exists */ + #existsDebugFile(){ + return fs.existsSync(this.path) + } + /**Read from the debug file */ + #readDebugFile(){ + if (this.#existsDebugFile()){ + try { + return fs.readFileSync(this.path).toString() + }catch{ + return false + } + }else{ + return false + } + } + /**Write to the debug file and shorten it when needed. */ + #writeDebugFile(text:string){ + const currenttext = this.#readDebugFile() + if (currenttext){ + const splitted = currenttext.split("\n") + + if (splitted.length+text.split("\n").length > this.maxlines){ + splitted.splice(7,(text.split("\n").length)) + } + + splitted.push(text) + fs.writeFileSync(this.path,splitted.join("\n")) + }else{ + //write new file: + const newtext = this.#createStatsText()+text + fs.writeFileSync(this.path,newtext) + } + } + /**Generate the stats/header of the debug file (containing the version) */ + #createStatsText(){ + const date = new Date() + const dstring = `${date.getDate()}/${date.getMonth()}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}` + return [ + "=========================", + "OPEN TICKET DEBUG FILE:", + "version: "+this.version.toString(), + "last startup: "+dstring, + "=========================\n\n" + ].join("\n") + } + /**Write the stats/header to the debug file on startup */ + #writeStartupStats(){ + const currenttext = this.#readDebugFile() + if (currenttext){ + //edit previous file: + const splitted = currenttext.split("\n") + splitted.splice(0,7) + + if (splitted.length+11 > this.maxlines){ + splitted.splice(0,((splitted.length+11) - this.maxlines)) + } + + splitted.unshift(this.#createStatsText()) + splitted.push("\n---------------------------------------------------------------------\n---------------------------------------------------------------------\n") + + fs.writeFileSync(this.path,splitted.join("\n")) + }else{ + //write new file: + const newtext = this.#createStatsText() + fs.writeFileSync(this.path,newtext) + } + } + /**Write an `ODConsoleMessage` to the debug file */ + writeConsoleMessage(message:ODConsoleMessage){ + this.#writeDebugFile(message.toDebugString()) + } + /**Write an `ODError` to the debug file */ + writeErrorMessage(error:ODError){ + this.#writeDebugFile(error.toDebugString()) + } + /**Write custom text to the debug file */ + writeText(text:string){ + this.#writeDebugFile(text) + } + /**Write a custom note to the debug file (starting with `[NODE]:`) */ + writeNote(text:string){ + this.#writeDebugFile("[NODE]: "+text) + } +} + +/**## ODDebugger `class` + * This is the open ticket debugger. + * + * It is a simple wrapper around the `ODConsoleManager` to handle debugging (primarily for `ODManagers`). + * Messages created using this debugger are only logged to the debug file unless specified otherwise. + * + * You will probably notice this class being used in the `ODManager` constructor. + * + * Using this system, all additions & removals inside a manager are logged to the debug file. This makes searching for errors a lot easier! + */ +export class ODDebugger { + /**An alias to the open ticket console manager. */ + console: ODConsoleManager + /**When enabled, debug logs are also shown in the console. */ + visible: boolean = false + + constructor(console:ODConsoleManager){ + this.console = console + } + + /**Create a debug message. This will always be logged to `otdebug.txt` & sometimes to the console (when enabled). Returns `true` when visible */ + debug(message:string, params?:{key:string,value:string}[]): boolean { + if (this.visible){ + this.console.log(new ODConsoleDebugMessage(message,params)) + return true + }else{ + this.console.debugfile.writeConsoleMessage(new ODConsoleDebugMessage(message,params)) + return false + } + } +} + +/**## ODLivestatusColor `type` + * This is a collection of all the colors available within the LiveStatus system. + */ +export type ODLiveStatusColor = "normal"|"red"|"green"|"blue"|"yellow"|"white"|"gray"|"magenta"|"cyan" + +/**## ODLiveStatusSourceData `interface` + * This is an interface containing all raw data received from the LiveStatus system. + */ +export interface ODLiveStatusSourceData { + /**The message to display */ + message:{ + /**The title of the message to display */ + title:string, + /**The title color of the message to display */ + titleColor:ODLiveStatusColor, + /**The description of the message to display */ + description:string, + /**The description color of the message to display */ + descriptionColor:ODLiveStatusColor + }, + /**The message will only be shown when the bot matches all statements */ + active:{ + /**A list of versions to match */ + versions:string[], + /**A list of languages to match */ + languages:string[], + /**All languages should match */ + allLanguages:boolean, + /**Match when the bot is using plugins */ + usingPlugins:boolean, + /**Match when the bot is not using plugins */ + notUsingPlugins:boolean, + /**Match when the bot is using slash commands */ + usingSlashCommands:boolean, + /**Match when the bot is not using slash commands */ + notUsingSlashCommands:boolean, + /**Match when the bot is not using transcripts */ + notUsingTranscripts:boolean, + /**Match when the bot is using text transcripts */ + usingTextTranscripts:boolean, + /**Match when the bot is using html transcripts */ + usingHtmlTranscripts:boolean + } +} + +/**## ODLiveStatusSource `class` + * This is the open ticket livestatus source. + * + * It is an empty template for a livestatus source. + * By default, you should use `ODLiveStatusUrlSource` or `ODLiveStatusFileSource`, + * unless you want to create one on your own! + * + * This class doesn't do anything on it's own! It's just a template! + */ +export class ODLiveStatusSource extends ODManagerData { + /**The raw data of this source */ + data: ODLiveStatusSourceData[] + + constructor(id:ODValidId, data:ODLiveStatusSourceData[]){ + super(id) + this.data = data + } + + /**Change the current data using this method! */ + setData(data:ODLiveStatusSourceData[]){ + this.data = data + } + /**Get all messages relevant to the bot based on some parameters. */ + async getMessages(main:ODMain): Promise { + const validMessages: ODLiveStatusSourceData[] = [] + + //parse data from ODMain + const currentVersion: string = main.versions.get("openticket:version").toString(true) + const usingSlashCommands: boolean = main.configs.get("openticket:general").data.slashCommands + const usingTranscripts: false|"text"|"html" = false as false|"text"|"html" //TODO + const currentLanguage: string = main.languages.getCurrentLanguageId() + const usingPlugins: boolean = (main.plugins.getLength() > 0) + + //check data for each message + this.data.forEach((msg) => { + const {active} = msg + + const correctVersion = active.versions.includes(currentVersion) + const correctSlashMode = (usingSlashCommands && active.usingSlashCommands) || (!usingSlashCommands && active.notUsingSlashCommands) + const correctTranscriptMode = (usingTranscripts == "text" && active.usingTextTranscripts) || (usingTranscripts == "html" && active.usingHtmlTranscripts) || (!usingTranscripts && active.notUsingTranscripts) + const correctLanguage = active.languages.includes(currentLanguage) || active.allLanguages + const correctPlugins = (usingPlugins && active.usingPlugins) || (!usingPlugins && active.notUsingPlugins) + + if (correctVersion && correctLanguage && correctPlugins && correctSlashMode && correctTranscriptMode) validMessages.push(msg) + }) + + //return the valid messages + return validMessages + } +} + +/**## ODLiveStatusFileSource `class` + * This is the open ticket livestatus file source. + * + * It is a LiveStatus source that will read the data from a local file. + * + * This can be used for testing/extending the LiveStatus system! + */ +export class ODLiveStatusFileSource extends ODLiveStatusSource { + /**The path to the source file */ + path: string + + constructor(id:ODValidId, path:string){ + if (fs.existsSync(path)){ + super(id,JSON.parse(fs.readFileSync(path).toString())) + }else throw new ODSystemError("LiveStatus source file doesn't exist!") + this.path = path + } +} + +/**## ODLiveStatusUrlSource `class` + * This is the open ticket livestatus url source. + * + * It is a LiveStatus source that will read the data from a http URL (json file). + * + * This is the default way of receiving LiveStatus messages! + */ +export class ODLiveStatusUrlSource extends ODLiveStatusSource { + /**The url used in the request */ + url: string + /**The `ODHTTPGetRequest` helper to fetch the url! */ + request: ODHTTPGetRequest + + constructor(id:ODValidId, url:string){ + super(id,[]) + this.url = url + this.request = new ODHTTPGetRequest(url,false) + } + async getMessages(main:ODMain): Promise { + //additional setup + this.request.url = this.url + const rawRes = await this.request.run() + if (rawRes.status != 200) throw new ODSystemError("ODLiveStatusUrlSource => Request Failed!") + try{ + this.setData(JSON.parse(rawRes.body)) + }catch{ + throw new ODSystemError("ODLiveStatusUrlSource => Request Failed!") + } + + //default + return super.getMessages(main) + } +} + +/**## ODLiveStatusManager `class` + * This is the open ticket livestatus manager. + * + * It manages all LiveStatus sources and has the renderer for all LiveStatus messages. + * + * You will probably use this to customise or add stuff to the LiveStatus system. + * Access it in the global `openticket.startscreen.livestatus` variable! + */ +export class ODLiveStatusManager extends ODManager { + /**The class responsible for rendering the livestatus messages. */ + renderer: ODLiveStatusRenderer + /**A reference to the ODMain or "openticket" global variable */ + #main: ODMain + + constructor(debug:ODDebugger, main:ODMain){ + super(debug,"livestatus source") + this.renderer = new ODLiveStatusRenderer(main.console) + this.#main = main + } + + /**Get the messages from all sources combined! */ + async getAllMessages(): Promise { + const messages: ODLiveStatusSourceData[] = [] + for (const source of this.getAll()){ + try { + messages.push(...(await source.getMessages(this.#main))) + }catch{} + } + return messages + } +} + +/**## ODLiveStatusRenderer `class` + * This is the open ticket livestatus renderer. + * + * It's responsible for rendering all LiveStatus messages to the console. + */ +export class ODLiveStatusRenderer { + /**A reference to the ODConsoleManager or "openticket.console" global variable */ + #console: ODConsoleManager + + constructor(console:ODConsoleManager){ + this.#console = console + } + + /**Render all messages */ + render(messages:ODLiveStatusSourceData[]): string { + try { + //process data + const final: string[] = [] + messages.forEach((msg) => { + const titleColor = msg.message.titleColor + const title = "["+msg.message.title+"] " + + const descriptionColor = msg.message.descriptionColor + const description = msg.message.description.split("\n").map((text,row) => { + //first row row doesn't need prefix + if (row < 1) return text + //other rows do need a prefix + let text2 = text + for (const i of title){ + text2 = " "+text2 + } + return text2 + }).join("\n") + + + if (!["red","yellow","green","blue","gray","magenta","cyan"].includes(titleColor)) var finalTitle = ansis.white(title) + else var finalTitle = ansis[titleColor](title) + if (!["red","yellow","green","blue","gray","magenta","cyan"].includes(descriptionColor)) var finalDescription = ansis.white(description) + else var finalDescription = ansis[descriptionColor](description) + + final.push(finalTitle+finalDescription) + }) + + //return all messages + return final.join("\n") + }catch{ + this.#console.log("Failed to render LiveStatus messages!","error") + return "" + } + } +} \ No newline at end of file diff --git a/src/core/api/modules/cooldown.ts b/src/core/api/modules/cooldown.ts new file mode 100644 index 0000000..254a45a --- /dev/null +++ b/src/core/api/modules/cooldown.ts @@ -0,0 +1,348 @@ +/////////////////////////////////////// +//COOLDOWN MODULE +/////////////////////////////////////// +import { ODId, ODValidId, ODManager, ODSystemError, ODManagerData } from "./base" +import { ODDebugger } from "./console" + +/**## ODCooldownManager `class` + * This is an open ticket cooldown manager. + * + * It is responsible for managing all cooldowns in Open Ticket. An example of this is the ticket creation cooldown. + * + * There are many types of cooldowns available, but you can also create your own! + */ +export class ODCooldownManager extends ODManager> { + constructor(debug:ODDebugger){ + super(debug,"cooldown") + } + /**Initiate all cooldowns in this manager. */ + async init(){ + for (const cooldown of this.getAll()){ + await cooldown.init() + } + } +} + +/**## ODCooldownData `class` + * This is open ticket cooldown data. + * + * It contains the instance of an active cooldown (e.g. for a user). It is handled by the cooldown itself. + */ +export class ODCooldownData extends ODManagerData { + /**Is this cooldown active? */ + active: boolean + /**Additional data of this cooldown instance. (different for each cooldown type) */ + data: Data + + constructor(id:ODValidId,active:boolean,data:Data){ + super(id) + this.active = active + this.data = data + } +} + +/**## ODCooldown `class` + * This is an open ticket cooldown. + * + * It doesn't do anything on it's own, but it provides the methods that are used to interact with a cooldown. + * This class can be extended from to create a working cooldown. + * + * There are also premade cooldowns available in the bot! + */ +export class ODCooldown extends ODManagerData { + data: ODManager> = new ODManager() + /**Is this cooldown already initialized? */ + ready: boolean = false + + constructor(id:ODValidId){ + super(id) + } + + /**Check this id and start cooldown when it exeeds the limit! Returns `true` when on cooldown! */ + use(id:string): boolean { + throw new ODSystemError("Tried to use an unimplemented ODCooldown!") + } + /**Check this id without starting or updating the cooldown. Returns `true` when on cooldown! */ + check(id:string): boolean { + throw new ODSystemError("Tried to use an unimplemented ODCooldown!") + } + /**Remove the cooldown for an id when available.*/ + delete(id:string){ + throw new ODSystemError("Tried to use an unimplemented ODCooldown!") + } + /**Initialize the internal systems of this cooldown. */ + async init(){ + throw new ODSystemError("Tried to use an unimplemented ODCooldown!") + } +} + +/**## ODCounterCooldown `class` + * This is an open ticket counter cooldown. + * + * It is is a cooldown based on a counter. When the number exceeds the limit, the cooldown is activated. + * The number will automatically be decreased with a set amount & interval. + */ +export class ODCounterCooldown extends ODCooldown<{value:number}> { + /**The cooldown will activate when exceeding this limit. */ + activeLimit: number + /**The cooldown will deactivate when below this limit. */ + cancelLimit: number + /**The amount to increase the counter with everytime the cooldown is triggered/updated. */ + increment: number + /**The amount to decrease the counter over time. */ + decrement: number + /**The interval between decrements in milliseconds. */ + invervalMs: number + + constructor(id:ODValidId, activeLimit:number, cancelLimit:number, increment:number, decrement:number, intervalMs:number){ + super(id) + this.activeLimit = activeLimit + this.cancelLimit = cancelLimit + this.increment = increment + this.decrement = decrement + this.invervalMs = intervalMs + } + + use(id:string): boolean { + const cooldown = this.data.get(id) + if (cooldown){ + //cooldown for this id already exists + if (cooldown.active){ + return true + + }else if (cooldown.data.value < this.activeLimit){ + cooldown.data.value = cooldown.data.value + this.increment + return false + + }else{ + cooldown.active = true + return false + } + }else{ + //cooldown for this id doesn't exist + this.data.add(new ODCooldownData(id,(this.increment >= this.activeLimit),{ + value:this.increment + })) + return false + } + } + check(id:string): boolean { + const cooldown = this.data.get(id) + if (cooldown){ + //cooldown for this id already exists + return cooldown.active + }else return false + } + delete(id:string): void { + this.data.remove(id) + } + async init(){ + if (this.ready) return + setInterval(() => { + this.data.getAll().forEach((cooldown) => { + cooldown.data.value = cooldown.data.value - this.decrement + if (cooldown.data.value <= this.cancelLimit){ + cooldown.active = false + } + if (cooldown.data.value <= 0){ + this.data.remove(cooldown.id) + } + }) + },this.invervalMs) + this.ready = true + } +} + +/**## ODIncrementalCounterCooldown `class` + * This is an open ticket incremental counter cooldown. + * + * It is is a cooldown based on an incremental counter. It is exactly the same as the normal counter, + * with the only difference being that it still increments when the limit is already exeeded. + */ +export class ODIncrementalCounterCooldown extends ODCooldown<{value:number}> { + /**The cooldown will activate when exceeding this limit. */ + activeLimit: number + /**The cooldown will deactivate when below this limit. */ + cancelLimit: number + /**The amount to increase the counter with everytime the cooldown is triggered/updated. */ + increment: number + /**The amount to decrease the counter over time. */ + decrement: number + /**The interval between decrements in milliseconds. */ + invervalMs: number + + constructor(id:ODValidId, activeLimit:number, cancelLimit:number, increment:number, decrement:number, intervalMs:number){ + super(id) + this.activeLimit = activeLimit + this.cancelLimit = cancelLimit + this.increment = increment + this.decrement = decrement + this.invervalMs = intervalMs + } + + use(id:string): boolean { + const cooldown = this.data.get(id) + if (cooldown){ + //cooldown for this id already exists + if (cooldown.active){ + cooldown.data.value = cooldown.data.value + this.increment + return true + + }else if (cooldown.data.value < this.activeLimit){ + cooldown.data.value = cooldown.data.value + this.increment + return false + + }else{ + cooldown.active = true + return false + } + }else{ + //cooldown for this id doesn't exist + this.data.add(new ODCooldownData(id,(this.increment >= this.activeLimit),{ + value:this.increment + })) + return false + } + } + check(id:string): boolean { + const cooldown = this.data.get(id) + if (cooldown){ + //cooldown for this id already exists + return cooldown.active + }else return false + } + delete(id:string): void { + this.data.remove(id) + } + async init(){ + if (this.ready) return + setInterval(() => { + this.data.getAll().forEach((cooldown) => { + cooldown.data.value = cooldown.data.value - this.decrement + if (cooldown.data.value <= this.cancelLimit){ + cooldown.active = false + } + if (cooldown.data.value <= 0){ + this.data.remove(cooldown.id) + } + }) + },this.invervalMs) + this.ready = true + } +} + +/**## ODTimeoutCooldown `class` + * This is an open ticket timeout cooldown. + * + * It is a cooldown based on a timer. When triggered/updated, the cooldown is activated for the set amount of time. + * After the timer has timed out, the cooldown will be deleted. + */ +export class ODTimeoutCooldown extends ODCooldown<{date:number}> { + /**The amount of milliseconds before the cooldown times-out */ + timeoutMs: number + + constructor(id:ODValidId, timeoutMs:number){ + super(id) + this.timeoutMs = timeoutMs + } + + use(id:string): boolean { + const cooldown = this.data.get(id) + if (cooldown){ + //cooldown for this id already exists + if ((new Date().getTime() - cooldown.data.date) > this.timeoutMs){ + this.data.remove(id) + return false + }else{ + return true + } + }else{ + //cooldown for this id doesn't exist + this.data.add(new ODCooldownData(id,true,{ + date:new Date().getTime() + })) + return false + } + } + check(id:string): boolean { + const cooldown = this.data.get(id) + if (cooldown){ + //cooldown for this id already exists + return true + }else return false + } + delete(id:string): void { + this.data.remove(id) + } + /**Get the remaining amount of milliseconds before the timeout stops. */ + remaining(id:string): number|null { + const cooldown = this.data.get(id) + if (!cooldown) return null + const rawResult = this.timeoutMs - (new Date().getTime() - cooldown.data.date) + return (rawResult > 0) ? rawResult : 0 + } + async init(){ + if (this.ready) return + this.ready = true + } +} + +/**## ODIncrementalTimeoutCooldown `class` + * This is an open ticket incremental timeout cooldown. + * + * It is is a cooldown based on an incremental timer. It is exactly the same as the normal timer, + * with the only difference being that it adds additional time when triggered/updated while the cooldown is already active. + */ +export class ODIncrementalTimeoutCooldown extends ODCooldown<{date:number}> { + /**The amount of milliseconds before the cooldown times-out */ + timeoutMs: number + /**The amount of milliseconds to add when triggered/updated while the cooldown is already active. */ + incrementMs: number + + constructor(id:ODValidId, timeoutMs:number, incrementMs:number){ + super(id) + this.timeoutMs = timeoutMs + this.incrementMs = incrementMs + } + + use(id:string): boolean { + const cooldown = this.data.get(id) + if (cooldown){ + //cooldown for this id already exists + if ((new Date().getTime() - cooldown.data.date) > this.timeoutMs){ + this.data.remove(id) + return false + }else{ + cooldown.data.date = cooldown.data.date + this.incrementMs + return true + } + }else{ + //cooldown for this id doesn't exist + this.data.add(new ODCooldownData(id,true,{ + date:new Date().getTime() + })) + return false + } + } + check(id:string): boolean { + const cooldown = this.data.get(id) + if (cooldown){ + //cooldown for this id already exists + return true + }else return false + } + delete(id:string): void { + this.data.remove(id) + } + /**Get the remaining amount of milliseconds before the timeout stops. */ + remaining(id:string): number|null { + const cooldown = this.data.get(id) + if (!cooldown) return null + const rawResult = this.timeoutMs - (new Date().getTime() - cooldown.data.date) + return (rawResult > 0) ? rawResult : 0 + } + async init(){ + if (this.ready) return + this.ready = true + } +} \ No newline at end of file diff --git a/src/core/api/modules/database.ts b/src/core/api/modules/database.ts new file mode 100644 index 0000000..15047b5 --- /dev/null +++ b/src/core/api/modules/database.ts @@ -0,0 +1,316 @@ +/////////////////////////////////////// +//DATABASE MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODManagerData, ODValidId, ODValidJsonType } from "./base" +import fs from "fs" +import nodepath from "path" +import { ODDebugger } from "./console" +import * as fjs from "formatted-json-stringify" + +/**## ODDatabaseManager `class` + * This is an open ticket database manager. + * + * It manages all databases in the bot and allows to permanently store data from the bot! + * + * You will use this class to get/add a database (`ODDatabase`) in your plugin! + * @example + * //get ./database/ot-global.json => ODDatabase class + * const globalDB = openticket.databases.get("openticket:global") + * + * //add a new database with id "test" => ./database/idk-test.json + * const testDatabase = new api.ODDatabase("test","idk-test.json") + * openticket.databases.add(testDatabase) + */ +export class ODDatabaseManager extends ODManager { + constructor(debug:ODDebugger){ + super(debug,"database") + } + + /**Add data to the manager. The id will be fetched from the data class! You can optionally select to overwrite existing data! + * @example + * //add a new database with id "test" => ./database/idk-test.json + * const testDatabase = new api.ODDatabase("test","idk-test.json") + * openticket.databases.add(testDatabase) + */ + add(data:ODDatabase, overwrite?:boolean): boolean { + return super.add(data,overwrite) + } + /**Get data that matches the `ODId`. Returns the found data. + * @example + * //get ./database/ot-global.json => ODDatabase class + * const globalDB = openticket.databases.get("openticket:global") + */ + get(id:ODValidId): ODDatabase|null { + return super.get(id) + } + /**Remove data that matches the `ODId`. Returns the removed data. + * @example + * //remove the "test" database + * openticket.databases.remove("test") //returns null if non-existing + */ + remove(id:ODValidId): ODDatabase|null { + return super.remove(id) + } + /**Check if data that matches the `ODId` exists. Returns a boolean. + * @example + * //check if "./database/idk-test.json" (test) exists => boolean + * const exists = openticket.databases.exists("test") + */ + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODDatabase `class` + * This is an open ticket database template. + * This class doesn't do anything at all, it just gives a template & basic methods for a database. Use `ODJsonDatabase` instead! + * + * You will only use this class if you want to create your own database implementation (e.g. `mongodb`, `mysql`,...)! + * @example + * class SomeDatabase extends ODDatabase { + * //override this method + * setData(category:string, key:string, value:ODValidJsonType): boolean { + * return false + * } + * //override this method + * getData(category:string, key:string): ODValidJsonType|undefined { + * return undefined + * } + * //override this method + * deleteData(category:string, key:string): boolean { + * return false + * } + * } + */ +export class ODDatabase extends ODManagerData { + /**The full path to this database with extension */ + file: string = "" + + /**Add/Overwrite a specific category & key in the database. Returns `true` when overwritten. */ + set(category:string, key:string, value:ODValidJsonType): boolean { + return false + } + /**Get a specific category & key in the database */ + get(category:string, key:string): ODValidJsonType|undefined { + return undefined + } + /**Delete a specific category & key in the database */ + delete(category:string, key:string): boolean { + return false + } + /**Check if a specific category & key exists in the database */ + exists(category:string, key:string): boolean { + return false + } + /**Get a specific category in the database */ + getCategory(category:string): {key:string, value:ODValidJsonType}[]|undefined { + return undefined + } + /**Get all values in the database */ + getAll(): ODJsonDatabaseStructure { + return [] + } +} + +/**## ODJsonDatabaseStructure `type` + * This is the structure of how a JSON database file! + */ +export type ODJsonDatabaseStructure = {category:string, key:string, value:ODValidJsonType}[] + +/**## ODJsonDatabase `class` + * This is an open ticket JSON database. + * It stores data in a `json` file as a large `Array` using the `category`, `key`, `value` strategy. + * You can store the following types: `string`, `number`, `boolean`, `array`, `object` & `null`! + * + * You will only use this class if you want to create your own database or use an existing one! + * @example + * //get,set & delete data + * const data = database.getData("category","key") //data will be the value + * const didOverwrite = database.setData("category","key","value") //value can be any of the valid types + * const didExist = database.deleteData("category","key") //delete this value + * //You need an ODJsonDatabase class named "database" for this example to work! + */ +export class ODJsonDatabase extends ODDatabase { + constructor(id:ODValidId, file:string, customPath?:string){ + super(id) + const filename = (file.endsWith(".json")) ? file : file+".json" + this.file = customPath ? nodepath.join("./",customPath,filename) : nodepath.join("./database/",filename) + + //init file if it doesn't exist yet + this.#system.getData() + } + + /**Set/overwrite the value of `category` & `key`. Returns `true` when overwritten! + * @example + * const didOverwrite = database.setData("category","key","value") //value can be any of the valid types + * //You need an ODJsonDatabase class named "database" for this example to work! + */ + set(category:string, key:string, value:ODValidJsonType): boolean { + const currentList = this.#system.getData() + const currentData = currentList.find((d) => (d.category === category) && (d.key === key)) + + //overwrite when already present + if (currentData){ + currentList[currentList.indexOf(currentData)].value = value + }else{ + currentList.push({category,key,value}) + } + + this.#system.setData(currentList) + return currentData ? true : false + } + /**Get the value of `category` & `key`. Returns `undefined` when non-existent! + * @example + * const data = database.getData("category","key") //data will be the value + * //You need an ODJsonDatabase class named "database" for this example to work! + */ + get(category:string, key:string): ODValidJsonType|undefined { + const currentList = this.#system.getData() + const tempresult = currentList.find((d) => (d.category === category) && (d.key === key)) + return tempresult ? tempresult.value : undefined + } + /**Remove the value of `category` & `key`. Returns `undefined` when non-existent! + * @example + * const didExist = database.deleteData("category","key") //delete this value + * //You need an ODJsonDatabase class named "database" for this example to work! + */ + delete(category:string, key:string): boolean { + const currentList = this.#system.getData() + const currentData = currentList.find((d) => (d.category === category) && (d.key === key)) + if (currentData) currentList.splice(currentList.indexOf(currentData),1) + + this.#system.setData(currentList) + return currentData ? true : false + } + /**Check if a value of `category` & `key` exists. Returns `false` when non-existent! */ + exists(category:string, key:string): boolean { + const currentList = this.#system.getData() + const tempresult = currentList.find((d) => (d.category === category) && (d.key === key)) + return tempresult ? true : false + } + /**Get all values in `category`. Returns `undefined` when non-existent! */ + getCategory(category:string): {key:string, value:ODValidJsonType}[]|undefined { + const currentList = this.#system.getData() + const tempresult = currentList.filter((d) => (d.category === category)) + return tempresult ? tempresult.map((data) => {return {key:data.key,value:data.value}}) : undefined + } + /**Get all values in `category`. */ + getAll(): ODJsonDatabaseStructure { + return this.#system.getData() + } + + #system = { + /**Read parsed data from the json file */ + getData: (): ODJsonDatabaseStructure => { + if (fs.existsSync(this.file)){ + return JSON.parse(fs.readFileSync(this.file).toString()) + }else{ + fs.writeFileSync(this.file,"[]") + return [] + } + }, + /**Write parsed data to the json file */ + setData: (data:ODJsonDatabaseStructure) => { + fs.writeFileSync(this.file,JSON.stringify(data,null,"\t")) + } + } +} + + +/**## ODFormattedJsonDatabase `class` + * This is an open ticket Formatted JSON database. + * It stores data in a `json` file as a large `Array` using the `category`, `key`, `value` strategy. + * You can store the following types: `string`, `number`, `boolean`, `array`, `object` & `null`! + * + * This one is exactly the same as `ODJsonDatabase`, but it has a formatter from the `formatted-json-stringify` package. + * This can help you organise it a little bit better! + */ +export class ODFormattedJsonDatabase extends ODDatabase { + /**The formatter to use on the database array */ + formatter: fjs.ArrayFormatter + + constructor(id:ODValidId, file:string, formatter:fjs.ArrayFormatter, customPath?:string){ + super(id) + const filename = (file.endsWith(".json")) ? file : file+".json" + this.file = customPath ? nodepath.join("./",customPath,filename) : nodepath.join("./database/",filename) + this.formatter = formatter + + //init file if it doesn't exist yet + this.#system.getData() + } + + /**Set/overwrite the value of `category` & `key`. Returns `true` when overwritten! + * @example + * const didOverwrite = database.setData("category","key","value") //value can be any of the valid types + * //You need an ODFormattedJsonDatabase class named "database" for this example to work! + */ + set(category:string, key:string, value:ODValidJsonType): boolean { + const currentList = this.#system.getData() + const currentData = currentList.find((d) => (d.category === category) && (d.key === key)) + + //overwrite when already present + if (currentData){ + currentList[currentList.indexOf(currentData)].value = value + }else{ + currentList.push({category,key,value}) + } + + this.#system.setData(currentList) + return currentData ? true : false + } + /**Get the value of `category` & `key`. Returns `undefined` when non-existent! + * @example + * const data = database.getData("category","key") //data will be the value + * //You need an ODFormattedJsonDatabase class named "database" for this example to work! + */ + get(category:string, key:string): ODValidJsonType|undefined { + const currentList = this.#system.getData() + const tempresult = currentList.find((d) => (d.category === category) && (d.key === key)) + return tempresult ? tempresult.value : undefined + } + /**Remove the value of `category` & `key`. Returns `undefined` when non-existent! + * @example + * const didExist = database.deleteData("category","key") //delete this value + * //You need an ODFormattedJsonDatabase class named "database" for this example to work! + */ + delete(category:string, key:string): boolean { + const currentList = this.#system.getData() + const currentData = currentList.find((d) => (d.category === category) && (d.key === key)) + if (currentData) currentList.splice(currentList.indexOf(currentData),1) + + this.#system.setData(currentList) + return currentData ? true : false + } + /**Check if a value of `category` & `key` exists. Returns `false` when non-existent! */ + exists(category:string, key:string): boolean { + const currentList = this.#system.getData() + const tempresult = currentList.find((d) => (d.category === category) && (d.key === key)) + return tempresult ? true : false + } + /**Get all values in `category`. Returns `undefined` when non-existent! */ + getCategory(category:string): {key:string, value:ODValidJsonType}[]|undefined { + const currentList = this.#system.getData() + const tempresult = currentList.filter((d) => (d.category === category)) + return tempresult ? tempresult.map((data) => {return {key:data.key,value:data.value}}) : undefined + } + /**Get all values in `category`. */ + getAll(): ODJsonDatabaseStructure { + return this.#system.getData() + } + + #system = { + /**Read parsed data from the json file */ + getData: (): ODJsonDatabaseStructure => { + if (fs.existsSync(this.file)){ + return JSON.parse(fs.readFileSync(this.file).toString()) + }else{ + fs.writeFileSync(this.file,"[]") + return [] + } + }, + /**Write parsed data to the json file */ + setData: (data:ODJsonDatabaseStructure) => { + fs.writeFileSync(this.file,this.formatter.stringify(data)) + } + } +} \ No newline at end of file diff --git a/src/core/api/modules/defaults.ts b/src/core/api/modules/defaults.ts new file mode 100644 index 0000000..a2f1910 --- /dev/null +++ b/src/core/api/modules/defaults.ts @@ -0,0 +1,319 @@ +/////////////////////////////////////// +//DEFAULTS MODULE +/////////////////////////////////////// + +/**## ODDefaults `interface` + * This type is a list of all defaults available in the `ODDefaultsManager` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODDefaults { + /**Enable the default error handling system. */ + errorHandling:boolean, + /**Crash when there is an unknown bot error. */ + crashOnError:boolean, + /**Enable the system responsible for the `--debug` flag. */ + debugLoading:boolean, + /**Enable loading all Open Ticket plugins, sadly enough is only useful for the system :) */ + pluginLoading:boolean, + /**Don't crash the bot when a plugin crashes! */ + softPluginLoading:boolean, + + /**Load the default open ticket plugin classes. */ + pluginClassLoading:boolean, + /**Load the default open ticket plugin events. */ + pluginEventLoading:boolean, + + /**Load the default open ticket flags. */ + flagLoading:boolean, + /**Enable the default initializer for open ticket flags. */ + flagInitiating:boolean, + /**Load the default open ticket configs. */ + configLoading:boolean, + /**Load the default open ticket databases. */ + databaseLoading:boolean, + /**Load the default open ticket sessions. */ + sessionLoading:boolean, + + /**Load the default open ticket languages. */ + languageLoading:boolean, + /**Enable selecting the current language from `config/general.json`. */ + languageSelection:boolean, + /**Set the backup language when the primary language is missing a property. */ + backupLanguage:string, + /****[NOD FOR PLUGIN TRANSLATIONS]** The full list of available languages (used in the default config checker). */ + languageList:string[], + + /**Load the default open ticket config checker. */ + checkerLoading:boolean, + /**Load the default open ticket config checker functions. */ + checkerFunctionLoading:boolean, + /**Enable the default execution of the config checkers. */ + checkerExecution:boolean, + /**Load the default open ticket config checker translations. */ + checkerTranslationLoading:boolean, + /**Enable the default rendering of the config checkers. */ + checkerRendering:boolean, + /**Enable the default quit action when there is an error in the config checker. */ + checkerQuit:boolean, + /**Render the checker even when there are no errors & warnings. */ + checkerRenderEmpty:boolean, + + /**Load the default open ticket client configuration. */ + clientLoading:boolean, + /**Load the default open ticket client initialization. */ + clientInitiating:boolean, + /**Load the default open ticket client ready actions (status, commands, permissions, ...). */ + clientReady:boolean, + /**Create a warning when the bot is present in multiple guilds. */ + clientMultiGuildWarning:boolean, + /**Load the default open ticket client activity (from `config/general.json`). */ + clientActivityLoading:boolean, + /**Load the default open ticket client activity initialization (& status refresh). */ + clientActivityInitiating:boolean, + + /**Load the default open ticket slash commands. */ + slashCommandLoading:boolean, + /**Load the default open ticket slash command registerer (register slash cmds in discord). */ + slashCommandRegistering:boolean, + /**When enabled, the bot is forced to re-register all slash commands in the server. This can be used in case of a auto-update malfunction. */ + forceSlashCommandRegistration:boolean, + /**Load the default open ticket text commands. */ + textCommandLoading:boolean, + + /**Load the default open ticket questions (from `config/questions.json`) */ + questionLoading:boolean, + /**Load the default open ticket options (from `config/options.json`) */ + optionLoading:boolean, + /**Load the default open ticket panels (from `config/panels.json`) */ + panelLoading:boolean, + /**Load the default open ticket tickets (from `database/tickets.json`) */ + ticketLoading:boolean, + /**Load the default open ticket reaction roles (from `config/options.json`) */ + roleLoading:boolean, + /**Load the default open ticket blacklist (from `database/users.json`) */ + blacklistLoading:boolean, + /**Load the default open ticket transcript compilers. */ + transcriptCompilerLoading:boolean, + /**Load the default open ticket transcript history (from `database/transcripts.json`) */ + transcriptHistoryLoading:boolean, + + /**Load the default open ticket button builders. */ + buttonBuildersLoading:boolean, + /**Load the default open ticket dropdown builders. */ + dropdownBuildersLoading:boolean, + /**Load the default open ticket file builders. */ + fileBuildersLoading:boolean, + /**Load the default open ticket embed builders. */ + embedBuildersLoading:boolean, + /**Load the default open ticket message builders. */ + messageBuildersLoading:boolean, + /**Load the default open ticket modal builders. */ + modalBuildersLoading:boolean, + + /**Load the default open ticket command responders. */ + commandRespondersLoading:boolean, + /**Load the default open ticket button responders. */ + buttonRespondersLoading:boolean, + /**Load the default open ticket dropdown responders. */ + dropdownRespondersLoading:boolean, + /**Load the default open ticket modal responders. */ + modalRespondersLoading:boolean, + /**Set the time (in ms) before open ticket sends an error message when no reply is sent in a responder. */ + responderTimeoutMs:number, + + /**Load the default open ticket actions. */ + actionsLoading:boolean, + + /**Load the default open ticket verify bars. */ + verifyBarsLoading:boolean, + /**Load the default open ticket permissions. */ + permissionsLoading:boolean, + /**Load the default open ticket posts. */ + postsLoading:boolean, + /**Initiate the default open ticket posts. */ + postsInitiating:boolean, + /**Load the default open ticket cooldowns. */ + cooldownsLoading:boolean, + /**Initiate the default open ticket cooldowns. */ + cooldownsInitiating:boolean, + /**Load the default open ticket help menu categories. */ + helpMenuCategoryLoading:boolean, + /**Load the default open ticket help menu components. */ + helpMenuComponentLoading:boolean, + + /**Load the default open ticket stat scopes. */ + statScopesLoading:boolean, + /**Load the default open ticket stats. */ + statLoading:boolean, + /**Initiate the default open ticket stats. */ + statInitiating:boolean, + + /**Load the default open ticket code/functions. */ + codeLoading:boolean, + /**Execute the default open ticket code/functions. */ + codeExecution:boolean, + + /**Load the default open ticket livestatus. */ + liveStatusLoading:boolean, + /**Load the default open ticket startscreen. */ + startScreenLoading:boolean, + /**Render the default open ticket startscreen. */ + startScreenRendering:boolean, + + /**The emoji style to use in embed & message titles using `utilities.emoijTitle()` */ + emojiTitleStyle:"disabled"|"before"|"after"|"double", + /**The emoji divider to use in embed & message titles using `utilities.emoijTitle()` */ + emojiTitleDivider:string + /**The interval in milliseconds that are between autoclose timeout checkers. */ + autocloseCheckInterval:number + /**The interval in milliseconds that are between autodelete timeout checkers. */ + autodeleteCheckInterval:number +} + +/**## ODDefaultsBooleans `type` + * This type is a list of boolean defaults available in the `ODDefaultsManager` class. + * It's used to generate typescript declarations for this class. + */ +export type ODDefaultsBooleans = { + [Key in keyof ODDefaults]: ODDefaults[Key] extends boolean ? Key : never +}[keyof ODDefaults] + +/**## ODDefaultsStrings `type` + * This type is a list of string defaults available in the `ODDefaultsManager` class. + * It's used to generate typescript declarations for this class. + */ +export type ODDefaultsStrings = { + [Key in keyof ODDefaults]: ODDefaults[Key] extends string ? Key : never +}[keyof ODDefaults] + +/**## ODDefaultsNumbers `type` + * This type is a list of number defaults available in the `ODDefaultsManager` class. + * It's used to generate typescript declarations for this class. + */ +export type ODDefaultsNumbers = { + [Key in keyof ODDefaults]: ODDefaults[Key] extends number ? Key : never +}[keyof ODDefaults] + +/**## ODDefaultsStringArray `type` + * This type is a list of string[] defaults available in the `ODDefaultsManager` class. + * It's used to generate typescript declarations for this class. + */ +export type ODDefaultsStringArray = { + [Key in keyof ODDefaults]: ODDefaults[Key] extends string[] ? Key : never +}[keyof ODDefaults] + +/**## ODDefaultsManager `class` + * This is an open ticket defaults manager. + * + * It manages all settings in open ticket that are not meant to be in the config. + * Here you can disable certain default features to replace them or to specifically enable them! + * + * You are unable to add your own defaults, you can only edit Open Ticket defaults! + */ +export class ODDefaultsManager { + /**A list of all the defaults */ + #defaults: ODDefaults + + constructor(){ + this.#defaults = { + errorHandling:true, + crashOnError:false, + debugLoading:true, + pluginLoading:true, + softPluginLoading:false, + + pluginClassLoading:true, + pluginEventLoading:true, + + flagLoading:true, + flagInitiating:true, + configLoading:true, + databaseLoading:true, + sessionLoading:true, + + languageLoading:true, + languageSelection:true, + backupLanguage:"openticket:english", + languageList:["custom","english","dutch","portuguese","czech"], + + checkerLoading:true, + checkerFunctionLoading:true, + checkerExecution:true, + checkerTranslationLoading:true, + checkerRendering:true, + checkerQuit:true, + checkerRenderEmpty:false, + + clientLoading:true, + clientInitiating:true, + clientReady:true, + clientMultiGuildWarning:true, + clientActivityLoading:true, + clientActivityInitiating:true, + + slashCommandLoading:true, + slashCommandRegistering:true, + forceSlashCommandRegistration:false, + textCommandLoading:true, + + questionLoading:true, + optionLoading:true, + panelLoading:true, + ticketLoading:true, + roleLoading:true, + blacklistLoading:true, + transcriptCompilerLoading:true, + transcriptHistoryLoading:true, + + buttonBuildersLoading:true, + dropdownBuildersLoading:true, + fileBuildersLoading:true, + embedBuildersLoading:true, + messageBuildersLoading:true, + modalBuildersLoading:true, + + commandRespondersLoading:true, + buttonRespondersLoading:true, + dropdownRespondersLoading:true, + modalRespondersLoading:true, + responderTimeoutMs:2500, + + actionsLoading:true, + + verifyBarsLoading:true, + permissionsLoading:true, + postsLoading:true, + postsInitiating:true, + cooldownsLoading:true, + cooldownsInitiating:true, + helpMenuCategoryLoading:true, + helpMenuComponentLoading:true, + + statScopesLoading:true, + statLoading:true, + statInitiating:true, + + codeLoading:true, + codeExecution:true, + + liveStatusLoading:true, + startScreenLoading:true, + startScreenRendering:true, + + emojiTitleStyle:"before", + emojiTitleDivider:" ", + autocloseCheckInterval:300000, //5 minutes + autodeleteCheckInterval:300000 //5 minutes + } + } + + /**Set a default to a specific value. Remember! All plugins can edit these values, so your value could be overwritten! */ + setDefault(key:DefaultName, value:ODDefaults[DefaultName]): void { + this.#defaults[key] = value + } + + /**Get a default. Remember! All plugins can edit these values, so this value could be overwritten! */ + getDefault(key:DefaultName): ODDefaults[DefaultName] { + return this.#defaults[key] + } +} \ No newline at end of file diff --git a/src/core/api/modules/event.ts b/src/core/api/modules/event.ts new file mode 100644 index 0000000..27633c1 --- /dev/null +++ b/src/core/api/modules/event.ts @@ -0,0 +1,99 @@ +/////////////////////////////////////// +//EVENT MODULE +/////////////////////////////////////// +import { ODManagerData, ODManager, ODValidId } from "./base" +import { ODConsoleWarningMessage, ODDebugger } from "./console" + +/**## ODEvent `class` + * This is an open ticket event. + * + * This class is made to work with the `ODEventManager` to handle events. + * The function of this specific class is to manage all listeners for a specifc event! + */ +export class ODEvent extends ODManagerData { + /**Alias to open ticket debugger. */ + #debug?: ODDebugger + /**The list of permanent listeners. */ + listeners: Function[] = [] + /**The list of one-time listeners. List is cleared every time the event is emitted. */ + oncelisteners: Function[] = [] + /**The max listener limit before a possible memory leak will be announced */ + listenerLimit: number = 25 + + /**Use the open ticket debugger in this manager for logs*/ + useDebug(debug:ODDebugger|null){ + this.#debug = debug ?? undefined + } + /**Get a collection of listeners combined from both types. Also clears the one-time listeners array! */ + #getCurrentListeners(){ + const final: Function[] = [] + this.oncelisteners.forEach((l) => final.push(l)) + this.listeners.forEach((l) => final.push(l)) + + this.oncelisteners = [] + return final + } + /**Edit the listener limit */ + setListenerLimit(limit:number){ + this.listenerLimit = limit + } + /**Add a permanent callback to this event. This will stay as long as the bot is running! */ + listen(callback:Function){ + this.listeners.push(callback) + + if (this.listeners.length > this.listenerLimit){ + if (this.#debug) this.#debug.console.log(new ODConsoleWarningMessage("Possible event memory leak detected!",[ + {key:"event",value:this.id.value}, + {key:"listeners",value:this.listeners.length.toString()} + ])) + } + } + /**Add a one-time-only callback to this event. This will only trigger the callback once! */ + listenOnce(callback:Function){ + this.oncelisteners.push(callback) + } + /**Wait until this event is fired! Be carefull with it, because it could block the entire bot when wrongly used! */ + async wait(): Promise { + return new Promise((resolve,reject) => { + this.oncelisteners.push((...args:any) => {resolve(args)}) + }) + } + /**Emit this event to all listeners. You are required to provide all parameters of the event! */ + async emit(params:any[]): Promise { + for (const listener of this.#getCurrentListeners()){ + try{ + await listener(...params) + }catch(err){ + process.emit("uncaughtException",err) + } + } + } +} + +/**## ODEventManager `class` + * This is an open ticket event manager. + * + * This class is made to manage all events in the bot. You can compare it with the built-in node.js `EventEmitter` + * + * You will probably not create this class yourself, but use it globaly instead! + * Check out the `openticket.events` class! + */ +export class ODEventManager extends ODManager { + /**Reference to the Open Ticket debugger */ + #debug: ODDebugger + + constructor(debug:ODDebugger){ + super(debug,"event") + this.#debug = debug + } + + add(data:ODEvent, overwrite?:boolean): boolean { + data.useDebug(this.#debug) + return super.add(data,overwrite) + } + remove(id:ODValidId): ODEvent|null { + const data = super.remove(id) + if (data) data.useDebug(null) + return data + } +} \ No newline at end of file diff --git a/src/core/api/modules/flag.ts b/src/core/api/modules/flag.ts new file mode 100644 index 0000000..1b8a9cd --- /dev/null +++ b/src/core/api/modules/flag.ts @@ -0,0 +1,73 @@ +/////////////////////////////////////// +//FLAG MODULE +/////////////////////////////////////// +import { ODId, ODValidId, ODManager, ODManagerData } from "./base" +import { ODDebugger } from "./console" + +/**## ODFlag `class` + * This is an open ticket flag. + * + * A flag is a boolean that can be specified by a parameter in the console. + * It's useful for small settings that are only required once in a while. + * + * Flags can also be enabled manually by plugins! + */ +export class ODFlag extends ODManagerData { + /**The method that has been used to set the value of this flag. (`null` when not set) */ + method: "param"|"manual"|null = null + /**The name of this flag. Visible to the user. */ + name: string + /**The description of this flag. Visible to the user. */ + description: string + /**The name of the parameter in the console. (e.g. `--test`) */ + param: string + /**A list of aliases for the parameter in the console. */ + aliases: string[] + /**The value of this flag. */ + value: boolean = false + + constructor(id:ODValidId, name:string, description:string, param:string, aliases?:string[], initialValue?:boolean){ + super(id) + this.name = name + this.description = description + this.param = param + this.aliases = aliases ?? [] + this.value = initialValue ?? false + } + + /**Set the value of this flag. */ + setValue(value:boolean,method?:"param"|"manual"){ + this.value = value + this.method = method ?? "manual" + } + /**Detect if the process contains the param or aliases & set the value. Use `force` to overwrite a manually set value. */ + detectProcessParams(force?:boolean){ + if (force){ + const params = [this.param,...this.aliases] + this.setValue(params.some((p) => process.argv.includes(p)),"param") + + }else if (this.method != "manual"){ + const params = [this.param,...this.aliases] + this.setValue(params.some((p) => process.argv.includes(p)),"param") + } + } +} + +/**## ODFlagManager `class` + * This is an open ticket flag manager. + * + * This class is responsible for managing & initiating all flags of the bot. + * It also contains a shortcut for initiating all flags. + */ +export class ODFlagManager extends ODManager { + constructor(debug:ODDebugger){ + super(debug,"flag") + } + + /**Set all flags to their `process.argv` value. */ + init(){ + this.getAll().forEach((flag) => { + flag.detectProcessParams(false) + }) + } +} \ No newline at end of file diff --git a/src/core/api/modules/helpmenu.ts b/src/core/api/modules/helpmenu.ts new file mode 100644 index 0000000..8d2ad57 --- /dev/null +++ b/src/core/api/modules/helpmenu.ts @@ -0,0 +1,216 @@ +/////////////////////////////////////// +//HELP MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODManagerData, ODValidId } from "./base" +import { ODDebugger } from "./console" + +/**## ODHelpMenuComponentRenderer `type` + * This is the callback of the help menu component renderer. It also contains information about how & where it is rendered. + */ +export type ODHelpMenuComponentRenderer = (page:number, category:number, location:number, mode:"slash"|"text") => string|Promise + +/**## ODHelpMenuComponent `class` + * This is an open ticket help menu component. + * + * It can render something on the Open Ticket help menu. + */ +export class ODHelpMenuComponent extends ODManagerData { + /**The priority of this component. The higher, the earlier it will appear in the help menu. */ + priority: number + /**The render function for this component. */ + render: ODHelpMenuComponentRenderer + + constructor(id:ODValidId, priority:number, render:ODHelpMenuComponentRenderer){ + super(id) + this.priority = priority + this.render = render + } +} + +/**## ODHelpMenuTextComponent `class` + * This is an open ticket help menu text component. + * + * It can render a static piece of text on the Open Ticket help menu. + */ +export class ODHelpMenuTextComponent extends ODHelpMenuComponent { + constructor(id:ODValidId, priority:number, text:string){ + super(id,priority,() => { + return text + }) + } +} + +/**## ODHelpMenuCommandComponentOption `interface` + * This interface contains a command option for the `ODHelpMenuCommandComponent`. + */ +export interface ODHelpMenuCommandComponentOption { + /**The name of this option. */ + name:string, + /**Is this option optional? */ + optional:boolean +} + +/**## ODHelpMenuCommandComponentSettings `interface` + * This interface contains the settings for the `ODHelpMenuCommandComponent`. + */ +export interface ODHelpMenuCommandComponentSettings { + /**The name of this text command. */ + textName?:string, + /**The name of this slash command. */ + slashName?:string, + /**Options available in the text command. */ + textOptions?:ODHelpMenuCommandComponentOption[], + /**Options available in the slash command. */ + slashOptions?:ODHelpMenuCommandComponentOption[], + /**The description for the text command. */ + textDescription?:string, + /**The description for the slash command. */ + slashDescription?:string +} + +/**## ODHelpMenuCommandComponent `class` + * This is an open ticket help menu command component. + * + * It contains a useful helper to render a command in the Open Ticket help menu. + */ +export class ODHelpMenuCommandComponent extends ODHelpMenuComponent { + constructor(id:ODValidId, priority:number, settings:ODHelpMenuCommandComponentSettings){ + super(id,priority,(page,category,location,mode) => { + if (mode == "slash" && settings.slashName){ + return `\`${settings.slashName}${(settings.slashOptions) ? this.#renderOptions(settings.slashOptions) : ""}\` ➜ ${settings.slashDescription ?? ""}` + + }else if (mode == "text" && settings.textName){ + return `\`${settings.textName}${(settings.textOptions) ? this.#renderOptions(settings.textOptions) : ""}\` ➜ ${settings.textDescription ?? ""}` + + }else return "" + }) + } + + /**Utility function to render all command options. */ + #renderOptions(options:ODHelpMenuCommandComponentOption[]){ + return " "+options.map((opt) => (opt.optional) ? `[${opt.name}]` : `<${opt.name}>`).join(" ") + } +} + +/**## ODHelpMenuCategory `class` + * This is an open ticket help menu category. + * + * Every category in the help menu is an embed field by default. + * Try to limit the amount of components per category. + */ +export class ODHelpMenuCategory extends ODManager { + /**The id of this category. */ + id: ODId + /**The priority of this category. The higher, the earlier it will appear in the menu. */ + priority: number + /**The name of this category. (can include emoji's) */ + name: string + /**When enabled, it automatically starts this category on a new page. */ + newPage: boolean + + constructor(id:ODValidId, priority:number, name:string, newPage?:boolean){ + super() + this.id = new ODId(id) + this.priority = priority + this.name = name + this.newPage = newPage ?? false + } + + /**Render this category and it's components. */ + async render(page:number, category:number, mode:"slash"|"text"){ + //sort from high priority to low + const derefArray = [...this.getAll()] + derefArray.sort((a,b) => { + return b.priority-a.priority + }) + const result: string[] = [] + + let i = 0 + for (const component of derefArray){ + try { + result.push(await component.render(page,category,i,mode)) + }catch(err){ + process.emit("uncaughtException",err) + } + i++ + } + + //only return the non-empty components + return result.filter((component) => component !== "").join("\n\n") + } +} + +/**## ODHelpMenuRenderResult `type` + * This is the array returned when the help menu has been rendered succesfully. + * + * It contains a list of pages, which contain categories by name & value (content). + */ +export type ODHelpMenuRenderResult = {name:string, value:string}[][] + +/**## ODHelpMenuManager `class` + * This is an open ticket help menu manager. + * + * It is responsible for rendering the entire help menu content. + * You are also able to configure the amount of categories per page here. + * + * Fewer Categories == More Clean Menu + */ +export class ODHelpMenuManager extends ODManager { + /**Alias to open ticket debugger. */ + #debug: ODDebugger + /**The amount of categories per-page. */ + categoriesPerPage: number = 3 + + constructor(debug:ODDebugger){ + super(debug,"help menu category") + this.#debug = debug + } + + add(data:ODHelpMenuCategory, overwrite?:boolean): boolean { + data.useDebug(this.#debug,"help menu component") + return super.add(data,overwrite) + } + + /**Render this entire help menu & return a `ODHelpMenuRenderResult`. */ + async render(mode:"slash"|"text"): Promise { + //sort from high priority to low + const derefArray = [...this.getAll()] + derefArray.sort((a,b) => { + return b.priority-a.priority + }) + const result: {name:string, value:string}[][] = [] + let currentPage: {name:string, value:string}[] = [] + + for (const category of derefArray){ + try { + const renderedCategory = await category.render(result.length,currentPage.length,mode) + + if (renderedCategory !== ""){ + //create new page when category wants to + if (currentPage.length > 0 && category.newPage){ + result.push(currentPage) + currentPage = [] + } + + currentPage.push({ + name:category.name, + value:renderedCategory + }) + + //create new page when page is full + if (currentPage.length >= this.categoriesPerPage){ + result.push(currentPage) + currentPage = [] + } + } + }catch(err){ + process.emit("uncaughtException",err) + } + } + + //push current page when not-empty + if (currentPage.length > 0) result.push(currentPage) + + return result + } +} \ No newline at end of file diff --git a/src/core/api/modules/language.ts b/src/core/api/modules/language.ts new file mode 100644 index 0000000..806c99e --- /dev/null +++ b/src/core/api/modules/language.ts @@ -0,0 +1,107 @@ +/////////////////////////////////////// +//LANGUAGE MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODManagerData, ODSystemError, ODValidId } from "./base" +import nodepath from "path" +import { ODDebugger } from "./console" + +export class ODLanguage extends ODManagerData { + file: string + data: any + metadata: { + otversion:string, + language:string, + translator:string, + lastedited:string + }|null = null + + constructor(id:ODValidId, file:string, customPath?:string){ + super(id) + this.file = file + try{ + const filename = (file.endsWith(".json")) ? file : file+".json" + this.data = customPath ? require(nodepath.join("../../../../",customPath,filename)) : require(nodepath.join("../../../../languages",filename)) + }catch{ + throw new ODSystemError("[API ERROR] Language \""+file+"\" doesn't exist!") + } + if (this.data["_TRANSLATION"]) this.metadata = this.data["_TRANSLATION"] + } +} + +export class ODLanguageManager extends ODManager { + current: ODLanguage|null = null + backup: ODLanguage|null = null + + constructor(debug:ODDebugger, presets:boolean){ + super(debug,"language") + if (presets) this.add(new ODLanguage("english","english.json")) + this.current = presets ? new ODLanguage("english","english.json") : null + this.backup = presets ? new ODLanguage("english","english.json") : null + } + + setCurrentLanguage(id:ODValidId){ + this.current = this.get(id) + } + getCurrentLanguage(){ + return (this.current) ? this.current : null + } + setBackupLanguage(id:ODValidId){ + this.backup = this.get(id) + } + getBackupLanguage(){ + return (this.backup) ? this.backup : null + } + getLanguageMetadata(frombackup?:boolean){ + if (frombackup) return (this.backup) ? this.backup.metadata : null + return (this.current) ? this.current.metadata : null + } + getCurrentLanguageId(){ + return (this.current) ? this.current.id.value : "" + } + getTranslation(id:string): string|null { + if (!this.current) return this.#getBackupTranslation(id) + + const splitted = id.split(".") + let currentObject = this.current.data + let result: string|false = false + splitted.forEach((id) => { + if (typeof currentObject[id] == "object"){ + currentObject = currentObject[id] + }else if (typeof currentObject[id] == "string"){ + result = currentObject[id] + } + }) + + if (typeof result == "string") return result + else return this.#getBackupTranslation(id) + } + + #getBackupTranslation(id:string): string|null { + if (!this.backup) return null + + const splitted = id.split(".") + let currentObject = this.backup.data + let result: string|false = false + splitted.forEach((id) => { + if (typeof currentObject[id] == "object"){ + currentObject = currentObject[id] + }else if (typeof currentObject[id] == "string"){ + result = currentObject[id] + } + }) + + if (typeof result == "string") return result + else return null + } + + getTranslationWithParams(id:string, params:string[]): string|null { + let translation = this.getTranslation(id) + if (!translation) return translation + + params.forEach((value,index) => { + if (!translation) return + translation = translation.replace(`{${index}}`,value) + }) + return translation + } +} \ No newline at end of file diff --git a/src/core/api/modules/permission.ts b/src/core/api/modules/permission.ts new file mode 100644 index 0000000..d46921b --- /dev/null +++ b/src/core/api/modules/permission.ts @@ -0,0 +1,211 @@ +/////////////////////////////////////// +//PERMISSION MODULE +/////////////////////////////////////// +import { ODId, ODValidId, ODManager, ODSystemError, ODManagerData } from "./base" +import * as discord from "discord.js" +import { ODDebugger } from "./console" + +export type ODPermissionType = "member"|"support"|"moderator"|"admin"|"owner"|"developer" +export type ODPermissionScope = "global-user"|"channel-user"|"global-role"|"channel-role" +export interface ODPermissionResult { + type:ODPermissionType + scope:ODPermissionScope|"default" + level:ODPermissionLevel, + source:ODPermission|null +} + +export enum ODPermissionLevel { + member, + support, + moderator, + admin, + owner, + developer +} + +export class ODPermission extends ODManagerData { + readonly scope: ODPermissionScope + readonly permission: ODPermissionType + readonly value: discord.Role|discord.User + readonly channel: discord.Channel|null + + constructor(id:ODValidId, scope:"global-user", permission:ODPermissionType, value:discord.User) + constructor(id:ODValidId, scope:"global-role", permission:ODPermissionType, value:discord.Role) + constructor(id:ODValidId, scope:"channel-user", permission:ODPermissionType, value:discord.User, channel:discord.Channel) + constructor(id:ODValidId, scope:"channel-role", permission:ODPermissionType, value:discord.Role, channel:discord.Channel) + constructor(id:ODValidId, scope:ODPermissionScope, permission:ODPermissionType, value:discord.Role|discord.User, channel?:discord.Channel){ + super(id) + this.scope = scope + this.permission = permission + this.value = value + this.channel = channel ?? null + } +} + +export interface ODPermissionSettings { + allowGlobalUserScope?:boolean, + allowGlobalRoleScope?:boolean, + allowChannelUserScope?:boolean, + allowChannelRoleScope?:boolean, + idRegex?:RegExp +} + +export type ODPermissionCalculationCallback = (user:discord.User, channel?:discord.Channel|null, guild?:discord.Guild|null, settings?:ODPermissionSettings|null) => Promise + +export class ODPermissionManager extends ODManager { + #calculation: ODPermissionCalculationCallback|null + defaultResult: ODPermissionResult = { + level:ODPermissionLevel["member"], + scope:"default", + type:"member", + source:null + } + + constructor(debug:ODDebugger, useDefaultCalculation?:boolean){ + super(debug,"permission") + this.#calculation = useDefaultCalculation ? this.#defaultCalculation : null + } + + setCalculation(calculation:ODPermissionCalculationCallback){ + this.#calculation = calculation + } + setDefaultResult(result:ODPermissionResult){ + this.defaultResult = result + } + getPermissions(user:discord.User, channel?:discord.Channel|null, guild?:discord.Guild|null, settings?:ODPermissionSettings|null): Promise { + try{ + if (!this.#calculation) throw new ODSystemError("ODPermissionManager:getPermissions() => missing perms calculation") + return this.#calculation(user,channel,guild,settings) + }catch(err){ + process.emit("uncaughtException",err) + throw new ODSystemError("ODPermissionManager:getPermissions() => failed perms calculation") + } + } + hasPermissions(minimum:ODPermissionType, data:ODPermissionResult){ + if (minimum == "member") return true + else if (minimum == "support") return (data.level >= ODPermissionLevel["support"]) + else if (minimum == "moderator") return (data.level >= ODPermissionLevel["moderator"]) + else if (minimum == "admin") return (data.level >= ODPermissionLevel["admin"]) + else if (minimum == "owner") return (data.level >= ODPermissionLevel["owner"]) + else if (minimum == "developer") return (data.level >= ODPermissionLevel["developer"]) + else throw new ODSystemError("Invalid minimum permission type at ODPermissionManager.hasPermissions()") + } + async #defaultCalculation(user:discord.User,channel?:discord.Channel|null,guild?:discord.Guild|null, settings?:ODPermissionSettings|null): Promise { + const idRegex = (settings && typeof settings.idRegex != "undefined") ? settings.idRegex : null + const allowChannelUserScope = (settings && typeof settings.allowChannelUserScope != "undefined") ? settings.allowChannelUserScope : true + const allowChannelRoleScope = (settings && typeof settings.allowChannelRoleScope != "undefined") ? settings.allowChannelRoleScope : true + const allowGlobalUserScope = (settings && typeof settings.allowGlobalUserScope != "undefined") ? settings.allowGlobalUserScope : true + const allowGlobalRoleScope = (settings && typeof settings.allowGlobalRoleScope != "undefined") ? settings.allowGlobalRoleScope : true + + if (guild && channel && !channel.isDMBased()){ + //check for channel user permissions + if (allowChannelUserScope){ + const users = this.getFiltered((permission) => (!idRegex || (idRegex && idRegex.test(permission.id.value))) && permission.scope == "channel-user" && permission.channel && (permission.channel.id == channel.id) && (permission.value instanceof discord.User) && permission.value.id == user.id) + + if (users.length > 0){ + //sort all permisions from highest to lowest + users.sort((a,b) => { + const levelA = ODPermissionLevel[a.permission] + const levelB = ODPermissionLevel[b.permission] + + if (levelB > levelA) return 1 + else if (levelA > levelB) return -1 + else return 0 + }) + + return { + type:users[0].permission, + scope:"channel-user", + level:ODPermissionLevel[users[0].permission], + source:users[0] ?? null + } + } + } + + //check for channel role permissions + if (allowChannelRoleScope){ + const member = await guild.members.fetch(user.id) + if (member){ + const memberRoles = member.roles.cache.map((role) => role.id) + const roles = this.getFiltered((permission) => (!idRegex || (idRegex && idRegex.test(permission.id.value))) && permission.scope == "channel-role" && permission.channel && (permission.channel.id == channel.id) && (permission.value instanceof discord.Role) && memberRoles.includes(permission.value.id) && permission.value.guild.id == guild.id) + + if (roles.length > 0){ + //sort all permisions from highest to lowest + roles.sort((a,b) => { + const levelA = ODPermissionLevel[a.permission] + const levelB = ODPermissionLevel[b.permission] + + if (levelB > levelA) return 1 + else if (levelA > levelB) return -1 + else return 0 + }) + + return { + type:roles[0].permission, + scope:"channel-role", + level:ODPermissionLevel[roles[0].permission], + source:roles[0] ?? null + } + } + } + } + } + + //check for global user permissions + if (allowGlobalUserScope){ + const users = this.getFiltered((permission) => (!idRegex || (idRegex && idRegex.test(permission.id.value))) && permission.scope == "global-user" && (permission.value instanceof discord.User) && permission.value.id == user.id) + + if (users.length > 0){ + //sort all permisions from highest to lowest + users.sort((a,b) => { + const levelA = ODPermissionLevel[a.permission] + const levelB = ODPermissionLevel[b.permission] + + if (levelB > levelA) return 1 + else if (levelA > levelB) return -1 + else return 0 + }) + + return { + type:users[0].permission, + scope:"global-user", + level:ODPermissionLevel[users[0].permission], + source:users[0] ?? null + } + } + } + + //check for global role permissions + if (allowGlobalRoleScope){ + if (guild){ + const member = await guild.members.fetch(user.id) + if (member){ + const memberRoles = member.roles.cache.map((role) => role.id) + const roles = this.getFiltered((permission) => (!idRegex || (idRegex && idRegex.test(permission.id.value))) && permission.scope == "global-role" && (permission.value instanceof discord.Role) && memberRoles.includes(permission.value.id) && permission.value.guild.id == guild.id) + + if (roles.length > 0){ + //sort all permisions from highest to lowest + roles.sort((a,b) => { + const levelA = ODPermissionLevel[a.permission] + const levelB = ODPermissionLevel[b.permission] + + if (levelB > levelA) return 1 + else if (levelA > levelB) return -1 + else return 0 + }) + + return { + type:roles[0].permission, + scope:"global-role", + level:ODPermissionLevel[roles[0].permission], + source:roles[0] ?? null + } + } + } + } + } + + //spread result to prevent accidental referencing + return {...this.defaultResult} + } +} \ No newline at end of file diff --git a/src/core/api/modules/plugin.ts b/src/core/api/modules/plugin.ts new file mode 100644 index 0000000..fc17aaa --- /dev/null +++ b/src/core/api/modules/plugin.ts @@ -0,0 +1,233 @@ +/////////////////////////////////////// +//PLUGIN MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODManagerData, ODSystemError, ODValidId, ODVersion } from "./base" +import nodepath from "path" +import { ODConsolePluginMessage, ODConsoleWarningMessage, ODDebugger } from "./console" + +export interface ODUnknownCrashedPlugin { + name:string, + description:string +} + +export class ODPluginManager extends ODManager { + events: ODPluginEventManager + classes: ODPluginClassManager + unknownCrashedPlugins: ODUnknownCrashedPlugin[] = [] + + constructor(debug:ODDebugger){ + super(debug,"plugin") + this.events = new ODPluginEventManager(debug) + this.classes = new ODPluginClassManager(debug) + } + + /**Check if a plugin has loaded successfully.*/ + isPluginLoaded(id:ODValidId){ + const newId = new ODId(id) + const plugin = this.get(newId) + return (plugin && plugin.executed) + } +} + +export interface ODPluginData { + name:string, + id:string, + version:string, + startFile:string, + + enabled:boolean, + priority:number, + events:string[] + + npmDependencies:string[], + requiredPlugins:string[], + incompatiblePlugins:string[], + + details:ODPluginDetails +} + +export interface ODPluginDetails { + author:string, + shortDescription:string, + longDescription:string, + imageUrl:string, + projectUrl:string, + tags:string[] +} + +export class ODPlugin extends ODManagerData { + dir: string + data: ODPluginData + name: string + priority: number + version: ODVersion + details: ODPluginDetails + + enabled: boolean + executed: boolean + crashed: boolean + crashReason: null|"incompatible.plugin"|"missing.plugin"|"missing.dependency"|"executed" = null + + constructor(dir:string, jsondata:ODPluginData){ + super(jsondata.id) + this.dir = dir + this.data = jsondata + this.name = jsondata.name + this.priority = jsondata.priority + this.version = ODVersion.fromString("plugin",jsondata.version) + this.details = jsondata.details + + this.enabled = jsondata.enabled + this.executed = false + this.crashed = false + } + + //Get the startfile location relative to the ./plugins/ directory + getStartFile(){ + return nodepath.join(this.dir,this.data.startFile) + } + /**Execute this plugin. Returns `false` on crash. */ + async execute(debug:ODDebugger,force?:boolean): Promise { + if ((this.enabled && !this.crashed) || force){ + try{ + await require(nodepath.join("../../../../plugins/",this.getStartFile())) + debug.console.log("Plugin \""+this.id.value+"\" loaded successfully!","plugin") + this.executed = true + return true + }catch(error){ + this.crashed = true + this.crashReason = "executed" + + debug.console.log(error.message+", canceling plugin execution...","plugin",[ + {key:"path",value:"./plugins/"+this.dir} + ]) + debug.console.log("You can see more about this error in the ./otdebug.txt file!","info") + debug.console.debugfile.writeText(error.stack) + + return false + } + }else return true + } + + #checkDependency(id:string){ + try{ + require.resolve(id) + return true + }catch{ + return false + } + } + + dependenciesInstalled(){ + const missing: string[] = [] + this.data.npmDependencies.forEach((d) => { + if (!this.#checkDependency(d)){ + missing.push(d) + } + }) + + return missing + } + pluginsInstalled(manager:ODPluginManager){ + const missing: string[] = [] + this.data.requiredPlugins.forEach((p) => { + const plugin = manager.get(p) + if (!plugin || !plugin.enabled){ + missing.push(p) + } + }) + + return missing + } +} + +export class ODPluginEvent extends ODManagerData { + listeners: Function[] = [] + oncelisteners: Function[] = [] + + getCurrentListeners(){ + const final: Function[] = [] + this.oncelisteners.forEach((l) => final.push(l)) + this.listeners.forEach((l) => final.push(l)) + + this.oncelisteners = [] + return final + } +} + +export class ODPluginEventManager extends ODManager { + #debug: ODDebugger + listenerLimit: number = 25 + + constructor(debug:ODDebugger){ + super(debug,"plugin event") + this.#debug = debug + } + + setListenerLimit(limit:number){ + this.listenerLimit = limit + } + + on(event:string, callback:Function){ + const eventdata = this.get(event) + if (eventdata){ + eventdata.listeners.push(callback) + + if (eventdata.listeners.length > this.listenerLimit){ + this.#debug.console.log(new ODConsoleWarningMessage("Possible plugin event memory leak detected!",[ + {key:"event",value:event}, + {key:"listeners",value:eventdata.listeners.length.toString()} + ])) + } + }else{ + throw new ODSystemError("unknown plugin event \""+event+"\"") + } + } + + once(event:string, callback:Function){ + const eventdata = this.get(event) + if (eventdata){ + eventdata.oncelisteners.push(callback) + + }else{ + throw new ODSystemError("unknown plugin event \""+event+"\"") + } + } + + wait(event:string): Promise { + return new Promise((resolve,reject) => { + const eventdata = this.get(event) + if (eventdata){ + eventdata.oncelisteners.push((...args:any) => {resolve(args)}) + }else{ + reject("unknown plugin event \""+event+"\"") + } + }) + } + + emit(event:string, params:any[]): Promise { + return new Promise(async (resolve,reject) => { + const eventdata = this.get(event) + if (eventdata){ + const listeners = eventdata.getCurrentListeners() + + for (const listener of listeners){ + try { + await listener(...params) + }catch(err){ + process.emit("uncaughtException",err) + } + } + resolve() + }else{ + reject("unknown plugin event \""+event+"\"") + } + }) + } +} + +export class ODPluginClassManager extends ODManager { + constructor(debug:ODDebugger){ + super(debug,"plugin class") + } +} \ No newline at end of file diff --git a/src/core/api/modules/post.ts b/src/core/api/modules/post.ts new file mode 100644 index 0000000..de5b5f6 --- /dev/null +++ b/src/core/api/modules/post.ts @@ -0,0 +1,90 @@ +/////////////////////////////////////// +//POST MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODManagerData, ODValidId } from "./base" +import { ODMessageBuildResult, ODMessageBuildSentResult } from "./builder" +import { ODDebugger } from "./console" +import * as discord from "discord.js" + +/**## ODPostManager `class` + * This is an open ticket post manager. + * + * It manages `ODPosts`'s for you. + * + * You will probably use this to get the logs channel of the bot (or some other static channel/category). + */ +export class ODPostManager extends ODManager> { + /**A reference to the main server of the bot */ + #guild: discord.Guild|null = null + + constructor(debug:ODDebugger){ + super(debug,"post") + } + + add(data:ODPost, overwrite?:boolean): boolean { + if (this.#guild) data.useGuild(this.#guild) + return super.add(data,overwrite) + } + /**Initialize the post manager & all posts. */ + async init(guild:discord.Guild){ + this.#guild = guild + for (const post of this.getAll()){ + post.useGuild(guild) + await post.init() + } + } +} + +/**## ODPost `class` + * This is an open ticket post class. + * + * A post is just a shortcut to a static discord channel or category. + * This can be used to get a specific channel over and over again! + * + * This class also contains utilities for sending messages via the Open Ticket builders. + */ +export class ODPost extends ODManagerData { + /**A reference to the main server of the bot */ + #guild: discord.Guild|null = null + /**Is this post already initialized? */ + ready: boolean = false + /**The discord.js channel */ + channel: ChannelType|null = null + /**The discord channel id */ + channelId: string + + constructor(id:ODValidId, channelId:string){ + super(id) + this.channelId = channelId + } + + /**Use a specific guild in this class for fetching the channel*/ + useGuild(guild:discord.Guild|null){ + this.#guild = guild + } + /**Change the channel id to another channel! */ + setChannelId(id:string){ + this.channelId = id + } + /**Initialize the discord.js channel of this post. */ + async init(){ + if (this.ready) return + if (!this.#guild) return this.channel = null + try{ + this.channel = await this.#guild.channels.fetch(this.channelId) as ChannelType + }catch{ + this.channel = null + } + this.ready = true + } + /**Send a message to this channel using the Open Ticket builder system */ + async send(msg:ODMessageBuildResult): Promise> { + if (!this.channel || !this.channel.isTextBased()) return {success:false,message:null} + try{ + const sent = await this.channel.send(msg.message) + return {success:true,message:sent} + }catch{ + return {success:false,message:null} + } + } +} \ No newline at end of file diff --git a/src/core/api/modules/responder.ts b/src/core/api/modules/responder.ts new file mode 100644 index 0000000..9749471 --- /dev/null +++ b/src/core/api/modules/responder.ts @@ -0,0 +1,884 @@ +/////////////////////////////////////// +//RESPONDER MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODValidId, ODSystemError, ODManagerData } from "./base" +import * as discord from "discord.js" +import { ODWorkerManager, ODWorkerCallback, ODWorker } from "./worker" +import { ODDebugger } from "./console" +import { ODClientManager, ODSlashCommand, ODTextCommand, ODTextCommandInteractionOption } from "./client" +import { ODDropdownData, ODMessageBuildResult, ODMessageBuildSentResult, ODModalBuildResult } from "./builder" + +/**## ODResponderImplementation `class` + * This is an open ticket responder implementation. + * + * It is a basic implementation of the `ODWorkerManager` used by all `ODResponder` classes. + * + * This class can't be used stand-alone & needs to be extended from! + */ +export class ODResponderImplementation extends ODManagerData { + /**The manager that has all workers of this implementation */ + workers: ODWorkerManager + /**The `commandName` or `customId` needs to match this string or regex for this responder to be executed. */ + match: string|RegExp + + constructor(id:ODValidId, match:string|RegExp, callback?:ODWorkerCallback, priority?:number, callbackId?:ODValidId){ + super(id) + this.match = match + this.workers = new ODWorkerManager("descending") + if (callback) this.workers.add(new ODWorker(callbackId ? callbackId : id,priority ?? 0,callback)) + } + /**Execute all workers & return the result. */ + async respond(instance:Instance, source:Source, params:Params): Promise { + throw new ODSystemError("Tried to build an unimplemented ODResponderImplementation") + } +} + +export type ODResponderTimeoutErrorCallback = (instance:Instance, source:Source) => void|Promise + +export class ODResponderManager { + commands: ODCommandResponderManager + buttons: ODButtonResponderManager + dropdowns: ODDropdownResponderManager + modals: ODModalResponderManager + + constructor(debug:ODDebugger, client:ODClientManager){ + this.commands = new ODCommandResponderManager(debug,"command responder",client) + this.buttons = new ODButtonResponderManager(debug,"button responder",client) + this.dropdowns = new ODDropdownResponderManager(debug,"dropdown responder",client) + this.modals = new ODModalResponderManager(debug,"modal responder",client) + } +} + +export class ODCommandResponderManager extends ODManager> { + #client: ODClientManager + #timeoutErrorCallback: ODResponderTimeoutErrorCallback|null = null + #timeoutMs: number|null = null + + constructor(debug:ODDebugger, debugname:string, client:ODClientManager){ + super(debug,debugname) + this.#client = client + } + + /**Set the message to send when the response times out! */ + setTimeoutErrorCallback(callback:ODResponderTimeoutErrorCallback|null, ms:number|null){ + this.#timeoutErrorCallback = callback + this.#timeoutMs = ms + } + + add(data:ODCommandResponder<"slash"|"text",any>, overwrite?:boolean){ + const res = super.add(data,overwrite) + + //add the callback to the slash command manager + this.#client.slashCommands.onInteraction(data.match,(interaction,cmd) => { + const newData = this.get(data.id) + if (!newData) return + newData.respond(new ODCommandResponderInstance(interaction,cmd,this.#timeoutErrorCallback,this.#timeoutMs),"slash",{}) + }) + + //add the callback to the text command manager + this.#client.textCommands.onInteraction(data.prefix,data.match,(interaction,cmd,options) => { + const newData = this.get(data.id) + if (!newData) return + newData.respond(new ODCommandResponderInstance(interaction,cmd,this.#timeoutErrorCallback,this.#timeoutMs,options),"text",{}) + }) + + return res + } +} + +export class ODCommandResponderInstanceOptions { + #interaction:discord.ChatInputCommandInteraction|discord.Message + #cmd:ODSlashCommand|ODTextCommand + #options: ODTextCommandInteractionOption[] + + constructor(interaction:discord.ChatInputCommandInteraction|discord.Message, cmd:ODSlashCommand|ODTextCommand, options?:ODTextCommandInteractionOption[]){ + this.#interaction = interaction + this.#cmd = cmd + this.#options = options ?? [] + } + getString(name:string,required:true): string + getString(name:string,required:false): string|null + getString(name:string,required:boolean){ + if (this.#interaction instanceof discord.ChatInputCommandInteraction){ + try { + return this.#interaction.options.getString(name,required) + }catch{ + throw new ODSystemError("ODCommandResponderInstanceOptions:getString() slash command option not found!") + } + + }else if (this.#interaction instanceof discord.Message){ + const opt = this.#options.find((opt) => opt.type == "string" && opt.name == name) + if (opt && typeof opt.value == "string") return opt.value + else return null + + }else return null + } + getBoolean(name:string,required:true): boolean + getBoolean(name:string,required:false): boolean|null + getBoolean(name:string,required:boolean){ + if (this.#interaction instanceof discord.ChatInputCommandInteraction){ + try { + return this.#interaction.options.getBoolean(name,required) + }catch{ + throw new ODSystemError("ODCommandResponderInstanceOptions:getBoolean() slash command option not found!") + } + + }else if (this.#interaction instanceof discord.Message){ + const opt = this.#options.find((opt) => opt.type == "boolean" && opt.name == name) + if (opt && typeof opt.value == "boolean") return opt.value + else return null + + }else return null + } + getNumber(name:string,required:true): number + getNumber(name:string,required:false): number|null + getNumber(name:string,required:boolean){ + if (this.#interaction instanceof discord.ChatInputCommandInteraction){ + try { + return this.#interaction.options.getNumber(name,required) + }catch{ + throw new ODSystemError("ODCommandResponderInstanceOptions:getNumber() slash command option not found!") + } + + }else if (this.#interaction instanceof discord.Message){ + const opt = this.#options.find((opt) => opt.type == "number" && opt.name == name) + if (opt && typeof opt.value == "number") return opt.value + else return null + + }else return null + } + getChannel(name:string,required:true): discord.TextChannel|discord.VoiceChannel|discord.StageChannel|discord.NewsChannel|discord.MediaChannel|discord.ForumChannel|discord.CategoryChannel + getChannel(name:string,required:false): discord.TextChannel|discord.VoiceChannel|discord.StageChannel|discord.NewsChannel|discord.MediaChannel|discord.ForumChannel|discord.CategoryChannel|null + getChannel(name:string,required:boolean){ + if (this.#interaction instanceof discord.ChatInputCommandInteraction){ + try { + return this.#interaction.options.getChannel(name,required) + }catch{ + throw new ODSystemError("ODCommandResponderInstanceOptions:getChannel() slash command option not found!") + } + + }else if (this.#interaction instanceof discord.Message){ + const opt = this.#options.find((opt) => opt.type == "channel" && opt.name == name) + if (opt && (opt.value instanceof discord.TextChannel || opt.value instanceof discord.VoiceChannel || opt.value instanceof discord.StageChannel || opt.value instanceof discord.NewsChannel || opt.value instanceof discord.MediaChannel || opt.value instanceof discord.ForumChannel || opt.value instanceof discord.CategoryChannel)) return opt.value + else return null + + }else return null + } + getRole(name:string,required:true): discord.Role + getRole(name:string,required:false): discord.Role|null + getRole(name:string,required:boolean){ + if (this.#interaction instanceof discord.ChatInputCommandInteraction){ + try { + return this.#interaction.options.getRole(name,required) + }catch{ + throw new ODSystemError("ODCommandResponderInstanceOptions:getRole() slash command option not found!") + } + + }else if (this.#interaction instanceof discord.Message){ + const opt = this.#options.find((opt) => opt.type == "role" && opt.name == name) + if (opt && opt.value instanceof discord.Role) return opt.value + else return null + + }else return null + } + getUser(name:string,required:true): discord.User + getUser(name:string,required:false): discord.User|null + getUser(name:string,required:boolean){ + if (this.#interaction instanceof discord.ChatInputCommandInteraction){ + try { + return this.#interaction.options.getUser(name,required) + }catch{ + throw new ODSystemError("ODCommandResponderInstanceOptions:getUser() slash command option not found!") + } + + }else if (this.#interaction instanceof discord.Message){ + const opt = this.#options.find((opt) => opt.type == "user" && opt.name == name) + if (opt && opt.value instanceof discord.User) return opt.value + else return null + + }else return null + } + getGuildMember(name:string,required:true): discord.GuildMember + getGuildMember(name:string,required:false): discord.GuildMember|null + getGuildMember(name:string,required:boolean){ + if (this.#interaction instanceof discord.ChatInputCommandInteraction){ + try { + const member = this.#interaction.options.getMember(name) + if (!member && required) throw new ODSystemError("ODCommandResponderInstanceOptions:getGuildMember() slash command option not found!") + return member + }catch{ + throw new ODSystemError("ODCommandResponderInstanceOptions:getGuildMember() slash command option not found!") + } + + }else if (this.#interaction instanceof discord.Message){ + const opt = this.#options.find((opt) => opt.type == "guildmember" && opt.name == name) + if (opt && opt.value instanceof discord.GuildMember) return opt.value + else return null + + }else return null + } + getMentionable(name:string,required:true): discord.User|discord.Role + getMentionable(name:string,required:false): discord.User|discord.Role|null + getMentionable(name:string,required:boolean){ + if (this.#interaction instanceof discord.ChatInputCommandInteraction){ + try { + return this.#interaction.options.getMentionable(name,required) + }catch{ + throw new ODSystemError("ODCommandResponderInstanceOptions:getGuildMember() slash command option not found!") + } + + }else if (this.#interaction instanceof discord.Message){ + const opt = this.#options.find((opt) => opt.type == "mentionable" && opt.name == name) + if (opt && (opt.value instanceof discord.User || opt.value instanceof discord.Role)) return opt.value + else return null + + }else return null + } + getSubGroup(): string|null + getSubGroup(){ + if (this.#interaction instanceof discord.ChatInputCommandInteraction){ + try { + return this.#interaction.options.getSubcommandGroup(true) + }catch{ + throw new ODSystemError("ODCommandResponderInstanceOptions:getSubGroup() slash command option not found!") + } + + }else if (this.#interaction instanceof discord.Message && this.#cmd instanceof ODTextCommand){ + //0: name, 1:sub/group, 2:sub + const splittedName: string[] = this.#cmd.builder.name.split(" ") + return splittedName[1] ?? null + + }else return null + } + getSubCommand(): string|null + getSubCommand(){ + if (this.#interaction instanceof discord.ChatInputCommandInteraction){ + try { + return this.#interaction.options.getSubcommand(true) + }catch{ + throw new ODSystemError("ODCommandResponderInstanceOptions:getSubCommand() slash command option not found!") + } + + }else if (this.#interaction instanceof discord.Message && this.#cmd instanceof ODTextCommand){ + //0: name, 1:sub/group, 2:sub + const splittedName: string[] = this.#cmd.builder.name.split(" ") + + //return the second subcommand when there is a subgroup + if (splittedName.length > 2){ + return splittedName[2] ?? null + }else return splittedName[1] ?? null + + }else return null + } +} + +export class ODCommandResponderInstance { + interaction:discord.ChatInputCommandInteraction|discord.Message + cmd:ODSlashCommand|ODTextCommand + type: "message"|"interaction" + didReply: boolean = false + options: ODCommandResponderInstanceOptions + user: discord.User + member: discord.GuildMember|null + guild: discord.Guild|null + channel: discord.TextBasedChannel + + constructor(interaction:discord.ChatInputCommandInteraction|discord.Message, cmd:ODSlashCommand|ODTextCommand, errorCallback:ODResponderTimeoutErrorCallback|null, timeoutMs:number|null, options?:ODTextCommandInteractionOption[]){ + if (!interaction.channel) throw new ODSystemError("ODCommandResponderInstance: Unable to find interaction channel!") + this.interaction = interaction + this.cmd = cmd + this.type = (interaction instanceof discord.Message) ? "message" : "interaction" + this.options = new ODCommandResponderInstanceOptions(interaction,cmd,options) + this.user = (interaction instanceof discord.Message) ? interaction.author : interaction.user + this.member = (interaction.member instanceof discord.GuildMember) ? interaction.member : null + this.guild = interaction.guild + this.channel = interaction.channel + + + setTimeout(async () => { + if (!this.didReply){ + try { + if (!errorCallback){ + this.reply({id:new ODId("looks-like-we-got-an-error-here"), ephemeral:true, message:{ + content:":x: **Something went wrong while replying to this command!**" + }}) + }else{ + await errorCallback(this,(this.type == "interaction") ? "slash" : "text") + } + + }catch(err){ + process.emit("uncaughtException",err) + } + } + },timeoutMs ?? 2500) + } + + async reply(msg:ODMessageBuildResult): Promise> { + try { + if (this.type == "interaction" && this.interaction instanceof discord.ChatInputCommandInteraction){ + if (this.interaction.replied || this.interaction.deferred){ + const sent = await this.interaction.editReply(Object.assign(msg.message,{ephemeral:msg.ephemeral})) + this.didReply = true + return {success:true,message:sent} + }else{ + const sent = await this.interaction.reply(Object.assign(msg.message,{ephemeral:msg.ephemeral})) + this.didReply = true + return {success:true,message:await sent.fetch()} + } + }else if (this.type == "message" && this.interaction instanceof discord.Message){ + const sent = await this.interaction.channel.send(msg.message) + this.didReply = true + return {success:true,message:sent} + }else return {success:false,message:null} + }catch{ + return {success:false,message:null} + } + } + async defer(ephemeral:boolean){ + if (this.type != "interaction" || !(this.interaction instanceof discord.ChatInputCommandInteraction)) return false + if (this.interaction.deferred) return false + await this.interaction.deferReply({ephemeral}) + this.didReply = true + return true + } + async modal(modal:ODModalBuildResult){ + if (this.type != "interaction" || !(this.interaction instanceof discord.ChatInputCommandInteraction)) return false + this.interaction.showModal(modal.modal) + this.didReply = true + return true + } +} + +export class ODCommandResponder extends ODResponderImplementation { + /**The prefix of the text command needs to match this */ + prefix: string + + constructor(id:ODValidId, prefix:string, match:string|RegExp, callback?:ODWorkerCallback, priority?:number, callbackId?:ODValidId){ + super(id,match,callback,priority,callbackId) + this.prefix = prefix + } + + /**Respond to this command */ + async respond(instance:ODCommandResponderInstance, source:Source, params:Params){ + //wait for workers to finish + await this.workers.executeWorkers(instance,source,params) + } +} + +export class ODButtonResponderManager extends ODManager> { + #client: ODClientManager + #timeoutErrorCallback: ODResponderTimeoutErrorCallback|null = null + #timeoutMs: number|null = null + #listeners: ((interaction:discord.ButtonInteraction) => void)[] = [] + + constructor(debug:ODDebugger, debugname:string, client:ODClientManager){ + super(debug,debugname) + this.#client = client + + this.#client.client.on("interactionCreate",(interaction) => { + if (!interaction.isButton()) return + this.#listeners.forEach((cb) => cb(interaction)) + }) + } + + /**Set the message to send when the response times out! */ + setTimeoutErrorCallback(callback:ODResponderTimeoutErrorCallback|null, ms:number|null){ + this.#timeoutErrorCallback = callback + this.#timeoutMs = ms + } + + add(data:ODButtonResponder<"button",any>, overwrite?:boolean){ + const res = super.add(data,overwrite) + + this.#listeners.push((interaction) => { + const newData = this.get(data.id) + if (!newData) return + if ((typeof newData.match == "string") ? interaction.customId == newData.match : newData.match.test(interaction.customId)) newData.respond(new ODButtonResponderInstance(interaction,this.#timeoutErrorCallback,this.#timeoutMs),"button",{}) + }) + + return res + } +} + +export class ODButtonResponderInstance { + interaction:discord.ButtonInteraction + didReply: boolean = false + user: discord.User + member: discord.GuildMember|null + guild: discord.Guild|null + channel: discord.TextBasedChannel + message: discord.Message + + constructor(interaction:discord.ButtonInteraction, errorCallback:ODResponderTimeoutErrorCallback|null, timeoutMs:number|null){ + if (!interaction.channel) throw new ODSystemError("ODButtonResponderInstance: Unable to find interaction channel!") + this.interaction = interaction + this.user = interaction.user + this.member = (interaction.member instanceof discord.GuildMember) ? interaction.member : null + this.guild = interaction.guild + this.channel = interaction.channel + this.message = interaction.message + + setTimeout(async () => { + if (!this.didReply){ + try { + if (!errorCallback){ + this.reply({id:new ODId("looks-like-we-got-an-error-here"), ephemeral:true, message:{ + content:":x: **Something went wrong while replying to this button!**" + }}) + }else{ + await errorCallback(this,"button") + } + + }catch(err){ + process.emit("uncaughtException",err) + } + } + },timeoutMs ?? 2500) + } + + async reply(msg:ODMessageBuildResult): Promise> { + try{ + if (this.interaction.replied || this.interaction.deferred){ + const sent = await this.interaction.editReply(Object.assign(msg.message,{ephemeral:msg.ephemeral})) + this.didReply = true + return {success:true,message:sent} + }else{ + const sent = await this.interaction.reply(Object.assign(msg.message,{ephemeral:msg.ephemeral})) + this.didReply = true + return {success:true,message:await sent.fetch()} + } + }catch{ + return {success:false,message:null} + } + } + async update(msg:ODMessageBuildResult): Promise> { + try{ + if (this.interaction.replied || this.interaction.deferred){ + const sent = await this.interaction.editReply(Object.assign(msg.message,{ephemeral:msg.ephemeral})) + this.didReply = true + return {success:true,message:await sent.fetch()} + }else{ + const sent = await this.interaction.update(Object.assign(msg.message,{ephemeral:msg.ephemeral})) + this.didReply = true + return {success:true,message:await sent.fetch()} + } + }catch{ + return {success:false,message:null} + } + } + async defer(type:"reply"|"update", ephemeral:boolean){ + if (this.interaction.deferred) return false + if (type == "reply"){ + await this.interaction.deferReply({ephemeral}) + }else{ + await this.interaction.deferUpdate() + } + this.didReply = true + return true + } + async modal(modal:ODModalBuildResult){ + this.interaction.showModal(modal.modal) + this.didReply = true + return true + } + + getMessageComponent(type:"button",id:string|RegExp): discord.ButtonComponent|null + getMessageComponent(type:"string-dropdown",id:string|RegExp): discord.StringSelectMenuComponent|null + getMessageComponent(type:"user-dropdown",id:string|RegExp): discord.UserSelectMenuComponent|null + getMessageComponent(type:"channel-dropdown",id:string|RegExp): discord.ChannelSelectMenuComponent|null + getMessageComponent(type:"role-dropdown",id:string|RegExp): discord.RoleSelectMenuComponent|null + getMessageComponent(type:"mentionable-dropdown",id:string|RegExp): discord.MentionableSelectMenuComponent|null + + getMessageComponent(type:"button"|"string-dropdown"|"user-dropdown"|"channel-dropdown"|"role-dropdown"|"mentionable-dropdown", id:string|RegExp): discord.ButtonComponent|discord.StringSelectMenuComponent|discord.RoleSelectMenuComponent|discord.ChannelSelectMenuComponent|discord.MentionableSelectMenuComponent|discord.UserSelectMenuComponent|null { + let result: discord.ButtonComponent|discord.StringSelectMenuComponent|discord.RoleSelectMenuComponent|discord.ChannelSelectMenuComponent|discord.MentionableSelectMenuComponent|discord.UserSelectMenuComponent|null = null + this.message.components.forEach((row) => { + row.components.forEach((component) => { + if (type == "button" && component.type == discord.ComponentType.Button && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component + else if (type == "string-dropdown" && component.type == discord.ComponentType.StringSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component + else if (type == "user-dropdown" && component.type == discord.ComponentType.UserSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component + else if (type == "channel-dropdown" && component.type == discord.ComponentType.ChannelSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component + else if (type == "role-dropdown" && component.type == discord.ComponentType.RoleSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component + else if (type == "mentionable-dropdown" && component.type == discord.ComponentType.MentionableSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component + }) + }) + + return result + } + + getMessageEmbed(): discord.Embed|null { + return this.message.embeds[0] ?? null + } +} + +export class ODButtonResponder extends ODResponderImplementation { + /**Respond to this button */ + async respond(instance:ODButtonResponderInstance, source:Source, params:Params){ + //wait for workers to finish + await this.workers.executeWorkers(instance,source,params) + } +} + +export class ODDropdownResponderManager extends ODManager> { + #client: ODClientManager + #timeoutErrorCallback: ODResponderTimeoutErrorCallback|null = null + #timeoutMs: number|null = null + #listeners: ((interaction:discord.AnySelectMenuInteraction) => void)[] = [] + + constructor(debug:ODDebugger, debugname:string, client:ODClientManager){ + super(debug,debugname) + this.#client = client + + this.#client.client.on("interactionCreate",(interaction) => { + if (!interaction.isAnySelectMenu()) return + this.#listeners.forEach((cb) => cb(interaction)) + }) + } + + /**Set the message to send when the response times out! */ + setTimeoutErrorCallback(callback:ODResponderTimeoutErrorCallback|null, ms:number|null){ + this.#timeoutErrorCallback = callback + this.#timeoutMs = ms + } + + add(data:ODDropdownResponder<"dropdown",any>, overwrite?:boolean){ + const res = super.add(data,overwrite) + + this.#listeners.push((interaction) => { + const newData = this.get(data.id) + if (!newData) return + if ((typeof newData.match == "string") ? interaction.customId == newData.match : newData.match.test(interaction.customId)) newData.respond(new ODDropdownResponderInstance(interaction,this.#timeoutErrorCallback,this.#timeoutMs),"dropdown",{}) + }) + + return res + } +} + +export class ODDropdownResponderInstanceValues { + #interaction:discord.AnySelectMenuInteraction + #type: ODDropdownData["type"] + + constructor(interaction:discord.AnySelectMenuInteraction, type:ODDropdownData["type"]){ + this.#interaction = interaction + this.#type = type + + if (interaction.isChannelSelectMenu()){ + interaction.values + } + } + getStringValues(): string[] { + try { + return this.#interaction.values + }catch{ + throw new ODSystemError("ODDropdownResponderInstanceValues:getStringValues() invalid values!") + } + } + async getRoleValues(): Promise { + if (this.#type != "role") throw new ODSystemError("ODDropdownResponderInstanceValues:getRoleValues() dropdown type isn't role!") + try { + const result: discord.Role[] = [] + for (const id of this.#interaction.values){ + if (!this.#interaction.guild) break + const role = await this.#interaction.guild.roles.fetch(id) + if (role) result.push(role) + } + return result + }catch{ + throw new ODSystemError("ODDropdownResponderInstanceValues:getRoleValues() invalid values!") + } + } + async getUserValues(): Promise { + if (this.#type != "role") throw new ODSystemError("ODDropdownResponderInstanceValues:getUserValues() dropdown type isn't user!") + try { + const result: discord.User[] = [] + for (const id of this.#interaction.values){ + const user = await this.#interaction.client.users.fetch(id) + if (user) result.push(user) + } + return result + }catch{ + throw new ODSystemError("ODDropdownResponderInstanceValues:getUserValues() invalid values!") + } + } + async getChannelValues(): Promise { + if (this.#type != "role") throw new ODSystemError("ODDropdownResponderInstanceValues:getChannelValues() dropdown type isn't channel!") + try { + const result: discord.GuildBasedChannel[] = [] + for (const id of this.#interaction.values){ + if (!this.#interaction.guild) break + const guild = await this.#interaction.guild.channels.fetch(id) + if (guild) result.push(guild) + } + return result + }catch{ + throw new ODSystemError("ODDropdownResponderInstanceValues:getChannelValues() invalid values!") + } + } +} + +export class ODDropdownResponderInstance { + interaction:discord.AnySelectMenuInteraction + didReply: boolean = false + type: ODDropdownData["type"] + values: ODDropdownResponderInstanceValues + user: discord.User + member: discord.GuildMember|null + guild: discord.Guild|null + channel: discord.TextBasedChannel + message: discord.Message + + constructor(interaction:discord.AnySelectMenuInteraction, errorCallback:ODResponderTimeoutErrorCallback|null, timeoutMs:number|null){ + if (!interaction.channel) throw new ODSystemError("ODDropdownResponderInstance: Unable to find interaction channel!") + this.interaction = interaction + if (interaction.isStringSelectMenu()){ + this.type = "string" + }else if (interaction.isRoleSelectMenu()){ + this.type = "role" + }else if (interaction.isUserSelectMenu()){ + this.type = "user" + }else if (interaction.isChannelSelectMenu()){ + this.type = "channel" + }else if (interaction.isMentionableSelectMenu()){ + this.type = "mentionable" + }else throw new ODSystemError("ODDropdownResponderInstance: invalid dropdown type!") + + this.values = new ODDropdownResponderInstanceValues(interaction,this.type) + this.user = interaction.user + this.member = (interaction.member instanceof discord.GuildMember) ? interaction.member : null + this.guild = interaction.guild + this.channel = interaction.channel + this.message = interaction.message + + setTimeout(async () => { + if (!this.didReply){ + try { + if (!errorCallback){ + this.reply({id:new ODId("looks-like-we-got-an-error-here"), ephemeral:true, message:{ + content:":x: **Something went wrong while replying to this dropdown!**" + }}) + }else{ + await errorCallback(this,"dropdown") + } + + }catch(err){ + process.emit("uncaughtException",err) + } + } + },timeoutMs ?? 2500) + } + + async reply(msg:ODMessageBuildResult): Promise> { + try { + if (this.interaction.replied || this.interaction.deferred){ + const sent = await this.interaction.editReply(Object.assign(msg.message,{ephemeral:msg.ephemeral})) + this.didReply = true + return {success:true,message:sent} + }else{ + const sent = await this.interaction.reply(Object.assign(msg.message,{ephemeral:msg.ephemeral})) + this.didReply = true + return {success:true,message:await sent.fetch()} + } + }catch{ + return {success:false,message:null} + } + } + async update(msg:ODMessageBuildResult): Promise> { + try{ + if (this.interaction.replied || this.interaction.deferred){ + const sent = await this.interaction.editReply(Object.assign(msg.message,{ephemeral:msg.ephemeral})) + this.didReply = true + return {success:true,message:await sent.fetch()} + }else{ + const sent = await this.interaction.update(Object.assign(msg.message,{ephemeral:msg.ephemeral})) + this.didReply = true + return {success:true,message:await sent.fetch()} + } + }catch{ + return {success:false,message:null} + } + } + async defer(type:"reply"|"update", ephemeral:boolean){ + if (this.interaction.deferred) return false + if (type == "reply"){ + await this.interaction.deferReply({ephemeral}) + }else{ + await this.interaction.deferUpdate() + } + this.didReply = true + return true + } + async modal(modal:ODModalBuildResult){ + this.interaction.showModal(modal.modal) + this.didReply = true + return true + } + + getMessageComponent(type:"button",id:string|RegExp): discord.ButtonComponent|null + getMessageComponent(type:"string-dropdown",id:string|RegExp): discord.StringSelectMenuComponent|null + getMessageComponent(type:"user-dropdown",id:string|RegExp): discord.UserSelectMenuComponent|null + getMessageComponent(type:"channel-dropdown",id:string|RegExp): discord.ChannelSelectMenuComponent|null + getMessageComponent(type:"role-dropdown",id:string|RegExp): discord.RoleSelectMenuComponent|null + getMessageComponent(type:"mentionable-dropdown",id:string|RegExp): discord.MentionableSelectMenuComponent|null + + getMessageComponent(type:"button"|"string-dropdown"|"user-dropdown"|"channel-dropdown"|"role-dropdown"|"mentionable-dropdown", id:string|RegExp): discord.ButtonComponent|discord.StringSelectMenuComponent|discord.RoleSelectMenuComponent|discord.ChannelSelectMenuComponent|discord.MentionableSelectMenuComponent|discord.UserSelectMenuComponent|null { + let result: discord.ButtonComponent|discord.StringSelectMenuComponent|discord.RoleSelectMenuComponent|discord.ChannelSelectMenuComponent|discord.MentionableSelectMenuComponent|discord.UserSelectMenuComponent|null = null + this.message.components.forEach((row) => { + row.components.forEach((component) => { + if (type == "button" && component.type == discord.ComponentType.Button && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component + else if (type == "string-dropdown" && component.type == discord.ComponentType.StringSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component + else if (type == "user-dropdown" && component.type == discord.ComponentType.UserSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component + else if (type == "channel-dropdown" && component.type == discord.ComponentType.ChannelSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component + else if (type == "role-dropdown" && component.type == discord.ComponentType.RoleSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component + else if (type == "mentionable-dropdown" && component.type == discord.ComponentType.MentionableSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component + }) + }) + + return result + } + + getMessageEmbed(): discord.Embed|null { + return this.message.embeds[0] ?? null + } +} + +export class ODDropdownResponder extends ODResponderImplementation { + /**Respond to this dropdown */ + async respond(instance:ODDropdownResponderInstance, source:Source, params:Params){ + //wait for workers to finish + await this.workers.executeWorkers(instance,source,params) + } +} + +export class ODModalResponderManager extends ODManager> { + #client: ODClientManager + #timeoutErrorCallback: ODResponderTimeoutErrorCallback|null = null + #timeoutMs: number|null = null + #listeners: ((interaction:discord.ModalSubmitInteraction) => void)[] = [] + + constructor(debug:ODDebugger, debugname:string, client:ODClientManager){ + super(debug,debugname) + this.#client = client + + this.#client.client.on("interactionCreate",(interaction) => { + if (!interaction.isModalSubmit()) return + this.#listeners.forEach((cb) => cb(interaction)) + }) + } + + /**Set the message to send when the response times out! */ + setTimeoutErrorCallback(callback:ODResponderTimeoutErrorCallback|null, ms:number|null){ + this.#timeoutErrorCallback = callback + this.#timeoutMs = ms + } + + add(data:ODModalResponder<"modal",any>, overwrite?:boolean){ + const res = super.add(data,overwrite) + + this.#listeners.push((interaction) => { + const newData = this.get(data.id) + if (!newData) return + if ((typeof newData.match == "string") ? interaction.customId == newData.match : newData.match.test(interaction.customId)) newData.respond(new ODModalResponderInstance(interaction,this.#timeoutErrorCallback,this.#timeoutMs),"modal",{}) + }) + + return res + } +} + +export class ODModalResponderInstanceValues { + #interaction:discord.ModalSubmitInteraction + + constructor(interaction:discord.ModalSubmitInteraction){ + this.#interaction = interaction + } + getTextField(name:string,required:true): string + getTextField(name:string,required:false): string|null + getTextField(name:string,required:boolean){ + try { + const data = this.#interaction.fields.getField(name,discord.ComponentType.TextInput) + if (!data && required) throw new ODSystemError("ODModalResponderInstanceValues:getTextField() field not found!") + return (data) ? data.value : null + }catch{ + throw new ODSystemError("ODModalResponderInstanceValues:getTextField() field not found!") + } + } +} + +export class ODModalResponderInstance { + interaction:discord.ModalSubmitInteraction + didReply: boolean = false + values: ODModalResponderInstanceValues + user: discord.User + member: discord.GuildMember|null + guild: discord.Guild|null + channel: discord.TextBasedChannel|null + + constructor(interaction:discord.ModalSubmitInteraction, errorCallback:ODResponderTimeoutErrorCallback|null, timeoutMs:number|null){ + this.interaction = interaction + this.values = new ODModalResponderInstanceValues(interaction) + this.user = interaction.user + this.member = (interaction.member instanceof discord.GuildMember) ? interaction.member : null + this.guild = interaction.guild + this.channel = interaction.channel + + setTimeout(async () => { + if (!this.didReply){ + try { + if (!errorCallback){ + this.reply({id:new ODId("looks-like-we-got-an-error-here"), ephemeral:true, message:{ + content:":x: **Something went wrong while replying to this modal!**" + }}) + }else{ + await errorCallback(this,"modal") + } + + }catch(err){ + process.emit("uncaughtException",err) + } + } + },timeoutMs ?? 2500) + } + + async reply(msg:ODMessageBuildResult): Promise> { + try{ + if (this.interaction.replied || this.interaction.deferred){ + const sent = await this.interaction.editReply(Object.assign(msg.message,{ephemeral:msg.ephemeral})) + this.didReply = true + return {success:true,message:sent} + }else{ + const sent = await this.interaction.reply(Object.assign(msg.message,{ephemeral:msg.ephemeral})) + this.didReply = true + return {success:true,message:await sent.fetch()} + } + }catch{ + return {success:false,message:null} + } + } + async update(msg:ODMessageBuildResult): Promise> { + try{ + if (this.interaction.replied || this.interaction.deferred){ + const sent = await this.interaction.editReply(Object.assign(msg.message,{ephemeral:msg.ephemeral})) + this.didReply = true + return {success:true,message:await sent.fetch()} + }else throw new ODSystemError() + }catch{ + return {success:false,message:null} + } + } + async defer(type:"reply"|"update", ephemeral:boolean){ + if (this.interaction.deferred) return false + if (type == "reply"){ + await this.interaction.deferReply({ephemeral}) + }else{ + await this.interaction.deferUpdate() + } + this.didReply = true + return true + } +} + +export class ODModalResponder extends ODResponderImplementation { + /**Respond to this modal */ + async respond(instance:ODModalResponderInstance, source:Source, params:Params){ + //wait for workers to finish + await this.workers.executeWorkers(instance,source,params) + } +} \ No newline at end of file diff --git a/src/core/api/modules/session.ts b/src/core/api/modules/session.ts new file mode 100644 index 0000000..9aed68b --- /dev/null +++ b/src/core/api/modules/session.ts @@ -0,0 +1,155 @@ +/////////////////////////////////////// +//SESSION MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODManagerData, ODValidId } from "./base" +import { ODDebugger } from "./console" +import * as crypto from "crypto" + +/**## ODSessionManager `class` + * This is an open ticket session manager. + * + * It contains all sessions in open ticket. Sessions are a sort of temporary storage which will be cleared when the bot stops. + * Data in sessions have a randomly generated key which will always be unique. + * + * Visit the `ODSession` class for more info + */ +export class ODSessionManager extends ODManager { + constructor(debug:ODDebugger){ + super(debug,"session") + } +} + +/**## ODSessionInstance `interface` + * This interface represents a single session instance. It contains an id, data & some dates. + */ +export interface ODSessionInstance { + /**The id of this session instance. */ + id:string, + /**The creation date of this session instance. */ + creation:number, + /**The custom amount of minutes before this session expires. */ + timeout:number|null, + /**This is the data from this session instance */ + data:any +} + +/**## ODSessionTimeoutCallback `type` + * This is the callback used for session timeout listeners. + */ +export type ODSessionTimeoutCallback = (id:string, timeout:"default"|"custom", data:any, creation:Date) => void + +/**## ODSession `class` + * This is an open ticket session. + * + * It can be used to create 100% unique id's for usage in the bot. An id can also store additional data which isn't saved to the filesystem. + * You can almost compare it to the PHP session system. + */ +export class ODSession extends ODManagerData { + /**The history of previously generated instance ids. Used to reduce the risk of generating the same id twice. */ + #idHistory: string[] = [] + /**The max length of the instance id history. */ + #maxIdHistoryLength: number = 500 + /**An array of all the currently active session instances. */ + sessions: ODSessionInstance[] = [] + /**The default amount of minutes before a session automatically stops. */ + timeoutMinutes: number = 30 + /**The id of the auto-timeout session checker interval */ + #intervalId: NodeJS.Timeout + /**Listeners for when a session times-out. */ + #timeoutListeners: ODSessionTimeoutCallback[] = [] + + constructor(id:ODValidId, intervalSeconds?:number){ + super(id) + + //create the auto-timeout session checker + this.#intervalId = setInterval(() => { + const deletableSessions: {instance:ODSessionInstance,reason:"default"|"custom"}[] = [] + + //collect all deletable sessions + this.sessions.forEach((session) => { + if (session.timeout && (new Date().getTime() - session.creation) > session.timeout*60000){ + //stop session => custom timeout + deletableSessions.push({instance:session,reason:"custom"}) + }else if (!session.timeout && (new Date().getTime() - session.creation) > this.timeoutMinutes*60000){ + //stop session => default timeout + deletableSessions.push({instance:session,reason:"default"}) + } + }) + + //permanently delete sessions + deletableSessions.forEach((session) => { + const index = this.sessions.findIndex((s) => s.id === session.instance.id) + this.sessions.splice(index,1) + + //emit timeout listeners + this.#timeoutListeners.forEach((cb) => cb(session.instance.id,session.reason,session.instance.data,new Date(session.instance.creation))) + }) + + },((intervalSeconds) ? (intervalSeconds * 1000) : 60000)) + } + + /**Create a unique hex id of 8 characters and add it to the instance id history */ + #createUniqueId(): string { + const hex = crypto.randomBytes(4).toString("hex") + if (this.#idHistory.includes(hex)){ + return this.#createUniqueId() + }else{ + this.#idHistory.push(hex) + if (this.#idHistory.length > this.#maxIdHistoryLength) this.#idHistory.shift() + return hex + } + } + /**Stop the global interval that automatically deletes timed-out sessions. (This action can't be reverted!) */ + stopAutoTimeout(){ + clearInterval(this.#intervalId) + } + + /**Start a session instance with data. Returns the unique id required to access the session. */ + start(data?:any): string { + const id = this.#createUniqueId() + this.sessions.push({ + id,data, + creation:new Date().getTime(), + timeout:null + }) + return id + } + /**Get the data of a session instance. Returns `null` when not found. */ + data(id:string): any|null { + const session = this.sessions.find((session) => session.id === id) + if (!session) return null + return session.data + } + /**Stop & delete a session instance. Returns `true` when sucessful. */ + stop(id:string): boolean { + const index = this.sessions.findIndex((session) => session.id === id) + if (index < 0) return false + this.sessions.splice(index,1) + return true + } + /**Update the data of a session instance. Returns `true` when sucessful. */ + update(id:string, data:any): boolean { + const session = this.sessions.find((session) => session.id === id) + if (!session) return false + session.data = data + return true + } + /**Change the global or session timeout minutes. Returns `true` when sucessful. */ + setTimeout(min:number, id?:string): boolean { + if (!id){ + //change global timeout minutes + this.timeoutMinutes = min + return true + }else{ + //change session instance timeout minutes + const session = this.sessions.find((session) => session.id === id) + if (!session) return false + session.timeout = min + return true + } + } + /**Listen for a session timeout (default or custom) */ + onTimeout(callback:ODSessionTimeoutCallback){ + this.#timeoutListeners.push(callback) + } +} \ No newline at end of file diff --git a/src/core/api/modules/startscreen.ts b/src/core/api/modules/startscreen.ts new file mode 100644 index 0000000..80c106d --- /dev/null +++ b/src/core/api/modules/startscreen.ts @@ -0,0 +1,219 @@ +/////////////////////////////////////// +//STARTSCREEN MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODManagerData, ODValidId } from "./base" +import { ODDebugger, ODError, ODLiveStatusManager } from "./console" +import { ODFlag } from "./flag" +import { ODPlugin, ODUnknownCrashedPlugin } from "./plugin" +import ansis from "ansis" + +export type ODStartScreenComponentRenderCallback = (location:number) => string|Promise + +export class ODStartScreenManager extends ODManager { + /**Alias to the Open Ticket debugger. */ + #debug: ODDebugger + /**Alias to the livestatus manager. */ + livestatus: ODLiveStatusManager + + constructor(debug:ODDebugger,livestatus:ODLiveStatusManager){ + super(debug,"startscreen component") + this.#debug = debug + this.livestatus = livestatus + } + + /**Get all components in sorted order. */ + getSortedComponents(priority:"ascending"|"descending"){ + return this.getAll().sort((a,b) => { + if (priority == "ascending") return a.priority-b.priority + else return b.priority-a.priority + }) + } + /**Render all startscreen components in priority order. */ + async renderAllComponents(){ + const components = this.getSortedComponents("descending") + + let location = 0 + for (const component of components){ + try { + const renderedText = await component.renderAll(location) + console.log(renderedText) + this.#debug.console.debugfile.writeText("[STARTSCREEN] Component: \""+component.id+"\"\n"+ansis.strip(renderedText)) + }catch(e){ + this.#debug.console.log("Unable to render \""+component.id+"\" startscreen component!","error") + this.#debug.console.debugfile.writeErrorMessage(new ODError(e,"uncaughtException")) + } + location++ + } + } +} + +export class ODStartScreenComponent extends ODManagerData { + priority: number + renderBefore: ODStartScreenComponentRenderCallback|null = null + render: ODStartScreenComponentRenderCallback + renderAfter: ODStartScreenComponentRenderCallback|null = null + + constructor(id:ODValidId, priority:number, render:ODStartScreenComponentRenderCallback){ + super(id) + this.priority = priority + this.render = render + } + + async renderAll(location:number){ + const textBefore = (this.renderBefore) ? await this.renderBefore(location) : "" + const text = await this.render(location) + const textAfter = (this.renderAfter) ? await this.renderAfter(location) : "" + return (textBefore ? textBefore+"\n" : "")+text+(textAfter ? "\n"+textAfter : "") + } +} + +export interface ODStartScreenProperty { + key:string, + value:string +} + +export class ODStartScreenLogoComponent extends ODStartScreenComponent { + logo: string[] + topPadding: boolean + bottomPadding: boolean + logoHexColor: string + + constructor(id:ODValidId, priority:number, logo:string[], topPadding?:boolean, bottomPadding?:boolean, logoHexColor?:string){ + super(id,priority,() => { + const renderedTop = (this.topPadding ? "\n" : "") + const renderedLogo = this.logo.join("\n") + const renderedBottom = (this.bottomPadding ? "\n" : "") + return ansis.hex(this.logoHexColor)(renderedTop+renderedLogo+renderedBottom) + }) + this.logo = logo + this.topPadding = topPadding ?? false + this.bottomPadding = bottomPadding ?? false + this.logoHexColor = logoHexColor ?? "#f8ba00" + } +} + +export interface ODStartScreenHeaderAlignmentSettings { + align:"center"|"left"|"right", + width:number|ODStartScreenComponent +} + +export class ODStartScreenHeaderComponent extends ODStartScreenComponent { + properties: ODStartScreenProperty[] + spacer: string + align: ODStartScreenHeaderAlignmentSettings|null + + constructor(id:ODValidId, priority:number, properties:ODStartScreenProperty[], spacer?:string, align?:ODStartScreenHeaderAlignmentSettings){ + super(id,priority,async () => { + const renderedProperties = ansis.bold(this.properties.map((prop) => prop.key+": "+prop.value).join(this.spacer)) + if (!this.align || this.align.align == "left"){ + return renderedProperties + }else if (this.align.align == "right"){ + const width = (typeof this.align.width == "number") ? this.align.width : ( + ansis.strip(await this.align.width.renderAll(0)).split("\n").map((row) => row.length).reduce((prev,curr) => { + if (prev < curr) return curr + else return prev + },0) + ) + const offset = width - ansis.strip(renderedProperties).length + if (offset < 0) return renderedProperties + else{ + return (" ".repeat(offset) + renderedProperties) + } + }else if (this.align.align == "center"){ + const width = (typeof this.align.width == "number") ? this.align.width : ( + ansis.strip(await this.align.width.renderAll(0)).split("\n").map((row) => row.length).reduce((prev,curr) => { + if (prev < curr) return curr + else return prev + }) + ) + const offset = Math.round((width - ansis.strip(renderedProperties).length)/2) + if (offset < 0) return renderedProperties + else{ + return (" ".repeat(offset) + renderedProperties) + } + } + return renderedProperties + }) + this.properties = properties + this.spacer = spacer ?? " - " + this.align = align ?? null + } +} + +export class ODStartScreenCategoryComponent extends ODStartScreenComponent { + name: string + renderIfEmpty: boolean + + constructor(id:ODValidId, priority:number, name:string, render:ODStartScreenComponentRenderCallback, renderIfEmpty?:boolean){ + super(id,priority,async (location) => { + const contents = await render(location) + if (contents != "" || this.renderIfEmpty){ + return ansis.bold.underline("\n"+name.toUpperCase()+(contents != "" ? ":\n" : ":")) + contents + }else return "" + }) + this.name = name + this.renderIfEmpty = renderIfEmpty ?? true + } +} + +export class ODStartScreenPropertiesCategoryComponent extends ODStartScreenCategoryComponent { + properties: ODStartScreenProperty[] + propertyHexColor: string + + constructor(id:ODValidId, priority:number, name:string, properties:ODStartScreenProperty[], propertyHexColor?:string, renderIfEmpty?:boolean){ + super(id,priority,name,() => { + return this.properties.map((prop) => ansis.hex(this.propertyHexColor)(prop.key+": ")+prop.value).join("\n") + },renderIfEmpty) + + this.properties = properties + this.propertyHexColor = propertyHexColor ?? "#f8ba00" + } +} + +export class ODStartScreenFlagsCategoryComponent extends ODStartScreenCategoryComponent { + flags: ODFlag[] + + constructor(id:ODValidId, priority:number, flags:ODFlag[]){ + super(id,priority,"flags",() => { + return this.flags.filter((flag) => (flag.value == true)).map((flag) => ansis.blue("["+flag.name+"] "+flag.description)).join("\n") + },false) + this.flags = flags + } +} + +export class ODStartScreenPluginsCategoryComponent extends ODStartScreenCategoryComponent { + plugins: ODPlugin[] + unknownCrashedPlugins: ODUnknownCrashedPlugin[] + + constructor(id:ODValidId, priority:number, plugins:ODPlugin[], unknownCrashedPlugins:ODUnknownCrashedPlugin[]){ + super(id,priority,"plugins",() => { + const renderedPlugins = this.plugins.sort((a,b) => b.priority-a.priority).map((plugin) => { + if (plugin.enabled && plugin.executed) return ansis.green("✅ ["+plugin.name+"] "+plugin.details.shortDescription) + else if (plugin.enabled && plugin.crashed) return ansis.red("❌ ["+plugin.name+"] "+plugin.details.shortDescription) + else return ansis.gray("💤 ["+plugin.name+"] "+plugin.details.shortDescription) + }) + const renderedUnknownPlugins = unknownCrashedPlugins.map((plugin) => ansis.red("❌ ["+plugin.name+"] "+plugin.description)) + return [...renderedPlugins,...renderedUnknownPlugins].join("\n") + },false) + this.plugins = plugins + this.unknownCrashedPlugins = unknownCrashedPlugins + } +} + +export class ODStartScreenLiveStatusCategoryComponent extends ODStartScreenCategoryComponent { + livestatus: ODLiveStatusManager + + constructor(id:ODValidId, priority:number, livestatus:ODLiveStatusManager){ + super(id,priority,"livestatus",async () => { + const messages = await this.livestatus.getAllMessages() + return this.livestatus.renderer.render(messages) + },false) + this.livestatus = livestatus + } +} + +export class ODStartScreenLogCategoryComponent extends ODStartScreenCategoryComponent { + constructor(id:ODValidId, priority:number){ + super(id,priority,"logs",() => "",true) + } +} \ No newline at end of file diff --git a/src/core/api/modules/stat.ts b/src/core/api/modules/stat.ts new file mode 100644 index 0000000..3ca36e0 --- /dev/null +++ b/src/core/api/modules/stat.ts @@ -0,0 +1,212 @@ +/////////////////////////////////////// +//STAT MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODManagerData, ODSystemError, ODValidId } from "./base" +import { ODDebugger } from "./console" +import { ODDatabase, ODJsonDatabaseStructure } from "./database" +import * as discord from "discord.js" + +export type ODValidStatValue = string|number|boolean +export type ODStatsManagerInitCallback = (database:ODJsonDatabaseStructure, deletables:ODJsonDatabaseStructure) => void|Promise +export type ODStatScopeSetMode = "set"|"increase"|"decrease" + +export class ODStatsManager extends ODManager { + /**Alias to open ticket debugger. */ + #debug: ODDebugger + /**Alias to open ticket stats database. */ + database: ODDatabase|null = null + /**All the listeners for the init event. */ + #initListeners: ODStatsManagerInitCallback[] = [] + + constructor(debug:ODDebugger){ + super(debug,"stat scope") + this.#debug = debug + } + + useDatabase(database:ODDatabase){ + this.database = database + } + add(data:ODStatScope, overwrite?:boolean): boolean { + data.useDebug(this.#debug,"stat") + if (this.database) data.useDatabase(this.database) + return super.add(data,overwrite) + } + async init(){ + if (!this.database) throw new ODSystemError("Unable to initialize stats scopes due to missing database!") + + //get all valid categories + const validCategories: string[] = [] + for (const scope of this.getAll()){ + validCategories.push(...scope.init()) + } + + //filter out the deletable stats + const deletableStats: ODJsonDatabaseStructure = [] + this.database.getAll().forEach((data) => { + if (!validCategories.includes(data.category)) deletableStats.push(data) + }) + + //do additional deletion + for (const cb of this.#initListeners){ + await cb(this.database.getAll(),deletableStats) + } + + //delete all deletable stats + deletableStats.forEach((data) => { + if (!this.database) return + this.database.delete(data.category,data.key) + }) + } + reset(){ + if (!this.database) return + this.database.getAll().forEach((data) => { + if (!this.database) return + this.database.delete(data.category,data.key) + }) + } + onInit(callback:ODStatsManagerInitCallback){ + this.#initListeners.push(callback) + } +} + +export class ODStatScope extends ODManager { + /**The id of this statistics scope. */ + id: ODId + /**Is this stat scope already initialized? */ + ready: boolean = false + /**Alias to open ticket stats database. */ + database: ODDatabase|null = null + /**The name of this scope (used in embed title) */ + name:string + + constructor(id:ODValidId, name:string){ + super() + this.id = new ODId(id) + this.name = name + } + + useDatabase(database:ODDatabase){ + this.database = database + } + getStat(id:ODValidId, scopeId:string): ODValidStatValue|null { + if (!this.database) return null + const newId = new ODId(id) + const data = this.database.get(this.id.value+"_"+newId.value,scopeId) + + if (typeof data == "undefined"){ + //set stats to default value & return + return this.resetStat(id,scopeId) + }else if (typeof data == "string" || typeof data == "boolean" || typeof data == "number"){ + //return value received from database + return data + } + //return null on error + return null + } + setStat(id:ODValidId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean { + if (!this.database) return false + const stat = this.get(id) + if (!stat) return false + if (mode == "set" || typeof value != "number"){ + this.database.set(this.id.value+"_"+stat.id.value,scopeId,value) + }else if (mode == "increase"){ + const currentValue = this.getStat(id,scopeId) + if (typeof currentValue != "number") this.database.set(this.id.value+"_"+stat.id.value,scopeId,0) + else this.database.set(this.id.value+"_"+stat.id.value,scopeId,currentValue+value) + }else if (mode == "decrease"){ + const currentValue = this.getStat(id,scopeId) + if (typeof currentValue != "number") this.database.set(this.id.value+"_"+stat.id.value,scopeId,0) + else this.database.set(this.id.value+"_"+stat.id.value,scopeId,currentValue-value) + } + return true + } + resetStat(id:ODValidId, scopeId:string): ODValidStatValue|null { + if (!this.database) return null + const stat = this.get(id) + if (!stat) return null + if (stat.value != null) this.database.set(this.id.value+"_"+stat.id.value,scopeId,stat.value) + return stat.value + } + init(): string[] { + //get all valid stats categories + this.ready = true + return this.getAll().map((stat) => this.id.value+"_"+stat.id.value) + } + async render(scopeId:string, guild:discord.Guild, channel:discord.TextBasedChannel, user:discord.User): Promise { + //sort from high priority to low + const derefArray = [...this.getAll()] + derefArray.sort((a,b) => { + return b.priority-a.priority + }) + const result: string[] = [] + + for (const stat of derefArray){ + try { + if (stat instanceof ODDynamicStat){ + //dynamic render (without value) + result.push(await stat.render("",scopeId,guild,channel,user)) + }else{ + //normal render (with value) + const value = this.getStat(stat.id,scopeId) + if (value != null) result.push(await stat.render(value,scopeId,guild,channel,user)) + } + + }catch(err){ + process.emit("uncaughtException",err) + } + } + + return result.filter((stat) => stat !== "").join("\n") + } +} + +export class ODStatGlobalScope extends ODStatScope { + getStat(id:ODValidId): ODValidStatValue|null { + return super.getStat(id,"GLOBAL") + } + setStat(id:ODValidId, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean { + return super.setStat(id,"GLOBAL",value,mode) + } + resetStat(id:ODValidId): ODValidStatValue|null { + return super.resetStat(id,"GLOBAL") + } + render(scopeId:"GLOBAL", guild:discord.Guild, channel:discord.TextBasedChannel, user: discord.User): Promise { + return super.render("GLOBAL",guild,channel,user) + } +} + +export type ODStatRenderer = (value:ODValidStatValue, scopeId:string, guild:discord.Guild, channel:discord.TextBasedChannel, user:discord.User) => string|Promise + +export class ODStat extends ODManagerData { + priority: number + render: ODStatRenderer + value: ODValidStatValue|null + + constructor(id:ODValidId, priority:number, render:ODStatRenderer, value?:ODValidStatValue){ + super(id) + this.priority = priority + this.render = render + this.value = value ?? null + } +} + +export class ODBasicStat extends ODStat { + name: string + + constructor(id:ODValidId, priority:number, name:string, value:ODValidStatValue){ + super(id,priority,(value) => { + return ""+name+": `"+value.toString()+"`" + },value) + this.name = name + } +} + +export type ODDynamicStatRenderer = (scopeId:string, guild:discord.Guild, channel:discord.TextBasedChannel, user:discord.User) => string|Promise + +export class ODDynamicStat extends ODStat { + constructor(id:ODValidId, priority:number, render:ODDynamicStatRenderer){ + super(id,priority,(value,scopeId,guild,channel,user) => { + return render(scopeId,guild,channel,user) + }) + } +} \ No newline at end of file diff --git a/src/core/api/modules/verifybar.ts b/src/core/api/modules/verifybar.ts new file mode 100644 index 0000000..6d8a914 --- /dev/null +++ b/src/core/api/modules/verifybar.ts @@ -0,0 +1,43 @@ +/////////////////////////////////////// +//VERIFYBAR MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODManagerData, ODValidId } from "./base" +import { ODMessage } from "./builder" +import { ODDebugger } from "./console" +import { ODButtonResponderInstance } from "./responder" +import * as discord from "discord.js" +import { ODWorkerManager } from "./worker" + +export type ODVerifyBarCallback = (responder:ODButtonResponderInstance,customData?:string) => void|Promise + +export class ODVerifyBar extends ODManagerData { + success: ODWorkerManager|null}> + failure: ODWorkerManager|null}> + message: ODMessage<"verifybar",{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,verifybar:ODVerifyBar,originalMessage:discord.Message}> + enabled: boolean + + constructor(id:ODValidId, message:ODMessage<"verifybar",{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,originalMessage:discord.Message}>, enabled?:boolean){ + super(id) + this.success = new ODWorkerManager("descending") + this.failure = new ODWorkerManager("descending") + this.message = message + this.enabled = enabled ?? true + } + + async activate(responder:ODButtonResponderInstance){ + if (this.enabled){ + //show verifybar + const {guild,channel,user,message} = responder + await responder.update(await this.message.build("verifybar",{guild,channel,user,verifybar:this,originalMessage:message})) + }else{ + //instant success + if (this.success) await this.success.executeWorkers(responder,"verifybar",{data:null,verifybarMessage:null}) + } + } +} + +export class ODVerifyBarManager extends ODManager { + constructor(debug:ODDebugger){ + super(debug,"verifybar") + } +} \ No newline at end of file diff --git a/src/core/api/modules/worker.ts b/src/core/api/modules/worker.ts new file mode 100644 index 0000000..617beb1 --- /dev/null +++ b/src/core/api/modules/worker.ts @@ -0,0 +1,93 @@ +/////////////////////////////////////// +//WORKER MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODManagerData, ODValidId } from "./base" + +/**## ODWorkerCallback `type` + * This is the callback used in `ODWorker`! + */ +export type ODWorkerCallback = (instance:Instance, params:Params, source:Source, cancel:() => void) => void|Promise + +/**## ODWorker `class` + * This is an open ticket worker. + * + * You can compare it with a normal javascript callback, but slightly more advanced! + * + * - It has an `id` for identification of the function + * - A `priority` to know when to execute this callback (related to others) + * - It knows who called this callback (`source`) + * - And much more! + */ +export class ODWorker extends ODManagerData { + /**The priority of this worker */ + priority: number + /**The main callback of this worker */ + callback: ODWorkerCallback + + constructor(id:ODValidId, priority:number, callback:ODWorkerCallback){ + super(id) + this.priority = priority + this.callback = callback + } +} + +/**## ODWorker `class` + * This is an open ticket worker manager. + * + * It manages & executes `ODWorker`'s in the correct order. + * + * You will probably register a custom worker in this class to create a message or button. + */ +export class ODWorkerManager extends ODManager> { + /**The order of execution for workers inside this manager. */ + #priorityOrder: "ascending"|"descending" + /**The backup worker will be executed when one of the workers fails or cancels execution. */ + backupWorker: ODWorker<{reason:"error"|"cancel"},Source,Params>|null = null + + constructor(priorityOrder:"ascending"|"descending"){ + super() + this.#priorityOrder = priorityOrder + } + + /**Get all workers in sorted order. */ + getSortedWorkers(priority:"ascending"|"descending"){ + const derefArray = [...this.getAll()] + + return derefArray.sort((a,b) => { + if (priority == "ascending") return a.priority-b.priority + else return b.priority-a.priority + }) + } + /**Execute all workers on an instance using the given source & parameters. */ + async executeWorkers(instance:Instance, source:Source, params:Params){ + const derefParams = {...params} + const workers = this.getSortedWorkers(this.#priorityOrder) + let didCancel = false + let didCrash = false + + for (const worker of workers){ + if (didCancel) break + try { + await worker.callback(instance,derefParams,source,() => { + didCancel = true + }) + }catch(err){ + process.emit("uncaughtException",err) + didCrash = true + } + } + if (didCancel && this.backupWorker){ + try{ + await this.backupWorker.callback({reason:"cancel"},derefParams,source,() => {}) + }catch(err){ + process.emit("uncaughtException",err) + } + }else if (didCrash && this.backupWorker){ + try{ + await this.backupWorker.callback({reason:"error"},derefParams,source,() => {}) + }catch(err){ + process.emit("uncaughtException",err) + } + } + } +} \ No newline at end of file diff --git a/src/core/api/openticket/blacklist.ts b/src/core/api/openticket/blacklist.ts new file mode 100644 index 0000000..4340139 --- /dev/null +++ b/src/core/api/openticket/blacklist.ts @@ -0,0 +1,28 @@ +/////////////////////////////////////// +//OPENTICKET BLACKLIST MODULE +/////////////////////////////////////// +import { ODManager, ODManagerData, ODValidId } from "../modules/base" +import { ODDebugger } from "../modules/console" + +export class ODBlacklist extends ODManagerData { + #reason: string|null + + constructor(id:ODValidId,reason:string|null){ + super(id) + this.#reason = reason + } + + set reason(reason:string|null) { + this.#reason = reason + this._change() + } + get reason(){ + return this.#reason + } +} + +export class ODBlacklistManager extends ODManager { + constructor(debug:ODDebugger){ + super(debug,"blacklist") + } +} \ No newline at end of file diff --git a/src/core/api/openticket/option.ts b/src/core/api/openticket/option.ts new file mode 100644 index 0000000..ffc967d --- /dev/null +++ b/src/core/api/openticket/option.ts @@ -0,0 +1,421 @@ +/////////////////////////////////////// +//OPENTICKET OPTION MODULE +/////////////////////////////////////// +import { ODDatabase } from "../modules/database" +import { ODJsonConfig_DefaultOptionEmbedSettingsType, ODJsonConfig_DefaultOptionPingSettingsType } from "../defaults/config" +import { ODId, ODManager, ODValidJsonType, ODValidId, ODVersion, ODValidButtonColor, ODManagerData, ODSystemError } from "../modules/base" +import { ODDebugger } from "../modules/console" +import * as discord from "discord.js" +import * as crypto from "crypto" +import { OTRoleUpdateMode } from "./role" + +export class ODOptionManager extends ODManager { + #debug: ODDebugger + suffix: ODOptionSuffixManager + + constructor(debug:ODDebugger){ + super(debug,"option") + this.#debug = debug + this.suffix = new ODOptionSuffixManager(debug) + } + + add(data:ODOption, overwrite?:boolean): boolean { + data.useDebug(this.#debug,"option data") + return super.add(data,overwrite) + } +} + +export interface ODOptionDataJson { + id:string, + value:ODValidJsonType +} + +export interface ODOptionJson { + id:string, + type:string, + version:string, + data:ODOptionDataJson[] +} + +export class ODOption extends ODManager> { + id:ODId + type: string + + constructor(id:ODValidId, type:string, data:ODOptionData[]){ + super() + this.id = new ODId(id) + this.type = type + data.forEach((data) => { + this.add(data) + }) + } + + toJson(version:ODVersion): ODOptionJson { + const data = this.getAll().map((data) => { + return { + id:data.id.toString(), + value:data.value + } + }) + + return { + id:this.id.toString(), + type:this.type, + version:version.toString(), + data + } + } + + static fromJson(json:ODOptionJson): ODOption { + return new ODOption(json.id,json.type,json.data.map((data) => new ODOptionData(data.id,data.value))) + } +} + +export class ODOptionData extends ODManagerData { + #value: DataType + + constructor(id:ODValidId, value:DataType){ + super(id) + this.#value = value + } + + set value(value:DataType){ + this.#value = value + this._change() + } + get value(): DataType { + return this.#value + } + /**Refresh the database. Is only required to be used when updating `ODOptionData` with an object/array as value. */ + refreshDatabase(){ + this._change() + } +} + +/**## ODTicketOptionIds `type` + * This type is an array of ids available in the `ODTicketOption` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODTicketOptionIds { + "openticket:name":ODOptionData, + "openticket:description":ODOptionData, + + "openticket:button-emoji":ODOptionData, + "openticket:button-label":ODOptionData, + "openticket:button-color":ODOptionData, + + "openticket:admins":ODOptionData, + "openticket:admins-readonly":ODOptionData, + "openticket:allow-blacklisted-users":ODOptionData, + "openticket:questions":ODOptionData, + + "openticket:channel-prefix":ODOptionData, + "openticket:channel-suffix":ODOptionData<"user-name"|"user-id"|"random-number"|"random-hex"|"counter-dynamic"|"counter-fixed">, + "openticket:channel-category":ODOptionData, + "openticket:channel-category-closed":ODOptionData, + "openticket:channel-category-backup":ODOptionData, + "openticket:channel-categories-claimed":ODOptionData<{user:string,category:string}[]>, + "openticket:channel-description":ODOptionData, + + "openticket:dm-message-enabled":ODOptionData, + "openticket:dm-message-text":ODOptionData, + "openticket:dm-message-embed":ODOptionData, + + "openticket:ticket-message-enabled":ODOptionData, + "openticket:ticket-message-text":ODOptionData, + "openticket:ticket-message-embed":ODOptionData, + "openticket:ticket-message-ping":ODOptionData, + + "openticket:autoclose-enable-hours":ODOptionData, + "openticket:autoclose-enable-leave":ODOptionData, + "openticket:autoclose-disable-claim":ODOptionData, + "openticket:autoclose-hours":ODOptionData, + + "openticket:autodelete-enable-days":ODOptionData, + "openticket:autodelete-enable-leave":ODOptionData, + "openticket:autodelete-disable-claim":ODOptionData, + "openticket:autodelete-days":ODOptionData, + + "openticket:cooldown-enabled":ODOptionData, + "openticket:cooldown-minutes":ODOptionData, + + "openticket:limits-enabled":ODOptionData, + "openticket:limits-maximum-global":ODOptionData, + "openticket:limits-maximum-user":ODOptionData +} + +export class ODTicketOption extends ODOption { + type: "openticket:ticket" = "openticket:ticket" + + constructor(id:ODValidId, data:ODOptionData[]){ + super(id,"openticket:ticket",data) + } + + get(id:OptionId): ODTicketOptionIds[OptionId] + get(id:ODValidId): ODOptionData|null + + get(id:ODValidId): ODOptionData|null { + return super.get(id) + } + + remove(id:OptionId): ODTicketOptionIds[OptionId] + remove(id:ODValidId): ODOptionData|null + + remove(id:ODValidId): ODOptionData|null { + return super.remove(id) + } + + exists(id:keyof ODTicketOptionIds): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + static fromJson(json: ODOptionJson): ODTicketOption { + return new ODTicketOption(json.id,json.data.map((data) => new ODOptionData(data.id,data.value))) + } +} + +/**## ODWebsiteOptionIds `type` + * This type is an array of ids available in the `ODWebsiteOption` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODWebsiteOptionIds { + "openticket:name":ODOptionData, + "openticket:description":ODOptionData, + + "openticket:button-emoji":ODOptionData, + "openticket:button-label":ODOptionData, + + "openticket:url":ODOptionData, +} + +export class ODWebsiteOption extends ODOption { + type: "openticket:website" = "openticket:website" + + constructor(id:ODValidId, data:ODOptionData[]){ + super(id,"openticket:website",data) + } + + get(id:OptionId): ODWebsiteOptionIds[OptionId] + get(id:ODValidId): ODOptionData|null + + get(id:ODValidId): ODOptionData|null { + return super.get(id) + } + + remove(id:OptionId): ODWebsiteOptionIds[OptionId] + remove(id:ODValidId): ODOptionData|null + + remove(id:ODValidId): ODOptionData|null { + return super.remove(id) + } + + exists(id:keyof ODWebsiteOptionIds): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + static fromJson(json: ODOptionJson): ODWebsiteOption { + return new ODWebsiteOption(json.id,json.data.map((data) => new ODOptionData(data.id,data.value))) + } +} + +/**## ODRoleOptionIds `type` + * This type is an array of ids available in the `ODRoleOption` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODRoleOptionIds { + "openticket:name":ODOptionData, + "openticket:description":ODOptionData, + + "openticket:button-emoji":ODOptionData, + "openticket:button-label":ODOptionData, + "openticket:button-color":ODOptionData, + + "openticket:roles":ODOptionData, + "openticket:mode":ODOptionData, + "openticket:remove-roles-on-add":ODOptionData, + "openticket:add-on-join":ODOptionData +} + +export class ODRoleOption extends ODOption { + type: "openticket:role" = "openticket:role" + + constructor(id:ODValidId, data:ODOptionData[]){ + super(id,"openticket:role",data) + } + + get(id:OptionId): ODRoleOptionIds[OptionId] + get(id:ODValidId): ODOptionData|null + + get(id:ODValidId): ODOptionData|null { + return super.get(id) + } + + remove(id:OptionId): ODRoleOptionIds[OptionId] + remove(id:ODValidId): ODOptionData|null + + remove(id:ODValidId): ODOptionData|null { + return super.remove(id) + } + + exists(id:keyof ODRoleOptionIds): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + static fromJson(json:ODOptionJson): ODRoleOption { + return new ODRoleOption(json.id,json.data.map((data) => new ODOptionData(data.id,data.value))) + } +} + +export class ODOptionSuffixManager extends ODManager { + /**Alias to open ticket global database. */ + database: ODDatabase|null = null + + constructor(debug:ODDebugger){ + super(debug,"ticket suffix") + } + + useDatabase(database:ODDatabase){ + this.database = database + } + /**Instantly get the suffix from an option. */ + getSuffixFromOption(option:ODTicketOption,user:discord.User): string|null { + const suffix = this.getAll().find((suffix) => suffix.option.id.value == option.id.value) + if (!suffix) return null + return suffix.getSuffix(user) + } +} + +export class ODOptionSuffix extends ODManagerData { + /**The option of this suffix. */ + option: ODTicketOption + + constructor(id:ODValidId, option:ODTicketOption){ + super(id) + this.option = option + } + + /**Get the suffix for a new ticket. */ + getSuffix(user:discord.User): string { + throw new ODSystemError("Tried to use an unimplemented ODOptionSuffix!") + } +} + +export class ODOptionUserNameSuffix extends ODOptionSuffix { + getSuffix(user:discord.User): string { + return user.username + } +} + +export class ODOptionUserIdSuffix extends ODOptionSuffix { + getSuffix(user:discord.User): string { + return user.id + } +} + +export class ODOptionCounterDynamicSuffix extends ODOptionSuffix { + /**The database where the value of this counter is stored. */ + database: ODDatabase + + constructor(id:ODValidId, option:ODTicketOption, database:ODDatabase){ + super(id,option) + this.database = database + if (!this.database.exists("openticket:option-suffix-counter",option.id.value)) this.database.set("openticket:option-suffix-counter",this.option.id.value,0) + } + + getSuffix(user:discord.User): string { + const rawCurrentValue = this.database.get("openticket:option-suffix-counter",this.option.id.value) + const currentValue = (typeof rawCurrentValue != "number") ? 0 : rawCurrentValue + const newValue = currentValue+1 + this.database.set("openticket:option-suffix-counter",this.option.id.value,newValue) + return newValue.toString() + } +} + +export class ODOptionCounterFixedSuffix extends ODOptionSuffix { + /**The database where the value of this counter is stored. */ + database: ODDatabase + + constructor(id:ODValidId, option:ODTicketOption, database:ODDatabase){ + super(id,option) + this.database = database + if (!this.database.exists("openticket:option-suffix-counter",option.id.value)) this.database.set("openticket:option-suffix-counter",this.option.id.value,0) + } + + getSuffix(user:discord.User): string { + const rawCurrentValue = this.database.get("openticket:option-suffix-counter",this.option.id.value) + const currentValue = (typeof rawCurrentValue != "number") ? 0 : rawCurrentValue + const newValue = (currentValue >= 9999) ? 0 : currentValue+1 + + this.database.set("openticket:option-suffix-counter",this.option.id.value,newValue) + if (newValue.toString().length == 1) return "000"+newValue.toString() + else if (newValue.toString().length == 2) return "00"+newValue.toString() + else if (newValue.toString().length == 3) return "0"+newValue.toString() + else return newValue.toString() + } +} + +export class ODOptionRandomNumberSuffix extends ODOptionSuffix { + /**The database where previous random numbers are stored. */ + database: ODDatabase + + constructor(id:ODValidId, option:ODTicketOption, database:ODDatabase){ + super(id,option) + this.database = database + if (!this.database.exists("openticket:option-suffix-history",option.id.value)) this.database.set("openticket:option-suffix-history",this.option.id.value,[]) + } + + #generateUniqueValue(history:string[]): string { + const rawNumber = Math.round(Math.random()*1000).toString() + let number = rawNumber + if (rawNumber.length == 1) number = "000"+rawNumber + else if (rawNumber.length == 2) number = "00"+rawNumber + else if (rawNumber.length == 3) number = "0"+rawNumber + + if (history.includes(number)) return this.#generateUniqueValue(history) + else return number + } + getSuffix(user:discord.User): string { + const rawCurrentValues = this.database.get("openticket:option-suffix-history",this.option.id.value) + const currentValues = ((Array.isArray(rawCurrentValues)) ? rawCurrentValues : []) as string[] + const newValue = this.#generateUniqueValue(currentValues) + currentValues.push(newValue) + if (currentValues.length > 50) currentValues.shift() + this.database.set("openticket:option-suffix-history",this.option.id.value,currentValues) + return newValue + } +} + +export class ODOptionRandomHexSuffix extends ODOptionSuffix { + /**The database where previous random hexes are stored. */ + database: ODDatabase + + constructor(id:ODValidId, option:ODTicketOption, database:ODDatabase){ + super(id,option) + this.database = database + if (!this.database.exists("openticket:option-suffix-history",option.id.value)) this.database.set("openticket:option-suffix-history",this.option.id.value,[]) + } + + #generateUniqueValue(history:string[]): string { + const hex = crypto.randomBytes(2).toString("hex") + if (history.includes(hex)) return this.#generateUniqueValue(history) + else return hex + } + getSuffix(user:discord.User): string { + const rawCurrentValues = this.database.get("openticket:option-suffix-history",this.option.id.value) + const currentValues = ((Array.isArray(rawCurrentValues)) ? rawCurrentValues : []) as string[] + const newValue = this.#generateUniqueValue(currentValues) + currentValues.push(newValue) + if (currentValues.length > 50) currentValues.shift() + this.database.set("openticket:option-suffix-history",this.option.id.value,currentValues) + return newValue + } +} \ No newline at end of file diff --git a/src/core/api/openticket/panel.ts b/src/core/api/openticket/panel.ts new file mode 100644 index 0000000..5c37b2d --- /dev/null +++ b/src/core/api/openticket/panel.ts @@ -0,0 +1,128 @@ +/////////////////////////////////////// +//OPENTICKET PANEL MODULE +/////////////////////////////////////// +import { ODJsonConfig_DefaultPanelEmbedSettingsType } from "../defaults/config" +import { ODId, ODManager, ODValidJsonType, ODValidId, ODVersion, ODValidButtonColor, ODManagerData } from "../modules/base" +import { ODDebugger } from "../modules/console" + +export class ODPanelManager extends ODManager { + #debug: ODDebugger + + constructor(debug:ODDebugger){ + super(debug,"option") + this.#debug = debug + } + + add(data:ODPanel, overwrite?:boolean): boolean { + data.useDebug(this.#debug,"option data") + return super.add(data,overwrite) + } +} + +export interface ODPanelDataJson { + id:string, + value:ODValidJsonType +} + +export interface ODPanelJson { + id:string, + version:string, + data:ODPanelDataJson[] +} + +/**## ODPanelIds `type` + * This type is an array of ids available in the `ODPanel` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODPanelIds { + "openticket:name":ODPanelData, + "openticket:options":ODPanelData, + "openticket:dropdown":ODPanelData, + + "openticket:text":ODPanelData, + "openticket:embed":ODPanelData, + + "openticket:dropdown-placeholder":ODPanelData, + + "openticket:enable-max-tickets-warning-text":ODPanelData, + "openticket:enable-max-tickets-warning-embed":ODPanelData, + + "openticket:describe-options-layout":ODPanelData<"simple"|"normal"|"detailed">, + "openticket:describe-options-custom-title":ODPanelData, + "openticket:describe-options-in-text":ODPanelData, + "openticket:describe-options-in-embed-fields":ODPanelData, + "openticket:describe-options-in-embed-description":ODPanelData +} + +export class ODPanel extends ODManager> { + id:ODId + + constructor(id:ODValidId, data:ODPanelData[]){ + super() + this.id = new ODId(id) + data.forEach((data) => { + this.add(data) + }) + } + + toJson(version:ODVersion): ODPanelJson { + const data = this.getAll().map((data) => { + return { + id:data.id.toString(), + value:data.value + } + }) + + return { + id:this.id.toString(), + version:version.toString(), + data + } + } + + static fromJson(json:ODPanelJson): ODPanel { + return new ODPanel(json.id,json.data.map((data) => new ODPanelData(data.id,data.value))) + } + + get(id:PanelId): ODPanelIds[PanelId] + get(id:ODValidId): ODPanelData|null + + get(id:ODValidId): ODPanelData|null { + return super.get(id) + } + + remove(id:PanelId): ODPanelIds[PanelId] + remove(id:ODValidId): ODPanelData|null + + remove(id:ODValidId): ODPanelData|null { + return super.remove(id) + } + + exists(id:keyof ODPanelIds): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +export class ODPanelData extends ODManagerData { + #value: DataType + + constructor(id:ODValidId, value:DataType){ + super(id) + this.#value = value + } + + set value(value:DataType){ + this.#value = value + this._change() + } + get value(): DataType { + return this.#value + } + /**Refresh the database. Is only required to be used when updating `ODPanelData` with an object/array as value. */ + refreshDatabase(){ + this._change() + } +} \ No newline at end of file diff --git a/src/core/api/openticket/question.ts b/src/core/api/openticket/question.ts new file mode 100644 index 0000000..da22cdd --- /dev/null +++ b/src/core/api/openticket/question.ts @@ -0,0 +1,180 @@ +/////////////////////////////////////// +//OPENTICKET OPTION MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODValidJsonType, ODValidId, ODVersion, ODManagerData } from "../modules/base" +import { ODDebugger } from "../modules/console" + +export class ODQuestionManager extends ODManager { + #debug: ODDebugger + + constructor(debug:ODDebugger){ + super(debug,"question") + this.#debug = debug + } + + add(data:ODQuestion, overwrite?:boolean): boolean { + data.useDebug(this.#debug,"question data") + return super.add(data,overwrite) + } +} + +export interface ODQuestionDataJson { + id:string, + value:ODValidJsonType +} + +export interface ODQuestionJson { + id:string, + type:string, + version:string, + data:ODQuestionDataJson[] +} + +export class ODQuestion extends ODManager> { + id:ODId + type: string + + constructor(id:ODValidId, type:string, data:ODQuestionData[]){ + super() + this.id = new ODId(id) + this.type = type + data.forEach((data) => { + this.add(data) + }) + } + + toJson(version:ODVersion): ODQuestionJson { + const data = this.getAll().map((data) => { + return { + id:data.id.toString(), + value:data.value + } + }) + + return { + id:this.id.toString(), + type:this.type, + version:version.toString(), + data + } + } + + static fromJson(json:ODQuestionJson): ODQuestion { + return new ODQuestion(json.id,json.type,json.data.map((data) => new ODQuestionData(data.id,data.value))) + } +} + +export class ODQuestionData extends ODManagerData { + #value: DataType + + constructor(id:ODValidId, value:DataType){ + super(id) + this.#value = value + } + + set value(value:DataType){ + this.#value = value + this._change() + } + get value(): DataType { + return this.#value + } + /**Refresh the database. Is only required to be used when updating `ODQuestionData` with an object/array as value. */ + refreshDatabase(){ + this._change() + } +} + +/**## ODShortQuestionIds `type` + * This type is an array of ids available in the `ODShortQuestion` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODShortQuestionIds { + "openticket:name":ODQuestionData, + "openticket:required":ODQuestionData, + "openticket:placeholder":ODQuestionData, + + "openticket:length-enabled":ODQuestionData, + "openticket:length-min":ODQuestionData, + "openticket:length-max":ODQuestionData +} + +export class ODShortQuestion extends ODQuestion { + type: "openticket:short" = "openticket:short" + + constructor(id:ODValidId, data:ODQuestionData[]){ + super(id,"openticket:short",data) + } + + get(id:QuestionId): ODShortQuestionIds[QuestionId] + get(id:ODValidId): ODQuestionData|null + + get(id:ODValidId): ODQuestionData|null { + return super.get(id) + } + + remove(id:QuestionId): ODShortQuestionIds[QuestionId] + remove(id:ODValidId): ODQuestionData|null + + remove(id:ODValidId): ODQuestionData|null { + return super.remove(id) + } + + exists(id:keyof ODShortQuestionIds): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + static fromJson(json: ODQuestionJson): ODShortQuestion { + return new ODShortQuestion(json.id,json.data.map((data) => new ODQuestionData(data.id,data.value))) + } +} + +/**## ODParagraphQuestionIds `type` + * This type is an array of ids available in the `ODParagraphQuestion` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODParagraphQuestionIds { + "openticket:name":ODQuestionData, + "openticket:required":ODQuestionData, + "openticket:placeholder":ODQuestionData, + + "openticket:length-enabled":ODQuestionData, + "openticket:length-min":ODQuestionData, + "openticket:length-max":ODQuestionData +} + +export class ODParagraphQuestion extends ODQuestion { + type: "openticket:paragraph" = "openticket:paragraph" + + constructor(id:ODValidId, data:ODQuestionData[]){ + super(id,"openticket:paragraph",data) + } + + get(id:QuestionId): ODParagraphQuestionIds[QuestionId] + get(id:ODValidId): ODQuestionData|null + + get(id:ODValidId): ODQuestionData|null { + return super.get(id) + } + + remove(id:QuestionId): ODParagraphQuestionIds[QuestionId] + remove(id:ODValidId): ODQuestionData|null + + remove(id:ODValidId): ODQuestionData|null { + return super.remove(id) + } + + exists(id:keyof ODParagraphQuestionIds): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + static fromJson(json: ODQuestionJson): ODParagraphQuestion { + return new ODParagraphQuestion(json.id,json.data.map((data) => new ODQuestionData(data.id,data.value))) + } +} \ No newline at end of file diff --git a/src/core/api/openticket/role.ts b/src/core/api/openticket/role.ts new file mode 100644 index 0000000..cae14b8 --- /dev/null +++ b/src/core/api/openticket/role.ts @@ -0,0 +1,122 @@ +/////////////////////////////////////// +//OPENTICKET ROLE MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODValidJsonType, ODValidId, ODVersion, ODManagerData } from "../modules/base" +import { ODDebugger } from "../modules/console" +import * as discord from "discord.js" + +export class ODRoleManager extends ODManager { + #debug: ODDebugger + + constructor(debug:ODDebugger){ + super(debug,"role") + this.#debug = debug + } + + add(data:ODRole, overwrite?:boolean): boolean { + data.useDebug(this.#debug,"role data") + return super.add(data,overwrite) + } +} + +export interface ODRoleDataJson { + id:string, + value:ODValidJsonType +} + +export interface ODRoleJson { + id:string, + version:string, + data:ODRoleDataJson[] +} + +export class ODRoleData extends ODManagerData { + #value: DataType + + constructor(id:ODValidId, value:DataType){ + super(id) + this.#value = value + } + + set value(value:DataType){ + this.#value = value + this._change() + } + get value(): DataType { + return this.#value + } + /**Refresh the database. Is only required to be used when updating `ODRoleData` with an object/array as value. */ + refreshDatabase(){ + this._change() + } +} + +/**## ODRoleIds `type` + * This type is an array of ids available in the `ODRole` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODRoleIds { + "openticket:roles":ODRoleData, + "openticket:mode":ODRoleData, + "openticket:remove-roles-on-add":ODRoleData, + "openticket:add-on-join":ODRoleData +} + +export class ODRole extends ODManager> { + id:ODId + + constructor(id:ODValidId, data:ODRoleData[]){ + super() + this.id = new ODId(id) + data.forEach((data) => { + this.add(data) + }) + } + + toJson(version:ODVersion): ODRoleJson { + const data = this.getAll().map((data) => { + return { + id:data.id.toString(), + value:data.value + } + }) + + return { + id:this.id.toString(), + version:version.toString(), + data + } + } + + static fromJson(json:ODRoleJson): ODRole { + return new ODRole(json.id,json.data.map((data) => new ODRoleData(data.id,data.value))) + } + + get(id:OptionId): ODRoleIds[OptionId] + get(id:ODValidId): ODRoleData|null + + get(id:ODValidId): ODRoleData|null { + return super.get(id) + } + + remove(id:OptionId): ODRoleIds[OptionId] + remove(id:ODValidId): ODRoleData|null + + remove(id:ODValidId): ODRoleData|null { + return super.remove(id) + } + + exists(id:keyof ODRoleIds): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +export interface OTRoleUpdateResult { + role:discord.Role, + action:"added"|"removed"|null +} + +export type OTRoleUpdateMode = "add&remove"|"add"|"remove" \ No newline at end of file diff --git a/src/core/api/openticket/ticket.ts b/src/core/api/openticket/ticket.ts new file mode 100644 index 0000000..02c7058 --- /dev/null +++ b/src/core/api/openticket/ticket.ts @@ -0,0 +1,251 @@ +/////////////////////////////////////// +//OPENTICKET TICKET MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODValidJsonType, ODValidId, ODVersion, ODManagerData } from "../modules/base" +import { ODDebugger } from "../modules/console" +import { ODClientManager_Default } from "../defaults/client" +import { ODOption, ODOptionJson, ODTicketOption } from "./option" +import * as discord from "discord.js" + +export class ODTicketManager extends ODManager { + /**A reference to the main server of the bot */ + #guild: discord.Guild|null = null + #client: ODClientManager_Default + #debug: ODDebugger + + constructor(debug:ODDebugger, client:ODClientManager_Default){ + super(debug,"ticket") + this.#debug = debug + this.#client = client + } + + add(data:ODTicket, overwrite?:boolean): boolean { + data.useDebug(this.#debug,"ticket data") + return super.add(data,overwrite) + } + /**Use a specific guild in this class for fetching the channel*/ + useGuild(guild:discord.Guild|null){ + this.#guild = guild + } + /**Get the discord channel for a specific ticket. */ + async getTicketChannel(ticket:ODTicket): Promise { + if (!this.#guild) return null + try { + const channel = await this.#guild.channels.fetch(ticket.id.value) + if (!channel || !channel.isTextBased()) return null + return channel + }catch{ + return null + } + } + /**Get the main ticket message of a ticket channel when found. */ + async getTicketMessage(ticket:ODTicket): Promise|null> { + const msgId = ticket.get("openticket:ticket-message").value + if (!this.#guild || !msgId) return null + try { + const channel = await this.getTicketChannel(ticket) + if (!channel) return null + return await channel.messages.fetch(msgId) + }catch{ + return null + } + } + /**Shortcut for getting a discord.js user within a ticket. */ + async getTicketUser(ticket:ODTicket, user:"creator"|"closer"|"claimer"|"pinner"): Promise { + if (!this.#guild) return null + try { + if (user == "creator"){ + const creatorId = ticket.get("openticket:opened-by").value + if (!creatorId) return null + else return (await this.#guild.client.users.fetch(creatorId)) + + }else if (user == "closer"){ + const closerId = ticket.get("openticket:closed-by").value + if (!closerId) return null + else return (await this.#guild.client.users.fetch(closerId)) + + }else if (user == "claimer"){ + const claimerId = ticket.get("openticket:claimed-by").value + if (!claimerId) return null + else return (await this.#guild.client.users.fetch(claimerId)) + + }else if (user == "pinner"){ + const pinnerId = ticket.get("openticket:pinned-by").value + if (!pinnerId) return null + else return (await this.#guild.client.users.fetch(pinnerId)) + + }else return null + }catch {return null} + } + /**Shortcut for getting all users that are able to view a ticket. */ + async getAllTicketParticipants(ticket:ODTicket): Promise<{user:discord.User,role:"creator"|"participant"|"admin"}[]|null> { + if (!this.#guild) return null + const final: {user:discord.User,role:"creator"|"participant"|"admin"}[] = [] + const channel = await this.getTicketChannel(ticket) + if (!channel) return null + + //add creator + const creatorId = ticket.get("openticket:opened-by").value + if (creatorId){ + const creator = await this.#client.fetchUser(creatorId) + if (creator) final.push({user:creator,role:"creator"}) + } + + //add participants + const participants = ticket.get("openticket:participants").value.filter((p) => p.type == "user") + for (const p of participants){ + if (!final.find((u) => u.user.id == p.id)){ + const participant = await this.#client.fetchUser(p.id) + if (participant) final.push({user:participant,role:"participant"}) + } + } + + //add admin roles + const roles = ticket.get("openticket:participants").value.filter((p) => p.type == "role") + for (const r of roles){ + const role = await this.#client.fetchGuildRole(channel.guild,r.id) + if (role){ + role.members.forEach((member) => { + if (final.find((u) => u.user.id == member.id)) return + final.push({user:member.user,role:"admin"}) + }) + } + } + + return final + } +} + +export interface ODTicketDataJson { + id:string, + value:ODValidJsonType +} + +export interface ODTicketJson { + id:string, + option:string, + version:string, + data:ODTicketDataJson[] +} + +/**## ODTicketIds `type` + * This type is an array of ids available in the `ODTicket` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODTicketIds { + "openticket:busy":ODTicketData, + "openticket:ticket-message":ODTicketData, + "openticket:participants":ODTicketData<{type:"role"|"user",id:string}[]>, + "openticket:channel-suffix":ODTicketData, + + "openticket:open":ODTicketData, + "openticket:opened-by":ODTicketData, + "openticket:opened-on":ODTicketData, + "openticket:closed":ODTicketData, + "openticket:closed-by":ODTicketData, + "openticket:closed-on":ODTicketData, + "openticket:claimed":ODTicketData, + "openticket:claimed-by":ODTicketData, + "openticket:claimed-on":ODTicketData, + "openticket:pinned":ODTicketData, + "openticket:pinned-by":ODTicketData, + "openticket:pinned-on":ODTicketData, + "openticket:for-deletion":ODTicketData, + + "openticket:category":ODTicketData, + "openticket:category-mode":ODTicketData, + + "openticket:autoclose-enabled":ODTicketData, + "openticket:autoclose-hours":ODTicketData, + "openticket:autoclosed":ODTicketData, + "openticket:autodelete-enabled":ODTicketData, + "openticket:autodelete-days":ODTicketData, + + "openticket:answers":ODTicketData<{id:string,name:string,type:"short"|"paragraph",value:string|null}[]>, +} + +export class ODTicket extends ODManager> { + id:ODId + #option: ODTicketOption + + constructor(id:ODValidId, option:ODTicketOption, data:ODTicketData[]){ + super() + this.id = new ODId(id) + this.#option = option + data.forEach((data) => { + this.add(data) + }) + } + + set option(option:ODTicketOption){ + this.#option = option + this._change() + } + get option(){ + return this.#option + } + + toJson(version:ODVersion): ODTicketJson { + const data = this.getAll().map((data) => { + return { + id:data.id.toString(), + value:data.value + } + }) + + return { + id:this.id.toString(), + option:this.option.id.value, + version:version.toString(), + data + } + } + + static fromJson(json:ODTicketJson, option:ODTicketOption): ODTicket { + return new ODTicket(json.id,option,json.data.map((data) => new ODTicketData(data.id,data.value))) + } + + get(id:OptionId): ODTicketIds[OptionId] + get(id:ODValidId): ODTicketData|null + + get(id:ODValidId): ODTicketData|null { + return super.get(id) + } + + remove(id:OptionId): ODTicketIds[OptionId] + remove(id:ODValidId): ODTicketData|null + + remove(id:ODValidId): ODTicketData|null { + return super.remove(id) + } + + exists(id:keyof ODTicketIds): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +export class ODTicketData extends ODManagerData { + #value: DataType + + constructor(id:ODValidId, value:DataType){ + super(id) + this.#value = value + } + + set value(value:DataType){ + this.#value = value + this._change() + } + get value(): DataType { + return this.#value + } + /**Refresh the database. Is only required to be used when updating `ODTicketData` with an object/array as value. */ + refreshDatabase(){ + this._change() + } +} + +export type ODTicketClearFilter = "all"|"open"|"closed"|"claimed"|"unclaimed"|"pinned"|"unpinned"|"autoclosed" \ No newline at end of file diff --git a/src/core/api/openticket/transcript.ts b/src/core/api/openticket/transcript.ts new file mode 100644 index 0000000..5e89525 --- /dev/null +++ b/src/core/api/openticket/transcript.ts @@ -0,0 +1,802 @@ +/////////////////////////////////////// +//OPENTICKET TRANSCRIPT MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODValidJsonType, ODValidId, ODManagerData, ODValidButtonColor } from "../modules/base" +import { ODDebugger } from "../modules/console" +import { ODTicket, ODTicketManager } from "./ticket" +import { ODMessageBuildResult } from "../modules/builder" +import * as discord from "discord.js" + +export class ODTranscriptManager extends ODManager> { + /**The manager responsible for collecting all messages in a channel. */ + collector: ODTranscriptCollector + + constructor(debug:ODDebugger, tickets:ODTicketManager){ + super(debug,"transcript compiler") + this.collector = new ODTranscriptCollector(tickets) + } +} + +export type ODTranscriptCompilerInitFunction = (ticket:ODTicket, channel:discord.TextChannel, user:discord.User) => (ODTranscriptCompilerInitResult)|Promise +export type ODTranscriptCompilerCompileFunction = (ticket:ODTicket, channel:discord.TextChannel, user:discord.User) => ODTranscriptCompilerCompileResult|Promise> +export type ODTranscriptCompilerReadyFunction = (result:ODTranscriptCompilerCompileResult) => ODTranscriptCompilerReadyResult|Promise + +export interface ODTranscriptCompilerInitResult { + success:boolean, + errorReason:string|null, + pendingMessage:ODMessageBuildResult|null +} + +export interface ODTranscriptCompilerCompileResult { + ticket:ODTicket, + channel:discord.TextChannel, + user:discord.User + success:boolean, + errorReason:string|null, + messages:ODTranscriptMessageData[]|null, + data:Data|null +} +export interface ODTranscriptCompilerReadyResult { + channelMessage?:ODMessageBuildResult, + creatorDmMessage?:ODMessageBuildResult, + participantDmMessage?:ODMessageBuildResult, + activeAdminDmMessage?:ODMessageBuildResult, + everyAdminDmMessage?:ODMessageBuildResult +} + +export class ODTranscriptCompiler extends ODManagerData { + /*Initialise the system every time a transcript is created. Returns optional "pending" message to display while the transcript is being compiled. */ + init: ODTranscriptCompilerInitFunction|null + /*Compile or create the transcript. Returns data to give to the ready() function for message creation. */ + compile: ODTranscriptCompilerCompileFunction|null + /*Unload the system & create the final transcript message that will be sent. */ + ready: ODTranscriptCompilerReadyFunction|null + + constructor(id:ODValidId, init?:ODTranscriptCompilerInitFunction, compile?:ODTranscriptCompilerCompileFunction, ready?:ODTranscriptCompilerReadyFunction|null){ + super(id) + this.init = init ?? null + this.compile = compile ?? null + this.ready = ready ?? null + } +} + +/**## ODTranscriptCompilerIds `type` + * This type is an array of ids available in the `ODTranscriptCompiler` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODTranscriptCompilerIds { + "openticket:html-compiler":ODTranscriptCompiler<{url:string}>, + "openticket:text-compiler":ODTranscriptCompiler<{contents:string}>, +} + +/**## ODTranscriptManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODTranscriptManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `openticket.transcripts`! + */ +export class ODTranscriptManager_Default extends ODTranscriptManager { + get(id:QuestionId): ODTranscriptCompilerIds[QuestionId] + get(id:ODValidId): ODTranscriptCompiler|null + + get(id:ODValidId): ODTranscriptCompiler|null { + return super.get(id) + } + + remove(id:QuestionId): ODTranscriptCompilerIds[QuestionId] + remove(id:ODValidId): ODTranscriptCompiler|null + + remove(id:ODValidId): ODTranscriptCompiler|null { + return super.remove(id) + } + + exists(id:keyof ODTranscriptCompilerIds): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +export class ODTranscriptCollector { + /**Alias for the ticket manager. */ + #tickets: ODTicketManager + + constructor(tickets:ODTicketManager){ + this.#tickets = tickets + } + + /**Collect all messages from a given ticket channel. It may not include all messages depending on the ratelimit. */ + async collectAllMessages(ticket:ODTicket, include?:ODTranscriptCollectorIncludeSettings): Promise[]|null> { + const newInclude: ODTranscriptCollectorIncludeSettings = include ?? {users:true,bots:true,client:true} + const channel = await this.#tickets.getTicketChannel(ticket) + if (!channel) return null + + const final: discord.Message[] = [] + const limit: number = 2000 + let lastId: string = "" + while (true){ + const options: discord.FetchMessagesOptions = {limit:100} + if (lastId) options.before = lastId + + const messages = await channel.messages.fetch(options) + messages.forEach(msg => { + if (msg.author.id == msg.client.user.id && newInclude.client) final.push(msg) + else if ((msg.author.bot || msg.author.system) && newInclude.bots) final.push(msg) + else if (newInclude.users) final.push(msg) + }) + + const lastMessage = messages.last() + if (messages.size != 100 || final.length >= limit || !lastMessage) break + else lastId = lastMessage.id + } + return final.reverse() + } + /**Count all messages from a given ticket channel. It may not include all messages depending on the ratelimit. */ + async countAllMessages(ticket:ODTicket, include?:ODTranscriptCollectorIncludeSettings){ + const messages = await this.collectAllMessages(ticket,include) + if (!messages) return null + return messages.length + } + async convertMessagesToTranscriptData(messages:discord.Message[]): Promise { + const final: ODTranscriptMessageData[] = [] + for (const msg of messages){ + const {guild,channel,id,createdTimestamp} = msg + + //create message author + const author = this.#handleUserData(msg.author,msg.member) + + //create message type + let type: ODTranscriptMessageType = "default" + if (msg.mentions.everyone || msg.content.includes("@here")) type = "important" + else if (msg.flags.has("Ephemeral")) type = "ephemeral" + else if (msg.type == discord.MessageType.UserJoin) type = "welcome.message" + else if (msg.type == discord.MessageType.ChannelPinnedMessage) type = "pinned.message" + else if (msg.type == discord.MessageType.GuildBoost || msg.type == discord.MessageType.GuildBoostTier1 || msg.type == discord.MessageType.GuildBoostTier2 || msg.type == discord.MessageType.GuildBoostTier3) type = "boost.message" + else if (msg.type == discord.MessageType.ThreadCreated) type = "thread.message" + + //create message embeds + const embeds: ODTranscriptEmbedData[] = [] + msg.embeds.forEach((embed) => { + embeds.push({ + title:embed.title, + description:embed.description, + authorimg:(embed.author && embed.author.iconURL) ? embed.author.iconURL : null, + authortext:(embed.author) ? embed.author.name : null, + footerimg:(embed.footer && embed.footer.iconURL) ? embed.footer.iconURL : null, + footertext:(embed.footer) ? embed.footer.text : null, + color:(embed.hexColor as discord.HexColorString) ?? "#000000", + image:(embed.image) ? embed.image.url : null, + thumbnail:(embed.thumbnail) ? embed.thumbnail.url : null, + url:embed.url, + fields:embed.fields.map((field) => {return {name:field.name,value:field.value,inline:field.inline ?? false}}) + }) + }) + + //create message files + const files: ODTranscriptFileData[] = [] + msg.attachments.forEach((attachment) => { + const {size,unit} = this.calculateFileSize(attachment.size) + files.push({ + type:attachment.contentType ?? "unknown", + size, + unit, + name:attachment.name, + url:attachment.url, + spoiler:attachment.spoiler, + alt:attachment.description + }) + }) + + //create message components + const rows: ODTranscriptComponentRowData[] = [] + msg.components.forEach((row) => { + const components: ODTranscriptComponentRowData["components"] = [] + row.components.forEach((component) => { + if (component.type == discord.ComponentType.Button){ + components.push({ + id:component.customId, + disabled:component.disabled, + type:"button", + label:component.label, + emoji:this.#handleComponentEmoji(msg,component.emoji), + color:this.#handleButtonComponentStyle(component.style), + mode:(component.style == discord.ButtonStyle.Link) ? "url" : "button", + url:component.url + }) + }else if (component.type == discord.ComponentType.StringSelect){ + components.push({ + id:component.customId, + disabled:component.disabled, + type:"dropdown", + placeholder:component.placeholder, + options:component.options.map((option) => { + return { + id:option.value, + label:option.label, + description:option.description ?? null, + emoji:this.#handleComponentEmoji(msg,option.emoji ?? null) + } + }) + }) + } + }) + rows.push({components}) + }) + + //create message reply + let reply: ODTranscriptMessageReplyData|ODTranscriptInteractionReplyData|null = null + if (msg.reference && msg.reference.messageId && msg.reference.guildId){ + //normal message reply + try{ + const replyChannel = await msg.client.channels.fetch(msg.reference.channelId) + if (replyChannel && !replyChannel.isDMBased() && replyChannel.isTextBased()){ + const replyMessage = await replyChannel.messages.fetch(msg.reference.messageId) + if (replyMessage){ + const replyUser = this.#handleUserData(replyMessage.author,replyMessage.member) + + reply = { + type:"message", + guild:msg.reference.guildId, + channel:msg.reference.channelId, + id:msg.reference.messageId, + user:replyUser, + content:(replyMessage.content != "") ? replyMessage.content : null + } + } + } + }catch{} + }else if (msg.interaction && msg.interaction.type == discord.InteractionType.ApplicationCommand){ + //slash command reply + const member = await msg.guild.members.fetch(msg.interaction.user.id) + reply = { + type:"interaction", + name:msg.interaction.commandName, + user:this.#handleUserData(msg.interaction.user,member) + } + } + + //create message reactions + const reactions: ODTranscriptReactionData[] = [] + msg.reactions.cache.forEach((reaction) => { + const {count,emoji} = reaction + + if (emoji instanceof discord.ReactionEmoji && !emoji.id && emoji.name){ + //build-in emoji + reactions.push({ + id:null, + name:null, + custom:false, + animated:false, + emoji:emoji.name, + amount:count, + super:false //unimplemented in discord.js + }) + }else if (emoji instanceof discord.ReactionEmoji && emoji.id){ + //custom emoji + reactions.push({ + id:emoji.id, + name:emoji.name, + custom:true, + animated:emoji.animated ?? false, + emoji:(emoji.animated ? emoji.imageURL({extension:"gif"}) : emoji.imageURL({extension:"png"})) ?? "❌", + amount:count, + super:false //unimplemented in discord.js + }) + } + }) + + //create message + final.push({ + author, + guild:guild.id, + channel:channel.id, + id, + edited:(msg.editedAt) ? true : false, + timestamp:createdTimestamp, + type, + content:(msg.content != "") ? msg.content : null, + embeds, + files, + components:rows, + reply, + reactions + }) + } + return final + } + /**Calculate a human-readable file size. Used in transcripts. */ + calculateFileSize(bytes:number): {size:number, unit:"B"|"KB"|"MB"|"GB"|"TB"} { + if (bytes >= 1000000000000) return {size:(Math.round((bytes/1000000000000)*100)/100),unit:"TB"} + if (bytes >= 1000000000) return {size:(Math.round((bytes/1000000000)*100)/100),unit:"GB"} + else if (bytes >= 1000000) return {size:(Math.round((bytes/1000000)*100)/100),unit:"MB"} + else if (bytes >= 1000) return {size:(Math.round((bytes/1000)*100)/100),unit:"KB"} + else return {size:bytes,unit:"B"} + } + /**Get the `ODTranscriptEmojiData` from a discord.js component emoji. */ + #handleComponentEmoji(message:discord.Message, rawEmoji:discord.APIMessageComponentEmoji|null): ODTranscriptEmojiData|null { + if (!rawEmoji) return null + //return built-in emoji + if (rawEmoji.name) return { + id:null, + name:null, + custom:false, + animated:false, + emoji:rawEmoji.name + } + if (!rawEmoji.id) return null + const emoji = message.client.emojis.resolve(rawEmoji.id) + if (!emoji) return null + //return custom emoji + return { + id:emoji.id, + name:emoji.name, + custom:true, + animated:emoji.animated ?? false, + emoji:(emoji.animated ? emoji.imageURL({extension:"gif"}) : emoji.imageURL({extension:"png"})) + } + } + /**Create the `ODValidButtonColor` from the discord.js button style. */ + #handleButtonComponentStyle(style:discord.ButtonStyle): ODValidButtonColor { + if (style == discord.ButtonStyle.Danger) return "red" + else if (style == discord.ButtonStyle.Success) return "green" + else if (style == discord.ButtonStyle.Primary) return "blue" + else return "gray" + } + /**Create the `ODTranscriptUserData` from a discord.js user. */ + #handleUserData(user:discord.User, member?:discord.GuildMember|null): ODTranscriptUserData { + const userData: ODTranscriptUserData = { + id:user.id, + username:user.username, + displayname:user.displayName, + pfp:user.displayAvatarURL(), + tag:null, + color:"#ffffff" + } + if (user.flags && user.flags.has("VerifiedBot")) userData.tag = "verified" + else if (user.system) userData.tag = "system" + else if (user.bot) userData.tag = "app" + if (member) userData.color = member.roles.highest.hexColor.replace("#000000","#ffffff") as discord.HexColorString + + return userData + } +} + +export interface ODTranscriptCollectorIncludeSettings { + /**Collect messages from normal discord users. */ + users: boolean, + /**Collect messages from bots & system users. */ + bots: boolean, + /**Collect messages from this bot. */ + client: boolean +} + +export interface ODTranscriptMessageData { + /**The message author. */ + author: ODTranscriptUserData, + /**The server this message originated from. */ + guild: string, + /**The channel this message originated from. */ + channel: string, + /**The id of this message. */ + id: string, + /**Is this message edited? */ + edited: boolean, + /**The unix timestamp of the creation of this message. */ + timestamp: number, + /**The type of message. */ + type: ODTranscriptMessageType, + /**The contents of this message. */ + content: string|null, + /**The embeds of this message. */ + embeds: ODTranscriptEmbedData[], + /**The files of this message. */ + files: ODTranscriptFileData[], + /**The components (buttons & dropdowns) of this message. */ + components: ODTranscriptComponentRowData[], + /**When this message is a reply to something, the data will be here. */ + reply: (ODTranscriptMessageReplyData|ODTranscriptInteractionReplyData|null), + /**All reactions of htis message. */ + reactions: ODTranscriptReactionData[] +} + +export type ODTranscriptMessageType = "default"|"important"|"ephemeral"|"pinned.message"|"welcome.message"|"boost.message"|"thread.message" + +export interface ODTranscriptUserData { + /**The id of this user. */ + id: string, + /**The username of this user. */ + username: string, + /**The display name of this user. */ + displayname: string, + /**The profile picture url of this user. */ + pfp: string, + /**The additional tag of this user. */ + tag: "app"|"verified"|"system"|null, + /**When in a server, this is the color of the highest role. */ + color: discord.HexColorString +} + +export interface ODTranscriptEmbedData { + /**The title of this embed. */ + title: string|null, + /**The description of this embed. */ + description: string|null, + /**The author image of this embed. */ + authorimg: string|null, + /**The author text of this embed. */ + authortext: string|null, + /**The footer image of this embed. */ + footerimg: string|null + /**The footer text of this embed. */ + footertext: string|null, + /**The color of this embed (hex color). */ + color: discord.HexColorString, + /**The image of this embed. */ + image: string|null, + /**The thumbnail of this embed. */ + thumbnail: string|null, + /**The url in the title of this embed. */ + url: string|null, + /**All fields available in this embed. */ + fields: ODTranscriptEmbedFieldData[] +} + +export interface ODTranscriptEmbedFieldData { + /**The name of this embed field. */ + name: string, + /**The value or contents of this embed field. */ + value: string, + /**Is this field rendered inline? */ + inline: boolean +} + +export interface ODTranscriptFileData { + /**The file type or extension. */ + type: string, + /**The size of this file. */ + size: number, + /**The unit used for the size of this file. */ + unit: "B"|"KB"|"MB"|"GB"|"TB", + /**The name of this file. */ + name: string, + /**The url of this file. */ + url: string, + /**Is this file a spoiler? */ + spoiler: boolean, + /**The alternative text for this file. */ + alt: string|null +} + +export interface ODTranscriptComponentRowData { + /**All components (buttons & dropdowns) present in this action row. */ + components: (ODTranscriptButtonComponentData|ODTranscriptDropdownComponentData)[] +} + +export interface ODTranscriptComponentData { + /**The custom id of this component. */ + id: string|null, + /**Is this component disabled? */ + disabled: boolean + /**The type of this component. */ + type: "button"|"dropdown" +} + +export interface ODTranscriptButtonComponentData extends ODTranscriptComponentData { + /**The type of this component. */ + type: "button" + /**The label of this button. */ + label: string|null, + /**The emoji of this button. */ + emoji: ODTranscriptEmojiData|null, + /**The color of this button. */ + color: ODValidButtonColor, + /**Is this button a url or button? */ + mode: "url"|"button", + /**The url of this button. */ + url: string|null +} + +export interface ODTranscriptDropdownComponentData extends ODTranscriptComponentData { + /**The type of this component. */ + type: "dropdown" + /**The placeholder in this dropdown. */ + placeholder: string|null, + /**All options available in this dropdown. */ + options: ODTranscriptDropdownComponentOptionData[] +} + +export interface ODTranscriptDropdownComponentOptionData { + /**The custom id of this dropdown option. */ + id: string, + /**The label of this dropdown option. */ + label: string|null, + /**The description of this dropdown option. */ + description: string|null, + /**The emoji of this dropdown option. */ + emoji: ODTranscriptEmojiData|null +} + +export interface ODTranscriptReplyData { + /**The type of reply. */ + type: "interaction"|"message", +} + +export interface ODTranscriptInteractionReplyData extends ODTranscriptReplyData { + /**The type of reply. */ + type: "interaction", + /**The bot used for this interaction. */ + user: ODTranscriptUserData, + /**The name of the slash command used. */ + name: string +} + +export interface ODTranscriptMessageReplyData extends ODTranscriptReplyData { + /**The type of reply. */ + type: "message", + /**The server this message originated from. */ + guild: string, + /**The channel this message originated from. */ + channel: string, + /**The id of this message. */ + id: string, + /**The author of this message. */ + user: ODTranscriptUserData, + /**The content of this message. */ + content: string|null +} + +export interface ODTranscriptEmojiData { + /**The id of this emoji. */ + id: string|null, + /**The name of this emoji. */ + name: string|null, + /**Is this emoji animated? */ + animated: boolean, + /**Is this a custom emoji (img/gif)? */ + custom: boolean, + /**The url or emoji. */ + emoji: string, +} + +export interface ODTranscriptReactionData extends ODTranscriptEmojiData { + /**The amount of emojis for this reaction. */ + amount: number, + /**Is this a super (nitro) reaction? */ + super: boolean +} + +export interface ODTranscriptHtmlV2Data { + version:"2", + otversion:string, + language:string, + style:{ + background:{ + enableCustomBackground:boolean, + backgroundModus:"color"|"image", + backgroundData:string + }, + header:{ + enableCustomHeader:boolean, + decoColor:string, + backgroundColor:string, + textColor:string + }, + stats:{ + enableCustomStats:boolean, + backgroundColor:string, + keyTextColor:string, + valueTextColor:string, + hideBackgroundColor:string, + hideTextColor:string + }, + favicon:{ + enableCustomFavicon:boolean, + imageUrl:string + } + }, + bot:{ + name:string, + id:string, + pfp:string + }, + ticket:{ + name:string, + id:string, + guildname:string, + guildid:string, + guildinvite:string|false, + + creatorname:string, + creatorid:string, + + closedbyname:string, + closedbyid:string, + + claimedname:string, + claimedid:string, + + creatorpfp:string, + closedbypfp:string, + claimedpfp:string, + + closedtime:number, + openedtime:number, + + components:{ + messages:number, + files:number, + embeds:number, + reactions:number, + interactions:{ + dropdowns:number, + buttons:number, + total:number + }, + attachments:{ + invites:number, + images:number, + gifs:number, + stickers:number + } + }, + roleColors:{id:string,color:string}[] + }, + messages:{ + author:{ + name:string, + id:string, + color:string, + pfp:string, + bot:boolean, + verifiedBot:boolean, + system:boolean + }, + content:string|false, + timestamp:number, + type:"normal", + important:boolean, + edited:boolean, + embeds:{ + title:string|false, + description:string|false, + authorimg:string|false, + authortext:string|false, + footerimg:string|false, + footertext:string|false, + color:string, + image:string|false, + thumbnail:string|false, + url:string|false, + fields:{name:string,value:string,inline:boolean}[] + }[], + attachments:( + { + type:"FILE", + name:string, + fileType:string, + size:string, + url:string + }|{ + type:"FILE:image", + name:string, + fileType:string, + size:string, + url:string + }|{ + type:"URL:invite", + guildId:string, + guildName:string, + guildSize:Number, + guildOnline:Number, + image:string, + url:string + }|{ + type:"URL:gif", + url:string + }|{ + type:"BUILDIN:sticker", + name:string, + url:string + } + )[], + components:( + { + type:"buttons", + buttons:{ + type:"interaction"|"url", + label:string|false, + icon:string|false, + color:"gray"|"green"|"red"|"blue", + id:string|false, + url:string|false, + disabled:boolean + }[] + }|{ + type:"dropdown", + placeholder:string|false, + options:{ + label:string|false, + icon:string|false, + description:string|false, + id:string|false + }[] + }|{ + type:"reactions", + reactions:{ + amount:number, + emoji:string, + type:"svg"|"image"|"gif" + }[] + } + )[], + reply:{ + type:"reply"|"command"|false, + user?:{ + name:string, + id:string, + color:string, + pfp:string, + bot:Boolean, + verifiedBot:Boolean, + system:Boolean + }, + replyOptions?:{ + content:string, + messageId:string, + channelId:string, + guildId:string + }, + commandOptions?:{ + interactionName:string, + interactionId:string + } + } + }[], + premium:{ + enabled:boolean, + premiumToken:string, + customCredits:{ + enable:boolean, + replaceText:string, + replaceURL:string, + enableReportBug:boolean + }, + customHeaderUrl:{ + enabled:boolean, + url:string, + text:string + }, + customTranscriptUrl:{ + enabled:boolean, + name:string + }, + customFavicon:{ + enabled:boolean, + image:string + }, + additionalFlags:string[] + } +} + +export interface ODTranscriptHtmlV2Response { + status:"success"|"error", + id:string, + time:number, + estimated:{ + lastdump: number, + processtime:number, + waittime:number + }, + transcriptstatus:{ + value:2, + data:{ + fetchedmsgs:true, + uploaded:true, + inqueue:true, + processed:false, + waiting:false, + available:false + } + } +} \ No newline at end of file diff --git a/src/core/startup/init.ts b/src/core/startup/init.ts new file mode 100644 index 0000000..12aea74 --- /dev/null +++ b/src/core/startup/init.ts @@ -0,0 +1,205 @@ +import * as fs from "fs" + +let tempErrors: string[] = [] +const tempError = () => { + if (tempErrors.length > 0){ + console.log("\n\n==============================\n[OPEN TICKET ERROR]: "+tempErrors.join("\n[OPEN TICKET ERROR]: ")+"\n==============================\n\n") + process.exit(0) + } + tempErrors = [] +} + +const nodev = process.versions.node.split(".") +if (Number(nodev[0]) < 18){ + tempErrors.push("Invalid node.js version. Open Ticket requires node.js v18 or above!") +} +tempError() + +const moduleInstalled = (id:string, throwError:boolean) => { + try{ + require.resolve(id) + return true + + }catch{ + if (throwError) tempErrors.push("npm module \""+id+"\" is not installed! Install it via 'npm install "+id+"'") + return false + } +} + +moduleInstalled("discord.js",true) +moduleInstalled("ansis",true) +moduleInstalled("formatted-json-stringify",true) +moduleInstalled("tsx",true) +tempError() + +//init API +import * as api from "../api/api" //import for local use +export * as api from "../api/api" //export to other parts of bot + + +export const openticket = new api.ODMain() +console.log("\n--------------------------- OPEN TICKET STARTUP ---------------------------") +openticket.log("Logging system activated!","system") +openticket.debug.debug("Using Node.js "+process.version+"!") + +try{ + const packageJson = JSON.parse(fs.readFileSync("./package.json").toString()) + openticket.debug.debug("Using discord.js "+packageJson.dependencies["discord.js"]+"!") + openticket.debug.debug("Using ansis "+packageJson.dependencies["ansis"]+"!") + openticket.debug.debug("Using formatted-json-stringify "+packageJson.dependencies["formatted-json-stringify"]+"!") + openticket.debug.debug("Using tsx "+packageJson.dependencies["tsx"]+"!") +}catch{ + openticket.debug.debug("Failed to fetch module versions!") +} + +const timer = (ms:number): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + },ms) + }) +} + +interface ODUtilities { + /**## project `utility variable` + * This is the name of the project you are currently in. + * + * Developers can use this to create a multi-plugin compatible with all bots supporting the `open-discord` framework! + */ + project:"openticket" + /**## isBeta `utility variable` + * Check if you're running a beta version of Open Ticket. + */ + isBeta:boolean + /**## moduleInstalled `utility function` + * Use this function to check if an npm package is installed or not! + * @example utilities.moduleInstalled("discord.js") //check if discord.js is installed + */ + moduleInstalled(id:string): boolean + /**## timer `utility function` + * Use this to wait for a certain amount of milliseconds. This only works when using `await` + * @example await utilities.timer(1000) //wait 1sec + */ + timer(ms:number): Promise + /**## emojiTitle `utility function` + * Use this function to create a title with an emoji before/after the text. The style & divider are set in `openticket.defaults` + * @example utilities.emojiTitle("📎","Links") //create a title with an emoji based on the bot emoji style + */ + emojiTitle(emoji:string, text:string): string + /**## runAsync `utility function` + * Use this function to run a piece of code asyncronous without creating a separate function for it! + */ + runAsync(func:() => Promise): void + /**## timedAwait `utility function` + * Use this function to await a promise but reject after the certain timeout has been reached. + */ + timedAwait>(promise:ReturnValue, timeout:number, onError:(err:Error) => void): ReturnValue + /**## dateString `utility function` + * Use this function to create a short date string in the following format: `DD/MM/YYYY HH:MM:SS` + */ + dateString(date:Date): string + /**## asyncReplace `utility function` + * Same as `string.replace(search, value)` but with async compatibility + */ + asyncReplace(text:string, regex:RegExp, func:(value:string,...args:any[]) => Promise): Promise + /**## easterEggs `utility object` + * Object containing data for Open Ticket easter eggs. + */ + easterEggs: api.ODEasterEggs, + /**## ODVersionMigration `utility class` + * This class is used to manage data migration between Open Ticket versions. + * + * It shouldn't be used by plugins because this is an internal API feature! + */ + ODVersionMigration:new (version:api.ODVersion,func:() => void|Promise) => ODVersionMigration +} + +/**## ODVersionMigration `utility class` + * This class is used to manage data migration between Open Ticket versions. + * + * It shouldn't be used by plugins because this is an internal API feature! + */ +class ODVersionMigration { + /**The version to migrate data to */ + version: api.ODVersion + /**The migration function */ + #func: () => void|Promise + + constructor(version:api.ODVersion,func:() => void|Promise){ + this.version = version + this.#func = func + } + /**Run this version migration as a plugin. Returns `false` when someting goes wrong. */ + async migrate(): Promise { + try{ + await this.#func() + return true + }catch{ + return false + } + } +} + +export const utilities: ODUtilities = { + project:"openticket", + isBeta:true, + moduleInstalled:(id:string) => { + return moduleInstalled(id,false) + }, + timer, + emojiTitle(emoji:string, text:string){ + const style = openticket.defaults.getDefault("emojiTitleStyle") + const divider = openticket.defaults.getDefault("emojiTitleDivider") + + if (style == "disabled") return text + else if (style == "before") return emoji+divider+text + else if (style == "after") return text+divider+emoji + else if (style == "double") return emoji+divider+text+divider+emoji + else return text + }, + runAsync(func){ + func() + }, + timedAwait(promise:ReturnValue,timeout:number,onError:(err:Error) => void): ReturnValue { + let allowResolve = true + return new Promise(async (resolve,reject) => { + //set timeout & stop if it is before the promise resolved + setTimeout(() => { + allowResolve = false + reject("utilities.timedAwait() => Promise Timeout") + },timeout) + + //get promise result & return if not already rejected + try{ + const res = await promise + if (allowResolve) resolve(res) + }catch(err){ + onError(err) + } + return promise + }) as ReturnValue + }, + dateString(date): string { + return `${date.getDate()}/${date.getMonth()}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}` + }, + async asyncReplace(text,regex,func): Promise { + const promises: Promise[] = [] + text.replace(regex,(match,...args) => { + promises.push(func(match,...args)) + return match + }) + const data = await Promise.all(promises) + const result = text.replace(regex,(match) => { + const replaceResult = data.shift() + return replaceResult ?? match + }) + return result + }, + easterEggs:{ + creator:"779742674932072469", + translators:[ + "779742674932072469" + ] + }, + ODVersionMigration +} \ No newline at end of file diff --git a/src/core/startup/manageMigration.ts b/src/core/startup/manageMigration.ts new file mode 100644 index 0000000..b090bca --- /dev/null +++ b/src/core/startup/manageMigration.ts @@ -0,0 +1,84 @@ +import {openticket, api, utilities} from "../../index" + +export const loadVersionMigrationSystem = async () => { + //ENTER MIGRATION CONTEXT + await preloadMigrationContext() + + const lastVersion = isMigrationRequired() + openticket.versions.add(lastVersion ? lastVersion : api.ODVersion.fromString("openticket:last-version",openticket.versions.get("openticket:version").toString())) + if (lastVersion && !openticket.flags.get("openticket:no-migration").value){ + //MIGRATION IS REQUIRED + openticket.log("Detected old data!","info") + openticket.log("Starting closed API context...","debug") + await utilities.timer(600) + openticket.log("Migrating data to new version...","debug") + await loadAllVersionMigrations(lastVersion) + openticket.log("Stopping closed API context...","debug") + await utilities.timer(400) + openticket.log("All data is now up to date!","info") + await utilities.timer(200) + console.log("---------------------------------------------------------------------") + } + saveAllVersionsToDatabase() + + //DEFAULT FLAGS + if (openticket.flags.exists("openticket:no-plugins") && openticket.flags.get("openticket:no-plugins").value) openticket.defaults.setDefault("pluginLoading",false) + if (openticket.flags.exists("openticket:soft-plugins") && openticket.flags.get("openticket:soft-plugins").value) openticket.defaults.setDefault("softPluginLoading",true) + if (openticket.flags.exists("openticket:crash") && openticket.flags.get("openticket:crash").value) openticket.defaults.setDefault("crashOnError",true) + if (openticket.flags.exists("openticket:force-slash-update") && openticket.flags.get("openticket:force-slash-update").value) openticket.defaults.setDefault("forceSlashCommandRegistration",true) + + //LEAVE MIGRATION CONTEXT + await unloadMigrationContext() +} + +const preloadMigrationContext = async () => { + openticket.debug.debug("-- MIGRATION CONTEXT START --") + await (await import("../../data/framework/flagLoader.ts")).loadAllFlags() + openticket.flags.init() + await (await import("../../data/framework/configLoader.ts")).loadAllConfigs() + await (await import("../../data/framework/databaseLoader.ts")).loadAllDatabases() + openticket.debug.visible = true +} + +const unloadMigrationContext = async () => { + openticket.debug.visible = false + openticket.databases.getAll().forEach((database) => openticket.databases.remove(database.id)) + openticket.configs.getAll().forEach((config) => openticket.configs.remove(config.id)) + openticket.flags.getAll().forEach((flag) => openticket.flags.remove(flag.id)) + openticket.debug.debug("-- MIGRATION CONTEXT END --") +} + +const isMigrationRequired = (): false|api.ODVersion => { + const rawVersion = openticket.databases.get("openticket:global").get("openticket:last-version","openticket:version") + if (!rawVersion) return false + const version = api.ODVersion.fromString("openticket:last-version",rawVersion) + if (openticket.versions.get("openticket:version").compare(version) == "higher"){ + return version + }else return false +} + +const loadAllVersionMigrations = async (lastVersion:api.ODVersion) => { + const migrations = (await import("./migration.ts")).migrations + migrations.sort((a,b) => { + const comparison = a.version.compare(b.version) + if (comparison == "equal") return 0 + else if (comparison == "higher") return 1 + else return -1 + }) + for (const migration of migrations){ + if (migration.version.compare(lastVersion) == "higher"){ + const success = await migration.migrate() + if (success) openticket.log("Migrated data to "+migration.version.toString()+"!","debug",[ + {key:"success",value:success ? "true" : "false"} + ]) + } + } +} + +const saveAllVersionsToDatabase = async () => { + const globalDatabase = openticket.databases.get("openticket:global") + + openticket.versions.getAll().forEach((version) => { + globalDatabase.set("openticket:last-version",version.id.value,version.toString()) + }) +} \ No newline at end of file diff --git a/src/core/startup/migration.ts b/src/core/startup/migration.ts new file mode 100644 index 0000000..96df7a0 --- /dev/null +++ b/src/core/startup/migration.ts @@ -0,0 +1,8 @@ +import {openticket, api, utilities} from "../../index" + +export const migrations = [ + //MIGRATE TO v4.0.0 + new utilities.ODVersionMigration(api.ODVersion.fromString("openticket:version","v4.0.0"),async () => { + //nothing needs to be transferred :) + }) +] \ No newline at end of file diff --git a/src/core/startup/pluginLauncher.ts b/src/core/startup/pluginLauncher.ts new file mode 100644 index 0000000..c14bfb9 --- /dev/null +++ b/src/core/startup/pluginLauncher.ts @@ -0,0 +1,197 @@ +import {openticket, api, utilities} from "../../index" +import fs from "fs" +import { ODPluginError } from "../api/api" + +export const loadAllPlugins = async () => { + //start launching plugins + openticket.log("Loading plugins...","system") + let initPluginError: boolean = false + + if (!fs.existsSync("./plugins")){ + openticket.log("Couldn't find ./plugins directory, canceling all plugin execution!","error") + return + } + const plugins = fs.readdirSync("./plugins") + + //check & validate + plugins.forEach(async (p) => { + //prechecks + if (!fs.statSync("./plugins/"+p).isDirectory()) return openticket.log("Plugin is not a directory, canceling plugin execution...","plugin",[ + {key:"plugin",value:"./plugins/"+p} + ]) + if (!fs.existsSync("./plugins/"+p+"/plugin.json")){ + initPluginError = true + openticket.log("Plugin doesn't have a plugin.json, canceling plugin execution...","plugin",[ + {key:"plugin",value:"./plugins/"+p} + ]) + return + } + + //plugin loading + try { + const rawplugindata: api.ODPluginData = JSON.parse(fs.readFileSync("./plugins/"+p+"/plugin.json").toString()) + + if (typeof rawplugindata != "object") throw new ODPluginError("Failed to load plugin.json") + if (typeof rawplugindata.id != "string") throw new ODPluginError("Failed to load plugin.json/id") + if (typeof rawplugindata.name != "string") throw new ODPluginError("Failed to load plugin.json/name") + if (typeof rawplugindata.version != "string") throw new ODPluginError("Failed to load plugin.json/version") + if (typeof rawplugindata.startFile != "string") throw new ODPluginError("Failed to load plugin.json/startFile") + + if (typeof rawplugindata.enabled != "boolean") throw new ODPluginError("Failed to load plugin.json/enabled") + if (typeof rawplugindata.priority != "number") throw new ODPluginError("Failed to load plugin.json/priority") + if (!Array.isArray(rawplugindata.events)) throw new ODPluginError("Failed to load plugin.json/events") + + if (!Array.isArray(rawplugindata.npmDependencies)) throw new ODPluginError("Failed to load plugin.json/npmDependencies") + if (!Array.isArray(rawplugindata.requiredPlugins)) throw new ODPluginError("Failed to load plugin.json/requiredPlugins") + if (!Array.isArray(rawplugindata.incompatiblePlugins)) throw new ODPluginError("Failed to load plugin.json/incompatiblePlugins") + + if (typeof rawplugindata.details != "object") throw new ODPluginError("Failed to load plugin.json/details") + if (typeof rawplugindata.details.author != "string") throw new ODPluginError("Failed to load plugin.json/details/author") + if (typeof rawplugindata.details.shortDescription != "string") throw new ODPluginError("Failed to load plugin.json/details/shortDescription") + if (typeof rawplugindata.details.longDescription != "string") throw new ODPluginError("Failed to load plugin.json/details/longDescription") + if (typeof rawplugindata.details.imageUrl != "string") throw new ODPluginError("Failed to load plugin.json/details/imageUrl") + if (typeof rawplugindata.details.projectUrl != "string") throw new ODPluginError("Failed to load plugin.json/details/projectUrl") + if (!Array.isArray(rawplugindata.details.tags)) throw new ODPluginError("Failed to load plugin.json/details/tags") + + if (rawplugindata.id != p) throw new ODPluginError("Failed to load plugin, directory name is required to match the id") + + if (openticket.plugins.exists(rawplugindata.id)) throw new ODPluginError("Failed to load plugin, this id already exists in another plugin") + + //plugin.json is valid => load plugin + const plugin = new api.ODPlugin(p,rawplugindata) + openticket.plugins.add(plugin) + + }catch(e){ + //when any of the above errors happen, crash the bot when soft mode isn't enabled + initPluginError = true + openticket.log(e.message+", canceling plugin execution...","plugin",[ + {key:"path",value:"./plugins/"+p} + ]) + openticket.log("You can see more about this error in the ./otdebug.txt file!","info") + openticket.debugfile.writeText(e.stack) + + //try to get some crashed plugin data + try{ + const rawplugindata: api.ODPluginData = JSON.parse(fs.readFileSync("./plugins/"+p+"/plugin.json").toString()) + openticket.plugins.unknownCrashedPlugins.push({ + name:rawplugindata.name ?? "./plugins/"+p, + description:(rawplugindata.details && rawplugindata.details.shortDescription) ? rawplugindata.details.shortDescription : "This plugin crashed :(", + }) + }catch{} + } + }) + + //sorted plugins (based on priority) + const sortedPlugins = openticket.plugins.getAll().sort((a,b) => { + return (b.priority - a.priority) + }) + + //check for incompatible & missing plugins/dependencies + const incompatibilities: {from:string,to:string}[] = [] + const missingDependencies: {id:string,missing:string}[] = [] + const missingPlugins: {id:string,missing:string}[] = [] + + //go trough all plugins for errors + sortedPlugins.forEach((plugin) => { + const from = plugin.id.value + plugin.data.incompatiblePlugins.forEach((to) => { + //deny incompatibility if it already exists + if (incompatibilities.find((p) => (p.from == from && p.to == to) || (p.to == from && p.from == to))) return + + //check for existence of both plugins => add to list + if (openticket.plugins.exists(to)) incompatibilities.push({from,to}) + }) + plugin.dependenciesInstalled().forEach((missing) => missingDependencies.push({id:from,missing})) + plugin.pluginsInstalled(openticket.plugins).forEach((missing) => missingPlugins.push({id:from,missing})) + }) + + //handle all incompatibilities + incompatibilities.forEach((match) => { + const fromPlugin = openticket.plugins.get(match.from) + if (fromPlugin && !fromPlugin.crashed){ + fromPlugin.crashed = true + fromPlugin.crashReason = "incompatible.plugin" + } + const toPlugin = openticket.plugins.get(match.to) + if (toPlugin && !toPlugin.crashed){ + toPlugin.crashed = true + toPlugin.crashReason = "incompatible.plugin" + } + + openticket.log(`Incompatible plugins => "${match.from}" & "${match.to}", canceling plugin execution...`,"plugin",[ + {key:"path1",value:"./plugins/"+match.from}, + {key:"path2",value:"./plugins/"+match.to} + ]) + initPluginError = true + }) + + //handle all missing dependencies + missingDependencies.forEach((match) => { + const plugin = openticket.plugins.get(match.id) + if (plugin && !plugin.crashed){ + plugin.crashed = true + plugin.crashReason = "missing.dependency" + } + + openticket.log(`Missing npm dependency "${match.missing}", canceling plugin execution...`,"plugin",[ + {key:"path",value:"./plugins/"+match.id} + ]) + initPluginError = true + }) + + //handle all missing plugins + missingPlugins.forEach((match) => { + const plugin = openticket.plugins.get(match.id) + if (plugin && !plugin.crashed){ + plugin.crashed = true + plugin.crashReason = "missing.plugin" + } + + openticket.log(`Missing required plugin "${match.missing}", canceling plugin execution...`,"plugin",[ + {key:"path",value:"./plugins/"+match.id} + ]) + initPluginError = true + }) + + //exit on error (when soft mode disabled) + if (!openticket.defaults.getDefault("softPluginLoading") && initPluginError){ + console.log("") + openticket.log("Please fix all plugin errors above & try again!","error") + process.exit(1) + } + + //preload all events required for every plugin + for (const plugin of sortedPlugins){ + if (plugin.enabled) plugin.data.events.forEach((event) => openticket.events.add(new api.ODEvent(event))) + } + + //execute all working plugins + for (const plugin of sortedPlugins){ + const status = await plugin.execute(openticket.debug,false) + + //exit on error (when soft mode disabled) + if (!status && !openticket.defaults.getDefault("softPluginLoading")){ + console.log("") + openticket.log("Please fix all plugin errors above & try again!","error") + process.exit(1) + } + } + + for (const plugin of sortedPlugins){ + if (plugin.enabled){ + openticket.debug.debug("Plugin \""+plugin.id.value+"\" loaded",[ + {key:"status",value:(plugin.crashed ? "crashed" : "success")}, + {key:"crashReason",value:(plugin.crashed ? (plugin.crashReason ?? "/") : "/")}, + {key:"author",value:plugin.details.author}, + {key:"version",value:plugin.version.toString()}, + {key:"priority",value:plugin.priority.toString()} + ]) + }else{ + openticket.debug.debug("Plugin \""+plugin.id.value+"\" disabled",[ + {key:"author",value:plugin.details.author}, + {key:"version",value:plugin.version.toString()}, + {key:"priority",value:plugin.priority.toString()} + ]) + } + } +} \ No newline at end of file diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts new file mode 100644 index 0000000..1e02728 --- /dev/null +++ b/src/data/framework/checkerLoader.ts @@ -0,0 +1,591 @@ +import {openticket, api, utilities} from "../../index" + +const generalConfig = openticket.configs.get("openticket:general") + +export const loadAllConfigCheckers = async () => { + openticket.checkers.add(new api.ODChecker("openticket:general",openticket.checkers.storage,0,openticket.configs.get("openticket:general"),defaultGeneralStructure)) + openticket.checkers.add(new api.ODChecker("openticket:options",openticket.checkers.storage,1,openticket.configs.get("openticket:options"),defaultOptionsStructure)) + openticket.checkers.add(new api.ODChecker("openticket:panels",openticket.checkers.storage,0,openticket.configs.get("openticket:panels"),defaultPanelsStructure)) + openticket.checkers.add(new api.ODChecker("openticket:questions",openticket.checkers.storage,2,openticket.configs.get("openticket:questions"),defaultQuestionsStructure)) + openticket.checkers.add(new api.ODChecker("openticket:transcripts",openticket.checkers.storage,0,openticket.configs.get("openticket:transcripts"),defaultTranscriptsStructure)) +} + +export const loadAllConfigCheckerFunctions = async () => { + openticket.checkers.functions.add(new api.ODCheckerFunction("openticket:unused-options",defaultUnusedOptionsFunction)) + openticket.checkers.functions.add(new api.ODCheckerFunction("openticket:dropdown-options",defaultDropdownOptionsFunction)) +} + +export const loadAllConfigCheckerTranslations = async () => { + if ((generalConfig && generalConfig.data.system && generalConfig.data.system.useTranslatedConfigChecker) ? generalConfig.data.system.useTranslatedConfigChecker : false){ + registerDefaultCheckerSystemTranslations() //translate checker system text + registerDefaultCheckerMessageTranslations() //translate checker messages + registerDefaultCheckerCustomTranslations() //translate custom checker messages + } +} + +//GLOBAL FUNCTIONS +export const registerDefaultCheckerSystemTranslations = () => { + const tm = openticket.checkers.translation + const lm = openticket.languages + + //SYSTEM + //tm.quickTranslate(lm,"checker.system.headerOpenTicket","other","openticket:header-openticket") //OPEN TICKET (ignore) + tm.quickTranslate(lm,"checker.system.typeError","other","openticket:type-error") // [ERROR] (ignore) + tm.quickTranslate(lm,"checker.system.typeWarning","other","openticket:type-warning") // [WARNING] (ignore) + tm.quickTranslate(lm,"checker.system.typeInfo","other","openticket:type-info") // [INFO] (ignore) + tm.quickTranslate(lm,"checker.system.headerConfigChecker","other","openticket:header-configchecker") // CONFIG CHECKER + tm.quickTranslate(lm,"checker.system.headerDescription","other","openticket:header-description") // check for errors in you config files! + tm.quickTranslate(lm,"checker.system.footerError","other","openticket:footer-error") // the bot won't start until all {0}'s are fixed! + tm.quickTranslate(lm,"checker.system.footerWarning","other","openticket:footer-warning") // it's recommended to fix all {0}'s before starting! + tm.quickTranslate(lm,"checker.system.footerSupport","other","openticket:footer-support") // SUPPORT: {0} - DOCS: {1} + tm.quickTranslate(lm,"checker.system.compactInformation","other","openticket:compact-information") // use {0} for more information! + tm.quickTranslate(lm,"checker.system.dataPath","other","openticket:data-path") // path + tm.quickTranslate(lm,"checker.system.dataDocs","other","openticket:data-docs") // docs + tm.quickTranslate(lm,"checker.system.dataMessages","other","openticket:data-message") // message +} + +export const registerDefaultCheckerMessageTranslations = () => { + const tm = openticket.checkers.translation + const lm = openticket.languages + + //STRUCTURES + tm.quickTranslate(lm,"checker.messages.invalidType","message","openticket:invalid-type") // This property needs to be the type: {0}! + tm.quickTranslate(lm,"checker.messages.propertyMissing","message","openticket:property-missing") // The property {0} is missing from this object! + tm.quickTranslate(lm,"checker.messages.propertyOptional","message","openticket:property-optional") // The property {0} is optional in this object! + tm.quickTranslate(lm,"checker.messages.objectDisabled","message","openticket:object-disabled") // This object is disabled, enable it using {0}! + tm.quickTranslate(lm,"checker.messages.nullInvalid","message","openticket:null-invalid") // This property can't be null! + tm.quickTranslate(lm,"checker.messages.switchInvalidType","message","openticket:switch-invalid-type") // This needs to be one of the following types: {0}! + tm.quickTranslate(lm,"checker.messages.objectSwitchInvalid","message","openticket:object-switch-invalid-type") // This object needs to be one of the following types: {0}! + + tm.quickTranslate(lm,"checker.messages.stringTooShort","message","openticket:string-too-short") // This string can't be shorter than {0} characters! + tm.quickTranslate(lm,"checker.messages.stringTooLong","message","openticket:string-too-long") // This string can't be longer than {0} characters! + tm.quickTranslate(lm,"checker.messages.stringLengthInvalid","message","openticket:string-length-invalid") // This string needs to be {0} characters long! + tm.quickTranslate(lm,"checker.messages.stringStartsWith","message","openticket:string-starts-with") // This string needs to start with {0}! + tm.quickTranslate(lm,"checker.messages.stringEndsWith","message","openticket:string-ends-with") // This string needs to end with {0}! + tm.quickTranslate(lm,"checker.messages.stringContains","message","openticket:string-contains") // This string needs to contain {0}! + tm.quickTranslate(lm,"checker.messages.stringChoices","message","openticket:string-choices") // This string can only be one of the following values: {0}! + tm.quickTranslate(lm,"checker.messages.stringRegex","message","openticket:string-regex") // This string is invalid! + + tm.quickTranslate(lm,"checker.messages.numberTooShort","message","openticket:number-too-short") // This number can't be shorter than {0} characters! + tm.quickTranslate(lm,"checker.messages.numberTooLong","message","openticket:number-too-long") // This number can't be longer than {0} characters! + tm.quickTranslate(lm,"checker.messages.numberLengthInvalid","message","openticket:number-length-invalid") // This number needs to be {0} characters long! + tm.quickTranslate(lm,"checker.messages.numberTooSmall","message","openticket:number-too-small") // This number needs to be at least {0}! + tm.quickTranslate(lm,"checker.messages.numberTooLarge","message","openticket:number-too-large") // This number needs to be at most {0}! + tm.quickTranslate(lm,"checker.messages.numberNotEqual","message","openticket:number-not-equal") // This number needs to be {0}! + tm.quickTranslate(lm,"checker.messages.numberStep","message","openticket:number-step") // This number needs to be a multiple of {0}! + tm.quickTranslate(lm,"checker.messages.numberStepOffset","message","openticket:number-step-offset") // This number needs to be a multiple of {0} starting with {1}! + tm.quickTranslate(lm,"checker.messages.numberStartsWith","message","openticket:number-starts-with") // This number needs to start with {0}! + tm.quickTranslate(lm,"checker.messages.numberEndsWith","message","openticket:number-ends-with") // This number needs to end with {0}! + tm.quickTranslate(lm,"checker.messages.numberContains","message","openticket:number-contains") // This number needs to contain {0}! + tm.quickTranslate(lm,"checker.messages.numberChoices","message","openticket:number-choices") // This number can only be one of the following values: {0}! + tm.quickTranslate(lm,"checker.messages.numberFloat","message","openticket:number-float") // This number can't be a decimal! + tm.quickTranslate(lm,"checker.messages.numberNegative","message","openticket:number-negative") // This number can't be negative! + tm.quickTranslate(lm,"checker.messages.numberPositive","message","openticket:number-positive") // This number can't be positive! + tm.quickTranslate(lm,"checker.messages.numberZero","message","openticket:number-zero") // This number can't be zero! + + tm.quickTranslate(lm,"checker.messages.booleanTrue","message","openticket:boolean-true") // This boolean can't be true! + tm.quickTranslate(lm,"checker.messages.booleanFalse","message","openticket:boolean-false") // This boolean can't be false! + + tm.quickTranslate(lm,"checker.messages.arrayEmptyDisabled","message","openticket:array-empty-disabled") // This array isn't allowed to be empty! + tm.quickTranslate(lm,"checker.messages.arrayEmptyRequired","message","openticket:array-empty-required") // This array is required to be empty! + tm.quickTranslate(lm,"checker.messages.arrayTooShort","message","openticket:array-too-short") // This array needs to have a length of at least {0}! + tm.quickTranslate(lm,"checker.messages.arrayTooLong","message","openticket:array-too-long") // This array needs to have a length of at most {0}! + tm.quickTranslate(lm,"checker.messages.arrayLengthInvalid","message","openticket:array-length-invalid") // This array needs to have a length of {0}! + tm.quickTranslate(lm,"checker.messages.arrayInvalidTypes","message","openticket:array-invalid-types") // This array can only contain the following types: {0}! + tm.quickTranslate(lm,"checker.messages.arrayDouble","message","openticket:array-double") // This array doesn't allow the same value twice! + + tm.quickTranslate(lm,"checker.messages.discordInvalidId","message","openticket:discord-invalid-id") // This is an invalid discord {0} id! + tm.quickTranslate(lm,"checker.messages.discordInvalidIdOptions","message","openticket:discord-invalid-id-options") // This is an invalid discord {0} id! You can also use one of these: {1}! + tm.quickTranslate(lm,"checker.messages.discordInvalidToken","message","openticket:discord-invalid-token") // This is an invalid discord token (syntactically)! + tm.quickTranslate(lm,"checker.messages.colorInvalid","message","openticket:color-invalid") // This is an invalid hex color! + tm.quickTranslate(lm,"checker.messages.emojiTooShort","message","openticket:emoji-too-short") // This string needs to have at least {0} emoji's! + tm.quickTranslate(lm,"checker.messages.emojiTooLong","message","openticket:emoji-too-long") // This string needs to have at most {0} emoji's! + tm.quickTranslate(lm,"checker.messages.emojiCustom","message","openticket:emoji-custom") // This emoji can't be a custom discord emoji! + tm.quickTranslate(lm,"checker.messages.emojiInvalid","message","openticket:emoji-invalid") // This is an invalid emoji! + tm.quickTranslate(lm,"checker.messages.urlInvalid","message","openticket:url-invalid") // This url is invalid! + tm.quickTranslate(lm,"checker.messages.urlInvalidHttp","message","openticket:url-invalid-http") // This url can only use the https:// protocol! + tm.quickTranslate(lm,"checker.messages.urlInvalidProtocol","message","openticket:url-invalid-protocol") // This url can only use the http:// & https:// protocols! + tm.quickTranslate(lm,"checker.messages.urlInvalidHostname","message","openticket:url-invalid-hostname") // This url has a disallowed hostname! + tm.quickTranslate(lm,"checker.messages.urlInvalidExtension","message","openticket:url-invalid-extension") // This url has an invalid extension! Choose between: {0}! + tm.quickTranslate(lm,"checker.messages.urlInvalidPath","message","openticket:url-invalid-path") // This url has an invalid path! + tm.quickTranslate(lm,"checker.messages.idNotUnique","message","openticket:id-not-unique") // This id isn't unique, use another id instead! + tm.quickTranslate(lm,"checker.messages.idNonExistent","message","openticket:id-non-existent") // The id {0} doesn't exist! +} + +export const registerDefaultCheckerCustomTranslations = () => { + const tm = openticket.checkers.translation + const lm = openticket.languages + + //CUSTOM + tm.quickTranslate(lm,"checker.messages.invalidLanguage","message","openticket:invalid-language") // This is an invalid language! + tm.quickTranslate(lm,"checker.messages.invalidButton","message","openticket:invalid-button") // This button needs to have at least an {0} or {1}! + tm.quickTranslate(lm,"checker.messages.unusedOption","message","openticket:unused-option") // The option {0} isn't used anywhere! + tm.quickTranslate(lm,"checker.messages.unusedQuestion","message","openticket:unused-question") // The question {0} isn't used anywhere! + tm.quickTranslate(lm,"checker.messages.dropdownOption","message","openticket:dropdown-option") // A panel with dropdown enabled can only contain options of the 'ticket' type! +} + +//UTILITY FUNCTIONS +const createMsgStructure = (id:api.ODValidId) => { + return new api.ODCheckerObjectStructure(id,{children:[ + {key:"dm",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:msg-dm",{})}, + {key:"logs",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:msg-logs",{})}, + ]}) +} +const createTicketEmbedStructure = (id:api.ODValidId) => { + return new api.ODCheckerEnabledObjectStructure(id,{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure(id,{children:[ + {key:"title",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-embed-text",{maxLength:256})}, + {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-embed-description",{maxLength:4096})}, + {key:"customColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("openticket:ticket-embed-color",true,true)}, + + {key:"image",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("openticket:ticket-embed-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, + {key:"thumbnail",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("openticket:ticket-embed-thumbnail",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, + {key:"fields",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("openticket:ticket-embed-fields",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("openticket:ticket-embed-fields",{children:[ + {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-embed-field-name",{minLength:1,maxLength:256})}, + {key:"value",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-embed-field-value",{minLength:1,maxLength:1024})}, + {key:"inline",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:ticket-embed-field-inline",{})} + ]})})}, + {key:"timestamp",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:ticket-embed-timestamp",{})} + ]})}) +} +const createTicketPingStructure = (id:api.ODValidId) => { + return new api.ODCheckerObjectStructure(id,{children:[ + {key:"@here",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:ticket-ping-here",{})}, + {key:"@everyone",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:ticket-ping-everyone",{})}, + {key:"custom",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("openticket:ticket-ping-custom","role",[],{allowDoubles:false})}, + ]}) +} +const createPanelEmbedStructure = (id:api.ODValidId) => { + return new api.ODCheckerEnabledObjectStructure(id,{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure(id,{children:[ + {key:"title",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:panel-embed-text",{maxLength:256})}, + {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:panel-embed-description",{maxLength:4096})}, + {key:"customColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("openticket:panel-embed-color",true,true)}, + {key:"url",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("openticket:panel-embed-url",true,{allowHttp:false})}, + + {key:"image",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("openticket:panel-embed-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, + {key:"thumbnail",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("openticket:panel-embed-thumbnail",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, + + {key:"footer",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:panel-embed-footer",{maxLength:2048})}, + {key:"fields",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("openticket:panel-embed-fields",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("openticket:panel-embed-fields",{children:[ + {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:panel-embed-field-name",{minLength:1,maxLength:256})}, + {key:"value",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:panel-embed-field-value",{minLength:1,maxLength:1024})}, + {key:"inline",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:panel-embed-field-inline",{})} + ]})})}, + {key:"timestamp",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:panel-embed-timestamp",{})} + ]})}) +} + +//STRUCTURES +export const defaultGeneralStructure = new api.ODCheckerObjectStructure("openticket:general",{children:[ + //BASIC + {key:"token",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordToken("openticket:token")}, + {key:"tokenFromENV",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:token-env",{})}, + {key:"mainColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("openticket:main-color",true,false)}, + {key:"language",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:language",{ + custom:(checker,value,locationTrace,locationId,locationDocs) => { + const lt = checker.locationTraceDeref(locationTrace) + + if (typeof value != "string") return false + else if (!openticket.defaults.getDefault("languageList").includes(value)){ + checker.createMessage("openticket:invalid-language","error","This is an invalid language!",lt,null,[],locationId,locationDocs) + return false + }else return true + }, + })}, + {key:"prefix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:prefix",{minLength:1})}, + {key:"serverId",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:server-id","server",false,[])}, + {key:"globalAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("openticket:global-admins","role",[],{allowDoubles:false})}, + {key:"slashCommands",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:slash-commands",{})}, + {key:"textCommands",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:text-commands",{})}, + + //STATUS + {key:"status",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("openticket:status",{ + property:"enabled", + enabledValue:true, + checker:new api.ODCheckerObjectStructure("openticket:status",{children:[ + {key:"type",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:status-type",{choices:["listening","watching","playing","custom"]})}, + {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:status-text",{minLength:1,maxLength:128})}, + {key:"status",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:status-type",{choices:["online","invisible","idle","dnd"]})}, + ]}) + })}, + + //SYSTEM + {key:"system",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("openticket:system",{children:[ + {key:"removeParticipantsOnClose",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:remove-participants-on-close",{})}, + {key:"replyOnTicketCreation",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:reply-on-ticket-creation",{})}, + {key:"replyOnReactionRole",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:reply-on-reaction-role",{})}, + {key:"useTranslatedConfigChecker",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:use-translated-config-checker",{})}, + {key:"preferSlashOverText",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:prefer-slash-over-text",{})}, + {key:"sendErrorOnUnknownCommand",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:send-error-on-unknown-command",{})}, + {key:"questionFieldsInCodeBlock",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:question-fields-in-code-block",{})}, + {key:"disableVerifyBars",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:disable-verify-bars",{})}, + {key:"useRedErrorEmbeds",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:use-red-error-embeds",{})}, + {key:"emojiStyle",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:emoji-style",{choices:["before","after","double","disabled"]})}, + + {key:"enableTicketClaimButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:enable-ticket-claim-buttons",{})}, + {key:"enableTicketCloseButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:enable-ticket-close-buttons",{})}, + {key:"enableTicketPinButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:enable-ticket-pin-buttons",{})}, + {key:"enableTicketDeleteButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:enable-ticket-delete-buttons",{})}, + {key:"enableTicketActionWithReason",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:enable-ticket-action-with-reason",{})}, + {key:"enableDeleteWithoutTranscript",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:enable-delete-without-transcript",{})}, + + {key:"logs",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("openticket:system-logs",{ + property:"enabled", + enabledValue:true, + checker:new api.ODCheckerObjectStructure("openticket:system-logs",{children:[ + {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:log-channel","channel",false,[])}, + ]}) + })}, + + {key:"limits",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("openticket:limits",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("openticket:limits",{children:[ + {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("openticket:limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1})}, + {key:"userMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("openticket:limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1})} + ]})})}, + + {key:"permissions",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("openticket:system-permissions",{children:[ + {key:"help",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-help","role",false,["admin","everyone","none"])}, + {key:"panel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-panel","role",false,["admin","everyone","none"])}, + {key:"ticket",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-ticket","role",false,["admin","everyone","none"])}, + {key:"close",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-close","role",false,["admin","everyone","none"])}, + {key:"delete",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-delete","role",false,["admin","everyone","none"])}, + {key:"reopen",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-reopen","role",false,["admin","everyone","none"])}, + {key:"claim",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-claim","role",false,["admin","everyone","none"])}, + {key:"unclaim",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-unclaim","role",false,["admin","everyone","none"])}, + {key:"pin",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-pin","role",false,["admin","everyone","none"])}, + {key:"unpin",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-unpin","role",false,["admin","everyone","none"])}, + {key:"move",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-move","role",false,["admin","everyone","none"])}, + {key:"rename",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-rename","role",false,["admin","everyone","none"])}, + {key:"add",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-add","role",false,["admin","everyone","none"])}, + {key:"remove",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-remove","role",false,["admin","everyone","none"])}, + {key:"blacklist",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-blacklist","role",false,["admin","everyone","none"])}, + {key:"stats",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-stats","role",false,["admin","everyone","none"])}, + {key:"clear",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-clear","role",false,["admin","everyone","none"])}, + {key:"autoclose",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-autoclose","role",false,["admin","everyone","none"])}, + {key:"autodelete",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:permissions-autodelete","role",false,["admin","everyone","none"])} + ]})}, + + {key:"messages",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("openticket:system-permissions",{children:[ + {key:"creation",optional:false,priority:0,checker:createMsgStructure("openticket:msg-creation")}, + {key:"closing",optional:false,priority:0,checker:createMsgStructure("openticket:msg-closing")}, + {key:"deleting",optional:false,priority:0,checker:createMsgStructure("openticket:msg-deleting")}, + {key:"reopening",optional:false,priority:0,checker:createMsgStructure("openticket:msg-reopening")}, + {key:"claiming",optional:false,priority:0,checker:createMsgStructure("openticket:msg-claiming")}, + {key:"pinning",optional:false,priority:0,checker:createMsgStructure("openticket:msg-pinning")}, + {key:"adding",optional:false,priority:0,checker:createMsgStructure("openticket:msg-adding")}, + {key:"removing",optional:false,priority:0,checker:createMsgStructure("openticket:msg-removing")}, + {key:"renaming",optional:false,priority:0,checker:createMsgStructure("openticket:msg-renaming")}, + {key:"moving",optional:false,priority:0,checker:createMsgStructure("openticket:msg-moving")}, + {key:"blacklisting",optional:false,priority:0,checker:createMsgStructure("openticket:msg-blacklisting")}, + {key:"roleAdding",optional:false,priority:0,checker:createMsgStructure("openticket:msg-role-adding")}, + {key:"roleRemoving",optional:false,priority:0,checker:createMsgStructure("openticket:msg-role-removing")} + ]})}, + ]})} +]}) + +export const defaultOptionsStructure = new api.ODCheckerArrayStructure("openticket:options",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectSwitchStructure("openticket:options",{objects:[ + //TICKET + {name:"ticket",priority:0,properties:[{key:"type",value:"ticket"}],checker:new api.ODCheckerObjectStructure("openticket:ticket",{children:[ + {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("openticket:ticket-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, + {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-name",{minLength:3,maxLength:50})}, + {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-description",{maxLength:256})}, + + //TICKET BUTTON + {key:"button",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("openticket:ticket-button",{children:[ + {key:"emoji",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_EmojiString("openticket:ticket-button-emoji",0,1,true)}, + {key:"label",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-button-label",{maxLength:50})}, + {key:"color",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-button-color",{choices:["gray","red","green","blue"]})}, + ],custom:(checker,value,locationTrace,locationId,locationDocs) => { + const lt = checker.locationTraceDeref(locationTrace) + //check if emoji & label exists + if (typeof value != "object") return false + else if (value && value["emoji"].length < 1 && value["label"].length < 1){ + //label & emoji are both empty + checker.createMessage("openticket:invalid-button","error",`This button needs to have at least an "emoji" or "label"!`,lt,null,[`"emoji"`,`"label"`],locationId,locationDocs) + return false + }else return true + }})}, + + //TICKET ADMINS + {key:"ticketAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("openticket:ticket-ticket-admins","role",[],{allowDoubles:false})}, + {key:"readonlyAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("openticket:ticket-readonly-admins","role",[],{allowDoubles:false})}, + {key:"allowCreationByBlacklistedUsers",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:ticket-allow-blacklisted-users",{})}, + {key:"questions",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("openticket:option-questions","openticket","question-ids","question-ids-used",{allowDoubles:false,maxLength:5})}, + + //TICKET CHANNEL + {key:"channel",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("openticket:ticket-channel",{children:[ + {key:"prefix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-channel-prefix",{maxLength:25,regex:/^[^\s]*$/})}, + {key:"suffix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-channel-suffix",{choices:["user-name","user-id","random-number","random-hex","counter-dynamic","counter-fixed"]})}, + + {key:"category",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:ticket-channel-category","category",true,[])}, + {key:"closedCategory",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:ticket-channel-closed-category","category",true,[])}, + {key:"backupCategory",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:ticket-channel-backup-category","category",true,[])}, + {key:"claimedCategory",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("openticket:ticket-channel-claimed-category",{allowDoubles:false,allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("openticket:ticket-channel-claimed-category",{children:[ + {key:"user",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:ticket-channel-claimed-user","user",false,[])}, + {key:"category",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:ticket-channel-claimed-category","category",false,[])} + ]})})}, + {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-channel-description",{})}, + ]})}, + + //DM MESSAGE + {key:"dmMessage",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("openticket:ticket-dm-message",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("openticket:ticket-dm-message",{children:[ + {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-message-text",{maxLength:4096})}, + {key:"embed",optional:false,priority:0,checker:createTicketEmbedStructure("openticket:ticket-message-embed")} + ]})})}, + + //TICKET MESSAGE + {key:"ticketMessage",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("openticket:ticket-message",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("openticket:ticket-message",{children:[ + {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-message-text",{maxLength:4096})}, + {key:"embed",optional:false,priority:0,checker:createTicketEmbedStructure("openticket:ticket-message-embed")}, + {key:"ping",optional:false,priority:0,checker:createTicketPingStructure("openticket:ticket-message-ping")} + ]})})}, + + //AUTOCLOSE + {key:"autoclose",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("openticket:ticket-autoclose",{children:[ + {key:"enableInactiveHours",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:ticket-autoclose-enable-hours",{})}, + {key:"inactiveHours",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("openticket:ticket-autoclose-hours",{zeroAllowed:false,negativeAllowed:false,floatAllowed:true,min:1,max:8544})}, + {key:"enableUserLeave",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:ticket-autoclose-enable-leave",{})}, + {key:"disableOnClaim",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:ticket-autoclose-disable-claim",{})}, + ]})}, + + //AUTODELETE + {key:"autodelete",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("openticket:ticket-autodelete",{children:[ + {key:"enableInactiveDays",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:ticket-autodelete-enable-days",{})}, + {key:"inactiveDays",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("openticket:ticket-autodelete-days",{zeroAllowed:false,negativeAllowed:false,floatAllowed:true,min:1,max:356})}, + {key:"enableUserLeave",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:ticket-autodelete-enable-leave",{})}, + {key:"disableOnClaim",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:ticket-autodelete-disable-claim",{})}, + ]})}, + + //COOLDOWN + {key:"cooldown",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("openticket:ticket-cooldown",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("openticket:ticket-cooldown",{children:[ + {key:"cooldownMinutes",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("openticket:ticket-cooldown-minutes",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,max:512640})}, + ]})})}, + + //LIMITS + {key:"limits",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("openticket:ticket-limits",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("openticket:ticket-limits",{children:[ + {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("openticket:ticket-limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1})}, + {key:"userMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("openticket:ticket-limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1})} + ]})})}, + ]})}, + + //WEBSITE + {name:"website",priority:0,properties:[{key:"type",value:"website"}],checker:new api.ODCheckerObjectStructure("openticket:options-website",{children:[ + {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("openticket:website-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, + {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:website-name",{minLength:3,maxLength:50})}, + {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:website-description",{maxLength:256})}, + + //WEBSITE BUTTON + {key:"button",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("openticket:ticket-button",{children:[ + {key:"emoji",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_EmojiString("openticket:ticket-button-emoji",0,1,true)}, + {key:"label",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-button-label",{maxLength:50})}, + ],custom:(checker,value,locationTrace,locationId,locationDocs) => { + const lt = checker.locationTraceDeref(locationTrace) + //check if emoji & label exists + if (typeof value != "object") return false + else if (value && value["emoji"].length < 1 && value["label"].length < 1){ + //label & emoji are both empty + checker.createMessage("openticket:invalid-button","error",`This button needs to have at least an "emoji" or "label"!`,lt,null,[`"emoji"`,`"label"`],locationId,locationDocs) + return false + }else return true + }})}, + + //WEBSITE URL + {key:"url",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("openticket:website-url",false,{allowHttp:false})}, + ]})}, + + //REACTION ROLES + {name:"role",priority:0,properties:[{key:"type",value:"role"}],checker:new api.ODCheckerObjectStructure("openticket:options-role",{children:[ + {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("openticket:role-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, + {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:role-name",{minLength:3,maxLength:50})}, + {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:role-description",{maxLength:256})}, + + //ROLE BUTTON + {key:"button",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("openticket:ticket-button",{children:[ + {key:"emoji",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_EmojiString("openticket:ticket-button-emoji",0,1,true)}, + {key:"label",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-button-label",{maxLength:50})}, + {key:"color",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:ticket-button-color",{choices:["gray","red","green","blue"]})}, + ],custom:(checker,value,locationTrace,locationId,locationDocs) => { + const lt = checker.locationTraceDeref(locationTrace) + //check if emoji & label exists + if (typeof value != "object") return false + else if (value && value["emoji"].length < 1 && value["label"].length < 1){ + //label & emoji are both empty + checker.createMessage("openticket:invalid-button","error",`This button needs to have at least an "emoji" or "label"!`,lt,null,[`"emoji"`,`"label"`],locationId,locationDocs) + return false + }else return true + }})}, + + //ROLE SETTINGS + {key:"roles",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("openticket:role-roles","role",[],{allowDoubles:false,minLength:1})}, + {key:"mode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:role-mode",{choices:["add","remove","add&remove"]})}, + {key:"removeRolesOnAdd",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("openticket:role-remove-roles","role",[],{allowDoubles:false})}, + {key:"addOnMemberJoin",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:role-add-on-join",{})}, + ]})}, +]})}) + +export const defaultPanelsStructure = new api.ODCheckerArrayStructure("openticket:panels",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("openticket:panels",{children:[ + {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("openticket:panel-id","openticket","panel-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, + {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:panel-name",{minLength:3,maxLength:50})}, + {key:"dropdown",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:panel-dropdown",{})}, + {key:"options",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("openticket:panel-options","openticket","option-ids","option-ids-used",{allowDoubles:false,maxLength:25})}, + + //EMBED & TEXT + {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:panel-text",{maxLength:4096})}, + {key:"embed",optional:false,priority:0,checker:createPanelEmbedStructure("openticket:panel-embed")}, + + //SETTINGS + {key:"settings",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("openticket:panel-settings",{children:[ + {key:"dropdownPlaceholder",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:panel-settings-placeholder",{maxLength:100})}, + {key:"enableMaxTicketsWarningInText",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:panel-settings-maxtickets-text",{})}, + {key:"enableMaxTicketsWarningInEmbed",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:panel-settings-maxtickets-embed",{})}, + + {key:"describeOptionsLayout",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:panel-settings-describe-layout",{choices:["simple","normal","detailed"]})}, + {key:"describeOptionsCustomTitle",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:panel-settings-describe-title",{maxLength:512})}, + {key:"describeOptionsInText",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:panel-settings-describe-text",{})}, + {key:"describeOptionsInEmbedFields",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:panel-settings-describe-fields",{})}, + {key:"describeOptionsInEmbedDescription",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:panel-settings-describe-embed",{})}, + ]})}, +]})}) + +export const defaultQuestionsStructure = new api.ODCheckerArrayStructure("openticket:questions",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("openticket:questions",{children:[ + {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("openticket:question-id","openticket","question-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, + {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:question-name",{minLength:3,maxLength:50})}, + {key:"type",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:question-type",{choices:["short","paragraph"]})}, + + {key:"required",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:question-required",{})}, + {key:"placeholder",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:question-placeholder",{maxLength:100})}, + + {key:"length",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("openticket:question-length",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("openticket:question-length",{children:[ + {key:"min",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("openticket:question-length-min",{min:0,max:1024,negativeAllowed:false,floatAllowed:false})}, + {key:"max",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("openticket:question-length-max",{min:1,max:1024,negativeAllowed:false,floatAllowed:false})}, + ]})})}, +]})}) + +export const defaultTranscriptsStructure = new api.ODCheckerObjectStructure("openticket:transcripts",{children:[ + //GENERAL + {key:"general",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("openticket:transcripts-general",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("openticket:transcripts-general",{children:[ + {key:"enableChannel",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:transcripts-enable-channel",{})}, + {key:"enableCreatorDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:transcripts-enable-creator-dm",{})}, + {key:"enableParticipantDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:transcripts-enable-participant-dm",{})}, + {key:"enableActiveAdminDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:transcripts-enable-active-admin-dm",{})}, + {key:"enableEveryAdminDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:transcripts-enable-every-admin-dm",{})}, + + {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("openticket:transcripts-channel","channel",true,[])}, + {key:"mode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:transcripts-mode",{choices:["html","text"]})}, + ]})})}, + + //EMBED SETTINGS + {key:"embedSettings",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("openticket:transcripts-embed-settings",{children:[ + {key:"customColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("openticket:transcripts-embed-color",false,true)}, + {key:"listAllParticipants",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:transcripts-embed-list-participants",{})}, + {key:"includeTicketStats",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:transcripts-embed-include-ticket-stats",{})}, + ]})}, + + //TEXT STYLE + {key:"textTranscriptStyle",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("openticket:transcripts-text",{children:[ + {key:"layout",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:transcripts-text-layout",{choices:["simple","normal","detailed"]})}, + {key:"includeStats",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:transcripts-text-include-stats",{})}, + {key:"includeIds",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:transcripts-text-include-ids",{})}, + {key:"includeEmbeds",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:transcripts-text-include-embeds",{})}, + {key:"includeFiles",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:transcripts-text-include-files",{})}, + {key:"includeBotMessages",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("openticket:transcripts-text-include-bots",{})}, + + {key:"fileMode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:transcripts-text-file-mode",{choices:["custom","name","id"]})}, + {key:"customFileName",optional:false,priority:0,checker:new api.ODCheckerStringStructure("openticket:transcripts-file-name",{maxLength:512,regex:/^[^\.#%&{}\\<>*?/!'":@`|=]*$/})}, + ]})}, + + //HTML STYLE + {key:"htmlTranscriptStyle",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("openticket:transcripts-html",{children:[ + //HTML BACKGROUND + {key:"background",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("openticket:transcripts-html-background",{property:"enableCustomBackground",enabledValue:true,checker:new api.ODCheckerObjectStructure("openticket:transcripts-html-background",{children:[ + {key:"backgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("openticket:transcripts-html-background-color",false,true)}, + {key:"backgroundImage",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("openticket:transcripts-html-background-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, + ]})})}, + + //HTML HEADER + {key:"header",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("openticket:transcripts-html-header",{property:"enableCustomHeader",enabledValue:true,checker:new api.ODCheckerObjectStructure("openticket:transcripts-html-header",{children:[ + {key:"backgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("openticket:transcripts-html-header-bgcolor",false,false)}, + {key:"decoColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("openticket:transcripts-html-header-decocolor",false,false)}, + {key:"textColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("openticket:transcripts-html-header-textcolor",false,false)}, + ]})})}, + + //HTML STATS + {key:"stats",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("openticket:transcripts-html-stats",{property:"enableCustomStats",enabledValue:true,checker:new api.ODCheckerObjectStructure("openticket:transcripts-html-stats",{children:[ + {key:"backgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("openticket:transcripts-html-stats-bgcolor",false,false)}, + {key:"keyTextColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("openticket:transcripts-html-stats-keycolor",false,false)}, + {key:"valueTextColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("openticket:transcripts-html-stats-valuecolor",false,false)}, + {key:"hideBackgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("openticket:transcripts-html-stats-hidebgcolor",false,false)}, + {key:"hideTextColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("openticket:transcripts-html-stats-hidecolor",false,false)}, + ]})})}, + + //HTML FAVICON + {key:"favicon",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("openticket:transcripts-html-favicon",{property:"enableCustomFavicon",enabledValue:true,checker:new api.ODCheckerObjectStructure("openticket:transcripts-html-favicon",{children:[ + {key:"imageUrl",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("openticket:transcripts-html-favicon-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp"]})}, + ]})})}, + ]})}, +]}) + +export const defaultUnusedOptionsFunction = (manager:api.ODCheckerManager, functions:api.ODCheckerFunctionManager): api.ODCheckerResult => { + const optionList: string[] = manager.storage.get("openticket","option-ids") + const usedOptionList: string[] = manager.storage.get("openticket","option-ids-used") + if (!optionList || ! usedOptionList) return {valid:true,messages:[]} + + const optionChecker = manager.get("openticket:options") + if (!optionChecker) return {valid:true,messages:[]} + + const final: api.ODCheckerMessage[] = [] + optionList.forEach((id) => { + if (!usedOptionList.includes(id)){ + //id isn't used anywhere => create warning + final.push(functions.createMessage("openticket:options","openticket:unused-option",optionChecker.config.file,"warning",`The option "${id}" isn't used anywhere!`,[],null,[`"${id}"`],new api.ODId("openticket:unused-options"),null)) + } + }) + + return {valid:true,messages:final} +} + +export const defaultUnusedQuestionsFunction = (manager:api.ODCheckerManager, functions:api.ODCheckerFunctionManager): api.ODCheckerResult => { + const questionList: string[] = manager.storage.get("openticket","question-ids") + const usedQuestionList: string[] = manager.storage.get("openticket","question-ids-used") + if (!questionList || ! usedQuestionList) return {valid:true,messages:[]} + + const questionChecker = manager.get("openticket:questions") + if (!questionChecker) return {valid:true,messages:[]} + + const final: api.ODCheckerMessage[] = [] + questionList.forEach((id) => { + if (!usedQuestionList.includes(id)){ + //id isn't used anywhere => create warning + final.push(functions.createMessage("openticket:questions","openticket:unused-question",questionChecker.config.file,"warning",`The question "${id}" isn't used anywhere!`,[],null,[`"${id}"`],new api.ODId("openticket:unused-questions"),null)) + } + }) + + return {valid:true,messages:final} +} + +export const defaultDropdownOptionsFunction = (manager:api.ODCheckerManager, functions:api.ODCheckerFunctionManager): api.ODCheckerResult => { + + const panelList: string[] = manager.storage.get("openticket","panel-ids") + if (!panelList) return {valid:true,messages:[]} + + const panelConfig = openticket.configs.get("openticket:panels") + if (!panelConfig) return {valid:true,messages:[]} + + const optionConfig = openticket.configs.get("openticket:options") + if (!optionConfig) return {valid:true,messages:[]} + + const final: api.ODCheckerMessage[] = [] + panelList.forEach((id,index) => { + const panel = panelConfig.data.find((panel) => panel.id == id) + if (!panel || !panel.dropdown) return false + if (panel.options.some((optId) => { + const option = optionConfig.data.find((option) => option.id == optId) + if (!option) return false + if (option.type != "ticket") return true + else return false + })){ + //give error when non-ticket options exist in dropdown panel! + final.push(functions.createMessage("openticket:panels","openticket:dropdown-option",panelConfig.file,"error","A panel with dropdown enabled can only contain options of the 'ticket' type!",[index,"options"],null,[],new api.ODId("openticket:dropdown-options"),null)) + } + }) + + return {valid:(final.length < 1),messages:final} +} \ No newline at end of file diff --git a/src/data/framework/codeLoader.ts b/src/data/framework/codeLoader.ts new file mode 100644 index 0000000..dd65bb7 --- /dev/null +++ b/src/data/framework/codeLoader.ts @@ -0,0 +1,481 @@ +import {openticket, api, utilities} from "../../index" +import * as discord from "discord.js" + +const generalConfig = openticket.configs.get("openticket:general") +const globalDatabase = openticket.databases.get("openticket:global") +const userDatabase = openticket.databases.get("openticket:users") +const ticketDatabase = openticket.databases.get("openticket:tickets") +const statsDatabase = openticket.databases.get("openticket:stats") +const optionDatabase = openticket.databases.get("openticket:options") +const mainServer = openticket.client.mainServer + +export const loadAllCode = async () => { + if (!generalConfig || !mainServer || !globalDatabase || !userDatabase || !ticketDatabase || !statsDatabase || !optionDatabase) return + + loadCommandErrorHandlingCode() + loadStartListeningInteractionsCode() + loadDatabaseCleanersCode() + loadPanelAutoUpdateCode() + loadDatabaseSaversCode() + loadAutoCode() +} + +export const loadCommandErrorHandlingCode = async () => { + //COMMAND ERROR HANDLING + openticket.code.add(new api.ODCode("openticket:command-error-handling",14,() => { + //invalid/missing options + openticket.client.textCommands.onError(async (error) => { + if (error.type == "invalid_option"){ + error.msg.channel.send((await openticket.builders.messages.getSafe("openticket:error-option-invalid").build("text",{guild:error.msg.guild,channel:error.msg.channel,user:error.msg.author,error})).message) + }else if (error.type == "missing_option"){ + error.msg.channel.send((await openticket.builders.messages.getSafe("openticket:error-option-missing").build("text",{guild:error.msg.guild,channel:error.msg.channel,user:error.msg.author,error})).message) + }else if (error.type == "unknown_command" && generalConfig.data.system.sendErrorOnUnknownCommand){ + error.msg.channel.send((await openticket.builders.messages.getSafe("openticket:error-unknown-command").build("text",{guild:error.msg.guild,channel:error.msg.channel,user:error.msg.author,error})).message) + } + }) + + //responder timeout + openticket.responders.commands.setTimeoutErrorCallback(async (instance,source) => { + instance.reply(await openticket.builders.messages.getSafe("openticket:error-responder-timeout").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user})) + },null) + openticket.responders.buttons.setTimeoutErrorCallback(async (instance,source) => { + instance.reply(await openticket.builders.messages.getSafe("openticket:error-responder-timeout").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user})) + },null) + openticket.responders.dropdowns.setTimeoutErrorCallback(async (instance,source) => { + instance.reply(await openticket.builders.messages.getSafe("openticket:error-responder-timeout").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user})) + },null) + openticket.responders.modals.setTimeoutErrorCallback(async (instance,source) => { + if (!instance.channel){ + instance.reply({id:new api.ODId("looks-like-we-got-an-error-here"), ephemeral:true, message:{ + content:":x: **Something went wrong while replying to this modal!**" + }}) + return + } + instance.reply(await openticket.builders.messages.getSafe("openticket:error-responder-timeout").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user})) + },null) + })) +} + +export const loadStartListeningInteractionsCode = async () => { + //START LISTENING TO INTERACTIONS + openticket.code.add(new api.ODCode("openticket:start-listening-interactions",13,() => { + openticket.client.slashCommands.startListeningToInteractions() + openticket.client.textCommands.startListeningToInteractions() + })) +} + +export const loadDatabaseCleanersCode = async () => { + if (!mainServer) return + + //PANEL DATABASE CLEANER + openticket.code.add(new api.ODCode("openticket:panel-database-cleaner",12,async () => { + const validPanels: string[] = [] + + //check global database for valid panel embeds + for (const panel of (globalDatabase.getCategory("openticket:panel-update") ?? [])){ + if (!validPanels.includes(panel.key)){ + try{ + const splittedId = panel.key.split("_") + const message = await openticket.client.fetchGuildChannelMessage(mainServer,splittedId[0],splittedId[1]) + if (message) validPanels.push(panel.key) + }catch{} + } + } + + //remove all unused panels + for (const panel of (globalDatabase.getCategory("openticket:panel-update") ?? [])){ + if (!validPanels.includes(panel.key)){ + globalDatabase.delete("openticket:panel-update",panel.key) + } + } + + //delete panel from database on delete + openticket.client.client.on("messageDelete",(msg) => { + if (globalDatabase.exists("openticket:panel-update",msg.channel.id+"_"+msg.id)){ + globalDatabase.delete("openticket:panel-update",msg.channel.id+"_"+msg.id) + } + }) + })) + + //SUFFIX DATABASE CLEANER + openticket.code.add(new api.ODCode("openticket:suffix-database-cleaner",11,async () => { + const validSuffixCounters: string[] = [] + const validSuffixHistories: string[] = [] + + //check global database for valid option suffix counters + for (const counter of (globalDatabase.getCategory("openticket:option-suffix-counter") ?? [])){ + if (!validSuffixCounters.includes(counter.key)){ + if (openticket.options.exists(counter.key)) validSuffixCounters.push(counter.key) + } + } + + //check global database for valid option suffix histories + for (const history of (globalDatabase.getCategory("openticket:option-suffix-history") ?? [])){ + if (!validSuffixHistories.includes(history.key)){ + if (openticket.options.exists(history.key)) validSuffixHistories.push(history.key) + } + } + + //remove all unused suffix counters + for (const counter of (globalDatabase.getCategory("openticket:option-suffix-counter") ?? [])){ + if (!validSuffixCounters.includes(counter.key)){ + globalDatabase.delete("openticket:option-suffix-counter",counter.key) + } + } + + //remove all unused suffix histories + for (const history of (globalDatabase.getCategory("openticket:option-suffix-history") ?? [])){ + if (!validSuffixHistories.includes(history.key)){ + globalDatabase.delete("openticket:option-suffix-history",history.key) + } + } + })) + + //OPTION DATABASE CLEANER + openticket.code.add(new api.ODCode("openticket:option-database-cleaner",10,() => { + //delete all unused options + openticket.options.getAll().forEach((option) => { + if (optionDatabase.exists("openticket:used-option",option.id.value) && !openticket.tickets.getAll().some((ticket) => ticket.option.id.value == option.id.value)){ + optionDatabase.delete("openticket:used-option",option.id.value) + } + }) + })) + + //USER DATABASE CLEANER (full async/parallel because it takes a lot of time) + openticket.code.add(new api.ODCode("openticket:user-database-cleaner",9,() => { + utilities.runAsync(async () => { + const validUsers: string[] = [] + + //check user database for valid users + for (const user of userDatabase.getAll()){ + if (!validUsers.includes(user.key)){ + try{ + const member = await mainServer.members.fetch(user.key) + if (member) validUsers.push(member.id) + }catch{} + } + } + + //check stats database for valid users + for (const stat of statsDatabase.getAll()){ + if (stat.category.startsWith("openticket:user_")){ + if (!validUsers.includes(stat.key)){ + try{ + const member = await mainServer.members.fetch(stat.key) + if (member) validUsers.push(member.id) + }catch{} + } + } + } + + //remove all unused users + for (const user of userDatabase.getAll()){ + if (!validUsers.includes(user.key)){ + userDatabase.delete(user.category,user.key) + } + } + + //remove all unused stats + for (const stat of statsDatabase.getAll()){ + if (stat.category.startsWith("openticket:user_")){ + if (!validUsers.includes(stat.key)){ + statsDatabase.delete(stat.category,stat.key) + } + } + } + }) + + //delete user from database on leave + openticket.client.client.on("guildMemberRemove",(member) => { + if (member.guild.id != mainServer.id) return + + //remove unused user + for (const user of userDatabase.getAll()){ + if (user.key == member.id){ + userDatabase.delete(user.category,user.key) + } + } + + //remove unused stats + for (const stat of statsDatabase.getAll()){ + if (stat.category.startsWith("openticket:user_")){ + if (stat.key == member.id){ + statsDatabase.delete(stat.category,stat.key) + } + } + } + }) + })) + + //TICKET DATABASE CLEANER + openticket.code.add(new api.ODCode("openticket:ticket-database-cleaner",8,async () => { + const validTickets: string[] = [] + + //check ticket database for valid tickets + for (const ticket of ticketDatabase.getAll()){ + if (!validTickets.includes(ticket.key)){ + try{ + const channel = await openticket.client.fetchGuildTextChannel(mainServer,ticket.key) + if (channel) validTickets.push(channel.id) + }catch{} + } + } + + //check stats database for valid tickets + for (const stat of statsDatabase.getAll()){ + if (stat.category.startsWith("openticket:ticket_")){ + if (!validTickets.includes(stat.key)){ + try{ + const channel = await openticket.client.fetchGuildTextChannel(mainServer,stat.key) + if (channel) validTickets.push(channel.id) + }catch{} + } + } + } + + //remove all unused tickets + for (const ticket of ticketDatabase.getAll()){ + if (!validTickets.includes(ticket.key)){ + ticketDatabase.delete(ticket.category,ticket.key) + openticket.tickets.remove(ticket.key) + } + } + + //remove all unused stats + for (const stat of statsDatabase.getAll()){ + if (stat.category.startsWith("openticket:ticket_")){ + if (!validTickets.includes(stat.key)){ + statsDatabase.delete(stat.category,stat.key) + } + } + } + + //delete ticket from database on delete + openticket.client.client.on("channelDelete",(channel) => { + if (channel.isDMBased() || channel.guild.id != mainServer.id) return + + //remove unused ticket + for (const ticket of ticketDatabase.getAll()){ + if (ticket.key == channel.id){ + ticketDatabase.delete(ticket.category,ticket.key) + openticket.tickets.remove(ticket.key) + } + } + + //remove unused stats + for (const stat of statsDatabase.getAll()){ + if (stat.category.startsWith("openticket:ticket_")){ + if (stat.key == channel.id){ + statsDatabase.delete(stat.category,stat.key) + } + } + } + }) + })) +} + +export const loadPanelAutoUpdateCode = async () => { + //PANEL AUTO UPDATE + openticket.code.add(new api.ODCode("openticket:panel-auto-update",7,async () => { + const globalDatabase = openticket.databases.get("openticket:global") + const panelIds = globalDatabase.getCategory("openticket:panel-update") ?? [] + if (!mainServer) return + + for (const panelId of panelIds){ + const panel = openticket.panels.get(panelId.value) + + //panel doesn't exist anymore in config and needs to be removed + if (!panel){ + globalDatabase.delete("openticket:panel-update",panelId.key) + return + } + + try{ + const splittedId = panelId.key.split("_") + const channel = await openticket.client.fetchGuildTextChannel(mainServer,splittedId[0]) + if (!channel) return + const message = await openticket.client.fetchGuildChannelMessage(mainServer,channel,splittedId[1]) + if (!message) return + + message.edit((await openticket.builders.messages.getSafe("openticket:panel").build("auto-update",{guild:mainServer,channel,user:openticket.client.client.user,panel})).message) + openticket.log("Panel in server got auto-updated!","info",[ + {key:"channelid",value:splittedId[0]}, + {key:"messageid",value:splittedId[1]}, + {key:"panel",value:panelId.value} + ]) + }catch{} + } + })) +} + +export const loadDatabaseSaversCode = async () => { + //TICKET SAVER + openticket.code.add(new api.ODCode("openticket:ticket-saver",6,() => { + const mainVersion = openticket.versions.get("openticket:version") + + openticket.tickets.onAdd((ticket) => { + ticketDatabase.set("openticket:ticket",ticket.id.value,ticket.toJson(mainVersion)) + + //add option to database if non-existent + if (!optionDatabase.exists("openticket:used-option",ticket.option.id.value)){ + optionDatabase.set("openticket:used-option",ticket.option.id.value,ticket.option.toJson(mainVersion)) + } + }) + openticket.tickets.onChange((ticket) => { + ticketDatabase.set("openticket:ticket",ticket.id.value,ticket.toJson(mainVersion)) + + //add option to database if non-existent + if (!optionDatabase.exists("openticket:used-option",ticket.option.id.value)){ + optionDatabase.set("openticket:used-option",ticket.option.id.value,ticket.option.toJson(mainVersion)) + } + + //delete all unused options on ticket move + openticket.options.getAll().forEach((option) => { + if (optionDatabase.exists("openticket:used-option",option.id.value) && !openticket.tickets.getAll().some((ticket) => ticket.option.id.value == option.id.value)){ + optionDatabase.delete("openticket:used-option",option.id.value) + } + }) + }) + openticket.tickets.onRemove((ticket) => { + ticketDatabase.delete("openticket:ticket",ticket.id.value) + + //remove option from database if unused + if (!openticket.tickets.getAll().some((ticket) => ticket.option.id.value == ticket.option.id.value)){ + optionDatabase.delete("openticket:used-option",ticket.option.id.value) + } + }) + })) + + //BLACKLIST SAVER + openticket.code.add(new api.ODCode("openticket:blacklist-saver",5,() => { + openticket.blacklist.onAdd((blacklist) => { + userDatabase.set("openticket:blacklist",blacklist.id.value,blacklist.reason) + }) + openticket.blacklist.onChange((blacklist) => { + userDatabase.set("openticket:blacklist",blacklist.id.value,blacklist.reason) + }) + openticket.blacklist.onRemove((blacklist) => { + userDatabase.delete("openticket:blacklist",blacklist.id.value) + }) + })) + + //AUTO ROLE ON JOIN + openticket.code.add(new api.ODCode("openticket:auto-role-on-join",4,() => { + openticket.client.client.on("guildMemberAdd",async (member) => { + for (const option of openticket.options.getAll()){ + if (option instanceof api.ODRoleOption && option.get("openticket:add-on-join").value){ + //add these roles on user join + await openticket.actions.get("openticket:reaction-role").run("panel-button",{guild:member.guild,user:member.user,option,overwriteMode:"add"}) + } + } + }) + })) +} + +const loadAutoCode = () => { + //AUTOCLOSE TIMEOUT + openticket.code.add(new api.ODCode("openticket:autoclose-timeout",3,() => { + setInterval(async () => { + let count = 0 + for (const ticket of openticket.tickets.getAll()){ + const channel = await openticket.tickets.getTicketChannel(ticket) + if (!channel) return + const lastMessage = (await channel.messages.fetch({limit:5})).first() + if (lastMessage && !ticket.get("openticket:closed").value){ + //ticket has last message + const disableOnClaim = ticket.option.get("openticket:autoclose-disable-claim").value && ticket.get("openticket:claimed").value + const enabled = (disableOnClaim) ? false : ticket.get("openticket:autoclose-enabled").value + const hours = ticket.get("openticket:autoclose-hours").value + + const time = hours*60*60*1000 //hours in milliseconds + if (enabled && (new Date().getTime() - lastMessage.createdTimestamp) >= time){ + //autoclose ticket + await openticket.actions.get("openticket:close-ticket").run("autoclose",{guild:channel.guild,channel,user:openticket.client.client.user,ticket,reason:"Autoclose",sendMessage:false}) + await channel.send((await openticket.builders.messages.getSafe("openticket:autoclose-message").build("timeout",{guild:channel.guild,channel,user:openticket.client.client.user,ticket})).message) + count++ + openticket.stats.get("openticket:global").setStat("openticket:tickets-autoclosed",1,"increase") + } + } + } + openticket.log("Finished autoclose timeout cycle!","system",[ + {key:"interval",value:openticket.defaults.getDefault("autocloseCheckInterval").toString(),hidden:true}, + {key:"closed",value:count.toString()} + ]) + },openticket.defaults.getDefault("autocloseCheckInterval")) + })) + + //AUTOCLOSE LEAVE + openticket.code.add(new api.ODCode("openticket:autoclose-leave",2,() => { + openticket.client.client.on("guildMemberRemove",async (member) => { + for (const ticket of openticket.tickets.getAll()){ + if (ticket.get("openticket:opened-by").value == member.id){ + const channel = await openticket.tickets.getTicketChannel(ticket) + if (!channel) return + //ticket has been created by this user + const disableOnClaim = ticket.option.get("openticket:autoclose-disable-claim").value && ticket.get("openticket:claimed").value + const enabled = (disableOnClaim || !ticket.get("openticket:autoclose-enabled").value) ? false : ticket.option.get("openticket:autoclose-enable-leave") + + if (enabled){ + //autoclose ticket + await openticket.actions.get("openticket:close-ticket").run("autoclose",{guild:channel.guild,channel,user:openticket.client.client.user,ticket,reason:"Autoclose",sendMessage:false}) + await channel.send((await openticket.builders.messages.getSafe("openticket:autoclose-message").build("leave",{guild:channel.guild,channel,user:openticket.client.client.user,ticket})).message) + openticket.stats.get("openticket:global").setStat("openticket:tickets-autoclosed",1,"increase") + } + } + } + }) + })) + + //AUTODELETE TIMEOUT + openticket.code.add(new api.ODCode("openticket:autodelete-timeout",1,() => { + setInterval(async () => { + let count = 0 + for (const ticket of openticket.tickets.getAll()){ + const channel = await openticket.tickets.getTicketChannel(ticket) + if (!channel) return + const lastMessage = (await channel.messages.fetch({limit:5})).first() + if (lastMessage){ + //ticket has last message + const disableOnClaim = ticket.option.get("openticket:autodelete-disable-claim").value && ticket.get("openticket:claimed").value + const enabled = (disableOnClaim) ? false : ticket.get("openticket:autodelete-enabled").value + const days = ticket.get("openticket:autodelete-days").value + + const time = days*24*60*60*1000 //days in milliseconds + if (enabled && (new Date().getTime() - lastMessage.createdTimestamp) >= time){ + //autodelete ticket + await channel.send((await openticket.builders.messages.getSafe("openticket:autodelete-message").build("timeout",{guild:channel.guild,channel,user:openticket.client.client.user,ticket})).message) + await openticket.actions.get("openticket:delete-ticket").run("autodelete",{guild:channel.guild,channel,user:openticket.client.client.user,ticket,reason:"Autodelete",sendMessage:false,withoutTranscript:false}) + count++ + openticket.stats.get("openticket:global").setStat("openticket:tickets-autodeleted",1,"increase") + } + } + } + openticket.log("Finished autodelete timeout cycle!","system",[ + {key:"interval",value:openticket.defaults.getDefault("autodeleteCheckInterval").toString(),hidden:true}, + {key:"deleted",value:count.toString()} + ]) + },openticket.defaults.getDefault("autodeleteCheckInterval")) + })) + + //AUTODELETE LEAVE + openticket.code.add(new api.ODCode("openticket:autodelete-leave",0,() => { + openticket.client.client.on("guildMemberRemove",async (member) => { + for (const ticket of openticket.tickets.getAll()){ + if (ticket.get("openticket:opened-by").value == member.id){ + const channel = await openticket.tickets.getTicketChannel(ticket) + if (!channel) return + //ticket has been created by this user + const disableOnClaim = ticket.option.get("openticket:autodelete-disable-claim").value && ticket.get("openticket:claimed").value + const enabled = (disableOnClaim || !ticket.get("openticket:autodelete-enabled").value) ? false : ticket.option.get("openticket:autodelete-enable-leave") + + if (enabled){ + //autodelete ticket + await channel.send((await openticket.builders.messages.getSafe("openticket:autodelete-message").build("leave",{guild:channel.guild,channel,user:openticket.client.client.user,ticket})).message) + await openticket.actions.get("openticket:delete-ticket").run("autodelete",{guild:channel.guild,channel,user:openticket.client.client.user,ticket,reason:"Autodelete",sendMessage:false,withoutTranscript:false}) + openticket.stats.get("openticket:global").setStat("openticket:tickets-autodeleted",1,"increase") + } + } + } + }) + })) +} \ No newline at end of file diff --git a/src/data/framework/commandLoader.ts b/src/data/framework/commandLoader.ts new file mode 100644 index 0000000..ee0e326 --- /dev/null +++ b/src/data/framework/commandLoader.ts @@ -0,0 +1,1076 @@ +import {openticket, api, utilities} from "../../index" +import * as discord from "discord.js" + +const lang = openticket.languages + +export const loadAllSlashCommands = async () => { + const commands = openticket.client.slashCommands + const generalConfig = openticket.configs.get("openticket:general") + if (!generalConfig) return + + const act = discord.ApplicationCommandType + const acot = discord.ApplicationCommandOptionType + + if (!generalConfig.data.slashCommands) return + + //create panel choices + const panelChoices : {name:string, value:string}[] = [] + openticket.configs.get("openticket:panels").data.forEach((panel) => { + panelChoices.push({name:panel.name, value:panel.id}) + }) + + //create ticket choices + const ticketChoices : {name:string, value:string}[] = [] + openticket.configs.get("openticket:options").data.forEach((option) => { + if (option.type != "ticket") return + ticketChoices.push({name:option.name, value:option.id}) + }) + + const allowedCommands: string[] = [] + for (const key in generalConfig.data.system.permissions){ + if (generalConfig.data.system.permissions[key] != "none") allowedCommands.push(key) + } + + //HELP + if (allowedCommands.includes("help")) commands.add(new api.ODSlashCommand("openticket:help",{ + type:act.ChatInput, + name:"help", + description:lang.getTranslation("commands.help"), + dmPermission:true + })) + + //PANEL + if (allowedCommands.includes("panel")) commands.add(new api.ODSlashCommand("openticket:panel",{ + type:act.ChatInput, + name:"panel", + description:lang.getTranslation("commands.panel"), + dmPermission:false, + options:[ + { + name:"id", + description:lang.getTranslation("commands.panelId"), + type:acot.String, + required:true, + choices:panelChoices + }, + { + name:"auto-update", + description:lang.getTranslation("commands.panelAutoUpdate"), + type:acot.Boolean, + required:false + } + ] + },(current) => { + //check if this slash command needs to be updated + if (!current.options) return true + const idOption = current.options.find((opt) => opt.name == "id" && opt.type == acot.String) as discord.ApplicationCommandStringOptionData|undefined + if (!idOption || !idOption.choices || idOption.choices.length != panelChoices.length) return true + else if (!panelChoices.every((panel) => { + if (!idOption.choices) return false + else if (!idOption.choices.find((choice) => choice.value == panel.value && choice.name == panel.name)) return false + else return true + })) return true + else return false + })) + + //TICKET (when enabled) + if (allowedCommands.includes("ticket")) commands.add(new api.ODSlashCommand("openticket:ticket",{ + type:act.ChatInput, + name:"ticket", + description:lang.getTranslation("commands.ticket"), + dmPermission:false, + options:[ + { + name:"id", + description:lang.getTranslation("commands.ticketId"), + type:acot.String, + required:true, + choices:ticketChoices + } + ] + },(current) => { + //check if this slash command needs to be updated + if (!current.options) return true + const idOption = current.options.find((opt) => opt.name == "id" && opt.type == acot.String) as discord.ApplicationCommandStringOptionData|undefined + if (!idOption || !idOption.choices || idOption.choices.length != ticketChoices.length) return true + else if (!ticketChoices.every((ticket) => { + if (!idOption.choices) return false + else if (!idOption.choices.find((choice) => choice.value == ticket.value && choice.name == ticket.name)) return false + else return true + })) return true + else return false + })) + + //CLOSE + if (allowedCommands.includes("close")) commands.add(new api.ODSlashCommand("openticket:close",{ + type:act.ChatInput, + name:"close", + description:lang.getTranslation("commands.close"), + dmPermission:false, + options:[ + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + })) + + //DELETE + if (allowedCommands.includes("delete") && generalConfig.data.system.enableDeleteWithoutTranscript) commands.add(new api.ODSlashCommand("openticket:delete",{ + type:act.ChatInput, + name:"delete", + description:lang.getTranslation("commands.delete"), + dmPermission:false, + options:[ + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + }, + { + name:"notranscript", + description:lang.getTranslation("commands.deleteNoTranscript"), + type:acot.Boolean, + required:false + } + ] + })) + else if (allowedCommands.includes("delete")) commands.add(new api.ODSlashCommand("openticket:delete",{ + type:act.ChatInput, + name:"delete", + description:lang.getTranslation("commands.delete"), + dmPermission:false, + options:[ + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + })) + + //REOPEN + if (allowedCommands.includes("reopen")) commands.add(new api.ODSlashCommand("openticket:reopen",{ + type:act.ChatInput, + name:"reopen", + description:lang.getTranslation("commands.reopen"), + dmPermission:false, + options:[ + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + })) + + //CLAIM + if (allowedCommands.includes("claim")) commands.add(new api.ODSlashCommand("openticket:claim",{ + type:act.ChatInput, + name:"claim", + description:lang.getTranslation("commands.claim"), + dmPermission:false, + options:[ + { + name:"user", + description:lang.getTranslation("commands.claimUser"), + type:acot.User, + required:false + }, + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + })) + + //UNCLAIM + if (allowedCommands.includes("unclaim")) commands.add(new api.ODSlashCommand("openticket:unclaim",{ + type:act.ChatInput, + name:"unclaim", + description:lang.getTranslation("commands.unclaim"), + dmPermission:false, + options:[ + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + })) + + //PIN + if (allowedCommands.includes("pin")) commands.add(new api.ODSlashCommand("openticket:pin",{ + type:act.ChatInput, + name:"pin", + description:lang.getTranslation("commands.pin"), + dmPermission:false, + options:[ + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + })) + + //UNPIN + if (allowedCommands.includes("unpin")) commands.add(new api.ODSlashCommand("openticket:unpin",{ + type:act.ChatInput, + name:"unpin", + description:lang.getTranslation("commands.unpin"), + dmPermission:false, + options:[ + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + })) + + //MOVE + if (allowedCommands.includes("move")) commands.add(new api.ODSlashCommand("openticket:move",{ + type:act.ChatInput, + name:"move", + description:lang.getTranslation("commands.move"), + dmPermission:false, + options:[ + { + name:"id", + description:lang.getTranslation("commands.moveId"), + type:acot.String, + required:true, + choices:ticketChoices + }, + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + },(current) => { + //check if this slash command needs to be updated + if (!current.options) return true + const idOption = current.options.find((opt) => opt.name == "id" && opt.type == acot.String) as discord.ApplicationCommandStringOptionData|undefined + if (!idOption || !idOption.choices || idOption.choices.length != ticketChoices.length) return true + else if (!ticketChoices.every((ticket) => { + if (!idOption.choices) return false + else if (!idOption.choices.find((choice) => choice.value == ticket.value && choice.name == ticket.name)) return false + else return true + })) return true + else return false + })) + + //RENAME + if (allowedCommands.includes("rename")) commands.add(new api.ODSlashCommand("openticket:rename",{ + type:act.ChatInput, + name:"rename", + description:lang.getTranslation("commands.rename"), + dmPermission:false, + options:[ + { + name:"name", + description:lang.getTranslation("commands.renameName"), + type:acot.String, + required:true, + maxLength:100 + }, + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + })) + + //ADD + if (allowedCommands.includes("add")) commands.add(new api.ODSlashCommand("openticket:add",{ + type:act.ChatInput, + name:"add", + description:lang.getTranslation("commands.add"), + dmPermission:false, + options:[ + { + name:"user", + description:lang.getTranslation("commands.addUser"), + type:acot.User, + required:true + }, + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + })) + + //REMOVE + if (allowedCommands.includes("remove")) commands.add(new api.ODSlashCommand("openticket:remove",{ + type:act.ChatInput, + name:"remove", + description:lang.getTranslation("commands.remove"), + dmPermission:false, + options:[ + { + name:"user", + description:lang.getTranslation("commands.removeUser"), + type:acot.User, + required:true + }, + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + })) + + //BLACKLIST + if (allowedCommands.includes("blacklist")) commands.add(new api.ODSlashCommand("openticket:blacklist",{ + type:act.ChatInput, + name:"blacklist", + description:lang.getTranslation("commands.blacklist"), + dmPermission:false, + options:[ + { + name:"view", + description:lang.getTranslation("commands.blacklistView"), + type:acot.Subcommand + }, + { + name:"add", + description:lang.getTranslation("commands.blacklistAdd"), + type:acot.Subcommand, + options:[ + { + name:"user", + description:lang.getTranslation("commands.addUser"), + type:acot.User, + required:true + }, + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + }, + { + name:"remove", + description:lang.getTranslation("commands.blacklistRemove"), + type:acot.Subcommand, + options:[ + { + name:"user", + description:lang.getTranslation("commands.removeUser"), + type:acot.User, + required:true + }, + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + }, + { + name:"get", + description:lang.getTranslation("commands.blacklistGet"), + type:acot.Subcommand, + options:[ + { + name:"user", + description:lang.getTranslation("commands.blacklistGetUser"), + type:acot.User, + required:true + } + ] + } + ] + })) + + //STATS + if (allowedCommands.includes("stats")) commands.add(new api.ODSlashCommand("openticket:stats",{ + type:act.ChatInput, + name:"stats", + description:lang.getTranslation("commands.stats"), + dmPermission:false, + options:[ + { + name:"reset", + description:lang.getTranslation("commands.statsReset"), + type:acot.Subcommand, + options:[ + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + }, + { + name:"global", + description:lang.getTranslation("commands.statsGlobal"), + type:acot.Subcommand + }, + { + name:"user", + description:lang.getTranslation("commands.statsUser"), + type:acot.Subcommand, + options:[ + { + name:"user", + description:lang.getTranslation("commands.statsUserUser"), + type:acot.User, + required:false + } + ] + }, + { + name:"ticket", + description:lang.getTranslation("commands.statsTicket"), + type:acot.Subcommand, + options:[ + { + name:"ticket", + description:lang.getTranslation("commands.statsTicketTicket"), + type:acot.Channel, + required:false + } + ] + } + ] + })) + + //CLEAR + if (allowedCommands.includes("clear")) commands.add(new api.ODSlashCommand("openticket:clear",{ + type:act.ChatInput, + name:"clear", + description:lang.getTranslation("commands.clear"), + dmPermission:false, + options:[ + { + name:"filter", + description:lang.getTranslation("commands.clearFilter"), + type:acot.String, + required:false, + choices:[ + {name:lang.getTranslation("commands.clearFilters.all"), value:"all"}, + {name:lang.getTranslation("commands.clearFilters.open"), value:"open"}, + {name:lang.getTranslation("commands.clearFilters.close"), value:"closed"}, + {name:lang.getTranslation("commands.clearFilters.claim"), value:"claimed"}, + {name:lang.getTranslation("commands.clearFilters.unclaim"), value:"unclaimed"}, + {name:lang.getTranslation("commands.clearFilters.pin"), value:"pinned"}, + {name:lang.getTranslation("commands.clearFilters.unpin"), value:"unpinned"}, + {name:lang.getTranslation("commands.clearFilters.autoclose"), value:"autoclosed"} + ] + } + ] + })) + + //AUTOCLOSE + if (allowedCommands.includes("autoclose")) commands.add(new api.ODSlashCommand("openticket:autoclose",{ + type:act.ChatInput, + name:"autoclose", + description:lang.getTranslation("commands.autoclose"), + dmPermission:false, + options:[ + { + name:"disable", + description:lang.getTranslation("commands.autocloseDisable"), + type:acot.Subcommand, + options:[ + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + }, + { + name:"enable", + description:lang.getTranslation("commands.autocloseEnable"), + type:acot.Subcommand, + options:[ + { + name:"time", + description:lang.getTranslation("commands.autocloseEnableTime"), + type:acot.Number, + required:true, + minValue:0.01 + }, + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + } + ] + })) + + //AUTODELETE + if (allowedCommands.includes("autodelete")) commands.add(new api.ODSlashCommand("openticket:autodelete",{ + type:act.ChatInput, + name:"autodelete", + description:lang.getTranslation("commands.autodelete"), + dmPermission:false, + options:[ + { + name:"disable", + description:lang.getTranslation("commands.autodeleteDisable"), + type:acot.Subcommand, + options:[ + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + }, + { + name:"enable", + description:lang.getTranslation("commands.autodeleteEnable"), + type:acot.Subcommand, + options:[ + { + name:"time", + description:lang.getTranslation("commands.autodeleteEnableTime"), + type:acot.Number, + required:true, + minValue:0.01 + }, + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + } + ] + })) +} + +export const loadAllTextCommands = async () => { + const commands = openticket.client.textCommands + const generalConfig = openticket.configs.get("openticket:general") + if (!generalConfig) return + + if (!generalConfig.data.textCommands) return + const prefix = generalConfig.data.prefix + + //create panel choices + const panelChoices : string[] = [] + openticket.configs.get("openticket:panels").data.forEach((panel) => { + panelChoices.push(panel.id) + }) + + //create ticket choices + const ticketChoices : string[] = [] + openticket.configs.get("openticket:options").data.forEach((option) => { + if (option.type != "ticket") return + ticketChoices.push(option.id) + }) + + const allowedCommands: string[] = [] + for (const key in generalConfig.data.system.permissions){ + if (generalConfig.data.system.permissions[key] != "none") allowedCommands.push(key) + } + + //HELP + if (allowedCommands.includes("help")) commands.add(new api.ODTextCommand("openticket:help",{ + name:"help", + prefix, + dmPermission:true, + guildPermission:true, + allowBots:false + })) + + //PANEL + if (allowedCommands.includes("panel")) commands.add(new api.ODTextCommand("openticket:panel",{ + name:"panel", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"id", + type:"string", + required:true + }, + { + name:"auto-update", + type:"boolean", + required:false + } + ] + })) + + //CLOSE + if (allowedCommands.includes("close")) commands.add(new api.ODTextCommand("openticket:close",{ + name:"close", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + //DELETE + if (allowedCommands.includes("delete")) commands.add(new api.ODTextCommand("openticket:delete",{ + name:"delete", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + //REOPEN + if (allowedCommands.includes("reopen")) commands.add(new api.ODTextCommand("openticket:reopen",{ + name:"reopen", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + //CLAIM + if (allowedCommands.includes("claim")) commands.add(new api.ODTextCommand("openticket:claim",{ + name:"claim", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"user", + type:"user", + required:false + }, + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + //UNCLAIM + if (allowedCommands.includes("unclaim")) commands.add(new api.ODTextCommand("openticket:unclaim",{ + name:"unclaim", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + //PIN + if (allowedCommands.includes("pin")) commands.add(new api.ODTextCommand("openticket:pin",{ + name:"pin", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + //UNPIN + if (allowedCommands.includes("unpin")) commands.add(new api.ODTextCommand("openticket:unpin",{ + name:"unpin", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + //MOVE + if (allowedCommands.includes("move")) commands.add(new api.ODTextCommand("openticket:move",{ + name:"move", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"id", + type:"string", + required:true, + choices:ticketChoices + }, + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + //RENAME + if (allowedCommands.includes("rename")) commands.add(new api.ODTextCommand("openticket:rename",{ + name:"rename", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"name", + type:"string", + required:true, + maxLength:100 + }, + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + //ADD + if (allowedCommands.includes("add")) commands.add(new api.ODTextCommand("openticket:add",{ + name:"add", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"user", + type:"user", + required:true + }, + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + //REMOVE + if (allowedCommands.includes("remove")) commands.add(new api.ODTextCommand("openticket:remove",{ + name:"remove", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"user", + type:"user", + required:true + }, + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + //BLACKLIST + if (allowedCommands.includes("blacklist")) commands.add(new api.ODTextCommand("openticket:blacklist-view",{ + name:"blacklist view", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false + })) + + if (allowedCommands.includes("blacklist")) commands.add(new api.ODTextCommand("openticket:blacklist-add",{ + name:"blacklist add", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"user", + type:"user", + required:true + }, + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + if (allowedCommands.includes("blacklist")) commands.add(new api.ODTextCommand("openticket:blacklist-remove",{ + name:"blacklist remove", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"user", + type:"user", + required:true + }, + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + if (allowedCommands.includes("blacklist")) commands.add(new api.ODTextCommand("openticket:blacklist-get",{ + name:"blacklist get", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"user", + type:"user", + required:true + } + ] + })) + + //STATS + if (allowedCommands.includes("stats")) commands.add(new api.ODTextCommand("openticket:stats-global",{ + name:"stats global", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false + })) + + if (allowedCommands.includes("stats")) commands.add(new api.ODTextCommand("openticket:stats-reset",{ + name:"stats reset", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + if (allowedCommands.includes("stats")) commands.add(new api.ODTextCommand("openticket:stats-user",{ + name:"stats user", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"user", + type:"user", + required:false + } + ] + })) + + if (allowedCommands.includes("stats")) commands.add(new api.ODTextCommand("openticket:stats-ticket",{ + name:"stats ticket", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"ticket", + type:"channel", + required:false, + channelTypes:[discord.ChannelType.GuildText] + } + ] + })) + + //CLEAR + if (allowedCommands.includes("clear")) commands.add(new api.ODTextCommand("openticket:clear",{ + name:"clear", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"filter", + type:"string", + required:false, + allowSpaces:false, + choices:[ + "all", + "open", + "closed", + "claimed", + "unclaimed", + "pinned", + "unpinned", + "autoclosed" + ] + } + ] + })) + + //AUTOCLOSE + if (allowedCommands.includes("autoclose")) commands.add(new api.ODTextCommand("openticket:autoclose-disable",{ + name:"autoclose disable", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + if (allowedCommands.includes("autoclose")) commands.add(new api.ODTextCommand("openticket:autoclose-enable",{ + name:"autoclose enable", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"time", + type:"number", + required:true, + min:0.01, + allowZero:false, + allowNegative:false + }, + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + //AUTODELETE + if (allowedCommands.includes("autodelete")) commands.add(new api.ODTextCommand("openticket:autodelete-disable",{ + name:"autodelete disable", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + + if (allowedCommands.includes("autodelete")) commands.add(new api.ODTextCommand("openticket:autodelete-enable",{ + name:"autodelete enable", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"time", + type:"number", + required:true, + min:0.01, + allowZero:false, + allowNegative:false + }, + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) +} \ No newline at end of file diff --git a/src/data/framework/configLoader.ts b/src/data/framework/configLoader.ts new file mode 100644 index 0000000..f64e711 --- /dev/null +++ b/src/data/framework/configLoader.ts @@ -0,0 +1,12 @@ +import {openticket, api, utilities} from "../../index" + +export const loadAllConfigs = async () => { + const devconfigFlag = openticket.flags.get("openticket:dev-config") + const isDevconfig = devconfigFlag ? devconfigFlag.value : false + + openticket.configs.add(new api.ODJsonConfig("openticket:general","general.json",(isDevconfig) ? "./devconfig/" : "./config/")) + openticket.configs.add(new api.ODJsonConfig("openticket:options","options.json",(isDevconfig) ? "./devconfig/" : "./config/")) + openticket.configs.add(new api.ODJsonConfig("openticket:panels","panels.json",(isDevconfig) ? "./devconfig/" : "./config/")) + openticket.configs.add(new api.ODJsonConfig("openticket:questions","questions.json",(isDevconfig) ? "./devconfig/" : "./config/")) + openticket.configs.add(new api.ODJsonConfig("openticket:transcripts","transcripts.json",(isDevconfig) ? "./devconfig/" : "./config/")) +} \ No newline at end of file diff --git a/src/data/framework/cooldownLoader.ts b/src/data/framework/cooldownLoader.ts new file mode 100644 index 0000000..8461ba3 --- /dev/null +++ b/src/data/framework/cooldownLoader.ts @@ -0,0 +1,17 @@ +import {openticket, api, utilities} from "../../index" + +export const loadAllCooldowns = async () => { + openticket.options.getAll().forEach((option) => { + if (!(option instanceof api.ODTicketOption)) return + loadTicketOptionCooldown(option) + }) +} + +export const loadTicketOptionCooldown = (option:api.ODTicketOption) => { + if (option.get("openticket:cooldown-enabled").value){ + //option has cooldown + const minutes = option.get("openticket:cooldown-minutes").value + const milliseconds = minutes*60000 + openticket.cooldowns.add(new api.ODTimeoutCooldown("openticket:option-cooldown_"+option.id.value,milliseconds)) + } +} \ No newline at end of file diff --git a/src/data/framework/databaseLoader.ts b/src/data/framework/databaseLoader.ts new file mode 100644 index 0000000..990e0a0 --- /dev/null +++ b/src/data/framework/databaseLoader.ts @@ -0,0 +1,53 @@ +import {openticket, api, utilities} from "../../index" +import * as fjs from "formatted-json-stringify" + +const devdatabaseFlag = openticket.flags.get("openticket:dev-database") +const isDevdatabase = devdatabaseFlag ? devdatabaseFlag.value : false + +export const loadAllDatabases = async () => { + openticket.databases.add(defaultGlobalDatabase) + openticket.databases.add(defaultStatsDatabase) + openticket.databases.add(defaultTicketsDatabase) + openticket.databases.add(defaultUsersDatabase) + openticket.databases.add(defaultOptionsDatabase) +} + +const defaultInlineFormatter = new fjs.ArrayFormatter(null,true,new fjs.ObjectFormatter(null,false,[ + new fjs.PropertyFormatter("category"), + new fjs.PropertyFormatter("key"), + new fjs.DefaultFormatter("value",false) +])) + +const defaultTicketFormatter = new fjs.ArrayFormatter(null,true,new fjs.ObjectFormatter(null,true,[ + new fjs.PropertyFormatter("category"), + new fjs.PropertyFormatter("key"), + new fjs.ObjectFormatter("value",true,[ + new fjs.PropertyFormatter("id"), + new fjs.PropertyFormatter("option"), + new fjs.PropertyFormatter("version"), + new fjs.ArrayFormatter("data",true,new fjs.ObjectFormatter(null,false,[ + new fjs.PropertyFormatter("id"), + new fjs.DefaultFormatter("value",false) + ])) + ]) +])) + +const defaultOptionFormatter = new fjs.ArrayFormatter(null,true,new fjs.ObjectFormatter(null,true,[ + new fjs.PropertyFormatter("category"), + new fjs.PropertyFormatter("key"), + new fjs.ObjectFormatter("value",true,[ + new fjs.PropertyFormatter("id"), + new fjs.PropertyFormatter("type"), + new fjs.PropertyFormatter("version"), + new fjs.ArrayFormatter("data",true,new fjs.ObjectFormatter(null,false,[ + new fjs.PropertyFormatter("id"), + new fjs.DefaultFormatter("value",false) + ])) + ]) +])) + +export const defaultGlobalDatabase = new api.ODFormattedJsonDatabase("openticket:global","global.json",defaultInlineFormatter,(isDevdatabase) ? "./devdatabase/" : "./database/") +export const defaultStatsDatabase = new api.ODFormattedJsonDatabase("openticket:stats","stats.json",defaultInlineFormatter,(isDevdatabase) ? "./devdatabase/" : "./database/") +export const defaultTicketsDatabase = new api.ODFormattedJsonDatabase("openticket:tickets","tickets.json",defaultTicketFormatter,(isDevdatabase) ? "./devdatabase/" : "./database/") +export const defaultUsersDatabase = new api.ODFormattedJsonDatabase("openticket:users","users.json",defaultInlineFormatter,(isDevdatabase) ? "./devdatabase/" : "./database/") +export const defaultOptionsDatabase = new api.ODFormattedJsonDatabase("openticket:options","options.json",defaultOptionFormatter,(isDevdatabase) ? "./devdatabase/" : "./database/") \ No newline at end of file diff --git a/src/data/framework/eventLoader.ts b/src/data/framework/eventLoader.ts new file mode 100644 index 0000000..0aa865b --- /dev/null +++ b/src/data/framework/eventLoader.ts @@ -0,0 +1,236 @@ +import {openticket, api, utilities} from "../../index" + +export const loadAllEvents = () => { + const eventList: (keyof api.ODEventIds_Default)[] = [ + //error handling + "onErrorHandling", + "afterErrorHandling", + + //plugins + "afterPluginsLoaded", + "onPluginClassLoad", + "afterPluginClassesLoaded", + "onPluginEventLoad", + "afterPluginEventsLoaded", + + //flags + "onFlagLoad", + "afterFlagsLoaded", + "onFlagInit", + "afterFlagsInitiated", + + //configs + "onConfigLoad", + "afterConfigsLoaded", + + //databases + "onDatabaseLoad", + "afterDatabasesLoaded", + + //languages + "onLanguageLoad", + "afterLanguagesLoaded", + "onLanguageSelect", + "afterLanguagesSelected", + + //sessions + "onSessionLoad", + "afterSessionsLoaded", + + //config checkers + "onCheckerLoad", + "afterCheckersLoaded", + "onCheckerFunctionLoad", + "afterCheckerFunctionsLoaded", + "onCheckerExecute", + "afterCheckersExecuted", + "onCheckerTranslationLoad", + "afterCheckerTranslationsLoaded", + "onCheckerRender", + "afterCheckersRendered", + "onCheckerQuit", + + //client configuration + "onClientLoad", + "afterClientLoaded", + "onClientInit", + "afterClientInitiated", + "onClientReady", + "afterClientReady", + "onClientActivityLoad", + "afterClientActivityLoaded", + "onClientActivityInit", + "afterClientActivityInitiated", + + //client slash commands + "onSlashCommandLoad", + "afterSlashCommandsLoaded", + "onSlashCommandRegister", + "afterSlashCommandsRegistered", + + //client text commands + "onTextCommandLoad", + "afterTextCommandsLoaded", + + //questions + "onQuestionLoad", + "afterQuestionsLoaded", + + //options + "onOptionLoad", + "afterOptionsLoaded", + + //panels + "onPanelLoad", + "afterPanelsLoaded", + "onPanelSpawn", + "afterPanelSpawned", + + //tickets + "onTicketLoad", + "afterTicketsLoaded", + + //ticket creation + "onTicketChannelCreation", + "afterTicketChannelCreated", + "onTicketChannelDeletion", + "afterTicketChannelDeleted", + "onTicketPermissionsCreated", + "afterTicketPermissionsCreated", + "onTicketMainMessageCreated", + "afterTicketMainMessageCreated", + + //ticket actions + "onTicketCreate", + "afterTicketCreated", + "onTicketClose", + "afterTicketClosed", + "onTicketReopen", + "afterTicketReopened", + "onTicketDelete", + "afterTicketDeleted", + "onTicketMove", + "afterTicketMoved", + "onTicketClaim", + "afterTicketClaimed", + "onTicketUnclaim", + "afterTicketUnclaimed", + "onTicketPin", + "afterTicketPinned", + "onTicketUnpin", + "afterTicketUnpinned", + "onTicketUserAdd", + "afterTicketUserAdded", + "onTicketUserRemove", + "afterTicketUserRemoved", + "onTicketRename", + "afterTicketRenamed", + "onTicketsClear", + "afterTicketsCleared", + + //roles + "onRoleLoad", + "afterRolesLoaded", + "onRoleUpdate", + "afterRolesUpdated", + + //blacklist + "onBlacklistLoad", + "afterBlacklistLoaded", + + //transcripts + "onTranscriptCompilerLoad", + "afterTranscriptCompilersLoaded", + "onTranscriptHistoryLoad", + "afterTranscriptHistoryLoaded", + + //transcript creation + "onTranscriptCreate", + "afterTranscriptCreated", + "onTranscriptInit", + "afterTranscriptInitiated", + "onTranscriptCompile", + "afterTranscriptCompiled", + "onTranscriptReady", + "afterTranscriptReady", + + //builders + "onButtonBuilderLoad", + "afterButtonBuildersLoaded", + "onDropdownBuilderLoad", + "afterDropdownBuildersLoaded", + "onFileBuilderLoad", + "afterFileBuildersLoaded", + "onEmbedBuilderLoad", + "afterEmbedBuildersLoaded", + "onMessageBuilderLoad", + "afterMessageBuildersLoaded", + "onModalBuilderLoad", + "afterModalBuildersLoaded", + + //responders + "onCommandResponderLoad", + "afterCommandRespondersLoaded", + "onButtonResponderLoad", + "afterButtonRespondersLoaded", + "onDropdownResponderLoad", + "afterDropdownRespondersLoaded", + "onModalResponderLoad", + "afterModalRespondersLoaded", + + //actions + "onActionLoad", + "afterActionsLoaded", + + //verifybars + "onVerifyBarLoad", + "afterVerifyBarsLoaded", + + //permissions + "onPermissionLoad", + "afterPermissionsLoaded", + + //posts + "onPostLoad", + "afterPostsLoaded", + "onPostInit", + "afterPostsInitiated", + + //cooldowns + "onCooldownLoad", + "afterCooldownsLoaded", + "onCooldownInit", + "afterCooldownsInitiated", + + //help menu + "onHelpMenuCategoryLoad", + "afterHelpMenuCategoriesLoaded", + "onHelpMenuComponentLoad", + "afterHelpMenuComponentsLoaded", + + //stats + "onStatScopeLoad", + "afterStatScopesLoaded", + "onStatLoad", + "afterStatsLoaded", + "onStatInit", + "afterStatsInitiated", + + //code + "onCodeLoad", + "afterCodeLoaded", + "onCodeExecute", + "afterCodeExecuted", + + //livestatus + "onLiveStatusSourceLoad", + "afterLiveStatusSourcesLoaded", + + //startscreen + "onStartScreenLoad", + "afterStartScreensLoaded", + "onStartScreenRender", + "afterStartScreensRendered" + ] + eventList.forEach((event) => openticket.events.add(new api.ODEvent(event))) +} \ No newline at end of file diff --git a/src/data/framework/flagLoader.ts b/src/data/framework/flagLoader.ts new file mode 100644 index 0000000..7552019 --- /dev/null +++ b/src/data/framework/flagLoader.ts @@ -0,0 +1,16 @@ +import {openticket, api, utilities} from "../../index" + +export const loadAllFlags = async () => { + openticket.flags.add(new api.ODFlag("openticket:no-migration","No Migration","Disable Open Ticket data migration on update!","--no-migration",["-nm"])) + openticket.flags.add(new api.ODFlag("openticket:dev-config","Developer Config","Use the configs in /devconfig instead of /config!","--dev-config",["-dc"])) + openticket.flags.add(new api.ODFlag("openticket:dev-database","Developer Database","Use the databases in /devdatabase instead of /database!","--dev-database",["-nd"])) + openticket.flags.add(new api.ODFlag("openticket:debug","Debug Mode","Couldn't you find the error? Try to check this out!","--debug",["-d"])) + openticket.flags.add(new api.ODFlag("openticket:crash","Crash On Error","Crash the bot on an unknown error!","--crash",["-cr"])) + openticket.flags.add(new api.ODFlag("openticket:no-transcripts","No HTML Transcripts","Disable uploading HTML transcripts (for debugging)","--no-transcripts",["-nt"])) + openticket.flags.add(new api.ODFlag("openticket:no-checker","No Config Checker","Disable the Config Checker (for debugging)","--no-checker",["-nc"])) + openticket.flags.add(new api.ODFlag("openticket:checker","Full Config Checker","Render the Config Checker with extra details!","--checker",["-c"])) + openticket.flags.add(new api.ODFlag("openticket:no-easter","No Easter Eggs","Disable little Open Ticket easter eggs hidden in the bot!","--no-easter",["-ne"])) + openticket.flags.add(new api.ODFlag("openticket:no-plugins","No Plugins","Disable all Open Ticket plugins!","--no-plugins",["-np"])) + openticket.flags.add(new api.ODFlag("openticket:soft-plugins","Soft Plugins","Don't crash the bot when a plugin crashes!","--soft-plugins",["-sp"])) + openticket.flags.add(new api.ODFlag("openticket:force-slash-update","Force Slash Update","Force update all slash commands.","--force-slash",["-fs"])) +} \ No newline at end of file diff --git a/src/data/framework/helpMenuLoader.ts b/src/data/framework/helpMenuLoader.ts new file mode 100644 index 0000000..55c84b6 --- /dev/null +++ b/src/data/framework/helpMenuLoader.ts @@ -0,0 +1,263 @@ +import {openticket, api, utilities} from "../../index" + +const lang = openticket.languages + +export const loadAllHelpMenuCategories = async () => { + const helpmenu = openticket.helpmenu + + helpmenu.add(new api.ODHelpMenuCategory("openticket:general",5,utilities.emojiTitle("📎","General Commands"))) //TODO TRANSLATION!!! + helpmenu.add(new api.ODHelpMenuCategory("openticket:ticket-basic",4,utilities.emojiTitle("🎫","Basic Ticket Commands"))) //TODO TRANSLATION!!! + helpmenu.add(new api.ODHelpMenuCategory("openticket:ticket-advanced",4,utilities.emojiTitle("💡","Advanced Ticket Commands"))) //TODO TRANSLATION!!! + helpmenu.add(new api.ODHelpMenuCategory("openticket:ticket-user",3,utilities.emojiTitle("👤","User Ticket Commands"))) //TODO TRANSLATION!!! + helpmenu.add(new api.ODHelpMenuCategory("openticket:admin",2,utilities.emojiTitle("🚨","Admin Commands"))) //TODO TRANSLATION!!! + helpmenu.add(new api.ODHelpMenuCategory("openticket:advanced",1,utilities.emojiTitle("🚧","Advanced Commands"))) //TODO TRANSLATION!!! + helpmenu.add(new api.ODHelpMenuCategory("openticket:extra",0,utilities.emojiTitle("✨","Extra Commands"))) //TODO TRANSLATION!!! +} + +export const loadAllHelpMenuComponents = async () => { + const helpmenu = openticket.helpmenu + const generalConfig = openticket.configs.get("openticket:general") + if (!generalConfig) return + + const prefix = generalConfig.data.prefix + const enableDeleteWithoutTranscript = generalConfig.data.system.enableDeleteWithoutTranscript + + const allowedCommands: string[] = [] + for (const key in generalConfig.data.system.permissions){ + if (generalConfig.data.system.permissions[key] != "none") allowedCommands.push(key) + } + + const general = helpmenu.get("openticket:general") + if (general){ + if (allowedCommands.includes("help")) general.add(new api.ODHelpMenuCommandComponent("openticket:help",1,{ + textName:prefix+"help", + textDescription:lang.getTranslation("helpMenu.help"), + slashName:"/help", + slashDescription:lang.getTranslation("helpMenu.help") + })) + if (allowedCommands.includes("ticket")) general.add(new api.ODHelpMenuCommandComponent("openticket:ticket",0,{ + slashName:"/ticket", + slashDescription:lang.getTranslation("commands.ticket"), + slashOptions:[{name:"id",optional:false}] + })) + } + + const ticketBasic = helpmenu.get("openticket:ticket-basic") + if (ticketBasic){ + if (allowedCommands.includes("close")) ticketBasic.add(new api.ODHelpMenuCommandComponent("openticket:close",10,{ + textName:prefix+"close", + textDescription:lang.getTranslation("helpMenu.close"), + slashName:"/close", + slashDescription:lang.getTranslation("helpMenu.close"), + textOptions:[{name:"reason",optional:true}], + slashOptions:[{name:"reason",optional:true}] + })) + if (enableDeleteWithoutTranscript){ + if (allowedCommands.includes("delete")) ticketBasic.add(new api.ODHelpMenuCommandComponent("openticket:delete",9,{ + textName:prefix+"delete", + textDescription:lang.getTranslation("helpMenu.delete"), + slashName:"/delete", + slashDescription:lang.getTranslation("helpMenu.delete"), + textOptions:[{name:"reason",optional:true}], + slashOptions:[{name:"notranscript",optional:true},{name:"reason",optional:true}] + })) + }else{ + if (allowedCommands.includes("delete")) ticketBasic.add(new api.ODHelpMenuCommandComponent("openticket:delete",9,{ + textName:prefix+"delete", + textDescription:lang.getTranslation("helpMenu.delete"), + slashName:"/delete", + slashDescription:lang.getTranslation("helpMenu.delete"), + textOptions:[{name:"reason",optional:true}], + slashOptions:[{name:"reason",optional:true}] + })) + } + if (allowedCommands.includes("reopen")) ticketBasic.add(new api.ODHelpMenuCommandComponent("openticket:reopen",8,{ + textName:prefix+"reopen", + textDescription:lang.getTranslation("helpMenu.reopen"), + slashName:"/reopen", + slashDescription:lang.getTranslation("helpMenu.reopen"), + textOptions:[{name:"reason",optional:true}], + slashOptions:[{name:"reason",optional:true}] + })) + } + + const ticketAdvanced = helpmenu.get("openticket:ticket-advanced") + if (ticketAdvanced){ + if (allowedCommands.includes("pin")) ticketAdvanced.add(new api.ODHelpMenuCommandComponent("openticket:pin",5,{ + textName:prefix+"pin", + textDescription:lang.getTranslation("helpMenu.pin"), + slashName:"/pin", + slashDescription:lang.getTranslation("helpMenu.pin"), + textOptions:[{name:"reason",optional:true}], + slashOptions:[{name:"reason",optional:true}] + })) + if (allowedCommands.includes("unpin")) ticketAdvanced.add(new api.ODHelpMenuCommandComponent("openticket:unpin",4,{ + textName:prefix+"unpin", + textDescription:lang.getTranslation("helpMenu.unpin"), + slashName:"/unpin", + slashDescription:lang.getTranslation("helpMenu.unpin"), + textOptions:[{name:"reason",optional:true}], + slashOptions:[{name:"reason",optional:true}] + })) + if (allowedCommands.includes("move")) ticketAdvanced.add(new api.ODHelpMenuCommandComponent("openticket:move",3,{ + textName:prefix+"move", + textDescription:lang.getTranslation("helpMenu.move"), + slashName:"/move", + slashDescription:lang.getTranslation("helpMenu.move"), + textOptions:[{name:"id",optional:false},{name:"reason",optional:true}], + slashOptions:[{name:"id",optional:false},{name:"reason",optional:true}] + })) + if (allowedCommands.includes("rename")) ticketAdvanced.add(new api.ODHelpMenuCommandComponent("openticket:rename",2,{ + textName:prefix+"rename", + textDescription:lang.getTranslation("helpMenu.rename"), + slashName:"/rename", + slashDescription:lang.getTranslation("helpMenu.rename"), + textOptions:[{name:"name",optional:false},{name:"reason",optional:true}], + slashOptions:[{name:"name",optional:false},{name:"reason",optional:true}] + })) + } + + const ticketUser = helpmenu.get("openticket:ticket-channel") + if (ticketUser){ + if (allowedCommands.includes("claim")) ticketUser.add(new api.ODHelpMenuCommandComponent("openticket:claim",7,{ + textName:prefix+"claim", + textDescription:lang.getTranslation("helpMenu.claim"), + slashName:"/claim", + slashDescription:lang.getTranslation("helpMenu.claim"), + textOptions:[{name:"user",optional:true},{name:"reason",optional:true}], + slashOptions:[{name:"user",optional:true},{name:"reason",optional:true}] + })) + if (allowedCommands.includes("unclaim")) ticketUser.add(new api.ODHelpMenuCommandComponent("openticket:unclaim",6,{ + textName:prefix+"unclaim", + textDescription:lang.getTranslation("helpMenu.unclaim"), + slashName:"/unclaim", + slashDescription:lang.getTranslation("helpMenu.unclaim"), + textOptions:[{name:"reason",optional:true}], + slashOptions:[{name:"reason",optional:true}] + })) + if (allowedCommands.includes("add")) ticketUser.add(new api.ODHelpMenuCommandComponent("openticket:add",1,{ + textName:prefix+"add", + textDescription:lang.getTranslation("helpMenu.add"), + slashName:"/add", + slashDescription:lang.getTranslation("helpMenu.add"), + textOptions:[{name:"user",optional:false},{name:"reason",optional:true}], + slashOptions:[{name:"user",optional:false},{name:"reason",optional:true}] + })) + if (allowedCommands.includes("remove")) ticketUser.add(new api.ODHelpMenuCommandComponent("openticket:remove",0,{ + textName:prefix+"remove", + textDescription:lang.getTranslation("helpMenu.remove"), + slashName:"/remove", + slashDescription:lang.getTranslation("helpMenu.remove"), + textOptions:[{name:"user",optional:false},{name:"reason",optional:true}], + slashOptions:[{name:"user",optional:false},{name:"reason",optional:true}] + })) + } + + const admin = helpmenu.get("openticket:admin") + if (admin){ + if (allowedCommands.includes("panel")) admin.add(new api.ODHelpMenuCommandComponent("openticket:panel",4,{ + textName:prefix+"panel", + textDescription:lang.getTranslation("helpMenu.panel"), + slashName:"/panel", + slashDescription:lang.getTranslation("helpMenu.panel"), + textOptions:[{name:"id",optional:false}], + slashOptions:[{name:"id",optional:false}] + })) + if (allowedCommands.includes("blacklist")) admin.add(new api.ODHelpMenuCommandComponent("openticket:blacklist-view",3,{ + textName:prefix+"blacklist view", + textDescription:lang.getTranslation("commands.blacklistView"), + slashName:"/blacklist view", + slashDescription:lang.getTranslation("commands.blacklistView") + })) + if (allowedCommands.includes("blacklist")) admin.add(new api.ODHelpMenuCommandComponent("openticket:blacklist-add",2,{ + textName:prefix+"blacklist add", + textDescription:lang.getTranslation("commands.blacklistAdd"), + slashName:"/blacklist add", + slashDescription:lang.getTranslation("commands.blacklistAdd"), + textOptions:[{name:"user",optional:false},{name:"reason",optional:true}], + slashOptions:[{name:"user",optional:false},{name:"reason",optional:true}] + })) + if (allowedCommands.includes("blacklist")) admin.add(new api.ODHelpMenuCommandComponent("openticket:blacklist-remove",1,{ + textName:prefix+"blacklist remove", + textDescription:lang.getTranslation("commands.blacklistRemove"), + slashName:"/blacklist remove", + slashDescription:lang.getTranslation("commands.blacklistRemove"), + textOptions:[{name:"user",optional:false},{name:"reason",optional:true}], + slashOptions:[{name:"user",optional:false},{name:"reason",optional:true}] + })) + if (allowedCommands.includes("blacklist")) admin.add(new api.ODHelpMenuCommandComponent("openticket:blacklist-get",0,{ + textName:prefix+"blacklist get", + textDescription:lang.getTranslation("commands.blacklistGet"), + slashName:"/blacklist get", + slashDescription:lang.getTranslation("commands.blacklistGet"), + textOptions:[{name:"user",optional:false}], + slashOptions:[{name:"user",optional:false}] + })) + } + + const advanced = helpmenu.get("openticket:advanced") + if (advanced){ + if (allowedCommands.includes("stats")) advanced.add(new api.ODHelpMenuCommandComponent("openticket:stats-global",5,{ + textName:prefix+"stats global", + textDescription:lang.getTranslation("commands.statsGlobal"), + slashName:"/stats global", + slashDescription:lang.getTranslation("commands.statsGlobal") + })) + if (allowedCommands.includes("stats")) advanced.add(new api.ODHelpMenuCommandComponent("openticket:stats-ticket",4,{ + textName:prefix+"stats ticket", + textDescription:lang.getTranslation("commands.statsTicket"), + slashName:"/stats ticket", + slashDescription:lang.getTranslation("commands.statsTicket"), + textOptions:[{name:"ticket",optional:false}], + slashOptions:[{name:"ticket",optional:false}] + })) + if (allowedCommands.includes("stats")) advanced.add(new api.ODHelpMenuCommandComponent("openticket:stats-user",2,{ + textName:prefix+"stats user", + textDescription:lang.getTranslation("commands.statsUser"), + slashName:"/stats user", + slashDescription:lang.getTranslation("commands.statsUser"), + textOptions:[{name:"user",optional:false}], + slashOptions:[{name:"user",optional:false}] + })) + if (allowedCommands.includes("stats")) advanced.add(new api.ODHelpMenuCommandComponent("openticket:stats-reset",2,{ + textName:prefix+"stats reset", + textDescription:lang.getTranslation("commands.statsReset"), + slashName:"/stats reset", + slashDescription:lang.getTranslation("commands.statsReset"), + textOptions:[{name:"reason",optional:true}], + slashOptions:[{name:"reason",optional:true}] + })) + if (allowedCommands.includes("autoclose")) advanced.add(new api.ODHelpMenuCommandComponent("openticket:autoclose-disable",1,{ + textName:prefix+"autoclose disable", + textDescription:lang.getTranslation("commands.autocloseDisable"), + slashName:"/autoclose disable", + slashDescription:lang.getTranslation("commands.autocloseDisable"), + textOptions:[{name:"reason",optional:true}], + slashOptions:[{name:"reason",optional:true}] + })) + if (allowedCommands.includes("autoclose")) advanced.add(new api.ODHelpMenuCommandComponent("openticket:autoclose-enable",0,{ + textName:prefix+"autoclose enable", + textDescription:lang.getTranslation("commands.autocloseEnable"), + slashName:"/autoclose enable", + slashDescription:lang.getTranslation("commands.autocloseEnable"), + textOptions:[{name:"time",optional:false},{name:"reason",optional:true}], + slashOptions:[{name:"time",optional:false},{name:"reason",optional:true}] + })) + if (allowedCommands.includes("autodelete")) advanced.add(new api.ODHelpMenuCommandComponent("openticket:autodelete-disable",1,{ + textName:prefix+"autodelete disable", + textDescription:lang.getTranslation("commands.autodeleteDisable"), + slashName:"/autodelete disable", + slashDescription:lang.getTranslation("commands.autodeleteDisable"), + textOptions:[{name:"reason",optional:true}], + slashOptions:[{name:"reason",optional:true}] + })) + if (allowedCommands.includes("autodelete")) advanced.add(new api.ODHelpMenuCommandComponent("openticket:autodelete-enable",0,{ + textName:prefix+"autodelete enable", + textDescription:lang.getTranslation("commands.autodeleteEnable"), + slashName:"/autodelete enable", + slashDescription:lang.getTranslation("commands.autodeleteEnable"), + textOptions:[{name:"time",optional:false},{name:"reason",optional:true}], + slashOptions:[{name:"time",optional:false},{name:"reason",optional:true}] + })) + } +} \ No newline at end of file diff --git a/src/data/framework/languageLoader.ts b/src/data/framework/languageLoader.ts new file mode 100644 index 0000000..11261b1 --- /dev/null +++ b/src/data/framework/languageLoader.ts @@ -0,0 +1,15 @@ +import {openticket, api, utilities} from "../../index" + +export const loadAllLanguages = async () => { + openticket.languages.add(new api.ODLanguage("openticket:custom","custom.json")) + openticket.languages.add(new api.ODLanguage("openticket:english","english.json")) + openticket.languages.add(new api.ODLanguage("openticket:dutch","dutch.json")) + openticket.languages.add(new api.ODLanguage("openticket:portuguese","portuguese.json")) + openticket.languages.add(new api.ODLanguage("openticket:czech","czech.json")) + + /** How to add more languages? + * - Add the language to the list above + * - Add the language to the "languageList" in the "ODDefaultsManager" class + * - Add the language to the list in the "ODLanguageManagerIds_Default" interface + */ +} \ No newline at end of file diff --git a/src/data/framework/liveStatusLoader.ts b/src/data/framework/liveStatusLoader.ts new file mode 100644 index 0000000..5864178 --- /dev/null +++ b/src/data/framework/liveStatusLoader.ts @@ -0,0 +1,6 @@ +import {openticket, api, utilities} from "../../index" + +export const loadAllLiveStatusSources = async () => { + //DEFAULT DJDJ DEV + openticket.livestatus.add(new api.ODLiveStatusUrlSource("openticket:default-djdj-dev","https://apis.dj-dj.be/status/openticket.json")) +} \ No newline at end of file diff --git a/src/data/framework/permissionLoader.ts b/src/data/framework/permissionLoader.ts new file mode 100644 index 0000000..4b080f8 --- /dev/null +++ b/src/data/framework/permissionLoader.ts @@ -0,0 +1,82 @@ +import {openticket, api, utilities} from "../../index" +import * as discord from "discord.js" + +export const loadAllPermissions = async () => { + const generalConfig = openticket.configs.get("openticket:general") + if (!generalConfig) return + const mainServer = openticket.client.mainServer + if (!mainServer) return + + //DEVELOPER & OWNER + const developer = (await openticket.client.client.application.fetch()).owner + if (developer instanceof discord.User){ + openticket.permissions.add(new api.ODPermission("openticket:developer-"+developer.id,"global-user","developer",developer)) + }else if (developer instanceof discord.Team){ + developer.members.forEach((member) => { + openticket.permissions.add(new api.ODPermission("openticket:developer-"+member.user.id,"global-user","developer",member.user)) + }) + } + const owner = (await mainServer.members.fetch(mainServer.ownerId)).user + openticket.permissions.add(new api.ODPermission("openticket:owner-"+owner.id,"global-user","owner",owner)) + + //GLOBAL ADMINS + generalConfig.data.globalAdmins.forEach(async (admin) => { + const role = await mainServer.roles.fetch(admin) + if (!role) return openticket.log("Unable to register permission for global admin!","error",[ + {key:"roleid",value:admin} + ]) + + openticket.permissions.add(new api.ODPermission("openticket:global-admin-"+admin,"global-role","admin",role)) + }) + + //TICKET ADMINS + openticket.tickets.getAll().forEach(async (ticket) => { + try { + const channel = await openticket.client.fetchGuildTextChannel(mainServer,ticket.id.value) + if (!channel) return + + const admins = ticket.option.exists("openticket:admins") ? ticket.option.get("openticket:admins").value : [] + const readAdmins = ticket.option.exists("openticket:admins-readonly") ? ticket.option.get("openticket:admins-readonly").value : [] + + admins.concat(readAdmins).forEach(async (admin) => { + const role = await mainServer.roles.fetch(admin) + if (!role) return openticket.log("Unable to register permission for ticket admin!","error",[ + {key:"roleid",value:admin} + ]) + + openticket.permissions.add(new api.ODPermission("openticket:ticket-admin_"+ticket.id.value+"_"+admin,"channel-role","support",role,channel)) + }) + }catch(err){ + process.emit("uncaughtException",err) + openticket.log("Ticket Admin Loading Permissions Error (see above)","error") + } + }) +} + +export const addTicketPermissions = async (ticket:api.ODTicket) => { + const mainServer = openticket.client.mainServer + if (!mainServer) return + const channel = await openticket.client.fetchGuildTextChannel(mainServer,ticket.id.value) + if (!channel) return + + const admins = ticket.option.exists("openticket:admins") ? ticket.option.get("openticket:admins").value : [] + const readAdmins = ticket.option.exists("openticket:admins-readonly") ? ticket.option.get("openticket:admins-readonly").value : [] + + admins.concat(readAdmins).forEach(async (admin) => { + const role = await mainServer.roles.fetch(admin) + if (!role) return openticket.log("Unable to register permission for ticket admin!","error",[ + {key:"roleid",value:admin} + ]) + + openticket.permissions.add(new api.ODPermission("openticket:ticket-admin_"+ticket.id.value+"_"+admin,"channel-role","support",role,channel)) + }) +} + +export const removeTicketPermissions = async (ticket:api.ODTicket) => { + const admins = ticket.option.exists("openticket:admins") ? ticket.option.get("openticket:admins").value : [] + const readAdmins = ticket.option.exists("openticket:admins-readonly") ? ticket.option.get("openticket:admins-readonly").value : [] + + admins.concat(readAdmins).forEach(async (admin) => { + openticket.permissions.remove("openticket:ticket-admin_"+ticket.id.value+"_"+admin) + }) +} \ No newline at end of file diff --git a/src/data/framework/postLoader.ts b/src/data/framework/postLoader.ts new file mode 100644 index 0000000..7ddcac0 --- /dev/null +++ b/src/data/framework/postLoader.ts @@ -0,0 +1,14 @@ +import {openticket, api, utilities} from "../../index" + +export const loadAllPosts = async () => { + const generalConfig = openticket.configs.get("openticket:general") + if (!generalConfig) return + const transcriptConfig = openticket.configs.get("openticket:transcripts") + if (!transcriptConfig) return + + //LOGS CHANNEL + if (generalConfig.data.system.logs.enabled) openticket.posts.add(new api.ODPost("openticket:logs",generalConfig.data.system.logs.channel)) + + //TRANSCRIPTS CHANNEL + if (transcriptConfig.data.general.enabled && transcriptConfig.data.general.enableChannel) openticket.posts.add(new api.ODPost("openticket:transcripts",transcriptConfig.data.general.channel)) +} \ No newline at end of file diff --git a/src/data/framework/startScreenLoader.ts b/src/data/framework/startScreenLoader.ts new file mode 100644 index 0000000..ecc407d --- /dev/null +++ b/src/data/framework/startScreenLoader.ts @@ -0,0 +1,54 @@ +import {openticket, api, utilities} from "../../index" +import ansis from "ansis" + +export const loadAllStartScreenComponents = async () => { + /** + "openticket:flags":ODStartScreenFlagsCategoryComponent, + "openticket:plugins":ODStartScreenPluginsCategoryComponent, + + "openticket:livestatus":ODStartScreenLiveStatusCategoryComponent, + */ + + //LOGO + openticket.startscreen.add(new api.ODStartScreenLogoComponent("openticket:logo",1000,[ + " ██████╗ ██████╗ ███████╗███╗ ██╗ ████████╗██╗ ██████╗██╗ ██╗███████╗████████╗ ", + " ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ╚══██╔══╝██║██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝ ", + " ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║██║ █████╔╝ █████╗ ██║ ", + " ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ", + " ╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ██║╚██████╗██║ ██╗███████╗ ██║ ", + " ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ " + ],true,false)) + + //HEADER + const currentLanguageMetadata = openticket.languages.getLanguageMetadata() + openticket.startscreen.add(new api.ODStartScreenHeaderComponent("openticket:header",999,[ + {key:"Version",value:openticket.versions.get("openticket:version").toString()}, + {key:"Support",value:"https://discord.dj-dj.be"}, + {key:"Language",value:(currentLanguageMetadata ? currentLanguageMetadata.language : "Unknown")} + ]," - ",{ + align:"center", + width:openticket.startscreen.get("openticket:logo") + })) + + //FLAGS + openticket.startscreen.add(new api.ODStartScreenFlagsCategoryComponent("openticket:flags",4,openticket.flags.getAll())) + + //PLUGINS + openticket.startscreen.add(new api.ODStartScreenPluginsCategoryComponent("openticket:plugins",3,openticket.plugins.getAll(),openticket.plugins.unknownCrashedPlugins)) + + //STATS + openticket.startscreen.add(new api.ODStartScreenPropertiesCategoryComponent("openticket:stats",2,"startup info",[ + {key:"status",value:ansis.bold(openticket.client.activity.getStatusType())+openticket.client.activity.text+" ("+openticket.client.activity.status+")"}, + {key:"options",value:"loaded "+ansis.bold(openticket.options.getLength().toString())+" options!"}, + {key:"panels",value:"loaded "+ansis.bold(openticket.panels.getLength().toString())+" panels!"}, + {key:"tickets",value:"loaded "+ansis.bold(openticket.tickets.getLength().toString())+" tickets!"}, + {key:"roles",value:"loaded "+ansis.bold(openticket.roles.getLength().toString())+" roles!"}, + {key:"help",value:ansis.bold(openticket.configs.get("openticket:general").data.prefix+"help")+" or "+ansis.bold("/help")} + ])) + + //LIVESTATUS + openticket.startscreen.add(new api.ODStartScreenLiveStatusCategoryComponent("openticket:livestatus",1,openticket.livestatus)) + + //LOGS + openticket.startscreen.add(new api.ODStartScreenLogCategoryComponent("openticket:logs",0)) +} \ No newline at end of file diff --git a/src/data/framework/statLoader.ts b/src/data/framework/statLoader.ts new file mode 100644 index 0000000..d3508e9 --- /dev/null +++ b/src/data/framework/statLoader.ts @@ -0,0 +1,133 @@ +import {openticket, api, utilities} from "../../index" +import * as discord from "discord.js" + +const stats = openticket.stats +const lang = openticket.languages + +export const loadAllStatScopes = async () => { + stats.add(new api.ODStatGlobalScope("openticket:global",utilities.emojiTitle("📊",lang.getTranslation("stats.scopes.global")))) + stats.add(new api.ODStatGlobalScope("openticket:system",utilities.emojiTitle("⚙️",lang.getTranslation("stats.scopes.system")))) + stats.add(new api.ODStatScope("openticket:user",utilities.emojiTitle("📊",lang.getTranslation("stats.scopes.user")))) + stats.add(new api.ODStatScope("openticket:ticket",utilities.emojiTitle("📊",lang.getTranslation("stats.scopes.ticket")))) + stats.add(new api.ODStatScope("openticket:participants",utilities.emojiTitle("👥",lang.getTranslation("stats.scopes.participants")))) +} + +export const loadAllStats = async () => { + const generalConfig = openticket.configs.get("openticket:general") + if (!generalConfig) return + + const global = stats.get("openticket:global") + if (global){ + global.add(new api.ODBasicStat("openticket:tickets-created",10,lang.getTranslation("stats.properties.ticketsCreated"),0)) + global.add(new api.ODBasicStat("openticket:tickets-closed",9,lang.getTranslation("stats.properties.ticketsClosed"),0)) + global.add(new api.ODBasicStat("openticket:tickets-deleted",8,lang.getTranslation("stats.properties.ticketsDeleted"),0)) + global.add(new api.ODBasicStat("openticket:tickets-reopened",7,lang.getTranslation("stats.properties.ticketsReopened"),0)) + global.add(new api.ODBasicStat("openticket:tickets-autoclosed",6,lang.getTranslation("stats.properties.ticketsAutoclosed"),0)) + global.add(new api.ODBasicStat("openticket:tickets-autodeleted",5,"Tickets Autodeleted",0)) //TODO TRANSLATION!!! + global.add(new api.ODBasicStat("openticket:tickets-claimed",4,lang.getTranslation("stats.properties.ticketsClaimed"),0)) + global.add(new api.ODBasicStat("openticket:tickets-pinned",3,lang.getTranslation("stats.properties.ticketsPinned"),0)) + global.add(new api.ODBasicStat("openticket:tickets-moved",2,lang.getTranslation("stats.properties.ticketsMoved"),0)) + global.add(new api.ODBasicStat("openticket:users-blacklisted",1,lang.getTranslation("stats.properties.usersBlacklisted"),0)) + global.add(new api.ODBasicStat("openticket:transcripts-created",0,lang.getTranslation("stats.properties.transcriptsCreated"),0)) + } + + const system = stats.get("openticket:system") + if (system){ + system.add(new api.ODDynamicStat("openticket:startup-date",1,() => { + return lang.getTranslation("params.uppercase.startupDate")+": "+discord.time(new Date(),"f") + })) + system.add(new api.ODDynamicStat("openticket:version",0,() => { + return lang.getTranslation("params.uppercase.version")+": `"+openticket.versions.get("openticket:version").toString()+"`" + })) + } + + const user = stats.get("openticket:user") + if (user){ + user.add(new api.ODDynamicStat("openticket:name",11,async (scopeId,guild,channel,user) => { + return lang.getTranslation("params.uppercase.name")+": "+discord.userMention(scopeId) + })) + user.add(new api.ODDynamicStat("openticket:role",10,async (scopeId,guild,channel,user) => { + try{ + const scopeMember = await guild.members.fetch(scopeId) + if (!scopeMember) return "" + + const permissions = await openticket.permissions.getPermissions(scopeMember.user,channel,guild) + if (permissions.type == "developer") return lang.getTranslation("params.uppercase.role")+": 💻 `Developer`" //TODO TRANSLATION!!! + if (permissions.type == "owner") return lang.getTranslation("params.uppercase.role")+": 👑 `Server Owner`" //TODO TRANSLATION!!! + if (permissions.type == "admin") return lang.getTranslation("params.uppercase.role")+": 💼 `Server Admin`" //TODO TRANSLATION!!! + if (permissions.type == "moderator") return lang.getTranslation("params.uppercase.role")+": 🚔 `Moderator Team`" //TODO TRANSLATION!!! + if (permissions.type == "support") return lang.getTranslation("params.uppercase.role")+": 💬 `Support Team`" //TODO TRANSLATION!!! + else return lang.getTranslation("params.uppercase.role")+": 👤 `Member`" //TODO TRANSLATION!!! + }catch{ + return "" + } + })) + user.add(new api.ODBasicStat("openticket:tickets-created",8,lang.getTranslation("stats.properties.ticketsCreated"),0)) + user.add(new api.ODBasicStat("openticket:tickets-closed",7,lang.getTranslation("stats.properties.ticketsClosed"),0)) + user.add(new api.ODBasicStat("openticket:tickets-deleted",6,lang.getTranslation("stats.properties.ticketsDeleted"),0)) + user.add(new api.ODBasicStat("openticket:tickets-reopened",5,lang.getTranslation("stats.properties.ticketsReopened"),0)) + user.add(new api.ODBasicStat("openticket:tickets-claimed",4,lang.getTranslation("stats.properties.ticketsClaimed"),0)) + user.add(new api.ODBasicStat("openticket:tickets-pinned",3,lang.getTranslation("stats.properties.ticketsPinned"),0)) + user.add(new api.ODBasicStat("openticket:tickets-moved",2,lang.getTranslation("stats.properties.ticketsMoved"),0)) + user.add(new api.ODBasicStat("openticket:users-blacklisted",1,lang.getTranslation("stats.properties.usersBlacklisted"),0)) + user.add(new api.ODBasicStat("openticket:transcripts-created",0,lang.getTranslation("stats.properties.transcriptsCreated"),0)) + } + + const ticket = stats.get("openticket:ticket") + if (ticket){ + ticket.add(new api.ODDynamicStat("openticket:name",5,async (scopeId,guild,channel,user) => { + return lang.getTranslation("params.uppercase.ticket")+": "+discord.channelMention(scopeId) + })) + ticket.add(new api.ODDynamicStat("openticket:status",4,async (scopeId,guild,channel,user) => { + const ticket = openticket.tickets.get(scopeId) + if (!ticket) return "" + + const closed = ticket.exists("openticket:closed") ? ticket.get("openticket:closed").value : false + + return closed ? lang.getTranslation("params.uppercase.status")+": 🔒 `Closed`" : lang.getTranslation("params.uppercase.status")+": 🔓 `Open`" //TODO TRANSLATION!!! + })) + ticket.add(new api.ODDynamicStat("openticket:claimed",3,async (scopeId,guild,channel,user) => { + const ticket = openticket.tickets.get(scopeId) + if (!ticket) return "" + + const claimed = ticket.exists("openticket:claimed") ? ticket.get("openticket:claimed").value : false + return claimed ? lang.getTranslation("params.uppercase.claimed")+": 🟢 `Yes`" : lang.getTranslation("params.uppercase.claimed")+": 🔴 `No`" + })) + ticket.add(new api.ODDynamicStat("openticket:pinned",2,async (scopeId,guild,channel,user) => { + const ticket = openticket.tickets.get(scopeId) + if (!ticket) return "" + + const pinned = ticket.exists("openticket:pinned") ? ticket.get("openticket:pinned").value : false + return pinned ? lang.getTranslation("params.uppercase.pinned")+": 🟢 `Yes`" : lang.getTranslation("params.uppercase.pinned")+": 🔴 `No`" + })) + ticket.add(new api.ODDynamicStat("openticket:creation-date",1,async (scopeId,guild,channel,user) => { + const ticket = openticket.tickets.get(scopeId) + if (!ticket) return "" + + const rawDate = ticket.get("openticket:opened-on").value ?? new Date().getTime() + return lang.getTranslation("params.uppercase.creationDate")+": "+discord.time(new Date(rawDate),"f") + })) + ticket.add(new api.ODDynamicStat("openticket:creator",0,async (scopeId,guild,channel,user) => { + const ticket = openticket.tickets.get(scopeId) + if (!ticket) return "" + + const creator = ticket.get("openticket:opened-by").value + return lang.getTranslation("params.uppercase.creator")+": "+ (creator ? discord.userMention(creator) : "`unknown`") + })) + } + + const participants = stats.get("openticket:participants") + if (participants){ + participants.add(new api.ODDynamicStat("openticket:participants",0,async (scopeId,guild,channel,user) => { + const ticket = openticket.tickets.get(scopeId) + if (!ticket) return "" + + const participants = ticket.exists("openticket:participants") ? ticket.get("openticket:participants").value : [] + + return participants.map((p) => { + return (p.type == "role") ? discord.roleMention(p.id) : discord.userMention(p.id) + }).join("\n") + })) + + } +} \ No newline at end of file diff --git a/src/data/openticket/blacklistLoader.ts b/src/data/openticket/blacklistLoader.ts new file mode 100644 index 0000000..63b1947 --- /dev/null +++ b/src/data/openticket/blacklistLoader.ts @@ -0,0 +1,12 @@ +import {openticket, api, utilities} from "../../index" + +export const loadAllBlacklistedUsers = async () => { + const userDatabase = openticket.databases.get("openticket:users") + if (!userDatabase) return + + const users = userDatabase.getCategory("openticket:blacklist") ?? [] + users.forEach((user) => { + if (typeof user.value != "string") return + openticket.blacklist.add(new api.ODBlacklist(user.key,user.value)) + }) +} \ No newline at end of file diff --git a/src/data/openticket/optionLoader.ts b/src/data/openticket/optionLoader.ts new file mode 100644 index 0000000..9e54417 --- /dev/null +++ b/src/data/openticket/optionLoader.ts @@ -0,0 +1,107 @@ +import {openticket, api, utilities} from "../../index" + +export const loadAllOptions = async () => { + const optionConfig = openticket.configs.get("openticket:options") + if (!optionConfig) return + + optionConfig.data.forEach((option) => { + if (option.type == "ticket"){ + const loadedOption = loadTicketOption(option) + openticket.options.add(loadedOption) + openticket.options.suffix.add(loadTicketOptionSuffix(loadedOption)) + }else if (option.type == "website"){ + openticket.options.add(loadWebsiteOption(option)) + }else if (option.type == "role"){ + openticket.options.add(loadRoleOption(option)) + } + }) +} + +export const loadTicketOption = (option:api.ODJsonConfig_DefaultOptionTicketType): api.ODTicketOption => { + return new api.ODTicketOption(option.id,[ + new api.ODOptionData("openticket:name",option.name), + new api.ODOptionData("openticket:description",option.description), + + new api.ODOptionData("openticket:button-emoji",option.button.emoji), + new api.ODOptionData("openticket:button-label",option.button.label), + new api.ODOptionData("openticket:button-color",option.button.color), + + new api.ODOptionData("openticket:admins",option.ticketAdmins), + new api.ODOptionData("openticket:admins-readonly",option.readonlyAdmins), + new api.ODOptionData("openticket:allow-blacklisted-users",option.allowCreationByBlacklistedUsers), + new api.ODOptionData("openticket:questions",option.questions), + + new api.ODOptionData("openticket:channel-prefix",option.channel.prefix), + new api.ODOptionData("openticket:channel-suffix",option.channel.suffix), + new api.ODOptionData("openticket:channel-category",option.channel.category), + new api.ODOptionData("openticket:channel-category-closed",option.channel.closedCategory), + new api.ODOptionData("openticket:channel-category-backup",option.channel.backupCategory), + new api.ODOptionData("openticket:channel-categories-claimed",option.channel.claimedCategory), + new api.ODOptionData("openticket:channel-description",option.channel.description), + + new api.ODOptionData("openticket:dm-message-enabled",option.dmMessage.enabled), + new api.ODOptionData("openticket:dm-message-text",option.dmMessage.text), + new api.ODOptionData("openticket:dm-message-embed",option.dmMessage.embed), + + new api.ODOptionData("openticket:ticket-message-enabled",option.ticketMessage.enabled), + new api.ODOptionData("openticket:ticket-message-text",option.ticketMessage.text), + new api.ODOptionData("openticket:ticket-message-embed",option.ticketMessage.embed), + new api.ODOptionData("openticket:ticket-message-ping",option.ticketMessage.ping), + + new api.ODOptionData("openticket:autoclose-enable-hours",option.autoclose.enableInactiveHours), + new api.ODOptionData("openticket:autoclose-enable-leave",option.autoclose.enableUserLeave), + new api.ODOptionData("openticket:autoclose-disable-claim",option.autoclose.disableOnClaim), + new api.ODOptionData("openticket:autoclose-hours",option.autoclose.inactiveHours), + + new api.ODOptionData("openticket:autodelete-enable-days",option.autodelete.enableInactiveDays), + new api.ODOptionData("openticket:autodelete-enable-leave",option.autodelete.enableUserLeave), + new api.ODOptionData("openticket:autodelete-disable-claim",option.autodelete.disableOnClaim), + new api.ODOptionData("openticket:autodelete-days",option.autodelete.inactiveDays), + + new api.ODOptionData("openticket:cooldown-enabled",option.cooldown.enabled), + new api.ODOptionData("openticket:cooldown-minutes",option.cooldown.cooldownMinutes), + + new api.ODOptionData("openticket:limits-enabled",option.limits.enabled), + new api.ODOptionData("openticket:limits-maximum-global",option.limits.globalMaximum), + new api.ODOptionData("openticket:limits-maximum-user",option.limits.userMaximum) + ]) +} + +export const loadWebsiteOption = (opt:api.ODJsonConfig_DefaultOptionWebsiteType): api.ODWebsiteOption => { + return new api.ODWebsiteOption(opt.id,[ + new api.ODOptionData("openticket:name",opt.name), + new api.ODOptionData("openticket:description",opt.description), + + new api.ODOptionData("openticket:button-emoji",opt.button.emoji), + new api.ODOptionData("openticket:button-label",opt.button.label), + + new api.ODOptionData("openticket:url",opt.url) + ]) +} + +export const loadRoleOption = (opt:api.ODJsonConfig_DefaultOptionRoleType): api.ODRoleOption => { + return new api.ODRoleOption(opt.id,[ + new api.ODOptionData("openticket:name",opt.name), + new api.ODOptionData("openticket:description",opt.description), + + new api.ODOptionData("openticket:button-emoji",opt.button.emoji), + new api.ODOptionData("openticket:button-label",opt.button.label), + new api.ODOptionData("openticket:button-color",opt.button.color), + + new api.ODOptionData("openticket:roles",opt.roles), + new api.ODOptionData("openticket:mode",opt.mode), + new api.ODOptionData("openticket:remove-roles-on-add",opt.removeRolesOnAdd), + new api.ODOptionData("openticket:add-on-join",opt.addOnMemberJoin) + ]) +} + +export const loadTicketOptionSuffix = (option:api.ODTicketOption): api.ODOptionSuffix => { + const mode = option.get("openticket:channel-suffix").value + const globalDatabase = openticket.databases.get("openticket:global") + if (mode == "user-name") return new api.ODOptionUserNameSuffix(option.id.value,option) + else if (mode == "random-number") return new api.ODOptionRandomNumberSuffix(option.id.value,option,globalDatabase) + else if (mode == "random-hex") return new api.ODOptionRandomHexSuffix(option.id.value,option,globalDatabase) + else if (mode == "counter-fixed") return new api.ODOptionCounterFixedSuffix(option.id.value,option,globalDatabase) + else if (mode == "counter-dynamic") return new api.ODOptionCounterDynamicSuffix(option.id.value,option,globalDatabase) + else return new api.ODOptionUserIdSuffix(option.id.value,option) +} \ No newline at end of file diff --git a/src/data/openticket/panelLoader.ts b/src/data/openticket/panelLoader.ts new file mode 100644 index 0000000..5737c53 --- /dev/null +++ b/src/data/openticket/panelLoader.ts @@ -0,0 +1,165 @@ +import {openticket, api, utilities} from "../../index" +import * as discord from "discord.js" + +export const loadAllPanels = async () => { + const panelConfig = openticket.configs.get("openticket:panels") + if (!panelConfig) return + + panelConfig.data.forEach((panel) => { + openticket.panels.add(loadPanel(panel)) + }) +} + +export const loadPanel = (panel:api.ODJsonConfig_DefaultPanelType) => { + return new api.ODPanel(panel.id,[ + new api.ODPanelData("openticket:name",panel.name), + new api.ODPanelData("openticket:options",panel.options), + new api.ODPanelData("openticket:dropdown",panel.dropdown), + + new api.ODPanelData("openticket:text",panel.text), + new api.ODPanelData("openticket:embed",panel.embed), + + new api.ODPanelData("openticket:dropdown-placeholder",panel.settings.dropdownPlaceholder), + new api.ODPanelData("openticket:enable-max-tickets-warning-text",panel.settings.enableMaxTicketsWarningInText), + new api.ODPanelData("openticket:enable-max-tickets-warning-embed",panel.settings.enableMaxTicketsWarningInEmbed), + new api.ODPanelData("openticket:describe-options-layout",panel.settings.describeOptionsLayout), + new api.ODPanelData("openticket:describe-options-custom-title",panel.settings.describeOptionsCustomTitle), + new api.ODPanelData("openticket:describe-options-in-text",panel.settings.describeOptionsInText), + new api.ODPanelData("openticket:describe-options-in-embed-fields",panel.settings.describeOptionsInEmbedFields), + new api.ODPanelData("openticket:describe-options-in-embed-description",panel.settings.describeOptionsInEmbedDescription), + ]) +} +export function describePanelOptions(mode:"fields",panel:api.ODPanel): {name:string,value:string}[] +export function describePanelOptions(mode:"text",panel:api.ODPanel): string +export function describePanelOptions(mode:"fields"|"text", panel:api.ODPanel): {name:string,value:string}[]|string { + const layout = panel.get("openticket:describe-options-layout").value + const dropdownMode = panel.get("openticket:dropdown").value + const options: api.ODOption[] = [] + let hasTicket = false + let hasWebsite = false + let hasRole = false + let ticketOnly = true + let websiteOnly = true + let roleOnly = true + panel.get("openticket:options").value.forEach((id) => { + const opt = openticket.options.get(id) + if (opt){ + if (opt instanceof api.ODTicketOption){ + options.push(opt) + hasTicket = true + roleOnly = false + websiteOnly = false + }else if (!dropdownMode && opt instanceof api.ODWebsiteOption){ + options.push(opt) + hasWebsite = true + ticketOnly = false + roleOnly = false + }else if (!dropdownMode && opt instanceof api.ODRoleOption){ + options.push(opt) + hasRole = true + ticketOnly = false + websiteOnly = false + } + } + }) + + const autotitle = (hasTicket && ticketOnly) ? "Select your ticket:" : ((hasRole && roleOnly) ? "Select your role:" : "Select your option:") + const title = (panel.get("openticket:describe-options-custom-title").value.length < 1) ? "__"+autotitle+"__\n" : "__"+panel.get("openticket:describe-options-custom-title").value+"__\n" + + if (mode == "fields") return options.map((opt) => { + if (opt instanceof api.ODTicketOption){ + //ticket option + const emoji = opt.exists("openticket:button-emoji") ? opt.get("openticket:button-emoji").value : "" + const name = opt.exists("openticket:name") ? opt.get("openticket:name").value : "``" + let description = opt.exists("openticket:description") ? opt.get("openticket:description").value : "``" + + if (layout == "normal" || layout == "detailed"){ + if (opt.exists("openticket:cooldown-enabled") && opt.get("openticket:cooldown-enabled").value) description = description + "\nCooldown: `"+opt.get("openticket:cooldown-minutes").value+" min`" + if (opt.exists("openticket:limits-enabled") && opt.get("openticket:limits-enabled").value) description = description + "\nMax Tickets: `"+opt.get("openticket:limits-maximum-user").value+"`" + } + if (layout == "detailed"){ + if (opt.exists("openticket:admins")) description = description + "\nAdmins: "+opt.get("openticket:admins").value.map((admin) => discord.roleMention(admin)).join(", ") + } + + return {name:utilities.emojiTitle(emoji,name),value:description} + + }else if (opt instanceof api.ODWebsiteOption){ + //website option + const emoji = opt.exists("openticket:button-emoji") ? opt.get("openticket:button-emoji").value : "" + const name = opt.exists("openticket:name") ? opt.get("openticket:name").value : "``" + let description = opt.exists("openticket:description") ? opt.get("openticket:description").value : "``" + + return {name:utilities.emojiTitle(emoji,name),value:description} + + }else if (opt instanceof api.ODRoleOption){ + //role option + const emoji = opt.exists("openticket:button-emoji") ? opt.get("openticket:button-emoji").value : "" + const name = opt.exists("openticket:name") ? opt.get("openticket:name").value : "``" + let description = opt.exists("openticket:description") ? opt.get("openticket:description").value : "``" + + if (layout == "normal" || layout == "detailed"){ + if (opt.exists("openticket:roles")) description = description + "\nRoles: "+opt.get("openticket:roles").value.map((admin) => discord.roleMention(admin)).join(", ") + } + + return {name:utilities.emojiTitle(emoji,name),value:description} + + }else{ + //auto-generated plugin option + const emoji = opt.get("openticket:button-emoji") as api.ODOptionData|null + const name = opt.get("openticket:name") as api.ODOptionData|null + const description = opt.get("openticket:description") as api.ODOptionData|null + return {name:utilities.emojiTitle((emoji ? emoji.value : ""),(name ? name.value : "`"+opt.id+"`")),value:(description ? description.value : "``")} + } + }) + else if (mode == "text") return title+options.map((opt) => { + if (opt instanceof api.ODTicketOption){ + //ticket option + const emoji = opt.exists("openticket:button-emoji") ? opt.get("openticket:button-emoji").value : "" + const name = opt.exists("openticket:name") ? opt.get("openticket:name").value : "``" + let description = opt.exists("openticket:description") ? opt.get("openticket:description").value : "``" + + if (layout == "normal" || layout == "detailed"){ + if (opt.exists("openticket:cooldown-enabled") && opt.get("openticket:cooldown-enabled").value) description = description + "\nCooldown: `"+opt.get("openticket:cooldown-minutes").value+" min`" + if (opt.exists("openticket:limits-enabled") && opt.get("openticket:limits-enabled").value) description = description + "\nMax Tickets: `"+opt.get("openticket:limits-maximum-user").value+"`" + } + if (layout == "detailed"){ + if (opt.exists("openticket:admins")) description = description + "\nAdmins: "+opt.get("openticket:admins").value.map((admin) => discord.roleMention(admin)).join(", ") + } + + if (layout == "simple") return "**"+utilities.emojiTitle(emoji,name)+":** "+description + else return "**"+utilities.emojiTitle(emoji,name)+"**\n"+description + + }else if (opt instanceof api.ODWebsiteOption){ + //website option + const emoji = opt.exists("openticket:button-emoji") ? opt.get("openticket:button-emoji").value : "" + const name = opt.exists("openticket:name") ? opt.get("openticket:name").value : "``" + let description = opt.exists("openticket:description") ? opt.get("openticket:description").value : "``" + + if (layout == "simple") return "**"+utilities.emojiTitle(emoji,name)+":** "+description + else return "**"+utilities.emojiTitle(emoji,name)+"**\n"+description + + }else if (opt instanceof api.ODRoleOption){ + //role option + const emoji = opt.exists("openticket:button-emoji") ? opt.get("openticket:button-emoji").value : "" + const name = opt.exists("openticket:name") ? opt.get("openticket:name").value : "``" + let description = opt.exists("openticket:description") ? opt.get("openticket:description").value : "``" + + if (layout == "normal" || layout == "detailed"){ + if (opt.exists("openticket:roles")) description = description + "\nRoles: "+opt.get("openticket:roles").value.map((admin) => discord.roleMention(admin)).join(", ") + } + + if (layout == "simple") return "**"+utilities.emojiTitle(emoji,name)+":** "+description + else return "**"+utilities.emojiTitle(emoji,name)+"**\n"+description + + }else{ + //auto-generated plugin option + const emoji = opt.get("openticket:button-emoji") as api.ODOptionData|null + const name = opt.get("openticket:name") as api.ODOptionData|null + const description = opt.get("openticket:description") as api.ODOptionData|null + + if (layout == "simple") return "**"+utilities.emojiTitle((emoji ? emoji.value : ""),(name ? name.value : "`"+opt.id+"`"))+":** "+(description ? description.value : "``") + else return "**"+utilities.emojiTitle((emoji ? emoji.value : ""),(name ? name.value : "`"+opt.id+"`"))+"**\n"+(description ? description.value : "``") + } + }).join("\n\n") + else throw new api.ODSystemError("Unknown panel generation mode, choose 'text' or 'fields'") +} \ No newline at end of file diff --git a/src/data/openticket/questionLoader.ts b/src/data/openticket/questionLoader.ts new file mode 100644 index 0000000..2105d83 --- /dev/null +++ b/src/data/openticket/questionLoader.ts @@ -0,0 +1,38 @@ +import {openticket, api, utilities} from "../../index" + +export const loadAllQuestions = async () => { + const questionConfig = openticket.configs.get("openticket:questions") + if (!questionConfig) return + + questionConfig.data.forEach((question) => { + if (question.type == "short"){ + openticket.questions.add(loadShortQuestion(question)) + }else if (question.type == "paragraph"){ + openticket.questions.add(loadParagraphQuestion(question)) + } + }) +} + +export const loadShortQuestion = (option:api.ODJsonConfig_DefaultShortQuestionType) => { + return new api.ODShortQuestion(option.id,[ + new api.ODQuestionData("openticket:name",option.name), + new api.ODQuestionData("openticket:required",option.required), + new api.ODQuestionData("openticket:placeholder",option.placeholder), + + new api.ODQuestionData("openticket:length-enabled",option.length.enabled), + new api.ODQuestionData("openticket:length-min",option.length.min), + new api.ODQuestionData("openticket:length-max",option.length.max), + ]) +} + +export const loadParagraphQuestion = (option:api.ODJsonConfig_DefaultParagraphQuestionType) => { + return new api.ODParagraphQuestion(option.id,[ + new api.ODQuestionData("openticket:name",option.name), + new api.ODQuestionData("openticket:required",option.required), + new api.ODQuestionData("openticket:placeholder",option.placeholder), + + new api.ODQuestionData("openticket:length-enabled",option.length.enabled), + new api.ODQuestionData("openticket:length-min",option.length.min), + new api.ODQuestionData("openticket:length-max",option.length.max), + ]) +} \ No newline at end of file diff --git a/src/data/openticket/roleLoader.ts b/src/data/openticket/roleLoader.ts new file mode 100644 index 0000000..2baa6d5 --- /dev/null +++ b/src/data/openticket/roleLoader.ts @@ -0,0 +1,18 @@ +import {openticket, api, utilities} from "../../index" + +export const loadAllRoles = async () => { + openticket.options.getAll().forEach((opt) => { + if (opt instanceof api.ODRoleOption){ + openticket.roles.add(loadRole(opt)) + } + }) +} + +export const loadRole = (option:api.ODRoleOption) => { + return new api.ODRole(option.id,[ + new api.ODRoleData("openticket:roles",option.get("openticket:roles").value), + new api.ODRoleData("openticket:mode",option.get("openticket:mode").value), + new api.ODRoleData("openticket:remove-roles-on-add",option.get("openticket:remove-roles-on-add").value), + new api.ODRoleData("openticket:add-on-join",option.get("openticket:add-on-join").value) + ]) +} \ No newline at end of file diff --git a/src/data/openticket/ticketLoader.ts b/src/data/openticket/ticketLoader.ts new file mode 100644 index 0000000..ea89958 --- /dev/null +++ b/src/data/openticket/ticketLoader.ts @@ -0,0 +1,35 @@ +import {openticket, api, utilities} from "../../index" + +const optionDatabase = openticket.databases.get("openticket:options") + +export const loadAllTickets = async () => { + const ticketDatabase = openticket.databases.get("openticket:tickets") + if (!ticketDatabase) return + + const tickets = ticketDatabase.getCategory("openticket:ticket") + if (!tickets) return + tickets.forEach((ticket) => { + try { + openticket.tickets.add(loadTicket(ticket.value)) + }catch (err){ + process.emit("uncaughtException",new api.ODSystemError("Failed to load ticket from database! => id: "+ticket.key+"\n ===> "+err)) + } + }) +} + +export const loadTicket = (ticket:api.ODTicketJson) => { + const backupOption = optionDatabase.exists("openticket:used-option",ticket.option) ? api.ODTicketOption.fromJson(optionDatabase.get("openticket:used-option",ticket.option) as api.ODOptionJson) : null + const configOption = openticket.options.get(ticket.option) + + //check if option is of type "ticket" + if (configOption && !(configOption instanceof api.ODTicketOption)) throw new api.ODSystemError("Unable to load ticket because option is not of 'ticket' type!") + + //manage backup option + if (configOption) optionDatabase.set("openticket:used-option",configOption.id.value,configOption.toJson(openticket.versions.get("openticket:version"))) + else if (backupOption) openticket.options.add(backupOption) + else throw new api.ODSystemError("Unable to use backup option! Normal option not found in config!") + + //load ticket & option + const option = (configOption ?? backupOption) as api.ODTicketOption + return api.ODTicket.fromJson(ticket,option) +} \ No newline at end of file diff --git a/src/data/openticket/transcriptLoader.ts b/src/data/openticket/transcriptLoader.ts new file mode 100644 index 0000000..b940bc7 --- /dev/null +++ b/src/data/openticket/transcriptLoader.ts @@ -0,0 +1,456 @@ +import {openticket, api, utilities} from "../../index" +import * as discord from "discord.js" + +const collector = openticket.transcripts.collector +const messages = openticket.builders.messages +const transcriptConfig = openticket.configs.get("openticket:transcripts") +const textConfig = transcriptConfig.data.textTranscriptStyle + +export const replaceHtmlTranscriptMentions = async (text:string) => { + const mainServer = openticket.client.mainServer + if (!mainServer) throw new api.ODSystemError("Unknown mainServer! => Required for mention replacement in Html Transcripts!") + + const usertext = await utilities.asyncReplace(text,/<@([0-9]+)>/g,async (match,id) => { + const member = await openticket.client.fetchGuildMember(mainServer,id) + return (member ? "<@"+(member.user.displayName).replace(/\s/g," ")+"> " : id) //replace with html spaces => BUG: whitespace CSS isn't "pre-wrap" + }) + + const channeltext = await utilities.asyncReplace(usertext,/<#([0-9]+)>/g,async (match,id) => { + const channel = await openticket.client.fetchGuildChannel(mainServer,id) + return (channel ? "<#"+channel.name.replace(/\s/g," ")+"> " : id) //replace with html spaces => BUG: whitespace CSS isn't "pre-wrap" + }) + + const roletext = await utilities.asyncReplace(channeltext,/<@&([0-9]+)>/g,async (match,id) => { + const role = await openticket.client.fetchGuildRole(mainServer,id) + let text = role ? role.name.replace(/\s/g," ") : id + let color = role ? ((role.hexColor == "#000000") ? "regular" : role.hexColor) : "regular" //when hex color is #000000 => render as default + return "<@&"+text+"::"+color+"> " + }) + + const defaultroletext = await utilities.asyncReplace(roletext,/@(everyone|here)/g,async (match,id) => { + return "<@&"+id+"::regular> " + }) + return defaultroletext +} + +export const loadAllTranscriptCompilers = async () => { + //TEXT COMPILER + openticket.transcripts.add(new api.ODTranscriptCompiler<{contents:string}>("openticket:text-compiler",undefined,async (ticket,channel,user) => { + //COMPILE + const rawMessages = await collector.collectAllMessages(ticket) + if (!rawMessages) return {ticket,channel,user,success:false,errorReason:"Unable to collect messages!",messages:null,data:null} + const messages = await collector.convertMessagesToTranscriptData(rawMessages) + + const finalMessages: string[] = [] + finalMessages.push("=============== MESSAGES ===============") + + messages.filter((msg) => textConfig.includeBotMessages || !msg.author.tag).forEach((msg) => { + const timestamp = utilities.dateString(new Date(msg.timestamp)) + const edited = (msg.edited ? " (edited)" : "") + const authorId = (textConfig.includeIds ? " ("+msg.author.id+")" : "") + const msgId = (textConfig.includeIds ? " ("+msg.id+")" : "") + + if (textConfig.layout == "simple"){ + //SIMPLE LAYOUT + const header = "["+timestamp+" | "+msg.author.displayname+authorId+"]"+edited+msgId + const embeds = (textConfig.includeEmbeds) ? "\nEmbeds: "+msg.embeds.length : "" + const files = (textConfig.includeFiles) ? "\nFiles: "+msg.files.length : "" + const content = (msg.content) ? msg.content : (""+embeds+files) + finalMessages.push(header+"\n "+content.split("\n").join("\n ")) + + }else if (textConfig.layout == "normal"){ + //NORMAL LAYOUT + const header = "["+timestamp+" | "+msg.author.displayname+authorId+"]"+edited+msgId + const embeds = (textConfig.includeEmbeds && msg.embeds.length > 0) ? "\n"+msg.embeds.map((embed) => { + return "==== (EMBED) "+(embed.title ?? "")+" ====\n"+(embed.description ?? "") + }) : "" + const files = (textConfig.includeFiles && msg.files.length > 0) ? "\n"+msg.files.map((file) => { + return "==== (FILE) "+(file.name)+" ====\nSize: "+(file.size+" "+file.unit)+"\nUrl: "+file.url + }) : "" + const content = (msg.content) ? msg.content : "" + finalMessages.push(header+"\n "+(content+embeds+files).split("\n").join("\n ")) + + }else if (textConfig.layout == "detailed"){ + //ADVANCED LAYOUT + const header = "["+timestamp+" | "+msg.author.displayname+authorId+"]"+edited+msgId + const embeds = (textConfig.includeEmbeds && msg.embeds.length > 0) ? "\n"+msg.embeds.map((embed) => { + return "\n==== (EMBED) "+(embed.title ?? "")+" ====\n"+(embed.description ?? "")+(embed.fields.length > 0 ? "\n\n== (FIELDS) ==\n"+embed.fields.map((field) => field.name+": "+field.value).join("\n") : "") + }) : "" + const files = (textConfig.includeFiles && msg.files.length > 0) ? "\n"+msg.files.map((file) => { + return "\n==== (FILE) "+(file.name)+" ====\nSize: "+(file.size+" "+file.unit)+"\nUrl: "+file.url+"\nAlt: "+(file.alt ?? "/") + }) : "" + const reactions = (msg.reactions.filter((r) => !r.custom).length > 0) ? "\n==== (REACTIONS) ====\n"+msg.reactions.filter((r) => !r.custom).map((r) => r.amount+" "+r.emoji).join(" - ") : "" + const content = (msg.content) ? msg.content : "" + finalMessages.push(header+"\n "+(content+embeds+files+reactions).split("\n").join("\n ")) + } + }) + + const finalStats: string[] = [] + + const creationDate = ticket.get("openticket:opened-on").value + const closeDate = ticket.get("openticket:closed-on").value + const claimDate = ticket.get("openticket:claimed-on").value + const pinDate = ticket.get("openticket:pinned-on").value + const creator = await openticket.tickets.getTicketUser(ticket,"creator") + const closer = await openticket.tickets.getTicketUser(ticket,"closer") + const claimer = await openticket.tickets.getTicketUser(ticket,"claimer") + const pinner = await openticket.tickets.getTicketUser(ticket,"pinner") + + if (textConfig.includeStats){ + finalStats.push("=============== STATS ===============") + if (textConfig.layout == "simple"){ + //SIMPLE LAYOUT + if (creationDate) finalStats.push("Created On: "+utilities.dateString(new Date(creationDate))) + if (creator) finalStats.push("Created By: "+creator.displayName) + finalStats.push("\n") + + }else if (textConfig.layout == "normal"){ + //NORMAL LAYOUT + if (creationDate) finalStats.push("Created On: "+utilities.dateString(new Date(creationDate))) + if (creator) finalStats.push("Created By: "+creator.displayName) + finalStats.push("") + if (closer) finalStats.push("Closed By: "+closer.displayName) + if (claimer) finalStats.push("Claimed By: "+claimer.displayName) + if (pinner) finalStats.push("Pinned By: "+pinner.displayName) + finalStats.push("Deleted By: "+user.displayName) + finalStats.push("\n") + + }else if (textConfig.layout == "detailed"){ + //ADVANCED LAYOUT + if (creationDate) finalStats.push("Created On: "+utilities.dateString(new Date(creationDate))) + if (creator) finalStats.push("Created By: "+creator.displayName) + finalStats.push("") + if (closeDate) finalStats.push("Closed On: "+utilities.dateString(new Date(closeDate))) + if (closer) finalStats.push("Closed By: "+closer.displayName) + finalStats.push("") + if (claimDate) finalStats.push("Claimed On: "+utilities.dateString(new Date(claimDate))) + if (claimer) finalStats.push("Claimed By: "+claimer.displayName) + finalStats.push("") + if (pinDate) finalStats.push("Pinned On: "+utilities.dateString(new Date(pinDate))) + if (pinner) finalStats.push("Pinned By: "+pinner.displayName) + finalStats.push("") + finalStats.push("Deleted On: "+utilities.dateString(new Date())) + finalStats.push("Deleted By: "+user.displayName) + finalStats.push("\n") + } + } + + const final: string[] = [] + final.push(...finalStats) + final.push(finalMessages.join("\n\n")) + + return {ticket,channel,user,success:true,errorReason:null,messages,data:{contents:final.join("\n")}} + },async (result) => { + //READY + return { + channelMessage:await messages.getSafe("openticket:transcript-text-ready").build("channel",{guild:result.channel.guild,channel:result.channel,user:result.user,ticket:result.ticket,result,compiler:openticket.transcripts.get("openticket:text-compiler")}), + creatorDmMessage:await messages.getSafe("openticket:transcript-text-ready").build("creator-dm",{guild:result.channel.guild,channel:result.channel,user:result.user,ticket:result.ticket,result,compiler:openticket.transcripts.get("openticket:text-compiler")}), + participantDmMessage:await messages.getSafe("openticket:transcript-text-ready").build("participant-dm",{guild:result.channel.guild,channel:result.channel,user:result.user,ticket:result.ticket,result,compiler:openticket.transcripts.get("openticket:text-compiler")}), + activeAdminDmMessage:await messages.getSafe("openticket:transcript-text-ready").build("active-admin-dm",{guild:result.channel.guild,channel:result.channel,user:result.user,ticket:result.ticket,result,compiler:openticket.transcripts.get("openticket:text-compiler")}), + everyAdminDmMessage:await messages.getSafe("openticket:transcript-text-ready").build("every-admin-dm",{guild:result.channel.guild,channel:result.channel,user:result.user,ticket:result.ticket,result,compiler:openticket.transcripts.get("openticket:text-compiler")}) + } + })) + + //HTML COMPILER + openticket.transcripts.add(new api.ODTranscriptCompiler<{url:string}>("openticket:html-compiler",async (ticket,channel,user) => { + //INIT + const req = new api.ODHTTPGetRequest("https://apis.dj-dj.be/transcripts/status.json",false) + const res = await req.run() + if (!res || res.status != 200 || !res.body){ + return {success:false,errorReason:"HTML Transcripts are currently unavailable!",pendingMessage:null} + } + try{ + const data = JSON.parse(res.body) + if (!data || data["v2"] != "online") return {success:false,errorReason:"HTML Transcripts are currently unavailable due to maintenance!",pendingMessage:null} + }catch{ + return {success:false,errorReason:"HTML Transcripts are currently unavailable due to JSON parse error!",pendingMessage:null} + } + + return {success:true,errorReason:null,pendingMessage:await messages.getSafe("openticket:transcript-html-progress").build("channel",{guild:channel.guild,channel,user,ticket,compiler:openticket.transcripts.get("openticket:html-compiler"),remaining:16000})} + },async (ticket,channel,user) => { + //COMPILE + const rawMessages = await collector.collectAllMessages(ticket) + if (!rawMessages) return {ticket,channel,user,success:false,errorReason:"Unable to collect messages!",messages:null,data:null} + const messages = await collector.convertMessagesToTranscriptData(rawMessages) + + const htmlMessages: api.ODTranscriptHtmlV2Data["messages"] = [] + for (const msg of messages){ + const components: api.ODTranscriptHtmlV2Data["messages"][0]["components"] = [] + msg.components.forEach((component) => { + if (component.components[0].type == "dropdown"){ + //row contains dropdown + components.push({ + type:"dropdown", + placeholder:component.components[0].placeholder ?? "Nothing Selected", + options:component.components[0].options.map((opt) => { + return { + id:opt.id, + label:opt.label ?? false, + description:opt.description ?? false, + icon:(opt.emoji && !opt.emoji.custom) ? opt.emoji.emoji : false + } + }), + + }) + }else if (component.components[0].type == "button"){ + //row contains buttons + components.push({ + type:"buttons", + buttons:component.components.map((button) => { + button = button as api.ODTranscriptButtonComponentData + return { + disabled:button.disabled, + type:(button.mode == "button") ? "interaction" : "url", + color:button.color, + id:button.id ?? false, + label:button.label ?? false, + icon:(button.emoji) ? button.emoji.emoji : false, + url:button.url ?? false + } + }), + + }) + } + }) + components.push({ + type:"reactions", + reactions:msg.reactions.map((reaction) => { + return { + amount:reaction.amount, + emoji:reaction.emoji, + type:(reaction.custom && reaction.animated) ? "gif" : (reaction.custom ? "image" : "svg") + + } + }) + }) + + const embeds: api.ODTranscriptHtmlV2Data["messages"][0]["embeds"] = [] + for (const embed of msg.embeds){ + embeds.push({ + title:embed.title ? await replaceHtmlTranscriptMentions(embed.title) : false, + color:embed.color, + description:embed.description ? await replaceHtmlTranscriptMentions(embed.description) : false, + image:embed.image ?? false, + thumbnail:embed.thumbnail ?? false, + url:embed.url ?? false, + authorimg:embed.authorimg ?? false, + authortext:embed.authortext ?? false, + footerimg:embed.footerimg ?? false, + footertext:embed.footertext ?? false, + fields:embed.fields + }) + } + + htmlMessages.push({ + author:{ + id:msg.author.id, + name:msg.author.displayname, + pfp:msg.author.pfp, + bot:msg.author.tag == "app", + system:msg.author.tag == "system", + verifiedBot:msg.author.tag == "verified", + color:msg.author.color + }, + edited:msg.edited, + timestamp:msg.timestamp, + important:msg.type == "important", + type:"normal", + content:msg.content ? await replaceHtmlTranscriptMentions(msg.content) : false, + embeds, + attachments:msg.files.map((file) => { + return { + type:"FILE", + fileType:file.type, + name:file.name, + size:file.size+" "+file.unit, + url:file.url + } + }), + components, + reply:{ + type:(msg.reply) ? (msg.reply.type == "interaction" ? "command" : "reply") : false, + user:(msg.reply) ? { + id:msg.reply.user.id, + name:msg.reply.user.displayname, + pfp:msg.reply.user.pfp, + bot:msg.reply.user.tag == "app", + system:msg.reply.user.tag == "system", + verifiedBot:msg.reply.user.tag == "verified", + color:msg.reply.user.color + } : undefined, + replyOptions:(msg.reply && msg.reply.type == "message") ? { + guildId:msg.reply.guild, + channelId:msg.reply.channel, + messageId:msg.reply.id, + content:(msg.reply.content ?? "")?.substring(0,80) + } : undefined, + commandOptions:(msg.reply && msg.reply.type == "interaction") ? { + interactionId:"", + interactionName:msg.reply.name + } : undefined + } + }) + } + + const htmlComponents: api.ODTranscriptHtmlV2Data["ticket"]["components"] = { + messages:messages.length, + embeds:0, + files:0, + interactions:{ + buttons:0, //unused + dropdowns:0, //unused + total:0 + }, + reactions:0, + attachments:{ + gifs:0, //unused + images:0, //unused + stickers:0, //unused + invites:0 //unused + } + } + messages.forEach((msg) => { + htmlComponents.embeds += msg.embeds.length + htmlComponents.files += msg.files.length + htmlComponents.reactions += msg.reactions.length + msg.components.forEach((row) => { + htmlComponents.interactions.total += row.components.length + }) + }) + + + const dsb = transcriptConfig.data.htmlTranscriptStyle.background + const dsh = transcriptConfig.data.htmlTranscriptStyle.header + const dss = transcriptConfig.data.htmlTranscriptStyle.stats + const dsf = transcriptConfig.data.htmlTranscriptStyle.favicon + + const creator = await openticket.tickets.getTicketUser(ticket,"creator") + const claimer = await openticket.tickets.getTicketUser(ticket,"claimer") + const closer = await openticket.tickets.getTicketUser(ticket,"closer") + + const htmlFinal: api.ODTranscriptHtmlV2Data = { + version:"2", + otversion:openticket.versions.get("openticket:version").toString(true), + bot:{ + name:openticket.client.client.user.displayName, + id:openticket.client.client.user.id, + pfp:openticket.client.client.user.displayAvatarURL({extension:"png"}), + }, + language:openticket.languages.getLanguageMetadata()?.language ?? "english", + style:{ + background:{ + backgroundData:(dsb.backgroundImage == "") ? dsb.backgroundColor : dsb.backgroundImage, + backgroundModus:(dsb.backgroundImage == "") ? "color" : "image", + enableCustomBackground:dsb.enableCustomBackground, + }, + header:{ + backgroundColor:dsh.backgroundColor || "#202225", + decoColor:dsh.decoColor || "#f8ba00", + textColor:dsh.textColor || "#ffffff", + enableCustomHeader:dsh.enableCustomHeader + }, + stats:{ + backgroundColor:dss.backgroundColor || "#202225", + keyTextColor:dss.keyTextColor || "#737373", + valueTextColor:dss.valueTextColor || "#ffffff", + hideBackgroundColor:dss.hideBackgroundColor || "#40444a", + hideTextColor:dss.hideTextColor || "#ffffff", + enableCustomStats:dss.enableCustomStats + }, + favicon:{ + imageUrl:dsf.imageUrl, + enableCustomFavicon:dsf.enableCustomFavicon + } + }, + ticket:{ + name:channel.name, + id:channel.id, + + guildname:channel.guild.name, + guildid:channel.guild.id, + guildinvite:"", + + creatorname:(creator ? creator.displayName : ""), + creatorid:(creator ? creator.id : ""), + creatorpfp:(creator ? creator.displayAvatarURL() : "https://transcripts.dj-dj.be/favicon.png"), + + //closer is ticket deleter (small bug) + closedbyname:user.displayName, + closedbyid:user.id, + closedbypfp:user.displayAvatarURL(), + + //claiming is currently unused + claimedname:(claimer ? claimer.displayName : ""), + claimedid:(claimer ? claimer.id : ""), + claimedpfp:(claimer ? claimer.displayAvatarURL() : "https://transcripts.dj-dj.be/favicon.png"), + + closedtime:new Date().getTime(), + openedtime:ticket.get("openticket:opened-on").value ?? new Date().getTime(), + + //role colors are currently unused + roleColors:[], + components:htmlComponents + }, + messages:htmlMessages, + + //premium is implemented, but currently unused + premium:{ + enabled:false, + premiumToken:"", + + customCredits:{ + enable:false, + replaceText:"Powered By Open Ticket!", + replaceURL:"https://openticket.dj-dj.be", + enableReportBug:true + }, + customHeaderUrl:{ + enabled:false, + url:"https://openticket.dj-dj.be", + text:"Hello!" + }, + customTranscriptUrl:{ + enabled:false, + name:"test-server" + }, + customFavicon:{ + enabled:dsf.enableCustomFavicon, + image:(dsf.imageUrl) ? dsf.imageUrl : "https://transcripts.dj-dj.be/favicon.png" + }, + additionalFlags:[] + } + } + + const req = new api.ODHTTPPostRequest("https://apis.dj-dj.be/transcripts/upload?auth=openticketTRANSCRIPT1234&version=2",true,{ + body:JSON.stringify(htmlFinal) + }) + const res = await req.run() + if (!res || res.status != 200 || !res.body){ + if (res.status == 429) return {ticket,channel,user,success:false,errorReason:"Failed to upload HTML Transcripts due to Ratelimt! Try again in a few minutes!",messages,data:null} + else return {ticket,channel,user,success:false,errorReason:"Failed to upload HTML Transcripts!",messages,data:null} + } + try{ + var data: api.ODTranscriptHtmlV2Response = JSON.parse(res.body) + if (!data || data["status"] != "success") return {ticket,channel,user,success:false,errorReason:"Failed to upload HTML Transcripts! (Status: Error)",messages,data:null} + }catch{ + return {ticket,channel,user,success:false,errorReason:"Failed to upload HTML Transcripts due to JSON parse error!",messages,data:null} + } + + const url = "https://transcripts.dj-dj.be/v2/"+data.time+"_"+data.id+".html" + return {ticket,channel,user,success:true,errorReason:null,messages,data:{url}} + },async (result) => { + //READY + await utilities.timer(16000) //wait until transcript is ready + return { + channelMessage:await messages.getSafe("openticket:transcript-html-ready").build("channel",{guild:result.channel.guild,channel:result.channel,user:result.user,ticket:result.ticket,result,compiler:openticket.transcripts.get("openticket:html-compiler")}), + creatorDmMessage:await messages.getSafe("openticket:transcript-html-ready").build("creator-dm",{guild:result.channel.guild,channel:result.channel,user:result.user,ticket:result.ticket,result,compiler:openticket.transcripts.get("openticket:html-compiler")}), + participantDmMessage:await messages.getSafe("openticket:transcript-html-ready").build("participant-dm",{guild:result.channel.guild,channel:result.channel,user:result.user,ticket:result.ticket,result,compiler:openticket.transcripts.get("openticket:html-compiler")}), + activeAdminDmMessage:await messages.getSafe("openticket:transcript-html-ready").build("active-admin-dm",{guild:result.channel.guild,channel:result.channel,user:result.user,ticket:result.ticket,result,compiler:openticket.transcripts.get("openticket:html-compiler")}), + everyAdminDmMessage:await messages.getSafe("openticket:transcript-html-ready").build("every-admin-dm",{guild:result.channel.guild,channel:result.channel,user:result.user,ticket:result.ticket,result,compiler:openticket.transcripts.get("openticket:html-compiler")}) + } + })) +} + +export const loadTranscriptHistory = async () => { + //UNIMPLEMENTED (made for html transcripts v3 update) +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6847de1 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,742 @@ +/* + ██████╗ ██████╗ ███████╗███╗ ██╗ ████████╗██╗ ██████╗██╗ ██╗███████╗████████╗ + ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ╚══██╔══╝██║██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝ + ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║██║ █████╔╝ █████╗ ██║ + ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ + ╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ██║╚██████╗██║ ██╗███████╗ ██║ + ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ + + > Hey! We are looking for you! + > Do you speak a language that isn't yet in our /languages directory? + > Or do you speak one that isn't up-to-date anymore? + > Open Ticket needs translators for lots of different languages! + > Feel free to join our translator team and help us improve Open Ticket! + + SUGGESTIONS: + ===================== + Did you know that almost 70% of all Open Ticket features were requested by the community? + Feel free to suggest new ideas in our discord server or via github issues! + They are always welcome! + + INFORMATION: + ============ + Open Ticket v4.0.0 - © DJdj Development + + discord: https://discord.dj-dj.be + website: https://openticket.dj-dj.be + github: https://otgithub.dj-dj.be + documentation: https://otdocs.dj-dj.be + + Config files: + ./config/....json + + Send ./otdebug.txt to us when there is an error! +*/ + +//initialize API & check npm libraries +import { api, openticket, utilities } from "./core/startup/init" +export { api, openticket, utilities } from "./core/startup/init" +import ansis from "ansis" + +/**The main sequence of Open Ticket. Runs `async` */ +const main = async () => { + //load all events + (await import("./data/framework/eventLoader.ts")).loadAllEvents() + + //error handling system + process.on("uncaughtException",async (error,origin) => { + try{ + await openticket.events.get("onErrorHandling").emit([error,origin]) + if (openticket.defaults.getDefault("errorHandling")){ + //custom error messages for known errors + if (error.message == "Used disallowed intents"){ + //invalid intents + openticket.log("Open Ticket doesn't work without Privileged Gateway Intents enabled!","error") + openticket.log("Enable them in the discord developer portal!","info") + console.log("\n") + process.exit(1) + }else if (error.message == "An invalid token was provided."){ + //invalid token + openticket.log("An invalid discord auth token was provided!","error") + openticket.log("Check the config if you have copied the token correctly!","info") + console.log("\n") + process.exit(1) + }else{ + //unknown error + const errmsg = new api.ODError(error,origin) + openticket.log(errmsg) + if (openticket.defaults.getDefault("crashOnError")) process.exit(1) + await openticket.events.get("afterErrorHandling").emit([error,origin,errmsg]) + } + } + + }catch(err){ + console.log("[ERROR HANDLER ERROR]:",err) + } + }) + + //handle data migration + await (await import("./core/startup/manageMigration.ts")).loadVersionMigrationSystem() + + //load plugins + if (openticket.defaults.getDefault("pluginLoading")){ + await (await import("./core/startup/pluginLauncher.ts")).loadAllPlugins() + } + await openticket.events.get("afterPluginsLoaded").emit([openticket.plugins]) + + //load plugin classes + openticket.log("Loading plugin classes...","system") + if (openticket.defaults.getDefault("pluginClassLoading")){ + + } + await openticket.events.get("onPluginClassLoad").emit([openticket.plugins.classes,openticket.plugins]) + await openticket.events.get("afterPluginClassesLoaded").emit([openticket.plugins.classes,openticket.plugins]) + + //load plugin events + openticket.log("Loading plugin events...","system") + if (openticket.defaults.getDefault("pluginEventLoading")){ + + } + await openticket.events.get("onPluginEventLoad").emit([openticket.plugins.events,openticket.plugins]) + await openticket.events.get("afterPluginEventsLoaded").emit([openticket.plugins.events,openticket.plugins]) + + //load flags + openticket.log("Loading flags...","system") + if (openticket.defaults.getDefault("flagLoading")){ + await (await import("./data/framework/flagLoader.ts")).loadAllFlags() + } + await openticket.events.get("onFlagLoad").emit([openticket.flags]) + await openticket.events.get("afterFlagsLoaded").emit([openticket.flags]) + + //initiate flags + await openticket.events.get("onFlagInit").emit([openticket.flags]) + if (openticket.defaults.getDefault("flagInitiating")){ + openticket.flags.init() + openticket.debugfile.writeText("\n[ENABLED FLAGS]:\n"+openticket.flags.getFiltered((flag) => (flag.value == true)).map((flag) => flag.id.value).join("\n")+"\n") + await openticket.events.get("afterFlagsInitiated").emit([openticket.flags]) + } + + //load debug + if (openticket.defaults.getDefault("debugLoading")){ + const debugFlag = openticket.flags.get("openticket:debug") + openticket.debug.visible = (debugFlag) ? debugFlag.value : false + } + + //load config + openticket.log("Loading configs...","system") + if (openticket.defaults.getDefault("configLoading")){ + await (await import("./data/framework/configLoader.ts")).loadAllConfigs() + } + await openticket.events.get("onConfigLoad").emit([openticket.configs]) + await openticket.events.get("afterConfigsLoaded").emit([openticket.configs]) + + //UTILITY CONFIG + const generalConfig = openticket.configs.get("openticket:general") + + //load database + openticket.log("Loading databases...","system") + if (openticket.defaults.getDefault("databaseLoading")){ + await (await import("./data/framework/databaseLoader.ts")).loadAllDatabases() + } + await openticket.events.get("onDatabaseLoad").emit([openticket.databases]) + await openticket.events.get("afterDatabasesLoaded").emit([openticket.databases]) + + //load sessions + openticket.log("Loading sessions...","system") + if (openticket.defaults.getDefault("sessionLoading")){ + + } + await openticket.events.get("onSessionLoad").emit([openticket.sessions]) + await openticket.events.get("afterSessionsLoaded").emit([openticket.sessions]) + + //load language + openticket.log("Loading languages...","system") + if (openticket.defaults.getDefault("languageLoading")){ + await (await import("./data/framework/languageLoader.ts")).loadAllLanguages() + } + await openticket.events.get("onLanguageLoad").emit([openticket.languages]) + await openticket.events.get("afterLanguagesLoaded").emit([openticket.languages]) + + //select language + await openticket.events.get("onLanguageSelect").emit([openticket.languages]) + if (openticket.defaults.getDefault("languageSelection")){ + //set current language + const languageId = (generalConfig && generalConfig.data.language) ? generalConfig.data.language : "english" + if (languageId.includes(":")){ + openticket.languages.setCurrentLanguage(languageId) + }else{ + openticket.languages.setCurrentLanguage("openticket:"+languageId) + } + + //set backup language + const backupLanguageId = openticket.defaults.getDefault("backupLanguage") + if (openticket.languages.exists(backupLanguageId)){ + openticket.languages.setBackupLanguage(backupLanguageId) + + }else throw new api.ODSystemError("Unknown backup language '"+backupLanguageId+"'!") + + await openticket.events.get("afterLanguagesSelected").emit([openticket.languages.get(languageId),openticket.languages.get(backupLanguageId),openticket.languages]) + } + + //load config checker + openticket.log("Loading config checker...","system") + if (openticket.defaults.getDefault("checkerLoading")){ + await (await import("./data/framework/checkerLoader.ts")).loadAllConfigCheckers() + } + await openticket.events.get("onCheckerLoad").emit([openticket.checkers]) + await openticket.events.get("afterCheckersLoaded").emit([openticket.checkers]) + + //load config checker functions + if (openticket.defaults.getDefault("checkerFunctionLoading")){ + await (await import("./data/framework/checkerLoader.ts")).loadAllConfigCheckerFunctions() + } + await openticket.events.get("onCheckerFunctionLoad").emit([openticket.checkers.functions,openticket.checkers]) + await openticket.events.get("afterCheckerFunctionsLoaded").emit([openticket.checkers.functions,openticket.checkers]) + + //execute config checker + await openticket.events.get("onCheckerExecute").emit([openticket.checkers]) + if (openticket.defaults.getDefault("checkerExecution")){ + const result = openticket.checkers.checkAll(true) + await openticket.events.get("afterCheckersExecuted").emit([result,openticket.checkers]) + } + + //load config checker translations + if (openticket.defaults.getDefault("checkerTranslationLoading")){ + await (await import("./data/framework/checkerLoader.ts")).loadAllConfigCheckerTranslations() + } + await openticket.events.get("onCheckerTranslationLoad").emit([openticket.checkers.translation,((generalConfig && generalConfig.data.system && generalConfig.data.system.useTranslatedConfigChecker) ? generalConfig.data.system.useTranslatedConfigChecker : false),openticket.checkers]) + await openticket.events.get("afterCheckerTranslationsLoaded").emit([openticket.checkers.translation,openticket.checkers]) + + //render config checker + const advancedCheckerFlag = openticket.flags.get("openticket:checker") + const disableCheckerFlag = openticket.flags.get("openticket:no-checker") + + await openticket.events.get("onCheckerRender").emit([openticket.checkers.renderer,openticket.checkers]) + if (openticket.defaults.getDefault("checkerRendering") && !(disableCheckerFlag ? disableCheckerFlag.value : false)){ + //check if there is a result (otherwise throw minor error) + const result = openticket.checkers.lastResult + if (!result) return openticket.log("Failed to render Config Checker! (couldn't fetch result)","error") + + //get components & check if full mode enabled + const components = openticket.checkers.renderer.getComponents(!(advancedCheckerFlag ? advancedCheckerFlag.value : false),openticket.defaults.getDefault("checkerRenderEmpty"),openticket.checkers.translation,result) + + //render + openticket.debugfile.writeText("\n[CONFIG CHECKER RESULT]:\n"+ansis.strip(components.join("\n"))+"\n") + openticket.checkers.renderer.render(components) + await openticket.events.get("afterCheckersRendered").emit([openticket.checkers.renderer,openticket.checkers]) + } + + //quit config checker (when required) + if (openticket.checkers.lastResult && !openticket.checkers.lastResult.valid && !(disableCheckerFlag ? disableCheckerFlag.value : false)){ + await openticket.events.get("onCheckerQuit").emit([openticket.checkers]) + if (openticket.defaults.getDefault("checkerQuit")){ + process.exit(0) + //there is no afterCheckerQuitted event :) + } + } + + //client configuration + openticket.log("Loading client...","system") + if (openticket.defaults.getDefault("clientLoading")){ + //add intents (for basic permissions) + openticket.client.intents.push( + "Guilds", + "GuildMessages", + "DirectMessages", + "GuildEmojisAndStickers", + "GuildMembers", + "MessageContent", + "GuildWebhooks", + "GuildInvites" + ) + + //add privileged intents (required for transcripts) + openticket.client.privileges.push("MessageContent","GuildMembers") + + //add partials (required for DM messages) + openticket.client.partials.push("Channel","Message") + + //add permissions (not required when Administrator) + openticket.client.permissions.push( + "AddReactions", + "AttachFiles", + "CreatePrivateThreads", + "CreatePublicThreads", + "EmbedLinks", + "ManageChannels", + "ManageGuild", + "ManageMessages", + "ChangeNickname", + "ManageRoles", + "ManageThreads", + "ManageWebhooks", + "MentionEveryone", + "ReadMessageHistory", + "SendMessages", + "SendMessagesInThreads", + "UseApplicationCommands", + "UseExternalEmojis", + "ViewAuditLog", + "ViewChannel" + ) + + //get token from config or env + const configToken = openticket.configs.get("openticket:general").data.token ? openticket.configs.get("openticket:general").data.token : "" + const envToken = openticket.env.getVariable("TOKEN") ? openticket.env.getVariable("TOKEN") : "" + const token = openticket.configs.get("openticket:general").data.tokenFromENV ? envToken : configToken + openticket.client.token = token + } + await openticket.events.get("onClientLoad").emit([openticket.client]) + await openticket.events.get("afterClientLoaded").emit([openticket.client]) + + //client ready + openticket.client.readyListener = async () => { + openticket.log("Loading client setup...","system") + await openticket.events.get("onClientReady").emit([openticket.client]) + if (openticket.defaults.getDefault("clientReady")){ + const client = openticket.client + + //check if all servers are valid + const botServers = client.getGuilds() + const generalConfig = openticket.configs.get("openticket:general") + const serverId = generalConfig.data.serverId ? generalConfig.data.serverId : "" + if (!serverId) throw new api.ODSystemError("Server Id Missing!") + + const mainServer = botServers.find((g) => g.id == serverId) + client.mainServer = mainServer ?? null + //throw if bot isn't member of main server + if (!mainServer || !client.checkBotInGuild(mainServer)){ + console.log("\n") + openticket.log("The bot isn't a member of the server provided in the config!","error") + openticket.log("Please invite your bot to the server!","info") + console.log("\n") + process.exit(0) + } + //throw if bot doesn't have permissions in main server + if (!client.checkGuildPerms(mainServer)){ + console.log("\n") + openticket.log("The bot doesn't have the correct permissions in the server provided in the config!","error") + openticket.log("Please give the bot \"Administrator\" permissions or visit the documentation!","info") + console.log("\n") + process.exit(0) + } + if (openticket.defaults.getDefault("clientMultiGuildWarning")){ + //warn if bot is in multiple servers + if (botServers.length > 1){ + openticket.log("This bot is part of multiple servers, but Open Ticket doesn't have support for it!","warning") + openticket.log("It may result in the bot crashing & glitching when used in these servers!","info") + } + botServers.forEach((server) => { + //warn if bot doesn't have permissions in multiple servers + if (!client.checkGuildPerms(server)) openticket.log(`The bot doesn't have the correct permissions in the server "${server.name}"!`,"warning") + }) + } + + //load client activity + openticket.log("Loading client activity...","system") + if (openticket.defaults.getDefault("clientActivityLoading")){ + //load config status + if (generalConfig.data.status && generalConfig.data.status.enabled) openticket.client.activity.setStatus(generalConfig.data.status.type,generalConfig.data.status.text,generalConfig.data.status.status) + } + await openticket.events.get("onClientActivityLoad").emit([openticket.client.activity,openticket.client]) + await openticket.events.get("afterClientActivityLoaded").emit([openticket.client.activity,openticket.client]) + + //initiate client activity + await openticket.events.get("onClientActivityInit").emit([openticket.client.activity,openticket.client]) + if (openticket.defaults.getDefault("clientActivityInitiating")){ + openticket.client.activity.initStatus() + await openticket.events.get("afterClientActivityInitiated").emit([openticket.client.activity,openticket.client]) + } + + //load slash commands + openticket.log("Loading slash commands...","system") + if (openticket.defaults.getDefault("slashCommandLoading")){ + await (await import("./data/framework/commandLoader.ts")).loadAllSlashCommands() + } + await openticket.events.get("onSlashCommandLoad").emit([openticket.client.slashCommands,openticket.client]) + await openticket.events.get("afterSlashCommandsLoaded").emit([openticket.client.slashCommands,openticket.client]) + + //register slash commands (create, update & remove) + if (openticket.defaults.getDefault("forceSlashCommandRegistration")) openticket.log("Forcing all slash commands to be re-registered...","system") + openticket.log("Registering slash commands... (this can take up to 2 minutes)","system") + await openticket.events.get("onSlashCommandRegister").emit([openticket.client.slashCommands,openticket.client]) + if (openticket.defaults.getDefault("slashCommandRegistering")){ + //GLOBAL + await openticket.client.slashCommands.removeUnusedCommands() //remove all commands that aren't used + await openticket.client.slashCommands.createNewCommands() //create all new commands that don't exist yet + await openticket.client.slashCommands.updateExistingCommands(undefined,openticket.defaults.getDefault("forceSlashCommandRegistration")) //update all commands that need to be re-registered + + //DEFAULT SERVER + await openticket.client.slashCommands.removeUnusedCommands(serverId) //remove all commands that aren't used + await openticket.client.slashCommands.createNewCommands(serverId) //create all new commands that don't exist yet + await openticket.client.slashCommands.updateExistingCommands(serverId) //update all commands that need to be re-registered + + await openticket.events.get("afterSlashCommandsRegistered").emit([openticket.client.slashCommands,openticket.client]) + } + + //load text commands + openticket.log("Loading text commands...","system") + if (openticket.defaults.getDefault("textCommandLoading")){ + await (await import("./data/framework/commandLoader.ts")).loadAllTextCommands() + } + await openticket.events.get("onTextCommandLoad").emit([openticket.client.textCommands,openticket.client]) + await openticket.events.get("afterTextCommandsLoaded").emit([openticket.client.textCommands,openticket.client]) + + //client ready + await openticket.events.get("afterClientReady").emit([openticket.client]) + } + } + + //client init (login) + openticket.log("Logging in...","system") + await openticket.events.get("onClientInit").emit([openticket.client]) + if (openticket.defaults.getDefault("clientInitiating")){ + //init client + openticket.client.initClient() + await openticket.events.get("afterClientInitiated").emit([openticket.client]) + + //client login + await openticket.client.login() + openticket.log("discord.js client ready!","info") + } + + //load questions + openticket.log("Loading questions...","system") + if (openticket.defaults.getDefault("questionLoading")){ + await (await import("./data/openticket/questionLoader.ts")).loadAllQuestions() + } + await openticket.events.get("onQuestionLoad").emit([openticket.questions]) + await openticket.events.get("afterQuestionsLoaded").emit([openticket.questions]) + + //load options + openticket.log("Loading options...","system") + if (openticket.defaults.getDefault("optionLoading")){ + await (await import("./data/openticket/optionLoader.ts")).loadAllOptions() + } + await openticket.events.get("onOptionLoad").emit([openticket.options]) + await openticket.events.get("afterOptionsLoaded").emit([openticket.options]) + + //load panels + openticket.log("Loading panels...","system") + if (openticket.defaults.getDefault("panelLoading")){ + await (await import("./data/openticket/panelLoader.ts")).loadAllPanels() + } + await openticket.events.get("onPanelLoad").emit([openticket.panels]) + await openticket.events.get("afterPanelsLoaded").emit([openticket.panels]) + + //load tickets + openticket.log("Loading tickets...","system") + if (openticket.defaults.getDefault("ticketLoading")){ + openticket.tickets.useGuild(openticket.client.mainServer) + await (await import("./data/openticket/ticketLoader.ts")).loadAllTickets() + } + await openticket.events.get("onTicketLoad").emit([openticket.tickets]) + await openticket.events.get("afterTicketsLoaded").emit([openticket.tickets]) + + //load roles + openticket.log("Loading roles...","system") + if (openticket.defaults.getDefault("roleLoading")){ + await (await import("./data/openticket/roleLoader.ts")).loadAllRoles() + } + await openticket.events.get("onRoleLoad").emit([openticket.roles]) + await openticket.events.get("afterRolesLoaded").emit([openticket.roles]) + + //load blacklist + openticket.log("Loading blacklist...","system") + if (openticket.defaults.getDefault("blacklistLoading")){ + await (await import("./data/openticket/blacklistLoader.ts")).loadAllBlacklistedUsers() + } + await openticket.events.get("onBlacklistLoad").emit([openticket.blacklist]) + await openticket.events.get("afterBlacklistLoaded").emit([openticket.blacklist]) + + //load transcript compilers + openticket.log("Loading transcripts...","system") + if (openticket.defaults.getDefault("transcriptCompilerLoading")){ + await (await import("./data/openticket/transcriptLoader.ts")).loadAllTranscriptCompilers() + } + await openticket.events.get("onTranscriptCompilerLoad").emit([openticket.transcripts]) + await openticket.events.get("afterTranscriptCompilersLoaded").emit([openticket.transcripts]) + + //load transcript history + if (openticket.defaults.getDefault("transcriptHistoryLoading")){ + await (await import("./data/openticket/transcriptLoader.ts")).loadTranscriptHistory() + } + await openticket.events.get("onTranscriptHistoryLoad").emit([openticket.transcripts]) + await openticket.events.get("afterTranscriptHistoryLoaded").emit([openticket.transcripts]) + + //load button builders + openticket.log("Loading buttons...","system") + if (openticket.defaults.getDefault("buttonBuildersLoading")){ + await (await import("./builders/buttons.ts")).registerAllButtons() + } + await openticket.events.get("onButtonBuilderLoad").emit([openticket.builders.buttons,openticket.builders,openticket.actions]) + await openticket.events.get("afterButtonBuildersLoaded").emit([openticket.builders.buttons,openticket.builders,openticket.actions]) + + //load dropdown builders + openticket.log("Loading dropdowns...","system") + if (openticket.defaults.getDefault("dropdownBuildersLoading")){ + await (await import("./builders/dropdowns.ts")).registerAllDropdowns() + } + await openticket.events.get("onDropdownBuilderLoad").emit([openticket.builders.dropdowns,openticket.builders,openticket.actions]) + await openticket.events.get("afterDropdownBuildersLoaded").emit([openticket.builders.dropdowns,openticket.builders,openticket.actions]) + + //load file builders + openticket.log("Loading files...","system") + if (openticket.defaults.getDefault("fileBuildersLoading")){ + await (await import("./builders/files.ts")).registerAllFiles() + } + await openticket.events.get("onFileBuilderLoad").emit([openticket.builders.files,openticket.builders,openticket.actions]) + await openticket.events.get("afterFileBuildersLoaded").emit([openticket.builders.files,openticket.builders,openticket.actions]) + + //load embed builders + openticket.log("Loading embeds...","system") + if (openticket.defaults.getDefault("embedBuildersLoading")){ + await (await import("./builders/embeds.ts")).registerAllEmbeds() + } + await openticket.events.get("onEmbedBuilderLoad").emit([openticket.builders.embeds,openticket.builders,openticket.actions]) + await openticket.events.get("afterEmbedBuildersLoaded").emit([openticket.builders.embeds,openticket.builders,openticket.actions]) + + //load message builders + openticket.log("Loading messages...","system") + if (openticket.defaults.getDefault("messageBuildersLoading")){ + await (await import("./builders/messages.ts")).registerAllMessages() + } + await openticket.events.get("onMessageBuilderLoad").emit([openticket.builders.messages,openticket.builders,openticket.actions]) + await openticket.events.get("afterMessageBuildersLoaded").emit([openticket.builders.messages,openticket.builders,openticket.actions]) + + //load modal builders + openticket.log("Loading modals...","system") + if (openticket.defaults.getDefault("modalBuildersLoading")){ + await (await import("./builders/modals.ts")).registerAllModals() + } + await openticket.events.get("onModalBuilderLoad").emit([openticket.builders.modals,openticket.builders,openticket.actions]) + await openticket.events.get("afterModalBuildersLoaded").emit([openticket.builders.modals,openticket.builders,openticket.actions]) + + //load command responders + openticket.log("Loading command responders...","system") + if (openticket.defaults.getDefault("commandRespondersLoading")){ + await (await import("./commands/help.ts")).registerCommandResponders() + await (await import("./commands/stats.ts")).registerCommandResponders() + await (await import("./commands/panel.ts")).registerCommandResponders() + await (await import("./commands/ticket.ts")).registerCommandResponders() + await (await import("./commands/blacklist.ts")).registerCommandResponders() + await (await import("./commands/close.ts")).registerCommandResponders() + await (await import("./commands/reopen.ts")).registerCommandResponders() + await (await import("./commands/delete.ts")).registerCommandResponders() + await (await import("./commands/claim.ts")).registerCommandResponders() + await (await import("./commands/unclaim.ts")).registerCommandResponders() + await (await import("./commands/pin.ts")).registerCommandResponders() + await (await import("./commands/unpin.ts")).registerCommandResponders() + await (await import("./commands/rename.ts")).registerCommandResponders() + await (await import("./commands/move.ts")).registerCommandResponders() + await (await import("./commands/add.ts")).registerCommandResponders() + await (await import("./commands/remove.ts")).registerCommandResponders() + await (await import("./commands/clear.ts")).registerCommandResponders() + await (await import("./commands/autoclose.ts")).registerCommandResponders() + await (await import("./commands/autodelete.ts")).registerCommandResponders() + } + await openticket.events.get("onCommandResponderLoad").emit([openticket.responders.commands,openticket.responders,openticket.actions]) + await openticket.events.get("afterCommandRespondersLoaded").emit([openticket.responders.commands,openticket.responders,openticket.actions]) + + //load button responders + openticket.log("Loading button responders...","system") + if (openticket.defaults.getDefault("buttonRespondersLoading")){ + await (await import("./actions/handleVerifyBar.ts")).registerButtonResponders() + await (await import("./actions/handleTranscriptErrors.ts")).registerButtonResponders() + await (await import("./commands/help.ts")).registerButtonResponders() + await (await import("./commands/ticket.ts")).registerButtonResponders() + await (await import("./commands/close.ts")).registerButtonResponders() + await (await import("./commands/reopen.ts")).registerButtonResponders() + await (await import("./commands/delete.ts")).registerButtonResponders() + await (await import("./commands/claim.ts")).registerButtonResponders() + await (await import("./commands/unclaim.ts")).registerButtonResponders() + await (await import("./commands/pin.ts")).registerButtonResponders() + await (await import("./commands/unpin.ts")).registerButtonResponders() + await (await import("./commands/role.ts")).registerButtonResponders() + await (await import("./commands/clear.ts")).registerButtonResponders() + } + await openticket.events.get("onButtonResponderLoad").emit([openticket.responders.buttons,openticket.responders,openticket.actions]) + await openticket.events.get("afterButtonRespondersLoaded").emit([openticket.responders.buttons,openticket.responders,openticket.actions]) + + //load dropdown responders + openticket.log("Loading dropdown responders...","system") + if (openticket.defaults.getDefault("dropdownRespondersLoading")){ + await (await import("./commands/ticket.ts")).registerDropdownResponders() + } + await openticket.events.get("onDropdownResponderLoad").emit([openticket.responders.dropdowns,openticket.responders,openticket.actions]) + await openticket.events.get("afterDropdownRespondersLoaded").emit([openticket.responders.dropdowns,openticket.responders,openticket.actions]) + + //load modal responders + openticket.log("Loading modal responders...","system") + if (openticket.defaults.getDefault("modalRespondersLoading")){ + await (await import("./commands/ticket.ts")).registerModalResponders() + await (await import("./commands/close.ts")).registerModalResponders() + await (await import("./commands/reopen.ts")).registerModalResponders() + await (await import("./commands/delete.ts")).registerModalResponders() + await (await import("./commands/claim.ts")).registerModalResponders() + await (await import("./commands/unclaim.ts")).registerModalResponders() + await (await import("./commands/pin.ts")).registerModalResponders() + await (await import("./commands/unpin.ts")).registerModalResponders() + } + await openticket.events.get("onModalResponderLoad").emit([openticket.responders.modals,openticket.responders,openticket.actions]) + await openticket.events.get("afterModalRespondersLoaded").emit([openticket.responders.modals,openticket.responders,openticket.actions]) + + //load actions + openticket.log("Loading actions...","system") + if (openticket.defaults.getDefault("actionsLoading")){ + await (await import("./actions/createTicketPermissions.ts")).registerActions() + await (await import("./actions/createTranscript.ts")).registerActions() + await (await import("./actions/createTicket.ts")).registerActions() + await (await import("./actions/closeTicket.ts")).registerActions() + await (await import("./actions/deleteTicket.ts")).registerActions() + await (await import("./actions/reopenTicket.ts")).registerActions() + await (await import("./actions/claimTicket.ts")).registerActions() + await (await import("./actions/unclaimTicket.ts")).registerActions() + await (await import("./actions/pinTicket.ts")).registerActions() + await (await import("./actions/unpinTicket.ts")).registerActions() + await (await import("./actions/renameTicket.ts")).registerActions() + await (await import("./actions/moveTicket.ts")).registerActions() + await (await import("./actions/addTicketUser.ts")).registerActions() + await (await import("./actions/removeTicketUser.ts")).registerActions() + await (await import("./actions/reactionRole.ts")).registerActions() + await (await import("./actions/clearTickets.ts")).registerActions() + } + await openticket.events.get("onActionLoad").emit([openticket.actions]) + await openticket.events.get("afterActionsLoaded").emit([openticket.actions]) + + //load verifybars + openticket.log("Loading verifybars...","system") + if (openticket.defaults.getDefault("verifyBarsLoading")){ + await (await import("./actions/closeTicket.ts")).registerVerifyBars() + await (await import("./actions/deleteTicket.ts")).registerVerifyBars() + await (await import("./actions/reopenTicket.ts")).registerVerifyBars() + await (await import("./actions/claimTicket.ts")).registerVerifyBars() + await (await import("./actions/unclaimTicket.ts")).registerVerifyBars() + await (await import("./actions/pinTicket.ts")).registerVerifyBars() + await (await import("./actions/unpinTicket.ts")).registerVerifyBars() + } + await openticket.events.get("onVerifyBarLoad").emit([openticket.verifybars]) + await openticket.events.get("afterVerifyBarsLoaded").emit([openticket.verifybars]) + + //load permissions + openticket.log("Loading permissions...","system") + if (openticket.defaults.getDefault("permissionsLoading")){ + await (await import("./data/framework/permissionLoader.ts")).loadAllPermissions() + } + await openticket.events.get("onPermissionLoad").emit([openticket.permissions]) + await openticket.events.get("afterPermissionsLoaded").emit([openticket.permissions]) + + //load posts + openticket.log("Loading posts...","system") + if (openticket.defaults.getDefault("postsLoading")){ + await (await import("./data/framework/postLoader.ts")).loadAllPosts() + } + await openticket.events.get("onPostLoad").emit([openticket.posts]) + await openticket.events.get("afterPostsLoaded").emit([openticket.posts]) + + //init posts + await openticket.events.get("onPostInit").emit([openticket.posts]) + if (openticket.defaults.getDefault("postsInitiating")){ + if (openticket.client.mainServer) openticket.posts.init(openticket.client.mainServer) + await openticket.events.get("afterPostsInitiated").emit([openticket.posts]) + } + + //load cooldowns + openticket.log("Loading cooldowns...","system") + if (openticket.defaults.getDefault("cooldownsLoading")){ + await (await import("./data/framework/cooldownLoader.ts")).loadAllCooldowns() + } + await openticket.events.get("onCooldownLoad").emit([openticket.cooldowns]) + await openticket.events.get("afterCooldownsLoaded").emit([openticket.cooldowns]) + + //init cooldowns + await openticket.events.get("onCooldownInit").emit([openticket.cooldowns]) + if (openticket.defaults.getDefault("cooldownsInitiating")){ + await openticket.cooldowns.init() + await openticket.events.get("afterCooldownsInitiated").emit([openticket.cooldowns]) + } + + //load help menu categories + openticket.log("Loading help menu...","system") + if (openticket.defaults.getDefault("helpMenuCategoryLoading")){ + await (await import("./data/framework/helpMenuLoader.ts")).loadAllHelpMenuCategories() + } + await openticket.events.get("onHelpMenuCategoryLoad").emit([openticket.helpmenu]) + await openticket.events.get("afterHelpMenuCategoriesLoaded").emit([openticket.helpmenu]) + + //load help menu components + if (openticket.defaults.getDefault("helpMenuComponentLoading")){ + await (await import("./data/framework/helpMenuLoader.ts")).loadAllHelpMenuComponents() + } + await openticket.events.get("onHelpMenuComponentLoad").emit([openticket.helpmenu]) + await openticket.events.get("afterHelpMenuComponentsLoaded").emit([openticket.helpmenu]) + + //load stat scopes + openticket.log("Loading stats...","system") + if (openticket.defaults.getDefault("statScopesLoading")){ + openticket.stats.useDatabase(openticket.databases.get("openticket:stats")) + await (await import("./data/framework/statLoader.ts")).loadAllStatScopes() + } + await openticket.events.get("onStatScopeLoad").emit([openticket.stats]) + await openticket.events.get("afterStatScopesLoaded").emit([openticket.stats]) + + //load stats + if (openticket.defaults.getDefault("statLoading")){ + await (await import("./data/framework/statLoader.ts")).loadAllStats() + } + await openticket.events.get("onStatLoad").emit([openticket.stats]) + await openticket.events.get("afterStatsLoaded").emit([openticket.stats]) + + //init stats + await openticket.events.get("onStatInit").emit([openticket.stats]) + if (openticket.defaults.getDefault("statInitiating")){ + await openticket.stats.init() + await openticket.events.get("afterStatsInitiated").emit([openticket.stats]) + } + + //load code + openticket.log("Loading code...","system") + if (openticket.defaults.getDefault("codeLoading")){ + await (await import("./data/framework/codeLoader.ts")).loadAllCode() + } + await openticket.events.get("onCodeLoad").emit([openticket.code]) + await openticket.events.get("afterCodeLoaded").emit([openticket.code]) + + //execute code + await openticket.events.get("onCodeExecute").emit([openticket.code]) + if (openticket.defaults.getDefault("codeExecution")){ + await openticket.code.execute() + await openticket.events.get("afterCodeExecuted").emit([openticket.code]) + } + + //finish setup + openticket.log("Setup complete!","info") + + //load livestatus sources + openticket.log("Loading livestatus...","system") + if (openticket.defaults.getDefault("liveStatusLoading")){ + await (await import("./data/framework/liveStatusLoader.ts")).loadAllLiveStatusSources() + } + await openticket.events.get("onLiveStatusSourceLoad").emit([openticket.livestatus]) + await openticket.events.get("afterLiveStatusSourcesLoaded").emit([openticket.livestatus]) + + //load startscreen + openticket.log("Loading startscreen...","system") + if (openticket.defaults.getDefault("startScreenLoading")){ + await (await import("./data/framework/startScreenLoader.ts")).loadAllStartScreenComponents() + } + await openticket.events.get("onStartScreenLoad").emit([openticket.startscreen]) + await openticket.events.get("afterStartScreensLoaded").emit([openticket.startscreen]) + + //render startscreen + await openticket.events.get("onStartScreenRender").emit([openticket.startscreen]) + if (openticket.defaults.getDefault("startScreenRendering")){ + await openticket.startscreen.renderAllComponents() + + await openticket.events.get("afterStartScreensRendered").emit([openticket.startscreen]) + } + + //YIPPPIE!! + //The startup of Open Ticket is completed :) +} +main() \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3398e8e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "rootDir": "./src/", + "outDir": "./dist/", + "module": "NodeNext", + "moduleResolution": "NodeNext", + + "typeRoots": ["./node_modules/@types","./plugins/*/"], + "noEmit": true, + "allowImportingTsExtensions": true, + + "allowJs": false, + "checkJs": false, + "declaration": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/*","./src/**/*"] +} \ No newline at end of file