diff --git a/.all-contributorsrc b/.all-contributorsrc index 9a46edc..ee203b6 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -4,13 +4,11 @@ "repoType": "github", "repoHost": "https://github.com", "files": [ - "CONTRIBUTORS.md", - "README.md" + "CONTRIBUTORS.md" ], "imageSize": 100, - "commit": false, + "commit": true, "commitConvention": "angular", - "badgeTemplate": "\n\n", "contributors": [ { "login": "eartharoid", diff --git a/.commitlintrc.js b/.commitlintrc.js new file mode 100644 index 0000000..3d88028 --- /dev/null +++ b/.commitlintrc.js @@ -0,0 +1 @@ +module.exports = { extends: ['@commitlint/config-conventional'] }; \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index b512c09..cbeb323 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,19 @@ -node_modules \ No newline at end of file +# only db, scripts, src, and user directories are needed +.github +.husky +.vscode +logs +node_modules +# prisma will generated by postinstall script +prisma +.all-contributorsrc +.commitlintrc.js +.dockerignore +.env +.eslintrc.json +.gitattributes +.gitignore +CONTRIBUTORS.md +Dockerfile +jsconfig.json +README.md \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a8a125a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.md] +indent_style = space +indent_size = 4 diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 9a258ff..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,189 +0,0 @@ -module.exports = { - 'env': { - 'commonjs': true, - 'es2021': true, - 'node': true - }, - 'extends': 'eslint:recommended', - 'parserOptions': { 'ecmaVersion': 12 }, - 'rules': { - 'array-bracket-newline': [ - 'error', - 'consistent' - ], - 'array-bracket-spacing': [ - 'error', - 'never' - ], - 'array-element-newline': [ - 'error', - 'consistent' - ], - 'arrow-body-style': [ - 'error', - 'as-needed' - ], - 'arrow-parens': [ - 'error', - 'as-needed' - ], - 'block-spacing': [ - 'error', - 'always' - ], - 'brace-style': [ - 'error', - '1tbs' - ], - 'comma-dangle': [ - 'error', - 'never' - ], - 'comma-spacing': [ - 'error', - { - 'after': true, - 'before': false - } - ], - 'comma-style': [ - 'error', - 'last' - ], - 'computed-property-spacing': [ - 'error', - 'never' - ], - 'curly': [ - 'error', - 'multi-line', // 'multi' - 'consistent' - ], - 'default-case-last': [ - 'error' - ], - 'dot-location': [ - 'error', - 'property' - ], - 'dot-notation': [ - 'error' - ], - 'eqeqeq': [ - 'error' - ], - 'func-call-spacing': [ - 'error', - 'never' - ], - 'indent': [ - 'error', - 'tab' - ], - 'linebreak-style': [ - 'off', - 'windows' - ], - 'max-depth': [ - 'warn', - { 'max': 5 } - ], - 'max-len': [ - 'warn', - { - 'code': 150, - 'ignoreRegExpLiterals': true, - 'ignoreStrings': true, - 'ignoreTemplateLiterals': true, - 'ignoreTrailingComments': true, - 'ignoreUrls': true - } - ], - 'max-lines': [ - 'warn', - 500 - ], - 'max-statements-per-line': [ - 'error' - ], - 'multiline-comment-style': [ - 'warn' - ], - 'no-console': [ - 'warn' - ], - 'no-return-assign': [ - 'error' - ], - 'no-template-curly-in-string': [ - 'warn' - ], - 'no-trailing-spaces': [ - 'error' - ], - 'no-underscore-dangle': [ - 'error' - ], - 'no-unneeded-ternary': [ - 'error' - ], - 'no-var': [ - 'error' - ], - 'no-whitespace-before-property': [ - 'error' - ], - 'object-curly-newline': [ - 'error', - { - 'minProperties': 2, - 'multiline': true - } - ], - 'object-curly-spacing': [ - 'error', - 'always' - ], - 'object-property-newline': [ - 'error' - ], - 'operator-linebreak': [ - 'error' - ], - 'prefer-arrow-callback': [ - 'error' - ], - 'prefer-const': [ - 'error', - { - 'destructuring': 'all', - 'ignoreReadBeforeAssign': false - } - ], - 'quotes': [ - 'error', - 'single' - ], - 'rest-spread-spacing': [ - 'error', - 'never' - ], - 'semi': [ - 'error', - 'always' - ], - 'sort-keys': [ - 'error', - 'asc', - { 'natural': true } - ], - 'space-in-parens': [ - 'error', - 'never' - ], - 'spaced-comment': [ - 'error', - 'always' - ] - } -}; \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..f2aff2d --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,228 @@ +{ + "env": { + "browser": false, + "commonjs": false, + "es6": true, + "node": true + }, + "extends": [ + "eslint:recommended" + ], + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": [ + "unused-imports" + ], + "root": true, + "rules": { + "array-bracket-newline": [ + "error", + "consistent" + ], + "array-bracket-spacing": [ + "error", + "never" + ], + "array-element-newline": [ + "error", + "consistent" + ], + "arrow-body-style": [ + "error", + "as-needed" + ], + "arrow-parens": [ + "error", + "as-needed" + ], + "block-spacing": [ + "error", + "always" + ], + "brace-style": [ + "error", + "1tbs" + ], + "comma-dangle": [ + "error", + { + "arrays": "always-multiline", + "exports": "always-multiline", + "functions": "always-multiline", + "imports": "always-multiline", + "objects": "always-multiline" + } + ], + "comma-spacing": [ + "error", + { + "after": true, + "before": false + } + ], + "comma-style": [ + "error", + "last" + ], + "computed-property-spacing": [ + "error", + "never" + ], + "curly": [ + "error", + "multi-line", // "multi" + "consistent" + ], + "default-case-last": [ + "error" + ], + "dot-location": [ + "error", + "property" + ], + "dot-notation": [ + "error" + ], + "eqeqeq": [ + "error" + ], + "func-call-spacing": [ + "error", + "never" + ], + "indent": [ + "error", + "tab" + ], + "linebreak-style": [ + "off", + "windows" + ], + "max-depth": [ + "warn", + { + "max": 5 + } + ], + "max-len": [ + "warn", + { + "code": 180, + "ignoreRegExpLiterals": true, + "ignoreStrings": true, + "ignoreTemplateLiterals": true, + "ignoreTrailingComments": true, + "ignoreUrls": true + } + ], + "max-lines": [ + "warn" + ], + "max-statements-per-line": [ + "error" + ], + "multiline-comment-style": [ + "off" + ], + "no-console": [ + "warn" + ], + "no-prototype-builtins": [ + "off" + ], + "no-return-assign": [ + "error" + ], + "no-template-curly-in-string": [ + "warn" + ], + "no-trailing-spaces": [ + "error" + ], + "no-underscore-dangle": [ + "warn", + { + "allowAfterThis": true, + "allowFunctionParams": true + } + ], + "no-unneeded-ternary": [ + "error" + ], + "no-unused-expressions": [ + "error" + ], + "no-var": [ + "error" + ], + "no-whitespace-before-property": [ + "error" + ], + "object-curly-newline": [ + "error", + { + "minProperties": 2, + "multiline": true + } + ], + "object-curly-spacing": [ + "error", + "always" + ], + "object-property-newline": [ + "error" + ], + "operator-linebreak": [ + "error" + ], + "prefer-arrow-callback": [ + "error" + ], + "prefer-const": [ + "error", + { + "destructuring": "all", + "ignoreReadBeforeAssign": false + } + ], + "quotes": [ + "error", + "single" + ], + "rest-spread-spacing": [ + "error", + "never" + ], + "semi": [ + "error", + "always" + ], + "sort-keys": [ + "warn", + "asc", + { + "natural": true + } + ], + "space-in-parens": [ + "error", + "never" + ], + "spaced-comment": [ + "error", + "always" + ], + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "warn", + { + "vars": "all", + "varsIgnorePattern": "^_", + "args": "after-used", + "argsIgnorePattern": "^_" + } + ] + } +} diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 0921801..994b8b3 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -6,10 +6,10 @@ Release versions that will receive security updates. | Version | Supported | | ------- | --------- | -| 4.x | ✅ | -| 3.x | ✅ | -| 2.x | ❌ | | 1.x | ❌ | +| 2.x | ❌ | +| 3.x | ❌ | +| 4.x | ✅ | ## Reporting a vulnerability diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..2244b3b --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,58 @@ +name: Publish Docker image + +on: + push: + branches: + - 'main' + tags: + - 'v*' + pull_request: + branches: + - 'main' + +jobs: + push_to_registries: + name: Push Docker image to multiple registries + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - name: Check out the repo + uses: actions/checkout@v3 + + - name: Log in to Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Log in to the Container registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: | + eartharoid/discord-tickets + ghcr.io/${{ github.repository }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Build and push Docker images + uses: docker/build-push-action@v4 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 10bae39..7c0e78c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,30 +1,32 @@ -name: eslint -on: - push: - branches: - - main +name: Lint +on: [push, pull_request] + # push: + # branches: + # - main # pull_request: - # branches: - # - main + # branches: + # - main jobs: - lint: + commitlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - - name: Cache pnpm modules - uses: actions/cache@v2 - env: - cache-name: cache-pnpm-modules + - uses: actions/checkout@v3 with: - path: ~/.pnpm-store - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/package.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}- - - uses: pnpm/action-setup@v2.0.1 + fetch-depth: 0 + - uses: wagoid/commitlint-github-action@v5 with: - version: 6.3.0 - run_install: false - - run: pnpm install eslint - - run: pnpm run lint + configFile: .commitlintrc.js + eslint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + with: + version: 7 + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: 'pnpm' + - run: pnpm install + - run: pnpm run lint \ No newline at end of file diff --git a/.gitignore b/.gitignore index dcc5186..10457e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,14 @@ # directories +.history/ .vscode/ node_modules/ -logs/ -site/ +prisma/ # files -.env -version -user/config.js -user/database.sqlite -user/plugins/*/ \ No newline at end of file +*.env* +*.db* +*.log +user/config.yml +user/**/*.* +!user/**/.gitkeep +!user/templates/* \ No newline at end of file diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..c160a77 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no -- commitlint --edit ${1} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..d24fdfc --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx lint-staged diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..18ae03c --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +auto-install-peers=true +engine-strict=true \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..e4f1473 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +lts/* \ No newline at end of file diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..6320210 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,3 @@ +tickets.example.com { + reverse_proxy 127.0.0.1:8169 +} diff --git a/Dockerfile b/Dockerfile index a842521..ae5d9a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,23 @@ -# Use the alpine image of node 16 -FROM node:16-alpine +# syntax=docker/dockerfile:1 -# Create a dir for the app and make it owned by a non-root user (node) -RUN mkdir /tickets && \ - chown -R 1000:1000 /tickets -WORKDIR /tickets +FROM node:18-alpine AS builder +WORKDIR /build +# install python etc so node-gyp works for the optional dependencies +RUN apk add --no-cache make gcc g++ python3 +# install pnpm to make dependency installation faster (because it has a lockfile) +RUN npm install -g pnpm +COPY package.json pnpm-lock.yaml ./ +COPY --link scripts scripts +RUN chmod +x ./scripts/start.sh +# install dependencies, CI=true to skip pre/postinstall scripts +RUN CI=true pnpm install --prod --frozen-lockfile +COPY --link . . -# Change user to node -USER node - -# Install packages -COPY --chown=1000:1000 package.json pnpm-lock.yaml ./ -RUN npx pnpm install --prod --frozen-lockfile - -# Copy src folder -COPY src ./src - -# Set the command -CMD ["node", "src/"] +FROM node:18-alpine AS runner +ENV NODE_ENV=production \ + HTTP_HOST=0.0.0.0 \ + HTTP_PORT=80 +WORKDIR /usr/bot +COPY --from=builder /build ./ +EXPOSE ${HTTP_PORT} +ENTRYPOINT [ "/usr/bot/scripts/start.sh" ] diff --git a/README.md b/README.md index 08b5fa5..1cbdbab 100644 --- a/README.md +++ b/README.md @@ -1,244 +1,156 @@
- Discord Tickets - - An open-source ticket management bot for Discord - a free alternative to the premium and white-label plans of other popular ticketing bots. - -
-
-
-

- - GitHub stars - - - - - - All contributors + +![bots](https://img.shields.io/badge/dynamic/json?color=5865F2&label=bots&query=clients.total&url=https%3A%2F%2Fstats.discordtickets.app%2Fapi%2Fv3%2Fcurrent&logo=discord&logoColor=white&style=for-the-badge) +![tickets](https://img.shields.io/badge/dynamic/json?color=5865F2&label=tickets&query=tickets&url=https%3A%2F%2Fstats.discordtickets.app%2Fapi%2Fv3%2Fcurrent&logo=discord&logoColor=white&style=for-the-badge) +[![GitHub stars](https://img.shields.io/github/stars/discord-tickets/bot?style=for-the-badge)](https://github.com/discord-tickets/bot/stargazers) +[![Codacy](https://img.shields.io/codacy/grade/b974eb5f984c40868e07d82c968bd02d?logo=codacy&style=for-the-badge)](https://www.codacy.com/gh/discord-tickets/bot/dashboard) +[![All Contributors](https://img.shields.io/github/all-contributors/discord-tickets/bot?color=ee8449&style=for-the-badge)](https://github.com/discord-tickets/bot/blob/main/CONTRIBUTORS.md) +[![Discord](https://img.shields.io/discord/451745464480432129?label=discord&color=7289DA&style=for-the-badge)](https://lnk.earth/discord) + +
+ +![Discord Tickets](https://static.eartharoid.me/discord-tickets/logo/wordmark/gradient-by-eartharoid.png) + +**A free alternative to the premium and white-label plans of other ticket management bots.** + +
+
+Discord Tickets - A free ticketing solution | Product Hunt - - Codacy - - Discord - -

-
-
-
- - Discord Tickets - A free ticketing solution | Product Hunt - -
-
- -
--- -
- - - -
-
- Partnered with PebbleHost -
- - for affordable bot hosting - -
+[![PebbleHost](https://img.eartharoid.me/insecure/rs:auto:180/plain/s3://eartharoid/sharex/21/10/pebblehost.webp)](https://pebble.host/discordtickets) + +Partnered with PebbleHost +
+for affordable bot hosting ---
+ + +## 📖 Contents + +- [📖 Contents](#-contents) +- [✨ Features](#-features) + - [Want to learn more?](#want-to-learn-more) +- [⚡ Getting started](#-getting-started) +- [🤑 Sponsors](#-sponsors) +- [🎖️ Contributors](#️-contributors) + - [💻 Contributing](#-contributing) + - [🌎 Translating](#-translating) +- [😕 Support](#-support) +- [⭐ Star History](#-star-history) +- [🥱 License](#-license) -## What is this? +## ✨ Features -Discord Tickets is a Discord bot for creating and managing ticket channels. It is a free and open-source alternative to the paid "premium" and "white-label" plans of popular ticketing bots, such as [Ticket Tool](https://tickettool.xyz/), [TicketsBot](https://ticketsbot.net/), [Tickety](https://tickety.net/), [Helper.gg](https://helper.gg/), [Helper](https://helper.wtf), and others. +- 📖 [**Documentation**](https://discordtickets.app/getting-started/) - comprehensive documentation and guides to help you get started +- ⚙️ **Simple settings** - configure your bot with the included and easy-to-use dashboard +- 🎨 **Highly customisable** - tweak features, colours, messages, and more to your liking +- 🛸 **Modern features** - including slash commands, buttons, select menus, and modals +- 🤖 **Automation** - ease your staff team's workload with configurable automation + - 🏷️ [**Tags**](https://v4--discordtickets.netlify.app/features/#tags) - resolve members' problems without escalating to tickets + - 🎫 **Tickets** - close inactive tickets automatically +- 📜 **Archiving** - store messages in the database and view transcripts later +- ❓ **Context** - ask for a topic or up to 5 custom questions before creating a ticket, and see references to a message or previous ticket at a glance +- 🗃️ **Organisation** - claim, release, move and transfer tickets between members and categories +- 🌎 [**Internationalisation**](#-translating) - available in more than 10 languages +- ⏱️ **Statistics** - analyse your staff members' performance +- 🪓 **Battle-tested** - trusted by thousands of servers, with over [half a million tickets](https://stats.discordtickets.app/) created since 2019 +- 🐳 [**Docker**](https://discordtickets.app/self-hosting/installation/docker/) - reliable and quick deployment with Docker -Discord Tickets is feature-rich and much more customisable than many of the bots mentioned above. As it is intended for self-hosting, the bot can have your community/company's logo, for free. +[*...and more!*](https://discordtickets.app/features/) -Although intended for use in a single Discord server, the bot can also function in multiple servers at once if you run more than one community. +### Want to learn more? -### Features +**Visit [the website](https://discordtickets.app/) for more features, details, and screenshots**, +or skip to the [full feature tour](https://discordtickets.app/features/). -Discord Tickets is packed full of features, many of which were suggested by its users. If it's missing a feature you want, you can: +## ⚡ Getting started -- Create a plugin for it, if you can code JavaScript -- Request someone else to make a plugin -- [Submit a feature request](https://github.com/discord-tickets/.github/blob/main//CONTRIBUTING.md#submitting-a-feature-request) if you think many other users would benefit from it +> *🙏 Please read the [documentation](https://discordtickets.app/self-hosting/installation/) before you start.* -Here's some of the things that makes Discord Tickets awesome: +There are 3 ways to get started with Discord Tickets: -1. **Highly customisable** -Some messages can be configured for each server and for each ticket category. Every other message is set in the locale files, making it relatively easy to override the default messages. -You can also configure the functionality of the bot to your liking and add commands with plugins. +- ☁️ [Add the public bot](https://discordtickets.app/public/) - instant setup, but branded *(great for testing)* +- 😴 [Get a managed bot](https://discordtickets.app/managed/) - your own bot, hosted by me *(easy and cheap)* +- 🧑‍💻 [Install your own bot](https://discordtickets.app/self-hosting/) - host it yourself *(experience recommended)* -2. **Localisable** -If the bot hasn't already been translated to your (community's) language, you can [translate](https://github.com/discord-tickets/.github/blob/main//CONTRIBUTING.md#translating) it yourself. -Plugin authors are encouraged to support multiple languages as well. +**[Read the documentation](https://discordtickets.app/getting-started/)** for more information. +> This button is here because it looks nice, but I will cry if you click it before reading the [documentation](https://discordtickets.app/getting-started/). :) -
- Translation status - - Weblate - -
+[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/eB6TkX?referralCode=Z3aYd2) -3. **Multiple ticket categories** -Each ticket category has its own settings for messages and the support team roles. There's also multiple methods of creating a ticket. + -4. **A beautiful ticket archives portal** -**\[COMING SOON\] :** Add the official [Discord Tickets Portal](https://github.com/discord-tickets/portal) plugin for an instant ticket archives website. +## 🤑 Sponsors - You can use [text transcripts](https://discordtickets.app/plugins/official/text-transcripts/) whilst you wait for the portal. +Discord Tickets is made possible by these awesome people and organisations: -5. **Open-source and self-hosted** -It's yours, you have full control. +![Sponsors](https://cdn.jsdelivr.net/gh/eartharoid/sponsors/sponsorkit/sponsors.svg) +Please consider sponsoring the project if it adds value to your business/community. -## Getting started - -| [**Host it yourself**](https://discordtickets.app/getting-started) | [**Managed bot**](https://discordtickets.app/getting-started#managed-hosting) | [**Public test bot**](https://discord.com/oauth2/authorize?permissions=8&scope=applications.commands%20bot&client_id=475371285531066368) | -|:-:|:-:|:-:| -| Recommended if you have some experience. | Recommended if you want a bot without hosting it. | Try out the bot for a few days. | -| [Go to the docs »](https://discordtickets.app/getting-started) | [Learn more »](https://discordtickets.app/getting-started#managed-hosting) | [Add to Discord »](https://discord.com/oauth2/authorize?permissions=8&scope=applications.commands%20bot&client_id=475371285531066368) | - -Discord Tickets is partnered with [PebbleHost](https://discordtickets.app/getting-started#pebblehost). Click on the logo below if you want to self host but you don't have a server. - -[![PebbleHost](https://img.eartharoid.me/insecure/rs:auto:180/plain/s3://eartharoid/sharex/21/10/pebblehost.webp)](https://discordtickets.app/getting-started#pebblehost) - -## Documentation - -You will find most of information you need at [discordtickets.app](https://discordtickets.app). - -## Support - -If the [documentation](https://discordtickets.app) leaves you with questions, you can ask for help in the [discussions](https://github.com/discord-tickets/bot/discussions/categories/support-q-a) or join the support server on Discord. - -[![Join the Discord server](https://img.eartharoid.me/insecure/rs:auto:440:200/plain/s3://eartharoid/images/join-discord.png@png)](https://lnk.earth/discord) - -## Contributing - -For contributing instructions, or to find out all of the ways you can contribute, read [CONTRIBUTING.md](https://github.com/discord-tickets/.github/blob/main//CONTRIBUTING.md). All contributions are welcome and encouraged, but please [read the information](https://github.com/discord-tickets/.github/blob/main//CONTRIBUTING.md) given before doing so. - -## Contributors - -Thank you to everyone to has contributed to Discord Tickets, including everyone who has: - -- Contributed code -- Translated -- Improved documentation -- Reported bugs -- Requested a feature -- Given help or support to other users -- Created a public plugin -- Created resources such as tutorials - -**A full list of contributors can be found in [CONTRIBUTORS.md](https://github.com/discord-tickets/bot/blob/main/CONTRIBUTORS.md).** - -## Sponsors - -*Does your community or company use Discord Tickets? [Sponsor the project](https://github.com/discord-tickets/bot/?sponsor=1) to get your logo shown here.* - -**These awesome people and communities sponsor Discord Tickets:** - - - - - - - - - -
- - -
- reSkybounds -
-
- - -
- DarkHosting™️ -
-
- - -
- Simply Vanilla -
-
- - -
- URHOST -
-
- - -
- SunriseNode -
-
- - -### Donate +**[Sponsor on GitHub](https://github.com/discord-tickets/bot/?sponsor=1)**, or [![Donate at ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/eartharoid) -## Star History +> **Note** +> +> Your logo will only appear here if you sponsor through GitHub Sponsors. +> [Create an organisation](https://github.com/account/organizations/new?plan=free) if you want to use your business/community logo. -[![Star History Chart](https://api.star-history.com/svg?repos=discord-tickets/bot&type=Date)](https://star-history.com/#discord-tickets/bot&Date) +## 🎖️ Contributors + + + +Discord Tickets is made possible by all of the people listed in [CONTRIBUTORS.md](https://github.com/discord-tickets/bot/blob/main/CONTRIBUTORS.md). -## License +### 💻 Contributing -Discord Tickets is licensed under the [GPLv3 license](https://github.com/discord-tickets/bot/blob/main/LICENSE). +If you want to help translate, suggest a feature, submit a bug report, +or contribute in any other way, please read the [contributing guidelines](https://github.com/discord-tickets/.github/blob/main//CONTRIBUTING.md). + +> **Note** +> You can add yourself to the list of contributors [here](https://github.com/discord-tickets/bot/issues/new/choose). + +#### 🌎 Translating + +[![Translation status](https://hosted.weblate.org/widgets/discord-tickets/-/open-graph.png)](https://hosted.weblate.org/engage/discord-tickets/) + +## 😕 Support + +[![Discord](https://discordapp.com/api/guilds/451745464480432129/widget.png?style=banner4)](https://lnk.earth/discord) + +## ⭐ Star History + +
+ Show graph + + [![Star History Chart](https://api.star-history.com/svg?repos=discord-tickets/bot&type=Date)](https://star-history.com/#discord-tickets/bot&Date) + +
+ +## 🥱 License + +Discord Tickets by eartharoid™️ is licensed under the [GPLv3 license](https://github.com/discord-tickets/bot/blob/main/LICENSE). This is not an official Discord product. It is not affiliated with nor endorsed by Discord Inc. -© 2021 Isaac Saunders +© 2023 Isaac Saunders diff --git a/compose.Dockerfile b/compose.Dockerfile deleted file mode 100644 index 2003f24..0000000 --- a/compose.Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# Use the alpine image of node 16 -FROM node:16-alpine - -# Create a dir for the app and make it owned by a non-root user (node) -RUN mkdir /tickets && \ - chown -R 1000:1000 /tickets -WORKDIR /tickets - -# Change user to node -USER node - -# Install packages -COPY --chown=1000:1000 package.json pnpm-lock.yaml ./ -RUN npx pnpm install --prod --frozen-lockfile --no-optional && \ - # Currently WIP since pnpm installs dev deps automatically when I don't want it to. - # Quick fix is to add to main deps - npx pnpm install mysql2 - -# Set the command -CMD ["node", "src/"] diff --git a/db/mysql/migrations/20230309144248_4_0_0/migration.sql b/db/mysql/migrations/20230309144248_4_0_0/migration.sql new file mode 100644 index 0000000..498761e --- /dev/null +++ b/db/mysql/migrations/20230309144248_4_0_0/migration.sql @@ -0,0 +1,257 @@ +-- CreateTable +CREATE TABLE `archivedChannels` ( + `channelId` VARCHAR(19) NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `name` VARCHAR(191) NOT NULL, + `ticketId` VARCHAR(19) NOT NULL, + + UNIQUE INDEX `archivedChannels_ticketId_channelId_key`(`ticketId`, `channelId`), + PRIMARY KEY (`ticketId`, `channelId`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `archivedMessages` ( + `authorId` VARCHAR(19) NOT NULL, + `content` TEXT NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `deleted` BOOLEAN NOT NULL DEFAULT false, + `edited` BOOLEAN NOT NULL DEFAULT false, + `external` BOOLEAN NOT NULL DEFAULT false, + `id` VARCHAR(19) NOT NULL, + `ticketId` VARCHAR(19) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `archivedRoles` ( + `colour` CHAR(6) NOT NULL DEFAULT '5865F2', + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `name` VARCHAR(191) NOT NULL, + `roleId` VARCHAR(19) NOT NULL, + `ticketId` VARCHAR(19) NOT NULL, + + UNIQUE INDEX `archivedRoles_ticketId_roleId_key`(`ticketId`, `roleId`), + PRIMARY KEY (`ticketId`, `roleId`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `archivedUsers` ( + `avatar` VARCHAR(191) NULL, + `bot` BOOLEAN NOT NULL DEFAULT false, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `discriminator` CHAR(4) NULL, + `displayName` TEXT NULL, + `roleId` VARCHAR(19) NULL, + `ticketId` VARCHAR(19) NOT NULL, + `userId` VARCHAR(19) NOT NULL, + `username` TEXT NULL, + + UNIQUE INDEX `archivedUsers_ticketId_userId_key`(`ticketId`, `userId`), + PRIMARY KEY (`ticketId`, `userId`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `categories` ( + `channelName` VARCHAR(191) NOT NULL, + `claiming` BOOLEAN NOT NULL DEFAULT false, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `cooldown` INTEGER NULL, + `customTopic` VARCHAR(191) NULL, + `description` VARCHAR(191) NOT NULL, + `discordCategory` VARCHAR(19) NOT NULL, + `emoji` VARCHAR(191) NOT NULL, + `enableFeedback` BOOLEAN NOT NULL DEFAULT false, + `guildId` VARCHAR(19) NOT NULL, + `id` INTEGER NOT NULL AUTO_INCREMENT, + `image` VARCHAR(191) NULL, + `memberLimit` INTEGER NOT NULL DEFAULT 1, + `name` VARCHAR(191) NOT NULL, + `openingMessage` TEXT NOT NULL, + `pingRoles` JSON NOT NULL, + `ratelimit` INTEGER NULL, + `requiredRoles` JSON NOT NULL, + `requireTopic` BOOLEAN NOT NULL DEFAULT false, + `staffRoles` JSON NOT NULL, + `totalLimit` INTEGER NOT NULL DEFAULT 50, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `feedback` ( + `comment` TEXT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `guildId` VARCHAR(19) NOT NULL, + `rating` INTEGER NOT NULL, + `ticketId` VARCHAR(19) NOT NULL, + `userId` VARCHAR(19) NULL, + + PRIMARY KEY (`ticketId`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `guilds` ( + `autoClose` INTEGER NOT NULL DEFAULT 43200000, + `autoTag` JSON NOT NULL, + `archive` BOOLEAN NOT NULL DEFAULT true, + `blocklist` JSON NOT NULL, + `claimButton` BOOLEAN NOT NULL DEFAULT false, + `closeButton` BOOLEAN NOT NULL DEFAULT false, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `errorColour` VARCHAR(191) NOT NULL DEFAULT 'Red', + `footer` VARCHAR(191) NULL DEFAULT 'Discord Tickets by eartharoid', + `id` VARCHAR(19) NOT NULL, + `locale` VARCHAR(191) NOT NULL DEFAULT 'en-GB', + `logChannel` VARCHAR(19) NULL, + `primaryColour` VARCHAR(191) NOT NULL DEFAULT '#009999', + `staleAfter` INTEGER NULL, + `successColour` VARCHAR(191) NOT NULL DEFAULT 'Green', + `workingHours` JSON NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `questions` ( + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `id` VARCHAR(191) NOT NULL, + `categoryId` INTEGER NOT NULL, + `label` VARCHAR(45) NOT NULL, + `maxLength` INTEGER NULL DEFAULT 4000, + `minLength` INTEGER NULL DEFAULT 0, + `options` JSON NOT NULL, + `order` INTEGER NOT NULL, + `placeholder` VARCHAR(100) NULL, + `required` BOOLEAN NOT NULL DEFAULT true, + `style` INTEGER NOT NULL DEFAULT 2, + `type` ENUM('MENU', 'TEXT') NOT NULL DEFAULT 'TEXT', + `value` TEXT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `questionAnswers` ( + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `id` INTEGER NOT NULL AUTO_INCREMENT, + `ticketId` VARCHAR(19) NOT NULL, + `questionId` VARCHAR(191) NOT NULL, + `userId` VARCHAR(19) NOT NULL, + `value` TEXT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `tags` ( + `content` TEXT NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `guildId` VARCHAR(19) NOT NULL, + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + `regex` VARCHAR(191) NULL, + + UNIQUE INDEX `tags_guildId_name_key`(`guildId`, `name`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `tickets` ( + `categoryId` INTEGER NULL, + `claimedById` VARCHAR(19) NULL, + `closedAt` DATETIME(3) NULL, + `closedById` VARCHAR(19) NULL, + `closedReason` TEXT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `createdById` VARCHAR(19) NOT NULL, + `deleted` BOOLEAN NOT NULL DEFAULT false, + `firstResponseAt` DATETIME(3) NULL, + `guildId` VARCHAR(19) NOT NULL, + `id` VARCHAR(19) NOT NULL, + `lastMessageAt` DATETIME(3) NULL, + `messageCount` INTEGER NULL, + `number` INTEGER NOT NULL, + `open` BOOLEAN NOT NULL DEFAULT true, + `openingMessageId` VARCHAR(19) NOT NULL, + `pinnedMessageIds` JSON NOT NULL, + `priority` ENUM('LOW', 'MEDIUM', 'HIGH') NULL, + `referencesMessageId` VARCHAR(19) NULL, + `referencesTicketId` VARCHAR(19) NULL, + `topic` TEXT NULL, + + UNIQUE INDEX `tickets_guildId_number_key`(`guildId`, `number`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `users` ( + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `id` VARCHAR(19) NOT NULL, + `messageCount` INTEGER NOT NULL DEFAULT 0, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `archivedChannels` ADD CONSTRAINT `archivedChannels_ticketId_fkey` FOREIGN KEY (`ticketId`) REFERENCES `tickets`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `archivedMessages` ADD CONSTRAINT `archivedMessages_ticketId_authorId_fkey` FOREIGN KEY (`ticketId`, `authorId`) REFERENCES `archivedUsers`(`ticketId`, `userId`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `archivedMessages` ADD CONSTRAINT `archivedMessages_ticketId_fkey` FOREIGN KEY (`ticketId`) REFERENCES `tickets`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `archivedRoles` ADD CONSTRAINT `archivedRoles_ticketId_fkey` FOREIGN KEY (`ticketId`) REFERENCES `tickets`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `archivedUsers` ADD CONSTRAINT `archivedUsers_ticketId_roleId_fkey` FOREIGN KEY (`ticketId`, `roleId`) REFERENCES `archivedRoles`(`ticketId`, `roleId`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `archivedUsers` ADD CONSTRAINT `archivedUsers_ticketId_fkey` FOREIGN KEY (`ticketId`) REFERENCES `tickets`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `categories` ADD CONSTRAINT `categories_guildId_fkey` FOREIGN KEY (`guildId`) REFERENCES `guilds`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `feedback` ADD CONSTRAINT `feedback_guildId_fkey` FOREIGN KEY (`guildId`) REFERENCES `guilds`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `feedback` ADD CONSTRAINT `feedback_ticketId_fkey` FOREIGN KEY (`ticketId`) REFERENCES `tickets`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `feedback` ADD CONSTRAINT `feedback_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `questions` ADD CONSTRAINT `questions_categoryId_fkey` FOREIGN KEY (`categoryId`) REFERENCES `categories`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `questionAnswers` ADD CONSTRAINT `questionAnswers_ticketId_fkey` FOREIGN KEY (`ticketId`) REFERENCES `tickets`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `questionAnswers` ADD CONSTRAINT `questionAnswers_questionId_fkey` FOREIGN KEY (`questionId`) REFERENCES `questions`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `questionAnswers` ADD CONSTRAINT `questionAnswers_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `tags` ADD CONSTRAINT `tags_guildId_fkey` FOREIGN KEY (`guildId`) REFERENCES `guilds`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `tickets` ADD CONSTRAINT `tickets_categoryId_fkey` FOREIGN KEY (`categoryId`) REFERENCES `categories`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `tickets` ADD CONSTRAINT `tickets_claimedById_fkey` FOREIGN KEY (`claimedById`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `tickets` ADD CONSTRAINT `tickets_closedById_fkey` FOREIGN KEY (`closedById`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `tickets` ADD CONSTRAINT `tickets_createdById_fkey` FOREIGN KEY (`createdById`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `tickets` ADD CONSTRAINT `tickets_guildId_fkey` FOREIGN KEY (`guildId`) REFERENCES `guilds`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `tickets` ADD CONSTRAINT `tickets_referencesTicketId_fkey` FOREIGN KEY (`referencesTicketId`) REFERENCES `tickets`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/db/mysql/migrations/migration_lock.toml b/db/mysql/migrations/migration_lock.toml new file mode 100644 index 0000000..e5a788a --- /dev/null +++ b/db/mysql/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "mysql" \ No newline at end of file diff --git a/db/mysql/schema.prisma b/db/mysql/schema.prisma new file mode 100644 index 0000000..305dffb --- /dev/null +++ b/db/mysql/schema.prisma @@ -0,0 +1,248 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DB_CONNECTION_URL") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") +} + +model ArchivedChannel { + channelId String @db.VarChar(19) + createdAt DateTime @default(now()) + name String + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + + @@id([ticketId, channelId]) + @@unique([ticketId, channelId]) + @@map("archivedChannels") +} + +model ArchivedMessage { + author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade) + authorId String @db.VarChar(19) + content String @db.Text + createdAt DateTime @default(now()) + deleted Boolean @default(false) + edited Boolean @default(false) + external Boolean @default(false) + id String @id @db.VarChar(19) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + + @@map("archivedMessages") +} + +model ArchivedRole { + archivedUsers ArchivedUser[] + colour String @default("5865F2") @db.Char(6) // 7289DA + createdAt DateTime @default(now()) + name String + roleId String @db.VarChar(19) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + + @@id([ticketId, roleId]) + @@unique([ticketId, roleId]) + @@map("archivedRoles") +} + +model ArchivedUser { + archivedMessages ArchivedMessage[] + avatar String? + bot Boolean @default(false) + createdAt DateTime @default(now()) + discriminator String? @db.Char(4) + displayName String? @db.Text + role ArchivedRole? @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) + roleId String? @db.VarChar(19) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + userId String @db.VarChar(19) + username String? @db.Text + + @@id([ticketId, userId]) + @@unique([ticketId, userId]) + @@map("archivedUsers") +} + +model Category { + channelName String + claiming Boolean @default(false) + createdAt DateTime @default(now()) + cooldown Int? + customTopic String? + description String + discordCategory String @db.VarChar(19) + emoji String + enableFeedback Boolean @default(false) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id Int @id @default(autoincrement()) + image String? + memberLimit Int @default(1) + name String + openingMessage String @db.Text + pingRoles Json @default("[]") + questions Question[] + ratelimit Int? + requiredRoles Json @default("[]") + requireTopic Boolean @default(false) + staffRoles Json + tickets Ticket[] + totalLimit Int @default(50) + + @@map("categories") +} + +model Feedback { + comment String? @db.Text + createdAt DateTime @default(now()) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + rating Int + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @id @db.VarChar(19) + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String? @db.VarChar(19) + + @@map("feedback") +} + +model Guild { + autoClose Int @default(43200000) + autoTag Json @default("[]") + archive Boolean @default(true) + blocklist Json @default("[]") + categories Category[] + claimButton Boolean @default(false) + closeButton Boolean @default(false) + createdAt DateTime @default(now()) + errorColour String @default("Red") + feedback Feedback[] + footer String? @default("Discord Tickets by eartharoid") + id String @id @db.VarChar(19) + locale String @default("en-GB") + logChannel String? @db.VarChar(19) + primaryColour String @default("#009999") + staleAfter Int? + successColour String @default("Green") + tags Tag[] + tickets Ticket[] + workingHours Json @default("[\"UTC\", [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"]]") + + @@map("guilds") +} + +model Question { + answers QuestionAnswer[] + createdAt DateTime @default(now()) + id String @id @default(uuid()) + category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) + categoryId Int + label String @db.VarChar(45) + maxLength Int? @default(4000) + minLength Int? @default(0) + options Json @default("[]") + order Int + placeholder String? @db.VarChar(100) + required Boolean @default(true) + style Int @default(2) + type QuestionType @default(TEXT) + value String? @db.Text + + @@map("questions") +} + +model QuestionAnswer { + createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + question Question @relation(fields: [questionId], references: [id], onDelete: Cascade) + questionId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String @db.VarChar(19) + value String? @db.Text + + @@map("questionAnswers") +} + +model Tag { + content String @db.Text + createdAt DateTime @default(now()) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id Int @id @default(autoincrement()) + name String + regex String? + + @@unique([guildId, name]) + @@map("tags") +} + +model Ticket { + archivedChannels ArchivedChannel[] + archivedMessages ArchivedMessage[] + archivedRoles ArchivedRole[] + archivedUsers ArchivedUser[] + category Category? @relation(fields: [categoryId], references: [id], onDelete: Cascade) + categoryId Int? + claimedBy User? @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id]) + claimedById String? @db.VarChar(19) + closedAt DateTime? + closedBy User? @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id]) + closedById String? @db.VarChar(19) + closedReason String? @db.Text + createdAt DateTime @default(now()) + createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id]) + createdById String @db.VarChar(19) + deleted Boolean @default(false) + feedback Feedback? + firstResponseAt DateTime? + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id String @id @db.VarChar(19) + lastMessageAt DateTime? + messageCount Int? + number Int + open Boolean @default(true) + openingMessageId String @db.VarChar(19) + pinnedMessageIds Json @default("[]") + priority TicketPriority? + referencedBy Ticket[] @relation("TicketsReferencedByTicket") + referencesMessageId String? @db.VarChar(19) + referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull) + referencesTicketId String? @db.VarChar(19) + topic String? @db.Text + questionAnswers QuestionAnswer[] + + @@unique([guildId, number]) + @@map("tickets") +} + +model User { + createdAt DateTime @default(now()) + feedback Feedback[] + id String @id @db.VarChar(19) + messageCount Int @default(0) + ticketsCreated Ticket[] @relation("TicketsCreatedByUser") + ticketsClosed Ticket[] @relation("TicketsClosedByUser") + ticketsClaimed Ticket[] @relation("TicketsClaimedByUser") + questionAnswers QuestionAnswer[] + + @@map("users") +} + +enum TicketPriority { + LOW + MEDIUM + HIGH +} + +enum QuestionType { + MENU + TEXT +} diff --git a/db/postgresql/migrations/20230309132703_4_0_0/migration.sql b/db/postgresql/migrations/20230309132703_4_0_0/migration.sql new file mode 100644 index 0000000..062c17a --- /dev/null +++ b/db/postgresql/migrations/20230309132703_4_0_0/migration.sql @@ -0,0 +1,273 @@ +-- CreateEnum +CREATE TYPE "TicketPriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH'); + +-- CreateEnum +CREATE TYPE "QuestionType" AS ENUM ('MENU', 'TEXT'); + +-- CreateTable +CREATE TABLE "archivedChannels" ( + "channelId" VARCHAR(19) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "name" TEXT NOT NULL, + "ticketId" VARCHAR(19) NOT NULL, + + CONSTRAINT "archivedChannels_pkey" PRIMARY KEY ("ticketId","channelId") +); + +-- CreateTable +CREATE TABLE "archivedMessages" ( + "authorId" VARCHAR(19) NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" BOOLEAN NOT NULL DEFAULT false, + "edited" BOOLEAN NOT NULL DEFAULT false, + "external" BOOLEAN NOT NULL DEFAULT false, + "id" VARCHAR(19) NOT NULL, + "ticketId" VARCHAR(19) NOT NULL, + + CONSTRAINT "archivedMessages_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "archivedRoles" ( + "colour" CHAR(6) NOT NULL DEFAULT '5865F2', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "name" TEXT NOT NULL, + "roleId" VARCHAR(19) NOT NULL, + "ticketId" VARCHAR(19) NOT NULL, + + CONSTRAINT "archivedRoles_pkey" PRIMARY KEY ("ticketId","roleId") +); + +-- CreateTable +CREATE TABLE "archivedUsers" ( + "avatar" TEXT, + "bot" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "discriminator" CHAR(4), + "displayName" TEXT, + "roleId" VARCHAR(19), + "ticketId" VARCHAR(19) NOT NULL, + "userId" VARCHAR(19) NOT NULL, + "username" TEXT, + + CONSTRAINT "archivedUsers_pkey" PRIMARY KEY ("ticketId","userId") +); + +-- CreateTable +CREATE TABLE "categories" ( + "channelName" TEXT NOT NULL, + "claiming" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "cooldown" INTEGER, + "customTopic" TEXT, + "description" TEXT NOT NULL, + "discordCategory" VARCHAR(19) NOT NULL, + "emoji" TEXT NOT NULL, + "enableFeedback" BOOLEAN NOT NULL DEFAULT false, + "guildId" VARCHAR(19) NOT NULL, + "id" SERIAL NOT NULL, + "image" TEXT, + "memberLimit" INTEGER NOT NULL DEFAULT 1, + "name" TEXT NOT NULL, + "openingMessage" TEXT NOT NULL, + "pingRoles" JSONB NOT NULL DEFAULT '[]', + "ratelimit" INTEGER, + "requiredRoles" JSONB NOT NULL DEFAULT '[]', + "requireTopic" BOOLEAN NOT NULL DEFAULT false, + "staffRoles" JSONB NOT NULL, + "totalLimit" INTEGER NOT NULL DEFAULT 50, + + CONSTRAINT "categories_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "feedback" ( + "comment" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "guildId" VARCHAR(19) NOT NULL, + "rating" INTEGER NOT NULL, + "ticketId" VARCHAR(19) NOT NULL, + "userId" VARCHAR(19), + + CONSTRAINT "feedback_pkey" PRIMARY KEY ("ticketId") +); + +-- CreateTable +CREATE TABLE "guilds" ( + "autoClose" INTEGER NOT NULL DEFAULT 43200000, + "autoTag" JSONB NOT NULL DEFAULT '[]', + "archive" BOOLEAN NOT NULL DEFAULT true, + "blocklist" JSONB NOT NULL DEFAULT '[]', + "claimButton" BOOLEAN NOT NULL DEFAULT false, + "closeButton" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "errorColour" TEXT NOT NULL DEFAULT 'Red', + "footer" TEXT DEFAULT 'Discord Tickets by eartharoid', + "id" VARCHAR(19) NOT NULL, + "locale" TEXT NOT NULL DEFAULT 'en-GB', + "logChannel" VARCHAR(19), + "primaryColour" TEXT NOT NULL DEFAULT '#009999', + "staleAfter" INTEGER, + "successColour" TEXT NOT NULL DEFAULT 'Green', + "workingHours" JSONB NOT NULL DEFAULT '["UTC", ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"]]', + + CONSTRAINT "guilds_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "questions" ( + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "id" TEXT NOT NULL, + "categoryId" INTEGER NOT NULL, + "label" VARCHAR(45) NOT NULL, + "maxLength" INTEGER DEFAULT 4000, + "minLength" INTEGER DEFAULT 0, + "options" JSONB NOT NULL DEFAULT '[]', + "order" INTEGER NOT NULL, + "placeholder" VARCHAR(100), + "required" BOOLEAN NOT NULL DEFAULT true, + "style" INTEGER NOT NULL DEFAULT 2, + "type" "QuestionType" NOT NULL DEFAULT 'TEXT', + "value" TEXT, + + CONSTRAINT "questions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "questionAnswers" ( + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "id" SERIAL NOT NULL, + "ticketId" VARCHAR(19) NOT NULL, + "questionId" TEXT NOT NULL, + "userId" VARCHAR(19) NOT NULL, + "value" TEXT, + + CONSTRAINT "questionAnswers_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "tags" ( + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "guildId" VARCHAR(19) NOT NULL, + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "regex" TEXT, + + CONSTRAINT "tags_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "tickets" ( + "categoryId" INTEGER, + "claimedById" VARCHAR(19), + "closedAt" TIMESTAMP(3), + "closedById" VARCHAR(19), + "closedReason" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdById" VARCHAR(19) NOT NULL, + "deleted" BOOLEAN NOT NULL DEFAULT false, + "firstResponseAt" TIMESTAMP(3), + "guildId" VARCHAR(19) NOT NULL, + "id" VARCHAR(19) NOT NULL, + "lastMessageAt" TIMESTAMP(3), + "messageCount" INTEGER, + "number" INTEGER NOT NULL, + "open" BOOLEAN NOT NULL DEFAULT true, + "openingMessageId" VARCHAR(19) NOT NULL, + "pinnedMessageIds" JSONB NOT NULL DEFAULT '[]', + "priority" "TicketPriority", + "referencesMessageId" VARCHAR(19), + "referencesTicketId" VARCHAR(19), + "topic" TEXT, + + CONSTRAINT "tickets_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "users" ( + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "id" VARCHAR(19) NOT NULL, + "messageCount" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "archivedChannels_ticketId_channelId_key" ON "archivedChannels"("ticketId", "channelId"); + +-- CreateIndex +CREATE UNIQUE INDEX "archivedRoles_ticketId_roleId_key" ON "archivedRoles"("ticketId", "roleId"); + +-- CreateIndex +CREATE UNIQUE INDEX "archivedUsers_ticketId_userId_key" ON "archivedUsers"("ticketId", "userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "tags_guildId_name_key" ON "tags"("guildId", "name"); + +-- CreateIndex +CREATE UNIQUE INDEX "tickets_guildId_number_key" ON "tickets"("guildId", "number"); + +-- AddForeignKey +ALTER TABLE "archivedChannels" ADD CONSTRAINT "archivedChannels_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "archivedMessages" ADD CONSTRAINT "archivedMessages_ticketId_authorId_fkey" FOREIGN KEY ("ticketId", "authorId") REFERENCES "archivedUsers"("ticketId", "userId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "archivedMessages" ADD CONSTRAINT "archivedMessages_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "archivedRoles" ADD CONSTRAINT "archivedRoles_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "archivedUsers" ADD CONSTRAINT "archivedUsers_ticketId_roleId_fkey" FOREIGN KEY ("ticketId", "roleId") REFERENCES "archivedRoles"("ticketId", "roleId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "archivedUsers" ADD CONSTRAINT "archivedUsers_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "categories" ADD CONSTRAINT "categories_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "feedback" ADD CONSTRAINT "feedback_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "feedback" ADD CONSTRAINT "feedback_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "feedback" ADD CONSTRAINT "feedback_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "questions" ADD CONSTRAINT "questions_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "categories"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "questionAnswers" ADD CONSTRAINT "questionAnswers_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "questionAnswers" ADD CONSTRAINT "questionAnswers_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "questions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "questionAnswers" ADD CONSTRAINT "questionAnswers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tags" ADD CONSTRAINT "tags_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tickets" ADD CONSTRAINT "tickets_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "categories"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tickets" ADD CONSTRAINT "tickets_claimedById_fkey" FOREIGN KEY ("claimedById") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tickets" ADD CONSTRAINT "tickets_closedById_fkey" FOREIGN KEY ("closedById") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tickets" ADD CONSTRAINT "tickets_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tickets" ADD CONSTRAINT "tickets_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tickets" ADD CONSTRAINT "tickets_referencesTicketId_fkey" FOREIGN KEY ("referencesTicketId") REFERENCES "tickets"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/db/postgresql/migrations/migration_lock.toml b/db/postgresql/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/db/postgresql/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma new file mode 100644 index 0000000..7e7841b --- /dev/null +++ b/db/postgresql/schema.prisma @@ -0,0 +1,247 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DB_CONNECTION_URL") +} + +model ArchivedChannel { + channelId String @db.VarChar(19) + createdAt DateTime @default(now()) + name String + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + + @@id([ticketId, channelId]) + @@unique([ticketId, channelId]) + @@map("archivedChannels") +} + +model ArchivedMessage { + author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade) + authorId String @db.VarChar(19) + content String @db.Text + createdAt DateTime @default(now()) + deleted Boolean @default(false) + edited Boolean @default(false) + external Boolean @default(false) + id String @id @db.VarChar(19) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + + @@map("archivedMessages") +} + +model ArchivedRole { + archivedUsers ArchivedUser[] + colour String @default("5865F2") @db.Char(6) // 7289DA + createdAt DateTime @default(now()) + name String + roleId String @db.VarChar(19) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + + @@id([ticketId, roleId]) + @@unique([ticketId, roleId]) + @@map("archivedRoles") +} + +model ArchivedUser { + archivedMessages ArchivedMessage[] + avatar String? + bot Boolean @default(false) + createdAt DateTime @default(now()) + discriminator String? @db.Char(4) + displayName String? @db.Text + role ArchivedRole? @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) + roleId String? @db.VarChar(19) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + userId String @db.VarChar(19) + username String? @db.Text + + @@id([ticketId, userId]) + @@unique([ticketId, userId]) + @@map("archivedUsers") +} + +model Category { + channelName String + claiming Boolean @default(false) + createdAt DateTime @default(now()) + cooldown Int? + customTopic String? + description String + discordCategory String @db.VarChar(19) + emoji String + enableFeedback Boolean @default(false) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id Int @id @default(autoincrement()) + image String? + memberLimit Int @default(1) + name String + openingMessage String @db.Text + pingRoles Json @default("[]") + questions Question[] + ratelimit Int? + requiredRoles Json @default("[]") + requireTopic Boolean @default(false) + staffRoles Json + tickets Ticket[] + totalLimit Int @default(50) + + @@map("categories") +} + +model Feedback { + comment String? @db.Text + createdAt DateTime @default(now()) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + rating Int + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @id @db.VarChar(19) + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String? @db.VarChar(19) + + @@map("feedback") +} + +model Guild { + autoClose Int @default(43200000) + autoTag Json @default("[]") + archive Boolean @default(true) + blocklist Json @default("[]") + categories Category[] + claimButton Boolean @default(false) + closeButton Boolean @default(false) + createdAt DateTime @default(now()) + errorColour String @default("Red") + feedback Feedback[] + footer String? @default("Discord Tickets by eartharoid") + id String @id @db.VarChar(19) + locale String @default("en-GB") + logChannel String? @db.VarChar(19) + primaryColour String @default("#009999") + staleAfter Int? + successColour String @default("Green") + tags Tag[] + tickets Ticket[] + workingHours Json @default("[\"UTC\", [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"]]") + + @@map("guilds") +} + +model Question { + answers QuestionAnswer[] + createdAt DateTime @default(now()) + id String @id @default(uuid()) + category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) + categoryId Int + label String @db.VarChar(45) + maxLength Int? @default(4000) + minLength Int? @default(0) + options Json @default("[]") + order Int + placeholder String? @db.VarChar(100) + required Boolean @default(true) + style Int @default(2) + type QuestionType @default(TEXT) + value String? @db.Text + + @@map("questions") +} + +model QuestionAnswer { + createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + question Question @relation(fields: [questionId], references: [id], onDelete: Cascade) + questionId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String @db.VarChar(19) + value String? @db.Text + + @@map("questionAnswers") +} + +model Tag { + content String @db.Text + createdAt DateTime @default(now()) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id Int @id @default(autoincrement()) + name String + regex String? + + @@unique([guildId, name]) + @@map("tags") +} + +model Ticket { + archivedChannels ArchivedChannel[] + archivedMessages ArchivedMessage[] + archivedRoles ArchivedRole[] + archivedUsers ArchivedUser[] + category Category? @relation(fields: [categoryId], references: [id], onDelete: Cascade) + categoryId Int? + claimedBy User? @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id]) + claimedById String? @db.VarChar(19) + closedAt DateTime? + closedBy User? @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id]) + closedById String? @db.VarChar(19) + closedReason String? @db.Text + createdAt DateTime @default(now()) + createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id]) + createdById String @db.VarChar(19) + deleted Boolean @default(false) + feedback Feedback? + firstResponseAt DateTime? + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id String @id @db.VarChar(19) + lastMessageAt DateTime? + messageCount Int? + number Int + open Boolean @default(true) + openingMessageId String @db.VarChar(19) + pinnedMessageIds Json @default("[]") + priority TicketPriority? + referencedBy Ticket[] @relation("TicketsReferencedByTicket") + referencesMessageId String? @db.VarChar(19) + referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull) + referencesTicketId String? @db.VarChar(19) + topic String? @db.Text + questionAnswers QuestionAnswer[] + + @@unique([guildId, number]) + @@map("tickets") +} + +model User { + createdAt DateTime @default(now()) + feedback Feedback[] + id String @id @db.VarChar(19) + messageCount Int @default(0) + ticketsCreated Ticket[] @relation("TicketsCreatedByUser") + ticketsClosed Ticket[] @relation("TicketsClosedByUser") + ticketsClaimed Ticket[] @relation("TicketsClaimedByUser") + questionAnswers QuestionAnswer[] + + @@map("users") +} + +enum TicketPriority { + LOW + MEDIUM + HIGH +} + +enum QuestionType { + MENU + TEXT +} diff --git a/db/sqlite/migrations/20230309142817_4_0_0/migration.sql b/db/sqlite/migrations/20230309142817_4_0_0/migration.sql new file mode 100644 index 0000000..29d423a --- /dev/null +++ b/db/sqlite/migrations/20230309142817_4_0_0/migration.sql @@ -0,0 +1,207 @@ +-- CreateTable +CREATE TABLE "archivedChannels" ( + "channelId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "name" TEXT NOT NULL, + "ticketId" TEXT NOT NULL, + + PRIMARY KEY ("ticketId", "channelId"), + CONSTRAINT "archivedChannels_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "archivedMessages" ( + "authorId" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" BOOLEAN NOT NULL DEFAULT false, + "edited" BOOLEAN NOT NULL DEFAULT false, + "id" TEXT NOT NULL PRIMARY KEY, + "external" BOOLEAN NOT NULL DEFAULT false, + "ticketId" TEXT NOT NULL, + CONSTRAINT "archivedMessages_ticketId_authorId_fkey" FOREIGN KEY ("ticketId", "authorId") REFERENCES "archivedUsers" ("ticketId", "userId") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "archivedMessages_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "archivedRoles" ( + "colour" TEXT NOT NULL DEFAULT '5865F2', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "name" TEXT NOT NULL, + "roleId" TEXT NOT NULL, + "ticketId" TEXT NOT NULL, + + PRIMARY KEY ("ticketId", "roleId"), + CONSTRAINT "archivedRoles_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "archivedUsers" ( + "avatar" TEXT, + "bot" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "discriminator" TEXT, + "displayName" TEXT, + "roleId" TEXT, + "ticketId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "username" TEXT, + + PRIMARY KEY ("ticketId", "userId"), + CONSTRAINT "archivedUsers_ticketId_roleId_fkey" FOREIGN KEY ("ticketId", "roleId") REFERENCES "archivedRoles" ("ticketId", "roleId") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "archivedUsers_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "categories" ( + "channelName" TEXT NOT NULL, + "claiming" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "cooldown" INTEGER, + "customTopic" TEXT, + "description" TEXT NOT NULL, + "discordCategory" TEXT NOT NULL, + "emoji" TEXT NOT NULL, + "enableFeedback" BOOLEAN NOT NULL DEFAULT false, + "guildId" TEXT NOT NULL, + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "image" TEXT, + "memberLimit" INTEGER NOT NULL DEFAULT 1, + "name" TEXT NOT NULL, + "openingMessage" TEXT NOT NULL, + "pingRoles" TEXT NOT NULL DEFAULT '[]', + "ratelimit" INTEGER, + "requiredRoles" TEXT NOT NULL DEFAULT '[]', + "requireTopic" BOOLEAN NOT NULL DEFAULT false, + "staffRoles" TEXT NOT NULL, + "totalLimit" INTEGER NOT NULL DEFAULT 50, + CONSTRAINT "categories_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "feedback" ( + "comment" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "guildId" TEXT NOT NULL, + "rating" INTEGER NOT NULL, + "ticketId" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT, + CONSTRAINT "feedback_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "feedback_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "feedback_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "guilds" ( + "autoClose" INTEGER NOT NULL DEFAULT 43200000, + "autoTag" TEXT NOT NULL DEFAULT '[]', + "archive" BOOLEAN NOT NULL DEFAULT true, + "blocklist" TEXT NOT NULL DEFAULT '[]', + "claimButton" BOOLEAN NOT NULL DEFAULT false, + "closeButton" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "errorColour" TEXT NOT NULL DEFAULT 'Red', + "footer" TEXT DEFAULT 'Discord Tickets by eartharoid', + "id" TEXT NOT NULL PRIMARY KEY, + "locale" TEXT NOT NULL DEFAULT 'en-GB', + "logChannel" TEXT, + "primaryColour" TEXT NOT NULL DEFAULT '#009999', + "staleAfter" INTEGER, + "successColour" TEXT NOT NULL DEFAULT 'Green', + "workingHours" TEXT NOT NULL DEFAULT '["UTC", ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"]]' +); + +-- CreateTable +CREATE TABLE "questions" ( + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "id" TEXT NOT NULL PRIMARY KEY, + "categoryId" INTEGER NOT NULL, + "label" TEXT NOT NULL, + "maxLength" INTEGER DEFAULT 4000, + "minLength" INTEGER DEFAULT 0, + "options" TEXT NOT NULL DEFAULT '[]', + "order" INTEGER NOT NULL, + "placeholder" TEXT, + "required" BOOLEAN NOT NULL DEFAULT true, + "style" INTEGER NOT NULL DEFAULT 2, + "type" TEXT NOT NULL DEFAULT 'TEXT', + "value" TEXT, + CONSTRAINT "questions_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "categories" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "questionAnswers" ( + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "ticketId" TEXT NOT NULL, + "questionId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "value" TEXT, + CONSTRAINT "questionAnswers_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "questionAnswers_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "questions" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "questionAnswers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "tags" ( + "content" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "guildId" TEXT NOT NULL, + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "regex" TEXT, + CONSTRAINT "tags_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "tickets" ( + "categoryId" INTEGER, + "claimedById" TEXT, + "closedAt" DATETIME, + "closedById" TEXT, + "closedReason" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdById" TEXT NOT NULL, + "deleted" BOOLEAN NOT NULL DEFAULT false, + "firstResponseAt" DATETIME, + "guildId" TEXT NOT NULL, + "id" TEXT NOT NULL PRIMARY KEY, + "lastMessageAt" DATETIME, + "messageCount" INTEGER, + "number" INTEGER NOT NULL, + "open" BOOLEAN NOT NULL DEFAULT true, + "openingMessageId" TEXT NOT NULL, + "pinnedMessageIds" TEXT NOT NULL DEFAULT '[]', + "priority" TEXT, + "referencesMessageId" TEXT, + "referencesTicketId" TEXT, + "topic" TEXT, + CONSTRAINT "tickets_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "categories" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "tickets_claimedById_fkey" FOREIGN KEY ("claimedById") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "tickets_closedById_fkey" FOREIGN KEY ("closedById") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "tickets_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "tickets_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "tickets_referencesTicketId_fkey" FOREIGN KEY ("referencesTicketId") REFERENCES "tickets" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "users" ( + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "id" TEXT NOT NULL PRIMARY KEY, + "messageCount" INTEGER NOT NULL DEFAULT 0 +); + +-- CreateIndex +CREATE UNIQUE INDEX "archivedChannels_ticketId_channelId_key" ON "archivedChannels"("ticketId", "channelId"); + +-- CreateIndex +CREATE UNIQUE INDEX "archivedRoles_ticketId_roleId_key" ON "archivedRoles"("ticketId", "roleId"); + +-- CreateIndex +CREATE UNIQUE INDEX "archivedUsers_ticketId_userId_key" ON "archivedUsers"("ticketId", "userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "tags_guildId_name_key" ON "tags"("guildId", "name"); + +-- CreateIndex +CREATE UNIQUE INDEX "tickets_guildId_number_key" ON "tickets"("guildId", "number"); diff --git a/db/sqlite/migrations/migration_lock.toml b/db/sqlite/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/db/sqlite/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/db/sqlite/schema.prisma b/db/sqlite/schema.prisma new file mode 100644 index 0000000..7c080b0 --- /dev/null +++ b/db/sqlite/schema.prisma @@ -0,0 +1,236 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = "file:../user/database.db" +} + +model ArchivedChannel { + channelId String + createdAt DateTime @default(now()) + name String + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String + + @@id([ticketId, channelId]) + @@unique([ticketId, channelId]) + @@map("archivedChannels") +} + +model ArchivedMessage { + author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade) + authorId String + content String + createdAt DateTime @default(now()) + deleted Boolean @default(false) + edited Boolean @default(false) + id String @id + external Boolean @default(false) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String + + @@map("archivedMessages") +} + +model ArchivedRole { + archivedUsers ArchivedUser[] + colour String @default("5865F2") // 7289DA + createdAt DateTime @default(now()) + name String + roleId String + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String + + @@id([ticketId, roleId]) + @@unique([ticketId, roleId]) + @@map("archivedRoles") +} + +model ArchivedUser { + archivedMessages ArchivedMessage[] + avatar String? + bot Boolean @default(false) + createdAt DateTime @default(now()) + discriminator String? + displayName String? + role ArchivedRole? @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) + roleId String? + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String + userId String + username String? + + @@id([ticketId, userId]) + @@unique([ticketId, userId]) + @@map("archivedUsers") +} + +model Category { + channelName String + claiming Boolean @default(false) + createdAt DateTime @default(now()) + cooldown Int? + customTopic String? + description String + discordCategory String + emoji String + enableFeedback Boolean @default(false) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String + id Int @id @default(autoincrement()) + image String? + memberLimit Int @default(1) + name String + openingMessage String + pingRoles String @default("[]") + questions Question[] + ratelimit Int? + requiredRoles String @default("[]") + requireTopic Boolean @default(false) + staffRoles String + tickets Ticket[] + totalLimit Int @default(50) + + @@map("categories") +} + +model Feedback { + comment String? + createdAt DateTime @default(now()) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String + rating Int + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @id + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String? + + @@map("feedback") +} + +model Guild { + autoClose Int @default(43200000) + autoTag String @default("[]") + archive Boolean @default(true) + blocklist String @default("[]") + categories Category[] + claimButton Boolean @default(false) + closeButton Boolean @default(false) + createdAt DateTime @default(now()) + errorColour String @default("Red") + feedback Feedback[] + footer String? @default("Discord Tickets by eartharoid") + id String @id + locale String @default("en-GB") + logChannel String? + primaryColour String @default("#009999") + staleAfter Int? + successColour String @default("Green") + tags Tag[] + tickets Ticket[] + workingHours String @default("[\"UTC\", [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"]]") + + @@map("guilds") +} + +model Question { + answers QuestionAnswer[] + createdAt DateTime @default(now()) + id String @id @default(uuid()) + category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) + categoryId Int + label String + maxLength Int? @default(4000) + minLength Int? @default(0) + options String @default("[]") + order Int + placeholder String? + required Boolean @default(true) + style Int @default(2) + type String @default("TEXT") + value String? + + @@map("questions") +} + +model QuestionAnswer { + createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String + question Question @relation(fields: [questionId], references: [id], onDelete: Cascade) + questionId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String + value String? + + @@map("questionAnswers") +} + +model Tag { + content String + createdAt DateTime @default(now()) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String + id Int @id @default(autoincrement()) + name String + regex String? + + @@unique([guildId, name]) + @@map("tags") +} + +model Ticket { + archivedChannels ArchivedChannel[] + archivedMessages ArchivedMessage[] + archivedRoles ArchivedRole[] + archivedUsers ArchivedUser[] + category Category? @relation(fields: [categoryId], references: [id], onDelete: Cascade) + categoryId Int? + claimedBy User? @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id]) + claimedById String? + closedAt DateTime? + closedBy User? @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id]) + closedById String? + closedReason String? + createdAt DateTime @default(now()) + createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id]) + createdById String + deleted Boolean @default(false) + feedback Feedback? + firstResponseAt DateTime? + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String + id String @id + lastMessageAt DateTime? + messageCount Int? + number Int + open Boolean @default(true) + openingMessageId String + pinnedMessageIds String @default("[]") + priority String? + referencedBy Ticket[] @relation("TicketsReferencedByTicket") + referencesMessageId String? + referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull) + referencesTicketId String? + topic String? + questionAnswers QuestionAnswer[] + + @@unique([guildId, number]) + @@map("tickets") +} + +model User { + createdAt DateTime @default(now()) + feedback Feedback[] + id String @id + messageCount Int @default(0) + ticketsCreated Ticket[] @relation("TicketsCreatedByUser") + ticketsClosed Ticket[] @relation("TicketsClosedByUser") + ticketsClaimed Ticket[] @relation("TicketsClaimedByUser") + questionAnswers QuestionAnswer[] + + @@map("users") +} diff --git a/docker-compose.yml b/docker-compose.yml index cb40e1d..3d4c2d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,38 +1,54 @@ -version: "3.8" +version: "3.9" services: + mysql: + image: mysql:8 + restart: unless-stopped + hostname: mysql + networks: + - discord-tickets + volumes: + - tickets-mysql:/var/lib/mysql + environment: + MYSQL_DATABASE: tickets + MYSQL_PASSWORD: insecure # change this to a secure password + MYSQL_ROOT_PASSWORD: insecure # change this to a (different) secure password + MYSQL_USER: tickets + bot: - build: - context: . - dockerfile: compose.Dockerfile - restart: unless-stopped - volumes: - - ./src:/tickets/src - - ./user:/tickets/user - - ./logs:/tickets/logs - - ./.env:/tickets/.env:ro - environment: - - DB_TYPE=mysql - - DB_HOST=db - - DB_PORT=3306 - - DB_NAME=tickets - - DB_USER=tickets - - DB_PASS=tickets - - DB_TABLE_PREFIX=dsctickets_ + image: eartharoid/discord-tickets:4.0 depends_on: - - db - - db: - image: mariadb:10.6 + - mysql restart: unless-stopped - environment: - - "MYSQL_DATABASE=tickets" - - "MYSQL_USER=tickets" - - "MYSQL_PASSWORD=tickets" - - - "MYSQL_RANDOM_ROOT_PASSWORD=yes" + hostname: bot + networks: + - discord-tickets + ports: + - 8169:8169 volumes: - - db:/var/lib/mysql + - tickets-bot:/usr/bot/user + tty: true + stdin_open: true + # Please refer to the documentation: + # https://discordtickets.app/self-hosting/configuration/#environment-variables + environment: + DB_CONNECTION_URL: mysql://tickets:insecure@mysql/tickets # change `insecure` to the MYSQL_PASSWORD you set above + DISCORD_SECRET: # required + DISCORD_TOKEN: # required + ENCRYPTION_KEY: # required + DB_PROVIDER: mysql + HTTP_EXTERNAL: http://127.0.0.1:8169 # change this to your server's external IP (or domain) + HTTP_HOST: 0.0.0.0 + HTTP_PORT: 8169 + HTTP_TRUST_PROXY: false # set to true if you're using a reverse proxy + OVERRIDE_ARCHIVE: null + PUBLIC_BOT: false + PUBLISH_COMMANDS: false + SUPER: 319467558166069248 # optionally add `,youruseridhere` + +networks: + discord-tickets: volumes: - db: + tickets-mysql: + tickets-bot: diff --git a/example.env b/example.env deleted file mode 100644 index db544bb..0000000 --- a/example.env +++ /dev/null @@ -1,9 +0,0 @@ -DISCORD_TOKEN= -DB_ENCRYPTION_KEY= -DB_TYPE=sqlite -DB_HOST= -DB_PORT= -DB_NAME= -DB_USER= -DB_PASS= -DB_TABLE_PREFIX=dsctickets_ \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..120fd76 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "ESNext", + "target": "ESNext", + "moduleResolution": "Node", + "baseUrl": "src", + "resolveJsonModule": true, + "checkJs": false, + }, + "include": [ + "src/**/*.js" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index e0c34c1..d4da2c2 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,25 @@ { - "name": "@eartharoid/discord-tickets", - "version": "3.1.3", - "private": true, + "name": "discord-tickets", + "version": "4.0.0-beta.12", + "private": "true", "description": "An open-source Discord bot for ticket management", - "main": "src/index.js", + "main": "src/", "scripts": { + "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "contributors:add": "all-contributors add", "contributors:generate": "all-contributors generate", - "lint": "eslint src --fix", - "start": "node src/", - "test": "echo \"Nothing to test! Run with 'npm start'\" && exit 1" + "keygen": "node scripts/keygen", + "lint": "eslint src scripts --fix", + "preinstall": "node scripts/preinstall", + "postinstall": "node scripts/postinstall", + "start": "node .", + "studio": "npx prisma studio", + "test": "echo \"There's nothing to test\" && exit 1" }, - "engines": { - "node": ">=16.6" + "lint-staged": { + "**/*.js": [ + "npm run lint --" + ] }, "repository": { "type": "git", @@ -29,41 +36,54 @@ "url": "https://github.com/discord-tickets/bot/issues" }, "homepage": "https://discordtickets.app", - "funding": "https://github.com/discord-tickets/bot/?sponsor=1", + "engines": { + "node": ">=18" + }, "dependencies": { - "@eartharoid/i18n": "^1.0.1", - "boxen": "^5.1.2", - "cryptr": "^6.0.2", - "discord.js": "^13.14.0", - "dotenv": "^8.6.0", - "keyv": "^4.0.3", - "leeks.js": "^0.2.4", - "leekslazylogger": "^3.0.2", + "@discord-tickets/settings": "^2.1.4", + "@eartharoid/dbf": "^0.4.0", + "@eartharoid/dtf": "^2.0.1", + "@eartharoid/i18n": "^1.2.1", + "@fastify/cookie": "^6.0.0", + "@fastify/jwt": "^5.0.1", + "@fastify/oauth2": "^5.1.0", + "@prisma/client": "^4.14.1", + "boxen": "^7.1.0", + "cryptr": "^6.2.0", + "discord.js": "^14.11.0", + "dotenv": "^16.0.3", + "fastify": "^4.17.0", + "figlet": "^1.6.0", + "fs-extra": "^10.1.0", + "keyv": "^4.5.2", + "leeks.js": "^0.3.0", + "leekslazylogger": "^5.0.1", "ms": "^2.1.3", "mustache": "^4.2.0", - "node-fetch": "^2.6.5", - "semver": "^7.3.5", - "sequelize": "^6.6.5", - "terminal-link": "^2.1.1" + "node-dir": "^0.1.17", + "node-emoji": "^1.11.0", + "object-diffy": "^1.0.4", + "prisma": "^4.14.1", + "semver": "^7.5.1", + "spacetime": "^7.4.4", + "terminal-link": "^2.1.1", + "yaml": "^1.10.2" }, "devDependencies": { - "all-contributors-cli": "^6.20.0", - "eslint": "^7.32.0", - "mariadb": "^2.5.4", - "mysql2": "^2.3.0", - "nodemon": "^2.0.13", - "pg": "^8.7.1", - "pg-hstore": "^2.3.4", - "tedious": "^11.8.0" + "@commitlint/cli": "^17.6.3", + "@commitlint/config-conventional": "^17.6.3", + "all-contributors-cli": "^6.26.0", + "conventional-changelog-cli": "^2.2.2", + "eslint": "^8.41.0", + "eslint-plugin-unused-imports": "^2.0.0", + "husky": "^8.0.3", + "lint-staged": "^13.2.2", + "nodemon": "^2.0.22" }, "optionalDependencies": { - "sqlite3": "^5.0.2" - }, - "peerDependencies": { - "mariadb": "^2.5.4", - "mysql2": "^2.3.0", - "pg": "^8.7.1", - "pg-hstore": "^2.3.4", - "tedious": "^11.4.0" + "bufferutil": "^4.0.7", + "erlpack": "github:discord/erlpack", + "utf-8-validate": "^5.0.10", + "zlib-sync": "^0.1.8" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3aa5d01..22c5413 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,740 +1,1055 @@ -lockfileVersion: 5.4 - -specifiers: - '@eartharoid/i18n': ^1.0.1 - all-contributors-cli: ^6.20.0 - boxen: ^5.1.2 - cryptr: ^6.0.2 - discord.js: ^13.14.0 - dotenv: ^8.6.0 - eslint: ^7.32.0 - keyv: ^4.0.3 - leeks.js: ^0.2.4 - leekslazylogger: ^3.0.2 - mariadb: ^2.5.4 - ms: ^2.1.3 - mustache: ^4.2.0 - mysql2: ^2.3.0 - node-fetch: ^2.6.5 - nodemon: ^2.0.13 - pg: ^8.7.1 - pg-hstore: ^2.3.4 - semver: ^7.3.5 - sequelize: ^6.6.5 - sqlite3: ^5.0.2 - tedious: ^11.8.0 - terminal-link: ^2.1.1 +lockfileVersion: '6.0' dependencies: - '@eartharoid/i18n': 1.0.1 - boxen: 5.1.2 - cryptr: 6.0.2 - discord.js: 13.14.0 - dotenv: 8.6.0 - keyv: 4.0.3 - leeks.js: 0.2.4 - leekslazylogger: 3.0.2 - ms: 2.1.3 - mustache: 4.2.0 - node-fetch: 2.6.5 - semver: 7.3.5 - sequelize: 6.6.5_gsgxvhwyg44bfarb3k5znvycta - terminal-link: 2.1.1 + '@discord-tickets/settings': + specifier: ^2.1.4 + version: 2.1.4(svelte@3.59.1) + '@eartharoid/dbf': + specifier: ^0.4.0 + version: 0.4.0(bufferutil@4.0.7)(utf-8-validate@5.0.10) + '@eartharoid/dtf': + specifier: ^2.0.1 + version: 2.0.1 + '@eartharoid/i18n': + specifier: ^1.2.1 + version: 1.2.1 + '@fastify/cookie': + specifier: ^6.0.0 + version: 6.0.0 + '@fastify/jwt': + specifier: ^5.0.1 + version: 5.0.1 + '@fastify/oauth2': + specifier: ^5.1.0 + version: 5.1.0 + '@prisma/client': + specifier: ^4.14.1 + version: 4.14.1(prisma@4.14.1) + boxen: + specifier: ^7.1.0 + version: 7.1.0 + cryptr: + specifier: ^6.2.0 + version: 6.2.0 + discord.js: + specifier: ^14.11.0 + version: 14.11.0(bufferutil@4.0.7)(utf-8-validate@5.0.10) + dotenv: + specifier: ^16.0.3 + version: 16.0.3 + fastify: + specifier: ^4.17.0 + version: 4.17.0 + figlet: + specifier: ^1.6.0 + version: 1.6.0 + fs-extra: + specifier: ^10.1.0 + version: 10.1.0 + keyv: + specifier: ^4.5.2 + version: 4.5.2 + leeks.js: + specifier: ^0.3.0 + version: 0.3.0 + leekslazylogger: + specifier: ^5.0.1 + version: 5.0.1 + ms: + specifier: ^2.1.3 + version: 2.1.3 + mustache: + specifier: ^4.2.0 + version: 4.2.0 + node-dir: + specifier: ^0.1.17 + version: 0.1.17 + node-emoji: + specifier: ^1.11.0 + version: 1.11.0 + object-diffy: + specifier: ^1.0.4 + version: 1.0.4 + prisma: + specifier: ^4.14.1 + version: 4.14.1 + semver: + specifier: ^7.5.1 + version: 7.5.1 + spacetime: + specifier: ^7.4.4 + version: 7.4.4 + terminal-link: + specifier: ^2.1.1 + version: 2.1.1 + yaml: + specifier: ^1.10.2 + version: 1.10.2 optionalDependencies: - sqlite3: 5.0.2 + bufferutil: + specifier: ^4.0.7 + version: 4.0.7 + erlpack: + specifier: github:discord/erlpack + version: github.com/discord/erlpack/cbe76be04c2210fc9cb6ff95910f0937c1011d04 + utf-8-validate: + specifier: ^5.0.10 + version: 5.0.10 + zlib-sync: + specifier: ^0.1.8 + version: 0.1.8 devDependencies: - all-contributors-cli: 6.20.0 - eslint: 7.32.0 - mariadb: 2.5.4 - mysql2: 2.3.0 - nodemon: 2.0.13 - pg: 8.7.1 - pg-hstore: 2.3.4 - tedious: 11.8.0 + '@commitlint/cli': + specifier: ^17.6.3 + version: 17.6.3 + '@commitlint/config-conventional': + specifier: ^17.6.3 + version: 17.6.3 + all-contributors-cli: + specifier: ^6.26.0 + version: 6.26.0 + conventional-changelog-cli: + specifier: ^2.2.2 + version: 2.2.2 + eslint: + specifier: ^8.41.0 + version: 8.41.0 + eslint-plugin-unused-imports: + specifier: ^2.0.0 + version: 2.0.0(eslint@8.41.0) + husky: + specifier: ^8.0.3 + version: 8.0.3 + lint-staged: + specifier: ^13.2.2 + version: 13.2.2 + nodemon: + specifier: ^2.0.22 + version: 2.0.22 packages: - /@azure/abort-controller/1.0.4: - resolution: {integrity: sha512-lNUmDRVGpanCsiUN3NWxFTdwmdFI53xwhkTFfHDGTYk46ca7Ind3nanJc+U6Zj9Tv+9nTCWRBscWEW1DyKOpTw==} - engines: {node: '>=8.0.0'} + /@babel/code-frame@7.21.4: + resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==} + engines: {node: '>=6.9.0'} dependencies: - tslib: 2.5.0 - - /@azure/core-asynciterator-polyfill/1.0.0: - resolution: {integrity: sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg==} - - /@azure/core-auth/1.3.2: - resolution: {integrity: sha512-7CU6DmCHIZp5ZPiZ9r3J17lTKMmYsm/zGvNkjArQwPkrLlZ1TZ+EUYfGgh2X31OLMVAQCTJZW4cXHJi02EbJnA==} - engines: {node: '>=12.0.0'} - dependencies: - '@azure/abort-controller': 1.0.4 - tslib: 2.5.0 - - /@azure/core-client/1.3.1: - resolution: {integrity: sha512-7IHm2DGg2u7dJYtCW84Ik7uENHfE8VsM/sWloZezPKYDoWZrg7JzwjvdGAfsaELKi2p0GE+JBaAbDYnNpr5V1w==} - engines: {node: '>=12.0.0'} - dependencies: - '@azure/abort-controller': 1.0.4 - '@azure/core-asynciterator-polyfill': 1.0.0 - '@azure/core-auth': 1.3.2 - '@azure/core-rest-pipeline': 1.3.1 - '@azure/core-tracing': 1.0.0-preview.13 - tslib: 2.5.0 - transitivePeerDependencies: - - supports-color - - /@azure/core-http/2.2.1: - resolution: {integrity: sha512-7ATnV3OGzCO2K9kMrh3NKUM8b4v+xasmlUhkNZz6uMbm+8XH/AexLkhRGsoo0GyKNlEGvyGEfytqTk0nUY2I4A==} - engines: {node: '>=12.0.0'} - dependencies: - '@azure/abort-controller': 1.0.4 - '@azure/core-asynciterator-polyfill': 1.0.0 - '@azure/core-auth': 1.3.2 - '@azure/core-tracing': 1.0.0-preview.13 - '@azure/logger': 1.0.3 - '@types/node-fetch': 2.6.3 - '@types/tunnel': 0.0.3 - form-data: 4.0.0 - node-fetch: 2.6.9 - process: 0.11.10 - tough-cookie: 4.0.0 - tslib: 2.5.0 - tunnel: 0.0.6 - uuid: 8.3.2 - xml2js: 0.4.23 - transitivePeerDependencies: - - encoding - - /@azure/core-lro/2.2.1: - resolution: {integrity: sha512-HE6PBl+mlKa0eBsLwusHqAqjLc5n9ByxeDo3Hz4kF3B1hqHvRkBr4oMgoT6tX7Hc3q97KfDctDUon7EhvoeHPA==} - engines: {node: '>=12.0.0'} - dependencies: - '@azure/abort-controller': 1.0.4 - '@azure/core-tracing': 1.0.0-preview.13 - '@azure/logger': 1.0.3 - tslib: 2.5.0 - - /@azure/core-paging/1.2.0: - resolution: {integrity: sha512-ZX1bCjm/MjKPCN6kQD/9GJErYSoKA8YWp6YWoo5EIzcTWlSBLXu3gNaBTUl8usGl+UShiKo7b4Gdy1NSTIlpZg==} - engines: {node: '>=12.0.0'} - dependencies: - '@azure/core-asynciterator-polyfill': 1.0.0 - tslib: 2.5.0 - - /@azure/core-rest-pipeline/1.3.1: - resolution: {integrity: sha512-xTQiv47O5cWzJFkwiDrUTT4K4IYbUIts0gaou5TZxAAuhQi9kAKWHEmFTjHVMOeAmyDhlMM5cb21M2n4WDto1A==} - engines: {node: '>=12.0.0'} - dependencies: - '@azure/abort-controller': 1.0.4 - '@azure/core-auth': 1.3.2 - '@azure/core-tracing': 1.0.0-preview.13 - '@azure/logger': 1.0.3 - form-data: 4.0.0 - http-proxy-agent: 4.0.1 - https-proxy-agent: 5.0.0 - tslib: 2.5.0 - uuid: 8.3.2 - transitivePeerDependencies: - - supports-color - - /@azure/core-tracing/1.0.0-preview.12: - resolution: {integrity: sha512-nvo2Wc4EKZGN6eFu9n3U7OXmASmL8VxoPIH7xaD6OlQqi44bouF0YIi9ID5rEsKLiAU59IYx6M297nqWVMWPDg==} - engines: {node: '>=12.0.0'} - dependencies: - '@opentelemetry/api': 1.0.3 - tslib: 2.5.0 - - /@azure/core-tracing/1.0.0-preview.13: - resolution: {integrity: sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==} - engines: {node: '>=12.0.0'} - dependencies: - '@opentelemetry/api': 1.0.3 - tslib: 2.5.0 - - /@azure/identity/1.5.2: - resolution: {integrity: sha512-vqyeRbd2i0h9F4mqW5JbkP1xfabqKQ21l/81osKhpOQ2LtwaJW6nw4+0PsVYnxcbPHFCIZt6EWAk74a3OGYZJA==} - engines: {node: '>=12.0.0'} - dependencies: - '@azure/core-auth': 1.3.2 - '@azure/core-client': 1.3.1 - '@azure/core-rest-pipeline': 1.3.1 - '@azure/core-tracing': 1.0.0-preview.12 - '@azure/logger': 1.0.3 - '@azure/msal-node': 1.0.0-beta.6 - '@types/stoppable': 1.1.1 - axios: 0.21.4 - events: 3.3.0 - jws: 4.0.0 - msal: 1.4.14 - open: 7.4.2 - qs: 6.10.1 - stoppable: 1.1.0 - tslib: 2.3.1 - uuid: 8.3.2 - optionalDependencies: - keytar: 7.7.0 - transitivePeerDependencies: - - debug - - supports-color - - /@azure/keyvault-keys/4.3.0: - resolution: {integrity: sha512-OEosl0/rE/mKD5Ji9KaQN7UH+yQnV5MS0MRhGqQIiJrG+qAvAla0MYudJzv3XvBlplpGk0+MVgyL9H3KX/UAwQ==} - engines: {node: '>=8.0.0'} - dependencies: - '@azure/abort-controller': 1.0.4 - '@azure/core-http': 2.2.1 - '@azure/core-lro': 2.2.1 - '@azure/core-paging': 1.2.0 - '@azure/core-tracing': 1.0.0-preview.13 - '@azure/logger': 1.0.3 - tslib: 2.3.1 - transitivePeerDependencies: - - encoding - - /@azure/logger/1.0.3: - resolution: {integrity: sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==} - engines: {node: '>=12.0.0'} - dependencies: - tslib: 2.5.0 - - /@azure/ms-rest-azure-env/2.0.0: - resolution: {integrity: sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==} - - /@azure/ms-rest-js/2.6.0: - resolution: {integrity: sha512-4C5FCtvEzWudblB+h92/TYYPiq7tuElX8icVYToxOdggnYqeec4Se14mjse5miInKtZahiFHdl8lZA/jziEc5g==} - dependencies: - '@azure/core-auth': 1.3.2 - abort-controller: 3.0.0 - form-data: 2.5.1 - node-fetch: 2.6.9 - tough-cookie: 3.0.1 - tslib: 1.14.1 - tunnel: 0.0.6 - uuid: 8.3.2 - xml2js: 0.4.23 - transitivePeerDependencies: - - encoding - - /@azure/ms-rest-nodeauth/3.1.0: - resolution: {integrity: sha512-F4NKrbkZg0qD3+rUM8fvJHOFRkXFoEiptYTZtLBruN3VwBFIqbTFW0fmgRyBW9seZl+mX2OexQA5GzWenSA3Kw==} - dependencies: - '@azure/ms-rest-azure-env': 2.0.0 - '@azure/ms-rest-js': 2.6.0 - adal-node: 0.2.3 - transitivePeerDependencies: - - debug - - encoding - - /@azure/msal-common/4.5.1: - resolution: {integrity: sha512-/i5dXM+QAtO+6atYd5oHGBAx48EGSISkXNXViheliOQe+SIFMDo3gSq3lL54W0suOSAsVPws3XnTaIHlla0PIQ==} - engines: {node: '>=0.8.0'} - dependencies: - debug: 4.3.2 - transitivePeerDependencies: - - supports-color - - /@azure/msal-node/1.0.0-beta.6: - resolution: {integrity: sha512-ZQI11Uz1j0HJohb9JZLRD8z0moVcPks1AFW4Q/Gcl67+QvH4aKEJti7fjCcipEEZYb/qzLSO8U6IZgPYytsiJQ==} - dependencies: - '@azure/msal-common': 4.5.1 - axios: 0.21.4 - jsonwebtoken: 8.5.1 - uuid: 8.3.2 - transitivePeerDependencies: - - debug - - supports-color - - /@babel/code-frame/7.12.11: - resolution: {integrity: sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==} - dependencies: - '@babel/highlight': 7.14.5 + '@babel/highlight': 7.18.6 dev: true - /@babel/helper-validator-identifier/7.15.7: - resolution: {integrity: sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==} + /@babel/helper-validator-identifier@7.19.1: + resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} engines: {node: '>=6.9.0'} dev: true - /@babel/highlight/7.14.5: - resolution: {integrity: sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==} + /@babel/highlight@7.18.6: + resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-validator-identifier': 7.15.7 + '@babel/helper-validator-identifier': 7.19.1 chalk: 2.4.2 js-tokens: 4.0.0 dev: true - /@babel/runtime/7.15.4: - resolution: {integrity: sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==} + /@babel/runtime@7.22.3: + resolution: {integrity: sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==} engines: {node: '>=6.9.0'} dependencies: - regenerator-runtime: 0.13.9 + regenerator-runtime: 0.13.11 + + /@commitlint/cli@17.6.3: + resolution: {integrity: sha512-ItSz2fd4F+CujgIbQOfNNerDF1eFlsBGEfp9QcCb1kxTYMuKTYZzA6Nu1YRRrIaaWwe2E7awUGpIMrPoZkOG3A==} + engines: {node: '>=v14'} + hasBin: true + dependencies: + '@commitlint/format': 17.4.4 + '@commitlint/lint': 17.6.3 + '@commitlint/load': 17.5.0 + '@commitlint/read': 17.5.1 + '@commitlint/types': 17.4.4 + execa: 5.1.1 + lodash.isfunction: 3.0.9 + resolve-from: 5.0.0 + resolve-global: 1.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' dev: true - /@discordjs/builders/0.16.0: - resolution: {integrity: sha512-9/NCiZrLivgRub2/kBc0Vm5pMBE5AUdYbdXsLu/yg9ANgvnaJ0bZKTY8yYnLbsEc/LYUP79lEIdC73qEYhWq7A==} - engines: {node: '>=16.9.0'} - deprecated: no longer supported + /@commitlint/config-conventional@17.6.3: + resolution: {integrity: sha512-bLyHEjjRWqlLQWIgYFHmUPbEFMOOLXeF3QbUinDIJev/u9e769tkoTH9YPknEywiuIrAgZaVo+OfzAIsJP0fsw==} + engines: {node: '>=v14'} dependencies: - '@sapphire/shapeshift': 3.8.1 - discord-api-types: 0.36.3 + conventional-changelog-conventionalcommits: 5.0.0 + dev: true + + /@commitlint/config-validator@17.4.4: + resolution: {integrity: sha512-bi0+TstqMiqoBAQDvdEP4AFh0GaKyLFlPPEObgI29utoKEYoPQTvF0EYqIwYYLEoJYhj5GfMIhPHJkTJhagfeg==} + engines: {node: '>=v14'} + dependencies: + '@commitlint/types': 17.4.4 + ajv: 8.12.0 + dev: true + + /@commitlint/ensure@17.4.4: + resolution: {integrity: sha512-AHsFCNh8hbhJiuZ2qHv/m59W/GRE9UeOXbkOqxYMNNg9pJ7qELnFcwj5oYpa6vzTSHtPGKf3C2yUFNy1GGHq6g==} + engines: {node: '>=v14'} + dependencies: + '@commitlint/types': 17.4.4 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + dev: true + + /@commitlint/execute-rule@17.4.0: + resolution: {integrity: sha512-LIgYXuCSO5Gvtc0t9bebAMSwd68ewzmqLypqI2Kke1rqOqqDbMpYcYfoPfFlv9eyLIh4jocHWwCK5FS7z9icUA==} + engines: {node: '>=v14'} + dev: true + + /@commitlint/format@17.4.4: + resolution: {integrity: sha512-+IS7vpC4Gd/x+uyQPTAt3hXs5NxnkqAZ3aqrHd5Bx/R9skyCAWusNlNbw3InDbAK6j166D9asQM8fnmYIa+CXQ==} + engines: {node: '>=v14'} + dependencies: + '@commitlint/types': 17.4.4 + chalk: 4.1.2 + dev: true + + /@commitlint/is-ignored@17.6.3: + resolution: {integrity: sha512-LQbNdnPbxrpbcrVKR5yf51SvquqktpyZJwqXx3lUMF6+nT9PHB8xn3wLy8pi2EQv5Zwba484JnUwDE1ygVYNQA==} + engines: {node: '>=v14'} + dependencies: + '@commitlint/types': 17.4.4 + semver: 7.5.0 + dev: true + + /@commitlint/lint@17.6.3: + resolution: {integrity: sha512-fBlXwt6SHJFgm3Tz+luuo3DkydAx9HNC5y4eBqcKuDuMVqHd2ugMNr+bQtx6riv9mXFiPoKp7nE4Xn/ls3iVDA==} + engines: {node: '>=v14'} + dependencies: + '@commitlint/is-ignored': 17.6.3 + '@commitlint/parse': 17.4.4 + '@commitlint/rules': 17.6.1 + '@commitlint/types': 17.4.4 + dev: true + + /@commitlint/load@17.5.0: + resolution: {integrity: sha512-l+4W8Sx4CD5rYFsrhHH8HP01/8jEP7kKf33Xlx2Uk2out/UKoKPYMOIRcDH5ppT8UXLMV+x6Wm5osdRKKgaD1Q==} + engines: {node: '>=v14'} + dependencies: + '@commitlint/config-validator': 17.4.4 + '@commitlint/execute-rule': 17.4.0 + '@commitlint/resolve-extends': 17.4.4 + '@commitlint/types': 17.4.4 + '@types/node': 20.2.5 + chalk: 4.1.2 + cosmiconfig: 8.1.3 + cosmiconfig-typescript-loader: 4.3.0(@types/node@20.2.5)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@5.0.4) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + resolve-from: 5.0.0 + ts-node: 10.9.1(@types/node@20.2.5)(typescript@5.0.4) + typescript: 5.0.4 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + dev: true + + /@commitlint/message@17.4.2: + resolution: {integrity: sha512-3XMNbzB+3bhKA1hSAWPCQA3lNxR4zaeQAQcHj0Hx5sVdO6ryXtgUBGGv+1ZCLMgAPRixuc6en+iNAzZ4NzAa8Q==} + engines: {node: '>=v14'} + dev: true + + /@commitlint/parse@17.4.4: + resolution: {integrity: sha512-EKzz4f49d3/OU0Fplog7nwz/lAfXMaDxtriidyGF9PtR+SRbgv4FhsfF310tKxs6EPj8Y+aWWuX3beN5s+yqGg==} + engines: {node: '>=v14'} + dependencies: + '@commitlint/types': 17.4.4 + conventional-changelog-angular: 5.0.13 + conventional-commits-parser: 3.2.4 + dev: true + + /@commitlint/read@17.5.1: + resolution: {integrity: sha512-7IhfvEvB//p9aYW09YVclHbdf1u7g7QhxeYW9ZHSO8Huzp8Rz7m05aCO1mFG7G8M+7yfFnXB5xOmG18brqQIBg==} + engines: {node: '>=v14'} + dependencies: + '@commitlint/top-level': 17.4.0 + '@commitlint/types': 17.4.4 + fs-extra: 11.1.1 + git-raw-commits: 2.0.11 + minimist: 1.2.8 + dev: true + + /@commitlint/resolve-extends@17.4.4: + resolution: {integrity: sha512-znXr1S0Rr8adInptHw0JeLgumS11lWbk5xAWFVno+HUFVN45875kUtqjrI6AppmD3JI+4s0uZlqqlkepjJd99A==} + engines: {node: '>=v14'} + dependencies: + '@commitlint/config-validator': 17.4.4 + '@commitlint/types': 17.4.4 + import-fresh: 3.3.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + resolve-global: 1.0.0 + dev: true + + /@commitlint/rules@17.6.1: + resolution: {integrity: sha512-lUdHw6lYQ1RywExXDdLOKxhpp6857/4c95Dc/1BikrHgdysVUXz26yV0vp1GL7Gv+avx9WqZWTIVB7pNouxlfw==} + engines: {node: '>=v14'} + dependencies: + '@commitlint/ensure': 17.4.4 + '@commitlint/message': 17.4.2 + '@commitlint/to-lines': 17.4.0 + '@commitlint/types': 17.4.4 + execa: 5.1.1 + dev: true + + /@commitlint/to-lines@17.4.0: + resolution: {integrity: sha512-LcIy/6ZZolsfwDUWfN1mJ+co09soSuNASfKEU5sCmgFCvX5iHwRYLiIuoqXzOVDYOy7E7IcHilr/KS0e5T+0Hg==} + engines: {node: '>=v14'} + dev: true + + /@commitlint/top-level@17.4.0: + resolution: {integrity: sha512-/1loE/g+dTTQgHnjoCy0AexKAEFyHsR2zRB4NWrZ6lZSMIxAhBJnmCqwao7b4H8888PsfoTBCLBYIw8vGnej8g==} + engines: {node: '>=v14'} + dependencies: + find-up: 5.0.0 + dev: true + + /@commitlint/types@17.4.4: + resolution: {integrity: sha512-amRN8tRLYOsxRr6mTnGGGvB5EmW/4DDjLMgiwK3CCVEmN6Sr/6xePGEpWaspKkckILuUORCwe6VfDBw6uj4axQ==} + engines: {node: '>=v14'} + dependencies: + chalk: 4.1.2 + dev: true + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@discord-tickets/settings@2.1.4(svelte@3.59.1): + resolution: {integrity: sha512-eABoWIOPLh8gdCRN9QfzFl7Nl9l0rVWthOQyawcbrdrmsSt1rKveNZ1aAZftJ9z8JvJMIacDRQurxVbb18/Wjg==} + dependencies: + '@fortawesome/fontawesome-free': 6.4.0 + '@skyra/discord-components-core': 3.6.0 + cookie: 0.5.0 + emoji-name-map: 1.2.9 + marked: 4.3.0 + ms: 2.1.3 + postcss: 8.4.24 + sortablejs: 1.15.0 + svelte-modals: 1.2.1(svelte@3.59.1) + svelte-toasts: 1.1.2 + transitivePeerDependencies: + - svelte + dev: false + + /@discordjs/builders@1.6.3: + resolution: {integrity: sha512-CTCh8NqED3iecTNuiz49mwSsrc2iQb4d0MjMdmS/8pb69Y4IlzJ/DIy/p5GFlgOrFbNO2WzMHkWKQSiJ3VNXaw==} + engines: {node: '>=16.9.0'} + dependencies: + '@discordjs/formatters': 0.3.1 + '@discordjs/util': 0.3.1 + '@sapphire/shapeshift': 3.9.0 + discord-api-types: 0.37.43 fast-deep-equal: 3.1.3 ts-mixer: 6.0.3 - tslib: 2.5.0 + tslib: 2.5.2 dev: false - /@discordjs/collection/0.7.0: - resolution: {integrity: sha512-R5i8Wb8kIcBAFEPLLf7LVBQKBDYUL+ekb23sOgpkpyGT+V4P7V83wTxcsqmX+PbqHt4cEHn053uMWfRqh/Z/nA==} + /@discordjs/collection@1.5.1: + resolution: {integrity: sha512-aWEc9DCf3TMDe9iaJoOnO2+JVAjeRNuRxPZQA6GVvBf+Z3gqUuWYBy2NWh4+5CLYq5uoc3MOvUQ5H5m8CJBqOA==} engines: {node: '>=16.9.0'} - deprecated: no longer supported dev: false - /@eartharoid/deep-merge/0.0.1: - resolution: {integrity: sha512-nxv2DRXgyqjNcbgDMOB2SoGlzPvge+yjZEI6+Ez+Ei5j00kAT4tzMG/mxseFhTRgZIlPgcaJ03THInVQnNmwqA==} + /@discordjs/formatters@0.3.1: + resolution: {integrity: sha512-M7X4IGiSeh4znwcRGcs+49B5tBkNDn4k5bmhxJDAUhRxRHTiFAOTVUNQ6yAKySu5jZTnCbSvTYHW3w0rAzV1MA==} + engines: {node: '>=16.9.0'} + dependencies: + discord-api-types: 0.37.43 dev: false - /@eartharoid/dtf/1.0.8: - resolution: {integrity: sha512-e3mR8JY6Uuy1Zj89iYEZfvK6s81GlWEPP4gO8NzfItzF4xFGFTVTwRjZ6sCVBmbhj3ouX2pyvx8O9snx4r5Xrg==} + /@discordjs/rest@1.7.1: + resolution: {integrity: sha512-Ofa9UqT0U45G/eX86cURQnX7gzOJLG2oC28VhIk/G6IliYgQF7jFByBJEykPSHE4MxPhqCleYvmsrtfKh1nYmQ==} + engines: {node: '>=16.9.0'} + dependencies: + '@discordjs/collection': 1.5.1 + '@discordjs/util': 0.3.1 + '@sapphire/async-queue': 1.5.0 + '@sapphire/snowflake': 3.5.1 + discord-api-types: 0.37.43 + file-type: 18.4.0 + tslib: 2.5.2 + undici: 5.22.1 dev: false - /@eartharoid/i18n/1.0.1: - resolution: {integrity: sha512-5Vl7RrRpSXEmza8bO2+4l4f5jwxfkUoiCzc7TEXyIPaG0lrjLLHwZE1aGuOpV67/HvsvAA2LQSKZcbt9I4LszA==} + /@discordjs/util@0.3.1: + resolution: {integrity: sha512-HxXKYKg7vohx2/OupUN/4Sd02Ev3PBJ5q0gtjdcvXb0ErCva8jNHWfe/v5sU3UKjIB/uxOhc+TDOnhqffj9pRA==} + engines: {node: '>=16.9.0'} dev: false - /@eslint/eslintrc/0.4.3: - resolution: {integrity: sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==} - engines: {node: ^10.12.0 || >=12.0.0} + /@discordjs/ws@0.8.3(bufferutil@4.0.7)(utf-8-validate@5.0.10): + resolution: {integrity: sha512-hcYtppanjHecbdNyCKQNH2I4RP9UrphDgmRgLYrATEQF1oo4sYSve7ZmGsBEXSzH72MO2tBPdWSThunbxUVk0g==} + engines: {node: '>=16.9.0'} + dependencies: + '@discordjs/collection': 1.5.1 + '@discordjs/rest': 1.7.1 + '@discordjs/util': 0.3.1 + '@sapphire/async-queue': 1.5.0 + '@types/ws': 8.5.4 + '@vladfrangu/async_event_emitter': 2.2.2 + discord-api-types: 0.37.43 + tslib: 2.5.2 + ws: 8.13.0(bufferutil@4.0.7)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /@eartharoid/dbf@0.4.0(bufferutil@4.0.7)(utf-8-validate@5.0.10): + resolution: {integrity: sha512-jKHLV/ZEdro8iVgmM38ZRCb2G7ZI3T1o5ibPjps/8LsiunMAI1OqdIYEdSswD0Amud4aStNkqVot63haxnG4vw==} + dependencies: + discord.js: 14.11.0(bufferutil@4.0.7)(utf-8-validate@5.0.10) + node-dir: 0.1.17 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /@eartharoid/deep-merge@0.0.2: + resolution: {integrity: sha512-t7kmNd6m7BOGxf25tE1YBhPZbgMEsXZT0tQyVV/Mlo+rcPEmiEEc6HV1DBnYm63MvMpgTk4o6yBkeAJYCMzvNg==} + dev: false + + /@eartharoid/dtf@2.0.1: + resolution: {integrity: sha512-H0oLRShkVqTskVRA8KMMIHjtwAu6Yc6GkxQm9cLPWMA7V0ZOxD5rg2PaZuQfDbt6UAk2sp7qb+k4ih5EHc/Jgg==} + dev: false + + /@eartharoid/i18n@1.2.1: + resolution: {integrity: sha512-nMQdHrGgpw+vNL5DbivELW2K3KAUGaMvTjjmFsEPf8mUW8+LAgRjvfFn2gkJq1mnlD6HoqUaHqEL4YpWr2T5MA==} + dev: false + + /@eslint-community/eslint-utils@4.4.0(eslint@8.41.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.41.0 + eslint-visitor-keys: 3.4.1 + dev: true + + /@eslint-community/regexpp@4.5.1: + resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.0.3: + resolution: {integrity: sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.2 - espree: 7.3.1 - globals: 13.11.0 - ignore: 4.0.6 + debug: 4.3.4 + espree: 9.5.2 + globals: 13.20.0 + ignore: 5.2.4 import-fresh: 3.3.0 - js-yaml: 3.14.1 - minimatch: 3.0.4 + js-yaml: 4.1.0 + minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color dev: true - /@humanwhocodes/config-array/0.5.0: - resolution: {integrity: sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==} + /@eslint/js@8.41.0: + resolution: {integrity: sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@fastify/ajv-compiler@3.5.0: + resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==} + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + fast-uri: 2.2.0 + dev: false + + /@fastify/cookie@6.0.0: + resolution: {integrity: sha512-Luy3Po3dOJmqAuPCiPcWsX0tV5+C3AOnULSdlsGjNGOvyE7jqzysp8kT9ICfsUvove+TeUMgTWl1y9XS3ZPPMg==} + dependencies: + cookie-signature: 1.2.1 + fastify-plugin: 3.0.1 + dev: false + + /@fastify/deepmerge@1.3.0: + resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} + dev: false + + /@fastify/error@3.2.0: + resolution: {integrity: sha512-KAfcLa+CnknwVi5fWogrLXgidLic+GXnLjijXdpl8pvkvbXU5BGa37iZO9FGvsh9ZL4y+oFi5cbHBm5UOG+dmQ==} + dev: false + + /@fastify/fast-json-stringify-compiler@4.3.0: + resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} + dependencies: + fast-json-stringify: 5.7.0 + dev: false + + /@fastify/jwt@5.0.1: + resolution: {integrity: sha512-BF2JrhSRcMRwHim2pbFEgWLJE8DdRh0NaGeAgU+ZFICj2AHxGvrHSOaYRaXNUQixgmGjVaTrbRFl3bSBc6Pj+g==} + engines: {node: '>=10'} + dependencies: + '@lukeed/ms': 2.0.1 + fast-jwt: 1.7.2 + fastify-plugin: 3.0.1 + http-errors: 2.0.0 + steed: 1.1.3 + dev: false + + /@fastify/oauth2@5.1.0: + resolution: {integrity: sha512-Z/2ZR8v2BB+vwFm1t0DnavxAKymCGvJiGD/lTtNV/dTPHTHRQ1X68hFs/Nl2Rd0Gv9AlSlrpYsX9hfoNjTSiYQ==} + dependencies: + fastify-plugin: 4.5.0 + simple-oauth2: 3.4.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@fortawesome/fontawesome-free@6.4.0: + resolution: {integrity: sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ==} + engines: {node: '>=6'} + requiresBuild: true + dev: false + + /@hapi/address@2.1.4: + resolution: {integrity: sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==} + deprecated: Moved to 'npm install @sideway/address' + dev: false + + /@hapi/boom@7.4.11: + resolution: {integrity: sha512-VSU/Cnj1DXouukYxxkes4nNJonCnlogHvIff1v1RVoN4xzkKhMXX+GRmb3NyH1iar10I9WFPDv2JPwfH3GaV0A==} + deprecated: This version has been deprecated and is no longer supported or maintained + dependencies: + '@hapi/hoek': 8.5.1 + dev: false + + /@hapi/bourne@1.3.2: + resolution: {integrity: sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==} + deprecated: This version has been deprecated and is no longer supported or maintained + dev: false + + /@hapi/formula@1.2.0: + resolution: {integrity: sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA==} + deprecated: Moved to 'npm install @sideway/formula' + dev: false + + /@hapi/hoek@8.5.1: + resolution: {integrity: sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==} + deprecated: This version has been deprecated and is no longer supported or maintained + dev: false + + /@hapi/joi@16.1.8: + resolution: {integrity: sha512-wAsVvTPe+FwSrsAurNt5vkg3zo+TblvC5Bb1zMVK6SJzZqw9UrJnexxR+76cpePmtUZKHAPxcQ2Bf7oVHyahhg==} + deprecated: Switch to 'npm install joi' + dependencies: + '@hapi/address': 2.1.4 + '@hapi/formula': 1.2.0 + '@hapi/hoek': 8.5.1 + '@hapi/pinpoint': 1.0.2 + '@hapi/topo': 3.1.6 + dev: false + + /@hapi/pinpoint@1.0.2: + resolution: {integrity: sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ==} + deprecated: Moved to 'npm install @sideway/pinpoint' + dev: false + + /@hapi/topo@3.1.6: + resolution: {integrity: sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==} + deprecated: This version has been deprecated and is no longer supported or maintained + dependencies: + '@hapi/hoek': 8.5.1 + dev: false + + /@hapi/wreck@15.1.0: + resolution: {integrity: sha512-tQczYRTTeYBmvhsek/D49En/5khcShaBEmzrAaDjMrFXKJRuF8xA8+tlq1ETLBFwGd6Do6g2OC74rt11kzawzg==} + deprecated: This version has been deprecated and is no longer supported or maintained + dependencies: + '@hapi/boom': 7.4.11 + '@hapi/bourne': 1.3.2 + '@hapi/hoek': 8.5.1 + dev: false + + /@humanwhocodes/config-array@0.11.8: + resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==} engines: {node: '>=10.10.0'} dependencies: - '@humanwhocodes/object-schema': 1.2.0 - debug: 4.3.2 - minimatch: 3.0.4 + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4 + minimatch: 3.1.2 transitivePeerDependencies: - supports-color dev: true - /@humanwhocodes/object-schema/1.2.0: - resolution: {integrity: sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==} + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} dev: true - /@js-joda/core/3.2.0: - resolution: {integrity: sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==} + /@humanwhocodes/object-schema@1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true - /@opentelemetry/api/1.0.3: - resolution: {integrity: sha512-puWxACExDe9nxbBB3lOymQFrLYml2dVOrd7USiVRnSbgXE+KwBu+HxFvxrzfqsiSda9IWsXJG1ef7C1O2/GmKQ==} - engines: {node: '>=8.0.0'} + /@hutson/parse-repository-url@3.0.2: + resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} + engines: {node: '>=6.9.0'} + dev: true - /@sapphire/async-queue/1.5.0: + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@lukeed/ms@2.0.1: + resolution: {integrity: sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==} + engines: {node: '>=8'} + dev: false + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@prisma/client@4.14.1(prisma@4.14.1): + resolution: {integrity: sha512-TZIswkeX1ccsHG/eN2kICzg/csXll0osK3EHu1QKd8VJ3XLcXozbNELKkCNfsCUvKJAwPdDtFCzF+O+raIVldw==} + engines: {node: '>=14.17'} + requiresBuild: true + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + dependencies: + '@prisma/engines-version': 4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c + prisma: 4.14.1 + dev: false + + /@prisma/engines-version@4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c: + resolution: {integrity: sha512-3jum8/YSudeSN0zGW5qkpz+wAN2V/NYCQ+BPjvHYDfWatLWlQkqy99toX0GysDeaUoBIJg1vaz2yKqiA3CFcQw==} + dev: false + + /@prisma/engines@4.14.1: + resolution: {integrity: sha512-APqFddPVHYmWNKqc+5J5SqrLFfOghKOLZxobmguDUacxOwdEutLsbXPVhNnpFDmuQWQFbXmrTTPoRrrF6B1MWA==} + requiresBuild: true + dev: false + + /@sapphire/async-queue@1.5.0: resolution: {integrity: sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} dev: false - /@sapphire/shapeshift/3.8.1: - resolution: {integrity: sha512-xG1oXXBhCjPKbxrRTlox9ddaZTvVpOhYLmKmApD/vIWOV1xEYXnpoFs68zHIZBGbqztq6FrUPNPerIrO1Hqeaw==} + /@sapphire/shapeshift@3.9.0: + resolution: {integrity: sha512-iJpHmjAdwX9aSL6MvFpVyo+tkokDtInmSjoJHbz/k4VJfnim3DjvG0hgGEKWtWZgCu45RaLgcoNgR1fCPdIz3w==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} dependencies: fast-deep-equal: 3.1.3 lodash: 4.17.21 dev: false - /@sindresorhus/is/0.14.0: - resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} - engines: {node: '>=6'} - dev: true - - /@szmarczak/http-timer/1.1.2: - resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} - engines: {node: '>=6'} - dependencies: - defer-to-connect: 1.1.3 - dev: true - - /@tootallnate/once/1.1.2: - resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} - engines: {node: '>= 6'} - - /@types/geojson/7946.0.8: - resolution: {integrity: sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==} - - /@types/keyv/3.1.4: - resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - dependencies: - '@types/node': 16.10.3 - dev: true - - /@types/node-fetch/2.6.3: - resolution: {integrity: sha512-ETTL1mOEdq/sxUtgtOhKjyB2Irra4cjxksvcMUR5Zr4n+PxVhsCD9WS46oPbHL3et9Zde7CNRr+WUNlcHvsX+w==} - dependencies: - '@types/node': 16.10.3 - form-data: 3.0.1 - - /@types/node/14.17.21: - resolution: {integrity: sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA==} - - /@types/node/16.10.3: - resolution: {integrity: sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==} - - /@types/responselike/1.0.0: - resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} - dependencies: - '@types/node': 16.10.3 - dev: true - - /@types/stoppable/1.1.1: - resolution: {integrity: sha512-b8N+fCADRIYYrGZOcmOR8ZNBOqhktWTB/bMUl5LvGtT201QKJZOOH5UsFyI3qtteM6ZAJbJqZoBcLqqxKIwjhw==} - dependencies: - '@types/node': 16.10.3 - - /@types/tunnel/0.0.3: - resolution: {integrity: sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==} - dependencies: - '@types/node': 16.10.3 - - /@types/ws/8.5.4: - resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} - dependencies: - '@types/node': 16.10.3 + /@sapphire/snowflake@3.5.1: + resolution: {integrity: sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} dev: false - /@xmldom/xmldom/0.7.5: - resolution: {integrity: sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==} - engines: {node: '>=10.0.0'} + /@skyra/discord-components-core@3.6.0: + resolution: {integrity: sha512-13aVNoleHyAMqd4lcfcfjEduV4QavlHn9P2TUlTUPQC8m8cd8n8wSUTDUxKhQbYpzJ0hn7AHnlmLixfiqQT4FQ==} + engines: {node: '>=v14.0.0'} + dependencies: + '@stencil/core': 2.22.3 + clsx: 1.2.1 + hex-to-rgba: 2.0.1 + dev: false - /abbrev/1.1.1: + /@stencil/core@2.22.3: + resolution: {integrity: sha512-kmVA0M/HojwsfkeHsifvHVIYe4l5tin7J5+DLgtl8h6WWfiMClND5K3ifCXXI2ETDNKiEk21p6jql3Fx9o2rng==} + engines: {node: '>=12.10.0', npm: '>=6.0.0'} + hasBin: true + dev: false + + /@tokenizer/token@0.3.0: + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + dev: false + + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + + /@types/minimist@1.2.2: + resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} + dev: true + + /@types/node@20.2.5: + resolution: {integrity: sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==} + + /@types/normalize-package-data@2.4.1: + resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + dev: true + + /@types/ws@8.5.4: + resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} + dependencies: + '@types/node': 20.2.5 + dev: false + + /@vladfrangu/async_event_emitter@2.2.2: + resolution: {integrity: sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + dev: false + + /JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + dev: true + + /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: true - /abort-controller/3.0.0: + /abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} dependencies: event-target-shim: 5.0.1 + dev: false - /acorn-jsx/5.3.2_acorn@7.4.1: + /abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + dev: false + + /acorn-jsx@5.3.2(acorn@8.8.2): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 7.4.1 + acorn: 8.8.2 dev: true - /acorn/7.4.1: - resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + /acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@8.8.2: + resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} engines: {node: '>=0.4.0'} hasBin: true dev: true - /adal-node/0.2.3: - resolution: {integrity: sha512-gMKr8RuYEYvsj7jyfCv/4BfKToQThz20SP71N3AtFn3ia3yAR8Qt2T3aVQhuJzunWs2b38ZsQV0qsZPdwZr7VQ==} - engines: {node: '>= 0.6.15'} - dependencies: - '@xmldom/xmldom': 0.7.5 - async: 2.6.3 - axios: 0.21.4 - date-utils: 1.2.21 - jws: 3.2.2 - underscore: 1.13.1 - uuid: 3.4.0 - xpath.js: 1.1.0 - transitivePeerDependencies: - - debug + /add-stream@1.0.0: + resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==} + dev: true - /agent-base/6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} + /aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} dependencies: - debug: 4.3.2 - transitivePeerDependencies: - - supports-color + clean-stack: 2.2.0 + indent-string: 4.0.0 + dev: true - /ajv/6.12.6: + /ajv-formats@2.1.1(ajv@8.12.0): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.12.0 + dev: false + + /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 + dev: true - /ajv/8.6.3: - resolution: {integrity: sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==} + /ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} dependencies: fast-deep-equal: 3.1.3 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 uri-js: 4.4.1 - dev: true - /all-contributors-cli/6.20.0: - resolution: {integrity: sha512-trEQlL1s1u8FSWSwY2w9uL4GCG7Fo9HIW5rm5LtlE0SQHSolfXQBzJib07Qes5j52/t72wjuE6sEKkuRrwiuuQ==} + /all-contributors-cli@6.26.0: + resolution: {integrity: sha512-HOMfawD0XyNbOvLUn7rOAP5N9RLnbH+Y/9/IoxwPzCmy6srHSFyRMwbpD0H7Tw+1QzdJT8RH7bTe1IZkPhF+NQ==} engines: {node: '>=4'} hasBin: true dependencies: - '@babel/runtime': 7.15.4 - async: 3.2.1 + '@babel/runtime': 7.22.3 + async: 3.2.4 chalk: 4.1.2 didyoumean: 1.2.2 inquirer: 7.3.3 - json-fixer: 1.6.12 + json-fixer: 1.6.15 lodash: 4.17.21 - node-fetch: 2.6.5 + node-fetch: 2.6.11 pify: 5.0.0 yargs: 15.4.1 + optionalDependencies: + prettier: 2.8.8 + transitivePeerDependencies: + - encoding dev: true - /ansi-align/3.0.1: + /ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} dependencies: string-width: 4.2.3 + dev: false - /ansi-colors/4.1.1: - resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} - engines: {node: '>=6'} - dev: true - - /ansi-escapes/4.3.2: + /ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} dependencies: type-fest: 0.21.3 - /ansi-regex/2.1.1: - resolution: {integrity: sha1-w7M6te42DYbg5ijwRorn7yfWVN8=} - engines: {node: '>=0.10.0'} - - /ansi-regex/5.0.1: + /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - /ansi-styles/3.2.1: + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} dependencies: color-convert: 1.9.3 dev: true - /ansi-styles/4.3.0: + /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} dependencies: color-convert: 2.0.1 + dev: true - /any-promise/1.3.0: - resolution: {integrity: sha1-q8av7tzqUugJzcA3au0845Y10X8=} - dev: false + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} - /anymatch/3.1.2: - resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} dependencies: normalize-path: 3.0.0 - picomatch: 2.3.0 + picomatch: 2.3.1 dev: true - /aproba/1.2.0: - resolution: {integrity: sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==} + /archy@1.0.0: + resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} + dev: false - /are-we-there-yet/1.1.7: - resolution: {integrity: sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==} - dependencies: - delegates: 1.0.0 - readable-stream: 2.3.7 - - /argparse/1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - dependencies: - sprintf-js: 1.0.3 + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: true - /asn1/0.2.4: - resolution: {integrity: sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==} + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + dev: true + + /arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: true + + /asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} dependencies: + bn.js: 4.12.0 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 safer-buffer: 2.1.2 dev: false - optional: true - /assert-plus/1.0.0: - resolution: {integrity: sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=} - engines: {node: '>=0.8'} - dev: false - optional: true - - /astral-regex/2.0.0: + /astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} dev: true - /async/2.6.3: - resolution: {integrity: sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==} - dependencies: - lodash: 4.17.21 - - /async/3.2.1: - resolution: {integrity: sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==} + /async@3.2.4: + resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} dev: true - /asynckit/0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - /aws-sign2/0.7.0: - resolution: {integrity: sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=} + /atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} dev: false - optional: true - /aws4/1.11.0: - resolution: {integrity: sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==} - dev: false - optional: true - - /axios/0.21.4: - resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} + /avvio@8.2.1: + resolution: {integrity: sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==} dependencies: - follow-redirects: 1.14.4 + archy: 1.0.0 + debug: 4.3.4 + fastq: 1.15.0 transitivePeerDependencies: - - debug + - supports-color + dev: false - /balanced-match/1.0.2: + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - /base64-js/1.5.1: + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - /bcrypt-pbkdf/1.0.2: - resolution: {integrity: sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=} - dependencies: - tweetnacl: 0.14.5 dev: false - optional: true - /binary-extensions/2.2.0: + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} dev: true - /bl/4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + /bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.0 - optional: true - - /bl/5.0.0: - resolution: {integrity: sha512-8vxFNZ0pflFfi0WXA3WQXlj6CaMEwsmh63I1CNp0q+wWv8sD0ARx1KovSQd0l2GkwrMIOyedq0EF1FxI+RCZLQ==} - dependencies: - buffer: 6.0.3 - inherits: 2.0.4 - readable-stream: 3.6.0 - - /block-stream/0.0.9: - resolution: {integrity: sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=} - engines: {node: 0.4 || >=0.5.8} - dependencies: - inherits: 2.0.4 + file-uri-to-path: 1.0.0 dev: false optional: true - /boxen/5.1.2: - resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} - engines: {node: '>=10'} + /bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + dev: false + + /boxen@7.1.0: + resolution: {integrity: sha512-ScG8CDo8dj7McqCZ5hz4dIBp20xj4unQ2lXIDa7ff6RcZElCpuNzutdwzKVvRikfNjm7CFAlR3HJHcoHkDOExQ==} + engines: {node: '>=14.16'} dependencies: ansi-align: 3.0.1 - camelcase: 6.2.0 - chalk: 4.1.2 - cli-boxes: 2.2.1 - string-width: 4.2.3 - type-fest: 0.20.2 - widest-line: 3.1.0 - wrap-ansi: 7.0.0 + camelcase: 7.0.1 + chalk: 5.2.0 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + dev: false - /brace-expansion/1.1.11: + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - /braces/3.0.2: + /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} engines: {node: '>=8'} dependencies: fill-range: 7.0.1 dev: true - /buffer-equal-constant-time/1.0.1: - resolution: {integrity: sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=} - - /buffer-writer/2.0.0: - resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} - engines: {node: '>=4'} - - /buffer/5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - optional: true - - /buffer/6.0.3: + /buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + dev: false - /cacheable-request/6.1.0: - resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} - engines: {node: '>=8'} + /bufferutil@4.0.7: + resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==} + engines: {node: '>=6.14.2'} + requiresBuild: true dependencies: - clone-response: 1.0.2 - get-stream: 5.2.0 - http-cache-semantics: 4.1.0 - keyv: 3.1.0 - lowercase-keys: 2.0.0 - normalize-url: 4.5.1 - responselike: 1.0.2 - dev: true + node-gyp-build: 4.6.0 + dev: false - /call-bind/1.0.2: - resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + /busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} dependencies: - function-bind: 1.1.1 - get-intrinsic: 1.1.1 + streamsearch: 1.1.0 + dev: false - /callsites/3.1.0: + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} dev: true - /camelcase/5.3.1: + /camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + dev: true + + /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} dev: true - /camelcase/6.2.0: - resolution: {integrity: sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==} - engines: {node: '>=10'} - - /caseless/0.12.0: - resolution: {integrity: sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=} + /camelcase@7.0.1: + resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} + engines: {node: '>=14.16'} dev: false - optional: true - /chalk/2.4.2: + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} dependencies: @@ -743,22 +1058,27 @@ packages: supports-color: 5.5.0 dev: true - /chalk/4.1.2: + /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + dev: true - /chardet/0.7.0: + /chalk@5.2.0: + resolution: {integrity: sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + /chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true - /chokidar/3.5.2: - resolution: {integrity: sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==} + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} dependencies: - anymatch: 3.1.2 + anymatch: 3.1.3 braces: 3.0.2 glob-parent: 5.1.2 is-binary-path: 2.1.0 @@ -769,30 +1089,45 @@ packages: fsevents: 2.3.2 dev: true - /chownr/1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - - /ci-info/2.0.0: - resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + /clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} dev: true - /cli-boxes/2.2.1: - resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} - engines: {node: '>=6'} + /cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + dev: false - /cli-cursor/3.1.0: + /cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} dependencies: restore-cursor: 3.1.0 dev: true - /cli-width/3.0.0: + /cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + dev: true + + /cli-truncate@3.1.0: + resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + slice-ansi: 5.0.0 + string-width: 5.1.2 + dev: true + + /cli-width@3.0.0: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} dev: true - /cliui/6.0.0: + /cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} dependencies: string-width: 4.2.3 @@ -800,68 +1135,279 @@ packages: wrap-ansi: 6.2.0 dev: true - /clone-response/1.0.2: - resolution: {integrity: sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=} + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} dependencies: - mimic-response: 1.0.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 dev: true - /code-point-at/1.1.0: - resolution: {integrity: sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=} - engines: {node: '>=0.10.0'} + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true - /color-convert/1.9.3: + /clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + dev: false + + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: color-name: 1.1.3 dev: true - /color-convert/2.0.1: + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} dependencies: color-name: 1.1.4 - - /color-name/1.1.3: - resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=} dev: true - /color-name/1.1.4: + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - /combined-stream/1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - dependencies: - delayed-stream: 1.0.0 - - /concat-map/0.0.1: - resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} - - /configstore/5.0.1: - resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} - engines: {node: '>=8'} - dependencies: - dot-prop: 5.3.0 - graceful-fs: 4.2.8 - make-dir: 3.1.0 - unique-string: 2.0.0 - write-file-atomic: 3.0.3 - xdg-basedir: 4.0.0 dev: true - /console-control-strings/1.1.0: - resolution: {integrity: sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=} + /colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + dev: true - /core-util-is/1.0.2: - resolution: {integrity: sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=} + /commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + dev: true + + /compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /conventional-changelog-angular@5.0.13: + resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} + engines: {node: '>=10'} + dependencies: + compare-func: 2.0.0 + q: 1.5.1 + dev: true + + /conventional-changelog-atom@2.0.8: + resolution: {integrity: sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-cli@2.2.2: + resolution: {integrity: sha512-8grMV5Jo8S0kP3yoMeJxV2P5R6VJOqK72IiSV9t/4H5r/HiRqEBQ83bYGuz4Yzfdj4bjaAEhZN/FFbsFXr5bOA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + add-stream: 1.0.0 + conventional-changelog: 3.1.25 + lodash: 4.17.21 + meow: 8.1.2 + tempfile: 3.0.0 + dev: true + + /conventional-changelog-codemirror@2.0.8: + resolution: {integrity: sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-conventionalcommits@4.6.3: + resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==} + engines: {node: '>=10'} + dependencies: + compare-func: 2.0.0 + lodash: 4.17.21 + q: 1.5.1 + dev: true + + /conventional-changelog-conventionalcommits@5.0.0: + resolution: {integrity: sha512-lCDbA+ZqVFQGUj7h9QBKoIpLhl8iihkO0nCTyRNzuXtcd7ubODpYB04IFy31JloiJgG0Uovu8ot8oxRzn7Nwtw==} + engines: {node: '>=10'} + dependencies: + compare-func: 2.0.0 + lodash: 4.17.21 + q: 1.5.1 + dev: true + + /conventional-changelog-core@4.2.4: + resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==} + engines: {node: '>=10'} + dependencies: + add-stream: 1.0.0 + conventional-changelog-writer: 5.0.1 + conventional-commits-parser: 3.2.4 + dateformat: 3.0.3 + get-pkg-repo: 4.2.1 + git-raw-commits: 2.0.11 + git-remote-origin-url: 2.0.0 + git-semver-tags: 4.1.1 + lodash: 4.17.21 + normalize-package-data: 3.0.3 + q: 1.5.1 + read-pkg: 3.0.0 + read-pkg-up: 3.0.0 + through2: 4.0.2 + dev: true + + /conventional-changelog-ember@2.0.9: + resolution: {integrity: sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-eslint@3.0.9: + resolution: {integrity: sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-express@2.0.6: + resolution: {integrity: sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-jquery@3.0.11: + resolution: {integrity: sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-jshint@2.0.9: + resolution: {integrity: sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==} + engines: {node: '>=10'} + dependencies: + compare-func: 2.0.0 + q: 1.5.1 + dev: true + + /conventional-changelog-preset-loader@2.3.4: + resolution: {integrity: sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==} + engines: {node: '>=10'} + dev: true + + /conventional-changelog-writer@5.0.1: + resolution: {integrity: sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==} + engines: {node: '>=10'} + hasBin: true + dependencies: + conventional-commits-filter: 2.0.7 + dateformat: 3.0.3 + handlebars: 4.7.7 + json-stringify-safe: 5.0.1 + lodash: 4.17.21 + meow: 8.1.2 + semver: 6.3.0 + split: 1.0.1 + through2: 4.0.2 + dev: true + + /conventional-changelog@3.1.25: + resolution: {integrity: sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==} + engines: {node: '>=10'} + dependencies: + conventional-changelog-angular: 5.0.13 + conventional-changelog-atom: 2.0.8 + conventional-changelog-codemirror: 2.0.8 + conventional-changelog-conventionalcommits: 4.6.3 + conventional-changelog-core: 4.2.4 + conventional-changelog-ember: 2.0.9 + conventional-changelog-eslint: 3.0.9 + conventional-changelog-express: 2.0.6 + conventional-changelog-jquery: 3.0.11 + conventional-changelog-jshint: 2.0.9 + conventional-changelog-preset-loader: 2.3.4 + dev: true + + /conventional-commits-filter@2.0.7: + resolution: {integrity: sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==} + engines: {node: '>=10'} + dependencies: + lodash.ismatch: 4.4.0 + modify-values: 1.0.1 + dev: true + + /conventional-commits-parser@3.2.4: + resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==} + engines: {node: '>=10'} + hasBin: true + dependencies: + JSONStream: 1.3.5 + is-text-path: 1.0.1 + lodash: 4.17.21 + meow: 8.1.2 + split2: 3.2.2 + through2: 4.0.2 + dev: true + + /cookie-signature@1.2.1: + resolution: {integrity: sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==} + engines: {node: '>=6.6.0'} dev: false - optional: true - /core-util-is/1.0.3: + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + + /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: true - /cross-spawn/7.0.3: + /cosmiconfig-typescript-loader@4.3.0(@types/node@20.2.5)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@5.0.4): + resolution: {integrity: sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=7' + ts-node: '>=10' + typescript: '>=3' + dependencies: + '@types/node': 20.2.5 + cosmiconfig: 8.1.3 + ts-node: 10.9.1(@types/node@20.2.5)(typescript@5.0.4) + typescript: 5.0.4 + dev: true + + /cosmiconfig@8.1.3: + resolution: {integrity: sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==} + engines: {node: '>=14'} + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + dev: true + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} dependencies: @@ -870,51 +1416,27 @@ packages: which: 2.0.2 dev: true - /crypto-random-string/2.0.0: - resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + /cryptr@6.2.0: + resolution: {integrity: sha512-jYi8SxvOFebTT7EYOABiPpHKY6lwWaP9IVcvT/aIVJUVoFdzTgi0ySPCL78q1ig8w2kwfXFCZACXoCXaye57aw==} + dev: false + + /dargs@7.0.0: + resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} engines: {node: '>=8'} dev: true - /cryptr/6.0.2: - resolution: {integrity: sha512-1TRHI4bmuLIB8WgkH9eeYXzhEg1T4tonO4vVaMBKKde8Dre51J68nAgTVXTwMYXAf7+mV2gBCkm/9wksjSb2sA==} + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.22.3 dev: false - /dashdash/1.14.1: - resolution: {integrity: sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=} - engines: {node: '>=0.10'} - dependencies: - assert-plus: 1.0.0 - dev: false - optional: true - - /date-utils/1.2.21: - resolution: {integrity: sha1-YfsWzcEnSzyayq/+n8ad+HIKK2Q=} - engines: {node: '>0.4.0'} - - /debug/2.6.9_supports-color@5.5.0: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.0.0 - supports-color: 5.5.0 + /dateformat@3.0.3: + resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} dev: true - /debug/3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - dev: false - - /debug/3.2.7_supports-color@5.5.0: + /debug@3.2.7(supports-color@5.5.0): resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: supports-color: '*' @@ -926,8 +1448,8 @@ packages: supports-color: 5.5.0 dev: true - /debug/4.3.2: - resolution: {integrity: sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==} + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -937,295 +1459,285 @@ packages: dependencies: ms: 2.1.2 - /decamelize/1.2.0: - resolution: {integrity: sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=} + /decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + dev: true + + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} dev: true - /decompress-response/3.3.0: - resolution: {integrity: sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=} - engines: {node: '>=4'} - dependencies: - mimic-response: 1.0.1 - dev: true - - /decompress-response/4.2.1: - resolution: {integrity: sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==} - engines: {node: '>=8'} - dependencies: - mimic-response: 2.1.0 - optional: true - - /deep-extend/0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - - /deep-is/0.1.4: + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true - /defer-to-connect/1.1.3: - resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} - dev: true - - /delayed-stream/1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - - /delegates/1.0.0: - resolution: {integrity: sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=} - - /denque/1.5.1: - resolution: {integrity: sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==} - engines: {node: '>=0.10'} - - /depd/2.0.0: + /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dev: false - /detect-libc/1.0.3: - resolution: {integrity: sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=} - engines: {node: '>=0.10'} - hasBin: true - - /didyoumean/1.2.2: + /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true - /discord-api-types/0.33.5: - resolution: {integrity: sha512-dvO5M52v7m7Dy96+XUnzXNsQ/0npsYpU6dL205kAtEDueswoz3aU3bh1UMoK4cQmcGtB1YRyLKqp+DXi05lzFg==} + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + + /discord-api-types@0.37.43: + resolution: {integrity: sha512-bBhDWU3TF9KADxR/mHp1K4Bvu/LRtFQdGyBjADu4e66F3ZnD4kp12W/SJCttIaCcMXzPV3sfty6eDGRNRph51Q==} dev: false - /discord-api-types/0.36.3: - resolution: {integrity: sha512-bz/NDyG0KBo/tY14vSkrwQ/n3HKPf87a0WFW/1M9+tXYK+vp5Z5EksawfCWo2zkAc6o7CClc0eff1Pjrqznlwg==} - dev: false - - /discord.js/13.14.0: - resolution: {integrity: sha512-EXHAZmFHMf6qBHDsIANwSG792SYJpzEFv2nssfakyDqEn0HLxFLLXMaOxBtVohdkUMgtD+dzyeBlbDvAW/A0AA==} - engines: {node: '>=16.6.0', npm: '>=7.0.0'} + /discord.js@14.11.0(bufferutil@4.0.7)(utf-8-validate@5.0.10): + resolution: {integrity: sha512-CkueWYFQ28U38YPR8HgsBR/QT35oPpMbEsTNM30Fs8loBIhnA4s70AwQEoy6JvLcpWWJO7GY0y2BUzZmuBMepQ==} + engines: {node: '>=16.9.0'} dependencies: - '@discordjs/builders': 0.16.0 - '@discordjs/collection': 0.7.0 - '@sapphire/async-queue': 1.5.0 - '@types/node-fetch': 2.6.3 + '@discordjs/builders': 1.6.3 + '@discordjs/collection': 1.5.1 + '@discordjs/formatters': 0.3.1 + '@discordjs/rest': 1.7.1 + '@discordjs/util': 0.3.1 + '@discordjs/ws': 0.8.3(bufferutil@4.0.7)(utf-8-validate@5.0.10) + '@sapphire/snowflake': 3.5.1 '@types/ws': 8.5.4 - discord-api-types: 0.33.5 - form-data: 4.0.0 - node-fetch: 2.6.9 - ws: 8.13.0 + discord-api-types: 0.37.43 + fast-deep-equal: 3.1.3 + lodash.snakecase: 4.1.1 + tslib: 2.5.2 + undici: 5.22.1 + ws: 8.13.0(bufferutil@4.0.7)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - - encoding - utf-8-validate dev: false - /doctrine/3.0.0: + /doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} dependencies: esutils: 2.0.3 dev: true - /dot-prop/5.3.0: + /dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} dependencies: is-obj: 2.0.0 dev: true - /dotenv/8.6.0: - resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} - engines: {node: '>=10'} + /dotenv@16.0.3: + resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} + engines: {node: '>=12'} dev: false - /dottie/2.0.2: - resolution: {integrity: sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==} - dev: false + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - /duplexer3/0.1.4: - resolution: {integrity: sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=} - dev: true - - /ecc-jsbn/0.1.2: - resolution: {integrity: sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=} - dependencies: - jsbn: 0.1.1 - safer-buffer: 2.1.2 - dev: false - optional: true - - /ecdsa-sig-formatter/1.0.11: + /ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} dependencies: safe-buffer: 5.2.1 + dev: false - /emoji-regex/8.0.0: + /emoji-name-map@1.2.9: + resolution: {integrity: sha512-MSM8y6koSqh/2uEMI2VoKA+Ac0qL5RkgFGP/pzL6n5FOrOJ7FOZFxgs7+uNpqA+AT+WmdbMPXkd3HnFXXdz4AA==} + dependencies: + emojilib: 2.4.0 + iterate-object: 1.3.4 + map-o: 2.0.10 + dev: false + + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - /end-of-stream/1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - dependencies: - once: 1.4.0 + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - /enquirer/2.3.6: - resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} - engines: {node: '>=8.6'} + /emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + dev: false + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: - ansi-colors: 4.1.1 + is-arrayish: 0.2.1 dev: true - /escape-goat/2.1.1: - resolution: {integrity: sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==} - engines: {node: '>=8'} + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} dev: true - /escape-string-regexp/1.0.5: - resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=} + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} dev: true - /escape-string-regexp/4.0.0: + /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} dev: true - /eslint-scope/5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} + /eslint-plugin-unused-imports@2.0.0(eslint@8.41.0): + resolution: {integrity: sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.0.0 + eslint: ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + dependencies: + eslint: 8.41.0 + eslint-rule-composer: 0.3.0 + dev: true + + /eslint-rule-composer@0.3.0: + resolution: {integrity: sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==} + engines: {node: '>=4.0.0'} + dev: true + + /eslint-scope@7.2.0: + resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: esrecurse: 4.3.0 - estraverse: 4.3.0 + estraverse: 5.3.0 dev: true - /eslint-utils/2.1.0: - resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} - engines: {node: '>=6'} - dependencies: - eslint-visitor-keys: 1.3.0 + /eslint-visitor-keys@3.4.1: + resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint-visitor-keys/1.3.0: - resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} - engines: {node: '>=4'} - dev: true - - /eslint-visitor-keys/2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - dev: true - - /eslint/7.32.0: - resolution: {integrity: sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==} - engines: {node: ^10.12.0 || >=12.0.0} + /eslint@8.41.0: + resolution: {integrity: sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@babel/code-frame': 7.12.11 - '@eslint/eslintrc': 0.4.3 - '@humanwhocodes/config-array': 0.5.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.41.0) + '@eslint-community/regexpp': 4.5.1 + '@eslint/eslintrc': 2.0.3 + '@eslint/js': 8.41.0 + '@humanwhocodes/config-array': 0.11.8 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.2 + debug: 4.3.4 doctrine: 3.0.0 - enquirer: 2.3.6 escape-string-regexp: 4.0.0 - eslint-scope: 5.1.1 - eslint-utils: 2.1.0 - eslint-visitor-keys: 2.1.0 - espree: 7.3.1 - esquery: 1.4.0 + eslint-scope: 7.2.0 + eslint-visitor-keys: 3.4.1 + espree: 9.5.2 + esquery: 1.5.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 6.0.1 - functional-red-black-tree: 1.0.1 - glob-parent: 5.1.2 - globals: 13.11.0 - ignore: 4.0.6 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.20.0 + graphemer: 1.4.0 + ignore: 5.2.4 import-fresh: 3.3.0 imurmurhash: 0.1.4 is-glob: 4.0.3 - js-yaml: 3.14.1 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 levn: 0.4.1 lodash.merge: 4.6.2 - minimatch: 3.0.4 + minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.1 - progress: 2.0.3 - regexpp: 3.2.0 - semver: 7.3.5 strip-ansi: 6.0.1 strip-json-comments: 3.1.1 - table: 6.7.2 text-table: 0.2.0 - v8-compile-cache: 2.3.0 transitivePeerDependencies: - supports-color dev: true - /espree/7.3.1: - resolution: {integrity: sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==} - engines: {node: ^10.12.0 || >=12.0.0} + /espree@9.5.2: + resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 7.4.1 - acorn-jsx: 5.3.2_acorn@7.4.1 - eslint-visitor-keys: 1.3.0 + acorn: 8.8.2 + acorn-jsx: 5.3.2(acorn@8.8.2) + eslint-visitor-keys: 3.4.1 dev: true - /esprima/4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - dev: true - - /esquery/1.4.0: - resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} dependencies: - estraverse: 5.2.0 + estraverse: 5.3.0 dev: true - /esrecurse/4.3.0: + /esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} dependencies: - estraverse: 5.2.0 + estraverse: 5.3.0 dev: true - /estraverse/4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} dev: true - /estraverse/5.2.0: - resolution: {integrity: sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==} - engines: {node: '>=4.0'} - dev: true - - /esutils/2.0.3: + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} dev: true - /event-target-shim/5.0.1: + /event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + dev: false - /events/3.3.0: + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - - /expand-template/2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - optional: true - - /extend/3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} dev: false - optional: true - /external-editor/3.1.0: + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /execa@7.1.1: + resolution: {integrity: sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==} + engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 4.3.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.1.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + dev: true + + /external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} dependencies: @@ -1234,44 +1746,175 @@ packages: tmp: 0.0.33 dev: true - /extsprintf/1.3.0: - resolution: {integrity: sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=} - engines: {'0': node >=0.6.0} + /fast-content-type-parse@1.0.0: + resolution: {integrity: sha512-Xbc4XcysUXcsP5aHUU7Nq3OwvHq97C+WnbkeIefpeYLX+ryzFJlU6OStFJhs6Ol0LkUGpcK+wL0JwfM+FCU5IA==} dev: false - optional: true - /fast-deep-equal/3.1.3: + /fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + dev: false + + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - /fast-json-stable-stringify/2.1.0: + /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - /fast-levenshtein/2.0.6: - resolution: {integrity: sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=} dev: true - /figures/3.2.0: + /fast-json-stringify@5.7.0: + resolution: {integrity: sha512-sBVPTgnAZseLu1Qgj6lUbQ0HfjFhZWXAmpZ5AaSGkyLh5gAXBga/uPJjQPHpDFjC9adWIpdOcCLSDTgrZ7snoQ==} + dependencies: + '@fastify/deepmerge': 1.3.0 + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + fast-deep-equal: 3.1.3 + fast-uri: 2.2.0 + rfdc: 1.3.0 + dev: false + + /fast-jwt@1.7.2: + resolution: {integrity: sha512-OEInypGXJhtURzq9GbFM5KaALUu9+4IV3kJEbWPuqOBN5JBe7A51Tx0CaQYHGC9GNfZnr5npA0lCIMaWiZmz/A==} + engines: {node: '>=14 <20'} + dependencies: + asn1.js: 5.4.1 + ecdsa-sig-formatter: 1.0.11 + mnemonist: 0.39.5 + dev: false + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fast-querystring@1.1.1: + resolution: {integrity: sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q==} + dependencies: + fast-decode-uri-component: 1.0.1 + dev: false + + /fast-redact@3.2.0: + resolution: {integrity: sha512-zaTadChr+NekyzallAMXATXLOR8MNx3zqpZ0MUF2aGf4EathnG0f32VLODNlY8IuGY3HoRO2L6/6fSzNsLaHIw==} + engines: {node: '>=6'} + dev: false + + /fast-uri@2.2.0: + resolution: {integrity: sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==} + dev: false + + /fastfall@1.5.1: + resolution: {integrity: sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==} + engines: {node: '>=0.10.0'} + dependencies: + reusify: 1.0.4 + dev: false + + /fastify-plugin@3.0.1: + resolution: {integrity: sha512-qKcDXmuZadJqdTm6vlCqioEbyewF60b/0LOFCcYN1B6BIZGlYJumWWOYs70SFYLDAH4YqdE1cxH/RKMG7rFxgA==} + dev: false + + /fastify-plugin@4.5.0: + resolution: {integrity: sha512-79ak0JxddO0utAXAQ5ccKhvs6vX2MGyHHMMsmZkBANrq3hXc1CHzvNPHOcvTsVMEPl5I+NT+RO4YKMGehOfSIg==} + dev: false + + /fastify@4.17.0: + resolution: {integrity: sha512-tzuY1tgWJo2Y6qEKwmLhFvACUmr68Io2pqP/sDKU71KRM6A6R3DrCDqLGqANbeLZcKUfdfY58ut35CGqemcTgg==} + dependencies: + '@fastify/ajv-compiler': 3.5.0 + '@fastify/error': 3.2.0 + '@fastify/fast-json-stringify-compiler': 4.3.0 + abstract-logging: 2.0.1 + avvio: 8.2.1 + fast-content-type-parse: 1.0.0 + fast-json-stringify: 5.7.0 + find-my-way: 7.6.2 + light-my-request: 5.9.1 + pino: 8.14.1 + process-warning: 2.2.0 + proxy-addr: 2.0.7 + rfdc: 1.3.0 + secure-json-parse: 2.7.0 + semver: 7.5.1 + tiny-lru: 11.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /fastparallel@2.4.1: + resolution: {integrity: sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==} + dependencies: + reusify: 1.0.4 + xtend: 4.0.2 + dev: false + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + + /fastseries@1.7.2: + resolution: {integrity: sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==} + dependencies: + reusify: 1.0.4 + xtend: 4.0.2 + dev: false + + /figlet@1.6.0: + resolution: {integrity: sha512-31EQGhCEITv6+hi2ORRPyn3bulaV9Fl4xOdR169cBzH/n1UqcxsiSB/noo6SJdD7Kfb1Ljit+IgR1USvF/XbdA==} + engines: {node: '>= 0.4.0'} + hasBin: true + dev: false + + /figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} dependencies: escape-string-regexp: 1.0.5 dev: true - /file-entry-cache/6.0.1: + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} dependencies: flat-cache: 3.0.4 dev: true - /fill-range/7.0.1: + /file-type@18.4.0: + resolution: {integrity: sha512-o6MQrZKTAK6WpvmQk3jqTVUmqxYBxW5bloUfrdH1ZnRFDvvAPNr+l+rgOxM3nkqWT+3khaj3FRMDydWe0xhu+w==} + engines: {node: '>=14.16'} + dependencies: + readable-web-to-node-stream: 3.0.2 + strtok3: 7.0.0 + token-types: 5.0.1 + dev: false + + /file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + dev: false + optional: true + + /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 dev: true - /find-up/4.1.0: + /find-my-way@7.6.2: + resolution: {integrity: sha512-0OjHn1b1nCX3eVbm9ByeEHiscPYiHLfhei1wOUU9qffQkk98wE0Lo8VrVYfSGMgnSnDh86DxedduAnBf4nwUEw==} + engines: {node: '>=14'} + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.1 + safe-regex2: 2.0.0 + dev: false + + /find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + dependencies: + locate-path: 2.0.0 + dev: true + + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} dependencies: @@ -1279,80 +1922,54 @@ packages: path-exists: 4.0.0 dev: true - /flat-cache/3.0.4: + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.0.4: resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} engines: {node: ^10.12.0 || >=12.0.0} dependencies: - flatted: 3.2.2 + flatted: 3.2.7 rimraf: 3.0.2 dev: true - /flatted/3.2.2: - resolution: {integrity: sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==} + /flatted@3.2.7: + resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} dev: true - /follow-redirects/1.14.4: - resolution: {integrity: sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - - /forever-agent/0.6.1: - resolution: {integrity: sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=} - dev: false - optional: true - - /form-data/2.3.3: - resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} - engines: {node: '>= 0.12'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.33 - dev: false - optional: true - - /form-data/2.5.1: - resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==} - engines: {node: '>= 0.12'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.33 - - /form-data/3.0.1: - resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} - engines: {node: '>= 6'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.33 - - /form-data/4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} - engines: {node: '>= 6'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.33 - - /fs-constants/1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - optional: true - - /fs-minipass/1.2.7: - resolution: {integrity: sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==} - dependencies: - minipass: 2.9.0 + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} dev: false - /fs.realpath/1.0.0: - resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=} + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: false - /fsevents/2.3.2: + /fs-extra@11.1.1: + resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} + engines: {node: '>=14.14'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] @@ -1360,238 +1977,209 @@ packages: dev: true optional: true - /fstream/1.0.12: - resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} - engines: {node: '>=0.6'} - dependencies: - graceful-fs: 4.2.8 - inherits: 2.0.4 - mkdirp: 0.5.5 - rimraf: 2.7.1 - dev: false - optional: true - - /function-bind/1.1.1: + /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - - /functional-red-black-tree/1.0.1: - resolution: {integrity: sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=} dev: true - /gauge/2.7.4: - resolution: {integrity: sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=} - dependencies: - aproba: 1.2.0 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - object-assign: 4.1.1 - signal-exit: 3.0.5 - string-width: 1.0.2 - strip-ansi: 3.0.1 - wide-align: 1.1.3 - - /generate-function/2.3.1: - resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} - dependencies: - is-property: 1.0.2 - - /get-caller-file/2.0.5: + /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} dev: true - /get-intrinsic/1.1.1: - resolution: {integrity: sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==} + /get-pkg-repo@4.2.1: + resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==} + engines: {node: '>=6.9.0'} + hasBin: true dependencies: - function-bind: 1.1.1 - has: 1.0.3 - has-symbols: 1.0.2 - - /get-stream/4.1.0: - resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} - engines: {node: '>=6'} - dependencies: - pump: 3.0.0 + '@hutson/parse-repository-url': 3.0.2 + hosted-git-info: 4.1.0 + through2: 2.0.5 + yargs: 16.2.0 dev: true - /get-stream/5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - dependencies: - pump: 3.0.0 + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} dev: true - /getpass/0.1.7: - resolution: {integrity: sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=} + /git-raw-commits@2.0.11: + resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} + engines: {node: '>=10'} + hasBin: true dependencies: - assert-plus: 1.0.0 - dev: false - optional: true + dargs: 7.0.0 + lodash: 4.17.21 + meow: 8.1.2 + split2: 3.2.2 + through2: 4.0.2 + dev: true - /github-from-package/0.0.0: - resolution: {integrity: sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=} - optional: true + /git-remote-origin-url@2.0.0: + resolution: {integrity: sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==} + engines: {node: '>=4'} + dependencies: + gitconfiglocal: 1.0.0 + pify: 2.3.0 + dev: true - /glob-parent/5.1.2: + /git-semver-tags@4.1.1: + resolution: {integrity: sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + meow: 8.1.2 + semver: 6.3.0 + dev: true + + /gitconfiglocal@1.0.0: + resolution: {integrity: sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==} + dependencies: + ini: 1.3.8 + dev: true + + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} dependencies: is-glob: 4.0.3 dev: true - /glob/7.2.0: - resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.0.4 + minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - - /global-dirs/3.0.0: - resolution: {integrity: sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==} - engines: {node: '>=10'} - dependencies: - ini: 2.0.0 dev: true - /globals/13.11.0: - resolution: {integrity: sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==} + /global-dirs@0.1.1: + resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} + engines: {node: '>=4'} + dependencies: + ini: 1.3.8 + dev: true + + /globals@13.20.0: + resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} engines: {node: '>=8'} dependencies: type-fest: 0.20.2 dev: true - /got/9.6.0: - resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} - engines: {node: '>=8.6'} - dependencies: - '@sindresorhus/is': 0.14.0 - '@szmarczak/http-timer': 1.1.2 - '@types/keyv': 3.1.4 - '@types/responselike': 1.0.0 - cacheable-request: 6.1.0 - decompress-response: 3.3.0 - duplexer3: 0.1.4 - get-stream: 4.1.0 - lowercase-keys: 1.0.1 - mimic-response: 1.0.1 - p-cancelable: 1.1.0 - to-readable-stream: 1.0.0 - url-parse-lax: 3.0.0 + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true - /graceful-fs/4.2.8: - resolution: {integrity: sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==} + /handlebars@4.7.7: + resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.17.4 + dev: true - /har-schema/2.0.0: - resolution: {integrity: sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=} - engines: {node: '>=4'} - dev: false - optional: true - - /har-validator/5.1.5: - resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + /hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} engines: {node: '>=6'} - deprecated: this library is no longer supported - dependencies: - ajv: 6.12.6 - har-schema: 2.0.0 - dev: false - optional: true + dev: true - /has-flag/3.0.0: - resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=} + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} dev: true - /has-flag/4.0.0: + /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - /has-symbols/1.0.2: - resolution: {integrity: sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==} - engines: {node: '>= 0.4'} - - /has-unicode/2.0.1: - resolution: {integrity: sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=} - - /has-yarn/2.1.0: - resolution: {integrity: sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==} - engines: {node: '>=8'} - dev: true - - /has/1.0.3: + /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} dependencies: function-bind: 1.1.1 - - /http-cache-semantics/4.1.0: - resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==} dev: true - /http-proxy-agent/4.0.1: - resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} - engines: {node: '>= 6'} - dependencies: - '@tootallnate/once': 1.1.2 - agent-base: 6.0.2 - debug: 4.3.2 - transitivePeerDependencies: - - supports-color - - /http-signature/1.2.0: - resolution: {integrity: sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=} - engines: {node: '>=0.8', npm: '>=1.3.7'} - dependencies: - assert-plus: 1.0.0 - jsprim: 1.4.1 - sshpk: 1.16.1 + /hex-to-rgba@2.0.1: + resolution: {integrity: sha512-5XqPJBpsEUMsseJUi2w2Hl7cHFFi3+OO10M2pzAvKB1zL6fc+koGMhmBqoDOCB4GemiRM/zvDMRIhVw6EkB8dQ==} dev: false - optional: true - /https-proxy-agent/5.0.0: - resolution: {integrity: sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==} - engines: {node: '>= 6'} + /hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + dev: true + + /hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} dependencies: - agent-base: 6.0.2 - debug: 4.3.2 - transitivePeerDependencies: - - supports-color + lru-cache: 6.0.0 + dev: true - /iconv-lite/0.4.24: + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: true + + /human-signals@4.3.1: + resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} + engines: {node: '>=14.18.0'} + dev: true + + /husky@8.0.3: + resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} dependencies: safer-buffer: 2.1.2 - - /iconv-lite/0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - - /ieee754/1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - - /ignore-by-default/1.0.1: - resolution: {integrity: sha1-SMptcvbGo68Aqa1K5odr44ieKwk=} dev: true - /ignore-walk/3.0.4: - resolution: {integrity: sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==} - dependencies: - minimatch: 3.0.4 + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: false - /ignore/4.0.6: - resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==} + /ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + dev: true + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} dev: true - /import-fresh/3.3.0: + /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} dependencies: @@ -1599,39 +2187,31 @@ packages: resolve-from: 4.0.0 dev: true - /import-lazy/2.1.0: - resolution: {integrity: sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=} - engines: {node: '>=4'} - dev: true - - /imurmurhash/0.1.4: - resolution: {integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o=} + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} dev: true - /inflection/1.13.1: - resolution: {integrity: sha512-dldYtl2WlN0QDkIDtg8+xFwOS2Tbmp12t1cHa5/YClU6ZQjTFm7B66UcVbh9NQB+HvT5BAd2t5+yKsBkw5pcqA==} - engines: {'0': node >= 0.4.0} - dev: false + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true - /inflight/1.0.6: - resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=} + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: once: 1.4.0 wrappy: 1.0.2 - - /inherits/2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - /ini/1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - - /ini/2.0.0: - resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} - engines: {node: '>=10'} dev: true - /inquirer/7.3.3: + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: true + + /inquirer@7.3.3: resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} engines: {node: '>=8.0.0'} dependencies: @@ -1650,256 +2230,186 @@ packages: through: 2.3.8 dev: true - /ip-regex/2.1.0: - resolution: {integrity: sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=} - engines: {node: '>=4'} + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false - /is-binary-path/2.1.0: + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} dependencies: binary-extensions: 2.2.0 dev: true - /is-ci/2.0.0: - resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} - hasBin: true + /is-core-module@2.12.1: + resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} dependencies: - ci-info: 2.0.0 + has: 1.0.3 dev: true - /is-docker/2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - - /is-extglob/2.1.1: - resolution: {integrity: sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=} + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} dev: true - /is-fullwidth-code-point/1.0.0: - resolution: {integrity: sha1-754xOG8DGn8NZDr4L95QxFfvAMs=} - engines: {node: '>=0.10.0'} - dependencies: - number-is-nan: 1.0.1 - - /is-fullwidth-code-point/3.0.0: + /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - /is-glob/4.0.3: + /is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + dev: true + + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 dev: true - /is-installed-globally/0.4.0: - resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} - engines: {node: '>=10'} - dependencies: - global-dirs: 3.0.0 - is-path-inside: 3.0.3 - dev: true - - /is-npm/5.0.0: - resolution: {integrity: sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==} - engines: {node: '>=10'} - dev: true - - /is-number/7.0.0: + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} dev: true - /is-obj/2.0.0: + /is-obj@2.0.0: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} dev: true - /is-path-inside/3.0.3: + /is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} dev: true - /is-property/1.0.2: - resolution: {integrity: sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=} - - /is-typedarray/1.0.0: - resolution: {integrity: sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=} - - /is-wsl/2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - dependencies: - is-docker: 2.2.1 - - /is-yarn-global/0.3.0: - resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==} + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} dev: true - /isarray/1.0.0: - resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=} + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: true - /isexe/2.0.0: - resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true - /isstream/0.1.2: - resolution: {integrity: sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=} + /is-text-path@1.0.1: + resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==} + engines: {node: '>=0.10.0'} + dependencies: + text-extensions: 1.9.0 + dev: true + + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /iterate-object@1.3.4: + resolution: {integrity: sha512-4dG1D1x/7g8PwHS9aK6QV5V94+ZvyP4+d19qDv43EzImmrndysIl4prmJ1hWWIGCqrZHyaHBm6BSEWHOLnpoNw==} dev: false - optional: true - /js-tokens/4.0.0: + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true - /js-yaml/3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true dependencies: - argparse: 1.0.10 - esprima: 4.0.1 + argparse: 2.0.1 dev: true - /jsbi/3.2.5: - resolution: {integrity: sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ==} - - /jsbn/0.1.1: - resolution: {integrity: sha1-peZUwuWi3rXyAdls77yoDA7y9RM=} - dev: false - optional: true - - /json-buffer/3.0.0: - resolution: {integrity: sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=} - dev: true - - /json-buffer/3.0.1: + /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: false - /json-fixer/1.6.12: - resolution: {integrity: sha512-BGO9HExf0ZUVYvuWsps71Re513Ss0il1Wp7wYWkir2NthzincvNJEUu82KagEfAkGdjOMsypj3t2JB7drBKWnA==} + /json-fixer@1.6.15: + resolution: {integrity: sha512-TuDuZ5KrgyjoCIppdPXBMqiGfota55+odM+j2cQ5rt/XKyKmqGB3Whz1F8SN8+60yYGy/Nu5lbRZ+rx8kBIvBw==} engines: {node: '>=10'} dependencies: - '@babel/runtime': 7.15.4 + '@babel/runtime': 7.22.3 chalk: 4.1.2 pegjs: 0.10.0 dev: true - /json-schema-traverse/0.4.1: + /json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true - /json-schema-traverse/1.0.0: + /json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true - /json-schema/0.2.3: - resolution: {integrity: sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=} - dev: false - optional: true - - /json-stable-stringify-without-jsonify/1.0.1: - resolution: {integrity: sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=} + /json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} dev: true - /json-stringify-safe/5.0.1: - resolution: {integrity: sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=} - dev: false - optional: true - - /jsonwebtoken/8.5.1: - resolution: {integrity: sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==} - engines: {node: '>=4', npm: '>=1.4.28'} + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: - jws: 3.2.2 - lodash.includes: 4.3.0 - lodash.isboolean: 3.0.3 - lodash.isinteger: 4.0.4 - lodash.isnumber: 3.0.3 - lodash.isplainobject: 4.0.6 - lodash.isstring: 4.0.1 - lodash.once: 4.1.1 - ms: 2.1.3 - semver: 5.7.1 + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.11 - /jsprim/1.4.1: - resolution: {integrity: sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=} - engines: {'0': node >=0.6.0} - dependencies: - assert-plus: 1.0.0 - extsprintf: 1.3.0 - json-schema: 0.2.3 - verror: 1.10.0 - dev: false - optional: true - - /jwa/1.4.1: - resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - - /jwa/2.0.0: - resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - - /jws/3.2.2: - resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} - dependencies: - jwa: 1.4.1 - safe-buffer: 5.2.1 - - /jws/4.0.0: - resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} - dependencies: - jwa: 2.0.0 - safe-buffer: 5.2.1 - - /keytar/7.7.0: - resolution: {integrity: sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==} - requiresBuild: true - dependencies: - node-addon-api: 3.2.1 - prebuild-install: 6.1.4 - optional: true - - /keyv/3.1.0: - resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} - dependencies: - json-buffer: 3.0.0 + /jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} dev: true - /keyv/4.0.3: - resolution: {integrity: sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==} + /keyv@4.5.2: + resolution: {integrity: sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==} dependencies: json-buffer: 3.0.1 dev: false - /latest-version/5.1.0: - resolution: {integrity: sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==} - engines: {node: '>=8'} - dependencies: - package-json: 6.5.0 + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} dev: true - /leeks.js/0.2.4: + /leeks.js@0.2.4: resolution: {integrity: sha512-yFR6BtcTA/5s2FJAVkPn2VEzqO76DqBIdkC+1vNyersz1Hw0DxhunRu7bI7/XKxwrNy8x0K4e1p4YQKIGRCBww==} dev: false - /leekslazylogger/3.0.2: - resolution: {integrity: sha512-eXgQuEgoSIbtwJRQXy/DHFp1255C4br+x6CtOnrBb5j3GwmeARPMutPur1cMB5YonKBZxFXnvZLqQhr69qKg5A==} + /leeks.js@0.3.0: + resolution: {integrity: sha512-6viaW7OYf8ijUe3G+rQ6CvbCLkN32vmeaEnVg82M474MbEeB++FcA80Qya1lv3cCgwkuxnfzDgnmWxol1bfhfA==} + dev: false + + /leekslazylogger@5.0.1: + resolution: {integrity: sha512-JPA1o6psxrovivF/X84VbBHeMfwnAqxHwAv4S7LlnSX5aSHikDL1P77jGy7fxW0NKj3RA4F7G8lM0Eb813wqvg==} + engines: {node: '>=14'} dependencies: - '@eartharoid/deep-merge': 0.0.1 - '@eartharoid/dtf': 1.0.8 + '@eartharoid/deep-merge': 0.0.2 + '@eartharoid/dtf': 2.0.1 leeks.js: 0.2.4 dev: false - /levn/0.4.1: + /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} dependencies: @@ -1907,245 +2417,311 @@ packages: type-check: 0.4.0 dev: true - /locate-path/5.0.0: + /light-my-request@5.9.1: + resolution: {integrity: sha512-UT7pUk8jNCR1wR7w3iWfIjx32DiB2f3hFdQSOwy3/EPQ3n3VocyipUxcyRZR0ahoev+fky69uA+GejPa9KuHKg==} + dependencies: + cookie: 0.5.0 + process-warning: 2.2.0 + set-cookie-parser: 2.6.0 + dev: false + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /lint-staged@13.2.2: + resolution: {integrity: sha512-71gSwXKy649VrSU09s10uAT0rWCcY3aewhMaHyl2N84oBk4Xs9HgxvUp3AYu+bNsK4NrOYYxvSgg7FyGJ+jGcA==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + dependencies: + chalk: 5.2.0 + cli-truncate: 3.1.0 + commander: 10.0.1 + debug: 4.3.4 + execa: 7.1.1 + lilconfig: 2.1.0 + listr2: 5.0.8 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-inspect: 1.12.3 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.3.1 + transitivePeerDependencies: + - enquirer + - supports-color + dev: true + + /listr2@5.0.8: + resolution: {integrity: sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA==} + engines: {node: ^14.13.1 || >=16.0.0} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.20 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.3.0 + rxjs: 7.8.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + dev: true + + /load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + dev: true + + /locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + dev: true + + /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} dependencies: p-locate: 4.1.0 dev: true - /lodash.clonedeep/4.5.0: - resolution: {integrity: sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=} + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 dev: true - /lodash.includes/4.3.0: - resolution: {integrity: sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=} + /lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: true - /lodash.isboolean/3.0.3: - resolution: {integrity: sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=} + /lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + dev: true - /lodash.isinteger/4.0.4: - resolution: {integrity: sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=} + /lodash.ismatch@4.4.0: + resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} + dev: true - /lodash.isnumber/3.0.3: - resolution: {integrity: sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=} + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: true - /lodash.isplainobject/4.0.6: - resolution: {integrity: sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=} + /lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + dev: true - /lodash.isstring/4.0.1: - resolution: {integrity: sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=} - - /lodash.merge/4.6.2: + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true - /lodash.once/4.1.1: - resolution: {integrity: sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=} - - /lodash.truncate/4.4.2: - resolution: {integrity: sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=} + /lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} dev: true - /lodash/4.17.21: + /lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + /lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + dev: true + + /lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + dev: true + + /lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + dev: true + + /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - /long/4.0.0: - resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} - - /lowercase-keys/1.0.1: - resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} - engines: {node: '>=0.10.0'} - dev: true - - /lowercase-keys/2.0.0: - resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} - engines: {node: '>=8'} - dev: true - - /lru-cache/4.1.5: - resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + /log-update@4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} dependencies: - pseudomap: 1.0.2 - yallist: 2.1.2 + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + dev: true - /lru-cache/6.0.0: + /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} dependencies: yallist: 4.0.0 - /make-dir/3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} - dependencies: - semver: 6.3.0 + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: true - /mariadb/2.5.4: - resolution: {integrity: sha512-4vQgMRyBIN9EwSQG0vzjR9D8bscPH0dGPJt67qVlOkHSiSm0xUatg1Pft4o1LzORgeOW4PheiY/HBE9bYYmNCA==} - engines: {node: '>= 10.13'} + /map-o@2.0.10: + resolution: {integrity: sha512-BxazE81fVByHWasyXhqKeo2m7bFKYu+ZbEfiuexMOnklXW+tzDvnlTi/JaklEeuuwqcqJzPaf9q+TWptSGXeLg==} dependencies: - '@types/geojson': 7946.0.8 - '@types/node': 14.17.21 - denque: 1.5.1 - iconv-lite: 0.6.3 - long: 4.0.0 - moment-timezone: 0.5.33 - please-upgrade-node: 3.2.0 + iterate-object: 1.3.4 + dev: false - /mime-db/1.50.0: - resolution: {integrity: sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==} - engines: {node: '>= 0.6'} + /map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + dev: true - /mime-types/2.1.33: - resolution: {integrity: sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==} - engines: {node: '>= 0.6'} + /map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + dev: true + + /marked@4.3.0: + resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} + engines: {node: '>= 12'} + hasBin: true + dev: false + + /meow@8.1.2: + resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} + engines: {node: '>=10'} dependencies: - mime-db: 1.50.0 + '@types/minimist': 1.2.2 + camelcase-keys: 6.2.2 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.18.1 + yargs-parser: 20.2.9 + dev: true - /mimic-fn/2.1.0: + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} dev: true - /mimic-response/1.0.1: - resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + + /min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} dev: true - /mimic-response/2.1.0: - resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==} - engines: {node: '>=8'} - optional: true + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: false - /minimatch/3.0.4: - resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 - /minimist/1.2.5: - resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==} - - /minipass/2.9.0: - resolution: {integrity: sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==} + /minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} dependencies: - safe-buffer: 5.2.1 - yallist: 3.1.1 - dev: false - - /minizlib/1.3.3: - resolution: {integrity: sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==} - dependencies: - minipass: 2.9.0 - dev: false - - /mkdirp-classic/0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - optional: true - - /mkdirp/0.5.5: - resolution: {integrity: sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==} - hasBin: true - dependencies: - minimist: 1.2.5 - dev: false - - /moment-timezone/0.5.33: - resolution: {integrity: sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==} - dependencies: - moment: 2.29.1 - - /moment/2.29.1: - resolution: {integrity: sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==} - - /ms/2.0.0: - resolution: {integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=} + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 dev: true - /ms/2.1.2: + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /mnemonist@0.39.5: + resolution: {integrity: sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==} + dependencies: + obliterator: 2.0.4 + dev: false + + /modify-values@1.0.1: + resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} + engines: {node: '>=0.10.0'} + dev: true + + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - /ms/2.1.3: + /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - /msal/1.4.14: - resolution: {integrity: sha512-k8M5+/jbfSQoCf7CyQzBP5HE5mY8TkBujykLGTEp2x0MvOK/FQsfUTNis28zlvvPVzhgrhb5GQiGM8rRpXyHdA==} - engines: {node: '>=0.8.0'} - dependencies: - tslib: 1.14.1 - - /mustache/4.2.0: + /mustache@4.2.0: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true dev: false - /mute-stream/0.0.8: + /mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} dev: true - /mysql2/2.3.0: - resolution: {integrity: sha512-0t5Ivps5Tdy5YHk5NdKwQhe/4Qyn2pload+S+UooDBvsqngtzujG1BaTWBihQLfeKO3t3122/GtusBtmHEHqww==} - engines: {node: '>= 8.0'} - dependencies: - denque: 1.5.1 - generate-function: 2.3.1 - iconv-lite: 0.6.3 - long: 4.0.0 - lru-cache: 6.0.0 - named-placeholders: 1.1.2 - seq-queue: 0.0.5 - sqlstring: 2.3.2 - - /named-placeholders/1.1.2: - resolution: {integrity: sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA==} - engines: {node: '>=6.0.0'} - dependencies: - lru-cache: 4.1.5 - - /napi-build-utils/1.0.2: - resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + /nan@2.17.0: + resolution: {integrity: sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==} + dev: false optional: true - /native-duplexpair/1.0.0: - resolution: {integrity: sha1-eJkHjmS/PIo9cyYBs9QP8F21j6A=} - - /natural-compare/1.4.0: - resolution: {integrity: sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=} - dev: true - - /needle/2.9.1: - resolution: {integrity: sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==} - engines: {node: '>= 4.4.x'} + /nanoid@3.3.6: + resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dependencies: - debug: 3.2.7 - iconv-lite: 0.4.24 - sax: 1.2.4 - transitivePeerDependencies: - - supports-color dev: false - /node-abi/2.30.1: - resolution: {integrity: sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==} + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: true + + /node-dir@0.1.17: + resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==} + engines: {node: '>= 0.10.5'} dependencies: - semver: 5.7.1 - optional: true + minimatch: 3.1.2 + dev: false - /node-abort-controller/2.0.0: - resolution: {integrity: sha512-L8RfEgjBTHAISTuagw51PprVAqNZoG6KSB6LQ6H1bskMVkFs5E71IyjauLBv3XbuomJlguWF/VnRHdJ1gqiAqA==} - - /node-addon-api/3.2.1: - resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} - - /node-fetch/2.6.5: - resolution: {integrity: sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==} - engines: {node: 4.x || >=6.0.0} + /node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} dependencies: - whatwg-url: 5.0.0 + lodash: 4.17.21 + dev: false - /node-fetch/2.6.9: - resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==} + /node-fetch@2.6.11: + resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==} engines: {node: 4.x || >=6.0.0} peerDependencies: encoding: ^0.1.0 @@ -2154,160 +2730,112 @@ packages: optional: true dependencies: whatwg-url: 5.0.0 + dev: true - /node-gyp/3.8.0: - resolution: {integrity: sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==} - engines: {node: '>= 0.8.0'} + /node-gyp-build@4.6.0: + resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} hasBin: true - requiresBuild: true - dependencies: - fstream: 1.0.12 - glob: 7.2.0 - graceful-fs: 4.2.8 - mkdirp: 0.5.5 - nopt: 3.0.6 - npmlog: 4.1.2 - osenv: 0.1.5 - request: 2.88.2 - rimraf: 2.7.1 - semver: 5.3.0 - tar: 2.2.2 - which: 1.3.1 - dev: false - optional: true - - /node-pre-gyp/0.11.0: - resolution: {integrity: sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==} - deprecated: 'Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future' - hasBin: true - dependencies: - detect-libc: 1.0.3 - mkdirp: 0.5.5 - needle: 2.9.1 - nopt: 4.0.3 - npm-packlist: 1.4.8 - npmlog: 4.1.2 - rc: 1.2.8 - rimraf: 2.7.1 - semver: 5.7.1 - tar: 4.4.19 - transitivePeerDependencies: - - supports-color dev: false - /nodemon/2.0.13: - resolution: {integrity: sha512-UMXMpsZsv1UXUttCn6gv8eQPhn6DR4BW+txnL3IN5IHqrCwcrT/yWHfL35UsClGXknTH79r5xbu+6J1zNHuSyA==} + /nodemon@2.0.22: + resolution: {integrity: sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==} engines: {node: '>=8.10.0'} hasBin: true - requiresBuild: true dependencies: - chokidar: 3.5.2 - debug: 3.2.7_supports-color@5.5.0 + chokidar: 3.5.3 + debug: 3.2.7(supports-color@5.5.0) ignore-by-default: 1.0.1 - minimatch: 3.0.4 + minimatch: 3.1.2 pstree.remy: 1.1.8 semver: 5.7.1 + simple-update-notifier: 1.1.0 supports-color: 5.5.0 touch: 3.1.0 - undefsafe: 2.0.3_supports-color@5.5.0 - update-notifier: 5.1.0 + undefsafe: 2.0.5 dev: true - /nopt/1.0.10: - resolution: {integrity: sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=} + /nopt@1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} hasBin: true dependencies: abbrev: 1.1.1 dev: true - /nopt/3.0.6: - resolution: {integrity: sha1-xkZdvwirzU2zWTF/eaxopkayj/k=} - hasBin: true + /normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: - abbrev: 1.1.1 - dev: false - optional: true + hosted-git-info: 2.8.9 + resolve: 1.22.2 + semver: 5.7.1 + validate-npm-package-license: 3.0.4 + dev: true - /nopt/4.0.3: - resolution: {integrity: sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==} - hasBin: true + /normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} dependencies: - abbrev: 1.1.1 - osenv: 0.1.5 - dev: false + hosted-git-info: 4.1.0 + is-core-module: 2.12.1 + semver: 7.5.1 + validate-npm-package-license: 3.0.4 + dev: true - /normalize-path/3.0.0: + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} dev: true - /normalize-url/4.5.1: - resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 dev: true - /npm-bundled/1.1.2: - resolution: {integrity: sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==} + /npm-run-path@5.1.0: + resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: - npm-normalize-package-bin: 1.0.1 + path-key: 4.0.0 + dev: true + + /object-diffy@1.0.4: + resolution: {integrity: sha512-zsgUWZhu9YtP7kp+PvCzUhYtOurBa7qIS2XUJFyVooq+I/ZlwFe0aHp1pyek/dpqd+EEYxM46j8czpW54JM2EA==} dev: false - /npm-normalize-package-bin/1.0.1: - resolution: {integrity: sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==} + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: true + + /obliterator@2.0.4: + resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} dev: false - /npm-packlist/1.4.8: - resolution: {integrity: sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==} - dependencies: - ignore-walk: 3.0.4 - npm-bundled: 1.1.2 - npm-normalize-package-bin: 1.0.1 + /on-exit-leak-free@2.1.0: + resolution: {integrity: sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==} dev: false - /npmlog/4.1.2: - resolution: {integrity: sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==} - dependencies: - are-we-there-yet: 1.1.7 - console-control-strings: 1.1.0 - gauge: 2.7.4 - set-blocking: 2.0.0 - - /number-is-nan/1.0.1: - resolution: {integrity: sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=} - engines: {node: '>=0.10.0'} - - /oauth-sign/0.9.0: - resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} - dev: false - optional: true - - /object-assign/4.1.1: - resolution: {integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=} - engines: {node: '>=0.10.0'} - - /object-inspect/1.11.0: - resolution: {integrity: sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==} - - /once/1.4.0: - resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=} + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 + dev: true - /onetime/5.1.2: + /onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} dependencies: mimic-fn: 2.1.0 dev: true - /open/7.4.2: - resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} - engines: {node: '>=8'} + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} dependencies: - is-docker: 2.2.1 - is-wsl: 2.2.0 + mimic-fn: 4.0.0 + dev: true - /optionator/0.9.1: + /optionator@0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} engines: {node: '>= 0.8.0'} dependencies: @@ -2319,271 +2847,321 @@ packages: word-wrap: 1.2.3 dev: true - /os-homedir/1.0.2: - resolution: {integrity: sha1-/7xJiDNuDoM94MFox+8VISGqf7M=} + /os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} - dev: false - - /os-tmpdir/1.0.2: - resolution: {integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=} - engines: {node: '>=0.10.0'} - - /osenv/0.1.5: - resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==} - dependencies: - os-homedir: 1.0.2 - os-tmpdir: 1.0.2 - dev: false - - /p-cancelable/1.1.0: - resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} - engines: {node: '>=6'} dev: true - /p-limit/2.3.0: + /p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + dependencies: + p-try: 1.0.0 + dev: true + + /p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} dependencies: p-try: 2.2.0 dev: true - /p-locate/4.1.0: + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + dependencies: + p-limit: 1.3.0 + dev: true + + /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} dependencies: p-limit: 2.3.0 dev: true - /p-try/2.2.0: + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + dependencies: + aggregate-error: 3.1.0 + dev: true + + /p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + dev: true + + /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} dev: true - /package-json/6.5.0: - resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} - engines: {node: '>=8'} - dependencies: - got: 9.6.0 - registry-auth-token: 4.2.1 - registry-url: 5.1.0 - semver: 6.3.0 - dev: true - - /packet-reader/1.0.0: - resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} - - /parent-module/1.0.1: + /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} dependencies: callsites: 3.1.0 dev: true - /path-exists/4.0.0: + /parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + dependencies: + error-ex: 1.3.2 + json-parse-better-errors: 1.0.2 + dev: true + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.21.4 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + + /path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + dev: true + + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} dev: true - /path-is-absolute/1.0.1: - resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=} + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} + dev: true - /path-key/3.1.1: + /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} dev: true - /pegjs/0.10.0: - resolution: {integrity: sha1-z4uvrm7d/0tafvsYUmnqr0YQ3b0=} + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-type@3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + dependencies: + pify: 3.0.0 + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /peek-readable@5.0.0: + resolution: {integrity: sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==} + engines: {node: '>=14.16'} + dev: false + + /pegjs@0.10.0: + resolution: {integrity: sha512-qI5+oFNEGi3L5HAxDwN2LA4Gg7irF70Zs25edhjld9QemOgp0CbvMtbFcMvFtEo1OityPrcCzkQFB8JP/hxgow==} engines: {node: '>=0.10'} hasBin: true dev: true - /performance-now/2.1.0: - resolution: {integrity: sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=} + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: false - optional: true - /pg-connection-string/2.5.0: - resolution: {integrity: sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==} - - /pg-hstore/2.3.4: - resolution: {integrity: sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==} - engines: {node: '>= 0.8.x'} - dependencies: - underscore: 1.13.1 - - /pg-int8/1.0.1: - resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} - engines: {node: '>=4.0.0'} - - /pg-pool/3.4.1_pg@8.7.1: - resolution: {integrity: sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==} - peerDependencies: - pg: '>=8.0' - dependencies: - pg: 8.7.1 - - /pg-protocol/1.5.0: - resolution: {integrity: sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==} - - /pg-types/2.2.0: - resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} - engines: {node: '>=4'} - dependencies: - pg-int8: 1.0.1 - postgres-array: 2.0.0 - postgres-bytea: 1.0.0 - postgres-date: 1.0.7 - postgres-interval: 1.2.0 - - /pg/8.7.1: - resolution: {integrity: sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA==} - engines: {node: '>= 8.0.0'} - peerDependencies: - pg-native: '>=2.0.0' - peerDependenciesMeta: - pg-native: - optional: true - dependencies: - buffer-writer: 2.0.0 - packet-reader: 1.0.0 - pg-connection-string: 2.5.0 - pg-pool: 3.4.1_pg@8.7.1 - pg-protocol: 1.5.0 - pg-types: 2.2.0 - pgpass: 1.0.4 - - /pgpass/1.0.4: - resolution: {integrity: sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==} - dependencies: - split2: 3.2.2 - - /picomatch/2.3.0: - resolution: {integrity: sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==} + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} dev: true - /pify/5.0.0: + /pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + dev: true + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + dev: true + + /pify@5.0.0: resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} engines: {node: '>=10'} dev: true - /please-upgrade-node/3.2.0: - resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==} + /pino-abstract-transport@1.0.0: + resolution: {integrity: sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==} dependencies: - semver-compare: 1.0.0 + readable-stream: 4.4.0 + split2: 4.2.0 + dev: false - /postgres-array/2.0.0: - resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} - engines: {node: '>=4'} + /pino-std-serializers@6.2.1: + resolution: {integrity: sha512-wHuWB+CvSVb2XqXM0W/WOYUkVSPbiJb9S5fNB7TBhd8s892Xq910bRxwHtC4l71hgztObTjXL6ZheZXFjhDrDQ==} + dev: false - /postgres-bytea/1.0.0: - resolution: {integrity: sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=} - engines: {node: '>=0.10.0'} - - /postgres-date/1.0.7: - resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} - engines: {node: '>=0.10.0'} - - /postgres-interval/1.2.0: - resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} - engines: {node: '>=0.10.0'} - dependencies: - xtend: 4.0.2 - - /prebuild-install/6.1.4: - resolution: {integrity: sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==} - engines: {node: '>=6'} + /pino@8.14.1: + resolution: {integrity: sha512-8LYNv7BKWXSfS+k6oEc6occy5La+q2sPwU3q2ljTX5AZk7v+5kND2o5W794FyRaqha6DJajmkNRsWtPpFyMUdw==} hasBin: true dependencies: - detect-libc: 1.0.3 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.5 - mkdirp-classic: 0.5.3 - napi-build-utils: 1.0.2 - node-abi: 2.30.1 - npmlog: 4.1.2 - pump: 3.0.0 - rc: 1.2.8 - simple-get: 3.1.0 - tar-fs: 2.1.1 - tunnel-agent: 0.6.0 - optional: true + atomic-sleep: 1.0.0 + fast-redact: 3.2.0 + on-exit-leak-free: 2.1.0 + pino-abstract-transport: 1.0.0 + pino-std-serializers: 6.2.1 + process-warning: 2.2.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.3 + sonic-boom: 3.3.0 + thread-stream: 2.3.0 + dev: false - /prelude-ls/1.2.1: + /postcss@8.4.24: + resolution: {integrity: sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: false + + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} dev: true - /prepend-http/2.0.0: - resolution: {integrity: sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=} - engines: {node: '>=4'} + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + requiresBuild: true dev: true + optional: true - /process-nextick-args/2.0.1: + /prisma@4.14.1: + resolution: {integrity: sha512-z6hxzTMYqT9SIKlzD08dhzsLUpxjFKKsLpp5/kBDnSqiOjtUyyl/dC5tzxLcOa3jkEHQ8+RpB/fE3w8bgNP51g==} + engines: {node: '>=14.17'} + hasBin: true + requiresBuild: true + dependencies: + '@prisma/engines': 4.14.1 + dev: false + + /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - - /process/0.11.10: - resolution: {integrity: sha1-czIwDoQBYb2j5podHZGn1LwW8YI=} - engines: {node: '>= 0.6.0'} - - /progress/2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} dev: true - /pseudomap/1.0.2: - resolution: {integrity: sha1-8FKijacOYYkX7wqKw0wa5aaChrM=} + /process-warning@2.2.0: + resolution: {integrity: sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==} + dev: false - /psl/1.8.0: - resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==} + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: false - /pstree.remy/1.1.8: + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} dev: true - /pump/3.0.0: - resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} - dependencies: - end-of-stream: 1.4.4 - once: 1.4.0 - - /punycode/2.1.1: - resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} - /pupa/2.1.1: - resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==} - engines: {node: '>=8'} - dependencies: - escape-goat: 2.1.1 + /q@1.5.1: + resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} + engines: {node: '>=0.6.0', teleport: '>=0.2.0'} dev: true - /qs/6.10.1: - resolution: {integrity: sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==} - engines: {node: '>=0.6'} - dependencies: - side-channel: 1.0.4 + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true - /qs/6.5.2: - resolution: {integrity: sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==} - engines: {node: '>=0.6'} + /quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} dev: false - optional: true - /rc/1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true + /quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + dev: true + + /read-pkg-up@3.0.0: + resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} + engines: {node: '>=4'} dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.5 - strip-json-comments: 2.0.1 + find-up: 2.1.0 + read-pkg: 3.0.0 + dev: true - /readable-stream/2.3.7: - resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} + /read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + dev: true + + /read-pkg@3.0.0: + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} + engines: {node: '>=4'} + dependencies: + load-json-file: 4.0.0 + normalize-package-data: 2.5.0 + path-type: 3.0.0 + dev: true + + /read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + dependencies: + '@types/normalize-package-data': 2.4.1 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + dev: true + + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} dependencies: core-util-is: 1.0.3 inherits: 2.0.4 @@ -2592,275 +3170,259 @@ packages: safe-buffer: 5.1.2 string_decoder: 1.1.1 util-deprecate: 1.0.2 + dev: true - /readable-stream/3.6.0: - resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - /readdirp/3.6.0: + /readable-stream@4.4.0: + resolution: {integrity: sha512-kDMOq0qLtxV9f/SQv522h8cxZBqNZXuXNyjyezmfAAuribMyVXziljpQ/uQhfE1XLg2/TLTW2DsnoE4VAi/krg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + dev: false + + /readable-web-to-node-stream@3.0.2: + resolution: {integrity: sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==} + engines: {node: '>=8'} + dependencies: + readable-stream: 3.6.2 + dev: false + + /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} dependencies: - picomatch: 2.3.0 + picomatch: 2.3.1 dev: true - /regenerator-runtime/0.13.9: - resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} - dev: true - - /regexpp/3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - dev: true - - /registry-auth-token/4.2.1: - resolution: {integrity: sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==} - engines: {node: '>=6.0.0'} - dependencies: - rc: 1.2.8 - dev: true - - /registry-url/5.1.0: - resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==} - engines: {node: '>=8'} - dependencies: - rc: 1.2.8 - dev: true - - /request/2.88.2: - resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} - engines: {node: '>= 6'} - deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 - dependencies: - aws-sign2: 0.7.0 - aws4: 1.11.0 - caseless: 0.12.0 - combined-stream: 1.0.8 - extend: 3.0.2 - forever-agent: 0.6.1 - form-data: 2.3.3 - har-validator: 5.1.5 - http-signature: 1.2.0 - is-typedarray: 1.0.0 - isstream: 0.1.2 - json-stringify-safe: 5.0.1 - mime-types: 2.1.33 - oauth-sign: 0.9.0 - performance-now: 2.1.0 - qs: 6.5.2 - safe-buffer: 5.2.1 - tough-cookie: 2.5.0 - tunnel-agent: 0.6.0 - uuid: 3.4.0 + /real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} dev: false - optional: true - /require-directory/2.1.1: - resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=} + /redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: true + + /regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} dev: true - /require-from-string/2.0.2: + /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - dev: true - /require-main-filename/2.0.0: + /require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} dev: true - /resolve-from/4.0.0: + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} dev: true - /responselike/1.0.2: - resolution: {integrity: sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=} - dependencies: - lowercase-keys: 1.0.1 + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} dev: true - /restore-cursor/3.1.0: + /resolve-global@1.0.0: + resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==} + engines: {node: '>=8'} + dependencies: + global-dirs: 0.1.1 + dev: true + + /resolve@1.22.2: + resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} + hasBin: true + dependencies: + is-core-module: 2.12.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} dependencies: onetime: 5.1.2 - signal-exit: 3.0.5 + signal-exit: 3.0.7 dev: true - /retry-as-promised/3.2.0: - resolution: {integrity: sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg==} - dependencies: - any-promise: 1.3.0 + /ret@0.2.2: + resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} + engines: {node: '>=4'} dev: false - /rimraf/2.7.1: - resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - hasBin: true - dependencies: - glob: 7.2.0 - dev: false + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - /rimraf/3.0.2: + /rfdc@1.3.0: + resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} + + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true dependencies: - glob: 7.2.0 + glob: 7.2.3 dev: true - /run-async/2.4.1: + /run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} dev: true - /rxjs/6.6.7: + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /rxjs@6.6.7: resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} engines: {npm: '>=2.0.0'} dependencies: tslib: 1.14.1 dev: true - /safe-buffer/5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - /safe-buffer/5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - /safer-buffer/2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - /sax/1.2.4: - resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} - - /semver-compare/1.0.0: - resolution: {integrity: sha1-De4hahyUGrN+nvsXiPavxf9VN/w=} - - /semver-diff/3.1.1: - resolution: {integrity: sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==} - engines: {node: '>=8'} + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: - semver: 6.3.0 + tslib: 2.5.2 dev: true - /semver/5.3.0: - resolution: {integrity: sha1-myzl094C0XxgEq0yaqa00M9U+U8=} - hasBin: true - dev: false - optional: true + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: true - /semver/5.7.1: + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + /safe-regex2@2.0.0: + resolution: {integrity: sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==} + dependencies: + ret: 0.2.2 + dev: false + + /safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + /secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + dev: false + + /semver@5.7.1: resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} hasBin: true + dev: true - /semver/6.3.0: + /semver@6.3.0: resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} hasBin: true dev: true - /semver/7.3.5: - resolution: {integrity: sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==} + /semver@7.0.0: + resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} + hasBin: true + dev: true + + /semver@7.5.0: + resolution: {integrity: sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /semver@7.5.1: + resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==} engines: {node: '>=10'} hasBin: true dependencies: lru-cache: 6.0.0 - /seq-queue/0.0.5: - resolution: {integrity: sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=} + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: true - /sequelize-pool/6.1.0: - resolution: {integrity: sha512-4YwEw3ZgK/tY/so+GfnSgXkdwIJJ1I32uZJztIEgZeAO6HMgj64OzySbWLgxj+tXhZCJnzRfkY9gINw8Ft8ZMg==} - engines: {node: '>= 10.0.0'} + /set-cookie-parser@2.6.0: + resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} dev: false - /sequelize/6.6.5_gsgxvhwyg44bfarb3k5znvycta: - resolution: {integrity: sha512-QyRrJrDRiwuiILqTMHUA1yWOPIL12KlfmgZ3hnzQwbMvp2vJ6fzu9bYJQB+qPMosck4mBUggY4Cjoc6Et8FBIQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - mariadb: '*' - mysql2: '*' - pg: '*' - pg-hstore: '*' - sqlite3: '*' - tedious: '*' - peerDependenciesMeta: - mariadb: - optional: true - mysql2: - optional: true - pg: - optional: true - pg-hstore: - optional: true - sqlite3: - optional: true - tedious: - optional: true - dependencies: - debug: 4.3.2 - dottie: 2.0.2 - inflection: 1.13.1 - lodash: 4.17.21 - mariadb: 2.5.4 - moment: 2.29.1 - moment-timezone: 0.5.33 - mysql2: 2.3.0 - pg: 8.7.1 - pg-hstore: 2.3.4 - retry-as-promised: 3.2.0 - semver: 7.3.5 - sequelize-pool: 6.1.0 - sqlite3: 5.0.2 - tedious: 11.8.0 - toposort-class: 1.0.1 - uuid: 8.3.2 - validator: 13.6.0 - wkx: 0.5.0 - transitivePeerDependencies: - - supports-color + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: false - /set-blocking/2.0.0: - resolution: {integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc=} - - /shebang-command/2.0.0: + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} dependencies: shebang-regex: 3.0.0 dev: true - /shebang-regex/3.0.0: + /shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} dev: true - /side-channel/1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + + /simple-oauth2@3.4.0: + resolution: {integrity: sha512-gDPCC/xjq82FJnzF7+XGUUMWWfHeibuGsp3OYOV7yHwIibxHkq4+WSxywY63/3BF9j8SfIDygGqBrPLynx/iuQ==} + deprecated: simple-oauth2 v3 is no longer supported. Use simple-oauth2 v4 or higher for continued support dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.1.1 - object-inspect: 1.11.0 + '@hapi/hoek': 8.5.1 + '@hapi/joi': 16.1.8 + '@hapi/wreck': 15.1.0 + date-fns: 2.30.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false - /signal-exit/3.0.5: - resolution: {integrity: sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==} - - /simple-concat/1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - optional: true - - /simple-get/3.1.0: - resolution: {integrity: sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==} + /simple-update-notifier@1.1.0: + resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} + engines: {node: '>=8.10.0'} dependencies: - decompress-response: 4.2.1 - once: 1.4.0 - simple-concat: 1.0.1 - optional: true + semver: 7.0.0 + dev: true - /slice-ansi/4.0.0: + /slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: true + + /slice-ansi@4.0.0: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} dependencies: @@ -2869,67 +3431,103 @@ packages: is-fullwidth-code-point: 3.0.0 dev: true - /split2/3.2.2: - resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + /slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} dependencies: - readable-stream: 3.6.0 - - /sprintf-js/1.0.3: - resolution: {integrity: sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=} + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 dev: true - /sprintf-js/1.1.2: - resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==} - - /sqlite3/5.0.2: - resolution: {integrity: sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA==} - requiresBuild: true - peerDependenciesMeta: - node-gyp: - optional: true + /sonic-boom@3.3.0: + resolution: {integrity: sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==} dependencies: - node-addon-api: 3.2.1 - node-pre-gyp: 0.11.0 - optionalDependencies: - node-gyp: 3.8.0 - transitivePeerDependencies: - - supports-color + atomic-sleep: 1.0.0 dev: false - /sqlstring/2.3.2: - resolution: {integrity: sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg==} - engines: {node: '>= 0.6'} - - /sshpk/1.16.1: - resolution: {integrity: sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==} - engines: {node: '>=0.10.0'} - hasBin: true - dependencies: - asn1: 0.2.4 - assert-plus: 1.0.0 - bcrypt-pbkdf: 1.0.2 - dashdash: 1.14.1 - ecc-jsbn: 0.1.2 - getpass: 0.1.7 - jsbn: 0.1.1 - safer-buffer: 2.1.2 - tweetnacl: 0.14.5 + /sortablejs@1.15.0: + resolution: {integrity: sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==} dev: false - optional: true - /stoppable/1.1.0: - resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} - engines: {node: '>=4', npm: '>=6'} - - /string-width/1.0.2: - resolution: {integrity: sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=} + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} - dependencies: - code-point-at: 1.1.0 - is-fullwidth-code-point: 1.0.0 - strip-ansi: 3.0.1 + dev: false - /string-width/4.2.3: + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /spacetime@7.4.4: + resolution: {integrity: sha512-88oqexyzGTzMCOibaBTE+3HXmaT+mw3EZVMymNTn1Xkg/6bC+nA3ZvsfmFEwxzc0VfG9BYYaXevvLiDRP7RJ6A==} + dev: false + + /spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.13 + dev: true + + /spdx-exceptions@2.3.0: + resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + dev: true + + /spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.3.0 + spdx-license-ids: 3.0.13 + dev: true + + /spdx-license-ids@3.0.13: + resolution: {integrity: sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==} + dev: true + + /split2@3.2.2: + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + dependencies: + readable-stream: 3.6.2 + dev: true + + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + dev: false + + /split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + dependencies: + through: 2.3.8 + dev: true + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + + /steed@1.1.3: + resolution: {integrity: sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==} + dependencies: + fastfall: 1.5.1 + fastparallel: 2.4.1 + fastq: 1.15.0 + fastseries: 1.7.2 + reusify: 1.0.4 + dev: false + + /streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: false + + /string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + dev: true + + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} dependencies: @@ -2937,365 +3535,372 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - /string_decoder/1.1.1: + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + /string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} dependencies: safe-buffer: 5.1.2 + dev: true - /string_decoder/1.3.0: + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: safe-buffer: 5.2.1 - /strip-ansi/3.0.1: - resolution: {integrity: sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=} - engines: {node: '>=0.10.0'} - dependencies: - ansi-regex: 2.1.1 - - /strip-ansi/6.0.1: + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 - /strip-json-comments/2.0.1: - resolution: {integrity: sha1-PFMZQukIwml8DsNEhYwobHygpgo=} - engines: {node: '>=0.10.0'} + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 - /strip-json-comments/3.1.1: + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: true + + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + + /strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} dev: true - /supports-color/5.5.0: + /strtok3@7.0.0: + resolution: {integrity: sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==} + engines: {node: '>=14.16'} + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 5.0.0 + dev: false + + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} dependencies: has-flag: 3.0.0 dev: true - /supports-color/7.2.0: + /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} dependencies: has-flag: 4.0.0 - /supports-hyperlinks/2.2.0: - resolution: {integrity: sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==} + /supports-hyperlinks@2.3.0: + resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} engines: {node: '>=8'} dependencies: has-flag: 4.0.0 supports-color: 7.2.0 dev: false - /table/6.7.2: - resolution: {integrity: sha512-UFZK67uvyNivLeQbVtkiUs8Uuuxv24aSL4/Vil2PJVtMgU8Lx0CYkP12uCGa3kjyQzOSgV1+z9Wkb82fCGsO0g==} - engines: {node: '>=10.0.0'} - dependencies: - ajv: 8.6.3 - lodash.clonedeep: 4.5.0 - lodash.truncate: 4.4.2 - slice-ansi: 4.0.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} dev: true - /tar-fs/2.1.1: - resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + /svelte-modals@1.2.1(svelte@3.59.1): + resolution: {integrity: sha512-7MEKUx5wb5YppkXWFGeRlYM5FMGEnpix39u9Y6GtCNTMXRDZ7DB2Z50IYLMRTMW5lOsCdtJgFbB0E3iZMKmsAA==} + peerDependencies: + svelte: ^3.0.0 dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.0 - tar-stream: 2.2.0 - optional: true - - /tar-stream/2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.4 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.0 - optional: true - - /tar/2.2.2: - resolution: {integrity: sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==} - deprecated: This version of tar is no longer supported, and will not receive security updates. Please upgrade asap. - dependencies: - block-stream: 0.0.9 - fstream: 1.0.12 - inherits: 2.0.4 - dev: false - optional: true - - /tar/4.4.19: - resolution: {integrity: sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==} - engines: {node: '>=4.5'} - dependencies: - chownr: 1.1.4 - fs-minipass: 1.2.7 - minipass: 2.9.0 - minizlib: 1.3.3 - mkdirp: 0.5.5 - safe-buffer: 5.2.1 - yallist: 3.1.1 + svelte: 3.59.1 dev: false - /tedious/11.8.0: - resolution: {integrity: sha512-GtFrO694x/7CRiUBt0AI4jrMtrkXV+ywifiOrDy4K0ufJLeKB4rgmPjy5Ws366fCaBaKlqQ9RnJ+sCJ1Jbd1lw==} - engines: {node: '>= 10'} - dependencies: - '@azure/identity': 1.5.2 - '@azure/keyvault-keys': 4.3.0 - '@azure/ms-rest-nodeauth': 3.1.0 - '@js-joda/core': 3.2.0 - adal-node: 0.2.3 - bl: 5.0.0 - depd: 2.0.0 - iconv-lite: 0.6.3 - jsbi: 3.2.5 - native-duplexpair: 1.0.0 - node-abort-controller: 2.0.0 - punycode: 2.1.1 - sprintf-js: 1.1.2 - transitivePeerDependencies: - - debug - - encoding - - supports-color + /svelte-toasts@1.1.2: + resolution: {integrity: sha512-m+yL4eEKXyJoyjTYaH1j1GFwF0Pi8YDqnVfwWPDmwi4712iZesv+TNCmToSNlav3R5Vkmc8ZBRkT8DOcu3sywQ==} + dev: false - /terminal-link/2.1.1: + /svelte@3.59.1: + resolution: {integrity: sha512-pKj8fEBmqf6mq3/NfrB9SLtcJcUvjYSWyePlfCqN9gujLB25RitWK8PvFzlwim6hD/We35KbPlRteuA6rnPGcQ==} + engines: {node: '>= 8'} + dev: false + + /temp-dir@2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + dev: true + + /tempfile@3.0.0: + resolution: {integrity: sha512-uNFCg478XovRi85iD42egu+eSFUmmka750Jy7L5tfHI5hQKKtbPnxaSaXAbBqCDYrw3wx4tXjKwci4/QmsZJxw==} + engines: {node: '>=8'} + dependencies: + temp-dir: 2.0.0 + uuid: 3.4.0 + dev: true + + /terminal-link@2.1.1: resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} engines: {node: '>=8'} dependencies: ansi-escapes: 4.3.2 - supports-hyperlinks: 2.2.0 + supports-hyperlinks: 2.3.0 dev: false - /text-table/0.2.0: - resolution: {integrity: sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=} + /text-extensions@1.9.0: + resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} + engines: {node: '>=0.10'} dev: true - /through/2.3.8: - resolution: {integrity: sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=} + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true - /tmp/0.0.33: + /thread-stream@2.3.0: + resolution: {integrity: sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==} + dependencies: + real-require: 0.2.0 + dev: false + + /through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + dev: true + + /through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + dependencies: + readable-stream: 3.6.2 + dev: true + + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + dev: true + + /tiny-lru@11.0.1: + resolution: {integrity: sha512-iNgFugVuQgBKrqeO/mpiTTgmBsTP0WL6yeuLfLs/Ctf0pI/ixGqIRm8sDCwMcXGe9WWvt2sGXI5mNqZbValmJg==} + engines: {node: '>=12'} + dev: false + + /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} dependencies: os-tmpdir: 1.0.2 dev: true - /to-readable-stream/1.0.0: - resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==} - engines: {node: '>=6'} - dev: true - - /to-regex-range/5.0.1: + /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 dev: true - /toposort-class/1.0.1: - resolution: {integrity: sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=} + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} dev: false - /touch/3.1.0: + /token-types@5.0.1: + resolution: {integrity: sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==} + engines: {node: '>=14.16'} + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + dev: false + + /touch@3.1.0: resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} hasBin: true dependencies: nopt: 1.0.10 dev: true - /tough-cookie/2.5.0: - resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} - engines: {node: '>=0.8'} - dependencies: - psl: 1.8.0 - punycode: 2.1.1 - dev: false - optional: true - - /tough-cookie/3.0.1: - resolution: {integrity: sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==} - engines: {node: '>=6'} - dependencies: - ip-regex: 2.1.0 - psl: 1.8.0 - punycode: 2.1.1 - - /tough-cookie/4.0.0: - resolution: {integrity: sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==} - engines: {node: '>=6'} - dependencies: - psl: 1.8.0 - punycode: 2.1.1 - universalify: 0.1.2 - - /tr46/0.0.3: + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: true - /ts-mixer/6.0.3: + /trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + dev: true + + /ts-mixer@6.0.3: resolution: {integrity: sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==} dev: false - /tslib/1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - - /tslib/2.3.1: - resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} - - /tslib/2.5.0: - resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} - - /tunnel-agent/0.6.0: - resolution: {integrity: sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=} + /ts-node@10.9.1(@types/node@20.2.5)(typescript@5.0.4): + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true dependencies: - safe-buffer: 5.2.1 - optional: true + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.2.5 + acorn: 8.8.2 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.0.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true - /tunnel/0.0.6: - resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} - engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true - /tweetnacl/0.14.5: - resolution: {integrity: sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=} - dev: false - optional: true + /tslib@2.5.2: + resolution: {integrity: sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==} - /type-check/0.4.0: + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} dependencies: prelude-ls: 1.2.1 dev: true - /type-fest/0.20.2: + /type-fest@0.18.1: + resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} + dev: true - /type-fest/0.21.3: + /type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - /typedarray-to-buffer/3.1.5: - resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} - dependencies: - is-typedarray: 1.0.0 - dev: true - - /undefsafe/2.0.3_supports-color@5.5.0: - resolution: {integrity: sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==} - dependencies: - debug: 2.6.9_supports-color@5.5.0 - transitivePeerDependencies: - - supports-color - dev: true - - /underscore/1.13.1: - resolution: {integrity: sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==} - - /unique-string/2.0.0: - resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + /type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} - dependencies: - crypto-random-string: 2.0.0 dev: true - /universalify/0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - - /update-notifier/5.1.0: - resolution: {integrity: sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==} - engines: {node: '>=10'} - dependencies: - boxen: 5.1.2 - chalk: 4.1.2 - configstore: 5.0.1 - has-yarn: 2.1.0 - import-lazy: 2.1.0 - is-ci: 2.0.0 - is-installed-globally: 0.4.0 - is-npm: 5.0.0 - is-yarn-global: 0.3.0 - latest-version: 5.1.0 - pupa: 2.1.1 - semver: 7.3.5 - semver-diff: 3.1.1 - xdg-basedir: 4.0.0 + /type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} dev: true - /uri-js/4.4.1: + /type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + dev: false + + /typescript@5.0.4: + resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} + engines: {node: '>=12.20'} + hasBin: true + dev: true + + /uglify-js@3.17.4: + resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + engines: {node: '>=0.8.0'} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + dev: true + + /undici@5.22.1: + resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==} + engines: {node: '>=14.0'} + dependencies: + busboy: 1.6.0 + dev: false + + /universalify@2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + + /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: - punycode: 2.1.1 + punycode: 2.3.0 - /url-parse-lax/3.0.0: - resolution: {integrity: sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=} - engines: {node: '>=4'} + /utf-8-validate@5.0.10: + resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} + engines: {node: '>=6.14.2'} + requiresBuild: true dependencies: - prepend-http: 2.0.0 - dev: true + node-gyp-build: 4.6.0 + dev: false - /util-deprecate/1.0.2: - resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - /uuid/3.4.0: + /uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. hasBin: true - - /uuid/8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - - /v8-compile-cache/2.3.0: - resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} dev: true - /validator/13.6.0: - resolution: {integrity: sha512-gVgKbdbHgtxpRyR8K0O6oFZPhhB5tT1jeEHZR0Znr9Svg03U0+r9DXWMrnRAB+HtCStDQKlaIZm42tVsVjqtjg==} - engines: {node: '>= 0.10'} - dev: false + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true - /verror/1.10.0: - resolution: {integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=} - engines: {'0': node >=0.6.0} + /validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: - assert-plus: 1.0.0 - core-util-is: 1.0.2 - extsprintf: 1.3.0 - dev: false - optional: true + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + dev: true - /webidl-conversions/3.0.1: + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: true - /whatwg-url/5.0.0: + /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - - /which-module/2.0.0: - resolution: {integrity: sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=} dev: true - /which/1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: false - optional: true + /which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + dev: true - /which/2.0.2: + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true @@ -3303,29 +3908,23 @@ packages: isexe: 2.0.0 dev: true - /wide-align/1.1.3: - resolution: {integrity: sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==} + /widest-line@4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} dependencies: - string-width: 1.0.2 - - /widest-line/3.1.0: - resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} - engines: {node: '>=8'} - dependencies: - string-width: 4.2.3 - - /wkx/0.5.0: - resolution: {integrity: sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==} - dependencies: - '@types/node': 16.10.3 + string-width: 5.1.2 dev: false - /word-wrap/1.2.3: + /word-wrap@1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} dev: true - /wrap-ansi/6.2.0: + /wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + dev: true + + /wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} dependencies: @@ -3334,27 +3933,29 @@ packages: strip-ansi: 6.0.1 dev: true - /wrap-ansi/7.0.0: + /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - - /wrappy/1.0.2: - resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} - - /write-file-atomic/3.0.3: - resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} - dependencies: - imurmurhash: 0.1.4 - is-typedarray: 1.0.0 - signal-exit: 3.0.5 - typedarray-to-buffer: 3.1.5 dev: true - /ws/8.13.0: + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: false + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /ws@8.13.0(bufferutil@4.0.7)(utf-8-validate@5.0.10): resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} engines: {node: '>=10.0.0'} peerDependencies: @@ -3365,47 +3966,38 @@ packages: optional: true utf-8-validate: optional: true + dependencies: + bufferutil: 4.0.7 + utf-8-validate: 5.0.10 dev: false - /xdg-basedir/4.0.0: - resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} - engines: {node: '>=8'} - dev: true - - /xml2js/0.4.23: - resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} - engines: {node: '>=4.0.0'} - dependencies: - sax: 1.2.4 - xmlbuilder: 11.0.1 - - /xmlbuilder/11.0.1: - resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} - engines: {node: '>=4.0'} - - /xpath.js/1.1.0: - resolution: {integrity: sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ==} - engines: {node: '>=0.4.0'} - - /xtend/4.0.2: + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - /y18n/4.0.3: + /y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} dev: true - /yallist/2.1.2: - resolution: {integrity: sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=} + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true - /yallist/3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: false - - /yallist/4.0.0: + /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - /yargs-parser/18.1.3: + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: false + + /yaml@2.3.1: + resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} + engines: {node: '>= 14'} + dev: true + + /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} dependencies: @@ -3413,7 +4005,17 @@ packages: decamelize: 1.2.0 dev: true - /yargs/15.4.1: + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: true + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true + + /yargs@15.4.1: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} engines: {node: '>=8'} dependencies: @@ -3425,7 +4027,62 @@ packages: require-main-filename: 2.0.0 set-blocking: 2.0.0 string-width: 4.2.3 - which-module: 2.0.0 + which-module: 2.0.1 y18n: 4.0.3 yargs-parser: 18.1.3 dev: true + + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + dev: true + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true + + /zlib-sync@0.1.8: + resolution: {integrity: sha512-Xbu4odT5SbLsa1HFz8X/FvMgUbJYWxJYKB2+bqxJ6UOIIPaVGrqHEB3vyXDltSA6tTqBhSGYLgiVpzPQHYi3lA==} + requiresBuild: true + dependencies: + nan: 2.17.0 + dev: false + optional: true + + github.com/discord/erlpack/cbe76be04c2210fc9cb6ff95910f0937c1011d04: + resolution: {tarball: https://codeload.github.com/discord/erlpack/tar.gz/cbe76be04c2210fc9cb6ff95910f0937c1011d04} + name: erlpack + version: 0.1.3 + requiresBuild: true + dependencies: + bindings: 1.5.0 + nan: 2.17.0 + dev: false + optional: true diff --git a/pterodactyl.egg.json b/pterodactyl.egg.json deleted file mode 100644 index 30ffacf..0000000 --- a/pterodactyl.egg.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", - "meta": { - "version": "PTDL_v1", - "update_url": null - }, - "exported_at": "2021-05-26T22:41:44+02:00", - "name": "Discord Tickets", - "author": "contact@discordtickets.app", - "description": "https:\/\/discordtickets.app", - "features": null, - "images": [ - "ghcr.io\/parkervcp\/yolks:nodejs_16" - ], - "file_denylist": [], - "startup": "if [[ ! -z ${VERSION} ]]; then\r\n echo -e \\\"Using version ${VERSION}\\\";\r\nelse\r\n echo -e \\\"Please set the VERSION variable \\(e.g. v3.1.1\\)\\\";\r\n exit 0;\r\nfi;\r\n\r\nif [[ -d .git ]]; then\r\n git fetch --all --tags;\r\n git checkout tags\/${VERSION};\r\nfi;\r\n\r\nif [ -f \/home\/container\/package.json ]; then\r\n \/usr\/local\/bin\/npm install --production;\r\nfi;\r\n\r\nif [[ ! -z ${PLUGINS} ]]; then\r\n \/usr\/local\/bin\/npm install ${PLUGINS} --no-save;\r\nfi;\r\n\r\n\/usr\/local\/bin\/npm start", - "config": { - "files": "{}", - "startup": "{\r\n \"done\": \"Connected to Discord as \",\r\n \"userInteraction\": [\r\n \"Please set your bot's \\\"DISCORD_TOKEN\\\" in \\\".\/.env\\\".\"\r\n ]\r\n}", - "logs": "{}", - "stop": "^C^C" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/bash\r\n\r\napt update\r\napt install -y git curl jq file unzip make gcc g++ python python-dev libtool\r\n\r\nmkdir -p \/mnt\/server\r\ncd \/mnt\/server\r\n\r\nif [[ ! -z ${VERSION} ]]; then\r\n echo -e \\\"Using version ${VERSION}\\\"\r\nelse\r\n echo -e \\\"Please set the VERSION variable \\(e.g. v3.0.0\\)\\\"\r\n exit 1\r\nfi\r\n\r\nif [ \\\"$(ls -A \/mnt\/server)\\\" ] && [[ -d .git ]]; then\r\n echo -e \\\".git directory exists\\\"\r\n if [ -f .git\\\/config ]; then\r\n echo \\\"Upating...\\\"\r\n git fetch --all --tags\r\n git checkout tags\/${VERSION}\r\n else\r\n echo -e \\\"files found with no git config\\\"\r\n echo -e \\\"closing out without touching things to not break anything\\\"\r\n exit 1\r\n fi\r\nelse\r\n echo -e \\\"Cloning...\\\"\r\n git clone https:\/\/github.com\/discord-tickets\/bot.git .\r\n\tgit checkout tags\/${VERSION}\r\nfi\r\n\r\necho \\\"Installing dependencies\\\"\r\n\r\nif [[ ! -z ${PUGINS} ]]; then\r\n \/usr\/local\/bin\/npm install ${PUGINS}\r\nfi\r\n\r\nif [ -f \/mnt\/server\/package.json ]; then\r\n \/usr\/local\/bin\/npm install --production\r\nfi\r\n\r\necho -e \\\"Installed\\\"\r\nexit 0", - "container": "node:16-bullseye-slim", - "entrypoint": "bash" - } - }, - "variables": [ - { - "name": "Version", - "description": "The version of the bot to use.", - "env_variable": "VERSION", - "default_value": "v3.1.3", - "user_viewable": true, - "user_editable": true, - "rules": "required|string|max:20" - }, - { - "name": "Plugins", - "description": "A list of extra NPM package names to install as plugins, separated by a space: \"example1 exmaple2 example3\"", - "env_variable": "PLUGINS", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": "nullable|string" - } - ] -} diff --git a/scripts/keygen.js b/scripts/keygen.js new file mode 100644 index 0000000..6b19700 --- /dev/null +++ b/scripts/keygen.js @@ -0,0 +1,9 @@ +/* eslint-disable no-console */ +const { randomBytes } = require('crypto'); +const { short } = require('leeks.js'); + +console.log(short( + 'Set the "ENCRYPTION_KEY" environment variable to: \n&!b ' + + randomBytes(24).toString('hex') + + ' &r\n\n&0&!e WARNING &r &e&lIf you lose the encryption key, most of the data in the database will become unreadable, requiring a new key and a full reset.', +)); \ No newline at end of file diff --git a/scripts/postinstall.js b/scripts/postinstall.js new file mode 100644 index 0000000..f36bd39 --- /dev/null +++ b/scripts/postinstall.js @@ -0,0 +1,42 @@ +/* eslint-disable no-console */ +require('dotenv').config(); +const fs = require('fs-extra'); +const util = require('util'); +const exec = util.promisify(require('child_process').exec); +const { short } = require('leeks.js'); + +function log(...strings) { + console.log(short('&9[postinstall]&r'), ...strings); +} + +async function npx(cmd) { + log(`> ${cmd}`); + const { + stderr, + stdout, + } = await exec('npx ' + cmd); + if (stdout) console.log(stdout.toString()); + if (stderr) console.log(stderr.toString()); +} + +const providers = ['mysql', 'postgresql', 'sqlite']; +const provider = process.env.DB_PROVIDER; + +if (!provider) { + log('environment not set, exiting.'); + process.exit(0); +} + +if (!providers.includes(provider)) throw new Error(`DB_PROVIDER must be one of: ${providers}`); + +log(`provider=${provider}`); +log(`copying ${provider} schema & migrations`); + +if (!fs.existsSync('./prisma')) fs.mkdirSync('./prisma'); +fs.copySync(`./db/${provider}`, './prisma'); // copy schema & migrations + +(async () => { + await npx('prisma generate'); + await npx('prisma migrate deploy'); +})(); + diff --git a/scripts/preinstall.js b/scripts/preinstall.js new file mode 100644 index 0000000..c30c1f4 --- /dev/null +++ b/scripts/preinstall.js @@ -0,0 +1,40 @@ +/* eslint-disable no-console */ +const { randomBytes } = require('crypto'); +const fs = require('fs'); +const { short } = require('leeks.js'); + +function log (...strings) { + console.log(short('&9[preinstall]&r'), ...strings); +} + +if (process.env.CI) { + log('CI detected, skipping'); + process.exit(0); +} + +const env = { + DB_CONNECTION_URL: '', + DB_PROVIDER: '', // don't default to sqlite, postinstall checks if empty + DISCORD_SECRET: '', + DISCORD_TOKEN: '', + ENCRYPTION_KEY: randomBytes(24).toString('hex'), + HTTP_EXTERNAL: 'http://127.0.0.1:8169', + HTTP_HOST: '0.0.0.0', + HTTP_PORT: 8169, + HTTP_TRUST_PROXY: false, + NODE_ENV: 'production', // not bot-specific + OVERRIDE_ARCHIVE: '', + PUBLIC_BOT: false, + PUBLISH_COMMANDS: false, + SUPER: '319467558166069248', +}; + +// check ENCRYPTION_KEY because we don't want to force use of the .env file +if (!process.env.ENCRYPTION_KEY && !fs.existsSync('./.env')) { + log('generating ENCRYPTION_KEY'); + fs.writeFileSync('./.env', Object.entries(env).map(([k, v]) => `${k}=${v}`).join('\n')); + log('created .env file'); + log(short('&r&0&!e WARNING &r &e&lkeep your environment variables safe, don\'t lose your encryption key or you will lose data')); +} else { + log('nothing to do'); +} \ No newline at end of file diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100755 index 0000000..89fede7 --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +echo "Checking environment..." +node scripts/preinstall + +echo "Preparing the database..." +node scripts/postinstall + +echo "Starting..." +node src/ \ No newline at end of file diff --git a/src/autocomplete/category.js b/src/autocomplete/category.js new file mode 100644 index 0000000..fbba71d --- /dev/null +++ b/src/autocomplete/category.js @@ -0,0 +1,37 @@ +const { Autocompleter } = require('@eartharoid/dbf'); + +module.exports = class CategoryCompleter extends Autocompleter { + constructor(client, options) { + super(client, { + ...options, + id: 'category', + }); + } + + /** + * @param {string} value + * @param {*} command + * @param {import("discord.js").AutocompleteInteraction} interaction + */ + async run(value, command, interaction) { + /** @type {import("client")} */ + const client = this.client; + + let categories = await client.prisma.category.findMany({ where: { guildId: interaction.guild.id } }); + + if (command.name === 'move') { + const ticket = await client.prisma.ticket.findUnique({ where: { id: interaction.channel.id } }); + if (ticket) categories = categories.filter(category => ticket.categoryId !== category.id); + } + + const options = value ? categories.filter(category => category.name.match(new RegExp(value, 'i'))) : categories; + await interaction.respond( + options + .slice(0, 25) + .map(category => ({ + name: category.name, + value: category.id, + })), + ); + } +}; diff --git a/src/autocomplete/references.js b/src/autocomplete/references.js new file mode 100644 index 0000000..300371e --- /dev/null +++ b/src/autocomplete/references.js @@ -0,0 +1,26 @@ +const { Autocompleter } = require('@eartharoid/dbf'); + +module.exports = class ReferencesCompleter extends Autocompleter { + constructor(client, options) { + super(client, { + ...options, + id: 'references', + }); + } + + + /** + * @param {string} value + * @param {*} comamnd + * @param {import("discord.js").AutocompleteInteraction} interaction + */ + async run(value, comamnd, interaction) { + await interaction.respond( + await this.client.autocomplete.components.get('ticket').getOptions(value, { + guildId: interaction.guild.id, + open: false, + userId: interaction.user.id, + }), + ); + } +}; \ No newline at end of file diff --git a/src/autocomplete/tag.js b/src/autocomplete/tag.js new file mode 100644 index 0000000..90dcef6 --- /dev/null +++ b/src/autocomplete/tag.js @@ -0,0 +1,50 @@ +const { Autocompleter } = require('@eartharoid/dbf'); +const ms = require('ms'); + +module.exports = class TagCompleter extends Autocompleter { + constructor(client, options) { + super(client, { + ...options, + id: 'tag', + }); + } + + /** + * @param {string} value + * @param {*} command + * @param {import("discord.js").AutocompleteInteraction} interaction + */ + async run(value, command, interaction) { + /** @type {import("client")} */ + const client = this.client; + + const cacheKey = `cache/guild-tags:${interaction.guild.id}`; + let tags = await client.keyv.get(cacheKey); + if (!tags) { + tags = await client.prisma.tag.findMany({ + select: { + content: true, + id: true, + name: true, + regex: true, + }, + where: { guildId: interaction.guild.id }, + }); + client.keyv.set(cacheKey, tags, ms('1h')); + } + + const options = value ? tags.filter(tag => + tag.name.match(new RegExp(value, 'i')) || + tag.content.match(new RegExp(value, 'i')) || + tag.regex?.match(new RegExp(value, 'i')), + ) : tags; + await interaction.respond( + options + .slice(0, 25) + .map(tag => ({ + name: tag.name, + value: tag.id, + })), + ); + } +}; \ No newline at end of file diff --git a/src/autocomplete/ticket.js b/src/autocomplete/ticket.js new file mode 100644 index 0000000..c18eec6 --- /dev/null +++ b/src/autocomplete/ticket.js @@ -0,0 +1,86 @@ +/* eslint-disable no-underscore-dangle */ +const { Autocompleter } = require('@eartharoid/dbf'); +const emoji = require('node-emoji'); +const Cryptr = require('cryptr'); +const { decrypt } = new Cryptr(process.env.ENCRYPTION_KEY); +const Keyv = require('keyv'); +const ms = require('ms'); +const { isStaff } = require('../lib/users'); + +module.exports = class TicketCompleter extends Autocompleter { + constructor(client, options) { + super(client, { + ...options, + id: 'ticket', + }); + + this.cache = new Keyv(); + } + + async getOptions(value, { + guildId, + open, + userId, + }) { + /** @type {import("client")} */ + const client = this.client; + const cacheKey = [guildId, userId, open].join('/'); + + let tickets = await this.cache.get(cacheKey); + + if (!tickets) { + const { locale } = await client.prisma.guild.findUnique({ + select: { locale: true }, + where: { id: guildId }, + }); + tickets = await client.prisma.ticket.findMany({ + include: { + category: { + select: { + emoji: true, + name: true, + }, + }, + }, + where: { + createdById: userId, + guildId, + open, + }, + }); + tickets = tickets.map(ticket => { + const date = new Date(ticket.createdAt).toLocaleString([locale, 'en-GB'], { dateStyle: 'short' }); + const topic = ticket.topic ? '- ' + decrypt(ticket.topic).replace(/\n/g, ' ').substring(0, 50) : ''; + const category = emoji.hasEmoji(ticket.category.emoji) ? emoji.get(ticket.category.emoji) + ' ' + ticket.category.name : ticket.category.name; + ticket._name = `${category} #${ticket.number} (${date}) ${topic}`; + return ticket; + }); + this.cache.set(cacheKey, tickets, ms('1m')); + } + + const options = value ? tickets.filter(t => t._name.match(new RegExp(value, 'i'))) : tickets; + return options + .slice(0, 25) + .map(t => ({ + name: t._name, + value: t.id, + })); + } + + /** + * @param {string} value + * @param {*} command + * @param {import("discord.js").AutocompleteInteraction} interaction + */ + async run(value, command, interaction) { + const otherMember = await isStaff(interaction.guild, interaction.user.id) && interaction.options.data[1]?.value; + const userId = otherMember || interaction.user.id; + await interaction.respond( + await this.getOptions(value, { + guildId: interaction.guild.id, + open: ['add', 'close', 'force-close', 'remove'].includes(command.name), // false for `new`, `transcript` etc + userId, + }), + ); + } +}; diff --git a/src/banner.js b/src/banner.js deleted file mode 100644 index daec75d..0000000 --- a/src/banner.js +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-disable no-console */ -const link = require('terminal-link'); -const leeks = require('leeks.js'); - -const { - version, homepage -} = require('../package.json'); - -module.exports = () => { - console.log(leeks.colours.cyan(` -######## #### ###### ###### ####### ######## ######## -## ## ## ## ## ## ## ## ## ## ## ## ## -## ## ## ## ## ## ## ## ## ## ## -## ## ## ###### ## ## ## ######## ## ## -## ## ## ## ## ## ## ## ## ## ## -## ## ## ## ## ## ## ## ## ## ## ## ## -######## #### ###### ###### ####### ## ## ######## - -######## #### ###### ## ## ######## ######## ###### - ## ## ## ## ## ## ## ## ## ## - ## ## ## ## ## ## ## ## - ## ## ## ##### ###### ## ###### - ## ## ## ## ## ## ## ## - ## ## ## ## ## ## ## ## ## ## - ## #### ###### ## ## ######## ## ###### -`)); - console.log(leeks.colours.cyanBright(`Discord Tickets bot v${version} by eartharoid`)); - console.log(leeks.colours.cyanBright(homepage + '\n')); - console.log(leeks.colours.cyanBright(link('Sponsor this project', 'https://discordtickets.app/sponsor') + '\n')); -}; diff --git a/src/buttons/claim.js b/src/buttons/claim.js new file mode 100644 index 0000000..c57e5df --- /dev/null +++ b/src/buttons/claim.js @@ -0,0 +1,22 @@ +const { Button } = require('@eartharoid/dbf'); + +module.exports = class ClaimButton extends Button { + constructor(client, options) { + super(client, { + ...options, + id: 'claim', + }); + } + + /** + * @param {*} id + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(id, interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply({ ephemeral: false }); + await client.tickets.claim(interaction); + } +}; \ No newline at end of file diff --git a/src/buttons/close.js b/src/buttons/close.js new file mode 100644 index 0000000..53ef089 --- /dev/null +++ b/src/buttons/close.js @@ -0,0 +1,81 @@ +const { Button } = require('@eartharoid/dbf'); +const ExtendedEmbedBuilder = require('../lib/embed'); +const { isStaff } = require('../lib/users'); + +module.exports = class CloseButton extends Button { + constructor(client, options) { + super(client, { + ...options, + id: 'close', + }); + } + + /** + * @param {*} id + * @param {import("discord.js").ButtonInteraction} interaction + */ + async run(id, interaction) { + /** @type {import("client")} */ + const client = this.client; + + if (id.accepted === undefined) { + // the close button on the opening message, the same as using /close + await client.tickets.beforeRequestClose(interaction); + } else { + const ticket = await client.tickets.getTicket(interaction.channel.id, true); // true to override cache and load new feedback + const getMessage = client.i18n.getLocale(ticket.guild.locale); + const staff = await isStaff(interaction.guild, interaction.user.id); + + if (id.expect === 'staff' && !staff) { + return await interaction.reply({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(ticket.guild.errorColour) + .setDescription(getMessage('ticket.close.wait_for_staff')), + ], + ephemeral: true, + }); + } else if (id.expect === 'user' && interaction.user.id !== ticket.createdById) { + return await interaction.reply({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(ticket.guild.errorColour) + .setDescription(getMessage('ticket.close.wait_for_user')), + ], + ephemeral: true, + }); + } else { + if (id.accepted) { + if ( + ticket.createdById === interaction.user.id && + ticket.category.enableFeedback && + !ticket.feedback + ) { + return await interaction.showModal(client.tickets.buildFeedbackModal(ticket.guild.locale, { next: 'acceptClose' })); + } else { + await interaction.deferReply(); + await client.tickets.acceptClose(interaction); + } + } else { + try { + await interaction.update({ + components: [], + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.errorColour) + .setDescription(getMessage('ticket.close.rejected', { user: interaction.user.toString() })) + .setFooter({ text: null }), + ], + }); + + } finally { // this should run regardless of whatever happens above + client.tickets.$stale.delete(ticket.id); + } + } + } + } + } +}; diff --git a/src/buttons/create.js b/src/buttons/create.js new file mode 100644 index 0000000..b5f5cf1 --- /dev/null +++ b/src/buttons/create.js @@ -0,0 +1,23 @@ +const { Button } = require('@eartharoid/dbf'); + +module.exports = class CreateButton extends Button { + constructor(client, options) { + super(client, { + ...options, + id: 'create', + }); + } + + /** + * @param {*} id + * @param {import("discord.js").ButtonInteraction} interaction + */ + async run(id, interaction) { + if (id.targetUser && id.targetUser !== interaction.user.id) return; + await this.client.tickets.create({ + categoryId: id.target, + interaction, + topic: id.topic, + }); + } +}; diff --git a/src/buttons/edit.js b/src/buttons/edit.js new file mode 100644 index 0000000..96e1764 --- /dev/null +++ b/src/buttons/edit.js @@ -0,0 +1,111 @@ +const { Button } = require('@eartharoid/dbf'); +const { + ActionRowBuilder, + ModalBuilder, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, + TextInputBuilder, + TextInputStyle, +} = require('discord.js'); +const emoji = require('node-emoji'); +const Cryptr = require('cryptr'); +const cryptr = new Cryptr(process.env.ENCRYPTION_KEY); + +module.exports = class EditButton extends Button { + constructor(client, options) { + super(client, { + ...options, + id: 'edit', + }); + } + + async run(id, interaction) { + /** @type {import("client")} */ + const client = this.client; + + const ticket = await client.prisma.ticket.findUnique({ + select: { + category: { select: { name: true } }, + guild: { select: { locale: true } }, + questionAnswers: { include: { question: true } }, + topic: true, + }, + where: { id: interaction.channel.id }, + }); + + const getMessage = client.i18n.getLocale(ticket.guild.locale); + + if (ticket.questionAnswers.length === 0) { + await interaction.showModal( + new ModalBuilder() + .setCustomId(JSON.stringify({ + action: 'topic', + edit: true, + })) + .setTitle(ticket.category.name) + .setComponents( + new ActionRowBuilder() + .setComponents( + new TextInputBuilder() + .setCustomId('topic') + .setLabel(getMessage('modals.topic.label')) + .setStyle(TextInputStyle.Paragraph) + .setMaxLength(1000) + .setMinLength(5) + .setPlaceholder(getMessage('modals.topic.placeholder')) + .setRequired(true) + .setValue(ticket.topic ? cryptr.decrypt(ticket.topic) : ''), + ), + ), + ); + } else { + await interaction.showModal( + new ModalBuilder() + .setCustomId(JSON.stringify({ + action: 'questions', + edit: true, + })) + .setTitle(ticket.category.name) + .setComponents( + ticket.questionAnswers + .filter(a => a.question.type === 'TEXT') // TODO: remove this when modals support select menus + .map(a => { + if (a.question.type === 'TEXT') { + return new ActionRowBuilder() + .setComponents( + new TextInputBuilder() + .setCustomId(String(a.id)) + .setLabel(a.question.label) + .setStyle(a.question.style) + .setMaxLength(Math.min(a.question.maxLength, 1000)) + .setMinLength(a.question.minLength) + .setPlaceholder(a.question.placeholder) + .setRequired(a.question.required) + .setValue(a.value ? cryptr.decrypt(a.value) : a.question.value), + ); + } else if (a.question.type === 'MENU') { + return new ActionRowBuilder() + .setComponents( + new StringSelectMenuBuilder() + .setCustomId(a.question.id) + .setPlaceholder(a.question.placeholder || a.question.label) + .setMaxValues(a.question.maxLength) + .setMinValues(a.question.minLength) + .setOptions( + a.question.options.map((o, i) => { + const builder = new StringSelectMenuOptionBuilder() + .setValue(String(i)) + .setLabel(o.label); + if (o.description) builder.setDescription(o.description); + if (o.emoji) builder.setEmoji(emoji.hasEmoji(o.emoji) ? emoji.get(o.emoji) : { id: o.emoji }); + return builder; + }), + ), + ); + } + }), + ), + ); + } + } +}; \ No newline at end of file diff --git a/src/buttons/unclaim.js b/src/buttons/unclaim.js new file mode 100644 index 0000000..e6c7f4e --- /dev/null +++ b/src/buttons/unclaim.js @@ -0,0 +1,22 @@ +const { Button } = require('@eartharoid/dbf'); + +module.exports = class UnclaimButton extends Button { + constructor(client, options) { + super(client, { + ...options, + id: 'unclaim', + }); + } + + /** + * @param {*} id + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(id, interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply({ ephemeral: false }); + await client.tickets.release(interaction); + } +}; \ No newline at end of file diff --git a/src/client.js b/src/client.js new file mode 100644 index 0000000..0b2567d --- /dev/null +++ b/src/client.js @@ -0,0 +1,70 @@ +const { FrameworkClient } = require('@eartharoid/dbf'); +const { + GatewayIntentBits, + Partials, +} = require('discord.js'); +const { PrismaClient } = require('@prisma/client'); +const Keyv = require('keyv'); +const I18n = require('@eartharoid/i18n'); +const fs = require('fs'); +const { join } = require('path'); +const YAML = require('yaml'); +const TicketManager = require('./lib/tickets/manager'); +const sqliteMiddleware = require('./lib/middleware/prisma-sqlite'); + +module.exports = class Client extends FrameworkClient { + constructor(config, log) { + super({ + intents: [ + GatewayIntentBits.DirectMessages, + GatewayIntentBits.DirectMessageReactions, + GatewayIntentBits.DirectMessageTyping, + GatewayIntentBits.MessageContent, + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildPresences, + ], + partials: [ + Partials.Channel, + Partials.Message, + Partials.Reaction, + ], + }); + + const locales = {}; + fs.readdirSync(join(__dirname, 'i18n')) + .filter(file => file.endsWith('.yml')) + .forEach(file => { + const data = fs.readFileSync(join(__dirname, 'i18n/' + file), { encoding: 'utf8' }); + const name = file.slice(0, file.length - 4); + locales[name] = YAML.parse(data); + }); + + /** @type {I18n} */ + this.i18n = new I18n('en-GB', locales); + /** @type {TicketManager} */ + this.tickets = new TicketManager(this); + this.config = config; + this.log = log; + this.supers = (process.env.SUPER ?? '').split(','); + } + + async login(token) { + /** @type {PrismaClient} */ + this.prisma = new PrismaClient(); + if (process.env.DB_PROVIDER === 'sqlite') { + this.prisma.$use(sqliteMiddleware); + // make sqlite faster (https://www.sqlite.org/wal.html), + // and the missing parentheses are not a mistake, `$queryRaw` is a tagged template literal + this.log.debug(await this.prisma.$queryRaw`PRAGMA journal_mode=WAL;`); + } + this.keyv = new Keyv(); + return super.login(token); + } + + async destroy() { + await this.prisma.$disconnect(); + return super.destroy(); + } +}; diff --git a/src/commands/add.js b/src/commands/add.js deleted file mode 100644 index b6afbd0..0000000 --- a/src/commands/add.js +++ /dev/null @@ -1,118 +0,0 @@ -const Command = require('../modules/commands/command'); -const { - Interaction, // eslint-disable-line no-unused-vars - MessageEmbed -} = require('discord.js'); - -module.exports = class AddCommand extends Command { - constructor(client) { - const i18n = client.i18n.getLocale(client.config.locale); - super(client, { - description: i18n('commands.add.description'), - internal: true, - name: i18n('commands.add.name'), - options: [ - { - description: i18n('commands.add.options.member.description'), - name: i18n('commands.add.options.member.name'), - required: true, - type: Command.option_types.USER - }, - { - description: i18n('commands.add.options.ticket.description'), - name: i18n('commands.add.options.ticket.name'), - required: false, - type: Command.option_types.CHANNEL - } - ] - }); - } - - /** - * @param {Interaction} interaction - * @returns {Promise} - */ - async execute(interaction) { - const settings = await this.client.utils.getSettings(interaction.guild.id); - const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale - const i18n = this.client.i18n.getLocale(settings.locale); - - const channel = interaction.options.getChannel(default_i18n('commands.add.options.ticket.name')) ?? interaction.channel; - const t_row = await this.client.tickets.resolve(channel.id, interaction.guild.id); - - if (!t_row) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.add.response.not_a_ticket.title')) - .setDescription(i18n('commands.add.response.not_a_ticket.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - const member = interaction.options.getMember(default_i18n('commands.add.options.member.name')); - - if (!member) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.add.response.no_member.title')) - .setDescription(i18n('commands.add.response.no_member.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - if (t_row.creator !== interaction.member.id && !await this.client.utils.isStaff(interaction.member)) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.add.response.no_permission.title')) - .setDescription(i18n('commands.add.response.no_permission.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setAuthor(member.user.username, member.user.displayAvatarURL()) - .setTitle(i18n('commands.add.response.added.title')) - .setDescription(i18n('commands.add.response.added.description', member.toString(), channel.toString())) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - - await channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setAuthor(member.user.username, member.user.displayAvatarURL()) - .setTitle(i18n('ticket.member_added.title')) - .setDescription(i18n('ticket.member_added.description', member.toString(), interaction.user.toString())) - .setFooter(settings.footer, interaction.guild.iconURL()) - ] - }); - - await channel.permissionOverwrites.edit(member, { - ATTACH_FILES: true, - READ_MESSAGE_HISTORY: true, - SEND_MESSAGES: true, - VIEW_CHANNEL: true - }, `${interaction.user.tag} added ${member.user.tag} to the ticket`); - - await this.client.tickets.archives.updateMember(channel.id, member); - - this.client.log.info(`${interaction.user.tag} added ${member.user.tag} to ${channel.id}`); - } -}; diff --git a/src/commands/blacklist.js b/src/commands/blacklist.js deleted file mode 100644 index 29c7997..0000000 --- a/src/commands/blacklist.js +++ /dev/null @@ -1,156 +0,0 @@ -const Command = require('../modules/commands/command'); -const { - Interaction, // eslint-disable-line no-unused-vars - MessageEmbed, - Role -} = require('discord.js'); - -module.exports = class BlacklistCommand extends Command { - constructor(client) { - const i18n = client.i18n.getLocale(client.config.locale); - super(client, { - description: i18n('commands.blacklist.description'), - internal: true, - name: i18n('commands.blacklist.name'), - options: [ - { - description: i18n('commands.blacklist.options.add.description'), - name: i18n('commands.blacklist.options.add.name'), - options: [ - { - description: i18n('commands.blacklist.options.add.options.member_or_role.description'), - name: i18n('commands.blacklist.options.add.options.member_or_role.name'), - required: true, - type: Command.option_types.MENTIONABLE - } - ], - type: Command.option_types.SUB_COMMAND - }, - { - description: i18n('commands.blacklist.options.remove.description'), - name: i18n('commands.blacklist.options.remove.name'), - options: [ - { - description: i18n('commands.blacklist.options.remove.options.member_or_role.description'), - name: i18n('commands.blacklist.options.remove.options.member_or_role.name'), - required: true, - type: Command.option_types.MENTIONABLE - } - ], - type: Command.option_types.SUB_COMMAND - }, - { - description: i18n('commands.blacklist.options.show.description'), - name: i18n('commands.blacklist.options.show.name'), - type: Command.option_types.SUB_COMMAND - } - ], - staff_only: true - }); - } - - /** - * @param {Interaction} interaction - * @returns {Promise} - */ - async execute(interaction) { - const settings = await this.client.utils.getSettings(interaction.guild.id); - const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale - const i18n = this.client.i18n.getLocale(settings.locale); - const blacklist = JSON.parse(JSON.stringify(settings.blacklist)); // not the same as `const blacklist = { ...settings.blacklist };` ..? - - switch (interaction.options.getSubcommand()) { - case default_i18n('commands.blacklist.options.add.name'): { - const member_or_role = interaction.options.getMentionable(default_i18n('commands.blacklist.options.add.options.member_or_role.name')); - const type = member_or_role instanceof Role ? 'role' : 'member'; - - if (type === 'member' && await this.client.utils.isStaff(member_or_role)) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.blacklist.response.illegal_action.title')) - .setDescription(i18n('commands.blacklist.response.illegal_action.description', member_or_role.toString())) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - blacklist[type + 's'].push(member_or_role.id); - await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setTitle(i18n(`commands.blacklist.response.${type}_added.title`)) - .setDescription(i18n(`commands.blacklist.response.${type}_added.description`, member_or_role.id)) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - await settings.update({ blacklist }); - break; - } - case default_i18n('commands.blacklist.options.remove.name'): { - const member_or_role = interaction.options.getMentionable(default_i18n('commands.blacklist.options.remove.options.member_or_role.name')); - const type = member_or_role instanceof Role ? 'role' : 'member'; - const index = blacklist[type + 's'].findIndex(element => element === member_or_role.id); - - if (index === -1) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.blacklist.response.invalid.title')) - .setDescription(i18n('commands.blacklist.response.invalid.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - blacklist[type + 's'].splice(index, 1); - await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setTitle(i18n(`commands.blacklist.response.${type}_removed.title`)) - .setDescription(i18n(`commands.blacklist.response.${type}_removed.description`, member_or_role.id)) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - await settings.update({ blacklist }); - break; - } - case default_i18n('commands.blacklist.options.show.name'): { - if (blacklist.members.length === 0 && blacklist.roles.length === 0) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.blacklist.response.empty_list.title')) - .setDescription(i18n('commands.blacklist.response.empty_list.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } else { - const members = blacklist.members.map(id => `**·** <@${id}>`); - const roles = blacklist.roles.map(id => `**·** <@&${id}>`); - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.blacklist.response.list.title')) - .addField(i18n('commands.blacklist.response.list.fields.members'), members.join('\n') || 'none') - .addField(i18n('commands.blacklist.response.list.fields.roles'), roles.join('\n') || 'none') - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - } - } - } -}; diff --git a/src/commands/close.js b/src/commands/close.js deleted file mode 100644 index a026cce..0000000 --- a/src/commands/close.js +++ /dev/null @@ -1,322 +0,0 @@ -const Command = require('../modules/commands/command'); -const { - Interaction, // eslint-disable-line no-unused-vars - MessageActionRow, - MessageButton, - MessageEmbed -} = require('discord.js'); -const { Op } = require('sequelize'); -const ms = require('ms'); - -module.exports = class CloseCommand extends Command { - constructor(client) { - const i18n = client.i18n.getLocale(client.config.locale); - super(client, { - description: i18n('commands.close.description'), - internal: true, - name: i18n('commands.close.name'), - options: [ - { - description: i18n('commands.close.options.reason.description'), - name: i18n('commands.close.options.reason.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.close.options.ticket.description'), - name: i18n('commands.close.options.ticket.name'), - required: false, - type: Command.option_types.INTEGER - }, - { - description: i18n('commands.close.options.time.description'), - name: i18n('commands.close.options.time.name'), - required: false, - type: Command.option_types.STRING - } - ] - }); - } - - /** - * @param {Interaction} interaction - * @returns {Promise} - */ - async execute(interaction) { - const settings = await this.client.utils.getSettings(interaction.guild.id); - const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale - const i18n = this.client.i18n.getLocale(settings.locale); - - const reason = interaction.options.getString(default_i18n('commands.close.options.reason.name')); - const ticket = interaction.options.getInteger(default_i18n('commands.close.options.ticket.name')); - const time = interaction.options.getString(default_i18n('commands.close.options.time.name')); - - if (time) { - if (!await this.client.utils.isStaff(interaction.member)) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.close.response.no_permission.title')) - .setDescription(i18n('commands.close.response.no_permission.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - let period; - try { - period = ms(time); - } catch { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.close.response.invalid_time.title')) - .setDescription(i18n('commands.close.response.invalid_time.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - const tickets = await this.client.db.models.Ticket.findAndCountAll({ - where: { - guild: interaction.guild.id, - last_message: { [Op.lte]: new Date(Date.now() - period) }, - open: true - } - }); - - if (tickets.count === 0) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.close.response.no_tickets.title')) - .setDescription(i18n('commands.close.response.no_tickets.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } else { - await interaction.reply({ - components: [ - new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId(`confirm_close_multiple:${interaction.id}`) - .setLabel(i18n('commands.close.response.confirm_multiple.buttons.confirm', tickets.count, tickets.count)) - .setEmoji('✅') - .setStyle('SUCCESS') - ) - .addComponents( - new MessageButton() - .setCustomId(`cancel_close_multiple:${interaction.id}`) - .setLabel(i18n('commands.close.response.confirm_multiple.buttons.cancel')) - .setEmoji('❌') - .setStyle('SECONDARY') - ) - ], - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.close.response.confirm_multiple.title')) - .setDescription(i18n('commands.close.response.confirm_multiple.description', tickets.count, tickets.count)) - .setFooter(this.client.utils.footer(settings.footer, i18n('collector_expires_in', 30)), interaction.guild.iconURL()) - ], - ephemeral: true - }); - - - const filter = i => i.user.id === interaction.user.id && i.customId.includes(interaction.id); - const collector = interaction.channel.createMessageComponentCollector({ - filter, - time: 30000 - }); - - collector.on('collect', async i => { - await i.deferUpdate(); - - if (i.customId === `confirm_close_multiple:${interaction.id}`) { - for (const ticket of tickets.rows) { - await this.client.tickets.close(ticket.id, interaction.user.id, interaction.guild.id, reason); - } - - await i.editReply({ - components: [], - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setTitle(i18n('commands.close.response.closed_multiple.title', tickets.count, tickets.count)) - .setDescription(i18n('commands.close.response.closed_multiple.description', tickets.count, tickets.count)) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } else { - await i.editReply({ - components: [], - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.close.response.canceled.title')) - .setDescription(i18n('commands.close.response.canceled.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - collector.stop(); - }); - - collector.on('end', async collected => { - if (collected.size === 0) { - await interaction.editReply({ - components: [], - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.close.response.confirmation_timeout.title')) - .setDescription(i18n('commands.close.response.confirmation_timeout.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - }); - } - } else { - let t_row; - if (ticket) { - t_row = await this.client.tickets.resolve(ticket, interaction.guild.id); - if (!t_row) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.close.response.unresolvable.title')) - .setDescription(i18n('commands.close.response.unresolvable.description', ticket)) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - } else { - t_row = await this.client.db.models.Ticket.findOne({ where: { id: interaction.channel.id } }); - if (!t_row) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.close.response.not_a_ticket.title')) - .setDescription(i18n('commands.close.response.not_a_ticket.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - } - - if (t_row.creator !== interaction.member.id && !await this.client.utils.isStaff(interaction.member)) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.close.response.no_permission.title')) - .setDescription(i18n('commands.close.response.no_permission.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - await interaction.reply({ - components: [ - new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId(`confirm_close:${interaction.id}`) - .setLabel(i18n('commands.close.response.confirm.buttons.confirm')) - .setEmoji('✅') - .setStyle('SUCCESS') - ) - .addComponents( - new MessageButton() - .setCustomId(`cancel_close:${interaction.id}`) - .setLabel(i18n('commands.close.response.confirm.buttons.cancel')) - .setEmoji('❌') - .setStyle('SECONDARY') - ) - ], - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.close.response.confirm.title')) - .setDescription(settings.log_messages ? i18n('commands.close.response.confirm.description_with_archive') : i18n('commands.close.response.confirm.description')) - .setFooter(this.client.utils.footer(settings.footer, i18n('collector_expires_in', 30)), interaction.guild.iconURL()) - ], - ephemeral: true - }); - - - const filter = i => i.user.id === interaction.user.id && i.customId.includes(interaction.id); - const collector = interaction.channel.createMessageComponentCollector({ - filter, - time: 30000 - }); - - collector.on('collect', async i => { - await i.deferUpdate(); - - if (i.customId === `confirm_close:${interaction.id}`) { - await this.client.tickets.close(t_row.id, interaction.user.id, interaction.guild.id, reason); - await i.editReply({ - components: [], - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setTitle(i18n('commands.close.response.closed.title', t_row.number)) - .setDescription(i18n('commands.close.response.closed.description', t_row.number)) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } else { - await i.editReply({ - components: [], - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.close.response.canceled.title')) - .setDescription(i18n('commands.close.response.canceled.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - collector.stop(); - }); - - collector.on('end', async collected => { - if (collected.size === 0) { - await interaction.editReply({ - components: [], - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.close.response.confirmation_timeout.title')) - .setDescription(i18n('commands.close.response.confirmation_timeout.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - }); - } - } -}; diff --git a/src/commands/extra/survey.template.html b/src/commands/extra/survey.template.html deleted file mode 100644 index a20ed5a..0000000 --- a/src/commands/extra/survey.template.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - {{survey}} Survey Responses | Discord Tickets - - - - - - - - - -
- -
-

{{survey}} survey responses

-
- -
-
-
-

{{count.responses}}

-

Responses

-
-
-
-
-

{{count.users}}

-

Users

-
-
-
- -
- - - - {{#columns}} - - {{/columns}} - - - - {{#responses}} - - {{#.}} - - {{/.}} - - {{/responses}} - - - - {{#columns}} - - {{/columns}} - - -
{{.}}
{{.}}
{{.}}
-
-
-
- - - \ No newline at end of file diff --git a/src/commands/help.js b/src/commands/help.js deleted file mode 100644 index eed7c8a..0000000 --- a/src/commands/help.js +++ /dev/null @@ -1,49 +0,0 @@ -const Command = require('../modules/commands/command'); -const { - Interaction, // eslint-disable-line no-unused-vars - MessageEmbed -} = require('discord.js'); - -module.exports = class HelpCommand extends Command { - constructor(client) { - const i18n = client.i18n.getLocale(client.config.locale); - super(client, { - description: i18n('commands.help.description'), - internal: true, - name: i18n('commands.help.name') - }); - } - - /** - * @param {Interaction} interaction - * @returns {Promise} - */ - async execute(interaction) { - const settings = await this.client.utils.getSettings(interaction.guild.id); - const i18n = this.client.i18n.getLocale(settings.locale); - - const is_staff = await this.client.utils.isStaff(interaction.member); - const commands = this.manager.commands.filter(command => { - if (command.permissions.length >= 1) return interaction.member.permissions.has(command.permissions); - else if (command.staff_only) return is_staff; - else return true; - }); - const list = commands.map(command => { - const description = command.description.length > 50 - ? command.description.substring(0, 50) + '...' - : command.description; - return `**\`/${command.name}\` ·** ${description}`; - }); - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.help.response.list.title')) - .setDescription(i18n('commands.help.response.list.description')) - .addField(i18n('commands.help.response.list.fields.commands'), list.join('\n')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } -}; diff --git a/src/commands/message/create.js b/src/commands/message/create.js new file mode 100644 index 0000000..51dc5cf --- /dev/null +++ b/src/commands/message/create.js @@ -0,0 +1,23 @@ +const { MessageCommand } = require('@eartharoid/dbf'); +const { useGuild } = require('../../lib/tickets/utils'); + +module.exports = class CreateMessageCommand extends MessageCommand { + constructor(client, options) { + const nameLocalizations = {}; + client.i18n.locales.forEach(l => (nameLocalizations[l] = client.i18n.getMessage(l, 'commands.message.create.name'))); + + super(client, { + ...options, + dmPermission: false, + name: nameLocalizations['en-GB'], + nameLocalizations, + }); + } + + /** + * @param {import("discord.js").MessageContextMenuCommandInteraction} interaction + */ + async run(interaction) { + await useGuild(this.client, interaction, { referencesMessage: interaction.targetMessage.channelId + '/' + interaction.targetId }); + } +}; \ No newline at end of file diff --git a/src/commands/message/pin.js b/src/commands/message/pin.js new file mode 100644 index 0000000..be09d7e --- /dev/null +++ b/src/commands/message/pin.js @@ -0,0 +1,75 @@ +const { MessageCommand } = require('@eartharoid/dbf'); +const ExtendedEmbedBuilder = require('../../lib/embed'); + +module.exports = class PinMessageCommand extends MessageCommand { + constructor(client, options) { + const nameLocalizations = {}; + client.i18n.locales.forEach(l => (nameLocalizations[l] = client.i18n.getMessage(l, 'commands.message.pin.name'))); + + super(client, { + ...options, + dmPermission: false, + name: nameLocalizations['en-GB'], + nameLocalizations, + }); + } + + /** + * @param {import("discord.js").MessageContextMenuCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply({ ephemeral: true }); + const ticket = await client.prisma.ticket.findUnique({ + include: { guild: true }, + where: { id: interaction.channel.id }, + }); + + if (!ticket) { + const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } }); + const getMessage = client.i18n.getLocale(settings.locale); + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('commands.message.pin.not_ticket.title')) + .setDescription(getMessage('commands.message.pin.not_ticket.description')), + ], + }); + } + + const getMessage = client.i18n.getLocale(ticket.guild.locale); + + if (!interaction.targetMessage.pinnable) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.errorColour) + .setTitle(getMessage('commands.message.pin.not_pinnable.title')) + .setDescription(getMessage('commands.message.pin.not_pinnable.description')), + ], + }); + } + + await interaction.targetMessage.pin(); + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.successColour) + .setTitle(getMessage('commands.message.pin.pinned.title')) + .setDescription(getMessage('commands.message.pin.pinned.description')), + ], + }); + } +}; \ No newline at end of file diff --git a/src/commands/new.js b/src/commands/new.js deleted file mode 100644 index 8bec13e..0000000 --- a/src/commands/new.js +++ /dev/null @@ -1,190 +0,0 @@ -const Command = require('../modules/commands/command'); -const { - Interaction, // eslint-disable-line no-unused-vars, - MessageActionRow, - MessageEmbed, - MessageSelectMenu -} = require('discord.js'); - -module.exports = class NewCommand extends Command { - constructor(client) { - const i18n = client.i18n.getLocale(client.config.locale); - super(client, { - description: i18n('commands.new.description'), - internal: true, - name: i18n('commands.new.name'), - options: [ - { - description: i18n('commands.new.options.topic.description'), - name: i18n('commands.new.options.topic.name'), - required: false, - type: Command.option_types.STRING - } - ] - }); - } - - /** - * @param {Interaction} interaction - * @returns {Promise} - */ - async execute(interaction) { - const settings = await this.client.utils.getSettings(interaction.guild.id); - const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale - const i18n = this.client.i18n.getLocale(settings.locale); - - const topic = interaction.options.getString(default_i18n('commands.new.options.topic.name')); - - const create = async (cat_row, i) => { - const tickets = await this.client.db.models.Ticket.findAndCountAll({ - where: { - category: cat_row.id, - creator: interaction.user.id, - open: true - } - }); - - if (tickets.count >= cat_row.max_per_member) { - if (cat_row.max_per_member === 1) { - const response = { - components: [], - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.new.response.has_a_ticket.title')) - .setDescription(i18n('commands.new.response.has_a_ticket.description', tickets.rows[0].id)) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }; - await i ? i.editReply(response) : interaction.reply(response); - } else { - const list = tickets.rows.map(row => { - if (row.topic) { - const description = row.topic.substring(0, 30); - const ellipses = row.topic.length > 30 ? '...' : ''; - return `<#${row.id}>: \`${description}${ellipses}\``; - } else { - return `<#${row.id}>`; - } - }); - const response = { - components: [], - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.new.response.max_tickets.title', tickets.count)) - .setDescription(i18n('commands.new.response.max_tickets.description', list.join('\n'))) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }; - await i ? i.editReply(response) : interaction.reply(response); - } - } else { - try { - const t_row = await this.client.tickets.create(interaction.guild.id, interaction.user.id, cat_row.id, topic); - const response = { - components: [], - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.new.response.created.title')) - .setDescription(i18n('commands.new.response.created.description', `<#${t_row.id}>`)) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }; - await i ? i.editReply(response) : interaction.reply(response); - } catch (error) { - const response = { - components: [], - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.new.response.error.title')) - .setDescription(error.message) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }; - await i ? i.editReply(response) : interaction.reply(response); - } - } - }; - - const categories = await this.client.db.models.Category.findAndCountAll({ where: { guild: interaction.guild.id } }); - - if (categories.count === 0) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.new.response.no_categories.title')) - .setDescription(i18n('commands.new.response.no_categories.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ] - }); - } else if (categories.count === 1) { - create(categories.rows[0]); // skip the category selection - } else { - await interaction.reply({ - components: [ - new MessageActionRow() - .addComponents( - new MessageSelectMenu() - .setCustomId(`select_category:${interaction.id}`) - .setPlaceholder('Select a category') - .addOptions(categories.rows.map(row => ({ - label: row.name, - value: row.id - }))) - ) - ], - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.new.response.select_category.title')) - .setDescription(i18n('commands.new.response.select_category.description')) - .setFooter(this.client.utils.footer(settings.footer, i18n('collector_expires_in', 30)), interaction.guild.iconURL()) - ], - ephemeral: true - }); - - const filter = i => i.user.id === interaction.user.id && i.customId.includes(interaction.id); - const collector = interaction.channel.createMessageComponentCollector({ - filter, - time: 30000 - }); - - collector.on('collect', async i => { - await i.deferUpdate(); - create(categories.rows.find(row => row.id === i.values[0]), i); - collector.stop(); - }); - - collector.on('end', async collected => { - if (collected.size === 0) { - await interaction.editReply({ - components: [], - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.new.response.select_category_timeout.title')) - .setDescription(i18n('commands.new.response.select_category_timeout.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - }); - } - } -}; diff --git a/src/commands/panel.js b/src/commands/panel.js deleted file mode 100644 index bb87619..0000000 --- a/src/commands/panel.js +++ /dev/null @@ -1,210 +0,0 @@ -const Command = require('../modules/commands/command'); -const { - Interaction, // eslint-disable-line no-unused-vars - MessageActionRow, - MessageButton, - MessageEmbed, - MessageSelectMenu -} = require('discord.js'); -const { some } = require('../utils'); - -module.exports = class PanelCommand extends Command { - constructor(client) { - const i18n = client.i18n.getLocale(client.config.locale); - super(client, { - description: i18n('commands.panel.description'), - internal: true, - name: i18n('commands.panel.name'), - options: [ - { - description: i18n('commands.panel.options.categories.description'), - multiple: true, - name: i18n('commands.panel.options.categories.name'), - required: true, - type: Command.option_types.STRING - }, - { - description: i18n('commands.panel.options.description.description'), - name: i18n('commands.panel.options.description.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.panel.options.image.description'), - name: i18n('commands.panel.options.image.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.panel.options.just_type.description') + ' (false)', - name: i18n('commands.panel.options.just_type.name'), - required: false, - type: Command.option_types.BOOLEAN - }, - { - description: i18n('commands.panel.options.title.description'), - name: i18n('commands.panel.options.title.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.panel.options.thumbnail.description'), - name: i18n('commands.panel.options.thumbnail.name'), - required: false, - type: Command.option_types.STRING - } - ], - staff_only: true - }); - } - - /** - * @param {Interaction} interaction - * @returns {Promise} - */ - async execute(interaction) { - const settings = await this.client.utils.getSettings(interaction.guild.id); - const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale - const i18n = this.client.i18n.getLocale(settings.locale); - - const categories = interaction.options.getString(default_i18n('commands.panel.options.categories.name')).match(/\d{17,19}/g) ?? []; - const description = interaction.options.getString(default_i18n('commands.panel.options.description.name'))?.replace(/\\n/g, '\n'); - const image = interaction.options.getString(default_i18n('commands.panel.options.image.name')); - const just_type = interaction.options.getBoolean(default_i18n('commands.panel.options.just_type.name')); - const title = interaction.options.getString(default_i18n('commands.panel.options.title.name')); - const thumbnail = interaction.options.getString(default_i18n('commands.panel.options.thumbnail.name')); - - if (just_type && categories.length > 1) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.panel.response.too_many_categories.title')) - .setDescription(i18n('commands.panel.response.too_many_categories.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - const invalid_category = await some(categories, async id => { - const cat_row = await this.client.db.models.Category.findOne({ - where: { - guild: interaction.guild.id, - id - } - }); - return !cat_row; - }); - - if (invalid_category) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.panel.response.invalid_category.title')) - .setDescription(i18n('commands.panel.response.invalid_category.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - let panel_channel; - - const embed = new MessageEmbed() - .setColor(settings.colour) - .setFooter(settings.footer, interaction.guild.iconURL()); - - if (description) embed.setDescription(description); - if (image) embed.setImage(image); - if (title) embed.setTitle(title); - if (thumbnail) embed.setThumbnail(thumbnail); - - if (just_type) { - panel_channel = await interaction.guild.channels.create('create-a-ticket', { - permissionOverwrites: [ - { - allow: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'READ_MESSAGE_HISTORY'], - deny: ['ATTACH_FILES', 'EMBED_LINKS', 'ADD_REACTIONS'], - id: interaction.guild.roles.everyone - }, - { - allow: ['SEND_MESSAGES', 'EMBED_LINKS', 'ADD_REACTIONS'], - id: this.client.user.id - } - ], - position: 1, - rateLimitPerUser: 30, - reason: `${interaction.user.tag} created a new message panel`, - type: 'GUILD_TEXT' - }); - await panel_channel.send({ embeds: [embed] }); - this.client.log.info(`${interaction.user.tag} has created a new message panel`); - } else { - panel_channel = await interaction.guild.channels.create('create-a-ticket', { - permissionOverwrites: [ - { - allow: ['VIEW_CHANNEL', 'READ_MESSAGE_HISTORY'], - deny: ['SEND_MESSAGES', 'ADD_REACTIONS'], - id: interaction.guild.roles.everyone - }, - { - allow: ['SEND_MESSAGES', 'EMBED_LINKS', 'ADD_REACTIONS'], - id: this.client.user.id - } - ], - position: 1, - reason: `${interaction.user.tag} created a new panel`, - type: 'GUILD_TEXT' - }); - - if (categories.length === 1) { - // single category - await panel_channel.send({ - components: [ - new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId(`panel.single:${categories[0]}`) - .setLabel(i18n('panel.create_ticket')) - .setStyle('PRIMARY') - ) - ], - embeds: [embed] - }); - this.client.log.info(`${interaction.user.tag} has created a new button panel`); - } else { - // multi category - const rows = (await this.client.db.models.Category.findAll({ where: { guild: interaction.guild.id } })).filter(row => categories.includes(row.id)); - await panel_channel.send({ - components: [ - new MessageActionRow() - .addComponents( - new MessageSelectMenu() - .setCustomId(`panel.multiple:${panel_channel.id}`) - .setPlaceholder('Select a category') - .addOptions(rows.map(row => ({ - label: row.name, - value: row.id - }))) - ) - ], - embeds: [embed] - }); - this.client.log.info(`${interaction.user.tag} has created a new select panel`); - } - } - - interaction.reply({ - content: `✅ ${panel_channel}`, - ephemeral: true - }); - - await this.client.db.models.Panel.create({ - category: categories.length === 1 ? categories[0] : null, - channel: panel_channel.id, - guild: interaction.guild.id - }); - } -}; diff --git a/src/commands/remove.js b/src/commands/remove.js deleted file mode 100644 index 85de1da..0000000 --- a/src/commands/remove.js +++ /dev/null @@ -1,111 +0,0 @@ -const Command = require('../modules/commands/command'); -const { - Interaction, // eslint-disable-line no-unused-vars - MessageEmbed -} = require('discord.js'); - -module.exports = class RemoveCommand extends Command { - constructor(client) { - const i18n = client.i18n.getLocale(client.config.locale); - super(client, { - description: i18n('commands.remove.description'), - internal: true, - name: i18n('commands.remove.name'), - options: [ - { - description: i18n('commands.remove.options.member.description'), - name: i18n('commands.remove.options.member.name'), - required: true, - type: Command.option_types.USER - }, - { - description: i18n('commands.remove.options.ticket.description'), - name: i18n('commands.remove.options.ticket.name'), - required: false, - type: Command.option_types.CHANNEL - } - ] - }); - } - - /** - * @param {Interaction} interaction - * @returns {Promise} - */ - async execute(interaction) { - const settings = await this.client.utils.getSettings(interaction.guild.id); - const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale - const i18n = this.client.i18n.getLocale(settings.locale); - - const channel = interaction.options.getChannel(default_i18n('commands.remove.options.channel.name')) ?? interaction.channel; - const t_row = await this.client.tickets.resolve(channel.id, interaction.guild.id); - - if (!t_row) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.remove.response.not_a_channel.title')) - .setDescription(i18n('commands.remove.response.not_a_channel.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - const member = interaction.options.getMember(default_i18n('commands.remove.options.member.name')); - - if (!member) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.remove.response.no_member.title')) - .setDescription(i18n('commands.remove.response.no_member.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - if (t_row.creator !== interaction.user.id && !await this.client.utils.isStaff(interaction.member)) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.remove.response.no_permission.title')) - .setDescription(i18n('commands.remove.response.no_permission.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setAuthor(member.user.username, member.user.displayAvatarURL()) - .setTitle(i18n('commands.remove.response.removed.title')) - .setDescription(i18n('commands.remove.response.removed.description', member.toString(), channel.toString())) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - - await channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setAuthor(member.user.username, member.user.displayAvatarURL()) - .setTitle(i18n('ticket.member_removed.title')) - .setDescription(i18n('ticket.member_removed.description', member.toString(), interaction.user.toString())) - .setFooter(settings.footer, interaction.guild.iconURL()) - ] - }); - - await channel.permissionOverwrites.delete(member.user.id, `${interaction.user.tag} removed ${member.user.tag} from the ticket`); - - this.client.log.info(`${interaction.user.tag} removed ${member.user.tag} from ${channel.id}`); - } -}; diff --git a/src/commands/settings.js b/src/commands/settings.js deleted file mode 100644 index bdb6f81..0000000 --- a/src/commands/settings.js +++ /dev/null @@ -1,358 +0,0 @@ -/* eslint-disable max-lines */ -const Command = require('../modules/commands/command'); -const { - Interaction, // eslint-disable-line no-unused-vars - MessageEmbed -} = require('discord.js'); - -module.exports = class SettingsCommand extends Command { - constructor(client) { - const i18n = client.i18n.getLocale(client.config.locale); - super(client, { - description: i18n('commands.settings.description'), - internal: true, - name: i18n('commands.settings.name'), - options: [ - { - description: i18n('commands.settings.options.categories.description'), - name: i18n('commands.settings.options.categories.name'), - options: [ - { - description: i18n('commands.settings.options.categories.options.create.description'), - name: i18n('commands.settings.options.categories.options.create.name'), - options: [ - { - description: i18n('commands.settings.options.categories.options.create.options.name.description'), - name: i18n('commands.settings.options.categories.options.create.options.name.name'), - required: true, - type: Command.option_types.STRING - }, - { - description: i18n('commands.settings.options.categories.options.create.options.roles.description'), - name: i18n('commands.settings.options.categories.options.create.options.roles.name'), - required: true, - type: Command.option_types.STRING - } - ], - type: Command.option_types.SUB_COMMAND - }, - { - description: i18n('commands.settings.options.categories.options.delete.description'), - name: i18n('commands.settings.options.categories.options.delete.name'), - options: [ - { - description: i18n('commands.settings.options.categories.options.delete.options.id.description'), - name: i18n('commands.settings.options.categories.options.delete.options.id.name'), - required: true, - type: Command.option_types.STRING - } - ], - type: Command.option_types.SUB_COMMAND - }, - { - description: i18n('commands.settings.options.categories.options.edit.description'), - name: i18n('commands.settings.options.categories.options.edit.name'), - options: [ - { - description: i18n('commands.settings.options.categories.options.edit.options.id.description'), - name: i18n('commands.settings.options.categories.options.edit.options.id.name'), - required: true, - type: Command.option_types.STRING - }, - { - description: i18n('commands.settings.options.categories.options.edit.options.claiming.description'), - name: i18n('commands.settings.options.categories.options.edit.options.claiming.name'), - required: false, - type: Command.option_types.BOOLEAN - }, - { - description: i18n('commands.settings.options.categories.options.edit.options.image.description'), - name: i18n('commands.settings.options.categories.options.edit.options.image.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.settings.options.categories.options.edit.options.max_per_member.description'), - name: i18n('commands.settings.options.categories.options.edit.options.max_per_member.name'), - required: false, - type: Command.option_types.INTEGER - }, - { - description: i18n('commands.settings.options.categories.options.edit.options.name.description'), - name: i18n('commands.settings.options.categories.options.edit.options.name.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.settings.options.categories.options.edit.options.name_format.description'), - name: i18n('commands.settings.options.categories.options.edit.options.name_format.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.settings.options.categories.options.edit.options.opening_message.description'), - name: i18n('commands.settings.options.categories.options.edit.options.opening_message.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.settings.options.categories.options.edit.options.opening_questions.description'), - name: i18n('commands.settings.options.categories.options.edit.options.opening_questions.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.settings.options.categories.options.edit.options.ping.description'), - name: i18n('commands.settings.options.categories.options.edit.options.ping.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.settings.options.categories.options.edit.options.require_topic.description'), - name: i18n('commands.settings.options.categories.options.edit.options.require_topic.name'), - required: false, - type: Command.option_types.BOOLEAN - }, - { - description: i18n('commands.settings.options.categories.options.edit.options.roles.description'), - name: i18n('commands.settings.options.categories.options.edit.options.roles.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.settings.options.categories.options.edit.options.survey.description'), - name: i18n('commands.settings.options.categories.options.edit.options.survey.name'), - required: false, - type: Command.option_types.STRING - } - ], - type: Command.option_types.SUB_COMMAND - }, - { - description: i18n('commands.settings.options.categories.options.list.description'), - name: i18n('commands.settings.options.categories.options.list.name'), - type: Command.option_types.SUB_COMMAND - } - ], - type: Command.option_types.SUB_COMMAND_GROUP - }, - { - description: i18n('commands.settings.options.set.description'), - name: i18n('commands.settings.options.set.name'), - options: [ - { - description: i18n('commands.settings.options.set.options.close_button.description'), - name: i18n('commands.settings.options.set.options.close_button.name'), - required: false, - type: Command.option_types.BOOLEAN - }, - { - description: i18n('commands.settings.options.set.options.colour.description'), - name: i18n('commands.settings.options.set.options.colour.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.settings.options.set.options.error_colour.description'), - name: i18n('commands.settings.options.set.options.error_colour.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.settings.options.set.options.footer.description'), - name: i18n('commands.settings.options.set.options.footer.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.settings.options.set.options.locale.description'), - name: i18n('commands.settings.options.set.options.locale.name'), - required: false, - type: Command.option_types.STRING - }, - { - description: i18n('commands.settings.options.set.options.log_messages.description'), - name: i18n('commands.settings.options.set.options.log_messages.name'), - required: false, - type: Command.option_types.BOOLEAN - }, - { - description: i18n('commands.settings.options.set.options.success_colour.description'), - name: i18n('commands.settings.options.set.options.success_colour.name'), - required: false, - type: Command.option_types.STRING - } - ], - type: Command.option_types.SUB_COMMAND - } - ], - permissions: ['MANAGE_GUILD'] - }); - - } - - /** - * @param {Interaction} interaction - * @returns {Promise} - */ - async execute(interaction) { - const settings = await this.client.utils.getSettings(interaction.guild.id); - const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale - const i18n = this.client.i18n.getLocale(settings.locale); - - switch (interaction.options.getSubcommand()) { - case default_i18n('commands.settings.options.categories.options.create.name'): { - const name = interaction.options.getString(default_i18n('commands.settings.options.categories.options.create.options.name.name')); - const roles = interaction.options.getString(default_i18n('commands.settings.options.categories.options.create.options.roles.name'))?.match(/\d{17,19}/g) ?? []; - const allowed_permissions = ['VIEW_CHANNEL', 'READ_MESSAGE_HISTORY', 'SEND_MESSAGES', 'EMBED_LINKS', 'ATTACH_FILES']; - const cat_channel = await interaction.guild.channels.create(name, { - permissionOverwrites: [ - ...[ - { - deny: ['VIEW_CHANNEL'], - id: interaction.guild.roles.everyone - }, - { - allow: allowed_permissions, - id: this.client.user.id - } - ], - ...roles.map(r => ({ - allow: allowed_permissions, - id: r - })) - ], - position: 1, - reason: `Tickets category created by ${interaction.user.tag}`, - type: 'GUILD_CATEGORY' - }); - await this.client.db.models.Category.create({ - guild: interaction.guild.id, - id: cat_channel.id, - name, - roles - }); - await this.client.commands.updatePermissions(interaction.guild); - interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setTitle(i18n('commands.settings.response.category_created', name)) - ], - ephemeral: true - }); - break; - } - case default_i18n('commands.settings.options.categories.options.delete.name'): { - const category = await this.client.db.models.Category.findOne({ where: { id: interaction.options.getString(default_i18n('commands.settings.options.categories.options.delete.options.id.name')) } }); - if (category) { - const channel = this.client.channels.cache.get(interaction.options.getString(default_i18n('commands.settings.options.categories.options.delete.options.id.name'))); - if (channel) channel.delete(); - await category.destroy(); - interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setTitle(i18n('commands.settings.response.category_deleted', category.name)) - ], - ephemeral: true - }); - } else { - interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.settings.response.category_does_not_exist')) - ], - ephemeral: true - }); - } - break; - } - case default_i18n('commands.settings.options.categories.options.edit.name'): { - const category = await this.client.db.models.Category.findOne({ where: { id: interaction.options.getString(default_i18n('commands.settings.options.categories.options.delete.options.id.name')) } }); - if (!category) { - return interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.settings.response.category_does_not_exist')) - ], - ephemeral: true - }); - } - const claiming = interaction.options.getBoolean(default_i18n('commands.settings.options.categories.options.edit.options.claiming.name')); - const image = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.image.name')); - const max_per_member = interaction.options.getInteger(default_i18n('commands.settings.options.categories.options.edit.options.max_per_member.name')); - const name = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.name.name')); - const name_format = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.name_format.name')); - const opening_message = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.opening_message.name')); - const opening_questions = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.opening_questions.name')); - const ping = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.ping.name')); - const require_topic = interaction.options.getBoolean(default_i18n('commands.settings.options.categories.options.edit.options.require_topic.name')); - const roles = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.roles.name')); - const survey = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.survey.name')); - if (claiming !== null) category.set('claiming', claiming); - if (max_per_member !== null) category.set('max_per_member', max_per_member); - if (image !== null) category.set('image', image); - if (name !== null) category.set('name', name); - if (name_format !== null) category.set('name_format', name_format); - if (opening_message !== null) category.set('opening_message', opening_message.replace(/\\n/g, '\n')); - if (opening_questions !== null) category.set('opening_questions', JSON.parse(opening_questions)); - if (ping !== null) category.set('ping', ping.match(/\d{17,19}/g) ?? []); - if (require_topic !== null) category.set('require_topic', require_topic); - if (roles !== null) category.set('roles', roles.match(/\d{17,19}/g) ?? []); - if (survey !== null) category.set('survey', survey); - await category.save(); - interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setTitle(i18n('commands.settings.response.category_updated', category.name)) - ], - ephemeral: true - }); - break; - } - case default_i18n('commands.settings.options.categories.options.list.name'): { - const categories = await this.client.db.models.Category.findAll({ where: { guild: interaction.guild.id } }); - await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.settings.response.category_list')) - .setDescription(categories.map(c => `- ${c.name} (\`${c.id}\`)`).join('\n')) - ], - ephemeral: true - }); - break; - } - case default_i18n('commands.settings.options.set.name'): { - const close_button = interaction.options.getBoolean(default_i18n('commands.settings.options.set.options.close_button.name')); - const colour = interaction.options.getString(default_i18n('commands.settings.options.set.options.colour.name')); - const error_colour = interaction.options.getString(default_i18n('commands.settings.options.set.options.error_colour.name')); - const footer = interaction.options.getString(default_i18n('commands.settings.options.set.options.footer.name')); - const locale = interaction.options.getString(default_i18n('commands.settings.options.set.options.locale.name')); - const log_messages = interaction.options.getBoolean(default_i18n('commands.settings.options.set.options.log_messages.name')); - const success_colour = interaction.options.getString(default_i18n('commands.settings.options.set.options.success_colour.name')); - if (close_button !== null) settings.set('close_button', close_button); - if (colour !== null) settings.set('colour', colour.toUpperCase()); - if (error_colour !== null) settings.set('error_colour', error_colour.toUpperCase()); - if (footer !== null) settings.set('footer', footer); - if (locale !== null) settings.set('locale', locale); - if (log_messages !== null) settings.set('log_messages', log_messages); - if (success_colour !== null) settings.set('success_colour', success_colour.toUpperCase()); - await settings.save(); - interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setTitle(i18n('commands.settings.response.settings_updated')) - ], - ephemeral: true - }); - break; - } - } - } -}; \ No newline at end of file diff --git a/src/commands/slash/add.js b/src/commands/slash/add.js new file mode 100644 index 0000000..b8a54bf --- /dev/null +++ b/src/commands/slash/add.js @@ -0,0 +1,144 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { ApplicationCommandOptionType } = require('discord.js'); +const ExtendedEmbedBuilder = require('../../lib/embed'); +const { isStaff } = require('../../lib/users'); +const { logTicketEvent } = require('../../lib/logging'); + +module.exports = class AddSlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'add'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + options: [ + { + name: 'member', + required: true, + type: ApplicationCommandOptionType.User, + }, + { + autocomplete: true, + name: 'ticket', + required: false, + type: ApplicationCommandOptionType.String, + }, + ].map(option => { + option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`); + option.description = option.descriptionLocalizations['en-GB']; + option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`); + return option; + }), + }); + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply({ ephemeral: true }); + + const ticket = await client.prisma.ticket.findUnique({ + include: { guild: true }, + where: { id: interaction.options.getString('ticket', false) || interaction.channel.id }, + }); + + if (!ticket) { + const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } }); + const getMessage = client.i18n.getLocale(settings.locale); + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('misc.invalid_ticket.title')) + .setDescription(getMessage('misc.invalid_ticket.description')), + ], + }); + } + + const getMessage = client.i18n.getLocale(ticket.guild.locale); + + if ( + ticket.id !== interaction.channel.id && + ticket.createdById !== interaction.member.id && + !(await isStaff(interaction.guild, interaction.member.id)) + ) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.errorColour) + .setTitle(getMessage('commands.slash.add.not_staff.title')) + .setDescription(getMessage('commands.slash.add.not_staff.description')), + ], + }); + } + + /** @type {import("discord.js").TextChannel} */ + const ticketChannel = await interaction.guild.channels.fetch(ticket.id); + const member = interaction.options.getMember('member', true); + + await ticketChannel.permissionOverwrites.edit( + member, + { + AttachFiles: true, + EmbedLinks: true, + ReadMessageHistory: true, + SendMessages: true, + ViewChannel: true, + }, + `${interaction.user.tag} added ${member.user.tag} to the ticket`, + ); + + await ticketChannel.send({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(ticket.guild.primaryColour) + .setDescription(getMessage('commands.slash.add.added', { + added: member.toString(), + by: interaction.member.toString(), + })), + ], + }); + + await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.successColour) + .setTitle(getMessage('commands.slash.add.success.title')) + .setDescription(getMessage('commands.slash.add.success.description', { + member: member.toString(), + ticket: ticketChannel.toString(), + })), + ], + }); + + logTicketEvent(this.client, { + action: 'update', + diff: { + original: {}, + updated: { [getMessage('log.ticket.added')]: member.user.tag }, + }, + target: { + id: ticket.id, + name: `<#${ticket.id}>`, + }, + userId: interaction.user.id, + }); + + } +}; \ No newline at end of file diff --git a/src/commands/slash/claim.js b/src/commands/slash/claim.js new file mode 100644 index 0000000..0e457fa --- /dev/null +++ b/src/commands/slash/claim.js @@ -0,0 +1,26 @@ +const { SlashCommand } = require('@eartharoid/dbf'); + +module.exports = class ClaimSlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'claim'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + }); + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply({ ephemeral: false }); + await client.tickets.claim(interaction); + } +}; \ No newline at end of file diff --git a/src/commands/slash/close.js b/src/commands/slash/close.js new file mode 100644 index 0000000..af0010b --- /dev/null +++ b/src/commands/slash/close.js @@ -0,0 +1,37 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { ApplicationCommandOptionType } = require('discord.js'); + +module.exports = class CloseSlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'close'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + options: [ + { + name: 'reason', + required: false, + type: ApplicationCommandOptionType.String, + }, + ].map(option => { + option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`); + option.description = option.descriptionLocalizations['en-GB']; + option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`); + return option; + }), + }); + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + await client.tickets.beforeRequestClose(interaction); + } +}; \ No newline at end of file diff --git a/src/commands/slash/force-close.js b/src/commands/slash/force-close.js new file mode 100644 index 0000000..261a34c --- /dev/null +++ b/src/commands/slash/force-close.js @@ -0,0 +1,286 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { + ApplicationCommandOptionType, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ComponentType, +} = require('discord.js'); +const ExtendedEmbedBuilder = require('../../lib/embed'); +const { isStaff } = require('../../lib/users'); +const ms = require('ms'); + +module.exports = class ForceCloseSlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'force-close'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + options: [ + { + autocomplete: true, + name: 'category', + required: false, + type: ApplicationCommandOptionType.Integer, + }, + { + name: 'reason', + required: false, + type: ApplicationCommandOptionType.String, + }, + { + autocomplete: true, + name: 'ticket', + required: false, + type: ApplicationCommandOptionType.String, + }, + { + name: 'time', + required: false, + type: ApplicationCommandOptionType.String, + }, + ].map(option => { + option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`); + option.description = option.descriptionLocalizations['en-GB']; + option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`); + return option; + }), + }); + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply({ ephemeral: true }); + + const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } }); + const getMessage = client.i18n.getLocale(settings.locale); + let ticket; + + if (!(await isStaff(interaction.guild, interaction.user.id))) { // if user is not staff + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('commands.slash.force-close.not_staff.title')) + .setDescription(getMessage('commands.slash.force-close.not_staff.description')), + ], + }); + } + + if (interaction.options.getString('ticket', false)) { // if ticket option is passed + ticket = await client.prisma.ticket.findUnique({ + include: { category: true }, + where: { id: interaction.options.getString('ticket') }, + }); + + if (!ticket) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('misc.invalid_ticket.title')) + .setDescription(getMessage('misc.invalid_ticket.description')), + ], + }); + } + + await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.successColour) + .setTitle(getMessage('commands.slash.force-close.closed_one.title')) + .setDescription(getMessage('commands.slash.force-close.closed_one.description', { ticket: ticket.id })), + ], + }); + + setTimeout(async () => { + await client.tickets.finallyClose(ticket.id, { + closedBy: interaction.user.id, + reason: interaction.options.getString('reason', false), + }); + }, ms('3s')); + + } else if (interaction.options.getString('time', false)) { // if time option is passed + const time = ms(interaction.options.getString('time', false)); + + if (!time) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('commands.slash.close.invalid_time.title')) + .setDescription(getMessage('commands.slash.close.invalid_time.description', { input: interaction.options.getString('time', false) })), + ], + }); + } + + const categoryId = interaction.options.getInteger('category', false); + const tickets = await client.prisma.ticket.findMany({ + where: { + categoryId: categoryId ?? undefined, // must be undefined not null + lastMessageAt: { lte: new Date(Date.now() - time) }, + open: true, + }, + }); + + if (tickets.length === 0) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('commands.slash.force-close.no_tickets.title')) + .setDescription(getMessage('commands.slash.force-close.no_tickets.description', { time: ms(time, { long: true }) })), + ], + }); + } + + const collectorTime = ms('15s'); + const confirmationM = await interaction.editReply({ + components: [ + new ActionRowBuilder() + .addComponents([ + new ButtonBuilder() + .setCustomId(JSON.stringify({ + action: 'custom', + id: 'close', + })) + .setStyle(ButtonStyle.Danger) + .setEmoji(getMessage('buttons.close.emoji')) + .setLabel(getMessage('buttons.close.text')), + new ButtonBuilder() + .setCustomId(JSON.stringify({ + action: 'custom', + id: 'cancel', + })) + .setStyle(ButtonStyle.Secondary) + .setEmoji(getMessage('buttons.cancel.emoji')) + .setLabel(getMessage('buttons.cancel.text')), + ]), + ], + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: getMessage('misc.expires_in', { time: ms(collectorTime, { long: true }) }), + }) + .setColor(settings.primaryColour) + .setTitle(getMessage('commands.slash.force-close.confirm_multiple.title')) + .setDescription(getMessage('commands.slash.force-close.confirm_multiple.description', { + count: tickets.length, + tickets: tickets.map(t => `> <#${t.id}>`).join('\n'), + time: ms(time, { long: true }), + })), + ], + }); + + confirmationM.awaitMessageComponent({ + componentType: ComponentType.Button, + filter: i => i.user.id === interaction.user.id, + time: collectorTime, + }) + .then(async i => { + if (JSON.parse(i.customId).id === 'close') { + await i.reply({ + components: [], + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.successColour) + .setTitle(getMessage('commands.slash.force-close.confirmed_multiple.title', tickets.length, tickets.length)) + .setDescription(getMessage('commands.slash.force-close.confirmed_multiple.description')), + ], + ephemeral: true, + }); + setTimeout(async () => { + for (const ticket of tickets) { + await client.tickets.finallyClose(ticket.id, { + closedBy: interaction.user.id, + reason: interaction.options.getString('reason', false), + }); + } + }, ms('3s')); + } else { + await interaction.deleteReply(); + } + }) + .catch(async error => { + client.log.error(error); + await interaction.reply({ + components: [], + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('misc.expired.title')) + .setDescription(getMessage('misc.expired.description', { time: ms(time, { long: true }) })), + ], + }); + }); + } else { + ticket = await client.prisma.ticket.findUnique({ + include: { category: true }, + where: { id: interaction.channel.id }, + }); + + if (!ticket) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('misc.not_ticket.title')) + .setDescription(getMessage('misc.not_ticket.description')), + ], + }); + } + + await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.successColour) + .setTitle(getMessage('commands.slash.force-close.closed_one.title')) + .setDescription(getMessage('commands.slash.force-close.closed_one.description', { ticket: ticket.id })), + ], + }); + + setTimeout(async () => { + await client.tickets.finallyClose(ticket.id, { + closedBy: interaction.user.id, + reason: interaction.options.getString('reason', false), + }); + }, ms('3s')); + } + } +}; diff --git a/src/commands/slash/help.js b/src/commands/slash/help.js new file mode 100644 index 0000000..2d5f13a --- /dev/null +++ b/src/commands/slash/help.js @@ -0,0 +1,79 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { isStaff } = require('../../lib/users'); +const ExtendedEmbedBuilder = require('../../lib/embed'); +const { version } = require('../../../package.json'); + +module.exports = class ClaimSlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'help'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + }); + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply({ ephemeral: true }); + const staff = await isStaff(interaction.guild, interaction.member.id); + const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } }); + const getMessage = client.i18n.getLocale(settings.locale); + const commands = client.application.commands.cache + .filter(c => c.type === 1) + .map(c => `> : ${c.description}`) + .join('\n'); + const newCommand = client.application.commands.cache.find(c => c.name === 'new'); + const fields = [ + { + name: getMessage('commands.slash.help.response.commands'), + value: commands, + }, + ]; + + if (staff) { + fields.unshift( + { + inline: true, + name: getMessage('commands.slash.help.response.links.links'), + value: [ + ['commands', 'https://discordtickets.app/features/commands'], + ['docs', 'https://discordtickets.app'], + ['feedback', 'https://lnk.earth/dsctickets-feedback'], + ['support', 'https://lnk.earth/discord'], + ] + .map(([l, url]) => `> [${getMessage('commands.slash.help.response.links.' + l)}](${url})`) + .join('\n'), + }, + { + inline: true, + name: getMessage('commands.slash.help.response.settings'), + value: '> ' + process.env.HTTP_EXTERNAL + '/settings', + }, + ); + } + + interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.primaryColour) + .setTitle(getMessage('commands.slash.help.title')) + .setDescription(staff + ? `**Discord Tickets v${version} by eartharoid.**` + : getMessage('commands.slash.help.response.description', { command: `` })) + .setFields(fields), + ], + }); + } +}; \ No newline at end of file diff --git a/src/commands/slash/move.js b/src/commands/slash/move.js new file mode 100644 index 0000000..25c582c --- /dev/null +++ b/src/commands/slash/move.js @@ -0,0 +1,170 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { ApplicationCommandOptionType } = require('discord.js'); +const ExtendedEmbedBuilder = require('../../lib/embed'); +const { isStaff } = require('../../lib/users'); + +module.exports = class MoveSlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'move'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + options: [ + { + autocomplete: true, + name: 'category', + required: true, + type: ApplicationCommandOptionType.Integer, + }, + ].map(option => { + option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`); + option.description = option.descriptionLocalizations['en-GB']; + option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`); + return option; + }), + }); + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply({ ephemeral: false }); + + const ticket = await client.prisma.ticket.findUnique({ + include: { + category: true, + guild: true, + }, + where: { id: interaction.channel.id }, + }); + + if (!ticket) { + const { locale } = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } }); + const getMessage = client.i18n.getLocale(locale); + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.errorColour) + .setTitle(getMessage('misc.not_ticket.title')) + .setDescription(getMessage('misc.not_ticket.description')), + ], + ephemeral: true, + }); + } + + const getMessage = client.i18n.getLocale(ticket.guild.locale); + + if (!(await isStaff(interaction.guild, interaction.user.id))) { // if user is not staff + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.errorColour) + .setTitle(getMessage('commands.slash.move.not_staff.title')) + .setDescription(getMessage('commands.slash.move.not_staff.description')), + ], + }); + } + + const creator = await interaction.guild.members.fetch(ticket.createdById); + const newCategory = await client.prisma.category.findUnique({ where: { id: interaction.options.getInteger('category', true) } }); + const discordCategory = await interaction.guild.channels.fetch(newCategory.discordCategory); + + if (discordCategory.children.cache.size === 50) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.errorColour) + .setTitle(getMessage('misc.category_full.title')) + .setDescription(getMessage('misc.category_full.description')), + ], + ephemeral: true, + }); + } else { + // don't reassign `ticket`, the previous value is used below + await client.prisma.ticket.update({ + data: { category: { connect: { id: newCategory.id } } }, + where: { id: ticket.id }, + }); + + const $oldCategory = client.tickets.$count.categories[ticket.categoryId]; + const $newCategory = client.tickets.$count.categories[newCategory.id]; + + $oldCategory.total--; + $oldCategory[ticket.createdById]--; + + $newCategory.total ||= 0; + $newCategory.total++; + + $newCategory[ticket.createdById] ||= 0; + $newCategory[ticket.createdById]++; + + // these 3 could be done separately, + // but using `setParent`, `setName` etc instead of a single `edit` call increases the number of API requests + if ( + newCategory.staffRoles !== ticket.category.staffRoles || + newCategory.channelName !== ticket.category.channelName || + newCategory.discordCategory !== ticket.category.discordCategory + ) { + const allow = ['ViewChannel', 'ReadMessageHistory', 'SendMessages', 'EmbedLinks', 'AttachFiles']; + const channelName = newCategory.channelName + .replace(/{+\s?(user)?name\s?}+/gi, creator.user.username) + .replace(/{+\s?(nick|display)(name)?\s?}+/gi, creator.displayName) + .replace(/{+\s?num(ber)?\s?}+/gi, ticket.number === 1488 ? '1487b' : ticket.number); + await interaction.channel.edit({ + lockPermissions: false, + name: channelName, + parent: discordCategory, + permissionOverwrites: [ + { + deny: ['ViewChannel'], + id: interaction.guild.roles.everyone, + }, + { + allow, + id: this.client.user.id, + }, + { + allow, + id: creator.id, + }, + ...newCategory.staffRoles.map(id => ({ + allow, + id, + })), + ], + reason: `Moved by ${interaction.user.tag}`, + }); + } + + await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(ticket.guild.primaryColour) + .setDescription(getMessage('commands.slash.move.moved', { + by: interaction.user.toString(), + from: ticket.category.name, + to: newCategory.name, + })), + ], + }); + + } + } +}; diff --git a/src/commands/slash/new.js b/src/commands/slash/new.js new file mode 100644 index 0000000..6fad360 --- /dev/null +++ b/src/commands/slash/new.js @@ -0,0 +1,38 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { ApplicationCommandOptionType } = require('discord.js'); +const { useGuild } = require('../../lib/tickets/utils'); + +module.exports = class NewSlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'new'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + options: [ + { + autocomplete: true, + name: 'references', + required: false, + type: ApplicationCommandOptionType.String, + }, + ].map(option => { + option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`); + option.description = option.descriptionLocalizations['en-GB']; + option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`); + return option; + }), + }); + } + + /** + * + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + await useGuild(this.client, interaction, { referencesTicketId: interaction.options.getString('references', false) }); + } +}; \ No newline at end of file diff --git a/src/commands/slash/priority.js b/src/commands/slash/priority.js new file mode 100644 index 0000000..2a44394 --- /dev/null +++ b/src/commands/slash/priority.js @@ -0,0 +1,142 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { ApplicationCommandOptionType } = require('discord.js'); +const ExtendedEmbedBuilder = require('../../lib/embed'); +const { logTicketEvent } = require('../../lib/logging'); +const { isStaff } = require('../../lib/users'); + +module.exports = class PrioritySlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'priority'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + options: [ + { + choices: ['HIGH', 'MEDIUM', 'LOW'], + name: 'priority', + required: true, + type: ApplicationCommandOptionType.String, + }, + ].map(option => { + option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`); + option.description = option.descriptionLocalizations['en-GB']; + option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`); + if (option.choices) { + option.choices = option.choices.map(choice => ({ + name: client.i18n.getMessage(null, `commands.slash.priority.options.${option.name}.choices.${choice}`), + nameLocalizations: client.i18n.getAllMessages(`commands.slash.priority.options.${option.name}.choices.${choice}`), + value: choice, + })); + } + return option; + }), + }); + } + + getEmoji(priority) { + let emoji; + switch (priority) { + case 'HIGH': { + emoji = '🔴'; + break; + } + case 'MEDIUM': { + emoji = '🟠'; + break; + } + case 'LOW': { + emoji = '🟢'; + break; + } + } + return emoji; + } + + /** + * + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply(); + + const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } }); + const getMessage = client.i18n.getLocale(settings.locale); + const ticket = await client.prisma.ticket.findUnique({ + include: { category: { select: { channelName: true } } }, + where: { id: interaction.channel.id }, + }); + + if (!ticket) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('misc.not_ticket.title')) + .setDescription(getMessage('misc.not_ticket.description')), + ], + }); + } + + if (!(await isStaff(interaction.guild, interaction.user.id))) { // if user is not staff + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.errorColour) + .setTitle(getMessage('commands.slash.move.not_staff.title')) + .setDescription(getMessage('commands.slash.move.not_staff.description')), + ], + }); + } + + const priority = interaction.options.getString('priority', true); + let name = interaction.channel.name; + if (ticket.priority) name = name.replace(this.getEmoji(ticket.priority), this.getEmoji(priority)); + else name = this.getEmoji(priority) + name; + await interaction.channel.setName(name); + + // don't reassign ticket because the original is used below + await client.prisma.ticket.update({ + data: { priority }, + where: { id: interaction.channel.id }, + }); + + logTicketEvent(this.client, { + action: 'update', + diff: { + original: { priority: ticket.priority }, + updated: { priority: priority }, + }, + target: { + id: ticket.id, + name: `<#${ticket.id}>`, + }, + userId: interaction.user.id, + }); + + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.successColour) + .setTitle(getMessage('commands.slash.priority.success.title')) + .setDescription(getMessage('commands.slash.priority.success.description', { priority: getMessage(`commands.slash.priority.options.priority.choices.${priority}`) })), + ], + }); + + } +}; \ No newline at end of file diff --git a/src/commands/slash/release.js b/src/commands/slash/release.js new file mode 100644 index 0000000..b9430c7 --- /dev/null +++ b/src/commands/slash/release.js @@ -0,0 +1,26 @@ +const { SlashCommand } = require('@eartharoid/dbf'); + +module.exports = class ReleaseSlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'release'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + }); + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply({ ephemeral: false }); + await client.tickets.release(interaction); + } +}; \ No newline at end of file diff --git a/src/commands/slash/remove.js b/src/commands/slash/remove.js new file mode 100644 index 0000000..a78ab0c --- /dev/null +++ b/src/commands/slash/remove.js @@ -0,0 +1,143 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { ApplicationCommandOptionType } = require('discord.js'); +const ExtendedEmbedBuilder = require('../../lib/embed'); +const { isStaff } = require('../../lib/users'); +const { logTicketEvent } = require('../../lib/logging'); + +module.exports = class RemoveSlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'remove'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + options: [ + { + name: 'member', + required: true, + type: ApplicationCommandOptionType.User, + }, + { + autocomplete: true, + name: 'ticket', + required: false, + type: ApplicationCommandOptionType.String, + }, + ].map(option => { + option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`); + option.description = option.descriptionLocalizations['en-GB']; + option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`); + return option; + }), + }); + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply({ ephemeral: true }); + + const ticket = await client.prisma.ticket.findUnique({ + include: { guild: true }, + where: { id: interaction.options.getString('ticket', false) || interaction.channel.id }, + }); + + if (!ticket) { + const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } }); + const getMessage = client.i18n.getLocale(settings.locale); + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('misc.invalid_ticket.title')) + .setDescription(getMessage('misc.invalid_ticket.description')), + ], + }); + } + + const getMessage = client.i18n.getLocale(ticket.guild.locale); + + if ( + ticket.id !== interaction.channel.id && + ticket.createdById !== interaction.member.id && + !(await isStaff(interaction.guild, interaction.member.id)) + ) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.errorColour) + .setTitle(getMessage('commands.slash.remove.not_staff.title')) + .setDescription(getMessage('commands.slash.remove.not_staff.description')), + ], + }); + } + + /** @type {import("discord.js").TextChannel} */ + const ticketChannel = await interaction.guild.channels.fetch(ticket.id); + const member = interaction.options.getMember('member', true); + + if (member.id === client.user.id || member.id === ticket.createdById) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(ticket.guild.errorColour) + .setTitle('❌'), + ], + }); + } + + await ticketChannel.permissionOverwrites.delete(member, `${interaction.user.tag} removed ${member.user.tag} from the ticket`); + + await ticketChannel.send({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(ticket.guild.primaryColour) + .setDescription(getMessage('commands.slash.remove.removed', { + by: interaction.member.toString(), + removed: member.toString(), + })), + ], + }); + + await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.successColour) + .setTitle(getMessage('commands.slash.remove.success.title')) + .setDescription(getMessage('commands.slash.remove.success.description', { + member: member.toString(), + ticket: ticketChannel.toString(), + })), + ], + }); + + logTicketEvent(this.client, { + action: 'update', + diff: { + original: { [getMessage('log.ticket.removed')]: member.user.tag }, + updated: {}, + }, + target: { + id: ticket.id, + name: `<#${ticket.id}>`, + }, + userId: interaction.user.id, + }); + } +}; \ No newline at end of file diff --git a/src/commands/slash/tag.js b/src/commands/slash/tag.js new file mode 100644 index 0000000..4eea3b5 --- /dev/null +++ b/src/commands/slash/tag.js @@ -0,0 +1,60 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { ApplicationCommandOptionType } = require('discord.js'); +const ExtendedEmbedBuilder = require('../../lib/embed'); + +module.exports = class TagSlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'tag'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + options: [ + { + autocomplete: true, + name: 'tag', + required: true, + type: ApplicationCommandOptionType.Integer, + }, + { + name: 'for', + required: false, + type: ApplicationCommandOptionType.User, + }, + ].map(option => { + option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`); + option.description = option.descriptionLocalizations['en-GB']; + option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`); + return option; + }), + }); + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + const user = interaction.options.getUser('for', false); + await interaction.deferReply({ ephemeral: !user }); + const tag = await client.prisma.tag.findUnique({ + include: { guild: true }, + where: { id: interaction.options.getInteger('tag', true) }, + }); + + await interaction.editReply({ + allowedMentions: { users: user ? [user.id]: [] }, + content: user?.toString(), + embeds: [ + new ExtendedEmbedBuilder() + .setColor(tag.guild.primaryColour) + .setDescription(tag.content), + ], + }); + } +}; \ No newline at end of file diff --git a/src/commands/slash/tickets.js b/src/commands/slash/tickets.js new file mode 100644 index 0000000..e39dd8c --- /dev/null +++ b/src/commands/slash/tickets.js @@ -0,0 +1,133 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { ApplicationCommandOptionType } = require('discord.js'); +const { isStaff } = require('../../lib/users'); +const ExtendedEmbedBuilder = require('../../lib/embed'); +const Cryptr = require('cryptr'); +const { decrypt } = new Cryptr(process.env.ENCRYPTION_KEY); + +module.exports = class TicketsSlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'tickets'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + options: [ + { + name: 'member', + required: false, + type: ApplicationCommandOptionType.User, + }, + ].map(option => { + option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`); + option.description = option.descriptionLocalizations['en-GB']; + option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`); + return option; + }), + }); + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply({ ephemeral: true }); + await client.application.commands.fetch(); + + const member = interaction.options.getMember('member', false) ?? interaction.member; + const ownOrOther = member.id === interaction.member.id ? 'own' : 'other'; + const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } }); + const getMessage = client.i18n.getLocale(settings.locale); + + if (member.id !== interaction.member.id && !(await isStaff(interaction.guild, interaction.member.id))) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('commands.slash.tickets.not_staff.title')) + .setDescription(getMessage('commands.slash.tickets.not_staff.description')), + ], + }); + } + + const fields = []; + + const open = await client.prisma.ticket.findMany({ + include: { category: true }, + where: { + createdById: member.id, + guildId: interaction.guild.id, + open: true, + }, + }); + + const closed = await client.prisma.ticket.findMany({ + include: { category: true }, + orderBy: { createdAt: 'desc' }, + take: 10, // max 10 rows + where: { + createdById: member.id, + guildId: interaction.guild.id, + open: false, + }, + }); + + if (open.length >= 1) { + fields.push({ + name: getMessage('commands.slash.tickets.response.fields.open.name'), + value: open.map(ticket =>{ + const topic = ticket.topic ? `- \`${decrypt(ticket.topic).replace(/\n/g, ' ').slice(0, 30) }\`` : ''; + return `> <#${ticket.id}> ${topic}`; + }).join('\n'), + }); + } + + if (closed.length === 0) { + const newCommand = client.application.commands.cache.find(c => c.name === 'new'); + fields.push({ + name: getMessage('commands.slash.tickets.response.fields.closed.name'), + value: getMessage(`commands.slash.tickets.response.fields.closed.none.${ownOrOther}`, { + new: ``, + user: member.user.toString(), + }), + }); + } else { + fields.push({ + name: getMessage('commands.slash.tickets.response.fields.closed.name'), + value: closed.map(ticket => { + const topic = ticket.topic ? `- \`${decrypt(ticket.topic).replace(/\n/g, ' ').slice(0, 30)}\`` : ''; + return `> ${ticket.category.name} #${ticket.number} ${topic}`; + }).join('\n'), + }); + } + // TODO: add portal URL to view all (this list is limited to the last 10) + + const embed = new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.primaryColour) + .setAuthor({ + iconURL: member.displayAvatarURL(), + name: member.displayName, + }) + .setTitle(getMessage(`commands.slash.tickets.response.title.${ownOrOther}`, { displayName: member.displayName })) + .setFields(fields); + + if (settings.archive && process.env.OVERRIDE_ARCHIVE !== 'false') { + const transcriptCommand = client.application.commands.cache.find(c => c.name === 'transcript'); + embed.setDescription(getMessage('commands.slash.tickets.response.description', { transcript: `` })); + } + + return await interaction.editReply({ embeds: [embed] }); + } +}; diff --git a/src/commands/slash/topic.js b/src/commands/slash/topic.js new file mode 100644 index 0000000..eed8da8 --- /dev/null +++ b/src/commands/slash/topic.js @@ -0,0 +1,84 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { + ActionRowBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle, +} = require('discord.js'); +const Cryptr = require('cryptr'); +const { decrypt } = new Cryptr(process.env.ENCRYPTION_KEY); +const ExtendedEmbedBuilder = require('../../lib/embed'); + +module.exports = class TopicSlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'topic'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + }); + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + const ticket = await client.prisma.ticket.findUnique({ + select: { + category: { select: { name: true } }, + guild: { select: { locale: true } }, + questionAnswers: { include: { question: true } }, + topic: true, + }, + where: { id: interaction.channel.id }, + }); + + if (!ticket) { + const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } }); + const getMessage = client.i18n.getLocale(settings.locale); + return await interaction.reply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('misc.not_ticket.title')) + .setDescription(getMessage('misc.not_ticket.description')), + ], + }); + } + + const getMessage = client.i18n.getLocale(ticket.guild.locale); + + const field = new TextInputBuilder() + .setCustomId('topic') + .setLabel(getMessage('modals.topic.label')) + .setStyle(TextInputStyle.Paragraph) + .setMaxLength(1000) + .setMinLength(5) + .setPlaceholder(getMessage('modals.topic.placeholder')) + .setRequired(true); + + if (ticket.topic) field.setValue(decrypt(ticket.topic)); // why can't discord.js accept null or undefined :( + + await interaction.showModal( + new ModalBuilder() + .setCustomId(JSON.stringify({ + action: 'topic', + edit: true, + })) + .setTitle(ticket.category.name) + .setComponents( + new ActionRowBuilder() + .setComponents(field), + ), + ); + } +}; \ No newline at end of file diff --git a/src/commands/slash/transcript.js b/src/commands/slash/transcript.js new file mode 100644 index 0000000..c9642a5 --- /dev/null +++ b/src/commands/slash/transcript.js @@ -0,0 +1,151 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { ApplicationCommandOptionType } = require('discord.js'); +const fs = require('fs'); +const { join } = require('path'); +const Mustache = require('mustache'); +const { AttachmentBuilder } = require('discord.js'); +const Cryptr = require('cryptr'); +const { decrypt } = new Cryptr(process.env.ENCRYPTION_KEY); + +module.exports = class TranscriptSlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'transcript'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + options: [ + { + autocomplete: true, + name: 'ticket', + required: true, + type: ApplicationCommandOptionType.String, + }, + { + name: 'member', + required: false, + type: ApplicationCommandOptionType.User, + }, + ].map(option => { + option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`); + option.description = option.descriptionLocalizations['en-GB']; + option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`); + return option; + }), + }); + + Mustache.escape = text => text; // don't HTML-escape + this.template = fs.readFileSync( + join('./user/templates/', this.client.config.templates.transcript + '.mustache'), + { encoding: 'utf8' }, + ); + } + + async fillTemplate(ticketId) { + /** @type {import("client")} */ + const client = this.client; + const ticket = await client.prisma.ticket.findUnique({ + include: { + archivedChannels: true, + archivedMessages: { + orderBy: { createdAt: 'asc' }, + where: { external: false }, + }, + archivedRoles: true, + archivedUsers: true, + category: true, + claimedBy: true, + closedBy: true, + createdBy: true, + feedback: true, + guild: true, + questionAnswers: true, + }, + where: { id: ticketId }, + }); + if (!ticket) throw new Error(`Ticket ${ticketId} does not exist`); + + ticket.claimedBy = ticket.archivedUsers.find(u => u.userId === ticket.claimedById); + ticket.closedBy = ticket.archivedUsers.find(u => u.userId === ticket.closedById); + ticket.createdBy = ticket.archivedUsers.find(u => u.userId === ticket.createdById); + + if (ticket.closedReason) ticket.closedReason = decrypt(ticket.closedReason); + if (ticket.feedback?.comment) ticket.feedback.comment = decrypt(ticket.feedback.comment); + if (ticket.topic) ticket.topic = decrypt(ticket.topic).replace(/\n/g, '\n\t'); + + ticket.archivedUsers.forEach((user, i) => { + if (user.displayName) user.displayName = decrypt(user.displayName); + user.username = decrypt(user.username); + ticket.archivedUsers[i] = user; + }); + + ticket.archivedMessages.forEach((message, i) => { + message.author = ticket.archivedUsers.find(u => u.userId === message.authorId); + message.content = JSON.parse(decrypt(message.content)); + message.text = message.content.content?.replace(/\n/g, '\n\t') ?? ''; + message.content.attachments?.forEach(a => (message.text += '\n\t' + a.url)); + message.content.embeds?.forEach(() => (message.text += '\n\t[embedded content]')); + message.number = 'M' + String(i + 1).padStart(ticket.archivedMessages.length.toString().length, '0'); + ticket.archivedMessages[i] = message; + }); + + ticket.pinnedMessageIds = ticket.pinnedMessageIds.map(id => ticket.archivedMessages.find(message => message.id === id)?.number); + + const channelName = ticket.category.channelName + .replace(/{+\s?(user)?name\s?}+/gi, ticket.createdBy?.username) + .replace(/{+\s?(nick|display)(name)?\s?}+/gi, ticket.createdBy?.displayName) + .replace(/{+\s?num(ber)?\s?}+/gi, ticket.number); + const fileName = `${channelName}.${this.client.config.templates.transcript.split('.').slice(-1)[0]}`; + const transcript = Mustache.render(this.template, { + channelName, + closedAtFull: function () { + return new Intl.DateTimeFormat([ticket.guild.locale, 'en-GB'], { + dateStyle: 'full', + timeStyle: 'long', + timeZone: 'Etc/UTC', + }).format(this.closedAt); + }, + createdAtFull: function () { + return new Intl.DateTimeFormat([ticket.guild.locale, 'en-GB'], { + dateStyle: 'full', + timeStyle: 'long', + timeZone: 'Etc/UTC', + }).format(this.createdAt); + }, + createdAtTimestamp: function () { + return new Intl.DateTimeFormat([ticket.guild.locale, 'en-GB'], { + dateStyle: 'short', + timeStyle: 'long', + timeZone: 'Etc/UTC', + }).format(this.createdAt); + }, + guildName: client.guilds.cache.get(ticket.guildId)?.name, + pinned: ticket.pinnedMessageIds.join(', '), + ticket, + }); + + return { + fileName, + transcript, + }; + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + await interaction.deferReply({ ephemeral: true }); + const { + fileName, + transcript, + } = await this.fillTemplate(interaction.options.getString('ticket', true)); + const attachment = new AttachmentBuilder() + .setFile(Buffer.from(transcript)) + .setName(fileName); + await interaction.editReply({ files: [attachment] }); + // TODO: add portal link + } +}; diff --git a/src/commands/slash/transfer.js b/src/commands/slash/transfer.js new file mode 100644 index 0000000..aa03f98 --- /dev/null +++ b/src/commands/slash/transfer.js @@ -0,0 +1,87 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { + ApplicationCommandOptionType, + EmbedBuilder, +} = require('discord.js'); +const Cryptr = require('cryptr'); +const { decrypt } = new Cryptr(process.env.ENCRYPTION_KEY); + +module.exports = class TransferSlashCommand extends SlashCommand { + constructor(client, options) { + const name = 'transfer'; + super(client, { + ...options, + description: client.i18n.getMessage(null, `commands.slash.${name}.description`), + descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`), + dmPermission: false, + name, + nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`), + options: [ + { + name: 'member', + required: true, + type: ApplicationCommandOptionType.User, + }, + ].map(option => { + option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`); + option.description = option.descriptionLocalizations['en-GB']; + option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`); + return option; + }), + }); + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply({ ephemeral: false }); + + const member = interaction.options.getMember('member', true); + + let ticket = await client.prisma.ticket.findUnique({ where: { id: interaction.channel.id } }); + const from = ticket.createdById; + + ticket = await client.prisma.ticket.update({ + data: { + createdBy: { + connectOrCreate: { + create: { id: member.id }, + where: { id: member.id }, + }, + }, + }, + include: { guild: true }, + where: { id: interaction.channel.id }, + }); + + await interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(ticket.guild.primaryColour) + .setDescription(client.i18n.getMessage(ticket.guild.locale, `commands.slash.transfer.transferred${interaction.member.id !== from ? '_from' : ''}`, { + from: `<@${from}>`, + to: member.toString(), + user: interaction.user.toString(), + })), + + ], + }); + + await interaction.channel.setTopic(`${member.toString()}${ticket.topic?.length > 0 ? ` | ${decrypt(ticket.topic)}` : ''}`); + + await interaction.channel.permissionOverwrites.edit( + member, + { + AttachFiles: true, + EmbedLinks: true, + ReadMessageHistory: true, + SendMessages: true, + ViewChannel: true, + }, + ); + } +}; diff --git a/src/commands/stats.js b/src/commands/stats.js deleted file mode 100644 index 79a832d..0000000 --- a/src/commands/stats.js +++ /dev/null @@ -1,95 +0,0 @@ -const Command = require('../modules/commands/command'); -const Keyv = require('keyv'); -const { - Interaction, // eslint-disable-line no-unused-vars - MessageEmbed -} = require('discord.js'); - -module.exports = class StatsCommand extends Command { - constructor(client) { - const i18n = client.i18n.getLocale(client.config.locale); - super(client, { - description: i18n('commands.stats.description'), - internal: true, - name: i18n('commands.stats.name'), - staff_only: true - }); - - this.cache = new Keyv({ namespace: 'cache.commands.stats' }); - } - - /** - * @param {Interaction} interaction - * @returns {Promise} - */ - async execute(interaction) { - const settings = await this.client.utils.getSettings(interaction.guild.id); - const i18n = this.client.i18n.getLocale(settings.locale); - - const messages = await this.client.db.models.Message.findAndCountAll(); - - let stats = await this.cache.get(interaction.guild.id); - - if (!stats) { - const tickets = await this.client.db.models.Ticket.findAndCountAll({ where: { guild: interaction.guild.id } }); - stats = { // maths - messages: settings.log_messages - ? await messages.rows - .reduce(async (acc, row) => (await this.client.db.models.Ticket.findOne({ where: { id: row.ticket } })) - .guild === interaction.guild.id - ? await acc + 1 - : await acc, 0) - : null, - response_time: Math.floor(tickets.rows.reduce((acc, row) => row.first_response - ? acc + ((Math.abs(new Date(row.createdAt) - new Date(row.first_response)) / 1000) / 60) - : acc, 0) / tickets.count), - tickets: tickets.count - }; - await this.cache.set(interaction.guild.id, stats, 60 * 60 * 1000); // cache for an hour - } - - const guild_embed = new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.stats.response.guild.title')) - .setDescription(i18n('commands.stats.response.guild.description')) - .addField(i18n('commands.stats.fields.tickets'), String(stats.tickets), true) - .addField(i18n('commands.stats.fields.response_time.title'), i18n('commands.stats.fields.response_time.minutes', stats.response_time), true) - .setFooter(settings.footer, interaction.guild.iconURL()); - - if (stats.messages) guild_embed.addField(i18n('commands.stats.fields.messages'), String(stats.messages), true); - - const embeds = [guild_embed]; - - if (this.client.guilds.cache.size > 1) { - let global = await this.cache.get('global'); - - if (!global) { - const tickets = await this.client.db.models.Ticket.findAndCountAll(); - global = { // maths - messages: settings.log_messages - ? await messages.count - : null, - response_time: Math.floor(tickets.rows.reduce((acc, row) => row.first_response - ? acc + ((Math.abs(new Date(row.createdAt) - new Date(row.first_response)) / 1000) / 60) - : acc, 0) / tickets.count), - tickets: tickets.count - }; - await this.cache.set('global', global, 60 * 60 * 1000); // cache for an hour - } - - const global_embed = new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.stats.response.global.title')) - .setDescription(i18n('commands.stats.response.global.description')) - .addField(i18n('commands.stats.fields.tickets'), String(global.tickets), true) - .addField(i18n('commands.stats.fields.response_time.title'), i18n('commands.stats.fields.response_time.minutes', global.response_time), true) - .setFooter(settings.footer, interaction.guild.iconURL()); - - if (stats.messages) global_embed.addField(i18n('commands.stats.fields.messages'), String(global.messages), true); - - embeds.push(global_embed); - } - - await interaction.reply({ embeds }); - } -}; diff --git a/src/commands/survey.js b/src/commands/survey.js deleted file mode 100644 index 86be0a7..0000000 --- a/src/commands/survey.js +++ /dev/null @@ -1,113 +0,0 @@ -const Command = require('../modules/commands/command'); -// eslint-disable-next-line no-unused-vars -const { - Message, // eslint-disable-line no-unused-vars - MessageAttachment, - MessageEmbed -} = require('discord.js'); -const fsp = require('fs').promises; -const { path } = require('../utils/fs'); -const mustache = require('mustache'); - -module.exports = class SurveyCommand extends Command { - constructor(client) { - const i18n = client.i18n.getLocale(client.config.locale); - super(client, { - description: i18n('commands.survey.description'), - internal: true, - name: i18n('commands.survey.name'), - options: async guild => { - const surveys = await this.client.db.models.Survey.findAll({ where: { guild: guild.id } }); - return [ - { - choices: surveys.map(survey => ({ - name: survey.name, - value: survey.name - })), - description: i18n('commands.survey.options.survey.description'), - name: i18n('commands.survey.options.survey.name'), - required: true, - type: Command.option_types.STRING - } - ]; - }, - staff_only: true - - }); - } - - /** - * @param {Interaction} interaction - * @returns {Promise} - */ - async execute(interaction) { - const settings = await this.client.utils.getSettings(interaction.guild.id); - const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale - const i18n = this.client.i18n.getLocale(settings.locale); - - const name = interaction.options.getString(default_i18n('commands.survey.options.survey.name')); - - const survey = await this.client.db.models.Survey.findOne({ - where: { - guild: interaction.guild.id, - name - } - }); - - if (survey) { - const { - rows: responses, count - } = await this.client.db.models.SurveyResponse.findAndCountAll({ where: { survey: survey.id } }); - - const users = new Set(); - - for (const i in responses) { - const ticket = await this.client.db.models.Ticket.findOne({ where: { id: responses[i].ticket } }); - users.add(ticket.creator); - const answers = responses[i].answers.map(a => this.client.cryptr.decrypt(a)); - answers.unshift(ticket.number); - responses[i] = answers; - } - - let template = await fsp.readFile(path('./src/commands/extra/survey.template.html'), { encoding: 'utf8' }); - - template = template.replace(/[\r\n\t]/g, ''); - - survey.questions.unshift('Ticket #'); - - const html = mustache.render(template, { - columns: survey.questions, - count: { - responses: count, - users: users.size - }, - responses, - survey: survey.name.charAt(0).toUpperCase() + survey.name.slice(1) - }); - - const attachment = new MessageAttachment( - Buffer.from(html), - `${survey.name}.html` - ); - - return await interaction.reply({ - ephemeral: true, - files: [attachment] - }); - } else { - const surveys = await this.client.db.models.Survey.findAll({ where: { guild: interaction.guild.id } }); - - const list = surveys.map(s => `❯ **\`${s.name}\`**`); - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.survey.response.list.title')) - .setDescription(list.join('\n')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - } -}; diff --git a/src/commands/tag.js b/src/commands/tag.js deleted file mode 100644 index 7bbf00b..0000000 --- a/src/commands/tag.js +++ /dev/null @@ -1,71 +0,0 @@ -const Command = require('../modules/commands/command'); -const { - Interaction, // eslint-disable-line no-unused-vars - MessageEmbed -} = require('discord.js'); - -module.exports = class TagCommand extends Command { - constructor(client) { - const i18n = client.i18n.getLocale(client.config.locale); - super(client, { - description: i18n('commands.tag.description'), - internal: true, - name: i18n('commands.tag.name'), - options: async guild => { - const settings = await client.utils.getSettings(guild.id); - return Object.keys(settings.tags).map(tag => ({ - description: settings.tags[tag].substring(0, 100), - name: tag, - options: [...settings.tags[tag].matchAll(/(? ({ - description: match[1], - name: match[1], - required: true, - type: Command.option_types.STRING - })), - type: Command.option_types.SUB_COMMAND - })); - }, - staff_only: true - }); - } - - /** - * @param {Interaction} interaction - * @returns {Promise} - */ - async execute(interaction) { - const settings = await this.client.utils.getSettings(interaction.guild.id); - const i18n = this.client.i18n.getLocale(settings.locale); - - try { - const tag_name = interaction.options.getSubcommand(); - const tag = settings.tags[tag_name]; - const args = interaction.options.data[0]?.options; - const text = tag.replace(/(? { - const arg = args.find(arg => arg.name === $1); - return arg ? arg.value : $; - }); - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setDescription(text) - ], - ephemeral: false - }); - } catch { - const list = Object.keys(settings.tags).map(t => `❯ **\`${t}\`**`); - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.tag.response.list.title')) - .setDescription(list.join('\n')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - } -}; diff --git a/src/commands/topic.js b/src/commands/topic.js deleted file mode 100644 index 7fa6139..0000000 --- a/src/commands/topic.js +++ /dev/null @@ -1,87 +0,0 @@ -const Command = require('../modules/commands/command'); -const { - Interaction, // eslint-disable-line no-unused-vars - MessageEmbed -} = require('discord.js'); - -module.exports = class TopicCommand extends Command { - constructor(client) { - const i18n = client.i18n.getLocale(client.config.locale); - super(client, { - description: i18n('commands.topic.description'), - internal: true, - name: i18n('commands.topic.name'), - options: [ - { - description: i18n('commands.topic.options.new_topic.description'), - name: i18n('commands.topic.options.new_topic.name'), - required: true, - type: Command.option_types.STRING - } - ] - }); - } - - /** - * @param {Interaction} interaction - * @returns {Promise} - */ - async execute(interaction) { - const settings = await this.client.utils.getSettings(interaction.guild.id); - const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale - const i18n = this.client.i18n.getLocale(settings.locale); - - const topic = interaction.options.getString(default_i18n('commands.topic.options.new_topic.name')); - - const t_row = await this.client.db.models.Ticket.findOne({ where: { id: interaction.channel.id } }); - - if (!t_row) { - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.topic.response.not_a_ticket.title')) - .setDescription(i18n('commands.topic.response.not_a_ticket.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - await t_row.update({ topic: this.client.cryptr.encrypt(topic) }); - - const member = await interaction.guild.members.fetch(t_row.creator); - interaction.channel.setTopic(`${member} | ${topic}`, { reason: 'User updated ticket topic' }); - - const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } }); - const description = cat_row.opening_message - .replace(/{+\s?(user)?name\s?}+/gi, member.displayName) - .replace(/{+\s?(tag|ping|mention)?\s?}+/gi, member.user.toString()); - const opening_message = await interaction.channel.messages.fetch(t_row.opening_message); - - await opening_message.edit({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setAuthor(member.user.username, member.user.displayAvatarURL()) - .setDescription(description) - .addField(i18n('ticket.opening_message.fields.topic'), topic) - .setFooter(settings.footer, interaction.guild.iconURL()) - ] - }); - - await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.topic.response.changed.title')) - .setDescription(i18n('commands.topic.response.changed.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: false - }); - - this.client.log.info(`${interaction.user.tag} changed the topic of #${interaction.channel.name}`); - } -}; diff --git a/src/commands/user/create.js b/src/commands/user/create.js new file mode 100644 index 0000000..7176d40 --- /dev/null +++ b/src/commands/user/create.js @@ -0,0 +1,168 @@ +const { UserCommand } = require('@eartharoid/dbf'); +const { isStaff } = require('../../lib/users'); +const ExtendedEmbedBuilder = require('../../lib/embed'); +const ms = require('ms'); +const { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ComponentType, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, +} = require('discord.js'); +const emoji = require('node-emoji'); + +module.exports = class CreateUserCommand extends UserCommand { + constructor(client, options) { + const nameLocalizations = {}; + client.i18n.locales.forEach(l => (nameLocalizations[l] = client.i18n.getMessage(l, 'commands.user.create.name'))); + + super(client, { + ...options, + dmPermission: false, + name: nameLocalizations['en-GB'], + nameLocalizations, + }); + } + + /** + * @param {import("discord.js").UserContextMenuCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply({ ephemeral: true }); + + const settings = await client.prisma.guild.findUnique({ + include: { categories:true }, + where: { id: interaction.guild.id }, + }); + const getMessage = client.i18n.getLocale(settings.locale); + + if (!await isStaff(interaction.guild, interaction.user.id)) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('commands.user.create.not_staff.title')) + .setDescription(getMessage('commands.user.create.not_staff.description')), + ], + }); + } + + const prompt = async categoryId => { + interaction.followUp({ + components: [ + new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ + action: 'create', + target: categoryId, + targetUser: interaction.targetId, + })) + .setStyle(ButtonStyle.Primary) + .setEmoji(getMessage('buttons.create.emoji')) // emoji.get('ticket') + .setLabel(getMessage('buttons.create.text')), + ), + ], + content: interaction.targetUser.toString(), + embeds: [ + new ExtendedEmbedBuilder() + .setColor(settings.primaryColour) + .setAuthor({ + iconURL: interaction.member.displayAvatarURL(), + name: interaction.member.displayName, + }) + .setTitle(getMessage('commands.user.create.prompt.title')) + .setDescription(getMessage('commands.user.create.prompt.description')), + ], + ephemeral: false, + }); + }; + + if (settings.categories.length === 0) { + interaction.reply({ + components: [], + embeds: [ + new ExtendedEmbedBuilder() + .setColor(settings.errorColour) + .setTitle(getMessage('misc.no_categories.title')) + .setDescription(getMessage('misc.no_categories.description')), + ], + ephemeral: true, + }); + } else if (settings.categories.length === 1) { + await prompt(settings.categories[0].id); + } else { + const collectorTime = ms('15s'); + const confirmationM = await interaction.editReply({ + components: [ + new ActionRowBuilder() + .setComponents( + new StringSelectMenuBuilder() + .setCustomId(JSON.stringify({ + action: 'promptCreate', + user: interaction.targetId, + })) + .setPlaceholder(getMessage('menus.category.placeholder')) + .setOptions( + settings.categories.map(category => + new StringSelectMenuOptionBuilder() + .setValue(String(category.id)) + .setLabel(category.name) + .setDescription(category.description) + .setEmoji(emoji.hasEmoji(category.emoji) ? emoji.get(category.emoji) : { id: category.emoji }), + ), + ), + ), + ], + }); + + confirmationM.awaitMessageComponent({ + componentType: ComponentType.StringSelect, + filter: i => i.user.id === interaction.user.id, + time: collectorTime, + }) + .then(async i => { + const category = settings.categories.find(c => c.id === Number(i.values[0])); + await i.update({ + components: [], + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.successColour) + .setTitle(getMessage('commands.user.create.sent.title')) + .setDescription(getMessage('commands.user.create.sent.description', { + category: category.name, + user: interaction.targetUser.toString(), + })), + ], + ephemeral: true, + }); + await prompt(category.id); + }) + .catch(async error => { + client.log.error(error); + await interaction.reply({ + components: [], + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('misc.expired.title')) + .setDescription(getMessage('misc.expired.description')), + ], + }); + }); + } + } +}; diff --git a/src/database/dialects.js b/src/database/dialects.js deleted file mode 100644 index d7ecbf6..0000000 --- a/src/database/dialects.js +++ /dev/null @@ -1,42 +0,0 @@ -module.exports = { - maria: { - dialect: 'mariadb', - name: 'MariaDB', - packages: ['mariadb'] - }, - mariadb: { - dialect: 'mariadb', - name: 'MariaDB', - packages: ['mariadb'] - }, - microsoft: { - dialect: 'mssql', - name: 'Microsoft SQL', - packages: ['tedious'] - }, - mysql: { - dialect: 'mysql', - name: 'MySQL', - packages: ['mysql2'] - }, - postgre: { // this is wrong - dialect: 'postgres', - name: 'PostgreSQL', - packages: ['pg', 'pg-hstore'] - }, - postgres: { - dialect: 'postgres', - name: 'PostgreSQL', - packages: ['pg', 'pg-hstore'] - }, - postgresql: { - dialect: 'postgres', - name: 'PostgreSQL', - packages: ['pg', 'pg-hstore'] - }, - sqlite: { - dialect: 'sqlite', - name: 'SQLite', - packages: ['sqlite3'] - } -}; \ No newline at end of file diff --git a/src/database/index.js b/src/database/index.js deleted file mode 100644 index 9b391f9..0000000 --- a/src/database/index.js +++ /dev/null @@ -1,73 +0,0 @@ -const { Sequelize } = require('sequelize'); -const fs = require('fs'); -const { path } = require('../utils/fs'); -const types = require('./dialects'); - -module.exports = async client => { - - const { - DB_TYPE, - DB_HOST, - DB_PORT, - DB_USER, - DB_PASS, - DB_NAME - } = process.env; - - const type = (DB_TYPE || 'sqlite').toLowerCase(); - - const supported = Object.keys(types); - if (!supported.includes(type)) { - client.log.error(new Error(`DB_TYPE (${type}) is not a valid type`)); - return process.exit(); - } - - try { - types[type].packages.forEach(pkg => require(pkg)); - } catch { - const required = types[type].packages.map(i => `"${i}"`).join(' and '); - client.log.error(new Error(`Please install the package(s) for your selected database type: ${required}`)); - return process.exit(); - } - - let sequelize; - - if (type === 'sqlite') { - client.log.info('Using SQLite storage'); - sequelize = new Sequelize({ - dialect: types[type].dialect, - logging: text => client.log.debug(text), - storage: path('./user/database.sqlite') - }); - client.config.defaults.log_messages = false; - client.log.warn('Message logging is disabled due to insufficient database'); - } else { - client.log.info(`Connecting to ${types[type].name} database...`); - sequelize = new Sequelize(DB_NAME, DB_USER, DB_PASS, { - dialect: types[type].dialect, - host: DB_HOST, - logging: text => client.log.debug(text), - port: DB_PORT - }); - } - - try { - await sequelize.authenticate(); - client.log.success('Connected to database successfully'); - } catch (error) { - client.log.warn('Failed to connect to database'); - client.log.error(error); - return process.exit(); - } - - const models = fs.readdirSync(path('./src/database/models')) - .filter(filename => filename.endsWith('.model.js')); - - for (const model of models) { - require(`./models/${model}`)(client, sequelize); - } - - await sequelize.sync({ alter: false }); - - return sequelize; -}; \ No newline at end of file diff --git a/src/database/models/category.model.js b/src/database/models/category.model.js deleted file mode 100644 index d9887e8..0000000 --- a/src/database/models/category.model.js +++ /dev/null @@ -1,93 +0,0 @@ -const { DataTypes } = require('sequelize'); -module.exports = ({ config }, sequelize) => { - const { DB_TABLE_PREFIX } = process.env; - sequelize.define('Category', { - claiming: { - defaultValue: false, - type: DataTypes.BOOLEAN - }, - guild: { - allowNull: false, - references: { - key: 'id', - model: DB_TABLE_PREFIX + 'guilds' - }, - type: DataTypes.CHAR(19), - unique: 'name-guild' - }, - id: { - allowNull: false, - primaryKey: true, - type: DataTypes.CHAR(19) - }, - image: { - allowNull: true, - type: DataTypes.STRING - }, - max_per_member: { - defaultValue: 1, - type: DataTypes.INTEGER - }, - name: { - allowNull: false, - type: DataTypes.STRING, - unique: 'name-guild' - }, - name_format: { - allowNull: false, - defaultValue: config.defaults.name_format, - type: DataTypes.STRING - }, - opening_message: { - defaultValue: config.defaults.opening_message, - type: DataTypes.TEXT - }, - opening_questions: { - allowNull: true, - get() { - const raw_value = this.getDataValue('opening_questions'); - return raw_value - ? typeof raw_value === 'string' - ? JSON.parse(raw_value) - : raw_value - : null; - }, - type: DataTypes.JSON - }, - ping: { - defaultValue: [], - get() { - const raw_value = this.getDataValue('ping'); - return raw_value - ? typeof raw_value === 'string' - ? JSON.parse(raw_value) - : raw_value - : null; - }, - type: DataTypes.JSON - }, - require_topic: { - defaultValue: false, - type: DataTypes.BOOLEAN - }, - roles: { - allowNull: false, - get() { - const raw_value = this.getDataValue('roles'); - return raw_value - ? typeof raw_value === 'string' - ? JSON.parse(raw_value) - : raw_value - : null; - }, - type: DataTypes.JSON - }, - survey: { - allowNull: true, - type: DataTypes.STRING - } - }, { - paranoid: true, - tableName: DB_TABLE_PREFIX + 'categories' - }); -}; \ No newline at end of file diff --git a/src/database/models/channel_entity.model.js b/src/database/models/channel_entity.model.js deleted file mode 100644 index d0a403d..0000000 --- a/src/database/models/channel_entity.model.js +++ /dev/null @@ -1,21 +0,0 @@ -const { DataTypes } = require('sequelize'); -module.exports = (_client, sequelize) => { - const { DB_TABLE_PREFIX } = process.env; - sequelize.define('ChannelEntity', { - channel: { - allowNull: false, - type: DataTypes.CHAR(19), - unique: 'channel-ticket' - }, - name: DataTypes.TEXT, - ticket: { - allowNull: false, - references: { - key: 'id', - model: DB_TABLE_PREFIX + 'tickets' - }, - type: DataTypes.CHAR(19), - unique: 'channel-ticket' - } - }, { tableName: DB_TABLE_PREFIX + 'channel_entities' }); -}; \ No newline at end of file diff --git a/src/database/models/guild.model.js b/src/database/models/guild.model.js deleted file mode 100644 index bf8bfbf..0000000 --- a/src/database/models/guild.model.js +++ /dev/null @@ -1,66 +0,0 @@ -const { DataTypes } = require('sequelize'); -module.exports = ({ config }, sequelize) => { - const { DB_TABLE_PREFIX } = process.env; - sequelize.define('Guild', { - blacklist: { - defaultValue: { - members: [], - roles: [] - }, - get() { - const raw_value = this.getDataValue('blacklist'); - return raw_value - ? typeof raw_value === 'string' - ? JSON.parse(raw_value) - : raw_value - : null; - }, - type: DataTypes.JSON - }, - close_button: { - defaultValue: false, - type: DataTypes.BOOLEAN - }, - colour: { - defaultValue: config.defaults.colour, - type: DataTypes.STRING - }, - error_colour: { - defaultValue: 'RED', - type: DataTypes.STRING - }, - footer: { - defaultValue: 'Discord Tickets by eartharoid', - type: DataTypes.STRING - }, - id: { - allowNull: false, - primaryKey: true, - type: DataTypes.CHAR(19) - }, - locale: { - defaultValue: config.locale, - type: DataTypes.STRING - }, - log_messages: { - defaultValue: config.defaults.log_messages, - type: DataTypes.BOOLEAN - }, - success_colour: { - defaultValue: 'GREEN', - type: DataTypes.STRING - }, - tags: { - defaultValue: {}, - get() { - const raw_value = this.getDataValue('tags'); - return raw_value - ? typeof raw_value === 'string' - ? JSON.parse(raw_value) - : raw_value - : null; - }, - type: DataTypes.JSON - } - }, { tableName: DB_TABLE_PREFIX + 'guilds' }); -}; diff --git a/src/database/models/message.model.js b/src/database/models/message.model.js deleted file mode 100644 index 85b9ee6..0000000 --- a/src/database/models/message.model.js +++ /dev/null @@ -1,35 +0,0 @@ -const { DataTypes } = require('sequelize'); -module.exports = (_client, sequelize) => { - const { DB_TABLE_PREFIX } = process.env; - sequelize.define('Message', { - author: { - allowNull: false, - type: DataTypes.CHAR(19) - }, - data: { - allowNull: false, - type: DataTypes.TEXT - }, - deleted: { - defaultValue: false, - type: DataTypes.BOOLEAN - }, - edited: { - defaultValue: false, - type: DataTypes.BOOLEAN - }, - id: { - allowNull: false, - primaryKey: true, - type: DataTypes.CHAR(19) - }, - ticket: { - allowNull: false, - references: { - key: 'id', - model: DB_TABLE_PREFIX + 'tickets' - }, - type: DataTypes.CHAR(19) - } - }, { tableName: DB_TABLE_PREFIX + 'messages' }); -}; \ No newline at end of file diff --git a/src/database/models/panel.model.js b/src/database/models/panel.model.js deleted file mode 100644 index ea36aef..0000000 --- a/src/database/models/panel.model.js +++ /dev/null @@ -1,22 +0,0 @@ -const { DataTypes } = require('sequelize'); -module.exports = (client, sequelize) => { - const { DB_TABLE_PREFIX } = process.env; - sequelize.define('Panel', { - category: { - allowNull: true, - type: DataTypes.CHAR(19) - }, - channel: { - allowNull: false, - type: DataTypes.CHAR(19) - }, - guild: { - allowNull: false, - references: { - key: 'id', - model: DB_TABLE_PREFIX + 'guilds' - }, - type: DataTypes.CHAR(19) - } - }, { tableName: DB_TABLE_PREFIX + 'panels' }); -}; \ No newline at end of file diff --git a/src/database/models/role_entity.model.js b/src/database/models/role_entity.model.js deleted file mode 100644 index c6ea0e9..0000000 --- a/src/database/models/role_entity.model.js +++ /dev/null @@ -1,25 +0,0 @@ -const { DataTypes } = require('sequelize'); -module.exports = (client, sequelize) => { - const { DB_TABLE_PREFIX } = process.env; - sequelize.define('RoleEntity', { - colour: { - defaultValue: '7289DA', - type: DataTypes.CHAR(6) - }, - name: DataTypes.TEXT, - role: { - allowNull: false, - type: DataTypes.CHAR(19), - unique: 'role-ticket' - }, - ticket: { - allowNull: false, - references: { - key: 'id', - model: DB_TABLE_PREFIX + 'tickets' - }, - type: DataTypes.CHAR(19), - unique: 'role-ticket' - } - }, { tableName: DB_TABLE_PREFIX + 'role_entities' }); -}; \ No newline at end of file diff --git a/src/database/models/survey.model.js b/src/database/models/survey.model.js deleted file mode 100644 index 3a2d6d0..0000000 --- a/src/database/models/survey.model.js +++ /dev/null @@ -1,32 +0,0 @@ -const { DataTypes } = require('sequelize'); -module.exports = (client, sequelize) => { - const { DB_TABLE_PREFIX } = process.env; - sequelize.define('Survey', { - guild: { - allowNull: false, - references: { - key: 'id', - model: DB_TABLE_PREFIX + 'guilds' - }, - type: DataTypes.CHAR(19), - unique: 'name-guild' - }, - name: { - allowNull: false, - type: DataTypes.STRING, - unique: 'name-guild' - }, - questions: { - allowNull: true, - get() { - const raw_value = this.getDataValue('questions'); - return raw_value - ? typeof raw_value === 'string' - ? JSON.parse(raw_value) - : raw_value - : null; - }, - type: DataTypes.JSON - } - }, { tableName: DB_TABLE_PREFIX + 'surveys' }); -}; \ No newline at end of file diff --git a/src/database/models/survey_response.model.js b/src/database/models/survey_response.model.js deleted file mode 100644 index 379fe79..0000000 --- a/src/database/models/survey_response.model.js +++ /dev/null @@ -1,36 +0,0 @@ -const { DataTypes } = require('sequelize'); -module.exports = (client, sequelize) => { - const { DB_TABLE_PREFIX } = process.env; - sequelize.define('SurveyResponse', { - answers: { - allowNull: true, - get() { - const raw_value = this.getDataValue('answers'); - return raw_value - ? typeof raw_value === 'string' - ? JSON.parse(raw_value) - : raw_value - : null; - }, - type: DataTypes.JSON - }, - survey: { - allowNull: false, - references: { - key: 'id', - model: DB_TABLE_PREFIX + 'surveys' - }, - type: DataTypes.INTEGER, - unique: 'survey-ticket' - }, - ticket: { - allowNull: false, - references: { - key: 'id', - model: DB_TABLE_PREFIX + 'tickets' - }, - type: DataTypes.CHAR(19), - unique: 'survey-ticket' - } - }, { tableName: DB_TABLE_PREFIX + 'survey_responses' }); -}; \ No newline at end of file diff --git a/src/database/models/ticket.model.js b/src/database/models/ticket.model.js deleted file mode 100644 index 370e1be..0000000 --- a/src/database/models/ticket.model.js +++ /dev/null @@ -1,81 +0,0 @@ -const { DataTypes } = require('sequelize'); -module.exports = (_client, sequelize) => { - const { DB_TABLE_PREFIX } = process.env; - sequelize.define('Ticket', { - category: { - allowNull: false, - references: { - key: 'id', - model: DB_TABLE_PREFIX + 'categories' - }, - type: DataTypes.CHAR(19) - }, - claimed_by: { - allowNull: true, - type: DataTypes.CHAR(19) - }, - closed_by: { - allowNull: true, - type: DataTypes.CHAR(19) - }, - closed_reason: { - allowNull: true, - type: DataTypes.STRING - }, - creator: { - allowNull: false, - type: DataTypes.CHAR(19) - }, - first_response: { - allowNull: true, - type: DataTypes.DATE - }, - guild: { - allowNull: false, - references: { - key: 'id', - model: DB_TABLE_PREFIX + 'guilds' - }, - type: DataTypes.CHAR(19), - unique: 'number-guild' - }, - id: { - allowNull: false, - primaryKey: true, - type: DataTypes.CHAR(19) - }, - last_message: { - allowNull: true, - type: DataTypes.DATE - }, - number: { - allowNull: false, - type: DataTypes.INTEGER, - unique: 'number-guild' - }, - open: { - defaultValue: true, - type: DataTypes.BOOLEAN - }, - opening_message: { - allowNull: true, - type: DataTypes.CHAR(19) - }, - pinned_messages: { - defaultValue: [], - get() { - const raw_value = this.getDataValue('pinned_messages'); - return raw_value - ? typeof raw_value === 'string' - ? JSON.parse(raw_value) - : raw_value - : null; - }, - type: DataTypes.JSON - }, - topic: { - allowNull: true, - type: DataTypes.TEXT - } - }, { tableName: DB_TABLE_PREFIX + 'tickets' }); -}; \ No newline at end of file diff --git a/src/database/models/user_entity.model.js b/src/database/models/user_entity.model.js deleted file mode 100644 index 0117ae6..0000000 --- a/src/database/models/user_entity.model.js +++ /dev/null @@ -1,34 +0,0 @@ -const { DataTypes } = require('sequelize'); -module.exports = (client, sequelize) => { - const { DB_TABLE_PREFIX } = process.env; - sequelize.define('UserEntity', { - avatar: DataTypes.STRING, - bot: DataTypes.BOOLEAN, - discriminator: DataTypes.STRING, - display_name: DataTypes.TEXT, - role: { - allowNull: false, - references: { - key: 'role', - model: DB_TABLE_PREFIX + 'role_entities' - }, - type: DataTypes.CHAR(19) - }, - ticket: { - allowNull: false, - references: { - key: 'id', - model: DB_TABLE_PREFIX + 'tickets' - }, - type: DataTypes.CHAR(19), - unique: 'user-ticket' - }, - user: { - allowNull: false, - type: DataTypes.CHAR(19), - unique: 'user-ticket' - }, - username: DataTypes.TEXT - - }, { tableName: DB_TABLE_PREFIX + 'user_entities' }); -}; \ No newline at end of file diff --git a/src/env.js b/src/env.js new file mode 100644 index 0000000..f872f25 --- /dev/null +++ b/src/env.js @@ -0,0 +1,57 @@ +/* eslint-disable no-console */ + +const dotenv = require('dotenv'); +const { colours } = require('leeks.js'); + +const providers = ['mysql', 'postgresql', 'sqlite']; + +// ideally the defaults would be set here too, but the pre-install script may run when `src/` is not available +const env = { + DB_CONNECTION_URL: v => + !!v || + (process.env.DB_PROVIDER === 'sqlite') || + new Error('must be set when "DB_PROVIDER" is not "sqlite"'), + DB_PROVIDER: v => + (!!v && providers.includes(v)) || + new Error(`must be one of: ${providers.map(v => `"${v}"`).join(', ')}`), + DISCORD_SECRET: v => + !!v || + new Error('is required'), + DISCORD_TOKEN: v => + !!v || + new Error('is required'), + ENCRYPTION_KEY: v => + (!!v && v.length >= 48) || + new Error('is required and must be at least 48 characters long; run "npm run keygen" to generate a key'), + HTTP_EXTERNAL: v => + (!!v && v.startsWith('http') && !v.endsWith('/')) || + new Error('must be a valid URL without a trailing slash'), + HTTP_HOST: v => + (!!v && !v.startsWith('http')) || + new Error('is required and must be an address, not a URL'), + HTTP_PORT: v => + !!v || + new Error('is required'), + HTTP_TRUST_PROXY: () => true, // optional + INVALIDATE_TOKENS: () => true, // optional + OVERRIDE_ARCHIVE: () => true, // optional + PUBLIC_BOT: () => true, // optional + PUBLISH_COMMANDS: () => true, // optional + SUPER: () => true, // optional +}; + +const load = options => { + dotenv.config(options); + Object.entries(env).forEach(([name, validate]) => { + const result = validate(process.env[name]); // `true` for pass, or `Error` for fail + if (result instanceof Error) { + console.log('\x07' + colours.redBright(`Error: The "${name}" environment variable ${result.message}.`)); + process.exit(1); + } + }); +}; + +module.exports = { + env, + load, +}; diff --git a/src/http.js b/src/http.js new file mode 100644 index 0000000..2d1ff78 --- /dev/null +++ b/src/http.js @@ -0,0 +1,176 @@ +const fastify = require('fastify')({ trustProxy: process.env.HTTP_TRUST_PROXY === 'true' }); +const oauth = require('@fastify/oauth2'); +const { randomBytes } = require('crypto'); +const { short } = require('leeks.js'); +const { join } = require('path'); +const { files } = require('node-dir'); +const { PermissionsBitField } = require('discord.js'); + +process.env.ORIGIN = process.env.HTTP_EXTERNAL; + +module.exports = async client => { + // oauth2 plugin + fastify.states = new Map(); + fastify.register(oauth, { + callbackUri: `${process.env.HTTP_EXTERNAL}/auth/callback`, + checkStateFunction: (state, callback) => { + if (fastify.states.has(state)) { + callback(); + return; + } + callback(new Error('Invalid state')); + }, + credentials: { + auth: oauth.DISCORD_CONFIGURATION, + client: { + id: client.user.id, + secret: process.env.DISCORD_SECRET, + }, + }, + generateStateFunction: req => { + const state = randomBytes(12).toString('hex'); + fastify.states.set(state, req.query.r); + return state; + }, + name: 'discord', + scope: ['applications.commands.permissions.update', 'guilds', 'identify'], + startRedirectPath: '/auth/login', + }); + + // cookies plugin + fastify.register(require('@fastify/cookie')); + + // jwt plugin + fastify.register(require('@fastify/jwt'), { + cookie: { + cookieName: 'token', + signed: false, + }, + secret: process.env.ENCRYPTION_KEY, + }); + + // auth + fastify.decorate('authenticate', async (req, res) => { + try { + const data = await req.jwtVerify(); + if (data.expiresAt < Date.now()) throw 'expired'; + if (data.createdAt < new Date(process.env.INVALIDATE_TOKENS).getTime()) throw 'expired'; + } catch (error) { + return res.code(401).send({ + error: 'Unauthorised', + message: error === 'expired' ? 'Your token has expired; please re-authenticate.' : 'You are not authenticated.', + statusCode: 401, + }); + } + }); + + fastify.decorate('isAdmin', async (req, res) => { + try { + const userId = req.user.id; + const guildId = req.params.guild; + const guild = client.guilds.cache.get(guildId); + if (!guild) { + return res.code(404).send({ + error: 'Not Found', + message: 'The requested resource could not be found.', + statusCode: 404, + + }); + } + const guildMember = await guild.members.fetch(userId); + const isAdmin = guildMember?.permissions.has(PermissionsBitField.Flags.ManageGuild) || client.supers.includes(userId); + if (!isAdmin) { + return res.code(403).send({ + error: 'Forbidden', + message: 'You are not permitted for this action.', + statusCode: 403, + + }); + } + } catch (err) { + res.send(err); + } + }); + + // body processing + fastify.addHook('preHandler', (req, res, done) => { + if (req.body && typeof req.body === 'object') { + for (const prop in req.body) { + if (typeof req.body[prop] === 'string') { + req.body[prop] = req.body[prop].trim(); + } + } + } + done(); + }); + + // logging + fastify.addHook('onResponse', (req, res, done) => { + done(); + const status = (res.statusCode >= 500 + ? '&4' + : res.statusCode >= 400 + ? '&6' + : res.statusCode >= 300 + ? '&3' + : res.statusCode >= 200 + ? '&2' + : '&f') + res.statusCode; + let responseTime = res.getResponseTime().toFixed(2); + responseTime = (responseTime >= 100 + ? '&c' + : responseTime >= 10 + ? '&e' + : '&a') + responseTime + 'ms'; + const level = req.routerPath === '/*' ? 'verbose' : 'info'; + client.log[level].http(short(`${req.id} ${req.ip} ${req.method} ${req.routerPath ?? '*'} &m-+>&r ${status}&b in ${responseTime}`)); + if (!req.routerPath) client.log.verbose.http(`${req.id} ${req.method} ${req.url}`); + done(); + }); + + fastify.addHook('onError', async (req, res, err) => client.log.error.http(req.id, err)); + + // route loading + const dir = join(__dirname, '/routes'); + files(dir, { + exclude: /^\./, + match: /.js$/, + sync: true, + }).forEach(file => { + const path = file + .substring(0, file.length - 3) // remove `.js` + .substring(dir.length) // remove higher directories + .replace(/\\/g, '/') // replace `\` with `/` because Windows is stupid + .replace(/\[(\w+)\]/gi, ':$1') // convert [] to : + .replace('/index', '') || '/'; // remove index + const route = require(file); + + Object.keys(route).forEach(method => fastify.route({ + config: { client }, + method: method.toUpperCase(), + path, + ...route[method](fastify), + })); // register route + }); + + const { handler } = await import('@discord-tickets/settings/build/handler.js'); + + // https://stackoverflow.com/questions/72317071/how-to-set-up-fastify-correctly-so-that-sveltekit-works-fine + fastify.all('/*', {}, (req, res) => handler(req.raw, res.raw, () => { })); + + // start the fastify server + fastify.listen({ + host: process.env.HTTP_HOST, + port: process.env.HTTP_PORT, + }, (err, addr) => { + if (err) client.log.error.http(err); + else client.log.success.http(`Listening at ${addr}`); + }); + + process.on('sveltekit:error', ({ + error, + errorId, + }) => { + client.log.error.http(`SvelteKit ${errorId} ${error}`); + }); +}; diff --git a/src/i18n/cs.yml b/src/i18n/cs.yml new file mode 100644 index 0000000..9eaa289 --- /dev/null +++ b/src/i18n/cs.yml @@ -0,0 +1,231 @@ +commands: + message: + pin: + name: Připnout zprávu + pinned: + description: Zpráva byla připnuta. + title: ✅ Zpráva připnuta + not_pinnable: + description: "Tato zpráva nemůže být připnuta.\nProsím zeptejte se admininistrátora,\ + \ jestli má robot dostatečné oprávnění.\n" + title: ❌ Chyba + not_ticket: + description: Zprávy můžeš připnout jen v ticketech. + title: ❌ Tento kanál není ticket + create: + name: Vytvořit ticket ze zprávy + slash: + add: + name: přidat + options: + ticket: + name: tiket + description: Ticket na přidání člena + member: + name: člen + description: Člen, kterého chcete přidat do ticketu + added: ➡️{by} přidal {added}. + description: Přidat člena do ticketu + not_staff: + description: Pouze členové staff teamu můžou přidávat členy do ticketů ostatních. + title: ❌ Chyba + success: + description: '{member} byl přidán do {ticket}.' + title: ✅ Přidán + claim: + name: převzít + description: Převzít ticket + close: + invalid_time: + title: ❌ Neplatný + description: '`{input}` není platný formát času.' + description: Zažádat o uzavření ticketu + name: uzavřít + options: + reason: + description: Důvod pro uzavření ticketu + name: důvod + remove: + options: + ticket: + name: tiket + description: Tiket pro odebrání člena + member: + description: Člen, který má být odstraněn z tiketu + name: člen + not_staff: + description: Pouze členové staff teamu mohou odstranit členy z ostatních tiketů. + title: ❌ Chyba + success: + description: '{member} byl odstraněn z {ticket}.' + title: ✅ Odstraněn + description: Odstranit člena z tiketu + name: odstranit + removed: ⬅️ {removed} byl odstraněn uživatelem {by}. + help: + response: + links: + feedback: Zpětná vazba + commands: Celý list příkazů + docs: Dokumentace + links: Užitečné odkazy + support: Podpora + commands: Příkazy + settings: Nastavení bota + description: '**Použijte {command} pro vytvoření ticketu a získání podpory.**' + description: Zobrazit pomocné menu + name: pomoc + title: Pomoc + force-close: + options: + category: + name: kategorie + description: Uzavřít všechny tickety v zadané kategorii (lze použít s `time`). + reason: + description: Důvod pro uzavření ticketu(ů) + name: důvod + ticket: + description: Ticket k uzavření + name: tiket + time: + name: čas + description: Zavřít všechny tickety, které byly po zadanou dobu neaktivní + (lze použít s `category`). + confirm_multiple: + title: ❓Jste si jist? + description: "Chystáte se uzavřít **{count}** ticketů, které jsou neaktivní\ + \ déle než `{time}`:\n{tickets}\n" + name: uzavřít-silou + description: Násilně uzavřít tiketu + no_tickets: + title: ❌Žádné tikety + description: Neexistují žádné otevřené tickety, které by byly neaktivní déle + než `{time}`. + not_staff: + title: ❌Chyba + description: Tickety mohou násilně uzavírat pouze zaměstnanci. + move: + description: Přesunout ticket do jiné kategorie + name: přesunout + options: + category: + description: Kategorie, do které se má ticket přesunout + name: kategorie + moved: 🗃️ {by} přesunul tento ticket z **{from}** do **{to}**. + new: + description: Vytvořit nový ticket + name: nový + options: + references: + description: Číslo souvisejícího tiketu + name: reference + priority: + description: Nastavit důležitost ticketu + name: důležitost + options: + priority: + choices: + HIGH: 🔴 Vysoká + LOW: 🟢 Malá + MEDIUM: 🟠 Střední + description: Priorita tiketu + name: priorita + success: + description: Priorita tiketu byla nastavena na `{priority}`. + title: ✅ Priorita nastavena + release: + name: uvolnit + description: Uvolnit (vrátit) ticket + tag: + description: Použijte tag + name: tag + options: + for: + description: Uživatel, na kterého má cílit tag + name: pro + tag: + description: Jméno tagu pro použití + name: tag + tickets: + name: tickety + response: + fields: + closed: + name: Uzavřené tickety + open: + name: Otevřít tickety + title: + other: Tickety uživatele {displayName} + not_staff: + title: ❌ Chyba + topic: + name: téma +buttons: + accept_close_request: + emoji: ✅ + text: Přijmout + create: + text: Vytvořit ticket + emoji: 🎫 + cancel: + emoji: ✖️ + text: Zrušit + claim: + emoji: 🙌 + text: Převzít + edit: + emoji: ✏️ + text: Upravit + close: + emoji: ✖️ + text: Zavřít + confirm_open: + emoji: ✅ + text: Vytvořit ticket + reject_close_request: + emoji: ✖️ + text: Odmítnut + unclaim: + emoji: ♻️ + text: Vydat +dm: + closed: + archived: Chcete-li zobrazit archivované zprávy, zadejte `/transcript` do **{guild}**. + title: Váš ticket byl uzavřen + fields: + closed_by: Zavřeno uživatelem + ticket: Ticket + closed: + name: Zavřeno v + value: '{timestamp} (po {duration})' + created: Vytvořeno v + feedback: Vaše zpětná vazba + reason: Zavřeno, protože + response: Doba odezvy + topic: Téma + confirm_open: + title: Chcete otevřít ticket s následujícím tématem? +log: + admin: + description: + target: + tag: značka + settings: nastavení + category: kategorie + panel: panel + question: otázka + joined: '{user} {verb} {targetType}' + verb: + create: vytvořeno + title: + target: + panel: Panel + category: Kategorie + question: Otázka + settings: Nastavení + tag: Štítek + joined: '{targetType} {verb}' + changes: Změny +misc: + error: + title: ⚠️ Něco se pokazilo diff --git a/src/i18n/de.yml b/src/i18n/de.yml new file mode 100644 index 0000000..5831a22 --- /dev/null +++ b/src/i18n/de.yml @@ -0,0 +1,405 @@ +buttons: + cancel: + emoji: ✖️ + text: Abbrechen + close: + emoji: ✖️ + text: Schließen + create: + text: Ticket erstellen + emoji: 🎫 + accept_close_request: + emoji: ✅ + text: Akzeptieren + claim: + emoji: 🙌 + text: Übernehmen + confirm_open: + emoji: ✅ + text: Ticket erstellen + reject_close_request: + emoji: ✖️ + text: Ablehnen + edit: + emoji: ✏️ + text: Bearbeiten + unclaim: + text: Freigeben + emoji: ♻️ +commands: + message: + pin: + name: Nachricht anheften + not_pinnable: + description: "Diese Nachricht kann nicht angeheftet werden.\nBitte einen Administrator,\ + \ die Berechtigungen des Bots zu überprüfen.\n" + title: ❌ Fehler + not_ticket: + description: Du kannst Nachrichten nur in Tickets anheften. + title: ❌ Dies ist kein Ticketkanal + pinned: + description: Die Nachricht wurde angeheftet. + title: ✅ Nachricht angeheftet + create: + name: Ticket aus Nachricht erstellen + slash: + add: + added: ➡️ {added} wurde von {by} hinzugefügt. + not_staff: + title: ❌ Fehler + description: Nur Teammitglieder können Benutzer zu Tickets anderer hinzufügen. + options: + member: + name: mitglied + description: Das Mitglied, das dem Ticket hinzugefügt werden soll + ticket: + name: ticket + description: Das Ticket, zu dem das Mitglied hinzugefügt werden soll + success: + description: '{member} wurde zu {ticket} hinzugefügt.' + title: ✅ Hinzugefügt + description: Benutzer zu einem Ticket hinzufügen + name: hinzufügen + help: + response: + links: + support: Unterstützung + links: Nützliche Links + docs: Dokumentation + commands: Vollständige Befehlsliste + feedback: Feedback + commands: Befehle + description: '**Verwende {command} um ein Ticket zu erstellen und Hilfe zu + erhalten.**' + settings: Bot-Einstellungen + name: hilfe + title: hilfe + description: Zeigt das Hilfemenü an + force-close: + options: + reason: + name: grund + description: Der Grund für das Schließen des/der Tickets + category: + name: kategorie + description: Schließe alle Tickets einer Kategorie (kann mit `time` benutzt + werden) + time: + name: zeit + description: Alle Tickets schließen, die für die angegebene Zeit inaktiv + waren (mit `Kategorie` verwendbar) + ticket: + description: Das Ticket welches geschlossen werden soll + name: ticket + no_tickets: + description: Es gibt keine offenen Tickets, die länger als `{time}` inaktiv + waren. + title: ❌ Keine Tickets + not_staff: + title: ❌ Fehler + description: Nur Teammitglieder können Tickets zwangsweise abschliessen. + confirm_multiple: + description: "Du bist dabei **{count}** Tickets zu schließen die länger als\ + \ `{time}`:\n{tickets} inaktiv waren.\n" + title: ❓Bist du dir sicher? + description: Ticket zwangsweise schließen + name: force-close + close: + invalid_time: + title: ❌ Ungültig + description: '`{input}` ist kein gültiges Zeitformat.' + options: + reason: + name: grund + description: Der Grund für das Schließen des Tickets + description: Beantragen, dass ein Ticket geschlossen wird + name: schließen + move: + name: verschieben + description: Verschiebe ein Ticket in eine andere Kategorie + moved: 🗃️ {by} hat dieses Ticket von **{from}** nach **{to}** verschoben. + options: + category: + description: Die Kategorie in welche das Ticket verschoben werden soll + name: kategorie + not_staff: + description: Nur Mitarbeiter können Tickets verschieben. + title: ❌ Fehler + new: + name: neu + description: Neues Ticket erstellen + options: + references: + name: verweise + description: Die Nummer eines zugehörigen Tickets + claim: + description: Ticket übernehmen + name: übernehmen + not_staff: + title: ❌ Fehler + priority: + options: + priority: + description: Die Priorität des Tickets + name: priorität + choices: + HIGH: 🔴 Hoch + LOW: 🟢 Niedrig + MEDIUM: 🟠 Mittel + success: + title: ✅ Priorität gesetzt + description: Die Priorität dieses Tickets wurde auf `{priority}` gesetzt. + name: priorität + description: Legt die Priorität eines Tickets fest + not_staff: + title: ❌ Fehler + tickets: + not_staff: + title: ❌ Fehler + description: Nur Teammitglieder können Tickets anderer einsehen. + options: + member: + description: Das Mitglied, dessen Tickets aufgelistet werden sollen + name: mitglied + name: tickets + response: + title: + own: Deine Tickets + other: '{displayName}s Tickets' + fields: + closed: + none: + own: "Du hast noch keine Tickets erstellt\nBenutze {new} um ein Ticket\ + \ zu öffnen.\n" + other: '{user} hat noch keine Tickets erstellt.' + name: Geschlossene Tickets + open: + name: Offene Tickets + description: Verwende {transcript}, um die Abschrift eines Tickets herunterzuladen. + description: Eigene oder fremde Tickets auflisten + remove: + not_staff: + description: Nur Mitarbeiter können Mitglieder aus den Tickets anderer entfernen. + title: ❌ Fehler + options: + member: + name: mitglied + description: Das Mitglied, das aus dem Ticket entfernt werden soll + ticket: + name: ticket + description: Das Ticket, aus dem das Mitglied entfernt werden soll + success: + title: ✅ Entfernt + description: '{member} wurde von {ticket} entfernt.' + description: Entfernt ein Mitglied aus einem Ticket + name: entfernen + removed: ⬅️ {removed} wurde von {by} entfernt. + topic: + description: Thema eines Tickets ändern + name: thema + transcript: + name: abschrift + options: + ticket: + name: ticket + description: Die Nummer des Tickets von welchem die Abschrift erstellt werden + soll + description: Abschrift eines Tickets erhalten + transfer: + name: übertragen + transferred_from: 📨 {user} hat dieses Ticket von {from} auf {to} übertragen. + options: + member: + name: mitglied + description: Das Mitglied, an das die Eigentümerschaft übertragen werden + soll + transferred: 📨 {user} hat dieses Ticket an {to} übertragen. + description: Ein Ticket an ein anderes Mitglied übertragen + tag: + options: + for: + name: für + description: Der Benutzer, an den das Tag gerichtet werden soll + tag: + name: tag + description: Der Name des zu verwendenden Tags + name: tag + description: Verwende einen Tag + release: + description: Ein Ticket freigeben (unclaim) + name: freigeben + user: + create: + name: Erstelle Ticket für Benutzer +log: + admin: + changes: Änderungen + description: + target: + question: eine Frage + settings: Einstellungen + category: eine Kategorie + panel: ein Panel + tag: ein Tag + joined: '{user} {verb} {targetType}' + title: + target: + category: Kategorie + panel: Panel + settings: Einstellungen + question: Frage + tag: Tag + joined: '{targetType} {verb}' + verb: + delete: gelöscht + create: erstellt + update: Aktualisiert + message: + description: '{user} {verb} eine Nachricht' + verb: + delete: gelöscht + update: Aktualisiert + message: Nachricht + title: Nachricht {verb} + ticket: + title: Ticket {verb} + description: '{user} {verb} ein Ticket' + ticket: Ticket + verb: + claim: übernommen + close: Geschlossen + update: Aktualisiert + create: erstellt + unclaim: freigegeben + added: Mitglieder hinzugefügt + removed: Entfernte Mitglieder +menus: + category: + placeholder: Wähle eine Ticketkategorie + guild: + placeholder: Wähle einen Server +misc: + blocked: + title: ❌ Gesperrt + description: Du darfst keine Tickets erstellen. + cooldown: + description: Bitte warte {time}, bevor du ein weiteres Ticket in dieser Kategorie + erstellst. + title: ❌Bitte warten + error: + description: "Entschuldigung, ein unerwarteter Fehler ist aufgetreten.\nBitte\ + \ gib diese Informationen an einen Administrator weiter.\n" + fields: + code: Fehlercode + identifier: Kennung + title: ⚠️ Etwas ist schief gelaufen + expired: + description: Du hast nicht rechtzeitig geantwortet. Bitte versuche es erneut. + title: ⏰ Abgelaufen + expires_in: Läuft in {time} ab + invalid_ticket: + description: Bitte gib ein gültiges Ticket an. + title: ❌ Ungültiges Ticket + member_limit: + description: + - Bitte verwende dein vorhandenes Ticket oder schließe es ab, bevor du ein neues + erstellst. + - "Bitte schließe ein Ticket, bevor du ein neues erstellst.\nVerwende `/Tickets`,\ + \ um deine bestehenden Tickets anzuzeigen.\n" + title: + - ❌ Du hast bereits ein Ticket + - ❌ Du hast bereits %d offene Tickets + not_ticket: + title: ❌ Dies ist kein Ticketkanal + description: Du kannst diesen Befehl nur in Tickets verwenden. + ratelimited: + description: Versuche es in ein paar Sekunden erneut. + title: 🐢 Bitte langsamer + unknown_category: + description: Bitte versuche es mit einer anderen Kategorie. + title: ❌ Diese Ticketkategorie existiert nicht + no_categories: + title: ❌ Es gibt keine Ticketkategorien + description: Es wurden keine Ticketkategorien konfiguriert. + missing_roles: + description: Du hast nicht die erforderlichen Rollen, um ein Ticket in dieser + Kategorie erstellen zu können. + title: ❌ Unzureichende Rollen + category_full: + description: "Die Kategorie hat ihre maximale Kapazität erreicht.\nBitte versuche\ + \ es später erneut.\n" + title: ❌ Kategorie voll +modals: + feedback: + comment: + placeholder: Hast du zusätzliches Feedback? + label: Kommentar + rating: + label: Bewertung + placeholder: 1-5 + title: Wie haben wir das gemacht? + topic: + label: Thema + placeholder: Worum geht es in diesem Ticket? +ticket: + answers: + no_value: '*Keine Antwort*' + close: + closed: + description: Dieser Kanal wird in wenigen Sekunden gelöscht… + title: ✅ Ticket geschlossen + forbidden: + title: ❌ Fehler + description: Du bist nicht berechtigt, dieses Ticket zu schließen. + rejected: ✋ {user} hat eine Anfrage zum Schließen dieses Tickets abgelehnt. + staff_request: + title: ❓ Kann dieses Ticket geschlossen werden? + archived: "\nDie Nachrichten in diesem Kanal werden zum späteren Nachschlagen\ + \ archiviert.\n" + description: "{requestedBy} möchte dieses Ticket schließen.\nKlicke auf \"Akzeptieren\"\ + , um es jetzt zu schließen, oder auf \"Ablehnen\", wenn du noch Hilfe benötigst.\n" + user_request: + title: ❓ {requestedBy} möchte dieses Ticket schließen + wait_for_user: ✋ Bitte warte bis der Benutzer antwortet. + wait_for_staff: ✋ Bitte wartee, bis das Personal dieses Ticket schließt. + created: + title: ✅ Ticket erstellt + description: 'Dein Ticketkanal wurde erstellt: {channel}.' + edited: + description: Deine Änderungen wurden gespeichert. + title: ✅ Ticket aktualisiert + feedback: Danke für deine Rückmeldung. + opening_message: + content: "{staff}\n{creator} hat ein neues Ticket erstellt\n" + fields: + topic: Thema + references_message: + title: ℹ️ Referenz + description: Verweist auf [eine Nachricht]({url}), die {timestamp} von {author} + gesendet wurde. + references_ticket: + description: 'Dieses Ticket bezieht sich auf ein vorheriges Ticket:' + fields: + date: Erstellt am + number: Nummer + topic: Thema + title: ℹ️ Referenz + claimed: 🙌 {user} hat dieses Ticket übernommen. + released: ♻️ {user} hat das ticket freigegeben. +dm: + closed: + title: Dein Ticket wurde geschlossen + fields: + closed: + name: Geschlossen um + value: '{timestamp} (nach {duration})' + closed_by: Geschlossen durch + created: Erstellt um + feedback: Deine Rückmeldung + reason: Grund des Schließens + response: Reaktionszeit + ticket: Ticket + topic: Thema + archived: Benutze `/transcript` in **{guild}** um archivierte Nachrichten einzusehen. + confirm_open: + title: Möchtest du ein Ticket mit dem folgendem Thema eröffnen? diff --git a/src/i18n/el.yml b/src/i18n/el.yml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/src/i18n/el.yml @@ -0,0 +1 @@ +{} diff --git a/src/i18n/en-GB.yml b/src/i18n/en-GB.yml new file mode 100644 index 0000000..803f574 --- /dev/null +++ b/src/i18n/en-GB.yml @@ -0,0 +1,467 @@ +buttons: + accept_close_request: + emoji: ✅ + text: Accept + cancel: + emoji: ➖ + text: Cancel + claim: + emoji: 🙌 + text: Claim + close: + emoji: ✖️ + text: Close + confirm_open: + emoji: ✅ + text: Create ticket + create: + emoji: 🎫 + text: Create a ticket + edit: + emoji: ✏️ + text: Edit + reject_close_request: + emoji: ✖️ + text: Reject + unclaim: + emoji: ♻️ + text: Release +commands: + message: + create: + name: Create ticket from message + pin: + name: Pin message + not_pinnable: + description: | + This message can't be pinned. + Please ask an administrator to check the bot's permissions. + title: ❌ Error + not_ticket: + description: You can only pin messages in tickets. + title: ❌ This isn't a ticket channel + pinned: + description: The message has been pinned. + title: ✅ Pinned message + slash: + add: + added: ➡️ {added} has been added by {by}. + description: Add a member to a ticket + name: add + not_staff: + description: Only staff members can add members to others' tickets. + title: ❌ Error + options: + member: + description: The member to add to the ticket + name: member + ticket: + description: The ticket to add the member to + name: ticket + success: + description: "{member} has been added to {ticket}." + title: ✅ Added + claim: + description: Claim a ticket + name: claim + not_staff: + description: Only staff members can claim tickets. + title: ❌ Error + close: + description: Request a ticket to be closed + invalid_time: + description: "`{input}` is not a valid time format." + title: ❌ Invalid + name: close + options: + reason: + description: The reason for closing the ticket + name: reason + force-close: + closed_one: + description: The channel will be deleted in a few seconds. + title: ✅ Ticket closed + confirm_multiple: + description: > + You are about to close **{count}** tickets that have been inactive + for more than `{time}`: + + {tickets} + title: ❓ Are you sure? + confirmed_multiple: + description: The channels will be deleted in a few seconds. + title: + - ✅ Closing %d ticket + - ✅ Closing %d tickets + description: Forcibly close a ticket + name: force-close + no_tickets: + description: >- + There are no open tickets that have been inactive for more than + `{time}`. + title: ❌ No tickets + not_staff: + description: Only staff members can force-close tickets. + title: ❌ Error + options: + category: + description: + Close all tickets in the specified category (must be used with + `time`) + name: category + reason: + description: The reason for closing the ticket(s) + name: reason + ticket: + description: The ticket to close + name: ticket + time: + description: Close all tickets that have been inactive for the specified time + name: time + help: + description: Show the help menu + name: help + response: + commands: Commands + description: "**Use {command} to create a ticket and get support.**" + links: + commands: Full command list + docs: Documentation + feedback: Feedback + links: Useful links + support: Support + settings: Bot settings + title: Help + move: + description: Move a ticket to another category + moved: 🗃️ {by} has moved this ticket from **{from}** to **{to}**. + name: move + not_staff: + description: Only staff members can move tickets. + title: ❌ Error + options: + category: + description: The category to move the ticket to + name: category + new: + description: Create a new ticket + name: new + options: + references: + description: The number of a related ticket + name: references + priority: + description: Set the priority of a ticket + name: priority + not_staff: + description: Only staff members can change the priority of tickets. + title: ❌ Error + options: + priority: + choices: + HIGH: 🔴 High + LOW: 🟢 Low + MEDIUM: 🟠 Medium + description: The priority of the ticket + name: priority + success: + description: This ticket's priority has been set to `{priority}`. + title: ✅ Priority set + release: + description: Release (unclaim) a ticket + name: release + remove: + description: Remove a member from a ticket + name: remove + not_staff: + description: Only staff members can remove members from others' tickets. + title: ❌ Error + options: + member: + description: The member to remove from the ticket + name: member + ticket: + description: The ticket to remove the member from + name: ticket + removed: ⬅️ {removed} has been removed by {by}. + success: + description: "{member} has been removed from {ticket}." + title: ✅ Removed + tag: + description: Use a tag + name: tag + options: + for: + description: The user to target the tag to + name: for + tag: + description: The name of the tag to use + name: tag + tickets: + description: List your own or someone else's tickets + name: tickets + not_staff: + description: Only staff members can view others' tickets. + title: ❌ Error + options: + member: + description: The member to list the tickets of + name: member + response: + description: Use {transcript} to download the transcript of a ticket. + fields: + closed: + name: Closed tickets + none: + other: "{user} hasn't made any tickets." + own: | + You haven't made any tickets. + Use {new} to open a ticket. + open: + name: Open tickets + title: + other: "{displayName}'s tickets" + own: Your tickets + topic: + description: Change the topic of a ticket + name: topic + transcript: + description: Get the transcript of a ticket + name: transcript + options: + member: + description: The member to search for tickets of + name: member + ticket: + description: The ticket to get the transcript of + name: ticket + transfer: + description: Transfer ownership of a ticket to another member + name: transfer + options: + member: + description: The member to transfer ownership to + name: member + transferred: 📨 {user} has transferred this ticket to {to}. + transferred_from: 📨 {user} has transferred this ticket from {from} to {to}. + user: + create: + name: Create ticket for user + not_staff: + description: Only staff members can open tickets for other members. + title: ❌ Error + prompt: + description: Click the button below to create a ticket. + title: Please create a ticket + sent: + description: "{user} has been invited to create a ticket in **{category}**." + title: ✅ Prompt sent +dm: + closed: + archived: Use the `/transcript` command in **{guild}** to view the archived messages. + fields: + closed: + name: Closed at + value: "{timestamp} (after {duration})" + closed_by: Closed by + created: Created at + feedback: Your feedback + reason: Closed because + response: Response time + ticket: Ticket + topic: Topic + title: Your ticket has been closed + confirm_open: + title: Do you want to open a ticket with the following topic? +log: + admin: + changes: Changes + description: + joined: "{user} {verb} {targetType}" + target: + category: a category + panel: a panel + question: a question + settings: the settings + tag: a tag + title: + joined: "{targetType} {verb}" + target: + category: Category + panel: Panel + question: Question + settings: Settings + tag: Tag + verb: + create: created + delete: deleted + update: updated + message: + description: "{user} {verb} a message" + message: Message + title: Message {verb} + verb: + delete: deleted + update: updated + ticket: + added: Added members + description: "{user} {verb} a ticket" + removed: Removed members + ticket: Ticket + title: Ticket {verb} + verb: + claim: claimed + close: closed + create: created + unclaim: released + update: updated +menus: + category: + placeholder: Select a ticket category + guild: + placeholder: Select a server +misc: + blocked: + description: You are not allowed to create tickets. + title: ❌ Blocked + category_full: + description: | + The category has reached its maximum capacity. + Please try again later. + title: ❌ Category full + cooldown: + description: Please wait {time} before creating another ticket in this category. + title: ❌ Please wait + error: + description: | + Sorry, an unexpected error occurred. + Please give this information to an administrator. + fields: + code: Error code + identifier: Identifier + title: ⚠️ Something went wrong + expired: + description: You didn't respond in time. Please try again. + title: ⏰ Expired + expires_in: Expires in {time} + invalid_ticket: + description: Please specify a valid ticket. + title: ❌ Invalid ticket + member_limit: + description: + - Please use your existing ticket or close it before creating another. + - | + Please close a ticket before creating another. + Use `/tickets` to view your existing tickets. + title: + - ❌ You already have a ticket + - ❌ You already have %d open tickets + missing_roles: + description: >- + You do not have the roles required to be able to create a ticket in + this category. + title: ❌ Insufficient roles + no_categories: + description: No ticket categories have been configured. + title: ❌ There are no ticket categories + not_ticket: + description: You can only use this command in tickets. + title: ❌ This isn't a ticket channel + ratelimited: + description: Try again in a few seconds. + title: 🐢 Please slow down + unknown_category: + description: Please try a different category. + title: ❌ That ticket category doesn't exist + update: + description: | + > [View `{version}` on GitHub]({github}) + > [Changelog]({changelog}) + > [Update guide]({guide}) + title: An update is available +modals: + feedback: + comment: + label: Comment + placeholder: Do you have any additional feedback? + rating: + label: Rating + placeholder: 1-5 + title: How did we do? + topic: + label: Topic + placeholder: What is this ticket about? +ticket: + answers: + no_value: "*No response*" + claimed: 🙌 {user} has claimed this ticket. + close: + closed: + description: This channel will be deleted in a few seconds… + title: ✅ Ticket closed + forbidden: + description: You don't have permission to close this ticket. + title: ❌ Error + rejected: ✋ {user} rejected a request to close this ticket. + staff_request: + archived: | + + The messages in this channel will be archived for future reference. + description: | + {requestedBy} wants to close this ticket. + Click "Accept" to close it now, or "Reject" if you still need help. + title: ❓ Can this ticket be closed? + user_request: + title: ❓ {requestedBy} wants to close this ticket + wait_for_staff: ✋ Please wait for staff to close this ticket. + wait_for_user: ✋ Please wait for the user to respond. + closing_soon: + description: | + This ticket will be closed due to inactivity . + Send a message to cancel this automation. + title: ⌛ This ticket will be closed soon + created: + description: "Your ticket channel has been created: {channel}." + title: ✅ Ticket created + edited: + description: Your changes have been saved. + title: ✅ Ticket updated + feedback: Thank you for your feedback. + inactive: + description: | + There hasn't been any activity in this channel since . + Please continue the conversation or {close} the ticket. + title: ⏰ This ticket is inactive + offline: + description: + There aren't any staff members available at the moment, so it may + take longer than usual to get a response. + title: 😴 We're not online + opening_message: + content: | + {staff} + {creator} has created a new ticket + fields: + topic: Topic + references_message: + description: References [a message]({url}) sent {timestamp} by {author}. + title: ℹ️ Reference + references_ticket: + description: "This ticket is related to a previous ticket:" + fields: + date: Created at + number: Number + topic: Topic + title: ℹ️ Reference + released: ♻️ {user} has released this ticket. + working_hours: + next: + description: + We'll be back at (), although + you may receive a response before then. + title: 🕗 We're not working at the moment + today: + description: + You may receive a response before, but we don't start working until + today (). + title: 🕗 We're not working at the moment diff --git a/src/i18n/fi.yml b/src/i18n/fi.yml new file mode 100644 index 0000000..47af9f7 --- /dev/null +++ b/src/i18n/fi.yml @@ -0,0 +1,164 @@ +commands: + message: + pin: + not_ticket: + title: ❌ Tämä ei ole tiketti kanava + description: Voit kiinnittää viestejä vain tiketeissä. + name: Kiinnitä viesti + not_pinnable: + title: ❌ Virhe + pinned: + description: Viesti on kiinnitetty. + title: ✅ Kiinnitetty viesti + create: + name: Luo tiketti viestistä + slash: + add: + description: Lisää jäsen tikettiin + name: lisää + options: + ticket: + name: tiketti + description: Tiketti johon jäsen lisätään + member: + name: jäsen + description: Jäsen joka lisätään tikettiin + not_staff: + description: Vain henkilökunta voi lisätä toisia tiketteihin. + title: ❌ Virhe + success: + title: ✅ Lisätty + description: '{member} lisättiin {ticket}' + force-close: + not_staff: + title: ❌ Virhe + no_tickets: + title: ❌ Ei tikettejä + options: + category: + name: kategoria + reason: + name: syy + ticket: + name: tiketti + time: + name: aika + confirm_multiple: + title: ❓ Oletko varma? + help: + description: Näyttää apua valikon + response: + links: + feedback: Palaute + links: Hyödyllisiä linkkejä + support: Tuki + commands: Koko komento lista + settings: Botin asetukset + commands: Komennot + title: Apua + name: apua + move: + name: siirrä + options: + category: + name: kategoria + description: Siirrä tiketti toiseen kategoriaan + remove: + options: + member: + description: Jäsen joka poistetaan tiketistä + name: jäsen + ticket: + name: tiketti + name: poista + not_staff: + title: ❌ Virhe + success: + title: ✅ Poistettu + description: Poista jäsen tiketistä + close: + invalid_time: + title: ❌ Virheellinen + name: sulje + options: + reason: + name: syy + description: Syy tiketin(t) sulkemiseen + description: Pyydä tiketin sulkemista + claim: + description: Varaa tiketti + name: varaa + new: + description: Luo uusi tiketti + name: uusi + priority: + options: + priority: + choices: + LOW: 🟢 Hidas + HIGH: 🔴 Kiireellinen + MEDIUM: 🟠 Normaali + topic: + name: aihe + tickets: + not_staff: + title: ❌ Virhe + response: + title: + other: '{displayName} tiketit' + own: Sinun tiketit + fields: + closed: + name: Suljetut tiketit + name: tiketit + options: + member: + name: jäsen + transfer: + options: + member: + name: jäsen + name: siirrä + transcript: + options: + ticket: + name: tiketti + release: + name: julkaise +buttons: + close: + text: Sulje + emoji: ✖️ + edit: + text: Muokkaa + emoji: ✏️ + reject_close_request: + text: Hylkää + emoji: ✖️ + unclaim: + text: Julkaise + emoji: ♻️ + accept_close_request: + text: Hyväksy + emoji: ✅ + cancel: + text: Peruuta + emoji: ✖️ + confirm_open: + text: Luo tiketti + emoji: ✅ + create: + emoji: 🎫 + text: Luo tiketti + claim: + text: Varaa + emoji: 🙌 +ticket: + opening_message: + fields: + topic: Aihe + references_ticket: + fields: + topic: Aihe + number: Numero + feedback: Kiitos palautteestasi. diff --git a/src/i18n/fr.yml b/src/i18n/fr.yml new file mode 100644 index 0000000..1d0fc1d --- /dev/null +++ b/src/i18n/fr.yml @@ -0,0 +1,410 @@ +buttons: + accept_close_request: + text: Accepter + emoji: ✅ + cancel: + emoji: ✖️ + text: Annuler + claim: + emoji: 🙌 + text: S'approprier + close: + text: Fermer + emoji: ✖️ + reject_close_request: + emoji: ✖️ + text: Rejeter + confirm_open: + emoji: ✅ + text: Créer un ticket + create: + text: Créer un ticket + emoji: 🎫 + edit: + emoji: ✏️ + text: Modifier + unclaim: + emoji: ♻️ + text: Relâcher +commands: + message: + pin: + name: Épingler le message + not_pinnable: + title: ❌ Erreur + description: "Ce message ne peut être épinglé.\nVeuillez demander à un administrateur\ + \ de vérifier les permissions du bot\n" + pinned: + title: ✅ Message épinglé + description: Les message à été épinglé. + not_ticket: + title: ❌ Ceci n'est pas un salon de ticket + description: Vous pouvez seulement épingler des messages dans les tickets. + create: + name: Transformer ce message en ticket + slash: + add: + not_staff: + title: ❌ Erreur + description: Seul les membres de l'équipe peuvent ajouter des membres aux + tickets des autres. + success: + title: ✅ Ajouté + description: '{member} à été ajouté au ticket {ticket}.' + added: '{added} à été ajouté par {by}.' + description: Ajouter un membre à un ticket + name: ajouter + options: + member: + description: Le membre à ajouter au ticket + name: membre + ticket: + description: Le ticket auquel ajouter le membre + name: ticket + close: + invalid_time: + title: ❌ Invalide + description: "`{input}` n'est pas un format de date valide." + options: + reason: + description: La raison de la clôture du ticket + name: raison + description: Demander la fermeture d'un ticket + name: fermer + force-close: + options: + ticket: + name: ticket + description: le ticket à clôturer + category: + description: Fermer tout les tickets dans la catégorie spécifiée (peut être + utilisée avec `time`) + name: catégorie + time: + description: Clôturer tout les tickets inactifs depuis le temps spécifié + (peut être utilisé avec `category`) + name: temps + reason: + name: raison + description: La raison de la fermeture du/des ticket(s) + description: Forcer la clôture d'un ticket + name: fermeture-forcée + no_tickets: + description: Il n'y as pas de tickets ouverts inactifs depuis plus de `{time}`. + title: ❌ Aucun ticket + not_staff: + description: Seuls les membres de l'équipe peuvent forcer la fermeture de + tickets. + title: ❌ Erreur + confirm_multiple: + description: "Vous êtes sur le point de clôturer **{count}** tickets inactifs\ + \ depuis plus de `{time}` :\n{tickets}\n" + title: ❓Êtes vous sûr ? + claim: + description: S'approprier un ticket + name: claim + not_staff: + description: Seul les membres de l'équipe peuvent s'approprier un ticket. + title: ❌ Erreur + help: + name: aide + response: + links: + commands: Liste complète des commandes + docs: Documentation + support: Support + links: Liens utiles + feedback: Retour + commands: Commandes + settings: Paramètres du bot + description: '**Utilise {command} pour créer un ticket et obtenir un support.**' + title: Aide + description: Afficher le menu d'aide + move: + description: Déplacer un ticket dans une autre catégorie + options: + category: + name: catégorie + description: La catégorie dans laquelle déplacer le ticket + moved: 🗃️ {by} as déplacé ce ticket de **{from}** vers **{to}**. + name: déplacer + not_staff: + description: Seuls les membres de l'équipe peuvent déplacer des tickets. + title: ❌ Erreur + new: + name: nouveau + description: Créer un nouveau ticket + options: + references: + description: Le numéro d'un ticket associé + name: références + priority: + name: priorité + options: + priority: + choices: + MEDIUM: 🟠 Moyenne + HIGH: 🔴 Haute + LOW: 🟢 Basse + description: La priorité du ticket + name: priorité + description: Définir la priorité d'un ticket + success: + title: ✅ Priorité définie + description: La priorité du ticket à été définie sur `{priority}`. + not_staff: + title: ❌ Erreur + description: Seuls les membres de l'équipe peuvent changer la priorité d'un + ticket. + release: + name: relâcher + description: Relâcher (ne plus s'approprier) un ticket + remove: + description: Retirer un membre d'un ticket + name: retirer + success: + title: ✅ Retiré + description: '{member} as été retiré de {ticket}.' + options: + member: + description: Le membre à retirer du ticket + name: membre + ticket: + description: Le ticket dont il faut retirer le membre + name: ticket + removed: ⬅️ {removed} as été retiré par {by}. + not_staff: + description: Seuls les membres de l'équipe peuvent retirer des membres des + tickets des autres. + title: ❌ Erreur + tag: + name: tag + description: Utiliser un tag + options: + for: + description: L'utilisateur à qui attribuer le tag + name: pour + tag: + description: Le nom à utiliser pour le tag + name: tag + tickets: + options: + member: + name: membre + description: Le membre dont il faut lister les tickets + description: Lister votre tickets ou ceux de quelqu'un d'autre + not_staff: + description: Seuls les membres de l'équipe peuvent voir les tickets des autres + membres. + title: ❌ Erreur + response: + fields: + closed: + none: + other: "{user} n'as ouvert aucun ticket." + own: "Vous n'avez ouvert aucun ticket\nUtilisez {new} pour ouvrir un\ + \ ticket.\n" + name: Tickets clôturés + open: + name: Tickets ouverts + description: Utilisez {transcript} pour télécharger la transcription du ticket. + title: + other: Tickets de {displayName} + own: Vos tickets + name: tickets + transcript: + name: transcription + description: Obtenir la transcription d'un ticket + options: + ticket: + name: ticket + description: Le nombre de ticket dont vous souhaitez obtenir la transcription + transfer: + description: Transférer la propriété d'un ticket à un autre membre + name: transférer + transferred_from: '{user} as transféré ce ticket de {from} à {to}.' + options: + member: + description: Le membre à qui transférer la propriété + name: membre + transferred: '{user} as transféré ce ticket à {to}.' + topic: + name: sujet + description: Changer le sujet d'un ticket + user: + create: + name: Créer un ticket pour un membre +misc: + update: + description: "> [Voir`{version}` sur GitHub]({github})\n> [Notice de changement]({changelog})\n\ + > [Guide de mise à jour]({guide})\n" + title: Une mise à jour est disponible + category_full: + description: "Cette catégorie as atteint sa capacité maximum.\nVeuillez réessayer\ + \ ultérieurement.\n" + title: ❌ Catégorie pleine + expires_in: Expire dans {time} + not_ticket: + title: ❌ Ce salon n'est pas un ticket + description: Vous ne pouvez utiliser cette commande que dans un ticket. + ratelimited: + description: Réessayez dans quelques secondes. + title: 🐢 Veuillez ralentir + blocked: + description: Vous n'êtes pas autorisé à créer un ticket. + title: ❌ Bloqué + cooldown: + title: ❌ Veuillez patienter + description: Veuillez patienter {time} avant de créer un autre ticket dans cette + catégorie. + error: + fields: + code: Code d'erreur + identifier: Identifiant + description: "Désolé, une erreur inattendue s'est produite.\nVeuillez transmettre\ + \ cette information à un administrateur.\n" + title: ⚠️ Quelque chose s'est mal passé + missing_roles: + description: Vous n'avez pas les rôles requis pour créer un ticket dans cette + catégorie. + title: ❌ Rôles manquants + no_categories: + description: Aucune catégorie de ticket n'est configurée. + title: ❌ Il n'existe pas de catégorie de ticket + expired: + description: Vous n'avez pas répondu à temps. Veuillez réessayer. + title: ⏰ Expiré + invalid_ticket: + description: Veuillez spécifier un ticket valide. + title: ❌ Ticket invalide + member_limit: + title: + - ❌ Vous avez déjà un ticket + - Vous avez déjà %d tickets ouverts + description: + - Veuillez utiliser votre ticket ouvert ou le clôturer avant d'en créer un nouveau. + - "Veuillez clôturer votre ticket avant d'en créer un autre.\nUtilisez `/tickets`\ + \ afin de voir vos tickets existants.\n" + unknown_category: + description: Veuillez tenter une autre catégorie. + title: ❌ Cette catégorie de ticket n'existe pas +modals: + feedback: + rating: + label: Notation + placeholder: 1-5 + title: Comment avons-nous fait ? + comment: + label: Commentaire + placeholder: Avez vous un retour supplémentaire ? + topic: + label: Sujet + placeholder: Quelle est la raison de ce ticket ? +ticket: + answers: + no_value: '*Pas de réponse*' + claimed: 🙌 {user} s'est approprié ce ticket. + close: + closed: + description: Ce salon sera supprimé dans quelques secondes… + title: ✅ Ticket clôturé + staff_request: + title: ❓ Ce ticket peut il être clôturé ? + description: "{requestedBy} souhaite clôturer ce ticket.\nCliquez sur \"Accepter\"\ + \ pour le fermer, or \"Refuser\" si vous avez toujours besoin d'aide.\n" + archived: "\nLes messages de ce salon seront archivés pour une référence future.\n" + forbidden: + title: ❌ Erreur + description: Vous n'avez pas la permission de clôturer ce ticket. + rejected: ✋ {user} as rejeté une demande de clôture de ce ticket. + user_request: + title: ❓ {requestedBy} souhaite clôturer le ticket + wait_for_staff: ✋ Veuillez attendre que l'équipe ferme ce ticket. + wait_for_user: ✋ Veuillez attendre une réponse de l'utilisateur. + released: ♻️ {user} as relâché ce ticket. + references_message: + title: ℹ️ Référence + description: Fait référence à [a message]({url}) envoyé {timestamp} par {author}. + references_ticket: + title: ℹ️ Référence + fields: + number: Nombre + topic: Sujet + date: Crée à + description: 'Ce ticket est lié à un ticket précédent :' + opening_message: + content: "{staff}\n{creator} as ouvert un nouveau ticket\n" + fields: + topic: Sujet + edited: + title: ✅ Ticket mis à jour + description: Vos modifications ont été enregistrées. + created: + description: 'Votre ticket as été crée : {channel}.' + title: ✅ Ticket crée + feedback: Merci pour votre retour. +dm: + closed: + title: Votre ticket as été clôturé + fields: + created: Crée à + closed: + name: Clôturé à + value: '{timestamp} (après {duration})' + closed_by: Clôturé par + feedback: Vos retours + ticket: Ticket + topic: Sujet + reason: Raison de la clôture + response: Temps de réponse + archived: Utilisez la commande `/transcript` sur **{guild}** pour voir les messages + archivés. + confirm_open: + title: Souhaitez vous ouvrir un ticket avec le sujet suivant ? +log: + admin: + changes: Modifications + description: + joined: '{user} {verb} {targetType}' + target: + tag: un tag + question: une question + settings: les paramètres + panel: un panneau + category: une catégorie + title: + target: + tag: Tag + category: Catégorie + settings: Paramètres + panel: Panneau + question: Question + joined: '{targetType} {verb}' + verb: + update: mis à jour + delete: supprimé + create: crée + ticket: + description: '{user} {verb} un ticket' + verb: + unclaim: Relâché + update: mis à jour + close: Fermé + create: Crée + claim: Approprié + added: Membres ajoutés + removed: Membres supprimés + title: Ticket {verb} + ticket: Ticket + message: + title: Message {verb} + description: '{user} {verb} un message' + verb: + update: mis à jour + delete: supprimé + message: Message +menus: + category: + placeholder: Sélectionnez une catégorie de ticket + guild: + placeholder: Sélectionnez un serveur diff --git a/src/i18n/hu.yml b/src/i18n/hu.yml new file mode 100644 index 0000000..f644ca5 --- /dev/null +++ b/src/i18n/hu.yml @@ -0,0 +1,414 @@ +log: + admin: + description: + target: + panel: panel + question: kérdés + category: kategória + settings: beállítás + tag: címke + joined: '{user} {verb} {targetType}' + title: + target: + category: Kategória + settings: Beállítások + panel: Panel + question: Kérdés + tag: Címke + joined: '{targetType} {verb}' + verb: + create: létrehozva + delete: törölve + update: frissítve + changes: Változások + ticket: + added: Hozzáadott felhasználók + description: '{user} {verb} hibajegy' + removed: Eltávolított felhasználók + title: Hibjaegy {verb} + verb: + create: létrehozva + claim: begyűjtve + close: bezárva + unclaim: felszabadítva + update: frissítve + ticket: Hibajegy + message: + message: Üzenet + verb: + update: frissítve + delete: törölve + description: '{user} {verb} a(z) üzenet' + title: Üzenet {verb} +buttons: + accept_close_request: + emoji: ✅ + text: Elfogadás + cancel: + emoji: ✖️ + text: Mégse + claim: + emoji: 🙌 + text: Begyűjtés + close: + emoji: ✖️ + text: Bezárás + confirm_open: + text: Hibajegy létrehozása + emoji: ✅ + edit: + text: Szerkesztés + emoji: ✏️ + reject_close_request: + text: Elutasítás + emoji: ✖️ + create: + emoji: 🎫 + text: Hibajegy létrehozása + unclaim: + emoji: ♻️ + text: Felszabadítás +commands: + message: + pin: + not_ticket: + title: ❌ Nem hibajegy csatorna + description: Csak a hibajegyekben tudod kitűzni az üzeneteket. + name: Üzenet kitűzése + not_pinnable: + title: ❌ Hiba + description: "Ezt az üzenetet nem tudod kitűzni.\nKérlek vedd fel a kapcsolatot\ + \ egy adminsztrátorral, aki le tudja ellenőrizni a bot jogosultságait.\n" + pinned: + description: Az üzenet kitűzésre került. + title: ✅ Üzenet kitűzve + create: + name: Hibajegy létrehozása üzenetből + slash: + force-close: + options: + time: + name: idő + description: Összes hibajegy bezására, amely inaktív volt a megadott ideig + (használható a(z) `kategóriával` is) + category: + description: Összes hibajegy bezására a megadott kategóriában (használható + a(z) `idővel` is) + name: kategória + reason: + description: Indok a hibajegy(ek) bezárására + name: indok + ticket: + description: A hibajegy, amit be szeretnél zárni + name: hibajegy + no_tickets: + description: Nincs olyan nyitott hibajegy, mely inaktív lenne `{time}` ideje. + title: ❌ Nincs hibajegy + confirm_multiple: + description: "Bezására kerül **{count}** darab hibajegy, mely inaktív `{time}`\ + \ ideje:\n{tickets}\n" + title: ❓ Biztos vagy benne? + description: Hibajegy erőltetett bezárása + name: erőltetett-bezárás + not_staff: + description: Csak a személyzeti tagok erőltethetik a hibajegy bezárását. + title: ❌ Hiba + close: + invalid_time: + description: '`{input}` nem megfelelő formátumú.' + title: ❌ Érvénytelen + description: Kérelmezd a hibajegy bezárását + options: + reason: + description: Indok a hibajegy(ek) bezárásához + name: indok + name: bezárás + help: + name: segítség + response: + commands: Parancsok + links: + support: Segítség + commands: Teljes parancs lista + docs: Dokumentáció + feedback: Visszajelzés + links: Hasznos linkek + settings: Bot konfiguráció + description: '**Használd a(z) {command} parancsot a hibajegy létrehozásához..**' + description: Segítség menü megjelenítése + title: Segítség + tickets: + response: + title: + own: Hibajegyeid + other: '{displayName} hibajegyei' + description: Használd a(z) {transcript}, hogy letölthesd a hibajegy átiratát. + fields: + closed: + name: Bezárt hibajegyek + none: + other: '{user} még nem hozott létre hibajegyet.' + own: "Nem hoztál még létre hibajegyet.\nHasználd a(z) {new} a létrehozáshoz.\n" + open: + name: Nyitott hibajegyek + name: hibajegyek + not_staff: + description: Csak a személyzet tagjai tekinthetik meg mások hibajegyeit. + title: ❌ Hiba + description: Sorolja fel saját vagy valaki más hibajegyeit + options: + member: + description: A felhasználó, akinek a hibajegyeit szeretnéd listázni + name: felhasználó + new: + options: + references: + description: Kapcsolódó hibajegyek száma + name: hivatkozások + name: új + description: Hibajegy létrehozása + priority: + options: + priority: + description: A hibajegy prioritása + choices: + HIGH: 🔴 Magas + LOW: 🟢 Alacsony + MEDIUM: 🟠 Közepes + name: prioritás + name: prioritás + success: + title: ✅ Prioritás beállítva + description: 'A hibajegy prioritása beállításra került: `{priority}`.' + description: Hibajegy prioritásának beállítása + not_staff: + description: Csak a személyzet tagjai tudják megváltoztatni a hibajegy prioritását. + title: ❌ Hiba + move: + description: Hibajegy áthelyezése másik kategóriába + moved: '🗃️ {by} áthelyezte ezt a jegyet a következőről: **{from}** a következőre: + **{to}**.' + options: + category: + description: Kategória, amibe át szeretnéd helyezni a hibajegyet + name: kategória + name: mozgat + not_staff: + title: ❌ Hiba + description: Csak a személyzet tagjai mozgathatják a jegyeket. + remove: + options: + ticket: + name: hibajegy + description: A hibajegy, amiből el szeretnéd távolítani a felhasználót + member: + name: felhasználó + description: A felhasználó, akit el szeretnél távolítani a hibajegyből + name: eltávolítás + not_staff: + description: Csak személyzeti tagok tudnak eltávolítani felhasználókat a felhasználók + hibajegyéből. + title: ❌ Hiba + removed: ⬅️ {removed} eltávolításra került {by} által. + success: + description: '{member} eltávolításra került a(z) {ticket} hibajegyből.' + title: ✅ Eltávolítva + description: Felhasználó eltávolítása a hibajegyből + release: + name: felszabadítás + description: Hibajegy felszabadítása + tag: + options: + tag: + name: címke + description: A használandó címke neve + for: + name: részére + description: A felhasználó, akihez a címkét szeretnéd kötni + description: Címke használata + name: címke + transfer: + name: átruházás + description: A hibajegy tulajdonjogának átruházása egy másik felhasználóra + options: + member: + description: A felhasználó, akire át szeretnéd ruházni a tulajdonjogot + name: felhasználó + transferred: 📨 {user} átruházta a hibajegy tulajdonjogát {to} számára. + transferred_from: '📨 {user} áthelyezte ezt a hibahegyet innen: {from} ide: {to}.' + add: + description: Felhasználó hozzáadása a hibajegyhez + success: + title: ✅ Hozzáadva + description: '{member} hozzáadásra került a(z) {ticket} hibajegyhez.' + name: hozzáadás + added: ➡️ {added} hozzáadásra került {by} által. + options: + member: + description: Felhasználó, akit hozzá szeretnél adni a hibajegyhez + name: felhasználó + ticket: + description: A hibajegy, amihez hozzá szeretnéd adni a felhasználót + name: hibajegy + not_staff: + description: Csak a személyzet tagiai tudnak hozzáadni felhasználót más felhasználó + hibajegyéhez. + title: ❌ Hiba + claim: + description: Hibajegy begyűjtése + name: begyűjtés + not_staff: + description: Csak a személyzet tagjai tudják begyűjteni a hibajegyet. + title: ❌ Hiba + topic: + description: Hibajegy témájának megváltoztatása + name: téma + transcript: + options: + ticket: + description: A hibajegy száma, melynek az átiratát szeretnéd lekérni + name: hibajegy + description: Hibajegy átiratának lekérése + name: hibajegy + user: + create: + name: Create ticket for user +dm: + closed: + title: A hibajegyed bezárásra került + archived: Használd a(z) `/transcript` a(z)**{guild}** szerveren, hogy megtekintsd + az archivált üzeneteket. + fields: + closed: + name: Bezárva ekkor + value: '{timestamp} ({duration} alatt)' + closed_by: Bezárva általa + created: Létrehozva ekkor + feedback: Visszajelzésed + reason: Bezárva ezért + response: Válaszidő + ticket: Hibajegy + topic: Téma + confirm_open: + title: Létre szeretnél hozni egy hibajegyet a megadott témával? +misc: + blocked: + title: ❌ Letiltva + description: Nem tudsz hibajegyet létrehozni. + category_full: + description: "A kategória elérte a maximális kapacitást.\nPróbáld újra később.\n" + title: ❌ Kategória megtelt + cooldown: + description: Kérlek várj {time}, mielőtt új hibajegyet hoznál létre a kategóriában. + title: ❌ Kérlek várj + ratelimited: + description: Próbáld újra néhány másodperc múlva. + title: 🐢 Kérlek lassíts + expires_in: Lejár {time} + error: + description: "Sajnálom váratlan hiba lépett fel.\nKérlek vedd fel a kapcsolatot\ + \ az adminisztrátorral.\n" + fields: + code: Hibakód + identifier: Azonosító + title: ⚠️ Hiba lépett fel + invalid_ticket: + description: Kérlek add meg egy létező hibajegyet. + title: ❌ Érvénytelen hibajegy + expired: + description: Nem válaszoltál időben. Kérlek próbáld újra. + title: ⏰ Lejárt + member_limit: + description: + - Kérlek használd a meglévő hibajegyed, vagy zárd be mielőtt újat hoznál létre. + - "Kérlek zárd be a hibajegyet, mielőtt másikat hoznál létre.\nHasználd a(z) `/tickets`\ + \ parancsot a meglévő hibajegyeid megtekintéséhez.\n" + title: + - ❌ Már van hibajegyed + - ❌ Már van %d megnyitott hibajegyed + missing_roles: + title: ❌ Nem megfelelő rangok + description: Nincs megfelelő rangod ahhoz, hogy létrehozhass hibajegyet ebbe a + kategóriába. + no_categories: + description: Nem került konfigurálásra egyetlen hibajegy kategória sem. + title: ❌ Nincsenek hibajegy kategóriák + not_ticket: + description: Csak hibajegyekben használhatod ezt a parancsot. + title: ❌ Nem hibajegy csatorna + unknown_category: + description: Kérlek próbálj egy másik kategóriát. + title: ❌ Nem létezik ilyen hibajegy kategória + update: + description: "> [`{version}` megtekintése a GitHubon]({github})\n> [Változásnapló]({changelog})\n\ + > [Frissítési útmutató]({guide})\n" + title: Frissítés elérhető +menus: + category: + placeholder: Kategória választása + guild: + placeholder: Szerver választása +ticket: + edited: + description: A változtatások mentésre kerültek. + title: ✅ Hibajegy frissítve + references_ticket: + title: ℹ️ Referencia + fields: + topic: Téma + number: Szám + date: Létrehozva + description: 'Ez a hibajegy egy korábbi hibajegyhez kapcsolódik:' + close: + wait_for_staff: ✋ Kérlek várj a személyzetre, ahhoz hogy bezárhasd ezt a hibajegyet. + closed: + description: A csatorna törlésre kerül néhány másodperc múlva… + title: ✅ Hibajegy bezárva + forbidden: + description: Nincs jogosultságod a hibajegy bezárásához. + title: ❌ Hiba + rejected: ✋ {user} elutasította a kérést a hibajegy bezárásáról. + staff_request: + archived: "\nAz üzenetek ebben a csatornában archiválásra kerülnek a későbbiekre.\n" + description: "{requestedBy} be szeretné zárni ezt a hibajegyet.\nKattints \"\ + Elfogadás\" a bezáráshoz, vagy \"Elutasíts\" ha további segítsére van szükséged.\n" + title: ❓ Bezárásra kerülhet a hibajegy? + user_request: + title: ❓ {requestedBy} be szeretné zárni ezt a hibajegyet + wait_for_user: ✋ Kérlek várj a felhasználó válaszára. + released: ♻️ {user} felszabadította a hibajegyet. + created: + description: 'A hibajegyed létrehozásra került: {channel}.' + title: ✅ Hibajegy létrehozva + claimed: 🙌 {user} begyűjtötte a hibajegyet. + answers: + no_value: '*Nem válaszolt*' + feedback: Köszönjük a visszajelzést. + opening_message: + content: "{staff}\n{creator} létrehozott egy új hibajegyet\n" + fields: + topic: Téma + references_message: + title: ℹ️ Referencia + description: Referencia [üzenet]({url}) elküldve {timestamp} {author} által. + working_hours: + next: + description: Visszatérünk a () időpontra, + bár előfordulhat, hogy ez előtt választ kap. + title: 🕗 Jelenleg nem dolgozunk + today: + description: 'Előfordulhat, hogy korábban is kap választ, azonban nem dolgozunk + -ig (ma: )' + title: 🕗 Jelenleg nem dolgozunk +modals: + feedback: + rating: + placeholder: 1-5 + label: Értékelés + comment: + label: Megjegyzés + placeholder: Van további visszajelzése? + title: Mit gondolsz, hogy teljesítettünk? + topic: + label: Téma + placeholder: Miről szól a hibajegy? diff --git a/src/i18n/it.yml b/src/i18n/it.yml new file mode 100644 index 0000000..bdf7076 --- /dev/null +++ b/src/i18n/it.yml @@ -0,0 +1,50 @@ +buttons: + accept_close_request: + text: Conferma + emoji: ✅ + cancel: + text: Annulla + emoji: ✖️ + close: + text: Chiudi + emoji: ✖️ + edit: + text: Modifica + emoji: ✏️ + claim: + text: Rivendica + emoji: 🙌 + confirm_open: + text: Crea un ticket + emoji: ✅ + create: + text: Crea un ticket + emoji: 🎫 + reject_close_request: + text: Rifiuta + emoji: ✖️ + unclaim: + text: Rilascia + emoji: ♻️ +commands: + message: + pin: + not_pinnable: + description: "Questo messaggio non può essere fissato.\nPer favore chiedi\ + \ ad un amministratore di verificare i permessi del bot.\n" + title: ❌ Errore + pinned: + description: Il messaggio è stato fissato + name: Fissa il messaggio + not_ticket: + description: Puoi fissare solo i messaggi nei ticket + create: + name: Crea un ticket dal messaggio + slash: + add: + description: Aggiungi un utente ad un ticket + name: aggiungi + not_staff: + description: Solo i membri dello staff possono aggiungere utenti ai ticket + altrui. + added: ➡️ {added} è stato aggiunto da {by}. diff --git a/src/i18n/nl.yml b/src/i18n/nl.yml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/src/i18n/nl.yml @@ -0,0 +1 @@ +{} diff --git a/src/i18n/ru.yml b/src/i18n/ru.yml new file mode 100644 index 0000000..7d850af --- /dev/null +++ b/src/i18n/ru.yml @@ -0,0 +1,33 @@ +buttons: + accept_close_request: + emoji: ✅ + text: Принять + cancel: + emoji: ✖️ + text: Отмена + confirm_open: + emoji: ✅ + text: Создать тикет + close: + emoji: ✖️ + text: Закрыть + claim: + emoji: 🙌 + text: Взять + create: + emoji: 🎫 + text: Создать тикет + edit: + emoji: ✏️ + text: Редактировать + reject_close_request: + emoji: ✖️ + text: Отклонить + unclaim: + emoji: ♻️ +commands: + message: + create: + name: Создать тикет из сообщения + pin: + name: Закрепить сообщение diff --git a/src/i18n/tr.yml b/src/i18n/tr.yml new file mode 100644 index 0000000..314d07b --- /dev/null +++ b/src/i18n/tr.yml @@ -0,0 +1,15 @@ +buttons: + cancel: + emoji: ✖️ + close: + emoji: ✖️ + accept_close_request: + emoji: ✅ + create: + emoji: 🎫 + edit: + emoji: ✏️ + claim: + emoji: 🙌 + confirm_open: + emoji: ✅ diff --git a/src/index.js b/src/index.js index a3b866b..65f5ba5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ /** * Discord Tickets - * Copyright (C) 2021 Isaac Saunders + * Copyright (C) 2022 Isaac Saunders * * 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 @@ -15,203 +15,60 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . * - * @name @discord-tickets/bot + * @name discord-tickets/bot * @description An open-source Discord bot for ticket management - * @copyright 2021 Isaac Saunders + * @copyright 2022 Isaac Saunders * @license GNU-GPLv3 */ /* eslint-disable no-console */ -process.title = 'Discord Tickets'; +const pkg = require('../package.json'); +const banner = require('./lib/banner'); +console.log(banner(pkg.version)); // print big title -const min_node_version = '16.6.0'; const semver = require('semver'); -if (semver.lt(process.versions.node, min_node_version)) return console.log(`\x07Error: Discord Tickets does not work on Node v${process.versions.node}; please upgrade to v${min_node_version} or above.`); +const { colours } = require('leeks.js'); -const leeks = require('leeks.js'); -const fs = require('fs'); -const { path } = require('./utils/fs'); - -const checkFile = (file, example) => { - if (fs.existsSync(path(file))) return true; - if (!fs.existsSync(path(example))) { - console.log(`\x07Error: "${file}" not found, and unable to create it due to "${example}" being missing.`); - return process.exit(); - } - console.log(`Copying "${example}" to "${file}"...`); - fs.copyFileSync(path(example), path(file)); - return false; -}; - -checkFile('./user/config.js', './user/example.config.js'); - -if (!checkFile('./.env', './example.env')) { - console.log('Generating database encryption key...'); - - const file = path('./.env'); - const crypto = require('crypto'); - - const key = 'DB_ENCRYPTION_KEY='; - const value = crypto - .randomBytes(24) - .toString('hex'); - - let data = fs.readFileSync(file, { encoding: 'utf-8' }); - data = data.replace(key, key + value); - - fs.writeFileSync(file, data); - - console.log('Saved.'); - console.log(leeks.colours.yellow('Warning: do not lose your ENV file or encryption key; you will lose access to data in the database.')); - console.log('\x07Please set your bot\'s "DISCORD_TOKEN" in "./.env".'); - - process.exit(); +// check node version +if (!semver.satisfies(process.versions.node, pkg.engines.node)) { + console.log('\x07' + colours.redBright(`Error: Your current Node.js version, ${process.versions.node}, does not meet the requirement "${pkg.engines.node}". Please update to version ${semver.minVersion(pkg.engines.node).version} or higher.`)); + process.exit(1); } -require('dotenv').config({ path: path('./.env') }); +// this could be done first, but then there would be no banner :( +process.env.NODE_ENV ??= 'production'; // make sure NODE_ENV is set +require('./env').load(); // load and check environment variables -require('./banner')(); +const fs = require('fs'); +const YAML = require('yaml'); +const logger = require('./lib/logger'); +const Client = require('./client'); +const http = require('./http'); -const log = require('./logger'); +if (!fs.existsSync('./user/config.yml')) { + const examplePath = './user/example.config.yml'; + if (!fs.existsSync(examplePath)) { + console.log('\x07' + colours.redBright('The config file does not exist, and the example file is missing so cannot be copied from.')); + process.exit(1); + } else { + console.log('Creating config file...'); + fs.copyFileSync(examplePath, './user/config.yml'); + console.log(`Copied config to ${'./user/config.yml'}`); + } +} + +const config = YAML.parse(fs.readFileSync('./user/config.yml', 'utf8')); +const log = logger(config); -const { version } = require('../package.json'); process.on('unhandledRejection', error => { - log.notice('PLEASE INCLUDE THIS INFORMATION IF YOU ASK FOR HELP ABOUT THE FOLLOWING ERROR:'); - log.notice(`Discord Tickets v${version}, Node v${process.versions.node} on ${process.platform}`); - log.warn('An error was not caught'); + log.notice(`Discord Tickets v${pkg.version} on Node.js ${process.version} (${process.platform})`); + log.notice('An error was not caught'); if (error instanceof Error) log.warn(`Uncaught ${error.name}`); log.error(error); }); -const DiscordUtils = require('./utils/discord'); -const Cryptr = require('cryptr'); -const I18n = require('@eartharoid/i18n'); -const ListenerLoader = require('./modules/listeners/loader'); -const CommandManager = require('./modules/commands/manager'); -const PluginManager = require('./modules/plugins/manager'); -const TicketManager = require('./modules/tickets/manager'); - -const fetch = require('node-fetch'); - -const { - Client, - Intents -} = require('discord.js'); -// eslint-disable-next-line no-unused-vars -const Logger = require('leekslazylogger'); - -/** - * The Discord client - * @typedef {Bot} Bot - * @extends {Client} - */ -class Bot extends Client { - constructor() { - super({ - intents: [ - Intents.FLAGS.GUILDS, - Intents.FLAGS.GUILD_MEMBERS, - Intents.FLAGS.GUILD_MESSAGES, - Intents.FLAGS.GUILD_MESSAGE_REACTIONS - ], - partials: [ - 'CHANNEL', - 'MESSAGE', - 'REACTION' - ], - presence: DiscordUtils.selectPresence() - }); - - (async () => { - this.version = version; - - /** The global bot configuration */ - this.config = require('../user/config'); - - /** - * A [leekslazylogger](https://logger.eartharoid.me) instance - * @type {Logger} - */ - this.log = log; - - /** - * A [Cryptr](https://www.npmjs.com/package/cryptr) instance - * @type {Cryptr} - */ - this.cryptr = new Cryptr(process.env.DB_ENCRYPTION_KEY); - - const locales = {}; - fs.readdirSync(path('./src/locales')) - .filter(file => file.endsWith('.json')) - .forEach(file => { - const data = fs.readFileSync(path(`./src/locales/${file}`), { encoding: 'utf8' }); - const name = file.slice(0, file.length - 5); - locales[name] = JSON.parse(data); - }); - - /** - * An [@eartharoid/i18n](https://github.com/eartharoid/i18n) instance - * @type {I18n} - */ - this.i18n = new I18n('en-GB', locales); - - /** A sequelize instance */ - this.db = await require('./database')(this), // this.db.models.Ticket... - - this.setMaxListeners(this.config.max_listeners); // set the max listeners for each event - - require('./update/notifier')(this); // check for updates - - const listeners = new ListenerLoader(this); - listeners.load(); // load listeners - - /** The ticket manager */ - this.tickets = new TicketManager(this); - - /** The command manager, used by internal and plugin commands */ - this.commands = new CommandManager(this); - - /** The plugin manager */ - this.plugins = new PluginManager(this); - this.plugins.load(); // load plugins - - /** Some utility methods */ - this.utils = new DiscordUtils(this); - - this.log.info('Connecting to Discord API...'); - - this.login(); - })(); - } - - async postStats() { - /** - * OH NO, TELEMETRY!? - * Relax, it just counts how many people are using Discord Tickets. - * You can see the source here: https://github.com/discord-tickets/stats - */ - if (this.config.super_secret_setting) { // you can disable it if you really want - const data = { - client: this.user.id, - guilds: this.guilds.cache.size, - members: await this.guilds.cache.reduce(async (acc, guild) => await acc + (await guild.fetch()).approximateMemberCount, 0), - tickets: await this.db.models.Ticket.count(), - version: this.version - }; - this.log.debug('Sending statistics', data); - await fetch('https://stats.discordtickets.app/v2', { - body: JSON.stringify(data), - headers: { 'Content-Type': 'application/json' }, - method: 'POST' - }) - .catch(error => { - this.log.warn('Failed to send statistics'); - this.log.debug(error); - }); - } - } - -} - -new Bot(); \ No newline at end of file +const client = new Client(config, log); +client.login().then(() => { + http(client); +}); \ No newline at end of file diff --git a/src/lib/banner.js b/src/lib/banner.js new file mode 100644 index 0000000..ae474ed --- /dev/null +++ b/src/lib/banner.js @@ -0,0 +1,9 @@ +const { colours } = require('leeks.js'); +const figlet = require('figlet'); +const link = require('terminal-link'); + +module.exports = version => colours.cyan(figlet.textSync('Discord', { font: 'Banner3' })) + + colours.cyan('\n\n' + figlet.textSync('Tickets', { font: 'Banner3' })) + + colours.cyanBright(`\n\n${link('Discord Tickets', 'https://discordtickets.app')} bot v${version} by eartharoid`) + + colours.cyanBright('\nSponsor this project at https://discordtickets.app/sponsor') + + '\n\n'; diff --git a/src/lib/embed.js b/src/lib/embed.js new file mode 100644 index 0000000..4b3504d --- /dev/null +++ b/src/lib/embed.js @@ -0,0 +1,8 @@ +const { EmbedBuilder } = require('discord.js'); + +module.exports = class ExtendedEmbedBuilder extends EmbedBuilder { + constructor(footer, opts) { + super(opts); + if (footer && footer.text) this.setFooter(footer); + } +}; \ No newline at end of file diff --git a/src/lib/http.js b/src/lib/http.js new file mode 100644 index 0000000..9dbcdf2 --- /dev/null +++ b/src/lib/http.js @@ -0,0 +1 @@ +module.exports.domain = process.env.HTTP_EXTERNAL.match(/http(s?):\/\/(?[a-zA-Z0-9\-_.]+)(:\d+)?/).groups.domain; \ No newline at end of file diff --git a/src/lib/logger.js b/src/lib/logger.js new file mode 100644 index 0000000..f0a0ab4 --- /dev/null +++ b/src/lib/logger.js @@ -0,0 +1,67 @@ +const { + ConsoleTransport, + FileTransport, + Logger, +} = require('leekslazylogger'); +const DTF = require('@eartharoid/dtf'); +const { short } = require('leeks.js'); +const { format } = require('util'); + +const dtf = new DTF('en-GB'); +const colours = { + critical: ['&!4&f', '&!4&f'], + debug: ['&1', '&9'], + error: ['&4', '&c'], + info: ['&3', '&b'], + notice: ['&!6&0', '&!6&0'], + success: ['&2', '&a'], + verbose: ['&7', '&f'], + warn: ['&6', '&e'], +}; + +module.exports = config => { + const transports = [ + new ConsoleTransport({ + format: log => { + const timestamp = dtf.fill('DD/MM/YY HH:mm:ss', log.timestamp); + const colour = colours[log.level.name]; + return format( + short(`&f&!7 %s &r ${colour[0]}[%s]&r %s${colour[1]}%s&r`), + timestamp, + log.level.name.toUpperCase(), + log.namespace ? short(`&d(${log.namespace.toUpperCase()})&r `) : '', + log.content, + ); + }, + level: config.logs.level, + }), + ]; + + if (config.logs.files.enabled) { + transports.push( + new FileTransport({ + clean_directory: config.logs.files.keepFor, + directory: config.logs.files.directory, + format: '[{timestamp}] [{LEVEL}] ({NAMESPACE}) @{file}:{line}:{column} {content}', + level: config.logs.level, + name: 'Discord Tickets by eartharoid', + }), + ); + } + + return new Logger({ + namespaces: [ + 'autocomplete', + 'buttons', + 'commands', + 'http', + 'listeners', + 'menus', + 'modals', + 'settings', + 'tickets', + ], + transports, + }); +}; + diff --git a/src/lib/logging.js b/src/lib/logging.js new file mode 100644 index 0000000..5b24f86 --- /dev/null +++ b/src/lib/logging.js @@ -0,0 +1,241 @@ +const { + cleanCodeBlockContent, + EmbedBuilder, +} = require('discord.js'); +const { diff: getDiff } = require('object-diffy'); + +const uuidRegex = /[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/g; + +const exists = thing => typeof thing === 'string' ? thing.length > 0 : thing !== null && thing !== undefined; + +const arrToObj = obj => { + for (const key in obj) { + if (obj[key] instanceof Array && obj[key][0]?.id) { + const temp = {}; + obj[key].forEach(v => (temp[v.id] = v)); + obj[key] = temp; + } + } + return obj; +}; + +function makeDiff({ + original, updated, +}) { + const diff = getDiff(arrToObj(original), arrToObj(updated)); + const fields = []; + for (const key in diff) { + if (key === 'createdAt') continue; // object-diffy doesn't like dates + const from = exists(diff[key].from) ? `- ${String(diff[key].from).replace(/\n/g, '\\n')}\n` : ''; + const to = exists(diff[key].to) ? `+ ${String(diff[key].to).replace(/\n/g, '\\n')}\n` : ''; + fields.push({ + inline: true, + name: key.replace(uuidRegex, $1 => $1.split('-')[0]), + value: `\`\`\`diff\n${cleanCodeBlockContent(from + to)}\n\`\`\``, + }); + } + return fields; +} + +/** + * @param {import("client")} client + * @param {string} guildId + * @returns {import("discord.js").TextChannel?} +*/ +async function getLogChannel(client, guildId) { + const { logChannel: channelId } = await client.prisma.guild.findUnique({ + select: { logChannel: true }, + where: { id: guildId }, + }); + return channelId && client.channels.cache.get(channelId); +} + +/** + * @param {import("client")} client + * @param {object} details + * @param {string} details.guildId + * @param {string} details.userId + * @param {string} details.action +*/ +async function logAdminEvent(client, { + guildId, userId, action, target, diff, +}) { + const settings = await client.prisma.guild.findUnique({ + select: { + footer: true, + locale: true, + logChannel: true, + }, + where: { id: guildId }, + }); + /** @type {import("discord.js").Guild} */ + const guild = client.guilds.cache.get(guildId); + const member = await guild.members.fetch(userId); + client.log.info.settings(`${member.user.tag} ${action}d ${target.type} ${target.id}`); + if (!settings.logChannel) return; + const colour = action === 'create' + ? 'Green' : action === 'update' + ? 'Orange' : action === 'delete' + ? 'Red' : 'Default'; + const getMessage = client.i18n.getLocale(settings.locale); + const i18nOptions = { + user: `<@${member.user.id}>`, + verb: getMessage(`log.admin.verb.${action}`), + }; + const channel = client.channels.cache.get(settings.logChannel); + if (!channel) return; + const embeds = [ + new EmbedBuilder() + .setColor(colour) + .setAuthor({ + iconURL: member.displayAvatarURL(), + name: member.displayName, + }) + .setTitle(getMessage('log.admin.title.joined', { + ...i18nOptions, + targetType: getMessage(`log.admin.title.target.${target.type}`), + verb: getMessage(`log.admin.verb.${action}`), + })) + .setDescription(getMessage('log.admin.description.joined', { + ...i18nOptions, + targetType: getMessage(`log.admin.description.target.${target.type}`), + verb: getMessage(`log.admin.verb.${action}`), + })) + .addFields([ + { + name: getMessage(`log.admin.title.target.${target.type}`), + value: target.name ? `${target.name} (\`${target.id}\`)` : target.id, + }, + ]), + ]; + + if (diff?.original && Object.entries(makeDiff(diff)).length) { + embeds.push( + new EmbedBuilder() + .setColor(colour) + .setTitle(getMessage('log.admin.changes')) + .setFields(makeDiff(diff)), + ); + } + + return await channel.send({ embeds }); +} + +/** + * @param {import("client")} client + * @param {object} details + * @param {string} details.guildId + * @param {string} details.userId + * @param {string} details.action +*/ +async function logTicketEvent(client, { + userId, action, target, diff, +}) { + const ticket = await client.tickets.getTicket(target.id); + if (!ticket) return; + /** @type {import("discord.js").Guild} */ + const guild = client.guilds.cache.get(ticket.guild.id); + const member = await guild.members.fetch(userId); + client.log.info.tickets(`${member.user.tag} ${client.i18n.getMessage('en-GB', `log.ticket.verb.${action}`)} ticket ${target.id}`); + if (!ticket.guild.logChannel) return; + const channel = client.channels.cache.get(ticket.guild.logChannel); + if (!channel) return; + const colour = action === 'create' + ? 'Aqua' : action === 'close' + ? 'DarkAqua' : action === 'update' + ? 'Purple' : action === 'claim' + ? 'LuminousVividPink' : action === 'unclaim' + ? 'DarkVividPink' : 'Default'; + const getMessage = client.i18n.getLocale(ticket.guild.locale); + const i18nOptions = { + user: `<@${member.user.id}>`, + verb: getMessage(`log.ticket.verb.${action}`), + }; + const embeds = [ + new EmbedBuilder() + .setColor(colour) + .setAuthor({ + iconURL: member.displayAvatarURL(), + name: member.displayName, + }) + .setTitle(getMessage('log.ticket.title', i18nOptions)) + .setDescription(getMessage('log.ticket.description', i18nOptions)) + .addFields([ + { + name: getMessage('log.ticket.ticket'), + value: target.name ? `${target.name} (\`${target.id}\`)` : target.id, + }, + ]), + ]; + + if (diff?.original && Object.entries(makeDiff(diff)).length) { + embeds.push( + new EmbedBuilder() + .setColor(colour) + .setTitle(getMessage('log.admin.changes')) + .setFields(makeDiff(diff)), + ); + } + + return await channel.send({ embeds }); +} + +/** + * @param {import("client")} client + * @param {object} details + * @param {string} details.action + * @param {import("discord.js").Message} details.target + * @param {import("@prisma/client").Ticket & {guild: import("@prisma/client").Guild}} details.ticket +*/ +async function logMessageEvent(client, { + action, executor, target, ticket, diff, +}) { + if (!ticket) return; + if (executor === undefined) executor = target.member; + client.log.info.tickets(`${executor?.user.tag || 'Unknown'} ${client.i18n.getMessage('en-GB', `log.message.verb.${action}`)} message ${target.id}`); + if (!ticket.guild.logChannel) return; + const colour = action === 'update' + ? 'Purple' : action === 'delete' + ? 'DarkPurple' : 'Default'; + const getMessage = client.i18n.getLocale(ticket.guild.locale); + const i18nOptions = { + user: `<@${executor?.user.id}>`, + verb: getMessage(`log.message.verb.${action}`), + }; + const channel = client.channels.cache.get(ticket.guild.logChannel); + if (!channel) return; + const embeds = [ + new EmbedBuilder() + .setColor(colour) + .setAuthor({ + iconURL: target.member?.displayAvatarURL() || 'https://discord.com/assets/1f0bfc0865d324c2587920a7d80c609b.png', + name: target.member?.displayName || 'Unknown', + }) + .setTitle(getMessage('log.message.title', i18nOptions)) + .setDescription(getMessage('log.message.description', i18nOptions)) + .addFields([ + { + name: getMessage('log.message.message'), + value: `[\`${target.id}\`](${target.url})`, + }, + ]), + ]; + + if (diff?.original && Object.entries(makeDiff(diff)).length) { + embeds.push( + new EmbedBuilder() + .setColor(colour) + .setTitle(getMessage('log.admin.changes')) + .setFields(makeDiff(diff)), + ); + } + + return await channel.send({ embeds }); +} + +module.exports = { + getLogChannel, + logAdminEvent, + logMessageEvent, + logTicketEvent, +}; diff --git a/src/lib/middleware/prisma-sqlite.js b/src/lib/middleware/prisma-sqlite.js new file mode 100644 index 0000000..d858a4c --- /dev/null +++ b/src/lib/middleware/prisma-sqlite.js @@ -0,0 +1,47 @@ +const jsonFields = [ + 'pingRoles', + 'requiredRoles', + 'staffRoles', + 'autoTag', + 'blocklist', + 'workingHours', + 'options', + 'pinnedMessageIds', +]; + +const traverse = (obj, action) => { + for (let prop in obj) { + if (prop === 'createMany') { + obj.create = obj[prop].data; + delete obj[prop]; + prop = 'create'; + traverse(obj[prop], action); + } else if (jsonFields.includes(prop) && obj[prop] !== null && obj[prop] !== undefined) { + if (action === 'SERIALISE') { + if (typeof obj[prop] === 'string') { + try { + JSON.parse(obj[prop]); + } catch { + obj[prop] = JSON.stringify(obj[prop]); + } + } else { + obj[prop] = JSON.stringify(obj[prop]); + } + } else if (action === 'PARSE' && typeof obj[prop] === 'string') { + obj[prop] = JSON.parse(obj[prop]); + } + } else if (typeof obj[prop] === 'object' && obj[prop] !== null && obj[prop] !== undefined) { + traverse(obj[prop], action); + } + } + return obj; +}; + +module.exports = async (params, next) => { + if (params.args?.create) params.args.create = traverse(params.args.create, 'SERIALISE'); + if (params.args?.data) params.args.data = traverse(params.args.data, 'SERIALISE'); + if (params.args?.update) params.args.update = traverse(params.args.update, 'SERIALISE'); + let result = await next(params); + if (result) result = traverse(result, 'PARSE'); + return result; +}; \ No newline at end of file diff --git a/src/lib/misc.js b/src/lib/misc.js new file mode 100644 index 0000000..8b0d15a --- /dev/null +++ b/src/lib/misc.js @@ -0,0 +1 @@ +module.exports.msToMins = ms => Number((ms / 1000 / 60).toFixed(1)); \ No newline at end of file diff --git a/src/lib/stats.js b/src/lib/stats.js new file mode 100644 index 0000000..3ced74a --- /dev/null +++ b/src/lib/stats.js @@ -0,0 +1,2 @@ +module.exports.getAvgResolutionTime = tickets => (tickets.reduce((total, ticket) => total + (ticket.closedAt - ticket.createdAt), 0) || 1) / Math.max(tickets.length, 1); +module.exports.getAvgResponseTime = tickets => (tickets.reduce((total, ticket) => total + (ticket.firstResponseAt - ticket.createdAt), 0) || 1) / Math.max(tickets.length, 1); diff --git a/src/lib/sync.js b/src/lib/sync.js new file mode 100644 index 0000000..b0c3800 --- /dev/null +++ b/src/lib/sync.js @@ -0,0 +1,73 @@ +/** + * @param {import("client")} client + */ +module.exports = async client => { + // load ticket numbers + const guilds = await client.prisma.guild.findMany({ select: { id: true } }); + for (const guild of guilds) { + const { _max: { number: max } } = await client.prisma.ticket.aggregate({ + _max: { number: true }, + where: { guildId: guild.id }, + }); + client.tickets.$numbers[guild.id] = max ?? 0; + } + + // load total number of open tickets + const categories = await client.prisma.category.findMany({ + select: { + cooldown: true, + id: true, + tickets: { + select: { + createdById: true, + guildId: true, + id: true, + }, + where: { open: true }, + }, + }, + }); + let deleted = 0; + let ticketCount = 0; + let cooldowns = 0; + for (const category of categories) { + ticketCount += category.tickets.length; + client.tickets.$count.categories[category.id] = { total: category.tickets.length }; + for (const ticket of category.tickets) { + if (client.tickets.$count.categories[category.id][ticket.createdById]) client.tickets.$count.categories[category.id][ticket.createdById]++; + else client.tickets.$count.categories[category.id][ticket.createdById] = 1; + /** @type {import("discord.js").Guild} */ + const guild = client.guilds.cache.get(ticket.guildId); + if (guild && guild.available && !client.channels.cache.has(ticket.id)) { + deleted += 0; + await client.tickets.finallyClose(ticket.id, { reason: 'channel deleted' }); + } + + } + if (category.cooldown) { + const recent = await client.prisma.ticket.findMany({ + orderBy: { createdAt: 'asc' }, + select: { + createdAt: true, + createdById: true, + }, + where: { + categoryId: category.id, + createdAt: { gt: new Date(Date.now() - category.cooldown) }, + }, + }); + cooldowns += recent.length; + for (const ticket of recent) { + const cacheKey = `cooldowns/category-member:${category.id}-${ticket.createdById}`; + const expiresAt = ticket.createdAt.getTime() + category.cooldown; + const TTL = expiresAt - Date.now(); + await client.keyv.set(cacheKey, expiresAt, TTL); + } + } + } + // const ticketCount = categories.reduce((total, category) => total + category.tickets.length, 0); + client.log.info(`Cached ticket count of ${categories.length} categories (${ticketCount} open tickets)`); + client.log.info(`Loaded ${cooldowns} active cooldowns`); + client.log.info(`Closed ${deleted} deleted tickets`); + return true; +}; diff --git a/src/lib/tickets/archiver.js b/src/lib/tickets/archiver.js new file mode 100644 index 0000000..6687018 --- /dev/null +++ b/src/lib/tickets/archiver.js @@ -0,0 +1,155 @@ +const Cryptr = require('cryptr'); +const { encrypt } = new Cryptr(process.env.ENCRYPTION_KEY); + +/** + * Returns highest (roles.highest) hoisted role, or everyone + * @param {import("discord.js").GuildMember} member + * @returns {import("discord.js").Role} + */ +const hoistedRole = member => member.roles.hoist || member.guild.roles.everyone; + +module.exports = class TicketArchiver { + constructor(client) { + /** @type {import("client")} */ + this.client = client; + } + + /** Add or update a message + * @param {string} ticketId + * @param {import("discord.js").Message} message + * @param {boolean?} external + * @returns {import("@prisma/client").ArchivedMessage|boolean} + */ + async saveMessage(ticketId, message, external = false) { + if (process.env.OVERRIDE_ARCHIVE === 'false') return false; + + if (!message.member) { + try { + message.member = await message.guild.members.fetch(message.author.id); + } catch { + this.client.log.verbose('Failed to fetch member %s of %s', message.author.id, message.guild.id); + } + } + + const channels = message.mentions.channels; + const members = [...message.mentions.members.values()]; + const roles = [...message.mentions.roles.values()]; + + if (message.member) { + members.push(message.member); + roles.push(hoistedRole(message.member)); + } else { + this.client.log.warn('Message member does not exist'); + await this.client.prisma.archivedUser.upsert({ + create: {}, + update: {}, + where: { + ticketId_userId: { + ticketId, + userId: 'default', + }, + }, + }); + } + + for (const role of roles) { + const data = { + colour: role.hexColor.slice(1), + name: role.name, + roleId: role.id, + ticket: { connect: { id: ticketId } }, + }; + await this.client.prisma.archivedRole.upsert({ + create: data, + update: data, + where: { + ticketId_roleId: { + roleId: role.id, + ticketId, + }, + }, + }); + } + + for (const member of members) { + const data = { + avatar: member.avatar || member.user.avatar, // TODO: save avatar in user/avatars/ + bot: member.user.bot, + discriminator: member.user.discriminator, + displayName: member.displayName ? encrypt(member.displayName) : null, + roleId: !!member && hoistedRole(member).id, + ticketId, + userId: member.user.id, + username: encrypt(member.user.username), + }; + await this.client.prisma.archivedUser.upsert({ + create: data, + update: data, + where: { + ticketId_userId: { + ticketId, + userId: member.user.id, + }, + }, + }); + } + + let reference; + if (message.reference) reference = await message.fetchReference(); + + const messageD = { + author: { + connect: { + ticketId_userId: { + ticketId, + userId: message.author?.id || 'default', + }, + }, + }, + content: encrypt( + JSON.stringify({ + attachments: [...message.attachments.values()], + components: [...message.components.values()], + content: message.content, + embeds: message.embeds.map(embed => ({ ...embed })), + reference: reference ? reference.id : null, + }), + ), + createdAt: message.createdAt, + edited: !!message.editedAt, + external, + id: message.id, + }; + + return await this.client.prisma.ticket.update({ + data: { + archivedChannels: { + upsert: channels.map(channel => { + const data = { + channelId: channel.id, + name: channel.name, + }; + return { + create: data, + update: data, + where: { + ticketId_channelId: { + channelId: channel.id, + ticketId, + }, + }, + }; + }), + }, + archivedMessages: { + upsert: { + create: messageD, + update: messageD, + where: { id: message.id }, + }, + }, + }, + where: { id: ticketId }, + }); + } +}; \ No newline at end of file diff --git a/src/lib/tickets/manager.js b/src/lib/tickets/manager.js new file mode 100644 index 0000000..ca2c900 --- /dev/null +++ b/src/lib/tickets/manager.js @@ -0,0 +1,1303 @@ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable max-lines */ +const TicketArchiver = require('./archiver'); +const { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + inlineCode, + ModalBuilder, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, + TextInputBuilder, + TextInputStyle, +} = require('discord.js'); +const emoji = require('node-emoji'); +const ms = require('ms'); +const ExtendedEmbedBuilder = require('../embed'); +const { logTicketEvent } = require('../logging'); +const { isStaff } = require('../users'); +const { Collection } = require('discord.js'); +const spacetime = require('spacetime'); +const Cryptr = require('cryptr'); +const { + getAvgResolutionTime, + getAvgResponseTime, +} = require('../stats'); +const { + decrypt, + encrypt, +} = new Cryptr(process.env.ENCRYPTION_KEY); + +/** + * @typedef {import('@prisma/client').Category & + * {guild: import('@prisma/client').Guild} & + * {questions: import('@prisma/client').Question[]}} CategoryGuildQuestions + */ + +/** + * @typedef {import('@prisma/client').Ticket & + * {category: import('@prisma/client').Category} & + * {feedback: import('@prisma/client').Feedback} & + * {guild: import('@prisma/client').Guild}} TicketCategoryFeedbackGuild + */ + +module.exports = class TicketManager { + constructor(client) { + /** @type {import("client")} */ + this.client = client; + this.archiver = new TicketArchiver(client); + this.$count = { categories: {} }; + this.$numbers = {}; + this.$stale = new Collection(); + } + + /** + * Retrieve cached category data + * @param {string} categoryId the category ID + * @param {boolean} force bypass & update the cache? + * @returns {Promise} + */ + async getCategory(categoryId, force) { + const cacheKey = `cache/category+guild+questions:${categoryId}`; + /** @type {CategoryGuildQuestions} */ + let category = await this.client.keyv.get(cacheKey); + if (!category || force) { + category = await this.client.prisma.category.findUnique({ + include: { + guild: true, + questions: { orderBy: { order: 'asc' } }, + }, + where: { id: categoryId }, + }); + await this.client.keyv.set(cacheKey, category, ms('12h')); + } + return category; + } + + /** + * Retrieve cached ticket data for the closing sequence + * @param {string} ticketId the ticket ID + * @param {boolean} force bypass & update the cache? + * @returns {Promise} + */ + async getTicket(ticketId, force) { + const cacheKey = `cache/ticket+category+feedback+guild:${ticketId}`; + /** @type {TicketCategoryFeedbackGuild} */ + let ticket = await this.client.keyv.get(cacheKey); + if (!ticket || force) { + ticket = await this.client.prisma.ticket.findUnique({ + include: { + category: true, + feedback: true, + guild: true, + }, + where: { id: ticketId }, + }); + await this.client.keyv.set(cacheKey, ticket, ms('3m')); + } + return ticket; + } + + async getTotalCount(categoryId) { + this.$count.categories[categoryId] ||= {}; + let count = this.$count.categories[categoryId].total; + if (!count) { + count = await this.client.prisma.ticket.count({ + where: { + categoryId, + open: true, + }, + }); + this.$count.categories[categoryId].total = count; + } + return count; + } + + async getMemberCount(categoryId, memberId) { + this.$count.categories[categoryId] ||= {}; + let count = this.$count.categories[categoryId][memberId]; + if (!count) { + count = await this.client.prisma.ticket.count({ + where: { + categoryId: categoryId, + createdById: memberId, + open: true, + }, + }); + this.$count.categories[categoryId][memberId] = count; + } + return count; + } + + async getCooldown(categoryId, memberId) { + const cacheKey = `cooldowns/category-member:${categoryId}-${memberId}`; + return await this.client.keyv.get(cacheKey); + } + + getNextNumber(guildId) { + this.$numbers[guildId] += 1; + return this.$numbers[guildId]; + } + + /** + * @param {object} data + * @param {string} data.categoryId + * @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction|import("discord.js").SelectMenuInteraction} data.interaction + * @param {string?} [data.topic] + */ + async create({ + categoryId, interaction, topic, referencesMessage, referencesTicketId, + }) { + categoryId = Number(categoryId); + const category = await this.getCategory(categoryId); + + if (!category) { + let settings; + if (interaction.guild) { + settings = await this.client.prisma.guild.findUnique({ where: { id: interaction.guild.id } }); + } else { + settings = { + errorColour: 'Red', + locale: 'en-GB', + }; + } + const getMessage = this.client.i18n.getLocale(settings.locale); + return await interaction.reply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild?.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('misc.unknown_category.title')) + .setDescription(getMessage('misc.unknown_category.description')), + ], + ephemeral: true, + }); + } + + /** @type {import("discord.js").Guild} */ + const guild = this.client.guilds.cache.get(category.guild.id); + const member = interaction.member ?? await guild.members.fetch(interaction.user.id); + const getMessage = this.client.i18n.getLocale(category.guild.locale); + + const rlKey = `ratelimits/guild-user:${category.guildId}-${interaction.user.id}`; + const rl = await this.client.keyv.get(rlKey); + if (rl) { + return await interaction.reply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: guild.iconURL(), + text: category.guild.footer, + }) + .setColor(category.guild.errorColour) + .setTitle(getMessage('misc.ratelimited.title')) + .setDescription(getMessage('misc.ratelimited.description')), + ], + ephemeral: true, + }); + } else { + this.client.keyv.set(rlKey, true, ms('5s')); + } + + const sendError = name => interaction.reply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: guild.iconURL(), + text: category.guild.footer, + }) + .setColor(category.guild.errorColour) + .setTitle(getMessage(`misc.${name}.title`)) + .setDescription(getMessage(`misc.${name}.description`)), + ], + ephemeral: true, + }); + + if (category.guild.blocklist.length !== 0) { + const blocked = category.guild.blocklist.some(r => member.roles.cache.has(r)); + if (blocked) return await sendError('blocked'); + } + + if (category.requiredRoles.length !== 0) { + const missing = category.requiredRoles.some(r => !member.roles.cache.has(r)); + if (missing) return await sendError('missing_roles'); + } + + const discordCategory = guild.channels.cache.get(category.discordCategory); + if (discordCategory.children.cache.size === 50) return await sendError('category_full'); + + const totalCount = await this.getTotalCount(category.id); + if (totalCount >= category.totalLimit) return await sendError('category_full'); + + const memberCount = await this.getMemberCount(category.id, interaction.user.id); + if (memberCount >= category.memberLimit) { + return await interaction.reply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: guild.iconURL(), + text: category.guild.footer, + }) + .setColor(category.guild.errorColour) + .setTitle(getMessage('misc.member_limit.title', memberCount, memberCount)) + .setDescription(getMessage('misc.member_limit.description', memberCount)), + ], + ephemeral: true, + }); + } + + const cooldown = await this.getCooldown(category.id, interaction.user.id); + if (cooldown) { + return await interaction.reply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: guild.iconURL(), + text: category.guild.footer, + }) + .setColor(category.guild.errorColour) + .setTitle(getMessage('misc.cooldown.title')) + .setDescription(getMessage('misc.cooldown.description', { time: ms(cooldown - Date.now()) })), + ], + ephemeral: true, + }); + } + + if (category.questions.length >= 1) { + await interaction.showModal( + new ModalBuilder() + .setCustomId(JSON.stringify({ + action: 'questions', + categoryId, + referencesMessage, + referencesTicketId, + })) + .setTitle(category.name) + .setComponents( + category.questions + .filter(q => q.type === 'TEXT') // TODO: remove this when modals support select menus + .map(q => { + if (q.type === 'TEXT') { + return new ActionRowBuilder() + .setComponents( + new TextInputBuilder() + .setCustomId(q.id) + .setLabel(q.label) + .setStyle(q.style) + .setMaxLength(Math.min(q.maxLength, 1000)) + .setMinLength(q.minLength) + .setPlaceholder(q.placeholder) + .setRequired(q.required) + .setValue(q.value), + ); + } else if (q.type === 'MENU') { + return new ActionRowBuilder() + .setComponents( + new StringSelectMenuBuilder() + .setCustomId(q.id) + .setPlaceholder(q.placeholder || q.label) + .setMaxValues(q.maxLength) + .setMinValues(q.minLength) + .setOptions( + q.options.map((o, i) => { + const builder = new StringSelectMenuOptionBuilder() + .setValue(String(i)) + .setLabel(o.label); + if (o.description) builder.setDescription(o.description); + if (o.emoji) builder.setEmoji(emoji.hasEmoji(o.emoji) ? emoji.get(o.emoji) : { id: o.emoji }); + return builder; + }), + ), + ); + } + }), + ), + ); + } else if (category.requireTopic && !topic) { + await interaction.showModal( + new ModalBuilder() + .setCustomId(JSON.stringify({ + action: 'topic', + categoryId, + referencesMessage, + referencesTicketId, + })) + .setTitle(category.name) + .setComponents( + new ActionRowBuilder() + .setComponents( + new TextInputBuilder() + .setCustomId('topic') + .setLabel(getMessage('modals.topic.label')) + .setStyle(TextInputStyle.Paragraph) + .setMaxLength(1000) + .setMinLength(5) + .setPlaceholder(getMessage('modals.topic.placeholder')) + .setRequired(true), + ), + ), + ); + } else { + await this.postQuestions({ + categoryId, + interaction, + referencesMessage, + referencesTicketId, + topic, + }); + } + } + + /** + * @param {object} data + * @param {string} data.category + * @param {import("discord.js").ButtonInteraction|import("discord.js").SelectMenuInteraction|import("discord.js").ModalSubmitInteraction} data.interaction + * @param {string?} [data.topic] + */ + async postQuestions({ + action, categoryId, interaction, topic, referencesMessage, referencesTicketId, + }) { + const [, category] = await Promise.all([ + interaction.deferReply({ ephemeral: true }), + this.getCategory(categoryId), + ]); + + let answers; + if (interaction.isModalSubmit()) { + if (action === 'questions') { + answers = category.questions.filter(q => q.type === 'TEXT').map(q => ({ + questionId: q.id, + userId: interaction.user.id, + value: interaction.fields.getTextInputValue(q.id) ? encrypt(interaction.fields.getTextInputValue(q.id)) : '', + })); + if (category.customTopic) topic = interaction.fields.getTextInputValue(category.customTopic); + } else if (action === 'topic') { + topic = interaction.fields.getTextInputValue('topic'); + } + } + + /** @type {import("discord.js").Guild} */ + const guild = this.client.guilds.cache.get(category.guild.id); + const getMessage = this.client.i18n.getLocale(category.guild.locale); + const creator = await guild.members.fetch(interaction.user.id); + const number = this.getNextNumber(category.guild.id); + const channelName = category.channelName + .replace(/{+\s?(user)?name\s?}+/gi, creator.user.username) + .replace(/{+\s?(nick|display)(name)?\s?}+/gi, creator.displayName) + .replace(/{+\s?num(ber)?\s?}+/gi, number === 1488 ? '1487b' : number); + const allow = ['ViewChannel', 'ReadMessageHistory', 'SendMessages', 'EmbedLinks', 'AttachFiles']; + /** @type {import("discord.js").TextChannel} */ + const channel = await guild.channels.create({ + name: channelName, + parent: category.discordCategory, + permissionOverwrites: [ + { + deny: ['ViewChannel'], + id: guild.roles.everyone, + }, + { + allow, + id: this.client.user.id, + }, + { + allow, + id: creator.id, + }, + ...category.staffRoles.map(id => ({ + allow, + id, + })), + ], + rateLimitPerUser: category.ratelimit, + reason: `${creator.user.tag} created a ticket`, + topic: `${creator}${topic?.length > 0 ? ` | ${topic}` : ''}`, + }); + + if (category.image) await channel.send(category.image); + + const needsStats = /{+\s?(avgResponseTime|avgResolutionTime)\s?}+/i.test(category.openingMessage); + const statsCacheKey = `cache/category-stats/${categoryId}`; + let stats = await this.client.keyv.get(statsCacheKey); + if (needsStats && !stats) { + const closedTickets = await this.client.prisma.ticket.findMany({ + select: { + closedAt: true, + createdAt: true, + firstResponseAt: true, + }, + where: { + categoryId: category.id, + firstResponseAt: { not: null }, + open: false, + }, + }); + stats = { + avgResolutionTime: ms(getAvgResolutionTime(closedTickets), { long: true }), + avgResponseTime: ms(getAvgResponseTime(closedTickets), { long: true }), + }; + this.client.keyv.set(statsCacheKey, stats, ms('1h')); + } + + const embeds = [ + new ExtendedEmbedBuilder() + .setColor(category.guild.primaryColour) + .setAuthor({ + iconURL: creator.displayAvatarURL(), + name: creator.displayName, + }) + .setDescription( + category.openingMessage + .replace(/{+\s?(user)?name\s?}+/gi, creator.user.toString()) + .replace(/{+\s?avgResponseTime\s?}+/gi, stats?.avgResponseTime) + .replace(/{+\s?avgResolutionTime\s?}+/gi, stats?.avgResolutionTime), + ), + ]; + + if (answers) { + embeds.push( + new ExtendedEmbedBuilder() + .setColor(category.guild.primaryColour) + .setFields( + category.questions + .map(q => ({ + name: q.label, + value: interaction.fields.getTextInputValue(q.id) || getMessage('ticket.answers.no_value'), + })), + ), + ); + } else if (topic) { + embeds.push( + new ExtendedEmbedBuilder() + .setColor(category.guild.primaryColour) + .setFields({ + name: getMessage('ticket.opening_message.fields.topic'), + value: topic, + }), + ); + } + + if (category.guild.footer) { + embeds[embeds.length - 1].setFooter({ + iconURL: guild.iconURL(), + text: category.guild.footer, + }); + } + + const components = new ActionRowBuilder(); + + if (topic || answers) { + components.addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ action: 'edit' })) + .setStyle(ButtonStyle.Secondary) + .setEmoji(getMessage('buttons.edit.emoji')) + .setLabel(getMessage('buttons.edit.text')), + ); + } + + if (category.guild.claimButton && category.claiming) { + components.addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ action: 'claim' })) + .setStyle(ButtonStyle.Secondary) + .setEmoji(getMessage('buttons.claim.emoji')) + .setLabel(getMessage('buttons.claim.text')), + ); + } + + if (category.guild.closeButton) { + components.addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ action: 'close' })) + .setStyle(ButtonStyle.Danger) + .setEmoji(getMessage('buttons.close.emoji')) + .setLabel(getMessage('buttons.close.text')), + ); + } + + const pings = category.pingRoles.map(r => `<@&${r}>`).join(' '); + const sent = await channel.send({ + components: components.components.length >= 1 ? [components] : [], + content: getMessage('ticket.opening_message.content', { + creator: interaction.user.toString(), + staff: pings ? pings + ',' : '', + }), + embeds, + }); + await sent.pin({ reason: 'Ticket opening message' }); + const pinned = channel.messages.cache.last(); + + if (pinned.system) { + pinned + .delete({ reason: 'Cleaning up system message' }) + .catch(() => this.client.log.warn('Failed to delete system pin message')); + } + + /** @type {import("discord.js").Message|undefined} */ + let message; + if (referencesMessage) { + referencesMessage = referencesMessage.split('/'); + /** @type {import("discord.js").Message} */ + message = await (await this.client.channels.fetch(referencesMessage[0]))?.messages.fetch(referencesMessage[1]); + if (message) { + // not worth the effort of making system messages work atm + if (message.system) { + referencesMessage = null; + message = null; + } else { + if (!message.member) { + try { + message.member = await message.guild.members.fetch(message.author.id); + } catch { + this.client.log.verbose('Failed to fetch member %s of %s', message.author.id, message.guild.id); + } + } + await channel.send({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(category.guild.primaryColour) + .setTitle(getMessage('ticket.references_message.title')) + .setDescription( + getMessage('ticket.references_message.description', { + author: message.author.toString(), + timestamp: ``, + url: message.url, + })), + new ExtendedEmbedBuilder({ + iconURL: guild.iconURL(), + text: category.guild.footer, + }) + .setColor(category.guild.primaryColour) + .setAuthor({ + iconURL: message.member?.displayAvatarURL(), + name: message.member?.displayName || 'Unknown', + }) + .setDescription(message.content.substring(0, 1000) + (message.content.length > 1000 ? '...' : '')), + ], + }); + + } + + } + } else if (referencesTicketId) { + // TODO: add portal url + const ticket = await this.client.prisma.ticket.findUnique({ where: { id: referencesTicketId } }); + if (ticket) { + const embed = new ExtendedEmbedBuilder({ + iconURL: guild.iconURL(), + text: category.guild.footer, + }) + .setColor(category.guild.primaryColour) + .setTitle(getMessage('ticket.references_ticket.title')) + .setDescription(getMessage('ticket.references_ticket.description')) + .setFields([ + { + inline: true, + name: getMessage('ticket.references_ticket.fields.number'), + value: inlineCode(ticket.number), + }, + { + inline: true, + name: getMessage('ticket.references_ticket.fields.date'), + value: ``, + }, + ]); + if (ticket.topic) { + embed.addFields({ + inline: false, + name: getMessage('ticket.references_ticket.fields.topic'), + value: decrypt(ticket.topic), + }); + } + await channel.send({ embeds: [embed] }); + } + } + + const data = { + category: { connect: { id: categoryId } }, + createdBy: { + connectOrCreate: { + create: { id: interaction.user.id }, + where: { id: interaction.user.id }, + }, + }, + guild: { connect: { id: category.guild.id } }, + id: channel.id, + number, + openingMessageId: sent.id, + topic: topic ? encrypt(topic) : null, + }; + if (referencesTicketId) data.referencesTicket = { connect: { id: referencesTicketId } }; + if (answers) data.questionAnswers = { createMany: { data: answers } }; + + await interaction.editReply({ + components: [], + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: guild.iconURL(), + text: category.guild.footer, + }) + .setColor(category.guild.successColour) + .setTitle(getMessage('ticket.created.title')) + .setDescription(getMessage('ticket.created.description', { channel: channel.toString() })), + ], + }); + + try { + const ticket = await this.client.prisma.ticket.create({ data }); + this.$count.categories[categoryId].total++; + this.$count.categories[categoryId][creator.id]++; + + if (category.cooldown) { + const cacheKey = `cooldowns/category-member:${category.id}-${ticket.createdById}`; + const expiresAt = ticket.createdAt.getTime() + category.cooldown; + const TTL = category.cooldown; + await this.client.keyv.set(cacheKey, expiresAt, TTL); + } + + if (category.guild.archive && message) { + if ( + await this.client.prisma.archivedMessage.findUnique({ where: { id: message.id } }) || + await this.archiver.saveMessage(ticket.id, message, true) + ) { + await this.client.prisma.ticket.update({ + data: { referencesMessageId: message.id }, + where: { id: ticket.id }, + }); + } + } + + logTicketEvent(this.client, { + action: 'create', + target: { + id: ticket.id, + name: channel.toString(), + }, + userId: interaction.user.id, + }); + } catch (error) { + const ref = require('crypto').randomUUID(); + this.client.log.warn.tickets('An error occurred whilst creating ticket', channel.id); + this.client.log.error.tickets(ref); + this.client.log.error.tickets(error); + await interaction.editReply({ + components: [], + embeds: [ + new ExtendedEmbedBuilder() + .setColor('Orange') + .setTitle(getMessage('misc.error.title')) + .setDescription(getMessage('misc.error.description')) + .addFields({ + name: getMessage('misc.error.fields.identifier'), + value: inlineCode(ref), + }), + ], + }); + } + + const workingHours = category.guild.workingHours; + const timezone = workingHours[0]; + workingHours.shift(); // remove timezone + const now = spacetime.now(timezone); + const currentHours = workingHours[now.day()]; + const start = now.time(currentHours[0]); + const end = now.time(currentHours[1]); + let working = true; + + if (currentHours[0] === currentHours[1] || now.isAfter(end)) { // staff have the day off or have finished for the day + // first look for the next working day *this* week (after today) + let nextIndex = workingHours.findIndex((hours, i) => i > now.day() && hours[0] !== hours[1]); + // if there isn't one, look for the next working day *next* week (before and including today's weekday) + if (!nextIndex) nextIndex = workingHours.findIndex((hours, i) => i <= now.day() && hours[0] !== hours[1]); + if (nextIndex) { + working = false; + const next = workingHours[nextIndex]; + let then = now.add(nextIndex - now.day(), 'day'); + if (nextIndex <= now.day()) then = then.add(1, 'week'); + const timestamp = Math.ceil(then.time(next[0]).goto('utc').d.getTime() / 1000); // in seconds + await channel.send({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(category.guild.primaryColour) + .setTitle(getMessage('ticket.working_hours.next.title')) + .setDescription(getMessage('ticket.working_hours.next.description', { timestamp })), + ], + }); + } + } else if (now.isBefore(start)) { // staff haven't started working yet + working = false; + const timestamp = Math.ceil(start.goto('utc').d.getTime() / 1000); // in seconds + await channel.send({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(category.guild.primaryColour) + .setTitle(getMessage('ticket.working_hours.today.title')) + .setDescription(getMessage('ticket.working_hours.today.description', { timestamp })), + ], + }); + } + + + if (working) { + let online = 0; + for (const [, member] of channel.members) { + if (!await isStaff(channel.guild, member.id)) continue; + if (member.presence && member.presence !== 'offline') online++; + } + if (online === 0) { + await channel.send({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(category.guild.primaryColour) + .setTitle(getMessage('ticket.offline.title')) + .setDescription(getMessage('ticket.offline.description')), + ], + }); + this.client.keyv.set(`offline/${channel.id}`, Date.now(), ms('1h')); + } + } + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction} interaction + */ + async claim(interaction) { + const ticket = await this.client.prisma.ticket.findUnique({ + include: { + _count: { select: { questionAnswers: true } }, + category: true, + guild: true, + }, + where: { id: interaction.channel.id }, + }); + const getMessage = this.client.i18n.getLocale(ticket.guild.locale); + + if (!(await isStaff(interaction.guild, interaction.user.id))) { // if user is not staff + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.errorColour) + .setTitle(getMessage('commands.slash.claim.not_staff.title')) + .setDescription(getMessage('commands.slash.claim.not_staff.description')), + ], + }); + } + + await Promise.all([ + interaction.channel.permissionOverwrites.edit(interaction.user, { 'ViewChannel': true }, `Ticket claimed by ${interaction.user.tag}`), + ...ticket.category.staffRoles.map(role => interaction.channel.permissionOverwrites.edit(role, { 'ViewChannel': false }, `Ticket claimed by ${interaction.user.tag}`)), + this.client.prisma.ticket.update({ + data: { + claimedBy: { + connectOrCreate: { + create: { id: interaction.user.id }, + where: { id: interaction.user.id }, + }, + }, + }, + where: { id: interaction.channel.id }, + }), + ]); + + const openingMessage = await interaction.channel.messages.fetch(ticket.openingMessageId); + + if (openingMessage && openingMessage.components.length !== 0) { + const components = new ActionRowBuilder(); + + if (ticket.topic || ticket._count.questionAnswers !== 0) { + components.addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ action: 'edit' })) + .setStyle(ButtonStyle.Secondary) + .setEmoji(getMessage('buttons.edit.emoji')) + .setLabel(getMessage('buttons.edit.text')), + ); + } + + if (ticket.guild.claimButton && ticket.category.claiming) { + components.addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ action: 'unclaim' })) + .setStyle(ButtonStyle.Secondary) + .setEmoji(getMessage('buttons.unclaim.emoji')) + .setLabel(getMessage('buttons.unclaim.text')), + ); + } + + if (ticket.guild.closeButton) { + components.addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ action: 'close' })) + .setStyle(ButtonStyle.Danger) + .setEmoji(getMessage('buttons.close.emoji')) + .setLabel(getMessage('buttons.close.text')), + ); + } + + await openingMessage.edit({ components: [components] }); + } + + await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(ticket.guild.primaryColour) + .setDescription(getMessage('ticket.claimed', { user: interaction.user.toString() })), + ], + }); + + logTicketEvent(this.client, { + action: 'claim', + target: { + id: ticket.id, + name: interaction.channel.toString(), + }, + userId: interaction.user.id, + }); + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction} interaction + */ + async release(interaction) { + const ticket = await this.client.prisma.ticket.findUnique({ + include: { + _count: { select: { questionAnswers: true } }, + category: true, + guild: true, + }, + where: { id: interaction.channel.id }, + }); + const getMessage = this.client.i18n.getLocale(ticket.guild.locale); + + if (!(await isStaff(interaction.guild, interaction.user.id))) { // if user is not staff + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.errorColour) + .setTitle(getMessage('commands.slash.claim.not_staff.title')) + .setDescription(getMessage('commands.slash.claim.not_staff.description')), + ], + }); + } + + await Promise.all([ + interaction.channel.permissionOverwrites.delete(interaction.user, `Ticket released by ${interaction.user.tag}`), + ...ticket.category.staffRoles.map(role => interaction.channel.permissionOverwrites.edit(role, { 'ViewChannel': true }, `Ticket released by ${interaction.user.tag}`)), + this.client.prisma.ticket.update({ + data: { claimedBy: { disconnect: true } }, + where: { id: interaction.channel.id }, + }), + ]); + + const openingMessage = await interaction.channel.messages.fetch(ticket.openingMessageId); + + if (openingMessage && openingMessage.components.length !== 0) { + const components = new ActionRowBuilder(); + + if (ticket.topic || ticket._count.questionAnswers !== 0) { + components.addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ action: 'edit' })) + .setStyle(ButtonStyle.Secondary) + .setEmoji(getMessage('buttons.edit.emoji')) + .setLabel(getMessage('buttons.edit.text')), + ); + } + + if (ticket.guild.claimButton && ticket.category.claiming) { + components.addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ action: 'claim' })) + .setStyle(ButtonStyle.Secondary) + .setEmoji(getMessage('buttons.claim.emoji')) + .setLabel(getMessage('buttons.claim.text')), + ); + } + + if (ticket.guild.closeButton) { + components.addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ action: 'close' })) + .setStyle(ButtonStyle.Danger) + .setEmoji(getMessage('buttons.close.emoji')) + .setLabel(getMessage('buttons.close.text')), + ); + } + + await openingMessage.edit({ components: [components] }); + } + + await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(ticket.guild.primaryColour) + .setDescription(getMessage('ticket.released', { user: interaction.user.toString() })), + ], + }); + + logTicketEvent(this.client, { + action: 'unclaim', + target: { + id: ticket.id, + name: interaction.channel.toString(), + }, + userId: interaction.user.id, + }); + } + + buildFeedbackModal(locale, id) { + const getMessage = this.client.i18n.getLocale(locale); + return new ModalBuilder() + .setCustomId(JSON.stringify({ + action: 'feedback', + ...id, + })) + .setTitle(getMessage('modals.feedback.title')) + .setComponents( + new ActionRowBuilder() + .setComponents( + new TextInputBuilder() + .setCustomId('rating') + .setLabel(getMessage('modals.feedback.rating.label')) + .setStyle(TextInputStyle.Short) + .setMaxLength(3) + .setMinLength(1) + .setPlaceholder(getMessage('modals.feedback.rating.placeholder')) + .setRequired(true), + ), + new ActionRowBuilder() + .setComponents( + new TextInputBuilder() + .setCustomId('comment') + .setLabel(getMessage('modals.feedback.comment.label')) + .setStyle(TextInputStyle.Paragraph) + .setMaxLength(1000) + .setMinLength(4) + .setPlaceholder(getMessage('modals.feedback.comment.placeholder')) + .setRequired(false), + ), + ); + } + + + /** + * @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction} interaction + */ + async beforeRequestClose(interaction) { + const ticket = await this.getTicket(interaction.channel.id); + if (!ticket) { + await interaction.deferReply({ ephemeral: true }); + const { + errorColour, + footer, + locale, + } = await this.client.prisma.guild.findUnique({ + select: { + errorColour: true, + locale: true, + }, + where: { id: interaction.guild.id }, + }); + const getMessage = this.client.i18n.getLocale(locale); + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: footer, + }) + .setColor(errorColour) + .setTitle(getMessage('misc.not_ticket.title')) + .setDescription(getMessage('misc.not_ticket.description')), + ], + }); + } + + const getMessage = this.client.i18n.getLocale(ticket.guild.locale); + const staff = await isStaff(interaction.guild, interaction.user.id); + const reason = interaction.options?.getString('reason', false) || null; // ?. because it could be a button interaction + + if (ticket.createdById !== interaction.user.id && !staff) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(ticket.guild.errorColour) + .setTitle(getMessage('ticket.close.forbidden.title')) + .setDescription(getMessage('ticket.close.forbidden.description')), + ], + }); + } + + if ( + ticket.createdById === interaction.user.id && + ticket.category.enableFeedback && + !ticket.feedback + ) { + return await interaction.showModal(this.buildFeedbackModal(ticket.guild.locale, { + next: 'requestClose', + reason, // known issue: a reason longer than a few words will cause an error due to 100 character custom_id limit + })); + } + + // not showing feedback, so send the close request + + // defer asap + await interaction.deferReply(); + + // if the creator isn't in the guild , close the ticket immediately + // (although leaving should cause the ticket to be closed anyway) + try { + await interaction.guild.members.fetch(ticket.createdById); + } catch { + return this.finallyClose(ticket.id, { reason }); + } + + this.requestClose(interaction, reason); + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction|import("discord.js").ModalSubmitInteraction} interaction + * @param {string} reason + */ + async requestClose(interaction, reason) { + // interaction could be command, button. or modal + const ticket = await this.getTicket(interaction.channel.id); + const getMessage = this.client.i18n.getLocale(ticket.guild.locale); + const staff = interaction.user.id !== ticket.createdById && await isStaff(interaction.guild, interaction.user.id); + const closeButtonId = { + action: 'close', + expect: staff ? 'user' : 'staff', + }; + const embed = new ExtendedEmbedBuilder(/* { + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + } */) + .setColor(ticket.guild.primaryColour) + .setTitle(getMessage(`ticket.close.${staff ? 'staff' : 'user'}_request.title`, { requestedBy: interaction.member.displayName })); + + if (staff) { + embed.setDescription( + getMessage('ticket.close.staff_request.description', { requestedBy: interaction.user.toString() }) + + (ticket.guild.archive ? getMessage('ticket.close.staff_request.archived') : ''), + ); + } + + const sent = await interaction.editReply({ + components: [ + new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ + accepted: true, + ...closeButtonId, + })) + .setStyle(ButtonStyle.Success) + .setEmoji(getMessage('buttons.accept_close_request.emoji')) + .setLabel(getMessage('buttons.accept_close_request.text')), + new ButtonBuilder() + .setCustomId(JSON.stringify({ + accepted: false, + ...closeButtonId, + })) + .setStyle(ButtonStyle.Danger) + .setEmoji(getMessage('buttons.reject_close_request.emoji')) + .setLabel(getMessage('buttons.reject_close_request.text')), + ), + ], + content: staff ? `<@${ticket.createdById}>` : '', // ticket.category.pingRoles.map(r => `<@&${r}>`).join(' ') + embeds: [embed], + }); + + this.$stale.set(ticket.id, { + closeAt: ticket.guild.autoClose ? Date.now() + ticket.guild.autoClose : null, + closedBy: interaction.user.id, // null if set as stale due to inactivity + message: sent, + messages: 0, + reason, + staleSince: Date.now(), + }); + + if (ticket.priority && ticket.priority !== 'LOW') { + await this.client.prisma.ticket.update({ + data: { priority: 'LOW' }, + where: { id: ticket.id }, + }); + } + } + + /** + * @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction|import("discord.js").ModalSubmitInteraction} interaction + */ + async acceptClose(interaction) { + const ticket = await this.getTicket(interaction.channel.id); + const $ticket = this.$stale.get(interaction.channel.id); + const getMessage = this.client.i18n.getLocale(ticket.guild.locale); + await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.successColour) + .setTitle(getMessage('ticket.close.closed.title')) + .setDescription(getMessage('ticket.close.closed.description')), + ], + }); + await new Promise(resolve => setTimeout(resolve, 5000)); + await this.finallyClose(interaction.channel.id, $ticket); + } + + /** + * close a ticket + * @param {string} ticketId + */ + async finallyClose(ticketId, { + closedBy = null, + reason = null, + }) { + if (this.$stale.has(ticketId)) this.$stale.delete(ticketId); + let ticket = await this.getTicket(ticketId); + const getMessage = this.client.i18n.getLocale(ticket.guild.locale); + this.$count.categories[ticket.categoryId].total -= 1; + this.$count.categories[ticket.categoryId][ticket.createdById] -= 1; + + const { _count: { archivedMessages } } = await this.client.prisma.ticket.findUnique({ + select: { _count: { select: { archivedMessages: true } } }, + where: { id: ticket.id }, + }); + + /** @type {import("@prisma/client").Ticket} */ + const data = { + closedAt: new Date(), + closedBy: closedBy && { + connectOrCreate: { + create: { id: closedBy }, + where: { id: closedBy }, + }, + } || undefined, // Prisma wants undefined not null because it is a relation + closedReason: reason && encrypt(reason), + messageCount: archivedMessages, + open: false, + }; + + /** @type {import("discord.js").TextChannel} */ + const channel = this.client.channels.cache.get(ticketId); + if (channel) { + const pinned = await channel.messages.fetchPinned(); + data.pinnedMessageIds = [...pinned.keys()]; + } + + ticket = await this.client.prisma.ticket.update({ + data, + include: { + category: true, + feedback: true, + guild: true, + }, + where: { id: ticket.id }, + }); + + if (channel?.deletable) { + const member = closedBy ? channel.guild.members.cache.get(closedBy) : null; + await channel.delete('Ticket closed' + (member ? ` by ${member.displayName}` : '') + reason ? `: ${reason}` : ''); + } + + if (closedBy) { + logTicketEvent(this.client, { + action: 'close', + target: { + id: ticket.id, + name: `${ticket.category.name} **#${ticket.number}**`, + }, + userId: closedBy, + }); + } + + try { + const creator = channel?.guild.members.cache.get(ticket.createdById); + if (creator) { + const embed = new ExtendedEmbedBuilder({ + iconURL: channel.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.primaryColour) + .setTitle(getMessage('dm.closed.title')) + .addFields([ + { + inline: true, + name: getMessage('dm.closed.fields.ticket'), + value: `${ticket.category.name} **#${ticket.number}**`, + }, + + ]); + if (ticket.topic) { + embed.addFields({ + inline: true, + name: getMessage('dm.closed.fields.topic'), + value: decrypt(ticket.topic), + }); + } + + embed.addFields([ + { + inline: true, + name: getMessage('dm.closed.fields.created'), + value: ``, + }, + { + inline: true, + name: getMessage('dm.closed.fields.closed.name'), + value: getMessage('dm.closed.fields.closed.value', { + duration: ms(ticket.closedAt - ticket.createdAt, { long: true }), + timestamp: ``, + }), + }, + ]); + + if (ticket.firstResponseAt) { + embed.addFields({ + inline: true, + name: getMessage('dm.closed.fields.response'), + value: ms(ticket.firstResponseAt - ticket.createdAt, { long: true }), + }); + } + + if (ticket.feedback) { + embed.addFields({ + inline: true, + name: getMessage('dm.closed.fields.feedback'), + value: Array(ticket.feedback.rating).fill('⭐').join(' ') + ` (${ticket.feedback.rating}/5)`, + }); + } + + if (ticket.closedById) { + embed.addFields({ + inline: true, + name: getMessage('dm.closed.fields.closed_by'), + value: `<@${ticket.closedById}>`, + }); + } + + + if (reason) { + embed.addFields({ + inline: true, + name: getMessage('dm.closed.fields.reason'), + value: reason, + }); + } + + if (ticket.guild.archive) embed.setDescription(getMessage('dm.closed.archived', { guild: channel.guild.name })); + + await creator.send({ embeds: [embed] }); + } + } catch (error) { + this.client.log.error(error); + } + + } +}; diff --git a/src/lib/tickets/utils.js b/src/lib/tickets/utils.js new file mode 100644 index 0000000..4949e05 --- /dev/null +++ b/src/lib/tickets/utils.js @@ -0,0 +1,76 @@ +const { + ActionRowBuilder, + EmbedBuilder, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, +} = require('discord.js'); +const emoji = require('node-emoji'); + +module.exports = { + /** + * @param {import("client")} client + * @param {import("discord.js").ButtonInteraction|import("discord.js").SelectMenuInteraction} interaction + */ + async useGuild(client, interaction, { + referencesMessage, + referencesTicketId, + topic, + }) { + const settings = await client.prisma.guild.findUnique({ + select: { + categories: true, + errorColour: true, + locale: true, + primaryColour: true, + }, + where: { id: interaction.guild.id }, + }); + const getMessage = client.i18n.getLocale(settings.locale); + if (settings.categories.length === 0) { + interaction.reply({ + components: [], + embeds: [ + new EmbedBuilder() + .setColor(settings.errorColour) + .setTitle(getMessage('misc.no_categories.title')) + .setDescription(getMessage('misc.no_categories.description')), + ], + ephemeral: true, + }); + } else if (settings.categories.length === 1) { + await client.tickets.create({ + categoryId: settings.categories[0].id, + interaction, + referencesMessage, + referencesTicketId, + topic, + }); + } else { + await interaction.reply({ + components: [ + new ActionRowBuilder() + .setComponents( + new StringSelectMenuBuilder() + .setCustomId(JSON.stringify({ + action: 'create', + referencesMessage, + referencesTicketId, + topic, + })) + .setPlaceholder(getMessage('menus.category.placeholder')) + .setOptions( + settings.categories.map(category => + new StringSelectMenuOptionBuilder() + .setValue(String(category.id)) + .setLabel(category.name) + .setDescription(category.description) + .setEmoji(emoji.hasEmoji(category.emoji) ? emoji.get(category.emoji) : { id: category.emoji }), + ), + ), + ), + ], + ephemeral: true, + }); + } + }, +}; \ No newline at end of file diff --git a/src/lib/updates.js b/src/lib/updates.js new file mode 100644 index 0000000..4daec76 --- /dev/null +++ b/src/lib/updates.js @@ -0,0 +1,89 @@ +const semver = require('semver'); +const { short } = require('leeks.js'); +const ExtendedEmbedBuilder = require('./embed'); +const { version: currentVersion } = require('../../package.json'); + +/** @param {import("client")} client */ +module.exports = client => { + client.log.info('Checking for updates...'); + fetch('https://api.github.com/repos/discord-tickets/bot/releases') + .then(res => res.json()) + .then(async json => { + // releases are ordered by date, so a patch for an old version could be before the latest version + const releases = json + .filter(release => !release.prerelease) + .sort((a, b) => semver.compare(semver.coerce(b.tag_name)?.version, semver.coerce(a.tag_name)?.version)); + const latestRelease = releases[0]; + const latestVersion = semver.coerce(latestRelease.tag_name)?.version; + const compared = semver.compare(latestVersion, currentVersion); + + switch (compared) { + case -1: { + client.log.notice('You are running a pre-release version of Discord Tickets'); + break; + } + case 0: { + client.log.info('No updates available'); + break; + } + case 1: { + let currentRelease = releases.findIndex(release => semver.coerce(release.tag_name)?.version === currentVersion); + if (currentRelease === -1) return client.log.warn('Failed to find current release'); + const behind = currentRelease; + currentRelease = releases[currentRelease]; + const changelog = `https://discordtickets.app/changelogs/v${latestVersion.replaceAll('.', '') }/`; + const guide = 'https://discordtickets.app/self-hosting/updating/'; + const { default: boxen } = await import('boxen'); + + client.log.notice( + short('&r&6A new version of Discord Tickets is available (&c%s&6 -> &a%s&6)&r\n'), + currentVersion, + latestVersion, + boxen( + short([ // uses template literals to ensure boxen adds the correct padding + `&6You are &f${behind}&6 version${behind === 1 ? '' : 's'} behind the latest version, &a${latestVersion}&6.&r`, + `&6Changelog: &e${changelog}&r`, + `&6Update guide: &e${guide}&r`, + ].join('\n')), + { + align: 'center', + borderColor: 'yellow', + borderStyle: 'round', + margin: 1, + padding: 1, + title: 'Update available', + }), + ); + + if (process.env.PUBLIC_BOT !== 'true') { + const guilds = await client.prisma.guild.findMany({ where: { logChannel: { not: null } } }); + for (const guild of guilds) { + const getMessage = client.i18n.getLocale(guild.locale); + await client.channels.cache.get(guild.logChannel).send({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor('Blurple') + .setAuthor({ + iconURL: latestRelease.author.avatar_url, + name: latestRelease.author.login, + }) + .setTitle(getMessage('misc.update.title')) + .setDescription(getMessage('misc.update.fields.links.value', { + changelog, + github: latestRelease.html_url, + guide, + version: latestRelease.tag_name, + })), + ], + }); + } + } + break; + } + } + }) + .catch(error => { + client.log.warn('Failed to check for updates'); + client.log.error(error); + }); +}; \ No newline at end of file diff --git a/src/lib/users.js b/src/lib/users.js new file mode 100644 index 0000000..c263662 --- /dev/null +++ b/src/lib/users.js @@ -0,0 +1,48 @@ +const { PermissionsBitField } = require('discord.js'); + +/** + * + * @param {import("discord.js").Client} client + * @param {string} userId + * @returns {Promise} + */ +module.exports.getCommonGuilds = (client, userId) => client.guilds.cache.filter(guild => guild.members.cache.has(userId)); + +/** + * @param {import("discord.js").Guild} guild + * @returns {Promise} + */ +const updateStaffRoles = async guild => { + const { categories } = await guild.client.prisma.guild.findUnique({ + select: { categories: { select: { staffRoles: true } } }, + where: { id: guild.id }, + }); + const staffRoles = [ + ...new Set( + categories.reduce((acc, c) => { + acc.push(...c.staffRoles); + return acc; + }, []), + ), + ]; + await guild.client.keyv.set(`cache/guild-staff:${guild.id}`, staffRoles); + return staffRoles; +}; + +module.exports.updateStaffRoles = updateStaffRoles; + +/** + * + * @param {import("discord.js").Guild} guild + * @param {string} userId + * @returns {Promise} + */ +module.exports.isStaff = async (guild, userId) => { + /** @type {import("client")} */ + const client = guild.client; + if (client.supers.includes(userId)) return true; + const guildMember = guild.members.cache.get(userId) || await guild.members.fetch(userId); + if (guildMember.permissions.has(PermissionsBitField.Flags.ManageGuild)) return true; + const staffRoles = await client.keyv.get(`cache/guild-staff:${guild.id}`) || await updateStaffRoles(guild); + return staffRoles.some(r => guildMember.roles.cache.has(r)); +}; diff --git a/src/listeners/autocomplete/componentLoad.js b/src/listeners/autocomplete/componentLoad.js new file mode 100644 index 0000000..04a1743 --- /dev/null +++ b/src/listeners/autocomplete/componentLoad.js @@ -0,0 +1,16 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client.autocomplete, + event: 'componentLoad', + }); + } + + run(autocompleter) { + this.client.log.info.autocomplete(`Loaded "${autocompleter.id}" autocompleter`); + return true; + } +}; diff --git a/src/listeners/autocomplete/run.js b/src/listeners/autocomplete/run.js new file mode 100644 index 0000000..88bf76e --- /dev/null +++ b/src/listeners/autocomplete/run.js @@ -0,0 +1,19 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client.autocomplete, + event: 'run', + }); + } + + run({ + completer, + interaction, + }) { + this.client.log.verbose.autocomplete(`${interaction.user.tag} used the "${completer.id}" autocompleter`); + return true; + } +}; diff --git a/src/listeners/buttons/componentLoad.js b/src/listeners/buttons/componentLoad.js new file mode 100644 index 0000000..2bcc5c2 --- /dev/null +++ b/src/listeners/buttons/componentLoad.js @@ -0,0 +1,16 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client.buttons, + event: 'componentLoad', + }); + } + + run(button) { + this.client.log.info.buttons(`Loaded "${button.id}" button`); + return true; + } +}; diff --git a/src/listeners/buttons/error.js b/src/listeners/buttons/error.js new file mode 100644 index 0000000..d1f86d3 --- /dev/null +++ b/src/listeners/buttons/error.js @@ -0,0 +1,50 @@ +const { Listener } = require('@eartharoid/dbf'); +const { + EmbedBuilder, + codeBlock, +} = require('discord.js'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client.buttons, + event: 'error', + }); + } + + async run({ + button, + error, + interaction, + }) { + const ref = require('crypto').randomUUID(); + this.client.log.error.buttons(ref); + this.client.log.error.buttons(`"${button.id}" button execution error:`, error); + let locale = null; + if (interaction.guild) { + locale = (await this.client.prisma.guild.findUnique({ + select: { locale: true }, + where: { id: interaction.guild.id }, + })).locale; + } + const getMessage = this.client.i18n.getLocale(locale); + const data = { + components: [], + embeds: [ + new EmbedBuilder() + .setColor('Orange') + .setTitle(getMessage('misc.error.title')) + .setDescription(getMessage('misc.error.description')) + .addFields([ + { + name: getMessage('misc.error.fields.identifier'), + value: codeBlock(ref), + }, + ]), + ], + }; + + interaction.reply(data).catch(() => interaction.editReply(data)); + } +}; diff --git a/src/listeners/buttons/run.js b/src/listeners/buttons/run.js new file mode 100644 index 0000000..d33167e --- /dev/null +++ b/src/listeners/buttons/run.js @@ -0,0 +1,19 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client.buttons, + event: 'run', + }); + } + + run({ + button, + interaction, + }) { + this.client.log.info.buttons(`${interaction.user.tag} used the "${button.id}" button`); + return true; + } +}; diff --git a/src/listeners/channelDelete.js b/src/listeners/channelDelete.js deleted file mode 100644 index 3e4f430..0000000 --- a/src/listeners/channelDelete.js +++ /dev/null @@ -1,23 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); - -module.exports = class ChannelDeleteEventListener extends EventListener { - constructor(client) { - super(client, { event: 'channelDelete' }); - } - - async execute(channel) { - if (!channel.guild || channel.type !== 'GUILD_TEXT') return; - - // resolve ticket by id - const t_row = await this.client.tickets.resolve(channel.id, channel.guild.id); - if (!t_row) return; - - // fetch user from audit logs - const logEntry = (await channel.guild.fetchAuditLogs({ type: 'CHANNEL_DELETE' })).entries.find(entry => - entry.target.id === channel.id - ); - if (logEntry.executor.id === this.client.user.id) return; - - await this.client.tickets.close(t_row.id, logEntry?.executor?.id || null, channel.guild.id, 'Channel was deleted'); - } -}; \ No newline at end of file diff --git a/src/listeners/client/channelDelete.js b/src/listeners/client/channelDelete.js new file mode 100644 index 0000000..712cd92 --- /dev/null +++ b/src/listeners/client/channelDelete.js @@ -0,0 +1,26 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client, + event: 'channelDelete', + }); + } + + async run(channel) { + /** @type {import("client")} */ + const client = this.client; + + const ticket = await client.prisma.ticket.findUnique({ + include: { guild: true }, + where: { id: channel.id }, + }); + + if (ticket?.open) { + await client.tickets.finallyClose(ticket.id, { reason: 'channel deleted' }); + this.client.log.info.tickets(`Closed ticket ${ticket.id} because the channel was deleted`); + } + } +}; diff --git a/src/listeners/client/error.js b/src/listeners/client/error.js new file mode 100644 index 0000000..d101a43 --- /dev/null +++ b/src/listeners/client/error.js @@ -0,0 +1,15 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client, + event: 'error', + }); + } + + run(error) { + this.client.log.error(error); + } +}; diff --git a/src/listeners/client/guildCreate.js b/src/listeners/client/guildCreate.js new file mode 100644 index 0000000..6867f0f --- /dev/null +++ b/src/listeners/client/guildCreate.js @@ -0,0 +1,30 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client, + event: 'guildCreate', + }); + } + + /** + * @param {import("discord.js").Guild} guild + */ + async run(guild) { + /** @type {import("client")} */ + const client = this.client; + + this.client.log.success(`Added to guild "${guild.name}"`); + let settings = await client.prisma.guild.findUnique({ where: { id: guild.id } }); + if (!settings) { + settings = await client.prisma.guild.create({ + data: { + id: guild.id, + locale: client.i18n.locales.includes(guild.preferredLocale) ? guild.preferredLocale : 'en-GB', + }, + }); + } + } +}; diff --git a/src/listeners/client/guildDelete.js b/src/listeners/client/guildDelete.js new file mode 100644 index 0000000..01eb59c --- /dev/null +++ b/src/listeners/client/guildDelete.js @@ -0,0 +1,15 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client, + event: 'guildDelete', + }); + } + + run(guild) { + this.client.log.info(`Removed from guild "${guild.name}"`); + } +}; diff --git a/src/listeners/client/guildMemberRemove.js b/src/listeners/client/guildMemberRemove.js new file mode 100644 index 0000000..eb14514 --- /dev/null +++ b/src/listeners/client/guildMemberRemove.js @@ -0,0 +1,32 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client, + event: 'guildMemberRemove', + }); + } + + /** + * + * @param {import("discord.js").GuildMember} member + */ + async run(member) { + /** @type {import("client")} */ + const client = this.client; + + const tickets = await client.prisma.ticket.findMany({ + where: { + createdById: member.id, + guildId: member.guild.id, + open: true, + }, + }); + + for (const ticket of tickets) { + await client.tickets.finallyClose(ticket.id, { reason: 'user left server' }); + } + } +}; diff --git a/src/listeners/client/messageCreate.js b/src/listeners/client/messageCreate.js new file mode 100644 index 0000000..96a31d0 --- /dev/null +++ b/src/listeners/client/messageCreate.js @@ -0,0 +1,294 @@ +const { Listener } = require('@eartharoid/dbf'); +const { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle: { Success }, + ChannelType, + ComponentType, + EmbedBuilder, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, +} = require('discord.js'); +const { + getCommonGuilds, + isStaff, +} = require('../../lib/users'); +const ms = require('ms'); +const emoji = require('node-emoji'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client, + event: 'messageCreate', + }); + } + + /** + * @param {import('@prisma/client').Guild} settings + * @param {import("discord.js").ButtonInteraction|import("discord.js").SelectMenuInteraction} interaction + */ + async useGuild(settings, interaction, topic) { + const getMessage = this.client.i18n.getLocale(settings.locale); + if (settings.categories.length === 0) { + interaction.update({ + components: [], + embeds: [ + new EmbedBuilder() + .setColor(settings.errorColour) + .setTitle(getMessage('misc.no_categories.title')) + .setDescription(getMessage('misc.no_categories.description')), + ], + }); + } else if (settings.categories.length === 1) { + await this.client.tickets.create({ + categoryId: settings.categories[0].id, + interaction, + topic, + }); + } else { + await interaction.update({ + components: [ + new ActionRowBuilder() + .setComponents( + new StringSelectMenuBuilder() + .setCustomId(JSON.stringify({ + action: 'create', + topic, + })) + .setPlaceholder(getMessage('menus.category.placeholder')) + .setOptions( + settings.categories.map(category => + new StringSelectMenuOptionBuilder() + .setValue(String(category.id)) + .setLabel(category.name) + .setDescription(category.description) + .setEmoji(emoji.hasEmoji(category.emoji) ? emoji.get(category.emoji) : { id: category.emoji }), + ), + ), + ), + ], + }); + interaction.message.awaitMessageComponent({ + componentType: ComponentType.SelectMenu, + filter: () => true, + time: ms('30s'), + }) + .then(async () => { + interaction.message.delete(); + }) + .catch(error => { + if (error) this.client.log.error(error); + interaction.message.delete(); + }); + } + + } + + /** + * @param {import("discord.js").Message} message + */ + async run(message) { + /** @type {import("client")} */ + const client = this.client; + + if (message.channel.type === ChannelType.DM) { + if (message.author.bot) return false; + const commonGuilds = await getCommonGuilds(client, message.author.id); + if (commonGuilds.size === 0) { + return false; + } else if (commonGuilds.size === 1) { + const settings = await client.prisma.guild.findUnique({ + select: { + categories: true, + errorColour: true, + locale: true, + primaryColour: true, + }, + where: { id: commonGuilds.at(0).id }, + }); + const getMessage = client.i18n.getLocale(settings.locale); + const sent = await message.reply({ + components: [ + new ActionRowBuilder() + .setComponents( + new ButtonBuilder() + .setCustomId(message.id) + .setStyle(Success) + .setLabel(getMessage('buttons.confirm_open.text')) + .setEmoji(getMessage('buttons.confirm_open.emoji')), + ), + ], + embeds: [ + new EmbedBuilder() + .setColor(settings.primaryColour) + .setTitle(getMessage('dm.confirm_open.title')) + .setDescription(message.content), + ], + }); + sent.awaitMessageComponent({ + componentType: ComponentType.Button, + filter: () => true, + time: ms('30s'), + }) + .then(async interaction => await this.useGuild(settings, interaction, message.content)) + .catch(error => { + if (error) client.log.error(error); + sent.delete(); + }); + } else { + const getMessage = client.i18n.getLocale(); + const sent = await message.reply({ + components: [ + new ActionRowBuilder() + .setComponents( + new StringSelectMenuBuilder() + .setCustomId(message.id) + .setPlaceholder(getMessage('menus.guild.placeholder')) + .setOptions( + commonGuilds.map(g => + new StringSelectMenuOptionBuilder() + .setValue(String(g.id)) + .setLabel(g.name), + ), + ), + ), + + ], + }); + sent.awaitMessageComponent({ + componentType: ComponentType.SelectMenu, + filter: () => true, + time: ms('30s'), + }) + .then(async interaction => { + const settings = await client.prisma.guild.findUnique({ + select: { + categories: true, + errorColour: true, + locale: true, + primaryColour: true, + }, + where: { id: interaction.values[0] }, + }); + await this.useGuild(settings, interaction, message.content); + }) + .catch(error => { + if (error) client.log.error(error); + sent.delete(); + }); + } + } else { + const settings = await client.prisma.guild.findUnique({ where: { id: message.guild.id } }); + if (!settings) return; + const getMessage = client.i18n.getLocale(settings.locale); + let ticket = await client.prisma.ticket.findUnique({ where: { id: message.channel.id } }); + + if (ticket) { + // archive messages + if (settings.archive) { + try { + await client.tickets.archiver.saveMessage(ticket.id, message); + } catch (error) { + client.log.warn('Failed to archive message', message.id); + client.log.error(error); + } + } + + if (!message.author.bot) { + // update user's message count + await client.prisma.user.upsert({ + create: { + id: message.author.id, + messageCount: 1, + }, + update: { messageCount: { increment: 1 } }, + where: { id: message.author.id }, + }); + + // set first and last message timestamps + const data = { lastMessageAt: new Date() }; + if ( + ticket.firstResponseAt === null && + await isStaff(message.guild, message.author.id) + ) data.firstResponseAt = new Date(); + ticket = await client.prisma.ticket.update({ + data, + where: { id: ticket.id }, + }); + + // if the ticket was set as stale, unset it + if (client.tickets.$stale.has(ticket.id)) { + const $ticket = client.tickets.$stale.get(ticket.id); + $ticket.messages++; + if ($ticket.messages >= 5) { + await message.channel.messages.delete($ticket.message.id); + client.tickets.$stale.delete(ticket.id); + } else { + client.tickets.$stale.set(ticket.id, $ticket); + } + } + } + + if (!message.author.bot) { + const key = `offline/${message.channel.id}`; + let online = 0; + for (const [, member] of message.channel.members) { + if (!await isStaff(message.channel.guild, member.id)) continue; + if (member.presence && member.presence !== 'offline') online++; + } + if (online === 0 && !client.keyv.has(key)) { + await message.channel.send({ + embeds: [ + new EmbedBuilder() + .setColor(settings.primaryColour) + .setTitle(getMessage('ticket.offline.title')) + .setDescription(getMessage('ticket.offline.description')), + ], + }); + client.keyv.set(key, Date.now(), ms('1h')); + } + } + } + + // auto-tag + if ( + !message.author.bot && + ( + (settings.autoTag === 'all') || + (settings.autoTag === 'ticket' && ticket) || + (settings.autoTag === '!ticket' && !ticket) || + (settings.autoTag.includes(message.channel.id)) + ) + ) { + const cacheKey = `cache/guild-tags:${message.guild.id}`; + let tags = await client.keyv.get(cacheKey); + if (!tags) { + tags = await client.prisma.tag.findMany({ + select: { + content: true, + id: true, + name: true, + regex: true, + }, + where: { guildId: message.guild.id }, + }); + client.keyv.set(cacheKey, tags, ms('1h')); + } + + const tag = tags.find(tag => message.content.match(new RegExp(tag.regex, 'mi'))); + if (tag) { + await message.reply({ + embeds: [ + new EmbedBuilder() + .setColor(settings.primaryColour) + .setDescription(tag.content), + ], + }); + } + + } + } + } +}; diff --git a/src/listeners/client/messageDelete.js b/src/listeners/client/messageDelete.js new file mode 100644 index 0000000..d6ba785 --- /dev/null +++ b/src/listeners/client/messageDelete.js @@ -0,0 +1,83 @@ +const { Listener } = require('@eartharoid/dbf'); +const { AuditLogEvent } = require('discord.js'); +const { logMessageEvent } = require('../../lib/logging'); +const Cryptr = require('cryptr'); +const { decrypt } = new Cryptr(process.env.ENCRYPTION_KEY); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client, + event: 'messageDelete', + }); + } + + /** + * @param {import("discord.js").Message} message + */ + async run(message) { + /** @type {import("client")} */ + const client = this.client; + + if (!message.guild) return; + + const ticket = await client.prisma.ticket.findUnique({ + include: { guild: true }, + where: { id: message.channel.id }, + }); + if (!ticket) return; + + let content = message.cleanContent; + + const logEvent = (await message.guild.fetchAuditLogs({ + limit: 1, + type: AuditLogEvent.MessageDelete, + })).entries.first(); + + if (ticket.guild.archive) { + try { + const archived = await client.prisma.archivedMessage.findUnique({ where: { id: message.id } }); + if (archived) { + if (!content) content = JSON.parse(decrypt(archived.content)).content; // won't be cleaned + await client.prisma.archivedMessage.update({ + data: { deleted: true }, + where: { id: message.id }, + }); + } + } catch (error) { + client.log.warn('Failed to "delete" archived message', message.id); + client.log.error(error); + } + } + + let { + executor, + target, + } = logEvent ?? {}; + + executor ||= undefined; + if (target?.id !== message.author?.id) executor = undefined; + + if (executor) { + try { + executor = await message.guild.members.fetch(executor.id); + } catch (error) { + client.log.error(error); + } + } + + if (message.author.id !== client.user.id && !message.flags.has('Ephemeral')) { + await logMessageEvent(this.client, { + action: 'delete', + diff: { + original: { content }, + updated: { content: '' }, + }, + executor, + target: message, + ticket, + }); + } + } +}; diff --git a/src/listeners/client/messageUpdate.js b/src/listeners/client/messageUpdate.js new file mode 100644 index 0000000..98ee177 --- /dev/null +++ b/src/listeners/client/messageUpdate.js @@ -0,0 +1,62 @@ +const { Listener } = require('@eartharoid/dbf'); +const { MessageFlagsBitField } = require('discord.js'); +const { logMessageEvent } = require('../../lib/logging'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client, + event: 'messageUpdate', + }); + } + + + /** + * @param {import("discord.js").Message} oldMessage + * @param {import("discord.js").Message} newMessage + */ + async run(oldMessage, newMessage) { + /** @type {import("client")} */ + const client = this.client; + + if (newMessage.partial) { + try { + newMessage = await newMessage.fetch(); + } catch (error) { + client.log.error(error); + } + } + + if (!newMessage.guild) return; + if (newMessage.flags.has(MessageFlagsBitField.Flags.Ephemeral)) return; + if (!newMessage.editedAt) return; + + const ticket = await client.prisma.ticket.findUnique({ + include: { guild: true }, + where: { id: newMessage.channel.id }, + }); + if (!ticket) return; + + if (ticket.guild.archive) { + try { + await client.tickets.archiver.saveMessage(ticket.id, newMessage); + } catch (error) { + client.log.warn('Failed to update archived message', newMessage.id); + client.log.error(error); + } + } + + if (newMessage.author.id === client.user.id) return; + + await logMessageEvent(this.client, { + action: 'update', + diff: { + original: { content: oldMessage.cleanContent }, + updated: { content: newMessage.cleanContent }, + }, + target: newMessage, + ticket, + }); + } +}; diff --git a/src/listeners/client/ready.js b/src/listeners/client/ready.js new file mode 100644 index 0000000..eb2e343 --- /dev/null +++ b/src/listeners/client/ready.js @@ -0,0 +1,244 @@ +const { Listener } = require('@eartharoid/dbf'); +const crypto = require('crypto'); +const { + getAvgResolutionTime, + getAvgResponseTime, +} = require('../../lib/stats'); +const ms = require('ms'); +const { version } = require('../../../package.json'); +const { msToMins } = require('../../lib/misc'); +const sync = require('../../lib/sync'); +const checkForUpdates = require('../../lib/updates'); +const { isStaff } = require('../../lib/users'); +const { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, +} = require('discord.js'); +const ExtendedEmbedBuilder = require('../../lib/embed'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client, + event: 'ready', + once: true, + }); + } + + async run() { + /** @type {import("client")} */ + const client = this.client; + + // process.title = `"[Discord Tickets] ${client.user.tag}"`; // too long and gets cut off + process.title = 'tickets'; + client.log.success('Connected to Discord as "%s"', client.user.tag); + + // fill cache + await sync(client); + + if (process.env.PUBLISH_COMMANDS === 'true') { + client.log.info('Automatically publishing commands...'); + client.commands.publish() + .then(commands => client.log.success('Published %d commands', commands?.size)) + .catch(client.log.error); + } + + if (process.env.PUBLIC_BOT === 'true' && !client.application.botPublic) { + client.log.warn('The `PUBLIC_BOT` environment variable is set to `true`, but the bot is not public.'); + } else if (process.env.PUBLIC_BOT === 'false' && client.application.botPublic) { + client.log.warn('Your bot is public, but public features are disabled. Set the `PUBLIC_BOT` environment variable to `true`, or make your bot private.'); + } + + // commands are not cached automatically + await client.application.commands.fetch(); + + // presence/activity + let next = 0; + const setPresence = async () => { + const cacheKey = 'cache/presence'; + let cached = await client.keyv.get(cacheKey); + if (!cached) { + const tickets = await client.prisma.ticket.findMany({ + select: { + closedAt: true, + createdAt: true, + firstResponseAt: true, + }, + }); + const closedTickets = tickets.filter(t => t.firstResponseAt && t.closedAt); + cached = { + avgResolutionTime: ms(getAvgResolutionTime(closedTickets)), + avgResponseTime: ms(getAvgResponseTime(closedTickets)), + openTickets: tickets.length - closedTickets.length, + totalTickets: tickets.length, + }; + await client.keyv.set(cacheKey, cached, ms('15m')); + } + const activity = client.config.presence.activities[next]; + activity.name = activity.name + .replace(/{+avgResolutionTime}+/gi, cached.avgResolutionTime) + .replace(/{+avgResponseTime}+/gi, cached.avgResponseTime) + .replace(/{+openTickets}+/gi, cached.openTickets) + .replace(/{+totalTickets}+/gi, cached.totalTickets); + client.user.setPresence({ + activities: [activity], + status: client.config.presence.status, + }); + next++; + if (next === client.config.presence.activities.length) next = 0; + }; + setPresence(); + if (client.config.presence.activities.length > 1) setInterval(() => setPresence(), client.config.presence.interval * 1000); + + // stats posting + if (client.config.stats) { + const send = async () => { + const tickets = await client.prisma.ticket.findMany({ + select: { + createdAt: true, + firstResponseAt: true, + }, + }); + const closedTickets = tickets.filter(t => t.closedAt); + const users = await client.prisma.user.findMany({ select: { messageCount: true } }); + const stats = { + activated_users: users.length, + arch: process.arch, + avg_resolution_time: msToMins(closedTickets.reduce((total, ticket) => total + (ticket.closedAt - ticket.createdAt), 0) ?? 1 / closedTickets.length), + avg_response_time: msToMins(closedTickets.reduce((total, ticket) => total + (ticket.firstResponseAt - ticket.createdAt), 0) ?? 1 / closedTickets.length), + categories: await client.prisma.category.count(), + database: process.env.DB_PROVIDER, + guilds: client.guilds.cache.size, + id: crypto.createHash('md5').update(client.user.id).digest('hex'), + members: client.guilds.cache.reduce((t, g) => t + g.memberCount, 0), + messages: users.reduce((total, user) => total + user.messageCount, 0), // don't count archivedMessage table rows, they can be deleted, + node: process.version, + os: process.platform, + tags: await client.prisma.tag.count(), + tickets: tickets.length, + version, + }; + try { + const res = await fetch('https://stats.discordtickets.app/api/v3/houston', { + body: JSON.stringify(stats), + headers: { 'content-type': 'application/json' }, + method: 'POST', + }); + if (!res.ok) throw res; + client.log.success('Posted client stats'); + client.log.verbose(stats); + client.log.debug(res); + } catch (res) { + client.log.error('An error occurred whilst posting stats', (await res.json())?.error); + client.log.debug(res); + } + }; + send(); + setInterval(() => send(), ms('12h')); + } + + if (client.config.updates) { + checkForUpdates(client); + setInterval(() => checkForUpdates(client), ms('1w')); + } + + // send inactivity warnings and close stale tickets + const staleInterval = ms('5m'); + setInterval(async () => { + // close stale tickets + for (const [ticketId, $] of client.tickets.$stale) { + const autoCloseAfter = $.closeAt - $.staleSince; + const halfway = $.closeAt - (autoCloseAfter / 2); + if (Date.now() >= halfway && Date.now() < halfway + staleInterval) { + const channel = client.channels.cache.get(ticketId); + if (!channel) continue; + const { guild } = await client.prisma.ticket.findUnique({ + select: { guild: true }, + where: { id: ticketId }, + }); + const getMessage = client.i18n.getLocale(guild.locale); + await channel.send({ + embeds: [ + new ExtendedEmbedBuilder() + .setColor(guild.primaryColour) + .setTitle(getMessage('ticket.closing_soon.title')) + .setDescription(getMessage('ticket.closing_soon.description', { timestamp: Math.floor($.closeAt / 1000) })), + ], + }); + } else if ($.closeAt < Date.now()) { + client.tickets.finallyClose(ticketId, $.reason); + } + } + + const guilds = await client.prisma.guild.findMany({ + include: { + tickets: { + include: { category: true }, + where: { open: true }, + }, + }, + // where: { staleAfter: { not: null } }, + where: { staleAfter: { gte: staleInterval } }, + }); + + // set inactive tickets as stale + for (const guild of guilds) { + for (const ticket of guild.tickets) { + if (client.tickets.$stale.has(ticket.id)) continue; + if (ticket.lastMessageAt && Date.now() - ticket.lastMessageAt > guild.staleAfter) { + /** @type {import("discord.js").TextChannel} */ + const channel = client.channels.cache.get(ticket.id); + const messages = (await channel.messages.fetch({ limit: 5 })).filter(m => m.author.id !== client.user.id); + let ping = ''; + + if (messages.size > 0) { + const lastMessage = messages.first(); + const staff = await isStaff(channel.guild, lastMessage.author.id); + if (staff) ping = lastMessage.author.toString(); + else ping = ticket.category.pingRoles.map(r => `<@&${r}>`).join(' '); + } + + const getMessage = client.i18n.getLocale(guild.locale); + const closeComamnd = client.application.commands.cache.find(c => c.name === 'close'); + const sent = await channel.send({ + components: [ + new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ action: 'close' })) + .setStyle(ButtonStyle.Danger) + .setEmoji(getMessage('buttons.close.emoji')) + .setLabel(getMessage('buttons.close.text')), + ), + ], + content: ping, + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: channel.guild.iconURL(), + text: guild.footer, + }) + .setColor(guild.primaryColour) + .setTitle(getMessage('ticket.inactive.title')) + .setDescription(getMessage('ticket.inactive.description', { + close: ``, + timestamp: Math.floor(ticket.lastMessageAt.getTime() / 1000), + })), + ], + }); + + client.tickets.$stale.set(ticket.id, { + closeAt: guild.autoClose ? Date.now() + guild.autoClose : null, + closedBy: null, + message: sent, + messages: 0, + reason: 'inactivity', + staleSince: Date.now(), + }); + } + } + } + }, staleInterval); + } +}; diff --git a/src/listeners/client/warn.js b/src/listeners/client/warn.js new file mode 100644 index 0000000..b37bf6c --- /dev/null +++ b/src/listeners/client/warn.js @@ -0,0 +1,15 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client, + event: 'warn', + }); + } + + run(warn) { + this.client.log.warn(warn); + } +}; diff --git a/src/listeners/commands/componentLoad.js b/src/listeners/commands/componentLoad.js new file mode 100644 index 0000000..019f604 --- /dev/null +++ b/src/listeners/commands/componentLoad.js @@ -0,0 +1,21 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client.commands, + event: 'componentLoad', + }); + } + + run(command) { + const types = { + 1: 'slash', + 2: 'user', + 3: 'message', + }; + this.client.log.info.commands(`Loaded "${command.name}" ${types[command.type]} command`); + return true; + } +}; diff --git a/src/listeners/commands/error.js b/src/listeners/commands/error.js new file mode 100644 index 0000000..3baccd9 --- /dev/null +++ b/src/listeners/commands/error.js @@ -0,0 +1,50 @@ +const { Listener } = require('@eartharoid/dbf'); +const { + EmbedBuilder, + codeBlock, +} = require('discord.js'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client.commands, + event: 'error', + }); + } + + async run({ + command, + error, + interaction, + }) { + const ref = require('crypto').randomUUID(); + this.client.log.error.commands(ref); + this.client.log.error.commands(`"${command.name}" command execution error:`, error); + let locale = null; + if (interaction.guild) { + locale = (await this.client.prisma.guild.findUnique({ + select: { locale: true }, + where: { id: interaction.guild.id }, + })).locale; + } + const getMessage = this.client.i18n.getLocale(locale); + const data = { + components: [], + embeds: [ + new EmbedBuilder() + .setColor('Orange') + .setTitle(getMessage('misc.error.title')) + .setDescription(getMessage('misc.error.description')) + .addFields([ + { + name: getMessage('misc.error.fields.identifier'), + value: codeBlock(ref), + }, + ]), + ], + }; + + interaction.reply(data).catch(() => interaction.editReply(data)); + } +}; diff --git a/src/listeners/commands/run.js b/src/listeners/commands/run.js new file mode 100644 index 0000000..94649a1 --- /dev/null +++ b/src/listeners/commands/run.js @@ -0,0 +1,24 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client.commands, + event: 'run', + }); + } + + run({ + command, + interaction, + }) { + const types = { + 1: 'slash', + 2: 'user', + 3: 'message', + }; + this.client.log.info.commands(`${interaction.user.tag} used the "${command.name}" ${types[command.type]} command`); + return true; + } +}; diff --git a/src/listeners/debug.js b/src/listeners/debug.js deleted file mode 100644 index ad62c68..0000000 --- a/src/listeners/debug.js +++ /dev/null @@ -1,13 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); - -module.exports = class DebugEventListener extends EventListener { - constructor(client) { - super(client, { event: 'debug' }); - } - - async execute(data) { - if (this.client.config.developer.debug) { - this.client.log.debug(data); - } - } -}; \ No newline at end of file diff --git a/src/listeners/error.js b/src/listeners/error.js deleted file mode 100644 index ddfe695..0000000 --- a/src/listeners/error.js +++ /dev/null @@ -1,12 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); - -module.exports = class ErrorEventListener extends EventListener { - constructor(client) { - super(client, { event: 'error' }); - } - - async execute(error) { - this.client.log.warn('The client encountered an error'); - this.client.log.error(error); - } -}; \ No newline at end of file diff --git a/src/listeners/guildCreate.js b/src/listeners/guildCreate.js deleted file mode 100644 index 0d01ae5..0000000 --- a/src/listeners/guildCreate.js +++ /dev/null @@ -1,12 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); - -module.exports = class GuildCreateEventListener extends EventListener { - constructor(client) { - super(client, { event: 'guildCreate' }); - } - - async execute(guild) { - this.client.log.info(`Added to "${guild.name}"`); - this.client.commands.publish(guild); - } -}; \ No newline at end of file diff --git a/src/listeners/guildDelete.js b/src/listeners/guildDelete.js deleted file mode 100644 index a4b55cd..0000000 --- a/src/listeners/guildDelete.js +++ /dev/null @@ -1,11 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); - -module.exports = class GuildDeleteEventListener extends EventListener { - constructor(client) { - super(client, { event: 'guildDelete' }); - } - - async execute(guild) { - this.client.log.info(`Removed from "${guild.name}"`); - } -}; \ No newline at end of file diff --git a/src/listeners/guildMemberRemove.js b/src/listeners/guildMemberRemove.js deleted file mode 100644 index e698d3f..0000000 --- a/src/listeners/guildMemberRemove.js +++ /dev/null @@ -1,22 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); - -module.exports = class GuildMemberRemoveEventListener extends EventListener { - constructor(client) { - super(client, { event: 'guildMemberRemove' }); - } - - async execute(member) { - const tickets = await this.client.db.models.Ticket.findAndCountAll({ - where: { - creator: member.id, - guild: member.guild.id - } - }); - - for (const ticket of tickets.rows) { - await this.client.tickets.close(ticket.id, null, member.guild.id, 'Member left the guild'); - } - - this.client.log.info(`Closed ${tickets.count} ticket(s) belonging to ${member.user.tag} who left "${member.guild.name}"`); - } -}; \ No newline at end of file diff --git a/src/listeners/interactionCreate.js b/src/listeners/interactionCreate.js deleted file mode 100644 index c1449c3..0000000 --- a/src/listeners/interactionCreate.js +++ /dev/null @@ -1,323 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); -const { - Interaction, // eslint-disable-line no-unused-vars - MessageActionRow, - MessageButton, - MessageEmbed -} = require('discord.js'); - -module.exports = class InteractionCreateEventListener extends EventListener { - constructor(client) { - super(client, { event: 'interactionCreate' }); - } - - /** - * @param {Interaction} interaction - */ - async execute(interaction) { - this.client.log.debug(interaction); - - const settings = await this.client.utils.getSettings(interaction.guild.id); - const i18n = this.client.i18n.getLocale(settings.locale); - - const blacklisted = settings.blacklist.members.includes[interaction.user.id] || - interaction.member?.roles.cache?.some(role => settings.blacklist.roles.includes(role)); - if (blacklisted) { - return interaction.reply({ - content: i18n('blacklisted'), - ephemeral: true - }); - } - - const handlePanel = async id => { - const cat_row = await this.client.db.models.Category.findOne({ where: { id } }); - - if (!cat_row) { - this.client.log.warn('Could not find a category with the ID given by a panel interaction'); - return interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('command_execution_error.title')) - .setDescription(i18n('command_execution_error.description')) - ], - ephemeral: true - }); - } - - const tickets = await this.client.db.models.Ticket.findAndCountAll({ - where: { - category: cat_row.id, - creator: interaction.user.id, - open: true - } - }); - - if (tickets.count >= cat_row.max_per_member) { - if (cat_row.max_per_member === 1) { - return interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.new.response.has_a_ticket.title')) - .setDescription(i18n('commands.new.response.has_a_ticket.description', tickets.rows[0].id)) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } else { - const list = tickets.rows.map(row => { - if (row.topic) { - const description = row.topic.substring(0, 30); - const ellipses = row.topic.length > 30 ? '...' : ''; - return `<#${row.id}>: \`${description}${ellipses}\``; - } else { - return `<#${row.id}>`; - } - }); - return interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.new.response.max_tickets.title', tickets.count)) - .setDescription(i18n('commands.new.response.max_tickets.description', list.join('\n'))) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - } else { - try { - const t_row = await this.client.tickets.create(interaction.guild.id, interaction.user.id, id); - return interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.new.response.created.title')) - .setDescription(i18n('commands.new.response.created.description', `<#${t_row.id}>`)) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } catch (error) { - this.client.log.error(error); - return interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.new.response.error.title')) - .setDescription(error.message) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - } - }; - - if (interaction.isCommand()) { - // handle slash commands - this.client.commands.handle(interaction); - } else if (interaction.isButton()) { - if (interaction.customId.startsWith('panel.single')) { - // handle single-category panels - handlePanel(interaction.customId.split(':')[1]); - } else if (interaction.customId.startsWith('ticket.claim')) { - // handle ticket claiming - if (!(await this.client.utils.isStaff(interaction.member))) return; - const t_row = await this.client.db.models.Ticket.findOne({ where: { id: interaction.channel.id } }); - await t_row.update({ claimed_by: interaction.user.id }); - await interaction.channel.permissionOverwrites.edit(interaction.user.id, { VIEW_CHANNEL: true }, `Ticket claimed by ${interaction.user.tag}`); - - const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } }); - - for (const role of cat_row.roles) { - await interaction.channel.permissionOverwrites.edit(role, { VIEW_CHANNEL: false }, `Ticket claimed by ${interaction.user.tag}`); - } - - this.client.log.info(`${interaction.user.tag} has claimed "${interaction.channel.name}" in "${interaction.guild.name}"`); - - await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('ticket.claimed.title')) - .setDescription(i18n('ticket.claimed.description', interaction.member.toString())) - .setFooter(settings.footer, interaction.guild.iconURL()) - ] - }); - - const components = new MessageActionRow(); - - if (cat_row.claiming) { - components.addComponents( - new MessageButton() - .setCustomId('ticket.unclaim') - .setLabel(i18n('ticket.unclaim')) - .setEmoji('♻️') - .setStyle('SECONDARY') - ); - } - - if (settings.close_button) { - components.addComponents( - new MessageButton() - .setCustomId('ticket.close') - .setLabel(i18n('ticket.close')) - .setEmoji('✖️') - .setStyle('DANGER') - ); - } - - await interaction.message.edit({ components: [components] }); - } else if (interaction.customId.startsWith('ticket.unclaim')) { - // handle ticket unclaiming - if (!(await this.client.utils.isStaff(interaction.member))) return; - const t_row = await this.client.db.models.Ticket.findOne({ where: { id: interaction.channel.id } }); - await t_row.update({ claimed_by: null }); - - await interaction.channel.permissionOverwrites.delete(interaction.user.id, `Ticket released by ${interaction.user.tag}`); - - const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } }); - - for (const role of cat_row.roles) { - await interaction.channel.permissionOverwrites.edit(role, { VIEW_CHANNEL: true }, `Ticket released by ${interaction.user.tag}`); - } - - this.client.log.info(`${interaction.user.tag} has released "${interaction.channel.name}" in "${interaction.guild.name}"`); - - await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('ticket.released.title')) - .setDescription(i18n('ticket.released.description', interaction.member.toString())) - .setFooter(settings.footer, interaction.guild.iconURL()) - ] - }); - - const components = new MessageActionRow(); - - if (cat_row.claiming) { - components.addComponents( - new MessageButton() - .setCustomId('ticket.claim') - .setLabel(i18n('ticket.claim')) - .setEmoji('🙌') - .setStyle('SECONDARY') - ); - } - - if (settings.close_button) { - components.addComponents( - new MessageButton() - .setCustomId('ticket.close') - .setLabel(i18n('ticket.close')) - .setEmoji('✖️') - .setStyle('DANGER') - ); - } - - await interaction.message.edit({ components: [components] }); - } else if (interaction.customId.startsWith('ticket.close')) { - // handle ticket close button - const t_row = await this.client.db.models.Ticket.findOne({ where: { id: interaction.channel.id } }); - await interaction.reply({ - components: [ - new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId(`confirm_close:${interaction.id}`) - .setLabel(i18n('commands.close.response.confirm.buttons.confirm')) - .setEmoji('✅') - .setStyle('SUCCESS') - ) - .addComponents( - new MessageButton() - .setCustomId(`cancel_close:${interaction.id}`) - .setLabel(i18n('commands.close.response.confirm.buttons.cancel')) - .setEmoji('❌') - .setStyle('SECONDARY') - ) - ], - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.close.response.confirm.title')) - .setDescription(settings.log_messages ? i18n('commands.close.response.confirm.description_with_archive') : i18n('commands.close.response.confirm.description')) - .setFooter(this.client.utils.footer(settings.footer, i18n('collector_expires_in', 30)), interaction.guild.iconURL()) - ], - ephemeral: true - }); - - - const filter = i => i.user.id === interaction.user.id && i.customId.includes(interaction.id); - const collector = interaction.channel.createMessageComponentCollector({ - filter, - time: 30000 - }); - - collector.on('collect', async i => { - await i.deferUpdate(); - - if (i.customId === `confirm_close:${interaction.id}`) { - await this.client.tickets.close(t_row.id, interaction.user.id, interaction.guild.id); - await i.editReply({ - components: [], - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setTitle(i18n('commands.close.response.closed.title', t_row.number)) - .setDescription(i18n('commands.close.response.closed.description', t_row.number)) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } else { - await i.editReply({ - components: [], - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.close.response.canceled.title')) - .setDescription(i18n('commands.close.response.canceled.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - - collector.stop(); - }); - - collector.on('end', async collected => { - if (collected.size === 0) { - await interaction.editReply({ - components: [], - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) - .setTitle(i18n('commands.close.response.confirmation_timeout.title')) - .setDescription(i18n('commands.close.response.confirmation_timeout.description')) - .setFooter(settings.footer, interaction.guild.iconURL()) - ], - ephemeral: true - }); - } - }); - } - } else if (interaction.isSelectMenu()) { - if (interaction.customId.startsWith('panel.multiple')) { - // handle multi-category panels and new command - handlePanel(interaction.values[0]); - } - } - } -}; \ No newline at end of file diff --git a/src/listeners/menus/componentLoad.js b/src/listeners/menus/componentLoad.js new file mode 100644 index 0000000..4e1c3d3 --- /dev/null +++ b/src/listeners/menus/componentLoad.js @@ -0,0 +1,16 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client.menus, + event: 'componentLoad', + }); + } + + run(menu) { + this.client.log.info.menus(`Loaded "${menu.id}" menu`); + return true; + } +}; diff --git a/src/listeners/menus/run.js b/src/listeners/menus/run.js new file mode 100644 index 0000000..ab1fba2 --- /dev/null +++ b/src/listeners/menus/run.js @@ -0,0 +1,19 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client.menus, + event: 'run', + }); + } + + run({ + menu, + interaction, + }) { + this.client.log.info.menus(`${interaction.user.tag} used the "${menu.id}" menu`); + return true; + } +}; diff --git a/src/listeners/messageCreate.js b/src/listeners/messageCreate.js deleted file mode 100644 index fbab7a4..0000000 --- a/src/listeners/messageCreate.js +++ /dev/null @@ -1,190 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); -const fetch = require('node-fetch'); -const { - MessageAttachment, - MessageEmbed -} = require('discord.js'); - -module.exports = class MessageCreateEventListener extends EventListener { - constructor(client) { - super(client, { event: 'messageCreate' }); - } - - async execute(message) { - if (!message.guild) return; - - const settings = await this.client.utils.getSettings(message.guild.id); - const i18n = this.client.i18n.getLocale(settings.locale); - - const t_row = await this.client.db.models.Ticket.findOne({ where: { id: message.channel.id } }); - - if (t_row) { - const should_log_message = process.env.DB_TYPE.toLowerCase() !== 'sqlite' && settings.log_messages && !message.system; - if (should_log_message) this.client.tickets.archives.addMessage(message); // add the message to the archives (if it is in a ticket channel) - - const ignore = [this.client.user.id, t_row.creator]; - if (!t_row.first_response && !ignore.includes(message.author.id)) t_row.first_response = new Date(); - - t_row.last_message = new Date(); - await t_row.save(); - } else if (message.content.startsWith('tickets/')) { - if (!message.member.permissions.has('MANAGE_GUILD')) return; - - const match = message.content.toLowerCase().match(/tickets\/(\w+)/i); - - if (!match) return; - - switch (match[1]) { - case 'surveys': { - const attachments = [...message.attachments.values()]; - if (attachments.length >= 1) { - this.client.log.info(`Downloading surveys for "${message.guild.name}"`); - const data = await (await fetch(attachments[0].url)).json(); - for (const survey in data) { - const survey_data = { - guild: message.guild.id, - name: survey - }; - const [s_row] = await this.client.db.models.Survey.findOrCreate({ - defaults: survey_data, - where: survey_data - }); - s_row.questions = data[survey]; - await s_row.save(); - } - this.client.log.success(`Updated surveys for "${message.guild.name}"`); - message.channel.send({ content: i18n('commands.settings.response.settings_updated') }); - } else { - const surveys = await this.client.db.models.Survey.findAll({ where: { guild: message.guild.id } }); - const data = {}; - - for (const survey in surveys) { - const { - name, questions - } = surveys[survey]; - data[name] = questions; - } - - const attachment = new MessageAttachment( - Buffer.from(JSON.stringify(data, null, 2)), - 'surveys.json' - ); - message.channel.send({ files: [attachment] }); - } - break; - } - case 'tags': { - const attachments = [...message.attachments.values()]; - if (attachments.length >= 1) { - this.client.log.info(`Downloading tags for "${message.guild.name}"`); - const data = await (await fetch(attachments[0].url)).json(); - settings.tags = data; - await settings.save(); - this.client.log.success(`Updated tags for "${message.guild.name}"`); - this.client.commands.publish(message.guild); - message.channel.send({ content: i18n('commands.settings.response.settings_updated') }); - } else { - const list = Object.keys(settings.tags).map(t => `❯ **\`${t}\`**`); - const attachment = new MessageAttachment( - Buffer.from(JSON.stringify(settings.tags, null, 2)), - 'tags.json' - ); - return await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.tag.response.list.title')) - .setDescription(list.join('\n')) - .setFooter(settings.footer, message.guild.iconURL()) - ], - files: [attachment] - }); - } - break; - } - } - } else { - if (message.author.bot) return; - - const p_row = await this.client.db.models.Panel.findOne({ where: { channel: message.channel.id } }); - - if (p_row) { - // handle message panels - - await message.delete(); - - const cat_row = await this.client.db.models.Category.findOne({ where: { id: p_row.category } }); - - const tickets = await this.client.db.models.Ticket.findAndCountAll({ - where: { - category: cat_row.id, - creator: message.author.id, - open: true - } - }); - - let response; - - if (tickets.count >= cat_row.max_per_member) { - if (cat_row.max_per_member === 1) { - const embed = new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(message.author.username, message.author.displayAvatarURL()) - .setTitle(i18n('commands.new.response.has_a_ticket.title')) - .setDescription(i18n('commands.new.response.has_a_ticket.description', tickets.rows[0].id)) - .setFooter(this.client.utils.footer(settings.footer, i18n('message_will_be_deleted_in', 15)), message.guild.iconURL()); - try { - response = await message.author.send({ embeds: [embed] }); - } catch { - response = await message.channel.send({ embeds: [embed] }); - } - } else { - const list = tickets.rows.map(row => { - if (row.topic) { - const description = row.topic.substring(0, 30); - const ellipses = row.topic.length > 30 ? '...' : ''; - return `<#${row.id}>: \`${description}${ellipses}\``; - } else { - return `<#${row.id}>`; - } - }); - const embed = new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(message.author.username, message.author.displayAvatarURL()) - .setTitle(i18n('commands.new.response.max_tickets.title', tickets.count)) - .setDescription(i18n('commands.new.response.max_tickets.description', list.join('\n'))) - .setFooter(this.client.utils.footer(settings.footer, i18n('message_will_be_deleted_in', 15)), message.author.iconURL()); - try { - response = await message.author.send({ embeds: [embed] }); - } catch { - response = await message.channel.send({ embeds: [embed] }); - } - } - } else { - try { - await this.client.tickets.create(message.guild.id, message.author.id, cat_row.id, message.cleanContent); - } catch (error) { - const embed = new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(message.author.username, message.author.displayAvatarURL()) - .setTitle(i18n('commands.new.response.error.title')) - .setDescription(error.message) - .setFooter(this.client.utils.footer(settings.footer, i18n('message_will_be_deleted_in', 15)), message.guild.iconURL()); - try { - response = await message.author.send({ embeds: [embed] }); - } catch { - response = await message.channel.send({ embeds: [embed] }); - } - } - } - - if (response) { - setTimeout(async () => { - await response.delete(); - }, 15000); - } - } - } - } -}; - diff --git a/src/listeners/messageDelete.js b/src/listeners/messageDelete.js deleted file mode 100644 index 540a2e9..0000000 --- a/src/listeners/messageDelete.js +++ /dev/null @@ -1,15 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); - -module.exports = class MessageDeleteEventListener extends EventListener { - constructor(client) { - super(client, { event: 'messageDelete' }); - } - - async execute(message) { - if (!message.guild) return; - - const settings = await this.client.utils.getSettings(message.guild.id); - - if (settings.log_messages && !message.system) this.client.tickets.archives.deleteMessage(message); // mark the message as deleted in the database (if it exists) - } -}; \ No newline at end of file diff --git a/src/listeners/messageUpdate.js b/src/listeners/messageUpdate.js deleted file mode 100644 index 9f06053..0000000 --- a/src/listeners/messageUpdate.js +++ /dev/null @@ -1,23 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); - -module.exports = class MessageUpdateEventListener extends EventListener { - constructor(client) { - super(client, { event: 'messageUpdate' }); - } - - async execute(oldm, newm) { - if (newm.partial) { - try { - await newm.fetch(); - } catch (error) { - return this.client.log.error(error); - } - } - - if (!newm.guild) return; - - const settings = await this.client.utils.getSettings(newm.guild.id); - - if (settings.log_messages && !newm.system) this.client.tickets.archives.updateMessage(newm); // update the message in the database - } -}; \ No newline at end of file diff --git a/src/listeners/modals/componentLoad.js b/src/listeners/modals/componentLoad.js new file mode 100644 index 0000000..cccd669 --- /dev/null +++ b/src/listeners/modals/componentLoad.js @@ -0,0 +1,16 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client.modals, + event: 'componentLoad', + }); + } + + run(modal) { + this.client.log.info.modals(`Loaded "${modal.id}" modal`); + return true; + } +}; diff --git a/src/listeners/modals/run.js b/src/listeners/modals/run.js new file mode 100644 index 0000000..87003e0 --- /dev/null +++ b/src/listeners/modals/run.js @@ -0,0 +1,19 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client.modals, + event: 'run', + }); + } + + run({ + modal, + interaction, + }) { + this.client.log.info.modals(`${interaction.user.tag} used the "${modal.id}" modal`); + return true; + } +}; diff --git a/src/listeners/rateLimit.js b/src/listeners/rateLimit.js deleted file mode 100644 index 29053e0..0000000 --- a/src/listeners/rateLimit.js +++ /dev/null @@ -1,11 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); - -module.exports = class RateLimitEventListener extends EventListener { - constructor(client) { - super(client, { event: 'rateLimit' }); - } - - async execute(limit) { - this.client.log.warn('Rate-limited!', limit); - } -}; \ No newline at end of file diff --git a/src/listeners/ready.js b/src/listeners/ready.js deleted file mode 100644 index dc01439..0000000 --- a/src/listeners/ready.js +++ /dev/null @@ -1,35 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); - -module.exports = class ReadyEventListener extends EventListener { - constructor(client) { - super(client, { - event: 'ready', - once: true - }); - } - - async execute() { - this.client.log.success(`Connected to Discord as "${this.client.user.tag}"`); - - this.client.log.info('Loading commands'); - this.client.commands.load(); // load internal commands - this.client.plugins.plugins.forEach(p => p.load()); // call load function for each plugin - this.client.commands.publish(); // send commands to discord - - if (this.client.config.presence.presences.length > 1) { - const { selectPresence } = require('../utils/discord'); - setInterval(() => { - const presence = selectPresence(); - this.client.user.setPresence(presence); - this.client.log.debug(`Updated presence: ${presence.activities[0].type} ${presence.activities[0].name}`); - }, this.client.config.presence.duration * 1000); - } - - if (this.client.config.super_secret_setting) { - setInterval(async () => { - await this.client.postStats(); - }, 3600000); - await this.client.postStats(); - } - } -}; \ No newline at end of file diff --git a/src/listeners/stdin/unknown.js b/src/listeners/stdin/unknown.js new file mode 100644 index 0000000..1ddc9d3 --- /dev/null +++ b/src/listeners/stdin/unknown.js @@ -0,0 +1,15 @@ +const { Listener } = require('@eartharoid/dbf'); + +module.exports = class extends Listener { + constructor(client, options) { + super(client, { + ...options, + emitter: client.stdin, + event: 'unknown', + }); + } + + run(commandName) { + this.client.log.warn(`Unknown command: "${commandName}"; type "help" for a list of commands`); + } +}; diff --git a/src/listeners/warn.js b/src/listeners/warn.js deleted file mode 100644 index f1020e9..0000000 --- a/src/listeners/warn.js +++ /dev/null @@ -1,11 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); - -module.exports = class WarnEventListener extends EventListener { - constructor(client) { - super(client, { event: 'warn' }); - } - - async execute(warning) { - this.client.log.warn(warning); - } -}; \ No newline at end of file diff --git a/src/locales/ar.json b/src/locales/ar.json deleted file mode 100644 index 7cfdb0a..0000000 --- a/src/locales/ar.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "bot": { - "missing_permissions": { - "description": "احتاج الى الاذونات التالية : \n%s" - } - } -} diff --git a/src/locales/cs-CZ.json b/src/locales/cs-CZ.json deleted file mode 100644 index 3f5feae..0000000 --- a/src/locales/cs-CZ.json +++ /dev/null @@ -1,340 +0,0 @@ -{ - "blacklisted": "❌ Jsi na černé listině", - "bot": { - "missing_permissions": { - "description": "Discord Tickets potřebuje následující oprávnění\n%s", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s od [eartharoid](%s)" - }, - "collector_expires_in": "Vyprší za %d sekund", - "command_execution_error": { - "description": "Během provádění příkazu došlo k neočekávané chybě.\nPožádej správce serveru, aby zkontroloval výstup konzole / protokoly pro více detailů.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Přidat člena do tiketu", - "name": "pridat", - "options": { - "member": { - "description": "Člen k přidání do tiketu", - "name": "člen" - }, - "ticket": { - "description": "Tiket, do kterého chcete přidat člena", - "name": "ticket" - } - }, - "response": { - "added": { - "description": "%s byl přidán do %s.", - "title": "✅ Člen přidán" - }, - "no_member": { - "description": "Označte prosím člena, kterého chcete přidat.", - "title": "❌ Neznámý člen" - }, - "no_permission": { - "description": "Nejste tvůrcem tohoto tiketu a nejste ani člen týmu, nemůžete do tohoto tiketu přidávat členy.", - "title": "❌ Nedostatečná oprávnění" - }, - "not_a_ticket": { - "description": "Použijte tento příkaz c kanálu tiketu nebo označte kanál.", - "title": "❌ Toto není kanál tiketu" - } - } - }, - "blacklist": { - "description": "Zablokovat/povolit uživateli interakci s botem", - "name": "blacklist", - "response": { - "empty_list": { - "description": "Na černé listině nejsou žádní uživatelé nebo role. Použij `%sblacklist <členNeboRole>` pro přidání uživatele nebo role na černou listinu.", - "title": "📃 Zablokovaní uživatelé a role" - }, - "illegal_action": { - "description": "%s je člen týmu a nemůže být na černé listině.", - "title": "❌ Tohoto člena nemůžeš přidat na černou listinu" - }, - "list": { - "title": "📃 Zablokovaní uživatelé a role" - }, - "member_added": { - "description": "Uživatel <@%s> byl přidán na černou listinu. Už nebude moci využívat bota.", - "title": "✅ Uživatel přidán na černou listinu" - }, - "member_removed": { - "description": "Uživatel <@%s> byl odebrán z černé listiny. Nyní bude moci využívat bota.", - "title": "✅ Uživatel odebrán z z černé listiny" - }, - "role_added": { - "description": "Role <@&%s> byla přidána na černou listinu. Uživatelé s touto rolí nyní nebudou moci využívat bota.", - "title": "✅ Role přidána na černou listinu" - }, - "role_removed": { - "description": "Role <@&%s> byla odebrána z černé listiny. Nyní bude moci využívat bota.", - "title": "✅ Role odebrána z černé listiny" - } - } - }, - "close": { - "description": "Uzavře kanál s ticketem", - "name": "zavrit", - "response": { - "closed": { - "description": "Tiket #%s byl uzavřen.", - "title": "✅ Ticket uzavřen" - }, - "closed_multiple": { - "description": [ - "%d tiket byl uzavřen.", - "%d tiketů bylo uzavřeno." - ], - "title": [ - "✅ Ticket uzavřen", - "✅ Tikety uzavřeny" - ] - }, - "confirm": { - "buttons": { - "confirm": "zavrit" - }, - "description": "Zareagujte pomocí ✅ k uzavření tohoto tiketu.", - "description_with_archive": "Později si budete moci zobrazit archivovanou verzi.", - "title": "❔ Jste si jistí?" - }, - "confirm_multiple": { - "description": [ - "Zareagujte pomocí ✅ pro uzavření %d tiketu.", - "Zareagujte pomocí ✅ pro uzavření %d tiketů." - ], - "title": "❔ Jste si jistí?" - }, - "confirmation_timeout": { - "description": "Potvrzení vám trvalo příliš dlouho.", - "title": "❌ Doba pro reakci vypršela" - }, - "invalid_time": { - "description": "Zadaný čas nebyl rozpoznán.", - "title": "❌ Neplatný vstup" - }, - "no_tickets": { - "description": "Nejsou zde žádné tikety, které byly aktivní po zadané časově rozmezí.", - "title": "❌ Žádné tikety k uzavření" - }, - "not_a_ticket": { - "description": "Tento příkaz prosím použijte v kanálu tiketu nebo použijte vlajku tiketu.\nZadejte `%shelp close` pro více informací.", - "title": "❌ Toto není kanál tiketu" - }, - "unresolvable": { - "description": "`%s` nebyl rozpoznán jako tiket. Zadejte prosím ID nebo číslo tiketu nebo jej označte.", - "title": "❌ Chyba" - } - } - }, - "help": { - "description": "Seznam příkazů, ke kterým máš přístup, nebo k nalezení dalších informací o příkazu", - "name": "napoveda", - "response": { - "list": { - "description": "Příkazy, ke kterým máš přístup, jsou vypsány níže. Pro více informací o příkazu zadej `{prefix}napoveda [příkaz]`. Pro vytvoření ticketu zadej `{prefix}novy [téma]`.", - "fields": { - "commands": "Příkazy" - }, - "title": "❔ Nápověda" - } - } - }, - "new": { - "description": "Vytvořit ticket", - "name": "novy", - "request_topic": { - "description": "Popiš prosím stručně několika slovy, o čem je tento ticket.", - "title": "Téma ticketu" - }, - "response": { - "created": { - "description": "Tvůj ticket byl vytvořen: %s.", - "title": "✅ Ticket vytvořen" - }, - "error": { - "title": "❌ Chyba" - }, - "has_a_ticket": { - "description": "Prosím použij již existující ticket (<#%s>) nebo jej uzavři pro vytvoření nového.", - "title": "❌ Již máš otevřený ticket" - }, - "max_tickets": { - "description": "Prosím použij `%szavrit` pro uzavření nepotřebných ticketů.\n\n%s", - "title": "❌ Již máš %d otevřených ticketů" - }, - "no_categories": { - "description": "Než budeš moci vytvořit nový ticket, musí správce serveru vytvořit alespoň jednu kategorii pro tickety.", - "title": "❌ Nelze vytvořit ticket" - }, - "select_category": { - "description": "Vyber kategorii, která je nejrelevantnější pro téma ticketu.", - "title": "🔤 Prosím vyber kategorii ticketu" - }, - "select_category_timeout": { - "description": "Výběr kategorie ticketu trval příliš dlouho.", - "title": "❌ Doba pro reakci vypršela" - } - } - }, - "panel": { - "description": "Vytvořit nový ticket panel", - "name": "panel", - "response": { - "invalid_category": { - "description": "Jedno nebo více ID kategorií je vadných.", - "title": "❌ Neplatná kategorie" - } - } - }, - "remove": { - "description": "Odebrat člena z ticketu", - "name": "odebrat", - "response": { - "no_member": { - "description": "Označ prosím člena, kterého chceš přidat.", - "title": "❌ Neznámý člen" - }, - "no_permission": { - "description": "Nejsi tvůrcem tohoto ticketu a nejsi ani členem týmu, nemůžeš z tohoto ticketu odebírat členy.", - "title": "❌ Nedostatečná oprávnění" - }, - "not_a_ticket": { - "description": "Použijte tento příkaz c kanálu tiketu nebo označte kanál.", - "title": "❌ Toto není kanál tiketu" - }, - "removed": { - "description": "%s byl/a odebrán/a z %s.", - "title": "✅ Člen odebrán" - } - } - }, - "settings": { - "description": "Konfigurace Discord Tickets", - "name": "nastaveni" - }, - "stats": { - "description": "Zobrazit statistiky ticketu", - "fields": { - "messages": "Zprávy", - "response_time": { - "minutes": "%s minut", - "title": "Průměrná doba odpovědi" - }, - "tickets": "Tickety" - }, - "name": "statistiky", - "response": { - "global": { - "description": "Statistiky všech ticketů napříč všemi servery, kde je použita tato instance bota Discord Tickets.", - "title": "📊 Globální statistiky" - }, - "guild": { - "description": "Statistiky o ticketech v tomto serveru. Tato data jsou v mezipaměti jednu hodinu.", - "title": "📊 Statistiky tohoto serveru" - } - } - }, - "survey": { - "description": "Zobrazit odpovědi dotazníku", - "name": "dotazník", - "response": { - "list": { - "title": "📃 Dotazníky" - } - } - }, - "tag": { - "description": "Použít odpověď na značku", - "name": "značka", - "response": { - "error": "❌ Chyba", - "list": { - "title": "📃 Seznam značek" - }, - "missing": "Tato značka vyžaduje následující argumenty:\n%s", - "not_a_ticket": { - "description": "Tato značka může být použita pouze v kanálu ticketu, protože používá reference na tickety.", - "title": "❌ Toto není kanál tiketu" - } - } - }, - "topic": { - "description": "Změnit téma ticketu", - "name": "tema", - "response": { - "changed": { - "description": "Téma tohoto ticketu bylo změněno.", - "title": "✅ Téma změněno" - }, - "not_a_ticket": { - "description": "Tento příkaz prosím použij kanálu ticketu, jehož téma chceš změnit.", - "title": "❌ Toto není kanál tiketu" - } - } - } - }, - "message_will_be_deleted_in": "Tato zpráva bude smazána za %d sekund", - "missing_permissions": { - "description": "K provedení tohoto příkazu nemáš dostatečná oprávnění:\n%s", - "title": "❌" - }, - "ticket": { - "claimed": { - "description": "%s si vzal tento ticket.", - "title": "✅ Ticket vzat" - }, - "close": "Uzavřít", - "closed": { - "description": "Tento ticket byl uzavřen.\nKanál bude smazán za 5 sekund.", - "title": "✅ Ticket uzavřen" - }, - "closed_by_member": { - "description": "Tento ticket byl uzavřen uživatelem %s.\nKanál bude smazán za 5 sekund.", - "title": "✅ Ticket uzavřen" - }, - "closed_by_member_with_reason": { - "description": "Tento ticket byl uzavřen uživatelem %s: `%s`\nKanál bude smazán za 5 sekund.", - "title": "✅ Ticket uzavřen" - }, - "closed_with_reason": { - "description": "Tento ticket byl uzavřen: `%s`\nKanál bude smazán za 5 sekund.", - "title": "✅ Ticket uzavřen" - }, - "member_added": { - "description": "%s byl/a přidán/a uživatelem %s", - "title": "Člen přidán" - }, - "member_removed": { - "description": "%s byl/a odebrán/a uživatelem %s", - "title": "Člen odebrán" - }, - "opening_message": { - "content": "%s\n%s vytvořil nový ticket", - "fields": { - "topic": "Téma" - } - }, - "questions": "Prosím odpověz na následující otázky:\n\n%s", - "released": { - "description": "%s uvolnil ticket.", - "title": "✅ Ticket uvolněn" - }, - "survey": { - "complete": { - "description": "Děkujeme za zpětnou vazbu.", - "title": "✅ Děkujeme" - }, - "start": { - "description": "Zdravíme, %s. Než bude tento kanál odstraněn, nevadilo by ti vyplnit rychlý %d-otázkový dotazník?", - "title": "❔ Zpětná vazba" - } - } - } -} diff --git a/src/locales/da-DK.json b/src/locales/da-DK.json deleted file mode 100644 index b6adb8b..0000000 --- a/src/locales/da-DK.json +++ /dev/null @@ -1,610 +0,0 @@ -{ - "blacklisted": "❌ Du er blacklisted", - "bot": { - "missing_permissions": { - "description": "Discord Tickets kræver følgende tilladelser:\n%s", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s af [eartharoid](%s)" - }, - "collector_expires_in": "Udløber om %d sekunder", - "command_execution_error": { - "description": "Der opstod en uventet fejl under udførelse af kommandoer.\nBed en administrator om at kontrollere konsoloutputtet/-logfilerne for at få flere oplysninger.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Tilføj et medlem til en ticket", - "name": "add", - "options": { - "member": { - "description": "Det medlem der skal tilføjes til ticketen", - "name": "member" - }, - "ticket": { - "description": "Den ticket du vil tilføje medlemmet til", - "name": "ticket" - } - }, - "response": { - "added": { - "description": "%s er føjet til %s.", - "title": "✅ Medlem tilføjet" - }, - "no_member": { - "description": "Nævn det medlem, du vil tilføje.", - "title": "❌ Ukendt medlem" - }, - "no_permission": { - "description": "Du er ikke skaberen af denne ticket, og du er ikke et staff medlem; Du kan ikke føje medlemmer til denne ticket.", - "title": "❌ Utilstrækkelig tilladelser" - }, - "not_a_ticket": { - "description": "Brug denne kommando i ticket kanalen, eller nævn kanalen.", - "title": "❌ Dette er ikke en ticket kanal" - } - } - }, - "blacklist": { - "description": "Se eller rediger blacklisten", - "name": "blacklist", - "options": { - "add": { - "description": "Tilføj et medlem eller rolle til blacklisten", - "name": "add", - "options": { - "member_or_role": { - "description": "Det medlem eller rolle du vil tilføje til blacklisten", - "name": "member_or_role" - } - } - }, - "remove": { - "description": "Fjern et medlem eller rolle fra blacklisten", - "name": "remove", - "options": { - "member_or_role": { - "description": "Det medlem eller rolle du vil fjerne fra blacklisten", - "name": "member_or_role" - } - } - }, - "show": { - "description": "Vis medlemmer eller roller i blacklisten", - "name": "show" - } - }, - "response": { - "empty_list": { - "description": "Der er ingen medlemmer eller roller, der er blacklistet. Skriv `/blacklist add` for at tilføje et medlem eller en rolle til blacklisten.", - "title": "📃 Blacklistede medlemmer og roller" - }, - "illegal_action": { - "description": "%s er staff medlem og kan ikke blacklistes.", - "title": "❌ Du kan ikke blackliste dette medlem" - }, - "invalid": { - "description": "Denne bruger eller rolle kan ikke blive fjernet fra blacklisten eftersom de ikke er blacklisted.", - "title": "❌ Fejl" - }, - "list": { - "fields": { - "members": "Members", - "roles": "Roles" - }, - "title": "📃 Blacklistede medlemmer og roller" - }, - "member_added": { - "description": "<@%s> er blevet føjet til blacklisten. De vil ikke længere være i stand til at interagere med botten.", - "title": "✅ Føjet medlem til blacklisten" - }, - "member_removed": { - "description": "<@%s> er blevet fjernet fra den blacklisten. De kan nu bruge botten igen.", - "title": "✅ fjernet medlem fra blacklisten" - }, - "role_added": { - "description": "<@&%s> er blevet føjet til blacklisten. Medlemmer med denne rolle vil ikke længere være i stand til at interagere med botten.", - "title": "✅ Føjet rolle til blacklisten" - }, - "role_removed": { - "description": "<@&%s> er blevet fjernet fra blacklisten. Medlemmer med denne rolle kan nu bruge botten igen.", - "title": "✅ Fjernede rollen fra blacklisten" - } - } - }, - "close": { - "description": "Luk en ticketkanal", - "name": "close", - "options": { - "reason": { - "description": "Begrundelse for at lukke ticketen(e)", - "name": "reason" - }, - "ticket": { - "description": "Ticketen der skal lukkes, enten ticket nummeret eller kanal ID", - "name": "ticket" - }, - "time": { - "description": "Luk alle tickets der har været inaktive i den specificeret tid", - "name": "time" - } - }, - "response": { - "canceled": { - "description": "Du har annulleret handlingen.", - "title": "🚫 Annulleret" - }, - "closed": { - "description": "Ticket #%s er blevet lukket.", - "title": "✅ Ticket lukket" - }, - "closed_multiple": { - "description": [ - "%d Ticket er blevet lukket.", - "%d Tickets er blevet lukket." - ], - "title": [ - "✅ Ticket lukket", - "✅ Tickets lukket" - ] - }, - "confirm": { - "buttons": { - "cancel": "Annullér", - "confirm": "Bekræft" - }, - "description": "Venligst bekræft din beslutning.", - "description_with_archive": "Ticketen vil blive arkiveret for fremtidig reference.", - "title": "❔ Er du sikker?" - }, - "confirm_multiple": { - "buttons": { - "cancel": "Annullér", - "confirm": [ - "Luk %d ticket", - "Luk %d tickets" - ] - }, - "description": [ - "Du er ved at lukke %d ticket.", - "Du er ved at lukke %d tickets." - ], - "title": "❔ Er du sikker?" - }, - "confirmation_timeout": { - "description": "Du tog for lang tid på at bekræfte.", - "title": "❌ Interaktionstid er udløbet" - }, - "invalid_time": { - "description": "Den angivne tidsperiode kunne ikke parses.", - "title": "❌ Ugyldigt input" - }, - "no_permission": { - "description": "Du er ikke et staff medlem eller skaberen af denne ticket.", - "title": "❌ Der er ikke tilstrækkelig tilladelse" - }, - "no_tickets": { - "description": "Der er ingen tickets, der har været inaktive i denne periode.", - "title": "❌ Ingen tickets til at lukke" - }, - "not_a_ticket": { - "description": "Brug denne kommando i en ticketkanal, eller brug ticketflaget.\nSkriv '/help close' for at få yderligere oplysninger.", - "title": "❌ Dette er ikke en ticketkanal" - }, - "unresolvable": { - "description": "`%s` kunne ikke løses til en ticket. Angiv ticket ID/nævn eller nummer.", - "title": "❌ Fejl" - } - } - }, - "help": { - "description": "Vis de kommandoer du har adgang til", - "name": "help", - "response": { - "list": { - "description": "De kommandoer, du har adgang til, er angivet nedenfor. Hvis du vil oprette en ticket, skal du skrive **'/new'**.", - "fields": { - "commands": "commands" - }, - "title": "❔ Hjælp" - } - } - }, - "new": { - "description": "Opret en ny ticket", - "name": "new", - "options": { - "topic": { - "description": "Emnet på ticketen", - "name": "topic" - } - }, - "request_topic": { - "description": "Angiv kort, hvad denne ticket handler om, med et par ord.", - "title": "⚠️ Ticket emne" - }, - "response": { - "created": { - "description": "Din ticket er blevet oprettet: %s.", - "title": "✅ Ticket oprettet" - }, - "error": { - "title": "❌ Fejl" - }, - "has_a_ticket": { - "description": "Brug venligst din eksisterende ticket <# (%s>) eller luk den, før du opretter en anden.", - "title": "❌ Du har allerede en åben ticket" - }, - "max_tickets": { - "description": "Brug venligst '/close' for at lukke unødvendige tickets.\n\n%s", - "title": "❌ Du har allerede %d åbne tickets" - }, - "no_categories": { - "description": "En serveradministrator skal oprette mindst én ticketkategori, før en ny ticket kan åbnes.", - "title": "❌ Kan ikke oprette ticket" - }, - "select_category": { - "description": "Vælg den kategori, der er mest relevant for din tickets emne.", - "title": "🔤 Vælg ticket kategorien" - }, - "select_category_timeout": { - "description": "Du tog for lang tid om at vælge ticket kategorien.", - "title": "❌ Interaktionstid er udløbet" - } - } - }, - "panel": { - "description": "Opret et nyt ticket panel", - "name": "panel", - "options": { - "categories": { - "description": "En komma separeret liste af kategori ID'er", - "name": "categories" - }, - "description": { - "description": "Beskrivelsen for panel beskeden", - "name": "description" - }, - "image": { - "description": "Et billed URL for panel beskeden", - "name": "image" - }, - "just_type": { - "description": "Lav et \"bare skriv\" panel?", - "name": "just_type" - }, - "thumbnail": { - "description": "Et miniaturebilled -URL til panel beskeden", - "name": "thumbnail" - }, - "title": { - "description": "Titlen for panel beskeden", - "name": "title" - } - }, - "response": { - "invalid_category": { - "description": "Et eller flere af de angivne kategori-id'er er ugyldige.", - "title": "❌ Ugyldig kategori" - }, - "too_many_categories": { - "description": "\"just type\" panelet kan kun blive brugt med en enkelt kategori.", - "title": "❌ For mange kategorier" - } - } - }, - "remove": { - "description": "Fjerne et medlem fra en ticket", - "name": "remove", - "options": { - "member": { - "description": "medlemmet der skal fjernes fra ticketen", - "name": "member" - }, - "ticket": { - "description": "Den ticket du vil fjerne medlemmet fra", - "name": "ticket" - } - }, - "response": { - "no_member": { - "description": "Nævn det medlem, du vil fjerne.", - "title": "❌ Ukendt medlem" - }, - "no_permission": { - "description": "Du er ikke skaberen af denne ticket, og du er ikke et staff medlem; Du kan ikke fjerne medlemmer fra denne ticket.", - "title": "❌ Der er ikke tilstrækkelig tilladelse" - }, - "not_a_ticket": { - "description": "Brug denne kommando i ticket kanalen, eller nævn kanalen.", - "title": "❌ Dette er ikke en ticket kanal" - }, - "removed": { - "description": "%s er blevet fjernet fra %s.", - "title": "✅ Medlem fjernet" - } - } - }, - "settings": { - "description": "Konfigurer Discord Tickets", - "name": "settings", - "options": { - "categories": { - "description": "Administrer dine ticket kategorier", - "name": "categories", - "options": { - "create": { - "description": "Opret en ny kategori", - "name": "create", - "options": { - "name": { - "description": "Navnet på kategorien", - "name": "name" - }, - "roles": { - "description": "En komma separeret liste over staff rolle ID'er for denne kategori", - "name": "roles" - } - } - }, - "delete": { - "description": "Slet en kategori", - "name": "delete", - "options": { - "id": { - "description": "ID for den kategori der skal slettes", - "name": "id" - } - } - }, - "edit": { - "description": "Foretag ændringer i en kategoris konfiguration", - "name": "edit", - "options": { - "claiming": { - "description": "Vil du aktivere ticket claim?", - "name": "claiming" - }, - "id": { - "description": "ID for den kategori der skal redigeres", - "name": "id" - }, - "image": { - "description": "Et billed URL", - "name": "image" - }, - "max_per_member": { - "description": "Det maksimum antal tickets et medlem kan have i denne kategori", - "name": "max_per_member" - }, - "name": { - "description": "Kategori navnet", - "name": "name" - }, - "name_format": { - "description": "Ticket navne format", - "name": "name_format" - }, - "opening_message": { - "description": "Tekst der skal sendes når en ticket åbnes", - "name": "opening_message" - }, - "opening_questions": { - "description": "Spørgsmål at stille når en ticket er åbnet.", - "name": "opening_questions" - }, - "ping": { - "description": "En komma separeret liste over rolle-id'er, der skal pinges", - "name": "ping" - }, - "require_topic": { - "description": "Kræv at brugeren angiver emne for ticketen?", - "name": "require_topic" - }, - "roles": { - "description": "En komma separeret liste over staff rolle ID'er", - "name": "roles" - }, - "survey": { - "description": "Undersøgelse der skal bruges", - "name": "survey" - } - } - }, - "list": { - "description": "List kategorier", - "name": "list" - } - } - }, - "set": { - "description": "Indstil muligheder", - "name": "set", - "options": { - "close_button": { - "description": "Vil du aktivere lukning med en knap?", - "name": "close_button" - }, - "colour": { - "description": "Standardfarven", - "name": "colour" - }, - "error_colour": { - "description": "Fejl farven", - "name": "error_colour" - }, - "footer": { - "description": "Embed footer tekst", - "name": "footer" - }, - "locale": { - "description": "Locale (sprog)", - "name": "locale" - }, - "log_messages": { - "description": "Gem meddelelser fra tickets?", - "name": "log_messages" - }, - "success_colour": { - "description": "Succesfarven", - "name": "success_colour" - } - } - } - }, - "response": { - "category_created": "✅ `%s` ticket kategori er blevet oprettet", - "category_deleted": "✅ `%s` ticket kategori er blevet slettet", - "category_does_not_exist": "❌ Ingen kategori eksisterer med det angivet ID", - "category_list": "Ticket kategorier", - "category_updated": "✅ `%s` ticket kategori er blevet opdateret", - "settings_updated": "✅ Indstillinger er blevet opdateret" - } - }, - "stats": { - "description": "Vis ticket statistik", - "fields": { - "messages": "Meddelelser", - "response_time": { - "minutes": "%s minutter", - "title": "Gennemsnitlig svartid" - }, - "tickets": "Tickets" - }, - "name": "stats", - "response": { - "global": { - "description": "Statistik om tickets på tværs af alle guilds, hvor denne Discord Tickets instans bruges.", - "title": "📊 Global statistik" - }, - "guild": { - "description": "Statistik om tickets i dennee guild. Disse data cachelagres i en time.", - "title": "📊 Denne servers statistik" - } - } - }, - "survey": { - "description": "Vis undersøgelsessvar", - "name": "survey", - "options": { - "survey": { - "description": "Navnet på den undersøgelse du vil se svar på", - "name": "survey" - } - }, - "response": { - "list": { - "title": "📃 Undersøgelser" - } - } - }, - "tag": { - "description": "Brug et tagsvar", - "name": "tag", - "options": { - "tag": { - "description": "Navnet på det tag du vil bruge", - "name": "tag" - } - }, - "response": { - "error": "❌ Fejl", - "list": { - "title": "📃 Tag liste" - }, - "missing": "Dette tag kræver følgende argumenter:\n%s", - "not_a_ticket": { - "description": "Dette tag kan kun bruges inden for en ticket kanal, da det bruger ticket referencer.", - "title": "❌ Dette er ikke en ticket kanal" - } - } - }, - "topic": { - "description": "Skift emnet på ticketen", - "name": "topic", - "options": { - "new_topic": { - "description": "Det nye emne på ticketen", - "name": "new_topic" - } - }, - "response": { - "changed": { - "description": "Emnet for denne ticket er blevet ændret.", - "title": "✅ Emnet er ændret" - }, - "not_a_ticket": { - "description": "Brug venligst denne kommando i ticket kanalen, du ønsker at ændre emnet på.", - "title": "❌ Dette er ikke en ticket kanal" - } - } - } - }, - "message_will_be_deleted_in": "Denne meddelelse slettes om %d sekunder", - "missing_permissions": { - "description": "Du har ikke de nødvendige tilladelser til at bruge denne kommando:\n%s", - "title": "❌ Fejl" - }, - "panel": { - "create_ticket": "Opret en ticket" - }, - "ticket": { - "claim": "Claim", - "claimed": { - "description": "%s har gjort krav på denne ticket.", - "title": "✅ Ticket gjort krav på" - }, - "close": "Luk", - "closed": { - "description": "Denne ticket er blevet lukket.\nKanalen slettes om 5 sekunder.", - "title": "✅ Ticket lukket" - }, - "closed_by_member": { - "description": "Denne ticket er blevet lukket af %s.\nKanalen slettes om 5 sekunder.", - "title": "✅ Ticket lukket" - }, - "closed_by_member_with_reason": { - "description": "Denne ticket er blevet lukket af %s: '%s'\nKanalen slettes om 5 sekunder.", - "title": "✅ Ticket lukket" - }, - "closed_with_reason": { - "description": "Denne ticket er blevet lukket: '%s'\nKanalen slettes om 5 sekunder.", - "title": "✅ Ticket lukket" - }, - "member_added": { - "description": "%s er blevet tilføjet af %s", - "title": "Medlem tilføjet" - }, - "member_removed": { - "description": "%s er blevet fjernet af %s", - "title": "Medlem fjernet" - }, - "opening_message": { - "content": "%s\n%s har oprettet en ny ticket", - "fields": { - "topic": "Emne" - } - }, - "questions": "Besvar venligst følgende spørgsmål:\n\n%s", - "released": { - "description": "%s har frigivet denne ticket.", - "title": "✅ Ticket frigivet" - }, - "survey": { - "complete": { - "description": "Tak for din feedback.", - "title": "✅ Tak skal du have" - }, - "start": { - "buttons": { - "ignore": "Nej", - "start": "Start undersøgelse" - }, - "description": "Hej, %s. Før denne kanal slettes, har du noget imod at gennemføre en hurtig %d-spørgsmålsundersøgelse?", - "title": "❔ Feedback" - } - }, - "unclaim": "unclaim" - }, - "updated_permissions": "✅ Slash kommando tilladelser opdateret" -} diff --git a/src/locales/de-DE.json b/src/locales/de-DE.json deleted file mode 100644 index 0514ba5..0000000 --- a/src/locales/de-DE.json +++ /dev/null @@ -1,610 +0,0 @@ -{ - "blacklisted": "❌ Du stehst auf der Blacklist", - "bot": { - "missing_permissions": { - "description": "Discord Tickets erfordert die folgenden Berechtigungen: \n%s ", - "title": "⚠️" - }, - "version": "[Discord Tickets] (%s) v%s von [eartharoid](%s)" - }, - "collector_expires_in": "Läuft in %d Sekunden ab", - "command_execution_error": { - "description": "Ein unerwarteter Fehler ist aufgetreten.\nFrage bitte den Andministrator um Hilfe.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Füge einen Benutzer zum Ticket hinzu", - "name": "hinzufügen", - "options": { - "member": { - "description": "Das Mitglied, das dem Ticket hinzugefügt werden soll", - "name": "Mitglied" - }, - "ticket": { - "description": "Das Ticket, zu dem das Mitglied hinzugefügt werden soll", - "name": "ticket" - } - }, - "response": { - "added": { - "description": "%s wurde zu %s hinzugefügt.", - "title": "✅ Benutzer hinzugefügt" - }, - "no_member": { - "description": "Bitte erwähne den Benutzer, den Du hinzufügen möchtest.", - "title": "❌ Unbekanntes Mitglied" - }, - "no_permission": { - "description": "Du bist weder der Ersteller dieses Tickets, noch ein Support-Mitglied; Du kannst keine neuen Benutzer zu diesem Ticket hinzufügen.", - "title": "❌ Unzureichende Berechtigung" - }, - "not_a_ticket": { - "description": "Bitte führe den Befehl im Ticket-Kanal aus, oder erwähne diesen.", - "title": "❌ Dies ist kein Ticket-Kanal" - } - } - }, - "blacklist": { - "description": "Sieh dir die Blacklist an oder bearbeite sie", - "name": "blacklist", - "options": { - "add": { - "description": "Füge ein Mitglied oder eine Rolle der Blacklist hinzu", - "name": "hinzufügen", - "options": { - "member_or_role": { - "description": "Mitglied oder Rolle der Blacklist hinzufügen", - "name": "mitglied_oder_rolle" - } - } - }, - "remove": { - "description": "Entferne einen Nutzer oder eine Rolle von der Blacklist", - "name": "entfernen", - "options": { - "member_or_role": { - "description": "Der Nutzer oder die Rolle zum Entfernen von der Blacklist", - "name": "mitglied_oder_rolle" - } - } - }, - "show": { - "description": "Zeige die Nutzer und Rollen in der Blacklist", - "name": "zeigen" - } - }, - "response": { - "empty_list": { - "description": "Es sind keine Mitglieder oder Rollen auf der Blacklist. Gebe `/blacklist add` ein, um ein Mitglied oder eine Rolle zur Blacklist hinzuzufügen.", - "title": "📃 Blockierte Benutzer und Rollen" - }, - "illegal_action": { - "description": "%s ist ein Support-Mitglied und kann nicht blockiert werden.", - "title": "❌ Sie können kein Support-Mitglied blockieren" - }, - "invalid": { - "description": "Dieses Mitglied oder diese Rolle können nicht von der Blacklist entfernt werden, weil sie nicht geblacklisted sind.", - "title": "❌ Fehler" - }, - "list": { - "fields": { - "members": "Mitglieder", - "roles": "Rollen" - }, - "title": "📃 Blockierte Benutzer und Rollen" - }, - "member_added": { - "description": "<@%s> wurde blockiert. Er/Sie kann nicht mehr mit dem Bot interagieren.", - "title": "✅ Benutzer zur Blacklist hinzugefügt" - }, - "member_removed": { - "description": "<@%s> wurde von der Blacklist entfernt. Er/Sie kann den Bot wieder verwenden.", - "title": "✅ Mitglieder von der Blacklist entfernt" - }, - "role_added": { - "description": "<@&%s> wurde zur Blacklist hinzugefügt. Mitglieder mit dieser Rolle können nicht mehr mit dem Bot interagieren.", - "title": "✅ Rolle zur Blacklist hinzugefügt" - }, - "role_removed": { - "description": "<@&%s> wurde von der Blacklist entfernt. Mitglieder mit dieser Rolle können den Bot wieder benutzen.", - "title": "✅ Rolle von der Blacklist entfernt" - } - } - }, - "close": { - "description": "Schließe einen Ticket-Kanal", - "name": "schließen", - "options": { - "reason": { - "description": "Der Grund für die Schließung des Tickets", - "name": "grund" - }, - "ticket": { - "description": "Das zu schließende Ticket, entweder die Nummer oder die Kanal-ID", - "name": "ticket" - }, - "time": { - "description": "Schließe alle Tickets, die seit der angegebenen Zeit inaktiv sind", - "name": "zeit" - } - }, - "response": { - "canceled": { - "description": "Du hast den Vorgang abgebrochen.", - "title": "🚫 Abgebrochen" - }, - "closed": { - "description": "Das Ticket #%s wurde geschlossen.", - "title": "✅ Ticket geschlossen" - }, - "closed_multiple": { - "description": [ - "Das Ticket %d wurde geschlossen.", - "Die Tickets %d wurden geschlossen." - ], - "title": [ - "✅ Ticket geschlossen", - "✅ Tickets geschlossen" - ] - }, - "confirm": { - "buttons": { - "cancel": "Abbrechen", - "confirm": "Schließen" - }, - "description": "Bitte bestätige Deine Entscheidung.", - "description_with_archive": "Das Ticket wird zum späteren Nachschlagen archiviert.", - "title": "❔ Sind Sie sicher?" - }, - "confirm_multiple": { - "buttons": { - "cancel": "Abbruch", - "confirm": [ - "%d Ticket schließen", - "%d Tickets schließen" - ] - }, - "description": [ - "Du bist dabei, %d Ticket zu schließen.", - "Du bist dabei, %d Tickets zu schließen." - ], - "title": "❔ Sind Sie sicher?" - }, - "confirmation_timeout": { - "description": "Sie haben zu lange zum Bestätigen gebraucht.", - "title": "❌ Zu lange gewartet" - }, - "invalid_time": { - "description": "Die eingegebene Zeit konnte nicht verarbeitet werden.", - "title": "❌ Ungültige Eingabe" - }, - "no_permission": { - "description": "Du bist kein Teammitglied oder der Ticketersteller", - "title": "Unzureichende Rechte" - }, - "no_tickets": { - "description": "Es gibt keine inaktiven Tickets für den ausgewählten Zeitraum.", - "title": "❌ Keine Tickets zum schließen" - }, - "not_a_ticket": { - "description": "Bitte verwende diesen Befehl in einem Ticketkanal oder verwende das Ticket-Flag.\nGebe `/help close` ein, um weitere Informationen zu erhalten.", - "title": "❌ Das ist kein Ticket-Kanal" - }, - "unresolvable": { - "description": "`%s` konnte keinem Ticket zugeordnet werden. Bitte verwende die Kanal ID / Erwähnung des Kanals, oder die Nummer.", - "title": "❌ Fehler" - } - } - }, - "help": { - "description": "Liste die Befehle, auf die Du Zugriff hast", - "name": "Hilfe", - "response": { - "list": { - "description": "Die Befehle, auf die Du Zugriff hast, sind unten aufgeführt. Um ein Ticket zu erstellen, gebe **`/new`** ein.", - "fields": { - "commands": "Befehle" - }, - "title": "❔ Hilfe" - } - } - }, - "new": { - "description": "Erstelle ein neues Ticket", - "name": "neu", - "options": { - "topic": { - "description": "Das Thema des Tickets", - "name": "thema" - } - }, - "request_topic": { - "description": "Bitte gebe in wenigen Worten kurz an, worum es bei diesem Ticket geht.", - "title": "⚠️ Ticketthema" - }, - "response": { - "created": { - "description": "Dein Ticket wurde erstellt: %s.", - "title": "✅ Ticket erstellt" - }, - "error": { - "title": "❌ Fehler" - }, - "has_a_ticket": { - "description": "Bitte verwende Dein schon vorhandenes Ticket (<#%s>) oder schließe es, bevor Du ein neues erstellst.", - "title": "❌ Du hast bereits ein offenes Ticket" - }, - "max_tickets": { - "description": "Bitte verwende `/close`, um nicht benötigte Tickets zu schließen.\n\n%s", - "title": "❌ Du hast bereits %d offene Tickets" - }, - "no_categories": { - "description": "Ein Server Administrator muss zuerst eine Kategorie erstellen, bevor Du ein Ticket erstellen kannst.", - "title": "❌ Kann kein neues Ticket erstellen" - }, - "select_category": { - "description": "Wähle die Kategorie aus, die für das Thema Deines Tickets am relevantesten ist.", - "title": "🔤 Bitte wähle eine Kategorie" - }, - "select_category_timeout": { - "description": "Du hast zu lange gebraucht um eine Ticket Kategorie zu wählen.", - "title": "❌ Interaktionszeit abgelaufen" - } - } - }, - "panel": { - "description": "Erstelle ein neues Ticket-Panel", - "name": "panel", - "options": { - "categories": { - "description": "Eine durch Kommas getrennte Liste von Kategorie-IDs", - "name": "kategorien" - }, - "description": { - "description": "Die Beschreibung für die Panel-Nachricht", - "name": "beschreibung" - }, - "image": { - "description": "Eine Bild-URL für die Panel-Nachricht", - "name": "bild" - }, - "just_type": { - "description": "Ein \"just type\"-Panel erstellen?", - "name": "just_type" - }, - "thumbnail": { - "description": "Eine Miniaturbild-URL für die Panel-Nachricht", - "name": "thumbnail" - }, - "title": { - "description": "Der Titel für die Panel-Nachricht", - "name": "titel" - } - }, - "response": { - "invalid_category": { - "description": "Eine oder mehrere der angegebenen Kategorie IDs sind nicht gültig.", - "title": "❌ Ungültige Kategorie" - }, - "too_many_categories": { - "description": "Das Feld \"just type\" kann nur mit einer einzigen Kategorie verwendet werden.", - "title": "❌ Zu viele Kategorien" - } - } - }, - "remove": { - "description": "Entferne einen Benutzer von einem Ticket", - "name": "entfernen", - "options": { - "member": { - "description": "Das Mitglied, das aus dem Ticket entfernt werden soll", - "name": "mitglied" - }, - "ticket": { - "description": "Das Ticket, aus dem das Mitglied entfernt werden soll", - "name": "ticket" - } - }, - "response": { - "no_member": { - "description": "Bitte nenne den Benutzer den Du entfernen möchtest.", - "title": "❌ Unbekannter Benutzer" - }, - "no_permission": { - "description": "Du bist weder Ersteller dieses Tickets, noch Support-Mitglied; Du kannst keine Benutzer von diesem Ticket entfernen.", - "title": "❌ Unzureichende Berechtigung" - }, - "not_a_ticket": { - "description": "Bitte führe en Befehl im Ticket-Kanal aus, oder erwähne diesen.", - "title": "❌ Das ist kein Ticket-Kanal" - }, - "removed": { - "description": "%s wurde von %s entfernt.", - "title": "✅ Benutzer entfernt" - } - } - }, - "settings": { - "description": "Konfiguriere Discord Tickets", - "name": "settings", - "options": { - "categories": { - "description": "Verwalte Deine Ticketkategorien", - "name": "kategorien", - "options": { - "create": { - "description": "Erstelle eine neue Kategorie", - "name": "erstellen", - "options": { - "name": { - "description": "Der Name der Kategorie", - "name": "name" - }, - "roles": { - "description": "Eine durch Kommas getrennte Liste von Mitarbeiterrollen-IDs für diese Kategorie", - "name": "roles" - } - } - }, - "delete": { - "description": "Lösche eine Kategorie", - "name": "delete", - "options": { - "id": { - "description": "Die ID der zu löschenden Kategorie", - "name": "id" - } - } - }, - "edit": { - "description": "Nehme Änderungen an der Konfiguration einer Kategorie vor", - "name": "edit", - "options": { - "claiming": { - "description": "Ticketanspruch aktivieren?", - "name": "beanspruchen" - }, - "id": { - "description": "Die ID der zu bearbeitenden Kategorie", - "name": "id" - }, - "image": { - "description": "Eine Bild-URL", - "name": "bild" - }, - "max_per_member": { - "description": "Die maximale Anzahl an Tickets, die ein Mitglied in dieser Kategorie haben kann", - "name": "max_per_member" - }, - "name": { - "description": "Der Kategoriename", - "name": "name" - }, - "name_format": { - "description": "Das Format des Ticketnamens", - "name": "name_format" - }, - "opening_message": { - "description": "Der zu sendende Text, wenn ein Ticket geöffnet wird", - "name": "opening_message" - }, - "opening_questions": { - "description": "Fragen, die gestellt werden müssen, wenn ein Ticket eröffnet wird.", - "name": "opening_questions" - }, - "ping": { - "description": "Eine durch Kommas getrennte Liste von Rollen-IDs, die gepingt werden sollen", - "name": "ping" - }, - "require_topic": { - "description": "Den Benutzer auffordern, das Thema des Tickets anzugeben?", - "name": "require_topic" - }, - "roles": { - "description": "Eine durch Kommas getrennte Liste von Mitarbeiterrollen-IDs", - "name": "roles" - }, - "survey": { - "description": "Die zu verwendende Umfrage", - "name": "survey" - } - } - }, - "list": { - "description": "Kategorien auflisten", - "name": "list" - } - } - }, - "set": { - "description": "Optionen festlegen", - "name": "set", - "options": { - "close_button": { - "description": "Schließen per Knopfdruck aktivieren?", - "name": "close_button" - }, - "colour": { - "description": "Die Standardfarbe", - "name": "colour" - }, - "error_colour": { - "description": "Die Fehlerfarbe", - "name": "error_colour" - }, - "footer": { - "description": "Der eingebettete Fußzeilentext", - "name": "footer" - }, - "locale": { - "description": "Das Gebietsschema (Sprache)", - "name": "gebietsschema" - }, - "log_messages": { - "description": "Nachrichten von Tickets speichern?", - "name": "log_messages" - }, - "success_colour": { - "description": "Die Erfolgsfarbe", - "name": "success_colour" - } - } - } - }, - "response": { - "category_created": "✅ Die Ticketkategorie `%s` wurde erstellt", - "category_deleted": "✅ Die Ticketkategorie `%s` wurde gelöscht", - "category_does_not_exist": "❌ Es existiert keine Kategorie mit der angegebenen ID", - "category_list": "Ticketkategorien", - "category_updated": "✅ Die Ticketkategorie `%s` wurde aktualisiert", - "settings_updated": "✅ Die Einstellungen wurden aktualisiert" - } - }, - "stats": { - "description": "Ticket Statistiken ansehen", - "fields": { - "messages": "Nachrichten", - "response_time": { - "minutes": "%s Minuten", - "title": "Durchschnittliche Reaktionszeit" - }, - "tickets": "Tickets" - }, - "name": "stats", - "response": { - "global": { - "description": "Statistiken über Tickets in allen Gilden, in denen diese Discord-Tickets-Instanz verwendet wird.", - "title": "📊 Globale Statistiken" - }, - "guild": { - "description": "Statistiken von diesem Server. Die Daten sind für eine Stunde im Zwischenspeicher.", - "title": "📊 Statistiken von diesem Server" - } - } - }, - "survey": { - "description": "Umfragen-Ergebnisse ansehen", - "name": "Umfrage", - "options": { - "survey": { - "description": "Der Name der Umfrage, deren Antworten angezeigt werden sollen", - "name": "Umfrage" - } - }, - "response": { - "list": { - "title": "📃 Umfragen" - } - } - }, - "tag": { - "description": "Verwende einen 'Tag' als Antwort", - "name": "tag", - "options": { - "tag": { - "description": "Der Name des zu verwendenden Tags", - "name": "tag" - } - }, - "response": { - "error": "❌ Fehler", - "list": { - "title": "📃 Tag-Liste" - }, - "missing": "Dieser Tag benötigt die folgenden Vorraussetzungen:\n%s", - "not_a_ticket": { - "description": "Dieses Tag kann nur innerhalb eines Ticketkanals verwendet werden, da es Ticketreferenzen verwendet.", - "title": "❌ Das ist kein Ticket-Kanal" - } - } - }, - "topic": { - "description": "Änder das Anliegen Deines Tickets", - "name": "Thema", - "options": { - "new_topic": { - "description": "Das neue Thema des Tickets", - "name": "new_topic" - } - }, - "response": { - "changed": { - "description": "Das Anliegen dieses Tickets wurde erfolgreich geändert.", - "title": "✅ Anliegen geändert" - }, - "not_a_ticket": { - "description": "Bitte verwende diesen Befehl nur in einem Ticket-Kanal in dem Du das Anliegen ändern willst.", - "title": "❌ Das ist kein Ticket-Kanal" - } - } - } - }, - "message_will_be_deleted_in": "Diese Nachricht wird in %d Sekunden gelöscht.", - "missing_permissions": { - "description": "Du hast nicht die benötigten Berechtigungen um diesen Befehl zu verwenden:\n%s", - "title": "❌ Fehler" - }, - "panel": { - "create_ticket": "Ticket erstellen" - }, - "ticket": { - "claim": "Beanspruchen", - "claimed": { - "description": "Dieses Ticket gehört jetzt %s.", - "title": "✅ Ticket beansprucht" - }, - "close": "Schließen", - "closed": { - "description": "Dieses Ticket wurde geschlossen.\nDieser Kanal wird in 5 Sekunden gelöscht.", - "title": "✅ Ticket geschlossen" - }, - "closed_by_member": { - "description": "Dieses Ticket wurde von %s geschlossen.\nDieser Kanal wird in 5 Sekunden gelöscht.", - "title": "✅ Ticket geschlossen" - }, - "closed_by_member_with_reason": { - "description": "Dieses Ticket wurde von %s geschlossen: `%s`\nDieser Kanal wird in 5 Sekunden gelöscht.", - "title": "✅ Ticket geschlossen" - }, - "closed_with_reason": { - "description": "Dieses Ticket wurde geschlossen: `%s`\nDieser Kanal wird in 5 Sekunden gelöscht.", - "title": "✅ Ticket geschlossen" - }, - "member_added": { - "description": "%s wurde von %s hinzugefügt.", - "title": "Mitglied hinzugefügt" - }, - "member_removed": { - "description": "%s wurde von %s entfernt.", - "title": "Benutzer entfernt" - }, - "opening_message": { - "content": "%s\n%s hat ein neues Ticket erstellt", - "fields": { - "topic": "Anliegen" - } - }, - "questions": "Bitte beantworte die folgenden Fragen:\n\n%s", - "released": { - "description": "%s hat ein neues Ticket veröffentlicht.", - "title": "✅ Ticket veröffentlicht" - }, - "survey": { - "complete": { - "description": "Vielen Dank für Dein Feedback!", - "title": "✅ Danke" - }, - "start": { - "buttons": { - "ignore": "Nein", - "start": "Umfrage starten" - }, - "description": "Hey, %s. Würde es Dir etwas ausmachen, eine kurze %d-Fragenumfrage auszufüllen, bevor dieser Kanal gelöscht wird?", - "title": "❔ Feedback" - } - }, - "unclaim": "Freigeben" - }, - "updated_permissions": "✅ Slash-Befehlsberechtigungen aktualisiert" -} diff --git a/src/locales/en-GB.json b/src/locales/en-GB.json deleted file mode 100644 index b59ead5..0000000 --- a/src/locales/en-GB.json +++ /dev/null @@ -1,610 +0,0 @@ -{ - "blacklisted": "❌ You are blacklisted", - "bot": { - "missing_permissions": { - "description": "Discord Tickets requires the following permissions:\n%s ", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s by [eartharoid](%s)" - }, - "collector_expires_in": "Expires in %d seconds", - "command_execution_error": { - "description": "An unexpected error occurred during command execution.\nPlease ask an administrator to check the console output / logs for details.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Add a member to a ticket", - "name": "add", - "options": { - "member": { - "description": "The member to add to the ticket", - "name": "member" - }, - "ticket": { - "description": "The ticket to add the member to", - "name": "ticket" - } - }, - "response": { - "added": { - "description": "%s has been added to %s.", - "title": "✅ Member added" - }, - "no_member": { - "description": "Please mention the member you want to add.", - "title": "❌ Unknown member" - }, - "no_permission": { - "description": "You are not the creator of this ticket and you are not a staff member; you can't add members to this ticket.", - "title": "❌ Insufficient permission" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ This isn't a ticket channel" - } - } - }, - "blacklist": { - "description": "View or modify the blacklist", - "name": "blacklist", - "options": { - "add": { - "description": "Add a member or role to the blacklist", - "name": "add", - "options": { - "member_or_role": { - "description": "The member or role to add to the blacklist", - "name": "member_or_role" - } - } - }, - "remove": { - "description": "Remove a member or role from the blacklist", - "name": "remove", - "options": { - "member_or_role": { - "description": "The member or role to remove from the blacklist", - "name": "member_or_role" - } - } - }, - "show": { - "description": "Show the members and roles in the blacklist", - "name": "show" - } - }, - "response": { - "empty_list": { - "description": "There are no members or roles blacklisted. Type `/blacklist add` to add a member or role to the blacklist.", - "title": "📃 Blacklisted members and roles" - }, - "illegal_action": { - "description": "%s is a staff member and cannot be blacklisted.", - "title": "❌ You can't blacklist this member" - }, - "invalid": { - "description": "This member or role can not be removed from the blacklist as they are not blacklisted.", - "title": "❌ Error" - }, - "list": { - "fields": { - "members": "Members", - "roles": "Roles" - }, - "title": "📃 Blacklisted members and roles" - }, - "member_added": { - "description": "<@%s> has been added to the blacklist. They will no longer be able to interact with the bot.", - "title": "✅ Added member to blacklist" - }, - "member_removed": { - "description": "<@%s> has been removed from the blacklist. They can now use the bot again.", - "title": "✅ Removed member from blacklist" - }, - "role_added": { - "description": "<@&%s> has been added to the blacklist. Members with this role will no longer be able to interact with the bot.", - "title": "✅ Added role to blacklist" - }, - "role_removed": { - "description": "<@&%s> has been removed from the blacklist. Members with this role can now use the bot again.", - "title": "✅ Removed role from blacklist" - } - } - }, - "close": { - "description": "Close a ticket channel", - "name": "close", - "options": { - "reason": { - "description": "The reason for closing the ticket(s)", - "name": "reason" - }, - "ticket": { - "description": "The ticket to close, either the number or the channel ID", - "name": "ticket" - }, - "time": { - "description": "Close all tickets that have been inactive for the specified time", - "name": "time" - } - }, - "response": { - "canceled": { - "description": "You canceled the operation.", - "title": "🚫 Canceled" - }, - "closed": { - "description": "Ticket #%s has been closed.", - "title": "✅ Ticket closed" - }, - "closed_multiple": { - "description": [ - "%d ticket has been closed.", - "%d tickets have been closed." - ], - "title": [ - "✅ Ticket closed", - "✅ Tickets closed" - ] - }, - "confirm": { - "buttons": { - "cancel": "Cancel", - "confirm": "Close" - }, - "description": "Please confirm your decision.", - "description_with_archive": "The ticket will be archived for future reference.", - "title": "❔ Are you sure?" - }, - "confirm_multiple": { - "buttons": { - "cancel": "Cancel", - "confirm": [ - "Close %d ticket", - "Close %d tickets" - ] - }, - "description": [ - "You are about to close %d ticket.", - "You are about to close %d tickets." - ], - "title": "❔ Are you sure?" - }, - "confirmation_timeout": { - "description": "You took too long to confirm.", - "title": "❌ Interaction time expired" - }, - "invalid_time": { - "description": "The time period provided could not be parsed.", - "title": "❌ Invalid input" - }, - "no_permission": { - "description": "You are not a staff member or the ticket creator.", - "title": "❌ Insufficient permission" - }, - "no_tickets": { - "description": "There are no tickets which have been inactive for this time period.", - "title": "❌ No tickets to close" - }, - "not_a_ticket": { - "description": "Please use this command in a ticket channel or use the ticket flag.\nType `/help close` for more information.", - "title": "❌ This isn't a ticket channel" - }, - "unresolvable": { - "description": "`%s` could not be resolved to a ticket. Please provide the ticket ID/mention or number.", - "title": "❌ Error" - } - } - }, - "help": { - "description": "List the commands you have access to", - "name": "help", - "response": { - "list": { - "description": "The commands you have access to are listed below. To create a ticket, type **`/new`**.", - "fields": { - "commands": "Commands" - }, - "title": "❔ Help" - } - } - }, - "new": { - "description": "Create a new ticket", - "name": "new", - "options": { - "topic": { - "description": "The topic of the ticket", - "name": "topic" - } - }, - "request_topic": { - "description": "Please briefly state what this ticket is about in a few words.", - "title": "⚠️ Ticket topic" - }, - "response": { - "created": { - "description": "Your ticket has been created: %s.", - "title": "✅ Ticket created" - }, - "error": { - "title": "❌ Error" - }, - "has_a_ticket": { - "description": "Please use your existing ticket (<#%s>) or close it before creating another.", - "title": "❌ You already have an open ticket" - }, - "max_tickets": { - "description": "Please use `/close` to close any unneeded tickets.\n\n%s", - "title": "❌ You already have %d open tickets" - }, - "no_categories": { - "description": "A server administrator must create at least one ticket category before a new ticket can be opened.", - "title": "❌ Can't create ticket" - }, - "select_category": { - "description": "Select the category most relevant to your ticket's topic.", - "title": "🔤 Please select the ticket category" - }, - "select_category_timeout": { - "description": "You took too long to select the ticket category.", - "title": "❌ Interaction time expired" - } - } - }, - "panel": { - "description": "Create a new ticket panel", - "name": "panel", - "options": { - "categories": { - "description": "A comma-separated list of category IDs", - "name": "categories" - }, - "description": { - "description": "The description for the panel message", - "name": "description" - }, - "image": { - "description": "An image URL for the panel message", - "name": "image" - }, - "just_type": { - "description": "Create a \"just type\" panel?", - "name": "just_type" - }, - "thumbnail": { - "description": "A thumbnail image URL for the panel message", - "name": "thumbnail" - }, - "title": { - "description": "The title for the panel message", - "name": "title" - } - }, - "response": { - "invalid_category": { - "description": "One or more of the specified category IDs is invalid.", - "title": "❌ Invalid category" - }, - "too_many_categories": { - "description": "The \"just type\" panel can only be used with a single category.", - "title": "❌ Too many categories" - } - } - }, - "remove": { - "description": "Remove a member from a ticket", - "name": "remove", - "options": { - "member": { - "description": "The member to remove from the ticket", - "name": "member" - }, - "ticket": { - "description": "The ticket to remove the member from", - "name": "ticket" - } - }, - "response": { - "no_member": { - "description": "Please mention the member you want to remove.", - "title": "❌ Unknown member" - }, - "no_permission": { - "description": "You are not the creator of this ticket and you are not a staff member; you can't remove members from this ticket.", - "title": "❌ Insufficient permission" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ This isn't a ticket channel" - }, - "removed": { - "description": "%s has been removed from %s.", - "title": "✅ Member removed" - } - } - }, - "settings": { - "description": "Configure Discord Tickets", - "name": "settings", - "options": { - "categories": { - "description": "Manage your ticket categories", - "name": "categories", - "options": { - "create": { - "description": "Create a new category", - "name": "create", - "options": { - "name": { - "description": "The name of the category", - "name": "name" - }, - "roles": { - "description": "A comma-separated list of staff role IDs for this category", - "name": "roles" - } - } - }, - "delete": { - "description": "Delete a category", - "name": "delete", - "options": { - "id": { - "description": "The ID of the category to delete", - "name": "id" - } - } - }, - "edit": { - "description": "Make changes to a category's configuration", - "name": "edit", - "options": { - "claiming": { - "description": "Enable ticket claiming?", - "name": "claiming" - }, - "id": { - "description": "The ID of the category to edit", - "name": "id" - }, - "image": { - "description": "An image URL", - "name": "image" - }, - "max_per_member": { - "description": "The maximum number of tickets a member can have in this category", - "name": "max_per_member" - }, - "name": { - "description": "The category name", - "name": "name" - }, - "name_format": { - "description": "The ticket name format", - "name": "name_format" - }, - "opening_message": { - "description": "The text to send when a ticket is opened", - "name": "opening_message" - }, - "opening_questions": { - "description": "Questions to ask when a ticket is opened.", - "name": "opening_questions" - }, - "ping": { - "description": "A comma-separated list of role IDs to ping", - "name": "ping" - }, - "require_topic": { - "description": "Require the user to give the topic of the ticket?", - "name": "require_topic" - }, - "roles": { - "description": "A comma-separated list of staff role IDs", - "name": "roles" - }, - "survey": { - "description": "The survey to use", - "name": "survey" - } - } - }, - "list": { - "description": "List categories", - "name": "list" - } - } - }, - "set": { - "description": "Set options", - "name": "set", - "options": { - "close_button": { - "description": "Enable closing with a button?", - "name": "close_button" - }, - "colour": { - "description": "The standard colour", - "name": "colour" - }, - "error_colour": { - "description": "The error colour", - "name": "error_colour" - }, - "footer": { - "description": "The embed footer text", - "name": "footer" - }, - "locale": { - "description": "The locale (language)", - "name": "locale" - }, - "log_messages": { - "description": "Store messages from tickets?", - "name": "log_messages" - }, - "success_colour": { - "description": "The success colour", - "name": "success_colour" - } - } - } - }, - "response": { - "category_created": "✅ The `%s` ticket category has been created", - "category_deleted": "✅ The `%s` ticket category has been deleted", - "category_does_not_exist": "❌ No category exists with the provided ID", - "category_list": "Ticket categories", - "category_updated": "✅ The `%s` ticket category has been updated", - "settings_updated": "✅ Settings have been updated" - } - }, - "stats": { - "description": "Display ticket statistics", - "fields": { - "messages": "Messages", - "response_time": { - "minutes": "%s minutes", - "title": "Avg. response time" - }, - "tickets": "Tickets" - }, - "name": "stats", - "response": { - "global": { - "description": "Statistics about tickets across all guilds where this Discord Tickets instance is used.", - "title": "📊 Global stats" - }, - "guild": { - "description": "Statistics about tickets within this guild. This data is cached for an hour.", - "title": "📊 This server's stats" - } - } - }, - "survey": { - "description": "View survey responses", - "name": "survey", - "options": { - "survey": { - "description": "The name of the survey to view responses of", - "name": "survey" - } - }, - "response": { - "list": { - "title": "📃 Surveys" - } - } - }, - "tag": { - "description": "Use a tag response", - "name": "tag", - "options": { - "tag": { - "description": "The name of the tag to use", - "name": "tag" - } - }, - "response": { - "error": "❌ Error", - "list": { - "title": "📃 Tag list" - }, - "missing": "This tag requires the following arguments:\n%s", - "not_a_ticket": { - "description": "This tag can only be used within a ticket channel as it uses ticket references.", - "title": "❌ This isn't a ticket channel" - } - } - }, - "topic": { - "description": "Change the topic of the ticket", - "name": "topic", - "options": { - "new_topic": { - "description": "The new topic of the ticket", - "name": "new_topic" - } - }, - "response": { - "changed": { - "description": "This ticket's topic has been changed.", - "title": "✅ Topic changed" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel you want to change the topic of.", - "title": "❌ This isn't a ticket channel" - } - } - } - }, - "message_will_be_deleted_in": "This message will be deleted in %d seconds", - "missing_permissions": { - "description": "You do not have the permissions required to use this command:\n%s", - "title": "❌ Error" - }, - "panel": { - "create_ticket": "Create a ticket" - }, - "ticket": { - "claim": "Claim", - "claimed": { - "description": "%s has claimed this ticket.", - "title": "✅ Ticket claimed" - }, - "close": "Close", - "closed": { - "description": "This ticket has been closed.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_by_member": { - "description": "This ticket has been closed by %s.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_by_member_with_reason": { - "description": "This ticket has been closed by %s: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_with_reason": { - "description": "This ticket has been closed: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "member_added": { - "description": "%s has been added by %s", - "title": "Member added" - }, - "member_removed": { - "description": "%s has been removed by %s", - "title": "Member removed" - }, - "opening_message": { - "content": "%s\n%s has created a new ticket", - "fields": { - "topic": "Topic" - } - }, - "questions": "Please answer the following questions:\n\n%s", - "released": { - "description": "%s has released this ticket.", - "title": "✅ Ticket released" - }, - "survey": { - "complete": { - "description": "Thank you for your feedback.", - "title": "✅ Thank you" - }, - "start": { - "buttons": { - "ignore": "No", - "start": "Start survey" - }, - "description": "Hey, %s. Before this channel is deleted, would you mind completing a quick %d-question survey?", - "title": "❔ Feedback" - } - }, - "unclaim": "Release" - }, - "updated_permissions": "✅ Slash command permissions updated" -} diff --git a/src/locales/en-US.json b/src/locales/en-US.json deleted file mode 100644 index 61a4a84..0000000 --- a/src/locales/en-US.json +++ /dev/null @@ -1,324 +0,0 @@ -{ - "bot": { - "missing_permissions": { - "description": "Discord Tickets requires the following permissions:\n%s", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s by [eartharoid](%s)" - }, - "collector_expires_in": "Expires in %d seconds", - "command_execution_error": { - "description": "An unexpected error occurred during command execution.\nPlease ask an administrator to check the console output / logs for details.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Add a member to a ticket", - "name": "add", - "response": { - "added": { - "description": "%s has been added to %s.", - "title": "✅ Member added" - }, - "no_member": { - "description": "Please mention the member you want to add.", - "title": "❌ Unknown member" - }, - "no_permission": { - "description": "You are not the creator of this ticket and you are not a staff member; you can't add members to this ticket.", - "title": "❌ Insufficient permission" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ This isn't a ticket channel" - } - } - }, - "blacklist": { - "description": "Blacklist/unblacklist a member from interacting with the bot", - "name": "blacklist", - "response": { - "empty_list": { - "description": "There are no members or roles blacklisted. Type `%sblacklist ` to add a member or role to the blacklist.", - "title": "📃 Blacklisted members and roles" - }, - "illegal_action": { - "description": "%s is a staff member and cannot be blacklisted.", - "title": "❌ You can't blacklist this member" - }, - "list": { - "title": "📃 Blacklisted members and roles" - }, - "member_added": { - "description": "<@%s> has been added to the blacklist. They will no longer be able to interact with the bot.", - "title": "✅ Added member to blacklist" - }, - "member_removed": { - "description": "<@%s> has been removed from the blacklist. They can now use the bot again.", - "title": "✅ Removed member from blacklist" - }, - "role_added": { - "description": "<@&%s> has been added to the blacklist. Members with this role will no longer be able to interact with the bot.", - "title": "✅ Added role to blacklist" - }, - "role_removed": { - "description": "<@&%s> has been removed from the blacklist. Members with this role can now use the bot again.", - "title": "✅ Removed role from blacklist" - } - } - }, - "close": { - "description": "Close a ticket channel", - "name": "close", - "response": { - "closed": { - "description": "Ticket #%s has been closed.", - "title": "✅ Ticket closed" - }, - "closed_multiple": { - "description": [ - "%d ticket has been closed.", - "%d tickets have been closed." - ], - "title": [ - "✅ Ticket closed", - "✅ Tickets closed" - ] - }, - "confirm": { - "description": "React with ✅ to close this ticket.", - "description_with_archive": "You will be able to view an archived version of it after.\nReact with ✅ to close this ticket.", - "title": "❔ Are you sure?" - }, - "confirm_multiple": { - "description": [ - "React with ✅ to close %d ticket.", - "React with ✅ to close %d tickets." - ], - "title": "❔ Are you sure?" - }, - "confirmation_timeout": { - "description": "You took too long to confirm.", - "title": "❌ Reaction time expired" - }, - "invalid_time": { - "description": "The time period provided could not be parsed.", - "title": "❌ Invalid input" - }, - "no_tickets": { - "description": "There are no tickets which have been inactive for this time period.", - "title": "❌ No tickets to close" - }, - "not_a_ticket": { - "description": "Please use this command in a ticket channel or use the ticket flag.\nType `%shelp close` for more information.", - "title": "❌ This isn't a ticket channel" - }, - "unresolvable": { - "description": "`%s` could not be resolved to a ticket. Please provide the ticket ID/mention or number.", - "title": "❌ Error" - } - } - }, - "help": { - "description": "List commands you have access to, or find out more about a command", - "name": "help", - "response": { - "list": { - "description": "The commands you have access to are listed below. For more information about a command, type `{prefix}help [command]`. To create a ticket, type `{prefix}new [topic]`.", - "fields": { - "commands": "Commands" - }, - "title": "❔ Help" - } - } - }, - "new": { - "description": "Create a new ticket", - "name": "new", - "request_topic": { - "description": "Please briefly state what this ticket is about in a few words.", - "title": "Ticket topic" - }, - "response": { - "created": { - "description": "Your ticket has been created: %s.", - "title": "✅ Ticket created" - }, - "error": { - "title": "❌ Error" - }, - "has_a_ticket": { - "description": "Please use your existing ticket (<#%s>) or close it before creating another.", - "title": "❌ You already have an open ticket" - }, - "max_tickets": { - "description": "Please use `%sclose` to close any unneeded tickets.\n\n%s", - "title": "❌ You already have %d open tickets" - }, - "no_categories": { - "description": "A server administrator must create at least one ticket category before a new ticket can be opened.", - "title": "❌ Can't create ticket" - }, - "select_category": { - "description": "Select the category most relevant to your ticket's topic:\n\n%s", - "title": "🔤 Please select the ticket category" - }, - "select_category_timeout": { - "description": "You took too long to select the ticket category.", - "title": "❌ Reaction time expired" - } - } - }, - "panel": { - "description": "Create a new ticket panel", - "name": "panel", - "response": { - "invalid_category": { - "description": "One or more of the specified category IDs is invalid.", - "title": "❌ Invalid category" - } - } - }, - "remove": { - "description": "Remove a member from a ticket", - "name": "remove", - "response": { - "no_member": { - "description": "Please mention the member you want to remove.", - "title": "❌ Unknown member" - }, - "no_permission": { - "description": "You are not the creator of this ticket and you are not a staff member; you can't remove members from this ticket.", - "title": "❌ Insufficient permission" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ This isn't a ticket channel" - }, - "removed": { - "description": "%s has been removed from %s.", - "title": "✅ Member removed" - } - } - }, - "settings": { - "description": "Configure Discord Tickets", - "name": "settings" - }, - "stats": { - "description": "Display ticket statistics", - "fields": { - "messages": "Messages", - "response_time": { - "minutes": "%s minutes", - "title": "Avg. response time" - }, - "tickets": "Tickets" - }, - "name": "stats", - "response": { - "global": { - "description": "Statistics about tickets across all guilds where this Discord Tickets instance is used.", - "title": "📊 Global stats" - }, - "guild": { - "description": "Statistics about tickets within this guild. This data is cached for an hour.", - "title": "📊 This server's stats" - } - } - }, - "survey": { - "description": "View survey responses", - "name": "survey", - "response": { - "list": { - "title": "📃 Surveys" - } - } - }, - "tag": { - "description": "Use a tag response", - "name": "tag", - "response": { - "error": "❌ Error", - "list": { - "title": "📃 Tag list" - }, - "missing": "This tag requires the following arguments:\n%s", - "not_a_ticket": { - "description": "This tag can only be used within a ticket channel as it uses ticket references.", - "title": "❌ This isn't a ticket channel" - } - } - }, - "topic": { - "description": "Change the topic of the ticket", - "name": "topic", - "response": { - "changed": { - "description": "This ticket's topic has been changed.", - "title": "✅ Topic changed" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel you want to change the topic of.", - "title": "❌ This isn't a ticket channel" - } - } - } - }, - "message_will_be_deleted_in": "This message will be deleted in %d seconds", - "missing_permissions": { - "description": "You do not have the permissions required to use this command:\n%s", - "title": "❌" - }, - "ticket": { - "claimed": { - "description": "%s has claimed this ticket.", - "title": "✅ Ticket claimed" - }, - "closed": { - "description": "This ticket has been closed.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_by_member": { - "description": "This ticket has been closed by %s.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_by_member_with_reason": { - "description": "This ticket has been closed by %s: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_with_reason": { - "description": "This ticket has been closed: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "member_added": { - "description": "%s has been added by %s", - "title": "Member added" - }, - "member_removed": { - "description": "%s has been removed by %s", - "title": "Member removed" - }, - "opening_message": { - "fields": { - "topic": "Topic" - } - }, - "questions": "Please answer the following questions:\n\n%s", - "released": { - "description": "%s has released this ticket.", - "title": "✅ Ticket released" - }, - "survey": { - "complete": { - "description": "Thank you for your feedback.", - "title": "✅ Thank you" - }, - "start": { - "description": "Hey, %s. Before this channel is deleted, would you mind completing a quick %d-question survey? React with ✅ to start, or ignore this message.", - "title": "❔ Feedback" - } - } - } -} diff --git a/src/locales/es-ES.json b/src/locales/es-ES.json deleted file mode 100644 index 94420d4..0000000 --- a/src/locales/es-ES.json +++ /dev/null @@ -1,612 +0,0 @@ -{ - "blacklisted": "❌ Estás vetado del sistema", - "bot": { - "missing_permissions": { - "description": "Discord Tickets requiere los siguientes permisos:\n%s", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s creado por [eartharoid](%s)" - }, - "collector_expires_in": "Expira en %d segundos", - "command_execution_error": { - "description": "Un error inesperado ha ocurrido durante la ejecución del comando.\nPor favor, pregúntale a un administrador para revisar la consola o los registros y obtener más información.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Añadir a un usuario al ticket", - "name": "add", - "options": { - "member": { - "description": "El usuario que deseas agregar al ticket", - "name": "member" - }, - "ticket": { - "description": "El ticket al cual deseas agregar el usuario", - "name": "ticket" - } - }, - "response": { - "added": { - "description": "%s ha sido añadido a %s.", - "title": "✅ Miembro añadido" - }, - "no_member": { - "description": "Por favor, menciona al usuario que deseas añadir.", - "title": "❌ Usuario desconocido" - }, - "no_permission": { - "description": "No eres el creador de este ticket y no eres parte del staff, no puedes añadir usuarios a este ticket.", - "title": "❌ Permisos insuficientes" - }, - "not_a_ticket": { - "description": "Por favor utiliza este comando en el canal de tickets, o menciona el canal.", - "title": "❌ Este no es un canal de tickets" - } - } - }, - "blacklist": { - "description": "Ver o modificar la lista negra", - "name": "blacklist", - "options": { - "add": { - "description": "Añadir un miembro o rol a la lista negra", - "name": "add", - "options": { - "member_or_role": { - "description": "El miembro o rol a añadir a la lista negra", - "name": "member_or_role" - } - } - }, - "remove": { - "description": "Quitar un miembro o rol de la lista negra", - "name": "remove", - "options": { - "member_or_role": { - "description": "El miembro o rol a quitar de la lista negra", - "name": "member_or_role" - } - } - }, - "show": { - "description": "Muestra los miembros y roles que están en la lista negra", - "name": "show" - } - }, - "response": { - "empty_list": { - "description": "No hay miembros ni roles en la lista negra. Escribe `/blacklist add` para añadir a un usuario o rol a la misma.", - "title": "📃 Usuarios y roles en la lista negra" - }, - "illegal_action": { - "description": "%s es un miembro del personal, por lo tanto no puede ser añadido a la lista negra.", - "title": "❌ No puedes añadir a la lista negra a este usuario" - }, - "invalid": { - "description": "Este miembro o rol no puede ser quitado de la lista negra porque no está dentro de ella.", - "title": "❌ Error" - }, - "list": { - "fields": { - "members": "Miembros", - "roles": "Roles" - }, - "title": "📃 Usuarios y roles en la lista negra" - }, - "member_added": { - "description": "<@%s> fue añadido a la lista negra. Ya no podrá interactuar con el bot, a menos de que sea removido de la lista.", - "title": "✅ Usuario añadido a la lista negra" - }, - "member_removed": { - "description": "<@%s> fue eliminado de la lista negra. Ahora puede usar el bot.", - "title": "✅ Usuario eliminado de la lista negra" - }, - "role_added": { - "description": "<@&%s> fue añadido a la lista negra. Los usuarios que tengan este rol no podrán interactuar con el bot hasta ser removidos de la lista.", - "title": "✅ Rol añadido a la lista negra" - }, - "role_removed": { - "description": "<@&%s> fue eliminado de la lista negra. Los usuarios que tengan este rol podrán volver a hacer uso del bot.", - "title": "✅ Rol eliminado de la lista negra" - } - } - }, - "close": { - "description": "Cerrar un canal de ayuda", - "name": "close", - "options": { - "reason": { - "description": "El motivo por cerrar el/los ticket(s)", - "name": "reason" - }, - "ticket": { - "description": "El ticket a cerrar, sea el numero o la ID del canal", - "name": "ticket" - }, - "time": { - "description": "Cerrar todos los tickets que han estado inactivos durante un tiempo determinado", - "name": "time" - } - }, - "response": { - "canceled": { - "description": "Has cancelado la operación.", - "title": "🚫 Cancelado" - }, - "closed": { - "description": "El ticket #%s fue cerrado.", - "title": "✅ Ticket resuelto" - }, - "closed_multiple": { - "description": [ - "El ticket %d fue resuelto.", - "Se han cerrado %d tickets." - ], - "title": [ - "✅ Ticket resuelto", - "✅ Tickets cerrados" - ] - }, - "confirm": { - "buttons": { - "cancel": "Cancelar", - "confirm": "Cerrar" - }, - "description": "Por favor, confirm tu selección.", - "description_with_archive": "El ticket será archivado para referencia en el futuro.", - "title": "❔ ¿Estás seguro?" - }, - "confirm_multiple": { - "buttons": { - "cancel": "Cancelar", - "confirm": [ - "Cerrar %d ticket", - "Cerrar %d tickets" - ] - }, - "description": [ - "Reacciona con ✅ para cerrar el ticket %d.", - "Estás a punto de cerrar %d tickets." - ], - "title": "❔ ¿Estás seguro?" - }, - "confirmation_timeout": { - "description": "Has tardado demasiado en confirmar.", - "title": "❌ El tiempo de reacción ha expirado" - }, - "invalid_time": { - "description": "El período de tiempo ingresado no pudo ser analizado.", - "title": "❌ Entrada inválida" - }, - "no_permission": { - "description": "No eres un miembro del staff ni el creador del ticket..", - "title": "❌ Permisos insuficientes" - }, - "no_tickets": { - "description": "No hay tickets que hayan estado inactivos durante este período de tiempo.", - "title": "❌ No hay tickets para cerrar" - }, - "not_a_ticket": { - "description": "Por favor, utiliza este comando en un canal de tickets, o usa la bandera de tickets.\nEjecuta `%shelp close` para más información.", - "title": "❌ Este no es un canal de tickets" - }, - "unresolvable": { - "description": "`%s` no pudo ser resuelto como un ticket. Por favor, especifica el ID o número del ticket.", - "title": "❌ Error" - } - } - }, - "help": { - "description": "Listar comandos a los que tienes acceso", - "name": "help", - "options": {}, - "response": { - "list": { - "description": "Los comandos a los que tienes acceso están listados debajo. Para más información sobre un comando, ejecuta `{prefix}help [command]`. para crear un ticket, ejecuta `{prefix}new [topic]`.", - "fields": { - "commands": "Comandos" - }, - "title": "❔ Ayuda" - } - } - }, - "new": { - "description": "Crear un nuevo ticket", - "name": "new", - "options": { - "topic": { - "description": "Categoria del ticket", - "name": "topic" - } - }, - "request_topic": { - "description": "Por favor, indica de qué se trata este ticket en pocas palabras.", - "title": "Tema del ticket" - }, - "response": { - "created": { - "description": "Tu ticket fue creado: %s.", - "title": "✅ Ticket creado" - }, - "error": { - "title": "❌ Error" - }, - "has_a_ticket": { - "description": "Por favor, utiliza tu ticket existente (<#%s>) o ciérralo antes de crear otro.", - "title": "❌ Ya tienes un ticket abierto" - }, - "max_tickets": { - "description": "Por favor, utiliza el comando `%sclose` ´para cerrar tickets inatendidos.\n\n%s", - "title": "❌ Ya tienes %d tickets abiertos" - }, - "no_categories": { - "description": "Un administrador del servidor debe crear al menos una categoría de tickets antes de que un nuevo ticket pueda ser abierto.", - "title": "❌ No se puede crear el ticket" - }, - "select_category": { - "description": "Selecciona la categoría más relevante para el tema de tu ticket:\n\n%s", - "title": "🔤 Por favor, selecciona la categoría de tu ticket" - }, - "select_category_timeout": { - "description": "Has tardado demasiado tiempo en seleccionar la categoría de tickets.", - "title": "❌ El tiempo de la reacción ha expirado" - } - } - }, - "panel": { - "description": "Crear un nuevo panel de tickets", - "name": "panel", - "options": { - "categories": { - "description": "Una lista separada por comas de IDs de las categorías", - "name": "categories" - }, - "description": { - "description": "La descripción para el mensaje del panel", - "name": "description" - }, - "image": { - "description": "Una URL de imagen para el mensaje del panel", - "name": "image" - }, - "just_type": { - "description": "Crear un \"tipo\" de panel?", - "name": "just_type" - }, - "title": { - "description": "Titulo del mensaje del panel", - "name": "title" - }, - "thumbnail": { - "description": "Url de una imagen en miniatura para el mensaje del panel", - "name": "thumbnail" - } - }, - "response": { - "invalid_category": { - "description": "Una o más de las ID de categoría ingresadas no son válidas.", - "title": "❌ Categoría inválida" - }, - "too_many_categories": { - "description": "El \"tipo\" solo se puede utilizar para un tipo de categoria.", - "title": "❌ Demasiadas Categorias" - } - } - }, - "remove": { - "description": "Eliminar a un miembro de un ticket", - "name": "remove", - "options": { - "member": { - "description": "Miembro a eliminar del ticket", - "name": "member" - }, - "ticket": { - "description": "Miembro a eliminar del ticket", - "name": "ticket" - } - }, - "response": { - "no_member": { - "description": "Por favor, menciona al miembro que deseas eliminar.", - "title": "❌ Miembro desconocido" - }, - "no_permission": { - "description": "Tú no eres el creador de este ticket ni un miembro del personal, por lo que no puedes eliminar miembros de este ticket.", - "title": "❌ Permisos insuficientes" - }, - "not_a_ticket": { - "description": "Por favor utiliza este comando en el canal de tickets, o menciona el canal.", - "title": "❌ Este no es un canal de tickets" - }, - "removed": { - "description": "%s fue eliminado de %s.", - "title": "✅ Miembro eliminado" - } - } - }, - "settings": { - "description": "Configurar Discord Tickets", - "name": "settings", - "options": { - "categories": { - "description": "Gestiona tus categoría", - "name": "categories", - "options": { - "create": { - "description": "Crear una nueva categoria", - "name": "create", - "options": { - "name": { - "description": "Nombre de la categoria", - "name": "name" - }, - "roles": { - "description": "Una lista separada por comas de los ID de funciones del staff para esta categoría", - "name": "roles" - } - } - }, - "delete": { - "description": "Borrar Categoria", - "name": "delete", - "options": { - "id": { - "description": "ID de la categoria a borrar", - "name": "id" - } - } - }, - "edit": { - "description": "Realizar cambios en la configuración de una categoría", - "name": "edit", - "options": { - "claiming": { - "description": "Habilitar reclamar un ticket?", - "name": "claiming" - }, - "id": { - "description": "ID de categoria a editar", - "name": "id" - }, - "image": { - "description": "URL de una imagen", - "name": "image" - }, - "max_per_member": { - "description": "El número máximo de tickets que un miembro puede tener en esta categoría", - "name": "max_per_member" - }, - "name": { - "description": "Nombre de la categoria", - "name": "name" - }, - "name_format": { - "description": "Formato del ticket", - "name": "name_format" - }, - "opening_message": { - "description": "Texto a enviar tras abrir el ticket", - "name": "opening_message" - }, - "opening_questions": { - "description": "Preguntas que realizar tras abrir un ticket.", - "name": "opening_questions" - }, - "ping": { - "description": "Una lista separada por comas de ID de roles para hacer ping", - "name": "ping" - }, - "require_topic": { - "description": "¿Requerir al usuario dar el tema del ticket?", - "name": "require_topic" - }, - "roles": { - "description": "Una lista separada por comas de ID de funciones del personal", - "name": "roles" - }, - "survey": { - "description": "La encuesta a utilizar", - "name": "survey" - } - } - }, - "list": { - "description": "Lista de categorias", - "name": "list" - } - } - }, - "set": { - "description": "Setear Opciones", - "name": "set", - "options": { - "close_button": { - "description": "Habilitar el cierre del ticket con un botón?", - "name": "close_button" - }, - "colour": { - "description": "Color estandar", - "name": "colour" - }, - "error_colour": { - "description": "Color de error", - "name": "error_colour" - }, - "footer": { - "description": "El texto del pie de página incrustado", - "name": "footer" - }, - "locale": { - "description": "Idioma", - "name": "locale" - }, - "log_messages": { - "description": "¿Almacenar mensajes de tickets?", - "name": "log_messages" - }, - "success_colour": { - "description": "El color de acierto", - "name": "success_colour" - } - } - } - }, - "response": { - "category_created": "✅ La `%s` categoria del sistema de ticket fue creada", - "category_deleted": "✅ La `%s` categoria del sistema de ticket fue borrada", - "category_does_not_exist": "❌ No existe la categoria con esa id", - "category_updated": "✅ La `%s` categoria del sistema de ticket fue actualizada", - "category_list": "Categoria de tickets", - "settings_updated": "✅ Los cambios fueron guardados" - } - }, - "stats": { - "description": "Mostrar estadísticas del ticket", - "fields": { - "messages": "Mensajes", - "response_time": { - "minutes": "%s minutos", - "title": "Tiempo de respuesta promedio" - }, - "tickets": "Tickets" - }, - "name": "stats", - "options": {}, - "response": { - "global": { - "description": "Estadísticas sobre los tickets en todos los servidores donde se utiliza esta instancia de Discord Tickets.", - "title": "📊 Estadísticas globales" - }, - "guild": { - "description": "Estadísticas sobre los tickets de este servidor. Estos datos son almacenados en la caché durante una hora.", - "title": "📊 Estadísticas de este servidor" - } - } - }, - "survey": { - "description": "Ver respuestas de la encuesta", - "name": "survey", - "options": { - "survey": { - "description": "El nombre de la encuesta para ver las respuestas de", - "name": "survey" - } - }, - "response": { - "list": { - "title": "📃 Encuestas" - } - } - }, - "tag": { - "description": "Usar una respuesta de etiqueta", - "name": "tag", - "options": { - "tag": { - "description": "El nombre de la etiqueta a usar", - "name": "tag" - } - }, - "response": { - "error": "❌ Error", - "list": { - "title": "📃 Lista de etiquetas" - }, - "missing": "Esta etiqueta requiere los siguientes argumentos:\n%s", - "not_a_ticket": { - "description": "Esta etiqueta solo puede utilizarse dentro de un canal de tickets, ya que utiliza referencias de éste.", - "title": "❌ Este no es un canal de tickets" - } - } - }, - "topic": { - "description": "Cambiar el tema del ticket", - "name": "topic", - "options": { - "new_topic": { - "description": "Nuevo tema de los tickets", - "name": "new_topic" - } - }, - "response": { - "changed": { - "description": "El tema de este ticket ha sido cambiado.", - "title": "✅ Tema cambiado" - }, - "not_a_ticket": { - "description": "Por favor, utiliza este comando en el canal de ticket en el que deseas cambiar el tema.", - "title": "❌ Este no es un canal de tickets" - } - } - } - }, - "message_will_be_deleted_in": "Este mensaje será eliminado en %d segundos", - "missing_permissions": { - "description": "No tienes los permisos requeridos para ejecutar este comando:\n%s", - "title": "❌" - }, - "panel": { - "create_ticket": "Crear un ticket" - }, - "ticket": { - "claim": "Reclamar", - "claimed": { - "description": "%s ha reclamado el ticket.", - "title": "✅ Ticket reclamado" - }, - "close": "Cerrar", - "closed": { - "description": "Este ticket fue cerrado.\nEl canal será eliminado en 5 segundos.", - "title": "✅ Ticket cerrado" - }, - "closed_by_member": { - "description": "Este ticket ha sido cerrado por %s.\nEl canal será eliminado en 5 segundos.", - "title": "✅ Ticket cerrado" - }, - "closed_by_member_with_reason": { - "description": "Este ticket ha sido cerrado por %s: `%s`\nEl canal será eliminado en 5 segundos.", - "title": "✅ Ticket cerrado" - }, - "closed_with_reason": { - "description": "Este ticket ha sido cerrado: `%s`\nEl canal será eliminado en 5 segundos.", - "title": "✅ Ticket cerrado" - }, - "member_added": { - "description": "%s fue añadido por %s", - "title": "Miembro añadido" - }, - "member_removed": { - "description": "%s fue eliminado por %s", - "title": "Miembro removido" - }, - "opening_message": { - "content": "%s\n%s ha creado un nuevo ticket", - "fields": { - "topic": "Tema" - } - }, - "questions": "Por favor, responde las siguientes preguntas:\n\n%s", - "released": { - "description": "%s ha lanzado este ticket.", - "title": "✅ Ticket lanzado" - }, - "survey": { - "complete": { - "description": "Gracias por tus comentarios.", - "title": "✅ Gracias" - }, - "start": { - "buttons": { - "start": "Empezar encuesta", - "ignore": "No" - }, - "description": "Hey, %s. Antes de que se elimine este canal, ¿te importaría completar una encuesta rápida de %d preguntas?", - "title": "❔ Feedback" - } - }, - "unclaim": "Release" - }, - "updated_permissions": "✅ Se han actualizado los permisos" -} \ No newline at end of file diff --git a/src/locales/fr-FR.json b/src/locales/fr-FR.json deleted file mode 100644 index e7ec93d..0000000 --- a/src/locales/fr-FR.json +++ /dev/null @@ -1,610 +0,0 @@ -{ - "blacklisted": "❌ Vous êtes sur liste noire", - "bot": { - "missing_permissions": { - "description": "Discord Tickets nécessite les autorisations suivantes :\n%s", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s par [eartharoid](%s)" - }, - "collector_expires_in": "Expire dans %d secondes", - "command_execution_error": { - "description": "Une erreur inattendue s'est produite lors de l'exécution de commande.\nVeuillez demander à un administrateur de vérifier la console / les journaux pour plus de détails.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Ajouter un membre à un billet", - "name": "ajouter", - "options": { - "member": { - "description": "Le membre à ajouter au billet", - "name": "member" - }, - "ticket": { - "description": "Le billet auquel ajouter le membre", - "name": "ticket" - } - }, - "response": { - "added": { - "description": "%s a été ajouté à %s.", - "title": "✅ Membre ajouté" - }, - "no_member": { - "description": "Veuillez mentionner le membre que vous souhaitez ajouter.", - "title": "❌ Membre inconnu" - }, - "no_permission": { - "description": "Vous n'êtes pas le créateur de ce billet et vous n'êtes pas un membre du personnel; vous ne pouvez pas ajouter de membre à ce billet .", - "title": "❌ Permissions insuffisantes" - }, - "not_a_ticket": { - "description": "Veuillez utiliser cette commande dans le canal des billets, ou mentionner le salon.", - "title": "❌ Ce n'est pas un salon de billet" - } - } - }, - "blacklist": { - "description": "Voir ou modifier la liste noire", - "name": "blacklist", - "options": { - "add": { - "description": "Ajouter un membre ou un rôle à la liste noire", - "name": "ajouter", - "options": { - "member_or_role": { - "description": "Le membre ou rôle à ajouter à la liste noire", - "name": "membre_ou_rôle" - } - } - }, - "remove": { - "description": "Retirer un membre ou un rôle de la liste noire", - "name": "retirer", - "options": { - "member_or_role": { - "description": "Le membre ou le rôle à retirer de la liste noire", - "name": "membre_ou_rôle" - } - } - }, - "show": { - "description": "Voir les membres et rôles dans la liste noire", - "name": "voir" - } - }, - "response": { - "empty_list": { - "description": "Il n'y a aucun membre ou rôle dans la liste noir. Tapez `/blacklist add` pour ajouter un membre ou un rôle à la liste noire.", - "title": "📃 Membres et rôles sur la liste noire" - }, - "illegal_action": { - "description": "%s est un membre du personnel et ne peut pas être mis sur la liste noire.", - "title": "❌ Vous ne pouvez pas ajouter ce membre à la liste noire" - }, - "invalid": { - "description": "Ce membre ou rôle ne peut pas être retiré de la liste noire car il n'y est pas présent.", - "title": "❌ Erreur" - }, - "list": { - "fields": { - "members": "Membres", - "roles": "Rôles" - }, - "title": "📃 Membres et rôles sur la liste noire" - }, - "member_added": { - "description": "<@%s> a été ajouté à la liste noire. Ils ne seront plus en mesure d'interagir avec le bot.", - "title": "✅ Membre ajouté à la liste noire" - }, - "member_removed": { - "description": "<@%s> a été retiré de la liste noire. Ils peuvent désormais utiliser à nouveau le bot.", - "title": "✅ Membre retiré de la liste noire" - }, - "role_added": { - "description": "<@&%s> a été ajouté à la liste noire. Les membres avec ce rôle ne seront plus en mesure d'interagir avec le bot.", - "title": "✅ Rôle ajouté à la liste noire" - }, - "role_removed": { - "description": "<@&%s> a été retiré de la liste noire. Les membres avec ce rôle peuvent maintenant utiliser le bot à nouveau.", - "title": "✅ membre ajouté à la liste noire" - } - } - }, - "close": { - "description": "Fermer un canal de billets", - "name": "fermer", - "options": { - "reason": { - "description": "La raison de fermer les tickets", - "name": "reason" - }, - "ticket": { - "description": "Le ticket à fermer, soit le numéro ou l'ID du salon", - "name": "ticket" - }, - "time": { - "description": "Fermer tous les tickets qui ont été inactifs dans un temps impartis", - "name": "time" - } - }, - "response": { - "canceled": { - "description": "Vous avez annulé l'opération.", - "title": "🚫 Annulé" - }, - "closed": { - "description": "Le billet #%s a été fermé.", - "title": "✅ billet fermé" - }, - "closed_multiple": { - "description": [ - "le billet #%s a été fermé.", - "le billet #%s a été fermé." - ], - "title": [ - "✅ billet fermé", - "✅ billet fermé" - ] - }, - "confirm": { - "buttons": { - "cancel": "Annuler", - "confirm": "Fermer" - }, - "description": "Veuillez confirmer votre décision.", - "description_with_archive": "Le billet sera archivé pour référence future.", - "title": "❔ En êtes-vous sûr ?" - }, - "confirm_multiple": { - "buttons": { - "cancel": "Annuler", - "confirm": [ - "Fermer %d ticket", - "Fermer %d tickets" - ] - }, - "description": [ - "Vous êtes sur le point de fermer le billet %d.", - "Vous allez supprimer %d tickets, réagissez avec ✅ pour confirmer." - ], - "title": "❔ En êtes-vous sûr ?" - }, - "confirmation_timeout": { - "description": "Tu as mis trop de temps à confirmer.", - "title": "❌ Le temps de réaction a expiré" - }, - "invalid_time": { - "description": "La période prévue n'a pas pu être analysée.", - "title": "❌ entrée invalide" - }, - "no_permission": { - "description": "Vous n'êtes pas un membre du staff ou le créateur du ticket.", - "title": "❌ Permissions insuffisantes" - }, - "no_tickets": { - "description": "Il n'y a pas de billets inactifs pour cette période.", - "title": "❌ pas de billets à fermer" - }, - "not_a_ticket": { - "description": "Veuillez utiliser cette commande dans un salon de ticket ou utilisez le drapeau ticket.\nTapez «/help close» pour plus d'informations.", - "title": "❌ Ce n'est pas une chaîne de billets" - }, - "unresolvable": { - "description": "'%s' ne pouvait pas être résolu à un billet. Veuillez fournir l'iD/mention ou le numéro du billet.", - "title": "❌ erreur" - } - } - }, - "help": { - "description": "Listez les commandes dont vous avez accès", - "name": "help", - "response": { - "list": { - "description": "Les commandes dont vous avez accès sont listées ci-dessous. Pour créer un ticket, tapez **`/new`**", - "fields": { - "commands": "Commandes" - }, - "title": "❔ Aide" - } - } - }, - "new": { - "description": "Créer un nouveau billet", - "name": "new", - "options": { - "topic": { - "description": "Le sujet du ticket", - "name": "sujet" - } - }, - "request_topic": { - "description": "Veuillez indiquer brièvement l'objet de ce billet en quelques mots.", - "title": "⚠️ Sujet du ticket" - }, - "response": { - "created": { - "description": "Votre billet a été créé : %s.", - "title": "✅ Billet créé" - }, - "error": { - "title": "❌ erreur" - }, - "has_a_ticket": { - "description": "S'il vous plaît utiliser votre billet <# existant (%s>) ou le fermer avant de créer un autre.", - "title": "❌ Vous avez déjà un billet ouvert" - }, - "max_tickets": { - "description": "Utilisez **`/close`** pour fermer tous les tickets inutiles.\n\n%s", - "title": "❌ Vous avez déjà un billet ouvert" - }, - "no_categories": { - "description": "Un administrateur serveur doit créer au moins une catégorie de billets avant qu'un nouveau billet puisse être ouvert.", - "title": "❌ Impossible de créer un ticket" - }, - "select_category": { - "description": "Sélectionnez la catégorie la plus pertinente pour le sujet de votre billet.", - "title": "🔤 Veuillez sélectionner la catégorie de billets" - }, - "select_category_timeout": { - "description": "Vous avez mis trop de temps à sélectionner la catégorie de billets.", - "title": "❌Le temps de réaction a expiré" - } - } - }, - "panel": { - "description": "Créer un nouveau panneau de billet", - "name": "panel", - "options": { - "categories": { - "description": "La liste d'ID de catégorie séparé par des virgules", - "name": "categories" - }, - "description": { - "description": "La description du message du panneau d'affichage", - "name": "description" - }, - "image": { - "description": "L'URL d'une image pour le panneau d'affichage", - "name": "image" - }, - "just_type": { - "description": "Créer un panneau d'affichage \"juste écrire\"?", - "name": "just_type" - }, - "thumbnail": { - "description": "Une URL d'image de bannière pour le panneau d'affichage", - "name": "thumbnail" - }, - "title": { - "description": "Le titre du panneau d'affichage", - "name": "title" - } - }, - "response": { - "invalid_category": { - "description": "Un ou plusieurs des ID de catégorie spécifiés sont invalides.", - "title": "❌ Catégorie invalide" - }, - "too_many_categories": { - "description": "Le panneau d'affichage \"juste écrire\" ne peut être utilisé que dans une seule catégorie.", - "title": "❌ Trop de catégories" - } - } - }, - "remove": { - "description": "Retirer un membre d'un billet", - "name": "remove", - "options": { - "member": { - "description": "Le membre à retirer du ticket", - "name": "member" - }, - "ticket": { - "description": "Le ticket où retirer le membre", - "name": "ticket" - } - }, - "response": { - "no_member": { - "description": "Veuillez mentionner le membre que vous souhaitez ajouter.", - "title": "❌ membre inconnu" - }, - "no_permission": { - "description": "Vous n'êtes pas le créateur de ce billet et vous n'êtes pas un membre du personnel; vous ne pouvez pas ajouter de membres à ce billet.", - "title": "❌ autorisation insuffisante" - }, - "not_a_ticket": { - "description": "S'il vous plaît utiliser cette commande dans le canal de billet, ou mentionner le canal.", - "title": "❌ Ce n'est pas une chaîne de billets" - }, - "removed": { - "description": "%s a été ajouté à %s.", - "title": "✅ Membre retiré" - } - } - }, - "settings": { - "description": "Configurer Discord Tickets", - "name": "paramètres", - "options": { - "categories": { - "description": "Gérer les catégories des tickets", - "name": "catégories", - "options": { - "create": { - "description": "Créez une nouvelle catégorie", - "name": "créer", - "options": { - "name": { - "description": "Le nom de la catégorie", - "name": "nom" - }, - "roles": { - "description": "Une liste d'ID de rôle du personnel séparés par des virgules pour cette catégorie", - "name": "rôles" - } - } - }, - "delete": { - "description": "Supprimez une catégorie", - "name": "supprimer", - "options": { - "id": { - "description": "L'ID de la catégorie a supprimer", - "name": "id" - } - } - }, - "edit": { - "description": "Faire des changement sur la configuration d'une catégorie", - "name": "éditer", - "options": { - "claiming": { - "description": "Activer la réception d'un ticket ?", - "name": "claiming" - }, - "id": { - "description": "L'ID de la catégorie à éditer", - "name": "id" - }, - "image": { - "description": "L'URL d'une image", - "name": "image" - }, - "max_per_member": { - "description": "Le maximum de tickets qu'un membre peut avoir dans cette catégorie", - "name": "max_per_member" - }, - "name": { - "description": "Le nom de la catégorie", - "name": "name" - }, - "name_format": { - "description": "Le format du nom des tickets", - "name": "name_format" - }, - "opening_message": { - "description": "Le texte à envoyer quand un ticket est ouvert", - "name": "opening_message" - }, - "opening_questions": { - "description": "Questions à demander quand un ticket est ouvert.", - "name": "opening_questions" - }, - "ping": { - "description": "Une liste d'ID de rôle à ping séparés par des virgules", - "name": "ping" - }, - "require_topic": { - "description": "Obliger le membre à donner le sujet du ticket ?", - "name": "require_topic" - }, - "roles": { - "description": "Une liste d'ID de rôle du personnel séparés par des virgules", - "name": "roles" - }, - "survey": { - "description": "L'enquête à utiliser", - "name": "survey" - } - } - }, - "list": { - "description": "Lister les catégories", - "name": "lister" - } - } - }, - "set": { - "description": "Définir les options", - "name": "définir", - "options": { - "close_button": { - "description": "Activer la fermeture avec un bouton ?", - "name": "close_button" - }, - "colour": { - "description": "La couleur de base", - "name": "colour" - }, - "error_colour": { - "description": "La couleur des eurreurs", - "name": "error_colour" - }, - "footer": { - "description": "Le texte de fin de l'embed", - "name": "footer" - }, - "locale": { - "description": "La langue (language)", - "name": "locale" - }, - "log_messages": { - "description": "Stocker les messages des tickets ?", - "name": "log_messages" - }, - "success_colour": { - "description": "La couleur pour la réussite", - "name": "success_colour" - } - } - } - }, - "response": { - "category_created": "✅ La catégorie de tickets `%s` à été créée", - "category_deleted": "✅ La catégorie de tickets `%s` à bien été supprimée", - "category_does_not_exist": "❌ La catégorie avec cet ID n'existe pas", - "category_list": "La catégories de tickets", - "category_updated": "✅ La catégorie de ticket « %s» a été mise à jour", - "settings_updated": "✅ Les paramètres ont été mis à jour" - } - }, - "stats": { - "description": "Afficher les statistiques des billets", - "fields": { - "messages": "Messages", - "response_time": { - "minutes": "%s minutes", - "title": "Temps de réponse moyen" - }, - "tickets": "Billet" - }, - "name": "statistiques", - "response": { - "global": { - "description": "Statistiques sur les billets dans toutes les guildes où cette instance Discord Tickets est utilisée.", - "title": "📊 statistiques mondiales" - }, - "guild": { - "description": "Statistiques sur les billets au sein de cette guilde. Ces données sont mises en cache pendant une heure.", - "title": "📊 statistiques de ce serveur" - } - } - }, - "survey": { - "description": "Voir les enquêtes", - "name": "enquêtes", - "options": { - "survey": { - "description": "Voir les réponses au sondage", - "name": "enquêtes" - } - }, - "response": { - "list": { - "title": "📃 enquêtes" - } - } - }, - "tag": { - "description": "Utiliser une réponse d'étiquette", - "name": "étiquettes", - "options": { - "tag": { - "description": "Le nom du tag à utiliser", - "name": "tag" - } - }, - "response": { - "error": "❌ erreur", - "list": { - "title": "📃 Liste d'étiquettes" - }, - "missing": "Cette balise nécessite les arguments suivants :\n%s", - "not_a_ticket": { - "description": "Cette balise ne peut être utilisée que dans un canal de billets car elle utilise des références de billets.", - "title": "❌ Ce n'est pas un canal de billet" - } - } - }, - "topic": { - "description": "Le sujet du billet", - "name": "sujet", - "options": { - "new_topic": { - "description": "Le nouveau sujet du ticket", - "name": "new_topic" - } - }, - "response": { - "changed": { - "description": "Le sujet de ce billet a été changé.", - "title": "✅ sujet changé" - }, - "not_a_ticket": { - "description": "S'il vous plaît utiliser cette commande dans le canal de billet, ou mentionner le canal.", - "title": "❌ Ce n'est pas un canal de billet" - } - } - } - }, - "message_will_be_deleted_in": "Ce message sera supprimé dans %d secondes", - "missing_permissions": { - "description": "Vous n'avez pas les autorisations requises pour utiliser cette commande :\n%s", - "title": "❌" - }, - "panel": { - "create_ticket": "Crée un Ticket" - }, - "ticket": { - "claim": "Réclamée", - "claimed": { - "description": "%s a réclamé ce billet.", - "title": "✅ billet fermé" - }, - "close": "Fermé", - "closed": { - "description": "Ce billet a été fermé.\nLe canal sera supprimé en 5 secondes.", - "title": "✅ billet fermé" - }, - "closed_by_member": { - "description": "Ce billet a été fermé.\nLe canal sera supprimé en 5 secondes.", - "title": "✅ billet fermé" - }, - "closed_by_member_with_reason": { - "description": "Ce billet a été fermé.\nLe canal sera supprimé en 5 secondes.", - "title": "✅ billet fermé" - }, - "closed_with_reason": { - "description": "Ce billet a été fermé.\nLe canal sera supprimé en 5 secondes.", - "title": "✅ billet fermé" - }, - "member_added": { - "description": "%s a été ajouté à %s", - "title": "Membre ajouté" - }, - "member_removed": { - "description": "%s a été retirée %s", - "title": "Membre retiré" - }, - "opening_message": { - "content": "%s\n%s as crée un nouveau Ticket", - "fields": { - "topic": "Sujet" - } - }, - "questions": "S'il vous plaît répondre aux questions suivantes:\n\n%s", - "released": { - "description": "%s a abandonné ce billet.", - "title": "✅ billet abandonné" - }, - "survey": { - "complete": { - "description": "Merci pour vos commentaires.", - "title": "✅ Merci" - }, - "start": { - "buttons": { - "ignore": "Non", - "start": "Commencer l'enquête" - }, - "description": "Hé, %s. Avant que ce canal soit supprimé, pourriez-vous répondre à %d question(s) ?", - "title": "❔ commentaires" - } - }, - "unclaim": "Relachée" - }, - "updated_permissions": "✅ Mise à jour des permissions des commandes slash" -} diff --git a/src/locales/he-IL.json b/src/locales/he-IL.json deleted file mode 100644 index 5d973e7..0000000 --- a/src/locales/he-IL.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "blacklisted": "❌ אתה חסום", - "bot": { - "missing_permissions": { - "description": "דיסקורד-טיקטס דורש ממך להיות בעל ההרשאות הבאות:\n%s ", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s ע\"י [eartharoid](%s)" - }, - "collector_expires_in": "פג תוקף בעוד %d שניות", - "command_execution_error": { - "description": "שגיאה לא צפויה התחוללה!\nאנא צרו קשר עם האדמין על מנת שיבדוק את הקונסול לעוד פרטים.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "הוסף משתמש לטיקט", - "name": "הוסף", - "options": { - "member": { - "description": "המשתמש הוסף לטיקט", - "name": "משתמש" - }, - "ticket": { - "description": "הטיקט שתרצו לצרף אליו משתמש", - "name": "טיקט" - } - }, - "response": { - "added": { - "description": "%s צורף ל %s.", - "title": "✅ משתמש צורף" - }, - "no_member": { - "description": "אנא תייג את המשתמש שתרצה להוסיף.", - "title": "❌ משתמש לא ידוע" - }, - "no_permission": { - "description": "אינך יוצר הטיקט ולא בעל הרשאות אדמין, לכן אינך מורשה להוסיף משתמשים נוספים לטיקט.", - "title": "❌הרשאות לא מספיקות" - }, - "not_a_ticket": { - "description": "אנא השתמש בפקודה זו בערוץ הטיקטים, או ציין את הערוץ.", - "title": "❌ זה איננו ערוץ טיקטים" - } - } - }, - "blacklist": { - "description": "צפה או ערוץ את רשימת החסומים", - "name": "רשימת החסומים", - "options": { - "add": { - "description": "הוסף משתמש או רול לרשימת החסומים", - "name": "הוסף", - "options": { - "member_or_role": { - "description": "המשתמש או הרול להוסיף לרשימת החסומים", - "name": "משתמש_או_רול" - } - } - }, - "remove": { - "description": "הסר משתמש או רול מרשימת החסומים", - "name": "הסר", - "options": { - "member_or_role": { - "description": "המשתמש או הרול להסיר מרשימת החסומים", - "name": "משתמש_או_רול" - } - } - }, - "show": { - "description": "הצג את המשתמשים והרולים ברשימת החסומים", - "name": "הצג" - } - }, - "response": { - "empty_list": { - "description": "אין משתמשים או רולים ברשימת החסומים, כתוב `/blacklist add` על מנת להוסיף משתמשים או רולים לרשימה.", - "title": "📃רשימת חסומים של משתמשים ורולים" - }, - "illegal_action": { - "description": "%s הינו חבר צוות ואינו יכול להיחסם.", - "title": "❌לא ניתן לחסום משתמש זה" - }, - "invalid": { - "description": "לא ניתן להסיר את המשתמש או הרול הללו מן הרשימה מאחר ואינם חסומים.", - "title": "❌שגיאה" - }, - "list": { - "fields": { - "members": "משתמשים", - "roles": "רולים" - }, - "title": "📃משתמשים ורולים ברשימת החסומים" - }, - "member_added": { - "description": "<@%s> נחסם ולא יוכל לתקשר עם הבוט.", - "title": "✅משתמש הוסף לרשימת החסומים" - } - } - }, - "settings": { - "options": { - "categories": { - "options": { - "edit": { - "name": "ערוך", - "options": { - "id": { - "name": "איידי" - }, - "image": { - "name": "תמונה" - }, - "max_per_member": { - "name": "מקס_עבור_משתמש" - }, - "name": { - "name": "שם" - }, - "name_format": { - "name": "פורמט_שם" - }, - "opening_message": { - "name": "הודעת_פתיחה" - }, - "opening_questions": { - "name": "שאלות_פתיחה" - }, - "ping": { - "name": "פינג" - } - } - } - } - } - } - } - } -} diff --git a/src/locales/hi-IN.json b/src/locales/hi-IN.json deleted file mode 100644 index b3353a5..0000000 --- a/src/locales/hi-IN.json +++ /dev/null @@ -1,324 +0,0 @@ -{ - "bot": { - "missing_permissions": { - "description": "कलह टिकट निम्नलिखित अनुमतियों की आवश्यकता है:\n%s", - "title": "⚠️" - }, - "version": "[Discord टिकट] (%s) v%s [eartharoid](%s) द्वारा" - }, - "collector_expires_in": "%d सेकंड में समाप्त हो रहा है", - "command_execution_error": { - "description": "An unexpected error occurred during command execution.\nPlease ask an administrator to check the console output / logs for details.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Add a member to a ticket", - "name": "add", - "response": { - "added": { - "description": "%s has been added to %s.", - "title": "✅ Member added" - }, - "no_member": { - "description": "Please mention the member you want to add.", - "title": "❌ Unknown member" - }, - "no_permission": { - "description": "You are not the creator of this ticket and you are not a staff member; you can't add members to this ticket.", - "title": "❌ Insufficient permission" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ This isn't a ticket channel" - } - } - }, - "blacklist": { - "description": "Blacklist/unblacklist a member from interacting with the bot", - "name": "blacklist", - "response": { - "empty_list": { - "description": "There are no members or roles blacklisted. Type `%sblacklist ` to add a member or role to the blacklist.", - "title": "📃 Blacklisted members and roles" - }, - "illegal_action": { - "description": "%s is a staff member and cannot be blacklisted.", - "title": "❌ You can't blacklist this member" - }, - "list": { - "title": "📃 Blacklisted members and roles" - }, - "member_added": { - "description": "<@%s> has been added to the blacklist. They will no longer be able to interact with the bot.", - "title": "✅ Added member to blacklist" - }, - "member_removed": { - "description": "<@%s> has been removed from the blacklist. They can now use the bot again.", - "title": "✅ Removed member from blacklist" - }, - "role_added": { - "description": "<@&%s> has been added to the blacklist. Members with this role will no longer be able to interact with the bot.", - "title": "✅ Added role to blacklist" - }, - "role_removed": { - "description": "<@&%s> has been removed from the blacklist. Members with this role can now use the bot again.", - "title": "✅ Removed role from blacklist" - } - } - }, - "close": { - "description": "Close a ticket channel", - "name": "close", - "response": { - "closed": { - "description": "Ticket #%s has been closed.", - "title": "✅ Ticket closed" - }, - "closed_multiple": { - "description": [ - "%d ticket has been closed.", - "%d tickets have been closed." - ], - "title": [ - "✅ Ticket closed", - "✅ Tickets closed" - ] - }, - "confirm": { - "description": "React with ✅ to close this ticket.", - "description_with_archive": "You will be able to view an archived version of it after.\nReact with ✅ to close this ticket.", - "title": "❔ Are you sure?" - }, - "confirm_multiple": { - "description": [ - "React with ✅ to close %d ticket.", - "React with ✅ to close %d tickets." - ], - "title": "❔ Are you sure?" - }, - "confirmation_timeout": { - "description": "You took too long to confirm.", - "title": "❌ Reaction time expired" - }, - "invalid_time": { - "description": "The time period provided could not be parsed.", - "title": "❌ Invalid input" - }, - "no_tickets": { - "description": "There are no tickets which have been inactive for this time period.", - "title": "❌ No tickets to close" - }, - "not_a_ticket": { - "description": "Please use this command in a ticket channel or use the ticket flag.\nType `%shelp close` for more information.", - "title": "❌ This isn't a ticket channel" - }, - "unresolvable": { - "description": "`%s` could not be resolved to a ticket. Please provide the ticket ID/mention or number.", - "title": "❌ Error" - } - } - }, - "help": { - "description": "List commands you have access to, or find out more about a command", - "name": "help", - "response": { - "list": { - "description": "The commands you have access to are listed below. For more information about a command, type `{prefix}help [command]`. To create a ticket, type `{prefix}new [topic]`.", - "fields": { - "commands": "Commands" - }, - "title": "❔ Help" - } - } - }, - "new": { - "description": "Create a new ticket", - "name": "new", - "request_topic": { - "description": "Please briefly state what this ticket is about in a few words.", - "title": "Ticket topic" - }, - "response": { - "created": { - "description": "Your ticket has been created: %s.", - "title": "✅ Ticket created" - }, - "error": { - "title": "❌ Error" - }, - "has_a_ticket": { - "description": "Please use your existing ticket (<#%s>) or close it before creating another.", - "title": "❌ You already have an open ticket" - }, - "max_tickets": { - "description": "Please use `%sclose` to close any unneeded tickets.\n\n%s", - "title": "❌ You already have %d open tickets" - }, - "no_categories": { - "description": "A server administrator must create at least one ticket category before a new ticket can be opened.", - "title": "❌ Can't create ticket" - }, - "select_category": { - "description": "Select the category most relevant to your ticket's topic:\n\n%s", - "title": "🔤 Please select the ticket category" - }, - "select_category_timeout": { - "description": "You took too long to select the ticket category.", - "title": "❌ Reaction time expired" - } - } - }, - "panel": { - "description": "Create a new ticket panel", - "name": "panel", - "response": { - "invalid_category": { - "description": "One or more of the specified category IDs is invalid.", - "title": "❌ Invalid category" - } - } - }, - "remove": { - "description": "Remove a member from a ticket", - "name": "remove", - "response": { - "no_member": { - "description": "Please mention the member you want to remove.", - "title": "❌ Unknown member" - }, - "no_permission": { - "description": "You are not the creator of this ticket and you are not a staff member; you can't remove members from this ticket.", - "title": "❌ Insufficient permission" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ This isn't a ticket channel" - }, - "removed": { - "description": "%s has been removed from %s.", - "title": "✅ Member removed" - } - } - }, - "settings": { - "description": "Configure Discord Tickets", - "name": "settings" - }, - "stats": { - "description": "Display ticket statistics", - "fields": { - "messages": "Messages", - "response_time": { - "minutes": "%s minutes", - "title": "Avg. response time" - }, - "tickets": "Tickets" - }, - "name": "stats", - "response": { - "global": { - "description": "Statistics about tickets across all guilds where this Discord Tickets instance is used.", - "title": "📊 Global stats" - }, - "guild": { - "description": "Statistics about tickets within this guild. This data is cached for an hour.", - "title": "📊 This server's stats" - } - } - }, - "survey": { - "description": "View survey responses", - "name": "survey", - "response": { - "list": { - "title": "📃 Surveys" - } - } - }, - "tag": { - "description": "Use a tag response", - "name": "tag", - "response": { - "error": "❌ Error", - "list": { - "title": "📃 Tag list" - }, - "missing": "This tag requires the following arguments:\n%s", - "not_a_ticket": { - "description": "This tag can only be used within a ticket channel as it uses ticket references.", - "title": "❌ This isn't a ticket channel" - } - } - }, - "topic": { - "description": "Change the topic of the ticket", - "name": "topic", - "response": { - "changed": { - "description": "This ticket's topic has been changed.", - "title": "✅ Topic changed" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel you want to change the topic of.", - "title": "❌ This isn't a ticket channel" - } - } - } - }, - "message_will_be_deleted_in": "This message will be deleted in %d seconds", - "missing_permissions": { - "description": "You do not have the permissions required to use this command:\n%s", - "title": "❌" - }, - "ticket": { - "claimed": { - "description": "%s has claimed this ticket.", - "title": "✅ Ticket claimed" - }, - "closed": { - "description": "This ticket has been closed.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_by_member": { - "description": "This ticket has been closed by %s.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_by_member_with_reason": { - "description": "This ticket has been closed by %s: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_with_reason": { - "description": "This ticket has been closed: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "member_added": { - "description": "%s has been added by %s", - "title": "Member added" - }, - "member_removed": { - "description": "%s has been removed by %s", - "title": "Member removed" - }, - "opening_message": { - "fields": { - "topic": "Topic" - } - }, - "questions": "Please answer the following questions:\n\n%s", - "released": { - "description": "%s has released this ticket.", - "title": "✅ Ticket released" - }, - "survey": { - "complete": { - "description": "Thank you for your feedback.", - "title": "✅ Thank you" - }, - "start": { - "description": "Hey, %s. Before this channel is deleted, would you mind completing a quick %d-question survey? React with ✅ to start, or ignore this message.", - "title": "❔ Feedback" - } - } - } -} diff --git a/src/locales/hu.json b/src/locales/hu.json deleted file mode 100644 index a3cd10a..0000000 --- a/src/locales/hu.json +++ /dev/null @@ -1,610 +0,0 @@ -{ - "blacklisted": "❌Feketelistázva vagy", - "bot": { - "missing_permissions": { - "description": "Discord Ticketek az alábbi engedélyeket igényli:\n%s", - "title": "⚠️" - }, - "version": "[Discord Ticketek](%s) v%s által [eartharoid](%s)" - }, - "collector_expires_in": "%d másodperc múlva lejár", - "command_execution_error": { - "description": "Egy váratlan hiba lépett fel a parancs futtatása közben\nKérj meg egy adminisztrátort hogy nézze meg a parancssort / hibanaplókat további információkért.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Felhasználó hozzáadása a Hibajegyhez", - "name": "hozzáad", - "options": { - "member": { - "description": "A felhasználó akit hozzá szeretnél adni a Tickethez", - "name": "tag" - }, - "ticket": { - "description": "A Ticket amihez hozzá szeretnéd adni a felhasználót", - "name": "ticket" - } - }, - "response": { - "added": { - "description": "%s felhasználó hozzáadva %s.", - "title": "✅ Felhasználó hozzáadva" - }, - "no_member": { - "description": "Kérlek, említsd meg a hozzáadni kívánt felhasználót.", - "title": "❌ Ismeretlen felhasználó" - }, - "no_permission": { - "description": "Nem ennek a Ticket alkotója, és nem munkatársa, így nem adhatsz hozzá felhasználókat ehhez a Hibajegyhez.", - "title": "❌ Nincs engedélyed ehhez" - }, - "not_a_ticket": { - "description": "Kérlek, használd ezt a parancsot a Hibajegy csatornában, vagy említsd meg a csatornát.", - "title": "❌ Ez nem Ticket csatorna" - } - } - }, - "blacklist": { - "description": "A feketelista megtekintése vagy módosítása", - "name": "feketelista", - "options": { - "add": { - "description": "Rang vagy felhasználó hozzáadása a feketelistához", - "name": "hozzáadás", - "options": { - "member_or_role": { - "description": "A feketelistára felveendő felhasználó vagy rang", - "name": "felhazsnálo_vagy_rang" - } - } - }, - "remove": { - "description": "Rang vagy felhasználó eltávolítása a feketelistáról", - "name": "eltávolítás", - "options": { - "member_or_role": { - "description": "A feketelistáról eltávolítandó felhasználó vagy rang", - "name": "felhasználó_vagy_rang" - } - } - }, - "show": { - "description": "Megmutatja a feketelistán szereplő felhasználóka és rangokat", - "name": "megmutatás" - } - }, - "response": { - "empty_list": { - "description": "Nincsenek felhasználok vagy rangok feketelistán. Írd be a(z) \"/blacklist add\" parancsot, ha felhasználót vagy rangot szeretnél hozzáadni a feketelistához.", - "title": "📃 Feketelistára került felhasználók és rangok" - }, - "illegal_action": { - "description": "%s csapattag, és nem lehet feketelistára tenni.", - "title": "❌ Ezt a felhasználót nem lehet feketelistára tenni" - }, - "invalid": { - "description": "Ez a felhasználó vagy rang nem távolítható el a feketelistáról, mivel nincsenek feketelistán.", - "title": "❌ Hiba" - }, - "list": { - "fields": { - "members": "Felhasználók", - "roles": "Rangok" - }, - "title": "📃 Feketelistára került felhasználók és rangok" - }, - "member_added": { - "description": "<@%s> felkerült a feketelistára. Többé nem léphet kapcsolatba a bottal.", - "title": "✅ Felhasználó hozzáadva a feketelistára" - }, - "member_removed": { - "description": "<@%s> eltávolítva a feketelistáról. Most újra használhatja a botot.", - "title": "✅ Felhasználó eltávolítva a fektelistáról" - }, - "role_added": { - "description": "<@&%s> felkerült a feketelistára. Az ezzel a rangal rendelkező felhasználók többé nem léphetnek kapcsolatba a bottal.", - "title": "✅ Rang hozzáadva a feketelistára" - }, - "role_removed": { - "description": "<@&%s> eltávolítva a feketelistáról. Az ezzel a rangal rendelkező felhasználók most újra használhatják a botot.", - "title": "✅ Rang eltávolítva a feketelistáról" - } - } - }, - "close": { - "description": "Zárd be a Ticket csatornát", - "name": "bezárás", - "options": { - "reason": { - "description": "A ticket lezárásának indoka", - "name": "indok" - }, - "ticket": { - "description": "A záró ticket, akár a szám, akár a csatorna ID", - "name": "ticket" - }, - "time": { - "description": "Zárj be minden ticketet, amely a megadott ideig inaktív volt", - "name": "idő" - } - }, - "response": { - "canceled": { - "description": "Töröltél a műveletet.", - "title": "🚫 Törölve" - }, - "closed": { - "description": "Ticket #%s lezárva.", - "title": "✅ Ticketek lezárva" - }, - "closed_multiple": { - "description": [ - "%d Ticketek lezárták.", - "%d ticketet lezárták." - ], - "title": [ - "✅ Ticket lezárva", - "✅ Ticketek lezárva" - ] - }, - "confirm": { - "buttons": { - "cancel": "Mégse", - "confirm": "Bezárás" - }, - "description": "Kérlek, erősítsd meg döntésed.", - "description_with_archive": "A ticketet archiváljuk a későbbi hivatkozás érdekében.", - "title": "❔ Biztos vagy ebben?" - }, - "confirm_multiple": { - "buttons": { - "cancel": "Mégse", - "confirm": [ - "%d ticket bezárása", - "%d ticketek bezárása" - ] - }, - "description": [ - "Arra készülsz, hogy lezárod %d ticketét.", - "Arra készülsz, hogy lezárod %d ticketjüket." - ], - "title": "❔ Biztos vagy benne?" - }, - "confirmation_timeout": { - "description": "Túl sokáig tartott a megerősítés.", - "title": "❌ Az interakciós idő lejárt" - }, - "invalid_time": { - "description": "A megadott időszakot nem lehetett elemezni.", - "title": "❌ Érvénytelen bemenet" - }, - "no_permission": { - "description": "Nem vagy személyzeti felhasználó vagy ticket gyártója.", - "title": "❌ Nincs elegendő engedélyed" - }, - "no_tickets": { - "description": "Nincs olyan ticket, amely inaktív lenne ebben az időszakban.", - "title": "❌Ticketeknek nincs zárható ticketjük" - }, - "not_a_ticket": { - "description": "Kérlek, használd ezt a parancsot egy ticket csatornában, vagy használd a ticket zászlót.\nTovábbi információért írd be a(z) \"/help close\" parancsot.", - "title": "❌ Ez nem ticket csatorna" - }, - "unresolvable": { - "description": "`%s` nem lehet feloldani ticketjét. Kérlek, add meg a ticket ID-ját/említését vagy számát.", - "title": "❌ Hiba" - } - } - }, - "help": { - "description": "Felsorolja azokat a parancsokat, amelyekhez hozzáférsz", - "name": "segítség", - "response": { - "list": { - "description": "Az alábbiakban felsoroljuk a hozzáférhető parancsokat. Ticket létrehozásához írd be a **`/new`** parancsot.", - "fields": { - "commands": "Parancsok" - }, - "title": "❔ Segítség" - } - } - }, - "new": { - "description": "Új Ticket létrehozása", - "name": "új", - "options": { - "topic": { - "description": "A ticket témája", - "name": "téma" - } - }, - "request_topic": { - "description": "Kérlek, néhány szóban röviden írd le, miről szól ez a ticket.", - "title": "⚠️ Ticket témája" - }, - "response": { - "created": { - "description": "Tickete létrehozva: %s.", - "title": "✅ Ticket létrehozva" - }, - "error": { - "title": "❌ Hiba" - }, - "has_a_ticket": { - "description": "Kérlek, használd meglévő ticketed (<#%s>), vagy zárd be, mielőtt újat hozol létre.", - "title": "❌ Már van nyitott ticketed" - }, - "max_tickets": { - "description": "A \"/close\" parancsal zárd be a szükségtelen ticketeket.\n\n%s", - "title": "❌ Már van %d nyitott ticketed" - }, - "no_categories": { - "description": "A szerver adminisztrátorának legalább egy ticket kategóriát kell létrehoznia, mielőtt új ticketet nyithatnál.", - "title": "❌ Nem lehet ticketet létrehozni" - }, - "select_category": { - "description": "Válaszd ki a ticket témájának leginkább megfelelő kategóriáját.", - "title": "🔤 Kérlek, válaszd ki a ticket kategóriáját" - }, - "select_category_timeout": { - "description": "Túl sokáig tartott a ticket kategória kiválasztása.", - "title": "❌ Az interakciós idő lejárt" - } - } - }, - "panel": { - "description": "Hozz létre új ticket panelt", - "name": "panel", - "options": { - "categories": { - "description": "A ID-kat vesszővel elválasztott listája", - "name": "kategóriák" - }, - "description": { - "description": "A panelüzenet leírása", - "name": "leírás" - }, - "image": { - "description": "A panelüzenet kép URL -je", - "name": "kép" - }, - "just_type": { - "description": "Létrehozol egy \"csak típus\" panelt?", - "name": "csak_gépelj" - }, - "thumbnail": { - "description": "A panelüzenet miniatűr képének URL -je", - "name": "miniatűr kép" - }, - "title": { - "description": "A panelüzenet címe", - "name": "cím" - } - }, - "response": { - "invalid_category": { - "description": "A megadott ID közül egy vagy több érvénytelen.", - "title": "❌ Érvénytelen kategória" - }, - "too_many_categories": { - "description": "A \"csak típus\" panel csak egyetlen kategóriával használható.", - "title": "❌ Túl sok kategória" - } - } - }, - "remove": { - "description": "Felhasználó eltávolítása a ticketből", - "name": "eltávolítás", - "options": { - "member": { - "description": "A felhasználó, hogy távolítsd el a ticketet", - "name": "felhasználó" - }, - "ticket": { - "description": "Felhasználó eltávolítása a ticketből", - "name": "ticket" - } - }, - "response": { - "no_member": { - "description": "Kérlek, említsd meg az eltávolítani kívánt felhasználót.", - "title": "❌ Ismeretlen felhasználó" - }, - "no_permission": { - "description": "Nem vagy ennek a ticketnek az alkotója, és nem munkatársa; nem távolíthatsz el a felhasználókat ebből a ticketől.", - "title": "❌ Nincs elegendő engedélyed" - }, - "not_a_ticket": { - "description": "Kérlek, használd ezt a parancsot a ticket csatornában, vagy említsd meg a csatornát.", - "title": "❌ Ez nem ticket csatorna" - }, - "removed": { - "description": "%s eltávolítva innen: %s.", - "title": "✅ Felhasználó eltávolítva" - } - } - }, - "settings": { - "description": "Állítsd be a Discord Ticketeket", - "name": "beállítások", - "options": { - "categories": { - "description": "Kezeld ticket kategóriákat", - "name": "kategóriák", - "options": { - "create": { - "description": "Új kategória létrehozása", - "name": "létrehozás", - "options": { - "name": { - "description": "A kategória neve", - "name": "név" - }, - "roles": { - "description": "A kategória személyi szerepkör-ID-nak vesszővel elválasztott listája", - "name": "rangok" - } - } - }, - "delete": { - "description": "Kategória törlése", - "name": "törlés", - "options": { - "id": { - "description": "A törölni kívánt kategória ID-ja", - "name": "id" - } - } - }, - "edit": { - "description": "Módosítsd a kategória konfigurációját", - "name": "szerkesztés", - "options": { - "claiming": { - "description": "Engedélyezed a ticket igénylését?", - "name": "igénylés" - }, - "id": { - "description": "A szerkeszteni kívánt kategória ID-ja", - "name": "id" - }, - "image": { - "description": "Kép URL", - "name": "kép" - }, - "max_per_member": { - "description": "Ebben a kategóriában a ticketek maximális száma, amelyet egy felhasználó kaphat", - "name": "felhasználónként_maximális" - }, - "name": { - "description": "A kategória neve", - "name": "név" - }, - "name_format": { - "description": "A ticket nevének a formátuma", - "name": "név_formátum" - }, - "opening_message": { - "description": "A szöveg, amelyet el kell küldeni a ticket kinyitásakor", - "name": "nyitó_üzenet" - }, - "opening_questions": { - "description": "Kérdések, amelyeket fel kell tenni a ticket kinyitásakor.", - "name": "nyitó_kérdések" - }, - "ping": { - "description": "A pingelni kívánt rang ID vesszővel elválasztott listája", - "name": "ping" - }, - "require_topic": { - "description": "Követeld a felhasználótól, hogy adja meg a ticket témáját?", - "name": "témát_igényel" - }, - "roles": { - "description": "A személyzeti rang ID vesszővel elválasztott listája", - "name": "rangok" - }, - "survey": { - "description": "A felmérés használható", - "name": "felmérés" - } - } - }, - "list": { - "description": "Kategóriák listája", - "name": "lista" - } - } - }, - "set": { - "description": "Beállítások beállítása", - "name": "beállítás", - "options": { - "close_button": { - "description": "Engedélyezed a gombbal történő zárást?", - "name": "záró_gomb" - }, - "colour": { - "description": "A standard szín", - "name": "szín" - }, - "error_colour": { - "description": "A hiba színe", - "name": "hiba_színe" - }, - "footer": { - "description": "A lábléc beágyazása", - "name": "lábléc" - }, - "locale": { - "description": "A területi beállítás (nyelv)", - "name": "nyelv" - }, - "log_messages": { - "description": "Tárold a ticketekből származó üzeneteket?", - "name": "napló_üzenetek" - }, - "success_colour": { - "description": "A siker színe", - "name": "siker_színe" - } - } - } - }, - "response": { - "category_created": "✅ Létrehozva a(z) `%s` ticket kategória", - "category_deleted": "✅ Törölve a(z) `%s` ticket kategória", - "category_does_not_exist": "❌ A megadott ID-val nem létezik kategória", - "category_list": "Ticket kategóriák", - "category_updated": "✅ A(z) `%s` ticket kategória frissítve", - "settings_updated": "✅ A beállítások sikeresen frissítve" - } - }, - "stats": { - "description": "Ticket statisztikák megjelenítése", - "fields": { - "messages": "Üzenetek", - "response_time": { - "minutes": "%s perc", - "title": "Átl. válaszidő" - }, - "tickets": "Ticketek" - }, - "name": "statisztika", - "response": { - "global": { - "description": "Statisztikák a ticketekről az összes szerveren, ahol ezt a Discord Ticketek példányt használják.", - "title": "📊 Globális statisztika" - }, - "guild": { - "description": "Statisztikák a szerveren belüli ticketekről. Ezeket az adatokat egy órán keresztül tárolja a gyorsítótárban.", - "title": "📊 Szerver statisztikája" - } - } - }, - "survey": { - "description": "Felmérési válaszok megtekintése", - "name": "felmérés", - "options": { - "survey": { - "description": "A válaszok megtekintéséhez szükséges felmérés neve", - "name": "felmérés" - } - }, - "response": { - "list": { - "title": "📃 Felmérések" - } - } - }, - "tag": { - "description": "Címke válasz használata", - "name": "címke", - "options": { - "tag": { - "description": "A használni kívánt címke neve", - "name": "címke" - } - }, - "response": { - "error": "❌ Hiba", - "list": { - "title": "📃 Címke lista" - }, - "missing": "Ez a címke a következő érveket igényli:\n%s", - "not_a_ticket": { - "description": "Ez a címke csak ticket csatornán belül használható, mivel ticket referenciákat használ.", - "title": "❌ Ez nem ticket csatorna" - } - } - }, - "topic": { - "description": "Változtasd meg a ticket témáját", - "name": "téma", - "options": { - "new_topic": { - "description": "A ticket új témája", - "name": "új_téma" - } - }, - "response": { - "changed": { - "description": "Ennek a ticketnek a témája megváltozott.", - "title": "✅ Téma változtatva" - }, - "not_a_ticket": { - "description": "Kérlek, használd ezt a parancsot a ticket csatornában, amelynek témáját módosítani szeretnéd.", - "title": "❌ Ez nem ticket csatorna" - } - } - } - }, - "message_will_be_deleted_in": "Ez az üzenet %d másodperc múlva törlődik", - "missing_permissions": { - "description": "Nem rendelkezel a parancs használatához szükséges jogosultságokkal:\n%s", - "title": "❌ Hiba" - }, - "panel": { - "create_ticket": "Ticket létrehozása" - }, - "ticket": { - "claim": "Igénylés", - "claimed": { - "description": "%s igényelt egy ticketet.", - "title": "✅ Ticket igényelve" - }, - "close": "Bezárás", - "closed": { - "description": "Ezt a ticketet lezárták.\nA csatorna 5 másodpercen belül törlődik.", - "title": "✅ Ticket lezárva" - }, - "closed_by_member": { - "description": "Ezt a ticketet lezárta %s.\nA csatorna 5 másodpercen belül törlődik.", - "title": "✅ Ticket lezárva" - }, - "closed_by_member_with_reason": { - "description": "Ezt a ticketet %s lezárta: ` %s`\nA csatorna 5 másodpercen belül törlődik.", - "title": "✅ Ticket lezárva" - }, - "closed_with_reason": { - "description": "Ezt a ticketet lezárta: `%s`\nA csatorna 5 másodpercen belül törlődik.", - "title": "✅ Ticket lezárva" - }, - "member_added": { - "description": "%s hozzáadva %s által", - "title": "Felhasználó hozzáadva" - }, - "member_removed": { - "description": "%s eltávolítva %s által", - "title": "Felhasználó eltávolítva" - }, - "opening_message": { - "content": "%s\n%s létrehozott egy új ticketet", - "fields": { - "topic": "Téma" - } - }, - "questions": "Kérlek, válaszolj az alábbi kérdésekre:\n\n%s", - "released": { - "description": "%s kiadta ezt a ticketet.", - "title": "✅ Ticket kiadva" - }, - "survey": { - "complete": { - "description": "Köszönjük a visszajelzést.", - "title": "✅ Köszönjük" - }, - "start": { - "buttons": { - "ignore": "Nem", - "start": "Indítsd el a felmérést" - }, - "description": "Szia, %s. Mielőtt törölnéd ezt a csatornát, szeretnél kitölteni egy gyors %d-question felmérést?", - "title": "❔Visszajelzés" - } - }, - "unclaim": "Kiadás" - }, - "updated_permissions": "✅ A Slash parancs engedélyei frissítve" -} diff --git a/src/locales/id-ID.json b/src/locales/id-ID.json deleted file mode 100644 index f3db1e6..0000000 --- a/src/locales/id-ID.json +++ /dev/null @@ -1,324 +0,0 @@ -{ - "bot": { - "missing_permissions": { - "description": "Discord Tickets memerlukan izin berikut:\n%s", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) versi %s oleh [eartharoid](%s)" - }, - "collector_expires_in": "Kadaluwarsa dalam %d detik", - "command_execution_error": { - "description": "Eror tak terduga ketika mengeksekusi command.\nTolong tanya seorang adminstrator untuk memeriksa console atau log konsol untuk detail.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Tambahkan anggota ke tiket", - "name": "tambahkan", - "response": { - "added": { - "description": "%s telah ditambahkan ke %s.", - "title": "✅ Anggota ditambahkan" - }, - "no_member": { - "description": "Sebutkan anggota yang ingin Anda tambahkan.", - "title": "❌ Anggota tidak dikenal" - }, - "no_permission": { - "description": "Anda bukan pembuat tiket ini dan Anda bukan anggota staf; Anda tidak dapat menambahkan anggota ke tiket ini.", - "title": "❌ Izin tidak memadai" - }, - "not_a_ticket": { - "description": "Silakan gunakan perintah ini di saluran tiket, atau sebutkan salurannya.", - "title": "❌ Ini bukan saluran tiket" - } - } - }, - "blacklist": { - "description": "Blok/hapus blok seorang anggota dari berinteraksi dengan bot ini", - "name": "blok", - "response": { - "empty_list": { - "description": "Tidak ada anggota atau role yang di blok. Ketik `%sblok ` untuk menambahkan member atau role ke daftar blokir.", - "title": "📃 Daftar anggota-anggota yang di blok" - }, - "illegal_action": { - "description": "%s adalah seorang anggota staf dan tidak bisa di blokir.", - "title": "❌ Anda tidak bisa blokir anggota ini" - }, - "list": { - "title": "📃 Daftar anggota-anggota yang di blok" - }, - "member_added": { - "description": "<@%s> telah ditambahkan ke daftar blokir. Mereka tidak akan lagi dapat berinteraksi dengan bot ini.", - "title": "✅ Berhasil menambahkan anggota ke daftar blokir" - }, - "member_removed": { - "description": "<@%s> telah dihapus dari daftar blokir. Sekarang mereka dapat menggunakan bot ini lagi.", - "title": "✅ Berhasil menghapus anggota dari daftar blokir" - }, - "role_added": { - "description": "<@&%s> telah ditambahkan ke daftar blokir. Anggota-anggota dengan role ini tidak akan lagi dapat berinteraksi dengan bot ini.", - "title": "✅ Berhasil menambahkan role ke daftar blokir" - }, - "role_removed": { - "description": "<@&%s> telah dihapus dari daftar blokir. Anggota-anggota dengan role ini sekarang dapat menggunakan bot ini lagi.", - "title": "✅ Berhasil menghapus role dari daftar blokir" - } - } - }, - "close": { - "description": "Tutup sebuah channel tiket", - "name": "tutup", - "response": { - "closed": { - "description": "Ticket #%s sudah di tutup.", - "title": "✅ Tiket ditutup" - }, - "closed_multiple": { - "description": [ - "%d tiket sudah ditutup.", - "%d tiket sudah ditutup." - ], - "title": [ - "✅ Tiket ditutup", - "✅ Tickets closed" - ] - }, - "confirm": { - "description": "Bereaksi dengan ✅ untuk menutup tiket ini.", - "description_with_archive": "Anda akan dapat melihat versi yang diarsipkan setelahnya.\nBereaksi dengan ✅ untuk menutup tiket.", - "title": "❔ Apa kamu yakin?" - }, - "confirm_multiple": { - "description": [ - "Bereaksi dengan ✅ untuk menutup %d tiket.", - "Bereaksi dengan ✅ untuk menutup %d tiket." - ], - "title": "❔ Apa kamu yakin?" - }, - "confirmation_timeout": { - "description": "Anda terlalu lama untuk mengonfirmasi.", - "title": "❌ Waktu reaksi habis" - }, - "invalid_time": { - "description": "Jangka waktu yang disediakan tidak dapat diuraikan.", - "title": "❌ Masukan tidak valid" - }, - "no_tickets": { - "description": "Tidak ada tiket yang tidak aktif selama periode waktu ini.", - "title": "❌ Tidak ada tickets untuk menutup" - }, - "not_a_ticket": { - "description": "Silakan gunakan perintah ini di saluran tiket atau gunakan tanda tiket .\nKetik `%shelp close` untuk informasi lebih lanjut.", - "title": "❌ Ini bukan saluran tiket" - }, - "unresolvable": { - "description": "`%s` tidak bisa diselesaikan dengan tiket. Harap berikan ID tiket/sebutan atau nomor.", - "title": "❌ Error" - } - } - }, - "help": { - "description": "Buat daftar perintah yang dapat Anda akses, atau cari tahu lebih lanjut tentang sebuah perintah", - "name": "bantuan", - "response": { - "list": { - "description": "Perintah yang dapat Anda akses tercantum di bawah ini. Untuk informasi lebih lanjut tentang perintah, ketik `{prefix}help [command]`. Untuk membuat tiket, ketik `{prefix}new [topic]`.", - "fields": { - "commands": "Commands" - }, - "title": "❔ Help" - } - } - }, - "new": { - "description": "Buat tiket baru", - "name": "baru", - "request_topic": { - "description": "Tolong beri tahu secara singkat untuk apa tiket ini dibuat dalam beberapa kata.", - "title": "Topik tiket" - }, - "response": { - "created": { - "description": "Tiket anda telah dibuat: %s.", - "title": "✅ Tiket dibuat" - }, - "error": { - "title": "❌ Error" - }, - "has_a_ticket": { - "description": "Silahkan gunakan tiket yang ada (<#%s>) atau tutup tiket tersebut sebelum membuat tiket lain.", - "title": "❌ Anda sudah memiliki tiket yang buka" - }, - "max_tickets": { - "description": "Tolong gunakan `%stutup` untuk menutup tiket yang tidak perlu.\n\n%s", - "title": "❌ Anda sudah memiliki %d tiket yang buka" - }, - "no_categories": { - "description": "Seorang administrator server harus membuat setidaknya satu kategori tikey sebelum tiket baru dapat dibuka.", - "title": "❌ Tidak dapat membuat tiket" - }, - "select_category": { - "description": "Pilih kategory yang paling relevan denga topik tiket anda:\n\n%s", - "title": "🔤 Tolong pilih kategori tiket" - }, - "select_category_timeout": { - "description": "Anda terlalu lama untuk memilih kategori tiket.", - "title": "❌ Waktu reaksi habis" - } - } - }, - "panel": { - "description": "Create a new ticket panel", - "name": "panel", - "response": { - "invalid_category": { - "description": "Satu atau beberapa ID kategori yang ditentukan tidak valid.", - "title": "❌ Kategori tidak valid" - } - } - }, - "remove": { - "description": "Hapus anggota dari tiket", - "name": "remove", - "response": { - "no_member": { - "description": "Sebutkan anggota yang ingin Anda hapus.", - "title": "❌ Anggota tidak dikenal" - }, - "no_permission": { - "description": "Anda bukan pembuat tiket ini dan Anda bukan anggota staf; Anda tidak dapat menghapus anggota dari tiket ini.", - "title": "❌ Izin tidak memadai" - }, - "not_a_ticket": { - "description": "Silakan gunakan perintah ini di saluran tiket, atau sebutkan salurannya.", - "title": "❌ Ini bukan saluran tiket" - }, - "removed": { - "description": "%s telah dihapus dari %s.", - "title": "✅ Anggota dihapus" - } - } - }, - "settings": { - "description": "Konfigurasi Discord Tickets", - "name": "pengaturan" - }, - "stats": { - "description": "Tampilkan statistik tiket", - "fields": { - "messages": "Pesan", - "response_time": { - "minutes": "%s minutes", - "title": "Avg. waktu respon" - }, - "tickets": "Tiket" - }, - "name": "stistik", - "response": { - "global": { - "description": "Statistik tentang tiket di semua guild tempat instance Tiket Discord ini digunakan.", - "title": "📊 Statistik global" - }, - "guild": { - "description": "Statistics about tickets within this guild. This data is cached for an hour.", - "title": "📊 statistik server" - } - } - }, - "survey": { - "description": "Lihat tanggapan survei", - "name": "survey", - "response": { - "list": { - "title": "📃 Surveys" - } - } - }, - "tag": { - "description": "Gunakan tanggapan tag", - "name": "tag", - "response": { - "error": "❌ Error", - "list": { - "title": "📃 Tag list" - }, - "missing": "Tag ini membutuhkan argumen berikut::\n%s", - "not_a_ticket": { - "description": "Tag ini hanya dapat digunakan dalam saluran tiket karena menggunakan referensi tiket.", - "title": "❌ Ini bukan saluran tiket" - } - } - }, - "topic": { - "description": "Ubah topik tiket", - "name": "topik", - "response": { - "changed": { - "description": "Topik tiket ini telah diubah.", - "title": "✅ Topik di ganti" - }, - "not_a_ticket": { - "description": "Silakan gunakan perintah ini di saluran tiket yang ingin Anda ubah topiknya.", - "title": "❌ Ini bukan saluran tiket" - } - } - } - }, - "message_will_be_deleted_in": "Pesan ini akan dihapus dalam %d detik", - "missing_permissions": { - "description": "Anda tidak memiliki izin yang diperlukan untuk menggunakan perintah ini:\n%s", - "title": "❌" - }, - "ticket": { - "claimed": { - "description": "%s telah mengklaim tiket ini.", - "title": "✅ Tiket diklaim" - }, - "closed": { - "description": "Tiket ini telah ditutup.\nSaluran akan dihapus dalam 5 detik.", - "title": "✅ Tiket ditutup" - }, - "closed_by_member": { - "description": "Tiket ini telah ditutup by %s.\nSaluran akan dihapus dalam 5 detik.", - "title": "✅ Tiket ditutup" - }, - "closed_by_member_with_reason": { - "description": "Tiket ini telah ditutup by %s: `%s`\nSaluran akan dihapus dalam 5 detik.", - "title": "✅ Tiket ditutup" - }, - "closed_with_reason": { - "description": "Tiket ini telah ditutup: `%s`\nSaluran akan dihapus dalam 5 detik.", - "title": "✅ Tiket ditutup" - }, - "member_added": { - "description": "%s telah ditambahkan oleh %s", - "title": "Anggota ditambahkan" - }, - "member_removed": { - "description": "%s telah dihapus oleh %s", - "title": "Anggota dihapus" - }, - "opening_message": { - "fields": { - "topic": "Topik" - } - }, - "questions": "Jawablah pertanyaan berikut:\n\n%s", - "released": { - "description": "%s telah merilis tiket ini.", - "title": "✅ Tiket dirilis" - }, - "survey": { - "complete": { - "description": "Terima kasih atas tanggapan Anda.", - "title": "✅ Terima kasih" - }, - "start": { - "description": "Hey, %s. Sebelum saluran ini dihapus, maukah Anda menyelesaikannya dengan cepat %d-survei pertanyaan? Bereaksi dengan ✅ untuk mulai, atau biarkan pesan ini.", - "title": "❔ umpan balik" - } - } - } -} diff --git a/src/locales/it-IT.json b/src/locales/it-IT.json deleted file mode 100644 index 6073483..0000000 --- a/src/locales/it-IT.json +++ /dev/null @@ -1,610 +0,0 @@ -{ - "blacklisted": "❌ Sei bandito dal creare nuovi ticket", - "bot": { - "missing_permissions": { - "description": "Il bot richiede i seguenti permessi:\n%s ", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s di [eartharoid](%s)" - }, - "collector_expires_in": "Scade in %d secondi", - "command_execution_error": { - "description": "C'è stato un errore improvviso durante l'esecuzioni di questo comando.\nChiedi ad un amministratore di leggere i dettagli dell'errore per risolverlo", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Aggiungi un utente al ticket", - "name": "aggiungi", - "options": { - "member": { - "description": "L'utente da aggiungere al ticket", - "name": "membro" - }, - "ticket": { - "description": "Il ticket in cui l'utente sarà aggiunto", - "name": "ticket" - } - }, - "response": { - "added": { - "description": "%s è stato aggiunto a %s.", - "title": "✅ Utente aggiunto" - }, - "no_member": { - "description": "Menziona l'utente che vuoi aggiungere.", - "title": "❌ Utente sconosciuto" - }, - "no_permission": { - "description": "Non hai creato questo ticket e non fai parte dello staff; non puoi aggiungere altri membri.", - "title": "❌ Permessi insufficienti" - }, - "not_a_ticket": { - "description": "Usa questo comando nel canale testuale di un ticket.", - "title": "❌ Questo non è un ticket" - } - } - }, - "blacklist": { - "description": "Guarda o modifica la blacklist", - "name": "blacklist", - "options": { - "add": { - "description": "Aggiungi o rimuovi un utente dalla blacklist", - "name": "aggiungi", - "options": { - "member_or_role": { - "description": "L'ID del membro o del ruolo che vuoi aggiungere alla blacklist", - "name": "membro_o_ruolo" - } - } - }, - "remove": { - "description": "Rimuovi un membro dalla blacklist", - "name": "rimuovi", - "options": { - "member_or_role": { - "description": "Il membro o il ruolo da rimuovere dalla blacklist", - "name": "membro_o_ruolo" - } - } - }, - "show": { - "description": "Mostra i membri o i ruoli presenti nella blacklist", - "name": "mostra" - } - }, - "response": { - "empty_list": { - "description": "Non ci sono utenti o ruoli nella blacklist. Usa `/blacklist add` per aggiungere un utente o un ruolo", - "title": "📃 Utenti e Ruoli nella blacklist" - }, - "illegal_action": { - "description": "%s fa parte dello staff e non può essere aggiunto nella blacklist.", - "title": "❌ Non puoi aggiungere lo staff nella blacklist " - }, - "invalid": { - "description": "Questo utente o ruolo non può essere rimosso dalla blacklist in quanto non ne fa parte.", - "title": "❌ Error" - }, - "list": { - "fields": { - "members": "Utenti", - "roles": "Ruoli" - }, - "title": "📃 Utenti o Ruoli nella blacklist" - }, - "member_added": { - "description": "<@%s> è stato aggiunto alla blacklist. Non potranno più interagire col bot.", - "title": "✅ Utente aggiunto alla blacklist" - }, - "member_removed": { - "description": "<@%s> è stato rimosso dalla blacklist. Potranno di nuovo usare il bot.", - "title": "✅ Membro rimosso dalla blacklist" - }, - "role_added": { - "description": "<@&%s> è stato aggiunto alla blacklist. Gli utenti con questo ruolo non potranno più interagie il bot.", - "title": "✅ Ruolo aggiunto alla blacklist" - }, - "role_removed": { - "description": "<@&%s> è stato rimosso dalla blacklist. Potranno di nuovo usare il bot", - "title": "✅ Ruolo rimosso dalla blacklist" - } - } - }, - "close": { - "description": "Chiude un ticket", - "name": "chiudi", - "options": { - "reason": { - "description": "Il motivo della chiusura del ticket", - "name": "motivo" - }, - "ticket": { - "description": "Il ticket da chiudere, il numero o l'ID", - "name": "ticket" - }, - "time": { - "description": "Chiudi tutti i ticket inattivi da un specifico periodo di tempo", - "name": "tempo" - } - }, - "response": { - "canceled": { - "description": "Esecuzione annullata.", - "title": "🚫 Annullata" - }, - "closed": { - "description": "Ticket #%s è stato chiuso.", - "title": "✅ Ticket chiuso" - }, - "closed_multiple": { - "description": [ - "%d ticket è stato chiuso.", - "%d tickets sono stati chiusi." - ], - "title": [ - "✅ Ticket chiuso", - "✅ Tickets chiusi" - ] - }, - "confirm": { - "buttons": { - "cancel": "Annulla", - "confirm": "Chiudi" - }, - "description": "Conferma la tua azione.", - "description_with_archive": "Il ticket sarà archiviato in caso di neccessità futura.", - "title": "❔ Sei sicuro ?" - }, - "confirm_multiple": { - "buttons": { - "cancel": "Annulla", - "confirm": [ - "Chiudi %d ticket", - "Chiudi %d ticket" - ] - }, - "description": [ - "Stai per chiudere %d ticket.", - "Stai per chiudere %d tickets." - ], - "title": "❔ Sei sicuro?" - }, - "confirmation_timeout": { - "description": "Ci hai messo troppo tempo per rispondere.", - "title": "❌ Tempo per confermare finito" - }, - "invalid_time": { - "description": "Il periodo di tempo fornito è invalido.", - "title": "❌ Tempo invalido" - }, - "no_permission": { - "description": "Non fai parte dello staff e non sei l'autore del ticket.", - "title": "❌ Permessi insufficienti" - }, - "no_tickets": { - "description": "Nessun ticket è stato inattivo per così tanto tempo.", - "title": "❌ Nessun ticket da chiudere" - }, - "not_a_ticket": { - "description": "Usa questo comando in un ticket oppure usa una flag di chiusura.\nScrivi `/aiuto close` per maggiori informazioni", - "title": "❌ Questo non è un ticket" - }, - "unresolvable": { - "description": "`%s` non sembra essere un ticket. Specifica un numero o un ID", - "title": "❌ Errore" - } - } - }, - "help": { - "description": "Lista dei comandi a cui hai accesso", - "name": "aiuto", - "response": { - "list": { - "description": "Comandi a cui hai accesso. Per creare un ticket, usa **`/crea`**.", - "fields": { - "commands": "Comandi" - }, - "title": "❔ Aiuto" - } - } - }, - "new": { - "description": "Crea un nuovo ticket", - "name": "crea", - "options": { - "topic": { - "description": "L'argomento del ticket", - "name": "argomento" - } - }, - "request_topic": { - "description": "Descrivi brevemente il tuo problema", - "title": "⚠️ Argomento Ticket" - }, - "response": { - "created": { - "description": "Il tuo ticket è stato creato: %s.", - "title": "✅ Ticket creato" - }, - "error": { - "title": "❌ Errore" - }, - "has_a_ticket": { - "description": "Usa il ticket che hai già creato (<#%s>) o chiudilo prima di aprirne un altro .", - "title": "❌ Hai già aperto un ticket" - }, - "max_tickets": { - "description": "Usa `/close` per chiudere tutti i ticket non neccessari.\n\n%s", - "title": "❌ Hai già %d ticket aperti !" - }, - "no_categories": { - "description": "L'amministratore del ticket deve creare almeno una categoria prima che tu possa creare un ticket.", - "title": "❌ Impossibile aprire un ticket" - }, - "select_category": { - "description": "Seleziona la categoria del ticket.", - "title": "🔤 Seleziona una categoria" - }, - "select_category_timeout": { - "description": "Ci hai messo troppo tempo per seleziona una categoria.", - "title": "❌ Tempo scaduto" - } - } - }, - "panel": { - "description": "Crea un nuovo pannello", - "name": "panello", - "options": { - "categories": { - "description": "Gli ID delle categorie separati da una virgola", - "name": "categorie" - }, - "description": { - "description": "La descrizione del pannello", - "name": "descrizione" - }, - "image": { - "description": "URL di un'immagine per il pannello", - "name": "immagine" - }, - "just_type": { - "description": "Creare un pannello \"solo scrittura\"?", - "name": "solo_scrittura" - }, - "thumbnail": { - "description": "Un URL per la miniatura del pannello", - "name": "miniatura" - }, - "title": { - "description": "Il titolo del pannello", - "name": "titolo" - } - }, - "response": { - "invalid_category": { - "description": "Una o più categorie specificate non sono valide.", - "title": "❌ Categoria invalida" - }, - "too_many_categories": { - "description": "Il pannello \"solo scrittura\" può essere utilizzato solo con una categoria.", - "title": "❌ Troppe categorie" - } - } - }, - "remove": { - "description": "Rimuovi un utente dalla categoria", - "name": "rimuovi", - "options": { - "member": { - "description": "L'utente da rimuovere dal ticket", - "name": "utente" - }, - "ticket": { - "description": "Il ticket da cui rimuovere l'utente", - "name": "ticket" - } - }, - "response": { - "no_member": { - "description": "Menziona l'utente che vuoi rimuovere.", - "title": "❌ Utente sconosciuto" - }, - "no_permission": { - "description": "Non sei l'autore del ticket e un membro dello staff; non puoi rimuovere utenti dai ticket.", - "title": "❌ Permessi insufficienti " - }, - "not_a_ticket": { - "description": "Usa qeusto comando in un ticket, o menzionane uno.", - "title": "❌ Questo non è un ticket" - }, - "removed": { - "description": "%s è stato rimosso da %s.", - "title": "✅ Utente rimosso" - } - } - }, - "settings": { - "description": "Configura Discord Tickets", - "name": "impostazioni", - "options": { - "categories": { - "description": "Gestici le categorie dei ticket", - "name": "categorie", - "options": { - "create": { - "description": "Crea una nuova categoria", - "name": "crea", - "options": { - "name": { - "description": "Il nome della categoria", - "name": "nome" - }, - "roles": { - "description": "Una lista di ID dei ruoli dello staff separati da una virgola", - "name": "ruoli" - } - } - }, - "delete": { - "description": "Cancella una categoria", - "name": "cancella", - "options": { - "id": { - "description": "L'id della categoria da rimuovere", - "name": "id" - } - } - }, - "edit": { - "description": "Fai cambiamenti alle impostazioni delle categoria ", - "name": "modifica", - "options": { - "claiming": { - "description": "Attivare il claim dei ticket ?", - "name": "claiming" - }, - "id": { - "description": "ID della categoria da modificare", - "name": "id" - }, - "image": { - "description": "Un URL dell'immagine", - "name": "immagine" - }, - "max_per_member": { - "description": "Il numero massimo di ticket che l'utente può avere per questa categoria", - "name": "massimo_per_membro" - }, - "name": { - "description": "Il nome della categoria", - "name": "nome" - }, - "name_format": { - "description": "Il formato del nome del ticket", - "name": "formato_nome" - }, - "opening_message": { - "description": "Il testo del messaggio inviato quando si apre un ticket", - "name": "messaggio_di_apertura" - }, - "opening_questions": { - "description": "Domande a cui rispondere quando si apre un ticket.", - "name": "domande_di_apertura" - }, - "ping": { - "description": "Una lista di ID dei ruoli separati da una virgola che verranno menzionati ", - "name": "ping" - }, - "require_topic": { - "description": "Richiedere all'utente di specificare un argomento ?", - "name": "specifica_argomento" - }, - "roles": { - "description": "Una lista di ID dei ruoli dello staff separati da una virgola", - "name": "ruoli" - }, - "survey": { - "description": "Il questionario da usare", - "name": "questionario" - } - } - }, - "list": { - "description": "Lista delle categorie", - "name": "lista" - } - } - }, - "set": { - "description": "Imposta opzioni", - "name": "impposta", - "options": { - "close_button": { - "description": "Abilita la chiusura con bottone?", - "name": "bottone_per_chiusura" - }, - "colour": { - "description": "Il colore standard [HEX]", - "name": "colore" - }, - "error_colour": { - "description": "Il colore per gli errori [HEX]", - "name": "colore_per_errori" - }, - "footer": { - "description": "Testo per il footer degli embed", - "name": "footer" - }, - "locale": { - "description": "Lingua", - "name": "locale" - }, - "log_messages": { - "description": "Conservare i messaggi dei ticket?", - "name": "conservare_messaggi" - }, - "success_colour": { - "description": "Colore successo", - "name": "colore_successo" - } - } - } - }, - "response": { - "category_created": "✅ La categoria `%s` è stata creata", - "category_deleted": "✅ La categoria `%s` è stata cancellata", - "category_does_not_exist": "❌ Non esiste nessuna categoria con questo ID", - "category_list": "Categoria Ticket", - "category_updated": "✅ La categoria `%s` è stata aggiornata", - "settings_updated": "✅ Le impostazioni sono state aggiornate" - } - }, - "stats": { - "description": "Mostra le statistiche dei ticket", - "fields": { - "messages": "Messaggi", - "response_time": { - "minutes": "%s minutei", - "title": "Attesa media" - }, - "tickets": "Tickets" - }, - "name": "stats", - "response": { - "global": { - "description": "Statistiche di tutti i server in cui questo bot è utilizzato.", - "title": "📊 Statistiche globali" - }, - "guild": { - "description": "Statistiche per questo server. Vengono aggiornate ogni ora", - "title": "📊 Statistiche di questo server" - } - } - }, - "survey": { - "description": "Visualizza le risposte ai questionari", - "name": "questionari", - "options": { - "survey": { - "description": "Nome del questionario di cui vuoi vedere le risposte", - "name": "questionario" - } - }, - "response": { - "list": { - "title": "📃 Questionari" - } - } - }, - "tag": { - "description": "Utilizza un tag di risposta", - "name": "tag", - "options": { - "tag": { - "description": "Nome del tag che vuoi ", - "name": "tag" - } - }, - "response": { - "error": "❌ Errore", - "list": { - "title": "📃 Lista tag" - }, - "missing": "Questo tag richiedere i seguenti requisiti:\n%s", - "not_a_ticket": { - "description": "Questo tag può essere utilizzato solo nei ticket.", - "title": "❌ Questo non è un ticket" - } - } - }, - "topic": { - "description": "Cambia l'argomento del ticket", - "name": "argomento", - "options": { - "new_topic": { - "description": "Il nuovo argomento del ticket ", - "name": "nuovo_argomento" - } - }, - "response": { - "changed": { - "description": "Argomento di questo ticket è stato cambiato.", - "title": "✅ Argomento cambiato" - }, - "not_a_ticket": { - "description": "Utilizza questo comando nel ticket di cui vuoi cambiare argomento.", - "title": "❌ Questo non è un ticket" - } - } - } - }, - "message_will_be_deleted_in": "Questo messaggio sarà cancellato in %d secondi", - "missing_permissions": { - "description": "Non hai il permesso di usare questo comando:\n%s", - "title": "❌ Errore" - }, - "panel": { - "create_ticket": "Crea un ticket" - }, - "ticket": { - "claim": "Claima", - "claimed": { - "description": "%s ha claimato il ticket.", - "title": "✅ Ticket claimato" - }, - "close": "Chiudi", - "closed": { - "description": "Il ticket è stato chiuso.\nIl canale sarà cancellato tra 5 secondi.", - "title": "✅ Ticket Chiuso" - }, - "closed_by_member": { - "description": "Il ticket è stato chiuso da %s.\nIl canale sarà cancellato tra 5 secondi.", - "title": "✅ Ticket chiuso" - }, - "closed_by_member_with_reason": { - "description": "Il ticket è stato chiuso da %s per: `%s`\nIl canale sarà cancellato tra 5 secondi.", - "title": "✅ Ticket chiuso" - }, - "closed_with_reason": { - "description": "Il ticket è stato chiuso da %s per: `%s`\nIl canale sarà cancellato tra 5 secondi.", - "title": "✅ Ticket closed" - }, - "member_added": { - "description": "%s ha aggiunto %s", - "title": "Utente aggiunto" - }, - "member_removed": { - "description": "%s ha rimosso %s", - "title": "Membri rimosso" - }, - "opening_message": { - "content": "%s\n%s ha creato un nuovo ticket", - "fields": { - "topic": "Argomento" - } - }, - "questions": "Rispondi alle seguenti domande:\n\n%s", - "released": { - "description": "%s ha rilasciato il ticket.", - "title": "✅ Ticket rilasciato" - }, - "survey": { - "complete": { - "description": "Grazie per il tuo feedback", - "title": "✅ Grazie" - }, - "start": { - "buttons": { - "ignore": "No", - "start": "Inizia il questionario" - }, - "description": "Hey, %s. Prima di cancellare questo canale, vorresti rispondere a un %d-questionario?", - "title": "❔ Feedback" - } - }, - "unclaim": "Rilascia" - }, - "updated_permissions": "✅ Permessi per i comandi aggiornati" -} diff --git a/src/locales/ko-KR.json b/src/locales/ko-KR.json deleted file mode 100644 index b17054a..0000000 --- a/src/locales/ko-KR.json +++ /dev/null @@ -1,344 +0,0 @@ -{ - "bot": { - "missing_permissions": { - "description": "디스코드 티켓은 해당 권한이 필요합니다:\n%s", - "title": "⚠️" - }, - "version": "[디스코드 티켓](%s) v%s by [eartharoid](%s)" - }, - "collector_expires_in": "%d 초 안에 만료됨", - "command_execution_error": { - "description": "명령을 실행하는 동안 예기치 않은 오류가 발생했습니다.\n자세한 내용은 관리자에게 콘솔 출력/로그를 확인 하도록 요청하십시오.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "티켓에 멤버 추가하기", - "name": "추가", - "options": { - "member": { - "description": "티켓에 추가할 멤버", - "name": "맴버" - }, - "ticket": { - "description": "멤버를 추가할 티켓", - "name": "티켓" - } - }, - "response": { - "added": { - "description": "%s 님에게 %s 를 추가 했습니다.", - "title": "✅ 맴버가 추가됨" - }, - "no_member": { - "description": "추가하고 싶은 멤버를 멘션해 주세요.", - "title": "❌ 알수없는 맴버" - }, - "no_permission": { - "description": "이 티켓의 제작자 및 직원이 아니므로, 회원을 추가할 수 없습니다.", - "title": "❌ Insufficient permission" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ 여긴 티켓 채널이 아닙니다." - } - } - }, - "blacklist": { - "description": "블랙리스트 보기 또는 수정", - "name": "블랙리스트", - "options": { - "remove": { - "name": "제거함", - "options": { - "member_or_role": { - "name": "맴버 또는 역할" - } - } - } - }, - "response": { - "empty_list": { - "description": "There are no members or roles blacklisted. Type `%sblacklist ` to add a member or role to the blacklist.", - "title": "📃 Blacklisted members and roles" - }, - "illegal_action": { - "description": "%s is a staff member and cannot be blacklisted.", - "title": "❌ 당신은 맴버를 블랙리스트 화 시킬수 없습니다" - }, - "list": { - "title": "📃 Blacklisted members and roles" - }, - "member_added": { - "description": "<@%s> has been added to the blacklist. They will no longer be able to interact with the bot.", - "title": "✅ Added member to blacklist" - }, - "member_removed": { - "description": "<@%s> has been removed from the blacklist. They can now use the bot again.", - "title": "✅ Removed member from blacklist" - }, - "role_added": { - "description": "<@&%s> has been added to the blacklist. Members with this role will no longer be able to interact with the bot.", - "title": "✅ Added role to blacklist" - }, - "role_removed": { - "description": "<@&%s> has been removed from the blacklist. Members with this role can now use the bot again.", - "title": "✅ Removed role from blacklist" - } - } - }, - "close": { - "description": "Close a ticket channel", - "name": "close", - "response": { - "closed": { - "description": "Ticket #%s has been closed.", - "title": "✅ Ticket closed" - }, - "closed_multiple": { - "description": [ - "%d ticket has been closed.", - "%d tickets have been closed." - ], - "title": [ - "✅ Ticket closed", - "✅ Tickets closed" - ] - }, - "confirm": { - "description": "React with ✅ to close this ticket.", - "description_with_archive": "You will be able to view an archived version of it after.\nReact with ✅ to close this ticket.", - "title": "❔ Are you sure?" - }, - "confirm_multiple": { - "description": [ - "React with ✅ to close %d ticket.", - "React with ✅ to close %d tickets." - ], - "title": "❔ Are you sure?" - }, - "confirmation_timeout": { - "description": "You took too long to confirm.", - "title": "❌ Reaction time expired" - }, - "invalid_time": { - "description": "The time period provided could not be parsed.", - "title": "❌ Invalid input" - }, - "no_tickets": { - "description": "There are no tickets which have been inactive for this time period.", - "title": "❌ No tickets to close" - }, - "not_a_ticket": { - "description": "Please use this command in a ticket channel or use the ticket flag.\nType `%shelp close` for more information.", - "title": "❌ This isn't a ticket channel" - }, - "unresolvable": { - "description": "`%s` could not be resolved to a ticket. Please provide the ticket ID/mention or number.", - "title": "❌ Error" - } - } - }, - "help": { - "description": "List commands you have access to, or find out more about a command", - "name": "help", - "response": { - "list": { - "description": "The commands you have access to are listed below. For more information about a command, type `{prefix}help [command]`. To create a ticket, type `{prefix}new [topic]`.", - "fields": { - "commands": "Commands" - }, - "title": "❔ Help" - } - } - }, - "new": { - "description": "Create a new ticket", - "name": "new", - "request_topic": { - "description": "Please briefly state what this ticket is about in a few words.", - "title": "Ticket topic" - }, - "response": { - "created": { - "description": "Your ticket has been created: %s.", - "title": "✅ Ticket created" - }, - "error": { - "title": "❌ Error" - }, - "has_a_ticket": { - "description": "Please use your existing ticket (<#%s>) or close it before creating another.", - "title": "❌ You already have an open ticket" - }, - "max_tickets": { - "description": "Please use `%sclose` to close any unneeded tickets.\n\n%s", - "title": "❌ You already have %d open tickets" - }, - "no_categories": { - "description": "A server administrator must create at least one ticket category before a new ticket can be opened.", - "title": "❌ Can't create ticket" - }, - "select_category": { - "description": "Select the category most relevant to your ticket's topic:\n\n%s", - "title": "🔤 Please select the ticket category" - }, - "select_category_timeout": { - "description": "You took too long to select the ticket category.", - "title": "❌ Reaction time expired" - } - } - }, - "panel": { - "description": "Create a new ticket panel", - "name": "panel", - "response": { - "invalid_category": { - "description": "One or more of the specified category IDs is invalid.", - "title": "❌ Invalid category" - } - } - }, - "remove": { - "description": "Remove a member from a ticket", - "name": "remove", - "response": { - "no_member": { - "description": "Please mention the member you want to remove.", - "title": "❌ Unknown member" - }, - "no_permission": { - "description": "You are not the creator of this ticket and you are not a staff member; you can't remove members from this ticket.", - "title": "❌ Insufficient permission" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ This isn't a ticket channel" - }, - "removed": { - "description": "%s has been removed from %s.", - "title": "✅ Member removed" - } - } - }, - "settings": { - "description": "Configure Discord Tickets", - "name": "settings" - }, - "stats": { - "description": "Display ticket statistics", - "fields": { - "messages": "Messages", - "response_time": { - "minutes": "%s minutes", - "title": "Avg. response time" - }, - "tickets": "Tickets" - }, - "name": "stats", - "response": { - "global": { - "description": "Statistics about tickets across all guilds where this Discord Tickets instance is used.", - "title": "📊 Global stats" - }, - "guild": { - "description": "Statistics about tickets within this guild. This data is cached for an hour.", - "title": "📊 This server's stats" - } - } - }, - "survey": { - "description": "View survey responses", - "name": "survey", - "response": { - "list": { - "title": "📃 Surveys" - } - } - }, - "tag": { - "description": "Use a tag response", - "name": "tag", - "response": { - "error": "❌ Error", - "list": { - "title": "📃 Tag list" - }, - "missing": "This tag requires the following arguments:\n%s", - "not_a_ticket": { - "description": "This tag can only be used within a ticket channel as it uses ticket references.", - "title": "❌ This isn't a ticket channel" - } - } - }, - "topic": { - "description": "Change the topic of the ticket", - "name": "topic", - "response": { - "changed": { - "description": "This ticket's topic has been changed.", - "title": "✅ Topic changed" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel you want to change the topic of.", - "title": "❌ This isn't a ticket channel" - } - } - } - }, - "message_will_be_deleted_in": "This message will be deleted in %d seconds", - "missing_permissions": { - "description": "You do not have the permissions required to use this command:\n%s", - "title": "❌" - }, - "ticket": { - "claimed": { - "description": "%s has claimed this ticket.", - "title": "✅ Ticket claimed" - }, - "closed": { - "description": "This ticket has been closed.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_by_member": { - "description": "This ticket has been closed by %s.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_by_member_with_reason": { - "description": "This ticket has been closed by %s: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_with_reason": { - "description": "This ticket has been closed: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "member_added": { - "description": "%s has been added by %s", - "title": "Member added" - }, - "member_removed": { - "description": "%s has been removed by %s", - "title": "Member removed" - }, - "opening_message": { - "fields": { - "topic": "Topic" - } - }, - "questions": "Please answer the following questions:\n\n%s", - "released": { - "description": "%s has released this ticket.", - "title": "✅ Ticket released" - }, - "survey": { - "complete": { - "description": "Thank you for your feedback.", - "title": "✅ Thank you" - }, - "start": { - "description": "Hey, %s. Before this channel is deleted, would you mind completing a quick %d-question survey? React with ✅ to start, or ignore this message.", - "title": "❔ Feedback" - } - } - } -} diff --git a/src/locales/nl-NL.json b/src/locales/nl-NL.json deleted file mode 100644 index 431b3d7..0000000 --- a/src/locales/nl-NL.json +++ /dev/null @@ -1,402 +0,0 @@ -{ - "blacklisted": "❌ Je staat op de zwarte lijst", - "bot": { - "missing_permissions": { - "description": "Discord Tickets vereist de volgende machtigingen:\n%s", - "title": "⚠️" - }, - "version": "[Discord Tickets] (%s) v%s door [eartharoid](%s)" - }, - "collector_expires_in": "Verloopt over %d seconden", - "command_execution_error": { - "description": "Er is een onverwachte fout opgetreden tijdens de uitvoering van de opdracht.\nVraag een beheerder om de console-uitvoer / logboeken te controleren voor meer informatie.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Een lid toevoegen aan een ticket", - "name": "toevoegen", - "options": { - "member": { - "description": "Het lid is toegevoegd aan het ticket", - "name": "lid" - }, - "ticket": { - "description": "Het ticket om het lid aan toe te voegen", - "name": "ticket" - } - }, - "response": { - "added": { - "description": "%s is toegevoegd aan %s.", - "title": "✅ lid toegevoegd" - }, - "no_member": { - "description": "Vermeld het lid dat u wilt toevoegen.", - "title": "❌ Onbekend lid" - }, - "no_permission": { - "description": "U bent niet de maker van deze ticket en u bent geen werknemer, je kunt geen leden toevoegen aan deze ticket.", - "title": "❌ Onvoldoende rechten" - }, - "not_a_ticket": { - "description": "Gebruik deze commando in het ticketkanaal of vermeld het kanaal.", - "title": "❌ Dit is geen ticketkanaal" - } - } - }, - "blacklist": { - "description": "Bekijk of pas de zwarte lijst aan", - "name": "Blacklist", - "options": { - "add": { - "description": "Een member of rol aan de zwarte lijst", - "name": "toevoegen", - "options": { - "member_or_role": { - "description": "Een lid of rol toevoegen aan de zwarte lijst", - "name": "member_of_rol" - } - } - }, - "remove": { - "description": "Verwijder een lid of rol van de zwarte lijst", - "name": "verwijder", - "options": { - "member_or_role": { - "description": "Een lid of rol verwijderen van de zwarte lijst", - "name": "lid_of_rol" - } - } - }, - "show": { - "description": "Laat de leden en rollen zien die lid zijn van de zwarte lijst", - "name": "zien" - } - }, - "response": { - "empty_list": { - "description": "Er zijn geen leden of rollen op de zwarte lijst. Type '%sblacklist ' om een lid of rol aan de zwarte lijst toe te voegen.", - "title": "📃 leden en rollen op de blacklist" - }, - "illegal_action": { - "description": "%s is een personeelslid en kan niet op de blacklist worden geplaatst.", - "title": "❌ Je kunt dit lid niet op de blacklist zetten" - }, - "invalid": { - "description": "Dit lid of rol kan niet van de zwarte lijst worden gehaald omdat ze er niet op staan.", - "title": "❌ Fout" - }, - "list": { - "fields": { - "members": "Leden", - "roles": "Rollen" - }, - "title": "📃 leden en rollen op de blacklist" - }, - "member_added": { - "description": "<@%s> is toegevoegd aan de blacklist. Ze kunnen niet langer communiceren met de bot.", - "title": "✅ Lid toegevoegd aan blacklist" - }, - "member_removed": { - "description": "<@%s> is van de blacklist verwijderd. Ze kunnen de bot nu weer gebruiken.", - "title": "✅ Lid van blacklist verwijderd" - }, - "role_added": { - "description": "<@&%s> is toegevoegd aan de blacklist. Leden met deze rol kunnen niet langer communiceren met de bot.", - "title": "✅ Rol toegevoegd aan blacklist" - }, - "role_removed": { - "description": "<@&%s> is van de zwartelijst verwijderd. Leden met deze rol kunnen de bot nu opnieuw gebruiken.", - "title": "✅ Rol van blacklist verwijderd" - } - } - }, - "close": { - "description": "Een ticketkanaal sluiten", - "name": "sluiten", - "options": { - "reason": { - "description": "De reden waarom je dit/deze ticket(s) sluit", - "name": "reden" - }, - "ticket": { - "description": "Het ticket op te sluiten, of het nummer of het channel ID", - "name": "ticket" - }, - "time": { - "description": "Sluit alle tickets inactieve tickets voor een gespecificieerde tijd", - "name": "tijd" - } - }, - "response": { - "canceled": { - "description": "Je hebt de opdracht geannuleerd", - "title": "🚫 Geannuleerd" - }, - "closed": { - "description": "Ticket #%s is gesloten.", - "title": "✅ Ticket gesloten" - }, - "closed_multiple": { - "description": [ - "%d ticket is gesloten.", - "%d tickets zijn gesloten." - ], - "title": [ - "✅ Ticket gesloten", - "✅ Tickets gesloten" - ] - }, - "confirm": { - "buttons": { - "cancel": "Annuleren", - "confirm": "Sluiten" - }, - "description": "Reageer met ✅ om dit ticket af te sluiten.", - "description_with_archive": "U kunt er daarna een gearchiveerde versie van bekijken. Reageer met ✅ om dit ticket af te sluiten.", - "title": "❔ Weet je het zeker?" - }, - "confirm_multiple": { - "buttons": { - "cancel": "Annuleren", - "confirm": [ - "Sluit %d ticket", - "Sluit %d tickets" - ] - }, - "description": [ - "Reageer met ✅ om %d ticket af te sluiten.", - "Reageer met ✅ om %d tickets af te sluiten." - ], - "title": "❔ Weet je het zeker?" - }, - "confirmation_timeout": { - "description": "Je deed er te lang over om het te bevestigen.", - "title": "❌ Interactietijd verstreken" - }, - "invalid_time": { - "description": "De opgegeven periode kan niet worden geparseerd.", - "title": "❌ ongeldige invoer" - }, - "no_permission": { - "description": "Je bent geen stafflid of eigenaar van dit ticket.", - "title": "❌ Onvoldoende rechten" - }, - "no_tickets": { - "description": "Er zijn geen tickets die inactief zijn geweest voor deze periode.", - "title": "❌ Geen tickets om te sluiten" - }, - "not_a_ticket": { - "description": "Gebruik deze commando in een ticketkanaal of gebruik de ticket emoji.\nType '%shelp sluiten' voor meer informatie.", - "title": "❌ Dit is geen ticketkanaal" - }, - "unresolvable": { - "description": "'%s' kon niet worden omgezet in een ticket. Geef het ticket-ID/vermelding of nummer op.", - "title": "❌ fout" - } - } - }, - "help": { - "description": "Opdrachten weergeven waar u toegang toe heeft, of meer informatie over een opdracht", - "name": "help", - "response": { - "list": { - "description": "De commandos waar u toegang toe hebt, worden hieronder vermeld. Typ '{prefix}help [command]' voor meer informatie over een commando. Als u een ticket wilt maken, typt u '{prefix}new [topic]'.", - "fields": { - "commands": "Commandos" - }, - "title": "❔ Help" - } - } - }, - "new": { - "description": "Een nieuw ticket maken", - "name": "new", - "request_topic": { - "description": "Geef in een paar woorden kort aan waar deze ticket over gaat.", - "title": "Ticket onderwerp" - }, - "response": { - "created": { - "description": "Uw ticket is aangemaakt: %s.", - "title": "✅ ticket gemaakt" - }, - "error": { - "title": "❌ fout" - }, - "has_a_ticket": { - "description": "Gebruik uw bestaande ticket (<#%s>) of sluit uw oude ticket voordat u een ander ticket maakt.", - "title": "❌ Je hebt al een ticket open staan" - }, - "max_tickets": { - "description": "Gebruik '%ssluiten' om onnodige tickets te sluiten.\n\n%s", - "title": "❌ Je hebt al %d geopende tickets" - }, - "no_categories": { - "description": "Een serverbeheerder moet ten minste één ticketcategorie maken voordat er een nieuw ticket kan worden geopend.", - "title": "❌ Kan geen ticket maken" - }, - "select_category": { - "description": "Selecteer de categorie die het meest relevant is voor het onderwerp van uw ticket:\n\n%s", - "title": "🔤 Selecteer de ticketcategorie" - }, - "select_category_timeout": { - "description": "Het duurde te lang om de ticketcategorie te selecteren.", - "title": "❌ Reactietijd verstreken" - } - } - }, - "panel": { - "description": "Een nieuw ticketpaneel maken", - "name": "paneel", - "response": { - "invalid_category": { - "description": "Een of meer van de opgegeven categorie-ID's zijn ongeldig.", - "title": "❌ ongeldige categorie" - } - } - }, - "remove": { - "description": "Een lid uit een ticket verwijderen", - "name": "verwijderen", - "response": { - "no_member": { - "description": "Vermeld het lid dat u wilt verwijderen.", - "title": "❌ Onbekend lid" - }, - "no_permission": { - "description": "U bent niet de maker van dit ticket en u bent geen werknemer, je kunt geen leden van deze ticket verwijderen.", - "title": "❌ Onvoldoende rechten" - }, - "not_a_ticket": { - "description": "Gebruik deze commando in het ticketkanaal of vermeld het kanaal.", - "title": "❌ Dit is geen ticketkanaal" - }, - "removed": { - "description": "%s is verwijderd uit %s.", - "title": "✅ lid verwijderd" - } - } - }, - "settings": { - "description": "Discord-tickets configureren", - "name": "instellingen" - }, - "stats": { - "description": "Ticketstatistieken weergeven", - "fields": { - "messages": "Berichten", - "response_time": { - "minutes": "%s minuten", - "title": "Gemiddelde reactietijd" - }, - "tickets": "Tickets" - }, - "name": "statistieken", - "response": { - "global": { - "description": "Statistieken over tickets in alle gilden waar dit Discord Tickets-exemplaar wordt gebruikt.", - "title": "📊 Wereldwijde statistieken" - }, - "guild": { - "description": "Statistieken over tickets binnen deze gilde. Deze gegevens worden een uur in de cache opgeslagen.", - "title": "📊 De statistieken van deze server" - } - } - }, - "survey": { - "description": "Enquêtereacties weergeven", - "name": "enquête", - "response": { - "list": { - "title": "📃 enquêtes" - } - } - }, - "tag": { - "description": "Een tagrespons gebruiken", - "name": "label", - "response": { - "error": "❌ fout", - "list": { - "title": "📃 Tag lijst" - }, - "missing": "Deze tag vereist de volgende argumenten:\n%s", - "not_a_ticket": { - "description": "Deze tag kan alleen worden gebruikt binnen een ticketkanaal omdat deze ticketreferenties gebruikt.", - "title": "❌ Dit is geen ticketkanaal" - } - } - }, - "topic": { - "description": "Het onderwerp van het ticket wijzigen", - "name": "onderwerp", - "response": { - "changed": { - "description": "Het onderwerp van dit ticket is gewijzigd.", - "title": "✅ Onderwerp gewijzigd" - }, - "not_a_ticket": { - "description": "Gebruik deze opdracht in het ticketkanaal waarvan u het onderwerp wilt wijzigen.", - "title": "❌ Dit is geen ticketkanaal" - } - } - } - }, - "message_will_be_deleted_in": "Dit bericht wordt binnen %d seconden verwijderd", - "missing_permissions": { - "description": "U beschikt niet over de machtigingen die nodig zijn om deze opdracht te gebruiken:\n%s", - "title": "❌" - }, - "ticket": { - "claimed": { - "description": "%s heeft dit ticket opgeëist.", - "title": "✅ Ticket geclaimd" - }, - "closed": { - "description": "Deze ticket is gesloten.\nHet kanaal wordt binnen 5 seconden verwijderd.", - "title": "✅ Ticket gesloten" - }, - "closed_by_member": { - "description": "Deze ticket is gesloten door %s.\nHet kanaal wordt binnen 5 seconden verwijderd.", - "title": "✅ Ticket gesloten" - }, - "closed_by_member_with_reason": { - "description": "Deze ticket is gesloten door %s: '%s'\nHet kanaal wordt binnen 5 seconden verwijderd.", - "title": "✅ Ticket gesloten" - }, - "closed_with_reason": { - "description": "Deze ticket is gesloten: '%s'\nHet kanaal wordt binnen 5 seconden verwijderd.", - "title": "✅ Ticket gesloten" - }, - "member_added": { - "description": "%s is toegevoegd door %s", - "title": "Lid toegevoegd" - }, - "member_removed": { - "description": "%s is verwijderd door %s", - "title": "Lid verwijderd" - }, - "opening_message": { - "fields": { - "topic": "Onderwerp" - } - }, - "questions": "Beantwoord de volgende vragen:\n\n%s", - "released": { - "description": "%s heeft deze ticket vrijgegeven.", - "title": "✅ Ticket vrijgegeven" - }, - "survey": { - "complete": { - "description": "Bedankt voor uw feedback.", - "title": "✅ Dank u" - }, - "start": { - "description": "Hey %s. Voordat dit kanaal wordt verwijderd, wilt u een snelle %d-vragen enquête invullen? Reageer met ✅ om dit bericht te starten of negeer dit bericht.", - "title": "❔ Feedback" - } - } - } -} diff --git a/src/locales/no-NO.json b/src/locales/no-NO.json deleted file mode 100644 index da2533f..0000000 --- a/src/locales/no-NO.json +++ /dev/null @@ -1,324 +0,0 @@ -{ - "bot": { - "missing_permissions": { - "description": "Discord-billetter krever følgende tilganger:\n%s", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s av [eartharoid](%s)" - }, - "collector_expires_in": "Utløper om %d sekunder", - "command_execution_error": { - "description": "Det oppstod en uventet feil under kommandoutførelsen.\nBe en administrator kontrollere konsollutdataene/-loggene for mer informasjon.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Add a member to a ticket", - "name": "legg til", - "response": { - "added": { - "description": "%s har blitt lagt til i %s.", - "title": "✅ Medlem lagt til" - }, - "no_member": { - "description": "Nevn medlemmet du ønsker å legge til.", - "title": "❌ Ukjent medlem" - }, - "no_permission": { - "description": "You are not the creator of this ticket and you are not a staff member; you can't add members to this ticket.", - "title": "❌ Utilstrekkelige tilganger" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ This isn't a ticket channel" - } - } - }, - "blacklist": { - "description": "Svarteliste/fjerne fra svarteliste et medlem fra og bruke denne botten", - "name": "svarteliste", - "response": { - "empty_list": { - "description": "Det er ingen medlemmer eller roller svartelistet. Skriv `%sblacklist ` for og legge til et medlem eller rolle til svartelisten.", - "title": "📃 Svartelistet medlemmer og roller" - }, - "illegal_action": { - "description": "%s er en moderator og kan ikke bli svartelistet.", - "title": "❌ Du kan ikke svarteliste dette medlemmet" - }, - "list": { - "title": "📃 Svartelistet medlemmer og roller" - }, - "member_added": { - "description": "<@%s> har blitt lagt til i svartelisten. De kan ikke lengre bruke botten.", - "title": "✅ La til medlemmet i svartelisten" - }, - "member_removed": { - "description": "<@%s> har blitt fjernet fra svartelisten. De kan nå bruke botten igjen.", - "title": "✅ Fjernet medlemmet fra svartelisten" - }, - "role_added": { - "description": "<@&%s> er lagt til i svartelisten. Medlemmer med denne rollen vil ikke lenger kunne bruke botten.", - "title": "✅ La til rollen i svartelisten" - }, - "role_removed": { - "description": "<@&%s> er fjernet fra svartelisten. Medlemmer med denne rollen kan nå bruke botten igjen.", - "title": "✅ Fjernet rollen fra svartelisten" - } - } - }, - "close": { - "description": "Lukke en billettkanal", - "name": "lukk", - "response": { - "closed": { - "description": "Billett #%s har blitt stengt.", - "title": "✅ Billetten er stengt" - }, - "closed_multiple": { - "description": [ - "%d billett har blitt stengt.", - "%d billetter har blitt stengt." - ], - "title": [ - "✅ Billetten er stengt", - "✅ Billetter lukket" - ] - }, - "confirm": { - "description": "Reager med ✅ for å stenge denne billetten.", - "description_with_archive": "You will be able to view an archived version of it after.\nReact with ✅ to close this ticket.", - "title": "❔ Er du sikker?" - }, - "confirm_multiple": { - "description": [ - "Reager med ✅ for å stenge %d billett.", - "Reager med ✅ for å stenge %d billetter." - ], - "title": "❔ Er du sikker?" - }, - "confirmation_timeout": { - "description": "Du brukte for lang tid på å bekrefte.", - "title": "❌ Reaksjonstiden har utløpt" - }, - "invalid_time": { - "description": "The time period provided could not be parsed.", - "title": "❌ Ugyldig inndata" - }, - "no_tickets": { - "description": "There are no tickets which have been inactive for this time period.", - "title": "❌ Ingen billetter å lukke" - }, - "not_a_ticket": { - "description": "Please use this command in a ticket channel or use the ticket flag.\nType `%shelp close` for more information.", - "title": "❌ Dette er ikke en billett-kanal" - }, - "unresolvable": { - "description": "`%s` could not be resolved to a ticket. Please provide the ticket ID/mention or number.", - "title": "❌ Feil" - } - } - }, - "help": { - "description": "List commands you have access to, or find out more about a command", - "name": "hjelp", - "response": { - "list": { - "description": "The commands you have access to are listed below. For more information about a command, type `{prefix}help [command]`. To create a ticket, type `{prefix}new [topic]`.", - "fields": { - "commands": "Kommandoer" - }, - "title": "❔ Hjelp" - } - } - }, - "new": { - "description": "Opprett en ny billett", - "name": "ny", - "request_topic": { - "description": "Vennligst oppgi kort hva denne billetten handler om med noen få ord.", - "title": "Billettemne" - }, - "response": { - "created": { - "description": "Billetten er opprettet: %s.", - "title": "✅ Billetten er opprettet" - }, - "error": { - "title": "❌ Feil" - }, - "has_a_ticket": { - "description": "Bruk din eksisterende billett ( <#%s>) eller lukk den før du oppretter en ny.", - "title": "❌ Du har allerede en åpen billett" - }, - "max_tickets": { - "description": "Bruk «%sclose» for å lukke unødvendige billetter.\n\n%s", - "title": "❌ Du har allerede %d åpne billetter" - }, - "no_categories": { - "description": "En tjeneradministrator må opprette minst én billettkategori før en ny billett kan åpnes.", - "title": "❌ Kan ikke opprette billett" - }, - "select_category": { - "description": "Velg kategorien som er mest relevant for billettens emne:\n\n%s", - "title": "🔤 Velg billettkategori" - }, - "select_category_timeout": { - "description": "Det tok for lang tid å velge billettkategorien.", - "title": "❌ Reaksjonstiden har utløpt" - } - } - }, - "panel": { - "description": "Create a new ticket panel", - "name": "panel", - "response": { - "invalid_category": { - "description": "One or more of the specified category IDs is invalid.", - "title": "❌ Ugyldig kategori" - } - } - }, - "remove": { - "description": "Fjern et medlem fra en billett", - "name": "fjern", - "response": { - "no_member": { - "description": "Nevn medlemmet du ønsker å fjerne.", - "title": "❌ Ukjent medlem" - }, - "no_permission": { - "description": "You are not the creator of this ticket and you are not a staff member; you can't remove members from this ticket.", - "title": "❌ Utilstrekkelige tilganger" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ Dette er ikke en billettkanal" - }, - "removed": { - "description": "%s har blitt fjernet fra %s.", - "title": "✅ Medlem fjernet" - } - } - }, - "settings": { - "description": "Sett opp Discord-billetter", - "name": "innstillinger" - }, - "stats": { - "description": "Vis billett-statistikk", - "fields": { - "messages": "Meldinger", - "response_time": { - "minutes": "%s minutter", - "title": "Avg. response time" - }, - "tickets": "Billetter" - }, - "name": "statistikk", - "response": { - "global": { - "description": "Statistics about tickets across all guilds where this Discord Tickets instance is used.", - "title": "📊 Global stats" - }, - "guild": { - "description": "Statistics about tickets within this guild. This data is cached for an hour.", - "title": "📊 This server's stats" - } - } - }, - "survey": { - "description": "View survey responses", - "name": "survey", - "response": { - "list": { - "title": "📃 Surveys" - } - } - }, - "tag": { - "description": "Use a tag response", - "name": "tag", - "response": { - "error": "❌ Error", - "list": { - "title": "📃 Tag list" - }, - "missing": "This tag requires the following arguments:\n%s", - "not_a_ticket": { - "description": "This tag can only be used within a ticket channel as it uses ticket references.", - "title": "❌ Dette er ikke en billettkanal" - } - } - }, - "topic": { - "description": "Change the topic of the ticket", - "name": "emne", - "response": { - "changed": { - "description": "This ticket's topic has been changed.", - "title": "✅ Emne endret" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel you want to change the topic of.", - "title": "❌ Dette er ikke en billettkanal" - } - } - } - }, - "message_will_be_deleted_in": "Denne meldingen vil bli slettet om %d sekunder", - "missing_permissions": { - "description": "You do not have the permissions required to use this command:\n%s", - "title": "❌" - }, - "ticket": { - "claimed": { - "description": "%s has claimed this ticket.", - "title": "✅ Ticket claimed" - }, - "closed": { - "description": "This ticket has been closed.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Billetten er stengt" - }, - "closed_by_member": { - "description": "This ticket has been closed by %s.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Billetten er stengt" - }, - "closed_by_member_with_reason": { - "description": "This ticket has been closed by %s: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Billetten er stengt" - }, - "closed_with_reason": { - "description": "This ticket has been closed: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Billetten er stengt" - }, - "member_added": { - "description": "%s has been added by %s", - "title": "Member added" - }, - "member_removed": { - "description": "%s has been removed by %s", - "title": "Member removed" - }, - "opening_message": { - "fields": { - "topic": "Emne" - } - }, - "questions": "Please answer the following questions:\n\n%s", - "released": { - "description": "%s has released this ticket.", - "title": "✅ Ticket released" - }, - "survey": { - "complete": { - "description": "Takk for din tilbakemelding.", - "title": "✅ Takk" - }, - "start": { - "description": "Hey, %s. Before this channel is deleted, would you mind completing a quick %d-question survey? React with ✅ to start, or ignore this message.", - "title": "❔ Tilbakemelding" - } - } - } -} diff --git a/src/locales/pl-PL.json b/src/locales/pl-PL.json deleted file mode 100644 index 952dbc5..0000000 --- a/src/locales/pl-PL.json +++ /dev/null @@ -1,348 +0,0 @@ -{ - "blacklisted": "❌Jesteś na czarnej liście", - "bot": { - "missing_permissions": { - "description": "DiscordTickets potrzebuje tych uprawnień:\n%s", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s by [eartharoid](%s)" - }, - "collector_expires_in": "Wygasa w ciągu %d sekund", - "command_execution_error": { - "description": "Wystąpił nieoczekiwany błąd podczas wykonywania polecenia.\nPoproś administratora o sprawdzenie konsoli / logów w celu uzyskania szczegółów.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Dodaj użytkownika do ticketu", - "name": "dodaj", - "response": { - "added": { - "description": "%s został dodany do %s.", - "title": "✅ Użytkownik dodany" - }, - "no_member": { - "description": "Wspomnij o osobie, którą chcesz dodać.", - "title": "❌ Nieznany użytkownik" - }, - "no_permission": { - "description": "Nie jesteś twórca tego ticketum, ani nie jesteś administratorem. Nie możesz dodać osób do tego ticketu.", - "title": "❌ Niewystarczające uprawnienia" - }, - "not_a_ticket": { - "description": "Użyj tej komendy na kanale ticketu lub wspomnij o kanale.", - "title": "❌ To nie jest kanał ticketu" - } - } - }, - "blacklist": { - "description": "Wyświetl lub zmodyfikuj czarną listę", - "name": "blacklist", - "options": { - "show": { - "description": "Pokaż użytkowników i role na czarnej liście", - "name": "pokaż" - } - }, - "response": { - "empty_list": { - "description": "Nie ma żadnych osób i roli, które są zblacklistowane. Wpisz `%sblacklist `, aby dodać osobę lub role do blacklisty.", - "title": "📃 osoby i role, które są zblacklistowane" - }, - "illegal_action": { - "description": "%s jest administratorem i nie może zostać zblacklistowany.", - "title": "❌ Nie możesz zblacklistować tej osoby" - }, - "invalid": { - "title": "❌ Błąd" - }, - "list": { - "title": "📃 osoby i role, które są zblacklistowane" - }, - "member_added": { - "description": "<@%s> została dodana do czarnej listy. Nie będą już mogli wchodzić w interakcje z botem.", - "title": "✅ Dodano osobę do czarnej listy" - }, - "member_removed": { - "description": "<@%s> został usunięty z czarnej listy. Teraz mogą ponownie użyć bota.", - "title": "✅ Osoba została usunięta z blacklisty" - }, - "role_added": { - "description": "<@&%s> została dodana do blacklisty. Osoby z tej roli nie będą już mogli wchodzić w interakcje z botem.", - "title": "✅ Dodano rolę do blacklisty" - }, - "role_removed": { - "description": "<@&%s> został usunięty z blacklisty. Osoby z tej roli mogą teraz użyć bota ponownie.", - "title": "✅ Usunięto rolę z blacklisty" - } - } - }, - "close": { - "description": "Close a ticket channel", - "name": "zamknij", - "options": { - "reason": { - "name": "powód" - } - }, - "response": { - "canceled": { - "title": "🚫 Anulowane" - }, - "closed": { - "description": "Ticket #%s has been closed.", - "title": "✅ Ticket closed" - }, - "closed_multiple": { - "description": [ - "%d ticket has been closed.", - "%d tickets have been closed." - ], - "title": [ - "✅ Ticket zamknięty", - "✅ Tickets closed" - ] - }, - "confirm": { - "buttons": { - "cancel": "Anuluj" - }, - "description": "Proszę potwierdź swoją decyzję.", - "description_with_archive": "You will be able to view an archived version of it after.\nReact with ✅ to close this ticket.", - "title": "❔ Jesteś pewien?" - }, - "confirm_multiple": { - "description": [ - "React with ✅ to close %d ticket.", - "React with ✅ to close %d tickets." - ], - "title": "❔ Jesteś pewien?" - }, - "confirmation_timeout": { - "description": "You took too long to confirm.", - "title": "❌ Reaction time expired" - }, - "invalid_time": { - "description": "The time period provided could not be parsed.", - "title": "❌ Invalid input" - }, - "no_permission": { - "title": "❌ Niewystarczające pozwolenie" - }, - "no_tickets": { - "description": "There are no tickets which have been inactive for this time period.", - "title": "❌ No tickets to close" - }, - "not_a_ticket": { - "description": "Please use this command in a ticket channel or use the ticket flag.\nType `%shelp close` for more information.", - "title": "❌ To nie jest kanał ticketu" - }, - "unresolvable": { - "description": "`%s` could not be resolved to a ticket. Please provide the ticket ID/mention or number.", - "title": "❌ Error" - } - } - }, - "help": { - "description": "List commands you have access to, or find out more about a command", - "name": "help", - "response": { - "list": { - "description": "The commands you have access to are listed below. For more information about a command, type `{prefix}help [command]`. To create a ticket, type `{prefix}new [topic]`.", - "fields": { - "commands": "Komendy" - }, - "title": "❔ Help" - } - } - }, - "new": { - "description": "Create a new ticket", - "name": "new", - "request_topic": { - "description": "Proszę krótko opisać ten ticket.", - "title": "Temat ticketu" - }, - "response": { - "created": { - "description": "Your ticket has been created: %s.", - "title": "✅ Ticket utworzony" - }, - "error": { - "title": "❌ Błąd" - }, - "has_a_ticket": { - "description": "Użyj istniejącego ticketu <#(%s>) lub zamknij go przed utworzeniem innego.", - "title": "❌ Masz już otwarty ticket" - }, - "max_tickets": { - "description": "Użyj `%sclose`, aby zamknąć niepotrzebne tickety.\n\n%s", - "title": "❌ Masz już %d otwartych ticketów" - }, - "no_categories": { - "description": "Administrator serwera musi utworzyć co najmniej jedną kategorię ticketu przed otwarciem nowego.", - "title": "❌ Nie można stworzyć ticketu" - }, - "select_category": { - "description": "Wybierz kategorię najbardziej odpowiednią dla tematu ticketu:\n\n%s", - "title": "🔤 Proszę wybrać kategorię ticketu" - }, - "select_category_timeout": { - "description": "Wybór kategorii ticketu trwał zbyt długo.", - "title": "❌ Reaction time expired" - } - } - }, - "panel": { - "description": "Tworzenie nowego panelu ticketów", - "name": "panel", - "response": { - "invalid_category": { - "description": "Co najmniej jeden z określonych ID kategorii jest nieprawidłowy.", - "title": "❌ Nieprawidłowa kategoria" - } - } - }, - "remove": { - "description": "Usuń osobę z ticketu", - "name": "usuń", - "response": { - "no_member": { - "description": "Proszę wspomnieć o osobie, którą chcesz usunąć.", - "title": "❌ Nieznany użytkownik" - }, - "no_permission": { - "description": "Nie jesteś twórcą tego biletu i nie jesteś administratorem; nie można usunąć osób z tego ticketu.", - "title": "❌ Niewystarczające uprawnienia" - }, - "not_a_ticket": { - "description": "Użyj tej komendy na kanale ticketu lub wspomnij o kanale.", - "title": "❌ To nie jest kanał ticketu" - }, - "removed": { - "description": "%s został usunięty z %s.", - "title": "✅ osoba usunięta" - } - } - }, - "settings": { - "description": "Configure Discord Tickets", - "name": "settings" - }, - "stats": { - "description": "Display ticket statistics", - "fields": { - "messages": "Messages", - "response_time": { - "minutes": "%s minutes", - "title": "Avg. response time" - }, - "tickets": "Tickets" - }, - "name": "stats", - "response": { - "global": { - "description": "Statistics about tickets across all guilds where this Discord Tickets instance is used.", - "title": "📊 Global stats" - }, - "guild": { - "description": "Statistics about tickets within this guild. This data is cached for an hour.", - "title": "📊 This server's stats" - } - } - }, - "survey": { - "description": "View survey responses", - "name": "survey", - "response": { - "list": { - "title": "📃 Surveys" - } - } - }, - "tag": { - "description": "Use a tag response", - "name": "tag", - "response": { - "error": "❌ Error", - "list": { - "title": "📃 Tag list" - }, - "missing": "This tag requires the following arguments:\n%s", - "not_a_ticket": { - "description": "This tag can only be used within a ticket channel as it uses ticket references.", - "title": "❌ To nie jest kanał ticketu" - } - } - }, - "topic": { - "description": "Change the topic of the ticket", - "name": "topic", - "response": { - "changed": { - "description": "This ticket's topic has been changed.", - "title": "✅ Topic changed" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel you want to change the topic of.", - "title": "❌ To nie jest kanał ticketu" - } - } - } - }, - "message_will_be_deleted_in": "This message will be deleted in %d seconds", - "missing_permissions": { - "description": "You do not have the permissions required to use this command:\n%s", - "title": "❌" - }, - "ticket": { - "claimed": { - "description": "%s has claimed this ticket.", - "title": "✅ Ticket claimed" - }, - "closed": { - "description": "This ticket has been closed.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_by_member": { - "description": "This ticket has been closed by %s.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_by_member_with_reason": { - "description": "This ticket has been closed by %s: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_with_reason": { - "description": "This ticket has been closed: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "member_added": { - "description": "%s has been added by %s", - "title": "Member added" - }, - "member_removed": { - "description": "%s has been removed by %s", - "title": "Member removed" - }, - "opening_message": { - "fields": { - "topic": "Topic" - } - }, - "questions": "Please answer the following questions:\n\n%s", - "released": { - "description": "%s has released this ticket.", - "title": "✅ Ticket released" - }, - "survey": { - "complete": { - "description": "Thank you for your feedback.", - "title": "✅ Thank you" - }, - "start": { - "description": "Hey, %s. Before this channel is deleted, would you mind completing a quick %d-question survey? React with ✅ to start, or ignore this message.", - "title": "❔ Feedback" - } - } - } -} diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json deleted file mode 100644 index 41074ae..0000000 --- a/src/locales/pt-BR.json +++ /dev/null @@ -1,324 +0,0 @@ -{ - "bot": { - "missing_permissions": { - "description": "O Bot necessita das seguintes permissões:\n%s", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s feito por [eartharoid](%s)" - }, - "collector_expires_in": "Expira em %d segundos", - "command_execution_error": { - "description": "Ocorreu um erro inesperado durante a execução do comando. \nPor favor, peça a um administrador para verificar os logs do console para obter detalhes.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Adicionar um usuário ao ticket", - "name": "adicionar", - "response": { - "added": { - "description": "%s foi adicionado ao ticket %s.", - "title": "✅ Membro adicionado" - }, - "no_member": { - "description": "Por favor, mencione o membro que quer adicionar.", - "title": "❌ Membro não encontrado" - }, - "no_permission": { - "description": "Você não é o criador deste ticket e não é um membro da staff; você não pode adicionar membros a este ticket.", - "title": "❌ Você não tem permissão" - }, - "not_a_ticket": { - "description": "Use este comando no canal do ticket ou mencione o canal.", - "title": "❌ Este não é um canal de ticket" - } - } - }, - "blacklist": { - "description": "Blacklist/unblacklist um membro de interagir com o bot", - "name": "blacklist", - "response": { - "empty_list": { - "description": "Não há membros ou cargos na blacklist. Digite '%sblacklist ' para adicionar um membro ou cargo à blacklist.", - "title": "📃 Utilizadores e cargos na blacklist" - }, - "illegal_action": { - "description": "%s é um membro da staff e não pode ser colocado na blacklist.", - "title": "❌ Você não pode colocar este membro na blacklist" - }, - "list": { - "title": "📃 Usuários da blacklist" - }, - "member_added": { - "description": "<@%s> foi adicionado à blacklist. Eles não serão mais capazes de interagir com o bot.", - "title": "✅ Membro adicionado à blacklist" - }, - "member_removed": { - "description": "<@%s> foi removido da blacklist. Agora eles podem usar o bot novamente.", - "title": "✅ Membro removido da lista negra" - }, - "role_added": { - "description": "<@&%s> foi adicionado à blacklist. Eles não serão mais capazes de interagir com o bot.", - "title": "✅ Cargo adicionado à blacklist" - }, - "role_removed": { - "description": "<@&%s> foi removido da blacklist. Membros com esta função agora podem usar o bot novamente.", - "title": "✅ Cargo removido da lista negra" - } - } - }, - "close": { - "description": "Feche um canal de ticket", - "name": "fechar", - "response": { - "closed": { - "description": "Ticket #%s foi fechado.", - "title": "✅ Ticket fechado" - }, - "closed_multiple": { - "description": [ - "%d ticket foi fechado", - "%d tickets foram fechados" - ], - "title": [ - "✅ Ticket fechado", - "✅ Tickets fechados" - ] - }, - "confirm": { - "description": "Reaja com ✅ para confirmar", - "description_with_archive": "Você poderá ver uma versão arquivada dele depois.\nReaja com ✅ para fechar o ticket", - "title": "❔ Deseja realmente fechar o ticket?" - }, - "confirm_multiple": { - "description": [ - "Reaja com ✅ para fechar %d ticket.", - "Reaja com ✅ para fechar %d tickets." - ], - "title": "❔ Deseja realmente fechar o ticket?" - }, - "confirmation_timeout": { - "description": "Você demorou muito para confirmar.", - "title": "❌ Tempo de reação expirado" - }, - "invalid_time": { - "description": "O período de tempo não pode ser análisado", - "title": "❌ Entrada inválida" - }, - "no_tickets": { - "description": "Não há tickets inativos neste período.", - "title": "❌ Nenhum ticket para fechar" - }, - "not_a_ticket": { - "description": "Use este comando em um canal de ticket ou use o sinalizador de ticket.\nUse `%shelp close` para mais informações", - "title": "❌ Este não é um canal de ticket" - }, - "unresolvable": { - "description": "`%s` não pôde ser resolvido para um ticket. Forneça a ID / menção ou número do ticket.", - "title": "❌ Erro" - } - } - }, - "help": { - "description": "Lista de comandos que tens acesso, ou encontra mais informações sobre um comando", - "name": "ajuda", - "response": { - "list": { - "description": "Os comandos que tens acesso estão os que estão abaixo. Para mais informações acerca de um comando, escreve `{prefix}ajuda [comando]`. Para criar um ticket, escreve `{prefix}novo [motivo]`.", - "fields": { - "commands": "Comandos" - }, - "title": "❔ Ajuda" - } - } - }, - "new": { - "description": "Criar um novo Ticket", - "name": "novo", - "request_topic": { - "description": "Por favor, descreva resumidamente do que se trata este ticket em poucas palavras.", - "title": "Assunto do Ticket" - }, - "response": { - "created": { - "description": "Seu ticket foi criado: %s.", - "title": "✅ Ticket criado" - }, - "error": { - "title": "❌ Erro" - }, - "has_a_ticket": { - "description": "Por favor, use o seu ticket existente (<#%s>) ou feche-o antes de criar outro.", - "title": "❌ Você já tem um ticket aberto" - }, - "max_tickets": { - "description": "Use `%sclose` para fechar todos os tickets desnecessários.\n\n%s", - "title": "❌ Você já tem %d tickets abertos" - }, - "no_categories": { - "description": "Um administrador do servidor deve criar pelo menos uma categoria de ticket antes que um novo ticket possa ser aberto.", - "title": "❌ Não é possível criar ticket" - }, - "select_category": { - "description": "Selecione a categoria mais relevante para o tópico do seu ticket:\n\n%s", - "title": "🔤 Selecione a categoria do ticket" - }, - "select_category_timeout": { - "description": "Você demorou muito para selecionar a categoria do ticket.", - "title": "❌ Tempo de reação expirado" - } - } - }, - "panel": { - "description": "Cria um novo painel de tickets", - "name": "painel", - "response": { - "invalid_category": { - "description": "Um ou mais IDs de categorias são inválidos.", - "title": "❌ Categoria Inválida" - } - } - }, - "remove": { - "description": "Remove um mebro de um ticket", - "name": "remover", - "response": { - "no_member": { - "description": "Por favor menciona o membro que queres remover.", - "title": "❌ Membro não encontrado" - }, - "no_permission": { - "description": "Não és o criador do ticket nem um mebro da staff; não consegues remover membros deste ticket.", - "title": "❌ Você não tem permissões" - }, - "not_a_ticket": { - "description": "Use este comando no canal do ticket ou mencione o canal.", - "title": "❌ Este não é um canal de tickets" - }, - "removed": { - "description": "%s foi removido de %s.", - "title": "✅ Membro removido" - } - } - }, - "settings": { - "description": "Configurar o BOT", - "name": "settings" - }, - "stats": { - "description": "Mostra as estatísticas dos tickets", - "fields": { - "messages": "Mensagens", - "response_time": { - "minutes": "%s minutos", - "title": "Tempo médio de resposta" - }, - "tickets": "Tickets" - }, - "name": "Estatísticas", - "response": { - "global": { - "description": "Estatísticas sobre tickets em todos os servidores onde esta instância do bot de tickets é usada.", - "title": "📊 Estatísticas globais" - }, - "guild": { - "description": "Estatísticas acerca dos tickets neste servidor. Esta informação é guardada na cache por 1 hora.", - "title": "📊 Estatísticas deste servidor" - } - } - }, - "survey": { - "description": "Vê respostas dos questionários", - "name": "questionario", - "response": { - "list": { - "title": "📃 Questionários" - } - } - }, - "tag": { - "description": "Usa uma resposta de tag", - "name": "tag", - "response": { - "error": "❌ Erro", - "list": { - "title": "📃 Lista de Tags" - }, - "missing": "Esta tag necessita dos seguintes argumentos:\n%s", - "not_a_ticket": { - "description": "Esta tag só pode ser usada num ticket pois usa referências do mesmo.", - "title": "❌ Este não é um canal de ticket" - } - } - }, - "topic": { - "description": "Altera o motivo do ticket", - "name": "motivo", - "response": { - "changed": { - "description": "O motivo do ticket foi alterado", - "title": "✅ Motivo alterado" - }, - "not_a_ticket": { - "description": "Por favor usa este comando no ticket ao qual queres mudar o motivo.", - "title": "❌ Este não é um canal de ticket" - } - } - } - }, - "message_will_be_deleted_in": "Essa mensagem será apagada em %d segundos", - "missing_permissions": { - "description": "Você não tem as permissões necessárias para usar este comando:\n%s", - "title": "❌" - }, - "ticket": { - "claimed": { - "description": "%s has claimed this ticket.", - "title": "✅ Ticket reivindicado" - }, - "closed": { - "description": "Ticket fechado.\nO canal será apagado daqui a 5 segundos", - "title": "✅ Ticket fechado" - }, - "closed_by_member": { - "description": "Ticket fechado por %s.\nO ticket será apagado daqui a 5 segundos", - "title": "✅ Ticket fechado" - }, - "closed_by_member_with_reason": { - "description": "Ticket fechado por %s, motivo: `%s`\nO canal será apagado daqui a 5 segundos", - "title": "✅ Ticket fechado" - }, - "closed_with_reason": { - "description": "Ticket foi fechado, motivo: `%s`\nO canal será apagado daqui a 5 segundos", - "title": "✅ Ticket fechado" - }, - "member_added": { - "description": "%s foi adicionado ao ticket %s", - "title": "Utilizador adicionado" - }, - "member_removed": { - "description": "%s foi removido do ticket por %s", - "title": "Utilizador removido" - }, - "opening_message": { - "fields": { - "topic": "Motivo" - } - }, - "questions": "Por favor responda as seguintes questões:\n\n%s", - "released": { - "description": "%s libertou o ticket.", - "title": "✅ Ticket libertado" - }, - "survey": { - "complete": { - "description": "Obrigado pelo seu feedback.", - "title": "✅ Obrigado" - }, - "start": { - "description": "Olha, %s. Antes deste canal ser apagado, você se importaria de responder a um questionário rápido com %d perguntas? Reaja com ✅ para iniciar ou ignore esta mensagem.", - "title": "❔ Feedback" - } - } - } -} diff --git a/src/locales/pt.json b/src/locales/pt.json deleted file mode 100644 index 6c07619..0000000 --- a/src/locales/pt.json +++ /dev/null @@ -1,598 +0,0 @@ -{ - "blacklisted": "❌ Estás na blacklist", - "bot": { - "missing_permissions": { - "description": "Discord Ticket precisa das seguintes permissões:\n%s", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s por [eartharoid](%s)" - }, - "collector_expires_in": "Expira daqui a %d segundos", - "command_execution_error": { - "description": "Um erro desconhecido ocorreu durante a execução do comano.\nPor favor pergunta a um administrador para verificar os outputs/logs da consola para mais detalhes.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Adicionar um membro a um ticket", - "name": "adicionar", - "options": { - "member": { - "description": "O membro para adicionar ao ticket", - "name": "membro" - }, - "ticket": { - "description": "O ticket para adicionar o membro", - "name": "ticket" - } - }, - "response": { - "added": { - "description": "%s foi adicionado ao %s.", - "title": "✅ Membro adicionado" - }, - "no_member": { - "description": "Por favor menciona o membro que queres adicionar.", - "title": "❌ Membro desconhecido" - }, - "no_permission": { - "description": "Não és o criador deste ticket nem um membro da staff; não podes adicionar membros a este ticket.", - "title": "❌ Permissões insuficientes" - }, - "not_a_ticket": { - "description": "Por favor usa este comando num ticket, ou menciona o canal.", - "title": "❌ Este canal não é um ticket" - } - } - }, - "blacklist": { - "description": "Permite, ou não, um membro de interagir com o bot", - "name": "blacklist", - "options": { - "add": { - "description": "Adiciona um membro ou cargo á blacklist", - "name": "adicionar", - "options": { - "member_or_role": { - "description": "O membro ou cargo para adicionar á blacklist", - "name": "membro_ou_cargo" - } - } - }, - "remove": { - "description": "Remove um membro ou cargo da blacklist", - "name": "remover", - "options": { - "member_or_role": { - "description": "O membro ou cargo para remover da blacklist", - "name": "membro_ou_cargo" - } - } - }, - "show": { - "description": "Mostra os membros e cargos que estão na blacklist", - "name": "mostrar" - } - }, - "response": { - "empty_list": { - "description": "Não existem mebros ou cargos na blacklist. Escreve `%sblacklist ` para adicionao um membro ou cargo á blacklist.", - "title": "📃 Membros e cargos na blacklist" - }, - "illegal_action": { - "description": "%s é um membro da staff e não pode estar na blacklist.", - "title": "❌ Não podes adicionar este membro á blacklist" - }, - "invalid": { - "description": "Este membro ou cargo não pode ser removido da blacklist visto que não estão na mesma.", - "title": "❌ Erro" - }, - "list": { - "fields": { - "members": "Membros", - "roles": "Cargos" - }, - "title": "📃 Membros e cargos na blacklist" - }, - "member_added": { - "description": "<@%s> foi adicionado á blacklist. Ele não vai conseguir interagir com o bot.", - "title": "✅ Membro adicionado á blacklist" - }, - "member_removed": { - "description": "<@%s> foi removido da blacklist. Ele pode usar agora o bot.", - "title": "✅ Membro removido da blacklist" - }, - "role_added": { - "description": "<@&%s> foi adicionada á blacklist. Membros com este cargo já não podem interagir com o bot.", - "title": "✅ Cargo adicionado á blacklist" - }, - "role_removed": { - "description": "<@&%s> foi removida da blacklist. Membros com este cargo já podes interagir com o bot.", - "title": "✅ Cargo removido da blacklist" - } - } - }, - "close": { - "description": "Fecha um ticket", - "name": "fechar", - "options": { - "reason": { - "description": "O motivo para fechar o(s) ticket(s)", - "name": "motivo" - }, - "ticket": { - "description": "O ticket para fechar, ou o número ou o ID do canal", - "name": "ticket" - }, - "time": { - "description": "Fecha todos os tickets que estão inativos pelo tempo fornecido", - "name": "tempo" - } - }, - "response": { - "canceled": { - "description": "Cancelas-te a operação.", - "title": "🚫 Cancelado" - }, - "closed": { - "description": "O ticket #%s foi fechado.", - "title": "✅ Ticket fechado" - }, - "closed_multiple": { - "description": [ - "&d ticket foi fechado.", - "%d tickets foram fechados." - ], - "title": [ - "✅ Ticket fechado", - "✅ Tickets fechados" - ] - }, - "confirm": { - "buttons": { - "cancel": "Cencelar", - "confirm": "Fechar" - }, - "description": "Por favor confirma a tua decisão.", - "description_with_archive": "O ticket vai ser arquivado para referências futuras.", - "title": "❔ Tens a certeza?" - }, - "confirm_multiple": { - "buttons": { - "cancel": "Cancelar", - "confirm": [ - "Fechar %d ticket", - "Fechar %d tickets" - ] - }, - "description": [ - "Estás prestes a fechar %d ticket.", - "Estás prestes a fechar %d tickets." - ], - "title": "❔ Tens a certeza?" - }, - "confirmation_timeout": { - "description": "Demoras-te muito tempo para confirmar.", - "title": "❌ Tempo de interação expirado" - }, - "invalid_time": { - "description": "O período de tempo fornecido não pôde ser analisado.", - "title": "❌ Entrada inválida" - }, - "no_tickets": { - "description": "Não existem ticket que estiveram inativos por este período de tempo.", - "title": "❌ Sem tickets para fechar" - }, - "not_a_ticket": { - "description": "Usa este comando num ticket ou use um ticket flag.\nEscreve `/help close` para mais informações.", - "title": "❌ Este canal não é um ticket" - }, - "unresolvable": { - "description": "`%s` não pôde ser comparado com um ticket. Por favor fornece um ID/menção do ticket ou o número.", - "title": "❌ Erro" - } - } - }, - "help": { - "description": "Lista dos comandos que tens acesso", - "name": "ajuda", - "response": { - "list": { - "description": "Os comandos que tens acesso estão listados em baixo. Para criar um ticket, escreve **`/novo`**.", - "fields": { - "commands": "Comandos" - }, - "title": "❔ Ajuda" - } - } - }, - "new": { - "description": "Cria um novo ticket", - "name": "novo", - "options": { - "topic": { - "description": "O motivo do ticket", - "name": "motivo" - } - }, - "request_topic": { - "description": "Por favor escreve em poucas palavras o porquê da abertura deste ticket.", - "title": "⚠️ Motivo do Ticket" - }, - "response": { - "created": { - "description": "O teu ticket foi criado: %s.", - "title": "✅ Ticket criado" - }, - "error": { - "title": "❌ Erro" - }, - "has_a_ticket": { - "description": "Por favor usa o teu ticket já existente (<#%s>) ou fecha-o antes de criares outro.", - "title": "❌ Já tens um ticket aberto" - }, - "max_tickets": { - "description": "Por favor usa `/fechar` para fechares tickets desnecessários.\n\n%s", - "title": "❌ Já tens %d tickets abertos" - }, - "no_categories": { - "description": "Um administrador do servidor tem que criar pelo menos uma categoria de tickets antes que qualquer ticket possa ser aberto.", - "title": "❌ Não podes criar tickets" - }, - "select_category": { - "description": "Seleciona a categoria em que o teu motivo mais se enquadra.", - "title": "🔤 Por favor seleciona a categoria do ticket" - }, - "select_category_timeout": { - "description": "Demoras-te muito tempo a selecionar uma categoria.", - "title": "❌ Tempo de interação expirado" - } - } - }, - "panel": { - "description": "Cria um novo painel de ticket", - "name": "painel", - "options": { - "categories": { - "description": "A vírgula separa a lista dos IDs das categorias", - "name": "categorias" - }, - "description": { - "description": "A descrição para a mensagem do painel", - "name": "descrição" - }, - "image": { - "description": "Um link de imagem para a mensagem do painel", - "name": "imagem" - }, - "just_type": { - "description": "Criar um painel \"só escrever\"?", - "name": "so_escrever" - }, - "thumbnail": { - "description": "Um link para a thumbnail da mensagem do painel", - "name": "thumbnail" - }, - "title": { - "description": "O título da mensagem do painel", - "name": "título" - } - }, - "response": { - "invalid_category": { - "description": "Um ou mais IDs das categorias fornecidas são inválidas.", - "title": "❌ Categoria inválida" - }, - "too_many_categories": { - "description": "O painel \"só escrever\" só pode ser usado para uma categoria.", - "title": "❌ Demasiadas Categorias" - } - } - }, - "remove": { - "description": "Remove um membro de um ticket", - "name": "remover", - "options": { - "member": { - "description": "O membro para remover do ticket", - "name": "membro" - }, - "ticket": { - "description": "O ticket para remover o membro", - "name": "ticket" - } - }, - "response": { - "no_member": { - "description": "Por favor menciona o membro que queres remover.", - "title": "❌ Membro desconhecido" - }, - "no_permission": { - "description": "Não és o criador deste ticket nem um membro da staff; não podes remover membros deste ticket.", - "title": "❌ Permissões insuficientes" - }, - "not_a_ticket": { - "description": "Por favor usa este comando num ticket, ou menciona o canal.", - "title": "❌ Este canal não é um ticket" - }, - "removed": { - "description": "%s foi removido de %s.", - "title": "✅ Membro removido" - } - } - }, - "settings": { - "description": "Configura o Discord Tickets", - "name": "definicoes", - "options": { - "categories": { - "description": "Gere a categoria dos tickets", - "name": "categorias", - "options": { - "create": { - "description": "Cria uma nova categoria", - "name": "criar", - "options": { - "name": { - "description": "O nome da categoria", - "name": "nome" - }, - "roles": { - "description": "Uma vírgula separa a lista dos IDs dos cargos de staff para a categoria", - "name": "cargos" - } - } - }, - "delete": { - "description": "Apaga a categoria", - "name": "apagar", - "options": { - "id": { - "description": "O ID da categoria para apagar", - "name": "id" - } - } - }, - "edit": { - "description": "Faz alterações na configuração de uma categoria", - "name": "editar", - "options": { - "claiming": { - "description": "Ativar a reivindicação de tickets?", - "name": "reivindicação" - }, - "id": { - "description": "O ID da categoria para editar", - "name": "id" - }, - "image": { - "description": "Um link de imagem", - "name": "imagem" - }, - "max_per_member": { - "description": "O número máximo de tickets que um membro pode ter abertos nesta categoria", - "name": "max_por_membro" - }, - "name": { - "description": "O nome da categoria", - "name": "nome" - }, - "name_format": { - "description": "O formato do nome do ticket", - "name": "formato_do_nome" - }, - "opening_message": { - "description": "O texto para enviar quando um ticket é aberto", - "name": "mensagem_de_abertura" - }, - "ping": { - "description": "Uma vírgula separa a lista dos IDs dos cargos para mencionar", - "name": "mencionar" - }, - "require_topic": { - "description": "É necessário o membro dar um motivo do ticket?", - "name": "motivo_necessario" - }, - "roles": { - "description": "Uma vírgula separa a lista dos IDs do cargo de staff", - "name": "cargos" - }, - "survey": { - "description": "O questionário para usar", - "name": "questionário" - } - } - }, - "list": { - "description": "Lista de Categorias", - "name": "lista" - } - } - }, - "set": { - "description": "Definir Opções", - "name": "definir", - "options": { - "close_button": { - "description": "Ativar o botão de fechar o ticket?", - "name": "botao_fechar" - }, - "colour": { - "description": "A cor padrão", - "name": "cor" - }, - "error_colour": { - "description": "A cor de erro", - "name": "cor_erro" - }, - "footer": { - "description": "Texto para a legenda do embed", - "name": "legenda" - }, - "locale": { - "description": "O locale (idioma/linguagem)", - "name": "linguagem" - }, - "log_messages": { - "description": "Guardar mensagens dos tickets?", - "name": "guardar_msg" - }, - "success_colour": { - "description": "A cor de sucesso", - "name": "cor_sucesso" - } - } - } - }, - "response": { - "category_created": "✅ A categoria de ticket `%s` foi criada", - "category_deleted": "✅ A categoria de ticket `%s` foi apagada", - "category_does_not_exist": "❌ Nenhuma categoria existente tem este ID", - "category_list": "Categorias de Tickets", - "category_updated": "✅ A categoria do ticket `%s` foi atualizada", - "settings_updated": "✅ Definições foram atualizadas" - } - }, - "stats": { - "description": "Mostra as estatísticas dos tickets", - "fields": { - "messages": "Mensagens", - "response_time": { - "minutes": "%s minutos", - "title": "Tempo médio de resposta" - }, - "tickets": "Tickets" - }, - "name": "estatisticas", - "response": { - "global": { - "description": "Estatisticas dis tickets de todos os servidores onde esta versão do Discord Tickets é usada.", - "title": "📊 Estatisticas gerais" - }, - "guild": { - "description": "Estatisticas acerca dos tickets neste servidor. Esta informação é mantida na cache por 1 hora.", - "title": "📊 Estatisticas deste servidor" - } - } - }, - "survey": { - "description": "Vê as respostas do questionário", - "name": "questionario", - "options": { - "survey": { - "description": "O nome do questionário para ver as respostas", - "name": "questionário" - } - }, - "response": { - "list": { - "title": "📃 Questionários" - } - } - }, - "tag": { - "description": "Usa uma resposta em tag", - "name": "tag", - "options": { - "tag": { - "description": "O nome da tag para usar", - "name": "tag" - } - }, - "response": { - "error": "❌ Erro", - "list": { - "title": "📃 Lista de tags" - }, - "missing": "Esta tag necessita dos seguintes argumentos:\n%s", - "not_a_ticket": { - "description": "Esta tag só pode ser usada num ticket visto que usa referências do ticket.", - "title": "❌ Não é um ticket" - } - } - }, - "topic": { - "description": "Altera o motivo do ticket", - "name": "motivo", - "options": { - "new_topic": { - "description": "O novo motivo do ticket", - "name": "novo_motivo" - } - }, - "response": { - "changed": { - "description": "O motivo do ticket foi alterado.", - "title": "✅ Motivo alterado" - }, - "not_a_ticket": { - "description": "Por favor usa este comando no ticket que queres trocar o motivo.", - "title": "❌ Não é um ticket" - } - } - } - }, - "message_will_be_deleted_in": "Esta mensagem vai ser apagada daqui a %d segundos", - "missing_permissions": { - "description": "Não tens permissões suficientes para usar este comando.\n%s", - "title": "❌ Erro" - }, - "panel": { - "create_ticket": "Cria um ticket" - }, - "ticket": { - "claim": "Reivindicar", - "claimed": { - "description": "%s reivindicou o ticket.", - "title": "✅ Ticket reivindicado" - }, - "close": "Fechar", - "closed": { - "description": "Este ticket foi fechado.\nO canal vai ser apagado dentro de 5 segundos.", - "title": "✅ Ticket fechado" - }, - "closed_by_member": { - "description": "Este ticket foi fechado por %s.\nEste canal vai ser apagado dentro de 5 segundos.", - "title": "✅ Ticket fechado" - }, - "closed_by_member_with_reason": { - "description": "Este ticket foi fechado por %s: `%s`\nEste canal vai ser apagado dentro de 5 segundos.", - "title": "✅ Ticket fechado" - }, - "closed_with_reason": { - "description": "Este ticket foi fechado: `%s`\nEste canal vai ser apagado dentro de 5 segundos.", - "title": "✅ Ticket fechado" - }, - "member_added": { - "description": "%s foi adicionado por %s", - "title": "Membro adicionado" - }, - "member_removed": { - "description": "%s foi removido por %s", - "title": "Membro removido" - }, - "opening_message": { - "content": "%s\n%s criou um novo ticket", - "fields": { - "topic": "Motivo" - } - }, - "questions": "Por favor responde ás seguintes questões:\n\n%s", - "released": { - "description": "%s libertou este ticket.", - "title": "✅ Ticket libertado" - }, - "survey": { - "complete": { - "description": "Obrigado pelo teu feedback.", - "title": "✅ Obrigado" - }, - "start": { - "description": "Olha, %s. Antes deste canal ser apagado, importas-te de completar um questionário super rápido de %d-question questões? Reage com ✅ para começar, ou ignora esta mensagem.", - "title": "❔ Feedback" - } - }, - "unclaim": "Libertar" - }, - "updated_permissions": "✅ Atualizado permissões dos Slash commands" -} diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json deleted file mode 100644 index 6bb7a3f..0000000 --- a/src/locales/ru-RU.json +++ /dev/null @@ -1,324 +0,0 @@ -{ - "bot": { - "missing_permissions": { - "description": "Discord Tickets требует следующих разрешений:\n%s", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s от [eartharoid](%s)" - }, - "collector_expires_in": "Истекает через %d секунд", - "command_execution_error": { - "description": "An unexpected error occurred during command execution.\nPlease ask an administrator to check the console output / logs for details.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Add a member to a ticket", - "name": "add", - "response": { - "added": { - "description": "%s has been added to %s.", - "title": "✅ Member added" - }, - "no_member": { - "description": "Please mention the member you want to add.", - "title": "❌ Unknown member" - }, - "no_permission": { - "description": "You are not the creator of this ticket and you are not a staff member; you can't add members to this ticket.", - "title": "❌ Insufficient permission" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ Это не канал тикета" - } - } - }, - "blacklist": { - "description": "Запретить/разрешить участнику использовать бота", - "name": "blacklist", - "response": { - "empty_list": { - "description": "В черный список не внесли ни участников, ни ролей. Напишите `%sblacklist `, чтобы добавить пользователя или роль в черный список.", - "title": "📃 Черный список пользователей и ролей" - }, - "illegal_action": { - "description": "%s есть персоналом и не может быть внесен в черный список.", - "title": "❌ Вы не можете добавить в черный список этого участника" - }, - "list": { - "title": "📃 Черный список пользователей и ролей" - }, - "member_added": { - "description": "<@%s> добавлен в черный список. Они больше не смогут взаимодействовать с ботом.", - "title": "✅ Пользователь добавлен в черный список" - }, - "member_removed": { - "description": "<@%s> был убран из черного списка. Теперь он может использовать бота снова.", - "title": "✅ Пользователь убран из черного списка" - }, - "role_added": { - "description": "<@&%s> добавлена в черный список. Участники с этой ролью больше не смогут взаимодействовать с ботом.", - "title": "✅ Добавлена роль в черный список" - }, - "role_removed": { - "description": "<@&%s> была убран из черного списка. Теперь он может использовать бота снова.", - "title": "✅ Роль убрана из черного списка" - } - } - }, - "close": { - "description": "Закрыть тикет-канал", - "name": "закрыть", - "response": { - "closed": { - "description": "Тикет #%s был закрыт.", - "title": "✅ Тикет закрыт" - }, - "closed_multiple": { - "description": [ - "Тикет %d был закрыт.", - "%d тикетов было закрыто." - ], - "title": [ - "✅ Тикет закрыт", - "✅ Тикеты закрыты" - ] - }, - "confirm": { - "description": "Поставьте реакцию ✅, чтобы закрыть этот тикет.", - "description_with_archive": "Вы сможете просмотреть архивную версию этого тикета.\nПоставьте реакцию ✅, чтобы закрыть этот тикет.", - "title": "❔ Вы уверены?" - }, - "confirm_multiple": { - "description": [ - "Поставьте реакцию ✅, чтобы закрыть тикет %d.", - "Поставьте реакцию ✅, чтобы закрыть тикеты %d." - ], - "title": "❔ Вы уверены?" - }, - "confirmation_timeout": { - "description": "Подтверждение заняло слишком много времени.", - "title": "❌ Время реакции истекло" - }, - "invalid_time": { - "description": "Предоставленный период времени не может быть разобран.", - "title": "❌ Недействительный ввод" - }, - "no_tickets": { - "description": "Нет тикетов, которые были неактивны в течение этого периода времени.", - "title": "❌ Нет тикетов для закрытия" - }, - "not_a_ticket": { - "description": "Please use this command in a ticket channel or use the ticket flag.\nType `%shelp close` for more information.", - "title": "❌ Это не канал тикета" - }, - "unresolvable": { - "description": "`%s` could not be resolved to a ticket. Please provide the ticket ID/mention or number.", - "title": "❌ Ошибка" - } - } - }, - "help": { - "description": "List commands you have access to, or find out more about a command", - "name": "help", - "response": { - "list": { - "description": "The commands you have access to are listed below. For more information about a command, type `{prefix}help [command]`. To create a ticket, type `{prefix}new [topic]`.", - "fields": { - "commands": "Commands" - }, - "title": "❔ Help" - } - } - }, - "new": { - "description": "Создание нового тикета", - "name": "новый", - "request_topic": { - "description": "Please briefly state what this ticket is about in a few words.", - "title": "Тема тикета" - }, - "response": { - "created": { - "description": "Ваш тикет был создан: %s.", - "title": "✅ Тикет создан" - }, - "error": { - "title": "❌ Ошибка" - }, - "has_a_ticket": { - "description": "Пожалуйста, используйте существующий тикет (<#%s>) или закройте его перед созданием нового.", - "title": "❌ У вас уже есть открытый тикет" - }, - "max_tickets": { - "description": "Please use `%sclose` to close any unneeded tickets.\n\n%s", - "title": "❌ У вас уже есть %d открытых тикетов" - }, - "no_categories": { - "description": "A server administrator must create at least one ticket category before a new ticket can be opened.", - "title": "❌ Не могу создать тикет" - }, - "select_category": { - "description": "Select the category most relevant to your ticket's topic:\n\n%s", - "title": "🔤 Пожалуйста, выберите категорию тикетов" - }, - "select_category_timeout": { - "description": "Вы слишком долго выбирали категорию тикетов.", - "title": "❌ Время реакции истекло" - } - } - }, - "panel": { - "description": "Create a new ticket panel", - "name": "panel", - "response": { - "invalid_category": { - "description": "One or more of the specified category IDs is invalid.", - "title": "❌ Недействительная категория" - } - } - }, - "remove": { - "description": "Remove a member from a ticket", - "name": "remove", - "response": { - "no_member": { - "description": "Please mention the member you want to remove.", - "title": "❌ Unknown member" - }, - "no_permission": { - "description": "You are not the creator of this ticket and you are not a staff member; you can't remove members from this ticket.", - "title": "❌ Insufficient permission" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ Это не канал тикета" - }, - "removed": { - "description": "%s has been removed from %s.", - "title": "✅ Member removed" - } - } - }, - "settings": { - "description": "Configure Discord Tickets", - "name": "settings" - }, - "stats": { - "description": "Display ticket statistics", - "fields": { - "messages": "Messages", - "response_time": { - "minutes": "%s minutes", - "title": "Avg. response time" - }, - "tickets": "Tickets" - }, - "name": "stats", - "response": { - "global": { - "description": "Statistics about tickets across all guilds where this Discord Tickets instance is used.", - "title": "📊 Global stats" - }, - "guild": { - "description": "Statistics about tickets within this guild. This data is cached for an hour.", - "title": "📊 This server's stats" - } - } - }, - "survey": { - "description": "View survey responses", - "name": "survey", - "response": { - "list": { - "title": "📃 Surveys" - } - } - }, - "tag": { - "description": "Use a tag response", - "name": "tag", - "response": { - "error": "❌ Ошибка", - "list": { - "title": "📃 Tag list" - }, - "missing": "This tag requires the following arguments:\n%s", - "not_a_ticket": { - "description": "This tag can only be used within a ticket channel as it uses ticket references.", - "title": "❌ Это не канал тикета" - } - } - }, - "topic": { - "description": "Change the topic of the ticket", - "name": "топик", - "response": { - "changed": { - "description": "This ticket's topic has been changed.", - "title": "✅ Topic changed" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel you want to change the topic of.", - "title": "❌ Это не канал тикета" - } - } - } - }, - "message_will_be_deleted_in": "Это сообщение будет удалено в течение %d секунд", - "missing_permissions": { - "description": "You do not have the permissions required to use this command:\n%s", - "title": "❌" - }, - "ticket": { - "claimed": { - "description": "%s принял этот тикет.", - "title": "✅ Тикет принят" - }, - "closed": { - "description": "This ticket has been closed.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Тикет закрыт" - }, - "closed_by_member": { - "description": "This ticket has been closed by %s.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Тикет закрыт" - }, - "closed_by_member_with_reason": { - "description": "This ticket has been closed by %s: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Тикет закрыт" - }, - "closed_with_reason": { - "description": "This ticket has been closed: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Тикет закрыт" - }, - "member_added": { - "description": "%s has been added by %s", - "title": "Member added" - }, - "member_removed": { - "description": "%s has been removed by %s", - "title": "Member removed" - }, - "opening_message": { - "fields": { - "topic": "Topic" - } - }, - "questions": "Please answer the following questions:\n\n%s", - "released": { - "description": "%s выпустил этот тикет.", - "title": "✅ Тикет выпущен" - }, - "survey": { - "complete": { - "description": "Thank you for your feedback.", - "title": "✅ Thank you" - }, - "start": { - "description": "Hey, %s. Before this channel is deleted, would you mind completing a quick %d-question survey? React with ✅ to start, or ignore this message.", - "title": "❔ Feedback" - } - } - } -} diff --git a/src/locales/th-TH.json b/src/locales/th-TH.json deleted file mode 100644 index 04cedde..0000000 --- a/src/locales/th-TH.json +++ /dev/null @@ -1,373 +0,0 @@ -{ - "bot": { - "missing_permissions": { - "description": "Discord Tickets ต้องการการสิทธิ์อนุญาตดังต่อไปนี้: %s", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s สร้างโดย [eartharoid](%s)" - }, - "collector_expires_in": "หมดอายุในอีก %d วินาที", - "command_execution_error": { - "description": "An unexpected error occurred during command execution.\nPlease ask an administrator to check the console output / logs for details.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Add a member to a ticket", - "name": "add", - "options": { - "member": { - "name": "สมาชิก" - } - }, - "response": { - "added": { - "description": "%s has been added to %s.", - "title": "✅ Member added" - }, - "no_member": { - "description": "Please mention the member you want to add.", - "title": "❌ Unknown member" - }, - "no_permission": { - "description": "You are not the creator of this ticket and you are not a staff member; you can't add members to this ticket.", - "title": "❌ Insufficient permission" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ This isn't a ticket channel" - } - } - }, - "blacklist": { - "description": "Blacklist/unblacklist a member from interacting with the bot", - "name": "blacklist", - "options": { - "add": { - "name": "เพิ่ม", - "options": { - "member_or_role": { - "name": "สมาชิกหรือยศ" - } - } - }, - "remove": { - "name": "ลบ", - "options": { - "member_or_role": { - "name": "สมาชิกหรือยศ" - } - } - }, - "show": { - "name": "แสดง" - } - }, - "response": { - "empty_list": { - "description": "There are no members or roles blacklisted. Type `%sblacklist ` to add a member or role to the blacklist.", - "title": "📃 Blacklisted members and roles" - }, - "illegal_action": { - "description": "%s is a staff member and cannot be blacklisted.", - "title": "❌ You can't blacklist this member" - }, - "invalid": { - "title": "❌ ผิดพลาด" - }, - "list": { - "fields": { - "members": "สมาชิก", - "roles": "ยศ" - }, - "title": "📃 Blacklisted members and roles" - }, - "member_added": { - "description": "<@%s> has been added to the blacklist. They will no longer be able to interact with the bot.", - "title": "✅ Added member to blacklist" - }, - "member_removed": { - "description": "<@%s> has been removed from the blacklist. They can now use the bot again.", - "title": "✅ Removed member from blacklist" - }, - "role_added": { - "description": "<@&%s> has been added to the blacklist. Members with this role will no longer be able to interact with the bot.", - "title": "✅ Added role to blacklist" - }, - "role_removed": { - "description": "<@&%s> has been removed from the blacklist. Members with this role can now use the bot again.", - "title": "✅ Removed role from blacklist" - } - } - }, - "close": { - "description": "Close a ticket channel", - "name": "close", - "options": { - "reason": { - "name": "สาเหตุ" - }, - "time": { - "name": "เวลา" - } - }, - "response": { - "canceled": { - "description": "คุณยกเลิกการดำเนินการ", - "title": "🚫 ยกเลิก" - }, - "closed": { - "description": "Ticket #%s has been closed.", - "title": "✅ Ticket closed" - }, - "closed_multiple": { - "description": [ - "%d ticket has been closed.", - "%d tickets have been closed." - ], - "title": [ - "✅ Ticket closed", - "✅ Tickets closed" - ] - }, - "confirm": { - "buttons": { - "cancel": "ยกเลิก", - "confirm": "ปิด" - }, - "description": "React with ✅ to close this ticket.", - "description_with_archive": "You will be able to view an archived version of it after.\nReact with ✅ to close this ticket.", - "title": "❔ Are you sure?" - }, - "confirm_multiple": { - "description": [ - "React with ✅ to close %d ticket.", - "React with ✅ to close %d tickets." - ], - "title": "❔ Are you sure?" - }, - "confirmation_timeout": { - "description": "You took too long to confirm.", - "title": "❌ Reaction time expired" - }, - "invalid_time": { - "description": "The time period provided could not be parsed.", - "title": "❌ Invalid input" - }, - "no_tickets": { - "description": "There are no tickets which have been inactive for this time period.", - "title": "❌ No tickets to close" - }, - "not_a_ticket": { - "description": "Please use this command in a ticket channel or use the ticket flag.\nType `%shelp close` for more information.", - "title": "❌ This isn't a ticket channel" - }, - "unresolvable": { - "description": "`%s` could not be resolved to a ticket. Please provide the ticket ID/mention or number.", - "title": "❌ Error" - } - } - }, - "help": { - "description": "List commands you have access to, or find out more about a command", - "name": "help", - "response": { - "list": { - "description": "The commands you have access to are listed below. For more information about a command, type `{prefix}help [command]`. To create a ticket, type `{prefix}new [topic]`.", - "fields": { - "commands": "Commands" - }, - "title": "❔ Help" - } - } - }, - "new": { - "description": "Create a new ticket", - "name": "new", - "request_topic": { - "description": "Please briefly state what this ticket is about in a few words.", - "title": "Ticket topic" - }, - "response": { - "created": { - "description": "Your ticket has been created: %s.", - "title": "✅ Ticket created" - }, - "error": { - "title": "❌ Error" - }, - "has_a_ticket": { - "description": "Please use your existing ticket (<#%s>) or close it before creating another.", - "title": "❌ You already have an open ticket" - }, - "max_tickets": { - "description": "Please use `%sclose` to close any unneeded tickets.\n\n%s", - "title": "❌ You already have %d open tickets" - }, - "no_categories": { - "description": "A server administrator must create at least one ticket category before a new ticket can be opened.", - "title": "❌ Can't create ticket" - }, - "select_category": { - "description": "Select the category most relevant to your ticket's topic:\n\n%s", - "title": "🔤 Please select the ticket category" - }, - "select_category_timeout": { - "description": "You took too long to select the ticket category.", - "title": "❌ Reaction time expired" - } - } - }, - "panel": { - "description": "สร้างแผงตั๋วสนับสนุนใหม่", - "name": "แผง", - "response": { - "invalid_category": { - "description": "รหัสหมวดหมู่ที่ระบุอย่างน้อยหนึ่งรายการไม่ถูกต้อง", - "title": "❌หมวดหมู่ไม่ถูกต้อง" - } - } - }, - "remove": { - "description": "ลบสมาชิกออกจากตั๋วสนับสนุน", - "name": "ลบ", - "response": { - "no_member": { - "description": "โปรดแท็กสมาชิกที่คุณต้องการลบ", - "title": "❌ Unknown member" - }, - "no_permission": { - "description": "คุณไม่ใช่ผู้สร้างตั๋วสนับสนุนใบนี้และคุณก็ไม่ได้เป็นหนุ่งในทีมงานของเซิร์ฟเวอร์ คุณจึงไม่สามารถลบสมาชิกออกจากตั๋วนี้ได้", - "title": "❌ Insufficient permission" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ This isn't a ticket channel" - }, - "removed": { - "description": "%s ถูกลบออกจาก %s แล้ว", - "title": "✅สมาชิกถูกลบออกแล้ว" - } - } - }, - "settings": { - "description": "ตั้งค่าบอท Discord Tickets", - "name": "การตั้งค่า" - }, - "stats": { - "description": "Display ticket statistics", - "fields": { - "messages": "Messages", - "response_time": { - "minutes": "%s minutes", - "title": "Avg. response time" - }, - "tickets": "Tickets" - }, - "name": "stats", - "response": { - "global": { - "description": "Statistics about tickets across all guilds where this Discord Tickets instance is used.", - "title": "📊 Global stats" - }, - "guild": { - "description": "Statistics about tickets within this guild. This data is cached for an hour.", - "title": "📊 This server's stats" - } - } - }, - "survey": { - "description": "View survey responses", - "name": "survey", - "response": { - "list": { - "title": "📃 Surveys" - } - } - }, - "tag": { - "description": "Use a tag response", - "name": "tag", - "response": { - "error": "❌ Error", - "list": { - "title": "📃 Tag list" - }, - "missing": "This tag requires the following arguments:\n%s", - "not_a_ticket": { - "description": "This tag can only be used within a ticket channel as it uses ticket references.", - "title": "❌ This isn't a ticket channel" - } - } - }, - "topic": { - "description": "Change the topic of the ticket", - "name": "topic", - "response": { - "changed": { - "description": "This ticket's topic has been changed.", - "title": "✅ Topic changed" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel you want to change the topic of.", - "title": "❌ This isn't a ticket channel" - } - } - } - }, - "message_will_be_deleted_in": "This message will be deleted in %d seconds", - "missing_permissions": { - "description": "You do not have the permissions required to use this command:\n%s", - "title": "❌" - }, - "ticket": { - "claimed": { - "description": "%s has claimed this ticket.", - "title": "✅ Ticket claimed" - }, - "closed": { - "description": "This ticket has been closed.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_by_member": { - "description": "This ticket has been closed by %s.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_by_member_with_reason": { - "description": "This ticket has been closed by %s: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_with_reason": { - "description": "This ticket has been closed: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "member_added": { - "description": "%s has been added by %s", - "title": "Member added" - }, - "member_removed": { - "description": "%s has been removed by %s", - "title": "Member removed" - }, - "opening_message": { - "fields": { - "topic": "Topic" - } - }, - "questions": "Please answer the following questions:\n\n%s", - "released": { - "description": "%s has released this ticket.", - "title": "✅ Ticket released" - }, - "survey": { - "complete": { - "description": "Thank you for your feedback.", - "title": "✅ Thank you" - }, - "start": { - "description": "Hey, %s. Before this channel is deleted, would you mind completing a quick %d-question survey? React with ✅ to start, or ignore this message.", - "title": "❔ Feedback" - } - } - } -} diff --git a/src/locales/vi.json b/src/locales/vi.json deleted file mode 100644 index 0b0e32a..0000000 --- a/src/locales/vi.json +++ /dev/null @@ -1,287 +0,0 @@ -{ - "blacklisted": "❌ Bạn đã bị cấm", - "bot": { - "missing_permissions": { - "description": "Discord Tickets yêu cầu các quyền sau:\n%s ", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s bởi [eartharoid](%s)" - }, - "collector_expires_in": "Hết hạn trong %d giây", - "command_execution_error": { - "description": "Một lỗi không mong muốn đã xảy ra trong quá trình thực hiện lệnh.\nVui lòng hỏi một quản trị viên để kiểm tra output từ console / logs để biết thêm chi tiết.", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "Thêm một thành viên vào ticket", - "name": "add", - "options": { - "member": { - "description": "Thành viên để thêm vào ticket", - "name": "thành viên" - }, - "ticket": { - "description": "Ticket để thêm thành viên vào", - "name": "ticket" - } - }, - "response": { - "added": { - "description": "%s đã được thêm vào %s.", - "title": "✅ Đã thêm thành viên" - }, - "no_member": { - "description": "Vui lòng mention thành viên bạn muốn thêm.", - "title": "❌ Thành viên không xác định" - }, - "no_permission": { - "description": "Bạn không phải người tạo ra ticket và bạn cũng không phải là staff; bạn không thể thêm thành viên vào ticket này.", - "title": "❌ Không đủ quyền" - }, - "not_a_ticket": { - "description": "Vui lòng sử dụng lệnh này trong một kênh ticket, hoặc mention tên kênh.", - "title": "❌ Đây không phải là một kênh ticket" - } - } - }, - "blacklist": { - "description": "Xem hoặc sửa đổi danh sách cấm", - "name": "danh sách cấm", - "options": { - "add": { - "description": "Thêm một thành viên hoặc role vào danh sách cấm", - "name": "thêm", - "options": { - "member_or_role": { - "description": "Thành viên hoặc role để thêm vào danh sách cấm", - "name": "member_or_role" - } - } - }, - "remove": { - "description": "Xóa thành viên hoặc role khỏi danh sách cấm", - "name": "xóa", - "options": { - "member_or_role": { - "description": "Thành viên hoặc role để xóa khỏi danh sách cấm", - "name": "member_or_role" - } - } - }, - "show": { - "description": "Hiển thị các thành viên và role trong danh sách cấm", - "name": "hiển thị" - } - }, - "response": { - "empty_list": { - "description": "Không tìm thấy thành viên hoặc role bị cấm. Sử dụng `/blacklist add` để thêm một thành viên hoặc role vào danh sách cấm.", - "title": "📃 Các thành viên và roles trong danh sách cấm" - }, - "illegal_action": { - "description": "%s là một staff và không thể bị cấm.", - "title": "❌ Bạn không thể cấm thành viên này" - }, - "invalid": { - "description": "Thành viên hoặc role này không thể bị xóa khỏi danh sách cấm bởi họ chưa bị cấm.", - "title": "❌ Lỗi" - }, - "list": { - "fields": { - "members": "Các thành viên", - "roles": "Roles" - }, - "title": "📃 Các thành viên và roles trong danh sách cấm" - }, - "member_added": { - "description": "<@%s> đã được thêm vào danh sách cấm. Họ sẽ không thể tương tác với bot nữa.", - "title": "✅ Đã thêm thành viên vào danh sách cấm" - }, - "member_removed": { - "description": "<@%s> đã được xóa khỏi danh sách cấm. Họ có thể sử lại bot.", - "title": "✅ Đã xóa thành viên khỏi danh sách cấm" - }, - "role_added": { - "description": "<@&%s> đã được thêm vào danh sách cấm. Các thành viên với role này sẽ không thể tương tác với bot.", - "title": "✅ Đã thêm role vào danh sách cấm" - }, - "role_removed": { - "description": "<@&%s> đã được xóa khỏi danh sách cấm. Các thành viên với role này có thể sử dụng lại bot.", - "title": "✅ Đã xóa role khỏi danh sách cấm" - } - } - }, - "close": { - "description": "Đóng một ticket", - "name": "đóng", - "options": { - "reason": { - "description": "Lý do để đóng ticket(s)", - "name": "lý do" - }, - "ticket": { - "description": "Ticket để đóng, có thể là số hoặc ID của kênh", - "name": "ticket" - }, - "time": { - "description": "Đóng tất cả tickets không hoạt động trong thời gian nhất định", - "name": "thời gian" - } - }, - "response": { - "canceled": { - "description": "Bạn đã hủy hành động.", - "title": "🚫 Đã hủy" - }, - "closed": { - "description": "Ticket #%s đã được đóng.", - "title": "✅ Ticket đã được đóng" - }, - "closed_multiple": { - "description": [ - "%d ticket đã được đóng", - "%d tickets đã được đóng." - ], - "title": [ - "✅ Ticket đã được đóng", - "✅ Tickets đã được đóng" - ] - }, - "confirm": { - "buttons": { - "cancel": "Hủy", - "confirm": "Đóng" - }, - "description": "Vui lòng xác nhận quyết định của bạn.", - "description_with_archive": "Ticket sẽ được lưu trữ cho tương lai.", - "title": "❔ Bạn có chắc chứ?" - }, - "confirm_multiple": { - "buttons": { - "cancel": "Hủy", - "confirm": [ - "Đóng %d ticket", - "Đóng %d tickets" - ] - }, - "description": [ - "Bạn sắp đóng %d ticket.", - "Bạn sắp đóng %d tickets." - ], - "title": "❔ Bạn có chắc chứ?" - }, - "confirmation_timeout": { - "description": "Bạn đã mất quá nhiều thời gian để xác nhận.", - "title": "❌ Thời gian tương tác đã kết thúc" - }, - "invalid_time": { - "description": "Không thể phân tích khoảng thời gian đã cung cấp.", - "title": "❌ Input không hợp lệ" - }, - "no_permission": { - "description": "Bạn không phải là staff hoặc người tạo ticket.", - "title": "❌ Không đủ quyền" - }, - "no_tickets": { - "description": "Không có tickets nào không hoạt động trong khoảng thời gian này.", - "title": "❌ Không có tickets nào để đóng" - }, - "not_a_ticket": { - "description": "Vui lòng sử dụng lệnh này trong một kênh ticket hoặc sử dụng flag ticket.\nNhập `/help close` để biết thêm thông tin.", - "title": "❌ Đây không phải là một kênh ticket" - }, - "unresolvable": { - "description": "`%s` không thể giải quyết tới ticket. Vui lòng cung cấp ticket ID/mention hoặc số.", - "title": "❌ Lỗi" - } - } - }, - "help": { - "description": "Danh sách các lệnh mà bạn có thể truy cập", - "name": "help", - "response": { - "list": { - "description": "Các lệnh mà bạn có thể truy cập được liệt kê ở dưới. Để tạo một ticket, nhập **`/new`**.", - "fields": { - "commands": "Các lệnh" - }, - "title": "❔ Help" - } - } - }, - "new": { - "description": "Tạo một ticket mới", - "name": "mới", - "options": { - "topic": { - "description": "Chủ đề của ticket", - "name": "chủ đề" - } - }, - "request_topic": { - "description": "Vui lòng nói ngắn gọn ticket này có nội dung gì trong một vài từ.", - "title": "⚠️ Chủ đề ticket" - }, - "response": { - "created": { - "description": "Ticket của bạn đã được tạo: %s.", - "title": "✅ Ticket đã được tạo" - }, - "error": { - "title": "❌ Lỗi" - }, - "has_a_ticket": { - "description": "Vui lòng sử dụng ticket hiện tại của bạn (<#%s>) hoặc đóng nó trước khi tạo một cái khác.", - "title": "❌ Bạn đang có một ticket khác" - }, - "max_tickets": { - "description": "Vui lòng sử dụng `/close` để đóng những tickets không cần thiết.\n\n%s", - "title": "❌ Bạn đã có %d tickets đang mở" - }, - "no_categories": { - "description": "Quản trị viên của server phải tạo ít nhất một ticket category trước khi một ticket có thể được mở.", - "title": "❌ Không thể tạo ticket" - }, - "select_category": { - "description": "Hãy chọn category phù hợp nhất cho chủ đề ticket của bạn.", - "title": "🔤 Vui lòng chọn ticket category" - }, - "select_category_timeout": { - "description": "Bạn đã mất quá nhiều thời gian để ticket category.", - "title": "❌ Thời gian tương tác đã kết thúc" - } - } - }, - "panel": { - "description": "Tạo một panel ticket mới", - "name": "panel", - "options": { - "categories": { - "description": "Danh sách IDs của category cần sử dụng dấu phẩy để ngăn cách", - "name": "categories" - }, - "description": { - "description": "Điền mô tả cho tin nhắn của panel", - "name": "mô tả" - }, - "image": { - "description": "URL của một hình ảnh cho tin nhắn của panel", - "name": "hình ảnh" - }, - "just_type": { - "description": "Tạo một panel \"chỉ để viết\"?", - "name": "chỉ_để_viết" - }, - "thumbnail": { - "description": "URL hình thumbnail nhỏ cho tin nhắn của panel", - "name": "hình thumbnail" - }, - "title": { - "description": "Tiêu đề cho tin nhắn của panel", - "name": "tiêu đề" - } - } - } - } -} diff --git a/src/locales/zh-Hans.json b/src/locales/zh-Hans.json deleted file mode 100644 index 837086e..0000000 --- a/src/locales/zh-Hans.json +++ /dev/null @@ -1,610 +0,0 @@ -{ - "blacklisted": "❌ 你被列入黑名单,无法开启客务单", - "bot": { - "missing_permissions": { - "description": "客服单需要以下权限:\n%s", - "title": "⚠️" - }, - "version": "[客服单](%s) v%s 由 [无风团队工作室](%s)" - }, - "collector_expires_in": "%d 秒后过期", - "command_execution_error": { - "description": "执行指令时发生错误,\n请让管理员查看控制台日志以获取详细信息", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "将用户加入到客服单", - "name": "add", - "options": { - "member": { - "description": "要加入到客服单的用户", - "name": "member" - }, - "ticket": { - "description": "将用户加入到的客服单", - "name": "ticket" - } - }, - "response": { - "added": { - "description": "%s 已加入到 %s", - "title": "✅ 已加入用户" - }, - "no_member": { - "description": "请标注您要加入的用户", - "title": "❌ 未知用户" - }, - "no_permission": { - "description": "您不是管理人员,无法向此客务单加入用户", - "title": "❌ 权限不足" - }, - "not_a_ticket": { - "description": "请在客服单频道中使用此指令,或标注该频道", - "title": "❌ 这不是客服单频道" - } - } - }, - "blacklist": { - "description": "查看或修改黑名单", - "name": "blacklist", - "options": { - "add": { - "description": "将用户或身分组列入到黑名单", - "name": "add", - "options": { - "member_or_role": { - "description": "要列入到黑名单的用户或身分组", - "name": "member_or_role" - } - } - }, - "remove": { - "description": "从黑名单中移除用户或身分组", - "name": "remove", - "options": { - "member_or_role": { - "description": "要从黑名单中移除的用户或身分组", - "name": "member_or_role" - } - } - }, - "show": { - "description": "显示黑名单中的用户和身分组", - "name": "show" - } - }, - "response": { - "empty_list": { - "description": "没有用户或身分组被列入黑名单,请输入 `/blacklist add` 将用户或身分组加入到黑名单", - "title": "📃 黑名单用户和身分组" - }, - "illegal_action": { - "description": "%s 是一位管理人员,不能被列入黑名单", - "title": "❌ 您不能将此用户列入黑名单" - }, - "invalid": { - "description": "此用户或身分组未列入黑名单,因此无法从黑名单中移除", - "title": "❌ 错误" - }, - "list": { - "fields": { - "members": "用户", - "roles": "身分组" - }, - "title": "📃 黑名单用户和身分组" - }, - "member_added": { - "description": "<@%s> 已加入到黑名单,他将不能再开启客服单", - "title": "✅ 已将用户列入到黑名单" - }, - "member_removed": { - "description": "<@%s> 已从黑名单中移除,他可以开启客服单了", - "title": "✅ 已将用户从黑名单中移除" - }, - "role_added": { - "description": "<@&%s> 已列入黑名单,有此身分组的用户将无法再开启客服单", - "title": "✅ 已将身分组列入到黑名单" - }, - "role_removed": { - "description": "<@&%s> 已从黑名单中移除。 有此身分组的用户现在可以开启客服单了", - "title": "✅ 已将身分组从黑名单中移除" - } - } - }, - "close": { - "description": "关闭客服单频道", - "name": "close", - "options": { - "reason": { - "description": "关闭客服单的原因", - "name": "reason" - }, - "ticket": { - "description": "要关闭客服单的频道 ID", - "name": "ticket" - }, - "time": { - "description": "关闭所有在指定时间内处于非活动状态的客服单", - "name": "time" - } - }, - "response": { - "canceled": { - "description": "您取消了这个操作", - "title": "🚫 取消" - }, - "closed": { - "description": "客服单 #%s 已关闭", - "title": "✅ 关闭客服单" - }, - "closed_multiple": { - "description": [ - "%d 个客服单已关闭", - "%d 个客服单已关闭" - ], - "title": [ - "✅ 关闭客服单", - "✅ 关闭客服单" - ] - }, - "confirm": { - "buttons": { - "cancel": "取消", - "confirm": "关闭" - }, - "description": "请确认您的操作!", - "description_with_archive": "此客服单将被保存备份", - "title": "❔ 你确定?" - }, - "confirm_multiple": { - "buttons": { - "cancel": "取消", - "confirm": [ - "关闭 %d 客服单", - "关闭 %d 客服单" - ] - }, - "description": [ - "您即将关闭 %d 个客服单", - "您即将关闭 %d 个客服单" - ], - "title": "❔ 你确定?" - }, - "confirmation_timeout": { - "description": "你确认的时间太长了", - "title": "❌ 确认时间已过" - }, - "invalid_time": { - "description": "无法解析提供的时间段", - "title": "❌ 输入无效" - }, - "no_permission": { - "description": "您不是管理人员", - "title": "❌ 权限不足" - }, - "no_tickets": { - "description": "没有在此时间段内处于非活动状态的客服单", - "title": "❌ 没有客服单可以关闭" - }, - "not_a_ticket": { - "description": "请在客服频道中使用此指令,\n输入`/help close`来获取更多资讯", - "title": "❌ 这不是客服单频道" - }, - "unresolvable": { - "description": "`%s` 无法解析为客服单,请提供客服单 ID或标注", - "title": "❌ 错误" - } - } - }, - "help": { - "description": "列出您有权使用的指令", - "name": "help", - "response": { - "list": { - "description": "下面列出了您可以使用的指令,要创建客服单,请键入 **`/new`**", - "fields": { - "commands": "指令" - }, - "title": "❔ 帮助" - } - } - }, - "new": { - "description": "创建客服单", - "name": "new", - "options": { - "topic": { - "description": "想要询问的问题或需要的帮助", - "name": "topic" - } - }, - "request_topic": { - "description": "有什么问题需要我们的帮助呢还是想询问服务呢", - "title": "⚠️ 客服询问" - }, - "response": { - "created": { - "description": "您的客服单已创建,它在这里 ➤ %s", - "title": "✅ 客服单已创建!" - }, - "error": { - "title": "❌ 错误" - }, - "has_a_ticket": { - "description": "请使用您现有的客服单 (<#%s>) 或在创建另一个客服单之前将其关闭", - "title": "❌ 您已经有一张客服单" - }, - "max_tickets": { - "description": "请通知管理人员来关闭您的客服单", - "title": "❌ 您已经有 %d 张尚未结束的客服单" - }, - "no_categories": { - "description": "管理人员必须先创建一个客服单分类才能打开新的客服单", - "title": "❌ 无法创建客服单" - }, - "select_category": { - "description": "选择与您的客服单问题最相关的分类", - "title": "🔤 请选择客服单分类" - }, - "select_category_timeout": { - "description": "您选择客服单分类的时间过长", - "title": "❌ 选择时间已过" - } - } - }, - "panel": { - "description": "创建新的客服单开启介面", - "name": "panel", - "options": { - "categories": { - "description": "使用逗号来增加分类 ID", - "name": "categories" - }, - "description": { - "description": "开启介面资讯", - "name": "description" - }, - "image": { - "description": "开启介面的图片网址", - "name": "image" - }, - "just_type": { - "description": "创建一个\"只需输入\"面板?", - "name": "just_type" - }, - "thumbnail": { - "description": "开启介面资讯的缩图网址", - "name": "thumbnail" - }, - "title": { - "description": "开启介面的标题", - "name": "title" - } - }, - "response": { - "invalid_category": { - "description": "一个或多个指定的分类 ID 无效", - "title": "❌ 分类无效" - }, - "too_many_categories": { - "description": "\"只需键入\"介面只能用于单个分类", - "title": "❌ 分类太多" - } - } - }, - "remove": { - "description": "从客服单中移除用户", - "name": "remove", - "options": { - "member": { - "description": "要从客服单中移除的用户", - "name": "member" - }, - "ticket": { - "description": "删除用户的客服单", - "name": "ticket" - } - }, - "response": { - "no_member": { - "description": "请标注您要移除的用户", - "title": "❌ 未知用户" - }, - "no_permission": { - "description": "您不是管理人员,您不能从此客服单中移除用户", - "title": "❌ 权限不足" - }, - "not_a_ticket": { - "description": "请在客服单频道中使用此指令,或标注该频道", - "title": "❌ 这不是客服单频道" - }, - "removed": { - "description": "%s 已从 %s 中移除", - "title": "✅ 已移除用户" - } - } - }, - "settings": { - "description": "设定客服单系统", - "name": "settings", - "options": { - "categories": { - "description": "管理您的客服单分类", - "name": "categories", - "options": { - "create": { - "description": "创建一个新分类", - "name": "create", - "options": { - "name": { - "description": "分类名称", - "name": "name" - }, - "roles": { - "description": "此分类的管理人员身分组 ID ,使用逗号做分隔", - "name": "roles" - } - } - }, - "delete": { - "description": "删除分类", - "name": "delete", - "options": { - "id": { - "description": "要删除分类的 ID", - "name": "id" - } - } - }, - "edit": { - "description": "更改分类的设定", - "name": "edit", - "options": { - "claiming": { - "description": "开启客服单?", - "name": "claiming" - }, - "id": { - "description": "要编辑的分类的 ID", - "name": "id" - }, - "image": { - "description": "图片网址", - "name": "image" - }, - "max_per_member": { - "description": "用户在此分类中可以开启的最大客服单数量", - "name": "max_per_member" - }, - "name": { - "description": "分类名称", - "name": "name" - }, - "name_format": { - "description": "客服单格式", - "name": "name_format" - }, - "opening_message": { - "description": "打开客服单时要发送的讯息", - "name": "opening_message" - }, - "opening_questions": { - "description": "开启客服单时要问的问题", - "name": "opening_questions" - }, - "ping": { - "description": "要标注的身分组 ID ,使用逗号做分隔", - "name": "ping" - }, - "require_topic": { - "description": "要求用户在开启客服单时询问问题?", - "name": "require_topic" - }, - "roles": { - "description": "以逗号做分隔,管理人员身分组 ID 列表", - "name": "roles" - }, - "survey": { - "description": "开启意见调查", - "name": "survey" - } - } - }, - "list": { - "description": "列出分类", - "name": "list" - } - } - }, - "set": { - "description": "设定选项", - "name": "set", - "options": { - "close_button": { - "description": "使用按钮开启和关闭?", - "name": "close_button" - }, - "colour": { - "description": "标准颜色", - "name": "colour" - }, - "error_colour": { - "description": "错误颜色", - "name": "error_colour" - }, - "footer": { - "description": "嵌入页脚文字", - "name": "footer" - }, - "locale": { - "description": "系统语言", - "name": "locale" - }, - "log_messages": { - "description": "储存客服单的讯息?", - "name": "log_messages" - }, - "success_colour": { - "description": "成功的颜色", - "name": "success_colour" - } - } - } - }, - "response": { - "category_created": "✅ `%s` 客服单分类已创建", - "category_deleted": "✅ `%s` 客服单分类已被删除", - "category_does_not_exist": "❌ 提供的 ID 不存在分类", - "category_list": "客服单分类", - "category_updated": "✅ `%s` 客服单分类已更新", - "settings_updated": "✅ 设定已更新" - } - }, - "stats": { - "description": "显示客服单统计资讯", - "fields": { - "messages": "讯息", - "response_time": { - "minutes": "%s 分钟", - "title": "平均回应时间" - }, - "tickets": "客服单" - }, - "name": "stats", - "response": { - "global": { - "description": "使用此客服单系统的所有群组的客服单统计资讯", - "title": "📊 所有群组客服单统计资讯" - }, - "guild": { - "description": "该群组内的客服单统计,此资料将储存1小时", - "title": "📊 此群组的统计资讯" - } - } - }, - "survey": { - "description": "查看意见调查回复", - "name": "survey", - "options": { - "survey": { - "description": "用于查看回复的意见调查的名称", - "name": "survey" - } - }, - "response": { - "list": { - "title": "📃 意见调查" - } - } - }, - "tag": { - "description": "使用标注来回应", - "name": "tag", - "options": { - "tag": { - "description": "要使用的标注名称", - "name": "tag" - } - }, - "response": { - "error": "❌ 错误", - "list": { - "title": "📃 标注列表" - }, - "missing": "此标注需要以下参数:\n%s", - "not_a_ticket": { - "description": "此标注只能在客服单频道中使用,因为它使用客服单参考", - "title": "❌ 这不是客服单频道" - } - } - }, - "topic": { - "description": "更改客服单问题", - "name": "topic", - "options": { - "new_topic": { - "description": "客服单新问题", - "name": "new_topic" - } - }, - "response": { - "changed": { - "description": "此客服单的问题已更改", - "title": "✅ 问题已更改" - }, - "not_a_ticket": { - "description": "请在您要更改问题的客服单频道中使用此指令", - "title": "❌ 这不是客服单频道" - } - } - } - }, - "message_will_be_deleted_in": "此讯息将在 %d 秒后删除", - "missing_permissions": { - "description": "您没有使用此指令所需的权限:\n%s", - "title": "❌ 错误" - }, - "panel": { - "create_ticket": "创建客服单" - }, - "ticket": { - "claim": "要求", - "claimed": { - "description": "%s 已领取这张客服单", - "title": "✅ 已领取客服单" - }, - "close": "关闭", - "closed": { - "description": "此客服单已关闭,\n此频道将在 5 秒后删除", - "title": "✅ 客服单已关闭" - }, - "closed_by_member": { - "description": "此客服单已被 %s 关闭,\n此频道将在 5 秒后删除", - "title": "✅ 客服单已关闭" - }, - "closed_by_member_with_reason": { - "description": "此客服单已被 %s 关闭:`%s`\n此频道将在 5 秒后被删除", - "title": "✅ 客服单已关闭" - }, - "closed_with_reason": { - "description": "此客服单已关闭,`%s`\n此频道将在 5 秒后删除", - "title": "✅ 客服单已关闭" - }, - "member_added": { - "description": "%s 已被 %s 加入", - "title": "已加入用户" - }, - "member_removed": { - "description": "%s 已被 %s 移除", - "title": "已移除用户" - }, - "opening_message": { - "content": "%s\n%s 已创建新客服单", - "fields": { - "topic": "问题" - } - }, - "questions": "请回答以下问题:\n\n%s", - "released": { - "description": "%s 已完成此客服单", - "title": "✅ 客服单完成" - }, - "survey": { - "complete": { - "description": "感谢您的意见调查", - "title": "✅ 感谢您" - }, - "start": { - "buttons": { - "ignore": "不要", - "start": "开始意见调查" - }, - "description": "您好,%s\n在删除此客服单之前,您介意完成一个快速的 %d 意见调查吗?", - "title": "❔ 意见调查" - } - }, - "unclaim": "传送" - }, - "updated_permissions": "✅ /指令权限已更新" -} diff --git a/src/locales/zh-Hant.json b/src/locales/zh-Hant.json deleted file mode 100644 index 37e5c6d..0000000 --- a/src/locales/zh-Hant.json +++ /dev/null @@ -1,610 +0,0 @@ -{ - "blacklisted": "❌ 你被列入黑名單,無法開啟客務單", - "bot": { - "missing_permissions": { - "description": "Discord 票證需要以下權限:\n%s ", - "title": "⚠️" - }, - "version": "[客服單](%s) v%s 由 [無風團隊工作室](%s)" - }, - "collector_expires_in": "%d 秒後過期", - "command_execution_error": { - "description": "執行指令時發生錯誤,\n請讓管理員查看控制台日誌以獲取詳細信息", - "title": "⚠️" - }, - "commands": { - "add": { - "description": "將用戶加入到客服單", - "name": "add", - "options": { - "member": { - "description": "要加入到客服單的用戶", - "name": "member" - }, - "ticket": { - "description": "將用戶加入到的客服單", - "name": "ticket" - } - }, - "response": { - "added": { - "description": "%s 已加入到 %s", - "title": "✅ 已加入用戶" - }, - "no_member": { - "description": "請標註您要加入的用戶", - "title": "❌ 未知用戶" - }, - "no_permission": { - "description": "您不是管理人員,無法向此客務單加入用戶", - "title": "❌ 權限不足" - }, - "not_a_ticket": { - "description": "請在客服單頻道中使用此指令,或標註該頻道", - "title": "❌ 這不是客服單頻道" - } - } - }, - "blacklist": { - "description": "查看或修改黑名單", - "name": "blacklist", - "options": { - "add": { - "description": "將用戶或身分組列入到黑名單", - "name": "add", - "options": { - "member_or_role": { - "description": "要列入到黑名單的用戶或身分組", - "name": "member_or_role" - } - } - }, - "remove": { - "description": "從黑名單中移除用戶或身分組", - "name": "remove", - "options": { - "member_or_role": { - "description": "要從黑名單中移除的用戶或身分組", - "name": "member_or_role" - } - } - }, - "show": { - "description": "顯示黑名單中的用戶和身分組", - "name": "show" - } - }, - "response": { - "empty_list": { - "description": "沒有用戶或身分組被列入黑名單,請輸入 `/blacklist add` 將用戶或身分組加入到黑名單", - "title": "📃 黑名單用戶和身分組" - }, - "illegal_action": { - "description": "%s 是一位管理人員,不能被列入黑名單", - "title": "❌ 您不能將此用戶列入黑名單" - }, - "invalid": { - "description": "此用戶或身分組未列入黑名單,因此無法從黑名單中移除", - "title": "❌ 錯誤" - }, - "list": { - "fields": { - "members": "用戶", - "roles": "身分組" - }, - "title": "📃 黑名單用戶和身分組" - }, - "member_added": { - "description": "<@%s> 已加入到黑名單,他將不能再開啟客服單", - "title": "✅ 已將用戶列入到黑名單" - }, - "member_removed": { - "description": "<@%s> 已從黑名單中移除,他可以開啟客服單了", - "title": "✅ 已將用戶從黑名單中移除" - }, - "role_added": { - "description": "<@&%s> 已列入黑名單,有此身分組的用戶將無法再開啟客服單", - "title": "✅ 已將身分組列入到黑名單" - }, - "role_removed": { - "description": "<@&%s> 已從黑名單中移除。 有此身分組的用戶現在可以開啟客服單了", - "title": "✅ 已將身分組從黑名單中移除" - } - } - }, - "close": { - "description": "關閉客服單頻道", - "name": "close", - "options": { - "reason": { - "description": "關閉客服單的原因", - "name": "reason" - }, - "ticket": { - "description": "要關閉客服單的頻道 ID", - "name": "ticket" - }, - "time": { - "description": "關閉所有在指定時間內處於非活動狀態的客服單", - "name": "time" - } - }, - "response": { - "canceled": { - "description": "您取消了這個操作", - "title": "🚫 取消" - }, - "closed": { - "description": "客服單 #%s 已關閉", - "title": "✅ 關閉客服單" - }, - "closed_multiple": { - "description": [ - "%d 個客服單已關閉", - "%d 個客服單已關閉" - ], - "title": [ - "✅ 關閉客服單", - "✅ 關閉客服單" - ] - }, - "confirm": { - "buttons": { - "cancel": "取消", - "confirm": "關閉" - }, - "description": "請確認您的操作!", - "description_with_archive": "此客服單將被保存備份", - "title": "❔ 你確定?" - }, - "confirm_multiple": { - "buttons": { - "cancel": "取消", - "confirm": [ - "關閉 %d 客服單", - "關閉 %d 客服單" - ] - }, - "description": [ - "您即將關閉 %d 個客服單", - "您即將關閉 %d 個客服單" - ], - "title": "❔ 你確定?" - }, - "confirmation_timeout": { - "description": "你確認的時間太長了", - "title": "❌ 確認時間已過" - }, - "invalid_time": { - "description": "無法解析提供的時間段", - "title": "❌ 輸入無效" - }, - "no_permission": { - "description": "您不是管理人員", - "title": "❌ 權限不足" - }, - "no_tickets": { - "description": "沒有在此時間段內處於非活動狀態的客服單", - "title": "❌ 沒有客服單可以關閉" - }, - "not_a_ticket": { - "description": "請在客服頻道中使用此指令,\n輸入`/help close`來獲取更多資訊", - "title": "❌ 這不是客服單頻道" - }, - "unresolvable": { - "description": "`%s` 無法解析為客服單,請提供客服單 ID或標註", - "title": "❌ 錯誤" - } - } - }, - "help": { - "description": "列出您有權使用的指令", - "name": "help", - "response": { - "list": { - "description": "下面列出了您可以使用的指令,要創建客服單,請鍵入 **`/new`**", - "fields": { - "commands": "指令" - }, - "title": "❔ 幫助" - } - } - }, - "new": { - "description": "創建客服單", - "name": "new", - "options": { - "topic": { - "description": "想要詢問的問題或需要的幫助", - "name": "topic" - } - }, - "request_topic": { - "description": "有甚麼問題需要我們的幫助呢還是想詢問服務呢", - "title": "⚠️ 客服詢問" - }, - "response": { - "created": { - "description": "您的客服單已創建,它在這裡 ➤ %s", - "title": "✅ 客服單已創建!" - }, - "error": { - "title": "❌ 錯誤" - }, - "has_a_ticket": { - "description": "請使用您現有的客服單 (<#%s>) 或在創建另一個客服單之前將其關閉", - "title": "❌ 您已經有一張客服單" - }, - "max_tickets": { - "description": "請通知管理人員來關閉您的客服單", - "title": "❌ 您已經有 %d 張尚未結束的客服單" - }, - "no_categories": { - "description": "管理人員必須先創建一個客服單分類才能打開新的客服單", - "title": "❌ 無法創建客服單" - }, - "select_category": { - "description": "選擇與您的客服單問題最相關的分類", - "title": "🔤 請選擇客服單分類" - }, - "select_category_timeout": { - "description": "您選擇客服單分類的時間過長", - "title": "❌ 選擇時間已過" - } - } - }, - "panel": { - "description": "創建新的客服單開啟介面", - "name": "panel", - "options": { - "categories": { - "description": "使用逗號來增加分類 ID", - "name": "categories" - }, - "description": { - "description": "開啟介面資訊", - "name": "description" - }, - "image": { - "description": "開啟介面的圖片網址", - "name": "image" - }, - "just_type": { - "description": "創建一個\"只需輸入\"面板?", - "name": "just_type" - }, - "thumbnail": { - "description": "開啟介面資訊的縮圖網址", - "name": "thumbnail" - }, - "title": { - "description": "開啟介面的標題", - "name": "title" - } - }, - "response": { - "invalid_category": { - "description": "一個或多個指定的分類 ID 無效", - "title": "❌ 分類無效" - }, - "too_many_categories": { - "description": "\"只需鍵入\"介面只能用於單個分類", - "title": "❌ 分類太多" - } - } - }, - "remove": { - "description": "從客服單中移除用戶", - "name": "remove", - "options": { - "member": { - "description": "要從客服單中移除的用戶", - "name": "member" - }, - "ticket": { - "description": "刪除用戶的客服單", - "name": "ticket" - } - }, - "response": { - "no_member": { - "description": "請標註您要移除的用戶", - "title": "❌ 未知用戶" - }, - "no_permission": { - "description": "您不是管理人員,您不能從此客服單中移除用戶", - "title": "❌ 權限不足" - }, - "not_a_ticket": { - "description": "請在客服單頻道中使用此指令,或標註該頻道", - "title": "❌ 這不是客服單頻道" - }, - "removed": { - "description": "%s 已從 %s 中移除", - "title": "✅ 已移除用戶" - } - } - }, - "settings": { - "description": "設定客服單系統", - "name": "settings", - "options": { - "categories": { - "description": "管理您的客服單分類", - "name": "categories", - "options": { - "create": { - "description": "創建一個新分類", - "name": "create", - "options": { - "name": { - "description": "分類名稱", - "name": "name" - }, - "roles": { - "description": "此分類的管理人員身分組 ID ,使用逗號做分隔", - "name": "roles" - } - } - }, - "delete": { - "description": "刪除分類", - "name": "delete", - "options": { - "id": { - "description": "要刪除分類的 ID", - "name": "id" - } - } - }, - "edit": { - "description": "更改分類的設定", - "name": "edit", - "options": { - "claiming": { - "description": "開啟客服單?", - "name": "claiming" - }, - "id": { - "description": "要編輯的分類的 ID", - "name": "id" - }, - "image": { - "description": "圖片網址", - "name": "image" - }, - "max_per_member": { - "description": "用戶在此分類中可以開啟的最大客服單數量", - "name": "max_per_member" - }, - "name": { - "description": "分類名稱", - "name": "name" - }, - "name_format": { - "description": "客服單格式", - "name": "name_format" - }, - "opening_message": { - "description": "打開客服單時要發送的訊息", - "name": "opening_message" - }, - "opening_questions": { - "description": "開啟客服單時要問的問題", - "name": "opening_questions" - }, - "ping": { - "description": "要標註的身分組 ID ,使用逗號做分隔", - "name": "ping" - }, - "require_topic": { - "description": "要求用戶在開啟客服單時詢問問題?", - "name": "require_topic" - }, - "roles": { - "description": "以逗號做分隔,管理人員身分組 ID 列表", - "name": "roles" - }, - "survey": { - "description": "開啟意見調查", - "name": "survey" - } - } - }, - "list": { - "description": "列出分類", - "name": "list" - } - } - }, - "set": { - "description": "設定選項", - "name": "set", - "options": { - "close_button": { - "description": "使用按鈕開啟和關閉?", - "name": "close_button" - }, - "colour": { - "description": "標準顏色", - "name": "colour" - }, - "error_colour": { - "description": "錯誤顏色", - "name": "error_colour" - }, - "footer": { - "description": "嵌入頁腳文字", - "name": "footer" - }, - "locale": { - "description": "系統語言", - "name": "locale" - }, - "log_messages": { - "description": "儲存客服單的訊息?", - "name": "log_messages" - }, - "success_colour": { - "description": "成功的顏色", - "name": "success_colour" - } - } - } - }, - "response": { - "category_created": "✅ `%s` 客服單分類已創建", - "category_deleted": "✅ `%s` 客服單分類已被刪除", - "category_does_not_exist": "❌ 提供的 ID 不存在分類", - "category_list": "客服單分類", - "category_updated": "✅ `%s` 客服單分類已更新", - "settings_updated": "✅ 設定已更新" - } - }, - "stats": { - "description": "顯示客服單統計資訊", - "fields": { - "messages": "訊息", - "response_time": { - "minutes": "%s 分鐘", - "title": "平均回應時間" - }, - "tickets": "客服單" - }, - "name": "stats", - "response": { - "global": { - "description": "使用此客服單系統的所有群組的客服單統計資訊", - "title": "📊 所有群組客服單統計資訊" - }, - "guild": { - "description": "該群組內的客服單統計,此資料將儲存1小時", - "title": "📊 此群組的統計資訊" - } - } - }, - "survey": { - "description": "查看意見調查回復", - "name": "survey", - "options": { - "survey": { - "description": "用於查看回復的意見調查的名稱", - "name": "survey" - } - }, - "response": { - "list": { - "title": "📃 意見調查" - } - } - }, - "tag": { - "description": "使用標註來回應", - "name": "tag", - "options": { - "tag": { - "description": "要使用的標註名稱", - "name": "tag" - } - }, - "response": { - "error": "❌ 錯誤", - "list": { - "title": "📃 標註列表" - }, - "missing": "此標註需要以下參數:\n%s", - "not_a_ticket": { - "description": "此標註只能在客服單頻道中使用,因為它使用客服單參考", - "title": "❌ 這不是客服單頻道" - } - } - }, - "topic": { - "description": "更改客服單問題", - "name": "topic", - "options": { - "new_topic": { - "description": "客服單新問題", - "name": "new_topic" - } - }, - "response": { - "changed": { - "description": "此客服單的問題已更改", - "title": "✅ 問題已更改" - }, - "not_a_ticket": { - "description": "請在您要更改問題的客服單頻道中使用此指令", - "title": "❌ 這不是客服單頻道" - } - } - } - }, - "message_will_be_deleted_in": "此訊息將在 %d 秒後刪除", - "missing_permissions": { - "description": "您沒有使用此指令所需的權限:\n%s", - "title": "❌ 錯誤" - }, - "panel": { - "create_ticket": "創建客服單" - }, - "ticket": { - "claim": "要求", - "claimed": { - "description": "%s 已領取這張客服單", - "title": "✅ 已領取客服單" - }, - "close": "關閉", - "closed": { - "description": "此客服單已關閉,\n此頻道將在 5 秒後刪除", - "title": "✅ 客服單已關閉" - }, - "closed_by_member": { - "description": "此客服單已被 %s 關閉,\n此頻道將在 5 秒後刪除", - "title": "✅ 客服單已關閉" - }, - "closed_by_member_with_reason": { - "description": "此客服單已被 %s 關閉:`%s`\n此頻道將在 5 秒後被刪除", - "title": "✅ 客服單已關閉" - }, - "closed_with_reason": { - "description": "此客服單已關閉,`%s`\n此頻道將在 5 秒後刪除", - "title": "✅ 客服單已關閉" - }, - "member_added": { - "description": "%s 已被 %s 加入", - "title": "已加入用戶" - }, - "member_removed": { - "description": "%s 已被 %s 移除", - "title": "已移除用戶" - }, - "opening_message": { - "content": "%s\n%s 已創建新客服單", - "fields": { - "topic": "問題" - } - }, - "questions": "請回答以下問題:\n\n%s", - "released": { - "description": "%s 已完成此客服單", - "title": "✅ 客服單完成" - }, - "survey": { - "complete": { - "description": "感謝您的意見調查", - "title": "✅ 感謝您" - }, - "start": { - "buttons": { - "ignore": "不要", - "start": "開始意見調查" - }, - "description": "您好,%s\n在刪除此客服單之前,您介意完成一個快速的 %d 意見調查嗎?", - "title": "❔ 意見調查" - } - }, - "unclaim": "傳送" - }, - "updated_permissions": "✅ /指令權限已更新" -} diff --git a/src/logger.js b/src/logger.js deleted file mode 100644 index f6e8dfc..0000000 --- a/src/logger.js +++ /dev/null @@ -1,39 +0,0 @@ -const { path } = require('./utils/fs'); -const config = require('../user/config'); -const Logger = require('leekslazylogger'); -module.exports = new Logger({ - debug: config.developer.debug, - directory: path('./logs/'), - keepFor: config.logs.keep_for, - levels: { - _logger: { format: '&f&!7{timestamp}&r [LOGGER] {text}' }, - basic: { format: '&f&!7{timestamp} {text}' }, - commands: { - format: '&f&!7{timestamp}&r &3[INFO] &d(COMMANDS)&r {text}', - type: 'info' - }, - console: { format: '&f&!7{timestamp} [INFO] {text}' }, - debug: { format: '&f&!7{timestamp}&r &1[DEBUG] &9{text}' }, - error: { format: '&f&!7{timestamp}&r &4[ERROR] &c{text}' }, - http: { - format: '&f&!7{timestamp}&r &3[INFO] &d(HTTP)&r {text}', - type: 'info' - }, - info: { format: '&f&!7{timestamp}&r &3[INFO] &b{text}' }, - notice: { format: '&f&!7{timestamp}&r &0&!6[NOTICE] {text}' }, - plugins: { - format: '&f&!7{timestamp}&r &3[INFO] &d(PLUGINS)&r {text}', - type: 'info' - }, - success: { format: '&f&!7{timestamp}&r &2[SUCCESS] &a{text}' }, - warn: { format: '&f&!7{timestamp}&r &6[WARN] &e{text}' }, - ws: { - format: '&f&!7{timestamp}&r &3[INFO] &d(WS)&r {text}', - type: 'info' - } - }, - logToFile: config.logs.enabled, - name: 'Discord Tickets by eartharoid', - splitFile: config.logs.split, - timestamp: 'YYYY-MM-DD HH:mm:ss' -}); \ No newline at end of file diff --git a/src/menus/create.js b/src/menus/create.js new file mode 100644 index 0000000..ea102be --- /dev/null +++ b/src/menus/create.js @@ -0,0 +1,24 @@ +const { Menu } = require('@eartharoid/dbf'); +const { MessageFlags } = require('discord.js'); + +module.exports = class CreateMenu extends Menu { + constructor(client, options) { + super(client, { + ...options, + id: 'create', + }); + } + + /** + * @param {*} id + * @param {import("discord.js").SelectMenuInteraction} interaction + */ + async run(id, interaction) { + if (!interaction.message.flags.has(MessageFlags.Ephemeral)) interaction.message.edit({ components: interaction.message.components }).catch(() => { }); // reset the select menu (to fix a UI issue) + await this.client.tickets.create({ + ...id, + categoryId: interaction.values[0], + interaction, + }); + } +}; \ No newline at end of file diff --git a/src/modals/feedback.js b/src/modals/feedback.js new file mode 100644 index 0000000..2c49243 --- /dev/null +++ b/src/modals/feedback.js @@ -0,0 +1,68 @@ +const { Modal } = require('@eartharoid/dbf'); +const ExtendedEmbedBuilder = require('../lib/embed'); +const Cryptr = require('cryptr'); +const { encrypt } = new Cryptr(process.env.ENCRYPTION_KEY); + +module.exports = class FeedbackModal extends Modal { + constructor(client, options) { + super(client, { + ...options, + id: 'feedback', + }); + } + + /** + * @param {*} id + * @param {import("discord.js").ModalSubmitInteraction} interaction + */ + async run(id, interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply(); + + const comment = interaction.fields.getTextInputValue('comment'); + let rating = parseInt(interaction.fields.getTextInputValue('rating')) || null; // any integer, or null if NaN + rating = Math.min(Math.max(rating, 1), 5); // clamp between 1 and 5 (0 and null become 1, 6 becomes 5) + + const data = { + comment: comment?.length > 0 ? encrypt(comment) : null, + guild: { connect: { id: interaction.guild.id } }, + rating, + user: { connect: { id: interaction.user.id } }, + }; + const ticket = await client.prisma.ticket.update({ + data: { + feedback: { + upsert: { + create: data, + update: data, + }, + }, + }, + include: { guild: true }, + where: { id: interaction.channel.id }, + }); + + + if (id.next === 'requestClose') await client.tickets.requestClose(interaction, id.reason); + else if (id.next === 'acceptClose') await client.tickets.acceptClose(interaction); + + const getMessage = client.i18n.getLocale(ticket.guild.locale); + + // `followUp` must go after `reply`/`editReply` (the above) + if (comment?.length > 0 && rating !== null) { + await interaction.followUp({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.primaryColour) + .setDescription(getMessage('ticket.feedback')), + ], + ephemeral: true, + }); + } + } +}; \ No newline at end of file diff --git a/src/modals/questions.js b/src/modals/questions.js new file mode 100644 index 0000000..6874783 --- /dev/null +++ b/src/modals/questions.js @@ -0,0 +1,124 @@ +const { Modal } = require('@eartharoid/dbf'); +const { EmbedBuilder } = require('discord.js'); +const ExtendedEmbedBuilder = require('../lib/embed'); +const { logTicketEvent } = require('../lib/logging'); +const Cryptr = require('cryptr'); +const { + encrypt, + decrypt, +} = new Cryptr(process.env.ENCRYPTION_KEY); + +module.exports = class QuestionsModal extends Modal { + constructor(client, options) { + super(client, { + ...options, + id: 'questions', + }); + } + + /** + * + * @param {*} id + * @param {import("discord.js").ModalSubmitInteraction} interaction + */ + async run(id, interaction) { + /** @type {import("client")} */ + const client = this.client; + + if (id.edit) { + await interaction.deferReply({ ephemeral: true }); + const { category } = await client.prisma.ticket.findUnique({ + select: { category: { select: { customTopic: true } } }, + where: { id: interaction.channel.id }, + }); + let topic; + if (category.customTopic) topic = interaction.fields.getTextInputValue(category.customTopic); + const select = { + createdById: true, + guild: { + select: { + footer: true, + locale: true, + successColour: true, + }, + }, + id: true, + openingMessageId: true, + questionAnswers: { include: { question: true } }, + }; + const original = await client.prisma.ticket.findUnique({ + select, + where: { id: interaction.channel.id }, + }); + const ticket = await client.prisma.ticket.update({ + data: { + questionAnswers: { + update: interaction.fields.fields.map(f => ({ + data: { value: f.value ? encrypt(f.value) : '' }, + where: { id: Number(f.customId) }, + })), + }, + topic, + }, + select, + where: { id: interaction.channel.id }, + }); + const getMessage = client.i18n.getLocale(ticket.guild.locale); + + if (topic) await interaction.channel.setTopic(`<@${ticket.createdById}> | ${topic}`); + + const opening = await interaction.channel.messages.fetch(ticket.openingMessageId); + if (opening && opening.embeds.length >= 2) { + const embeds = [...opening.embeds]; + embeds[1] = new EmbedBuilder(embeds[1].data) + .setFields( + ticket.questionAnswers + .map(a => ({ + name: a.question.label, + value: a.value ? decrypt(a.value) : getMessage('ticket.answers.no_value'), + })), + ); + await opening.edit({ embeds }); + } + + await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.successColour) + .setTitle(getMessage('ticket.edited.title')) + .setDescription(getMessage('ticket.edited.description')), + ], + }); + + /** @param {ticket} ticket */ + const makeDiff = ticket => { + const diff = {}; + ticket.questionAnswers.forEach(a => { + diff[a.question.label] = a.value ? decrypt(a.value) : getMessage('ticket.answers.no_value'); + }); + return diff; + }; + + logTicketEvent(this.client, { + action: 'update', + diff: { + original: makeDiff(original), + updated: makeDiff(ticket), + }, + target: { + id: ticket.id, + name: `<#${ticket.id}>`, + }, + userId: interaction.user.id, + }); + } else { + await this.client.tickets.postQuestions({ + ...id, + interaction, + }); + } + } +}; \ No newline at end of file diff --git a/src/modals/topic.js b/src/modals/topic.js new file mode 100644 index 0000000..1b9ab94 --- /dev/null +++ b/src/modals/topic.js @@ -0,0 +1,101 @@ +const { Modal } = require('@eartharoid/dbf'); +const { EmbedBuilder } = require('discord.js'); +const ExtendedEmbedBuilder = require('../lib/embed'); +const { logTicketEvent } = require('../lib/logging'); +const Cryptr = require('cryptr'); +const { + encrypt, + decrypt, +} = new Cryptr(process.env.ENCRYPTION_KEY); + +module.exports = class TopicModal extends Modal { + constructor(client, options) { + super(client, { + ...options, + id: 'topic', + }); + } + + async run(id, interaction) { + /** @type {import("client")} */ + const client = this.client; + + if (id.edit) { + await interaction.deferReply({ ephemeral: true }); + const topic = interaction.fields.getTextInputValue('topic'); + const select = { + createdById: true, + guild: { + select: { + footer: true, + locale: true, + successColour: true, + }, + }, + id: true, + openingMessageId: true, + topic: true, + }; + const original = await client.prisma.ticket.findUnique({ + select, + where: { id: interaction.channel.id }, + }); + const ticket = await client.prisma.ticket.update({ + data: { topic: topic ? encrypt(topic) : null }, + select, + where: { id: interaction.channel.id }, + }); + const getMessage = client.i18n.getLocale(ticket.guild.locale); + + if (topic) interaction.channel.setTopic(`<@${ticket.createdById}> | ${topic}`); + + const opening = await interaction.channel.messages.fetch(ticket.openingMessageId); + if (opening && opening.embeds.length >= 2) { + const embeds = [...opening.embeds]; + embeds[1] = new EmbedBuilder(embeds[1].data) + .setFields({ + name: getMessage('ticket.opening_message.fields.topic'), + value: topic, + }); + await opening.edit({ embeds }); + } + + await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.successColour) + .setTitle(getMessage('ticket.edited.title')) + .setDescription(getMessage('ticket.edited.description')), + ], + }); + + /** @param {ticket} ticket */ + const makeDiff = ticket => { + const diff = {}; + diff[getMessage('ticket.opening_message.fields.topic')] = ticket.topic ? decrypt(ticket.topic) : ' '; + return diff; + }; + + logTicketEvent(this.client, { + action: 'update', + diff: { + original: makeDiff(original), + updated: makeDiff(ticket), + }, + target: { + id: ticket.id, + name: `<#${ticket.id}>`, + }, + userId: interaction.user.id, + }); + } else { + await this.client.tickets.postQuestions({ + ...id, + interaction, + }); + } + } +}; \ No newline at end of file diff --git a/src/modules/commands/command.js b/src/modules/commands/command.js deleted file mode 100644 index 8a193a9..0000000 --- a/src/modules/commands/command.js +++ /dev/null @@ -1,125 +0,0 @@ -const { - Message, // eslint-disable-line no-unused-vars - Interaction // eslint-disable-line no-unused-vars -} = require('discord.js'); - -/** - * A command - */ -module.exports = class Command { - /** - * - * @typedef CommandOption - * @property {string} name - The option's name - * @property {number} type - The option's type (use `Command.option_types`) - * @property {string} description - The option's description - * @property {CommandOption[]} [options] - The option's options - * @property {(string|number)[]} [choices] - The option's choices - * @property {boolean} [required] - Is this arg required? Defaults to `false` - */ - /** - * Create a new Command - * @param {import('../../').Bot} client - The Discord Client - * @param {Object} data - Command data - * @param {string} data.name - The name of the command (3-32) - * @param {string} data.description - The description of the command (1-100) - * @param {boolean} [data.staff_only] - Only allow staff to use this command? - * @param {string[]} [data.permissions] - Array of permissions needed for a user to use this command - * @param {CommandOption[]} [data.options] - The command's options - */ - constructor(client, data) { - - /** The Discord Client */ - this.client = client; - - /** The CommandManager */ - this.manager = this.client.commands; - - if (typeof data !== 'object') { - throw new TypeError(`Expected type of command "data" to be an object, got "${typeof data}"`); - } - - /** - * The name of the command - * @type {string} - */ - this.name = data.name; - - /** - * The command description - * @type {string} - */ - this.description = data.description; - - /** - * Only allow staff to use this command? - * @type {boolean} - * @default false - */ - this.staff_only = data.staff_only === true; - - /** - * Array of permissions needed for a user to use this command - * @type {string[]} - */ - this.permissions = data.permissions ?? []; - - /** - * The command options - * @type {CommandOption[]} - */ - this.options = data.options ?? []; - - /** - * True if command is internal, false if it is from a plugin - * @type {boolean} - */ - this.internal = data.internal === true; - - if (!this.internal) { - /** - * The plugin this command belongs to, if any - * @type {(undefined|Plugin)} - */ - this.plugin = this.client.plugins.plugins.find(p => p.commands?.includes(this.name)); - } - - try { - this.manager.register(this); // register the command - } catch (error) { - return this.client.log.error(error); - } - } - - /** - * The code to be executed when a command is invoked - * @abstract - * @param {Interaction} interaction - The message that invoked this command - */ - async execute(interaction) { } // eslint-disable-line no-unused-vars - - async build(guild) { - return { - defaultPermission: !this.staff_only, - description: this.description, - name: this.name, - options: typeof this.options === 'function' ? await this.options(guild) : this.options - }; - } - - static get option_types() { - return { - SUB_COMMAND: 1, - SUB_COMMAND_GROUP: 2, - STRING: 3, // eslint-disable-line sort-keys - INTEGER: 4, // eslint-disable-line sort-keys - BOOLEAN: 5, // eslint-disable-line sort-keys - USER: 6, - CHANNEL: 7, // eslint-disable-line sort-keys - ROLE: 8, - MENTIONABLE: 9, // eslint-disable-line sort-keys - NUMBER: 10 - }; - } - -}; \ No newline at end of file diff --git a/src/modules/commands/manager.js b/src/modules/commands/manager.js deleted file mode 100644 index d316b1f..0000000 --- a/src/modules/commands/manager.js +++ /dev/null @@ -1,213 +0,0 @@ - -const { - Client, // eslint-disable-line no-unused-vars - Collection, - Interaction, // eslint-disable-line no-unused-vars - MessageEmbed -} = require('discord.js'); - -const fs = require('fs'); -const { path } = require('../../utils/fs'); - -/** - * Manages the loading and execution of commands - */ -module.exports = class CommandManager { - /** - * Create a CommandManager instance - * @param {import('../..').Bot} client - */ - constructor(client) { - /** The Discord Client */ - this.client = client; - - /** - * A discord.js Collection (Map) of loaded commands - * @type {Collection} - */ - this.commands = new Collection(); - } - - /** Automatically load all internal commands */ - load() { - const files = fs.readdirSync(path('./src/commands')) - .filter(file => file.endsWith('.js')); - - for (let file of files) { - try { - file = require(`../../commands/${file}`); - new file(this.client); - } catch (e) { - this.client.log.warn('An error occurred whilst loading an internal command'); - this.client.log.error(e); - } - } - } - - /** Register a command */ - register(command) { - const exists = this.commands.has(command.name); - const is_internal = (exists && command.internal) || (exists && this.commands.get(command.name).internal); - - if (is_internal) { - const plugin = this.client.plugins.plugins.find(p => p.commands.includes(command.name)); - if (plugin) this.client.log.commands(`The "${plugin.name}" plugin has overridden the internal "${command.name}" command`); - else this.client.log.commands(`An unknown plugin has overridden the internal "${command.name}" command`); - if(command.internal) return; - } else if (exists) { - throw new Error(`A non-internal command with the name "${command.name}" already exists`); - } - - this.commands.set(command.name, command); - this.client.log.commands(`Loaded "${command.name}" command`); - } - - async publish(guild) { - if (!guild) { - return this.client.guilds.cache.forEach(guild => { - this.publish(guild); - }); - } - - try { - const commands = await Promise.all(this.client.commands.commands.map(async command => await command.build(guild))); - await this.client.application.commands.set(commands, guild.id); - await this.updatePermissions(guild); - this.client.log.success(`Published ${this.client.commands.commands.size} commands to "${guild.name}"`); - } catch (error) { - this.client.log.warn('An error occurred whilst publishing the commands'); - this.client.log.error(error); - } - } - - async updatePermissions(guild) { - // guild.commands.fetch().then(async commands => { - // const permissions = []; - // const settings = await this.client.utils.getSettings(guild.id); - // const blacklist = []; - // settings.blacklist.users?.forEach(userId => { - // blacklist.push({ - // id: userId, - // permission: false, - // type: 'USER' - // }); - // }); - // settings.blacklist.roles?.forEach(roleId => { - // blacklist.push({ - // id: roleId, - // permission: false, - // type: 'ROLE' - // }); - // }); - - // const categories = await this.client.db.models.Category.findAll({ where: { guild: guild.id } }); - // const staff_roles = new Set(categories.map(category => category.roles).flat()); - - // commands.forEach(async g_cmd => { - // const cmd_permissions = [...blacklist]; - // const command = this.client.commands.commands.get(g_cmd.name); - - // if (command.staff_only) { - // cmd_permissions.push({ - // id: guild.roles.everyone.id, - // permission: false, - // type: 'ROLE' - // }); - // staff_roles.forEach(roleId => { - // cmd_permissions.push({ - // id: roleId, - // permission: true, - // type: 'ROLE' - // }); - // }); - // } - - // permissions.push({ - // id: g_cmd.id, - // permissions: cmd_permissions - // }); - // }); - - // this.client.log.debug(`Command permissions for "${guild.name}"`, require('util').inspect(permissions, { - // colors: true, - // depth: 10 - // })); - - // try { - // await guild.commands.permissions.set({ fullPermissions: permissions }); - // } catch (error) { - // this.client.log.warn('An error occurred whilst updating command permissions'); - // this.client.log.error(error); - // } - // }); - } - - /** - * Execute a command - * @param {Interaction} interaction - Command message - */ - async handle(interaction) { - if (!interaction.guild) return this.client.log.debug('Ignoring non-guild command interaction'); - const settings = await this.client.utils.getSettings(interaction.guild.id); - const i18n = this.client.i18n.getLocale(settings.locale); - - const command = this.commands.get(interaction.commandName); - if (!command) return; - - const bot_permissions = interaction.guild.me.permissionsIn(interaction.channel); - const required_bot_permissions = [ - 'ATTACH_FILES', - 'EMBED_LINKS', - 'MANAGE_CHANNELS', - 'MANAGE_MESSAGES' - ]; - - if (!bot_permissions.has(required_bot_permissions)) { - const perms = required_bot_permissions.map(p => `\`${p}\``).join(', '); - if (bot_permissions.has('EMBED_LINKS')) { - await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor('ORANGE') - .setTitle(i18n('bot.missing_permissions.title')) - .setDescription(i18n('bot.missing_permissions.description', perms)) - ] - }); - } else { - await interaction.reply({ content: i18n('bot.missing_permissions.description', perms) }); - } - return; - } - - const missing_permissions = command.permissions instanceof Array && !interaction.member.permissions.has(command.permissions); - if (missing_permissions) { - const perms = command.permissions.map(p => `\`${p}\``).join(', '); - return await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('missing_permissions.title')) - .setDescription(i18n('missing_permissions.description', perms)) - ], - ephemeral: true - }); - } - - try { - this.client.log.commands(`Executing "${command.name}" command (invoked by ${interaction.user.tag})`); - await command.execute(interaction); // execute the command - } catch (e) { - this.client.log.warn(`An error occurred whilst executing the ${command.name} command`); - this.client.log.error(e); - await interaction.reply({ - embeds: [ - new MessageEmbed() - .setColor('ORANGE') - .setTitle(i18n('command_execution_error.title')) - .setDescription(i18n('command_execution_error.description')) - ] - }); // hopefully no user will ever see this message - } - } - -}; diff --git a/src/modules/listeners/listener.js b/src/modules/listeners/listener.js deleted file mode 100644 index f7202d6..0000000 --- a/src/modules/listeners/listener.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = class EventListener { - - /** - * - * @param {import("../..").Bot} client - * @param {*} data - */ - constructor(client, data) { - this.client = client; - this.event = data.event; - this.raw = data.raw || false; - this.once = data.once || false; - } - -}; \ No newline at end of file diff --git a/src/modules/listeners/loader.js b/src/modules/listeners/loader.js deleted file mode 100644 index b0c601c..0000000 --- a/src/modules/listeners/loader.js +++ /dev/null @@ -1,36 +0,0 @@ -const fs = require('fs'); -const { path } = require('../../utils/fs'); - -/** - * Manages the loading of event listeners - */ -module.exports = class ListenerLoader { - /** - * Create a ListenerLoader instance - * @param {import('../..').Bot} client - */ - constructor(client) { - /** The Discord Client */ - this.client = client; - } - - load() { - const files = fs.readdirSync(path('./src/listeners')) - .filter(file => file.endsWith('.js')); - - for (let file of files) { - try { - file = require(`../../listeners/${file}`); - const listener = new file(this.client); - const on = listener.once ? 'once' : 'on'; - if (listener.raw) this.client.ws[on](listener.event, (...data) => listener.execute(...data)); - else this.client[on](listener.event, (...data) => listener.execute(...data)); - } catch (e) { - this.client.log.warn('An error occurred whilst loading a listener'); - this.client.log.error(e); - } - } - - } - -}; \ No newline at end of file diff --git a/src/modules/plugins/manager.js b/src/modules/plugins/manager.js deleted file mode 100644 index 1a6b89d..0000000 --- a/src/modules/plugins/manager.js +++ /dev/null @@ -1,94 +0,0 @@ -// eslint-disable-next-line no-unused-vars -const { Collection } = require('discord.js'); -// eslint-disable-next-line no-unused-vars -const Plugin = require('./plugin'); - -/** - * Manages the loading of plugins - */ -module.exports = class PluginManager { - /** - * Create a PluginManager instance - * @param {import('../..').Bot} client - */ - constructor(client) { - /** The Discord Client */ - this.client = client; - - /** - * A discord.js Collection (Map) of loaded plugins - * @type {Collection} - */ - this.plugins = new Collection(); - - /** Array of official plugins to be used to check if a plugin is official */ - this.official = [ - 'dsctickets.settings-server', - 'dsctickets.portal', - 'dsctickets.text-transcripts' - ]; - } - - handleError(id) { - if (!this.official.includes(id)) this.client.log.notice(`"${id}" is NOT an official plugin, please do not ask for help with it in the Discord Tickets support server, seek help from the plugin author instead.`); - } - - /** - * Register and load a plugin - * @param {Plugin} plugin - the Plugin class - * @param {Object} pkg - contents of package.json - */ - register(plugin, pkg) { - let { - name: id, - version, - author, - description - } = pkg; - - if (this.plugins.has(id)) { - this.client.log.warn(`(PLUGINS) A plugin with the ID "${id}" is already loaded, skipping`); - return; - } - - if (typeof author === 'object') { - author = author.name ?? 'unknown'; - } - - const about = { - author, - description, - id, - version - }; - - try { - plugin = new (plugin(Plugin))(this.client, about); - this.plugins.set(id, plugin); - this.client.log.plugins(`Loading "${plugin.name}" v${version} by ${author}`); - plugin.preload(); - } catch (e) { - this.handleError(id); - this.client.log.warn(`An error occurred whilst loading the "${id}" plugin`); - this.client.log.error(e); - process.exit(); - } - } - - /** Automatically register and load plugins */ - load() { - this.client.config.plugins.forEach(plugin => { - try { - const main = require(plugin); - const pkg = require(`${plugin}/package.json`); - this.register(main, pkg); - } catch (e) { - this.handleError(plugin); - this.client.log.warn(`An error occurred whilst loading ${plugin}; have you installed it?`); - this.client.log.error(e); - process.exit(); - } - }); - } - -}; diff --git a/src/modules/plugins/plugin.js b/src/modules/plugins/plugin.js deleted file mode 100644 index d8df69f..0000000 --- a/src/modules/plugins/plugin.js +++ /dev/null @@ -1,141 +0,0 @@ -/* eslint-disable no-unused-vars */ -const { Client } = require('discord.js'); -const Command = require('../commands/command'); -const fs = require('fs'); -const { join } = require('path'); -const { path } = require('../../utils/fs'); - -/** - * A plugin - */ -module.exports = class Plugin { - /** - * Create a new Plugin - * @param {import('../..').Bot} client The Discord Client - * @param {String} id The plugin ID - * @param {Object} options Plugin options - * @param {String} options.name A human-friendly name (can be different to the name in package.json) - * @param {String[]} options.commands An array of command names the plugin registers - */ - constructor(client, about, options = {}) { - /** The Discord Client */ - this.client = client; - - /** The PluginManager */ - this.manager = this.client.plugins; - - /** An official plugin? */ - this.official = this.manager.official.includes(this.id); - - const { - id, - version, - author, - description - } = about; - - /** - * The human-friendly name of the plugin - * @type {string} - */ - this.name = options.name ?? id; - - /** - * An array of commands from this plugin - * @type {string[]} - */ - this.commands = options.commands; - - /** - * The unique ID of the plugin (NPM package name) - * @type {string} - */ - this.id = id; - - /** - * The version of the plugin (NPM package version) - * @type {string} - */ - this.version = version; - - /** - * The plugin author's name (NPM package author) - * @type {(undefined|string)} - */ - this.author = author; - - /** - * The plugin description (NPM package description) - * @type {string} - */ - this.description = description; - - const clean = this.id.replace(/@[-_a-zA-Z0-9]+\//, ''); - - /** - * Information about the plugin directory - * @property {string} name - A cleaned version of the plugin's ID suitable for use in the directory name - * @property {string} path - The absolute path of the plugin directory - */ - this.directory = { - name: clean, - path: path(`./user/plugins/${clean}`) - }; - } - - /** - * Create the plugin directory if it doesn't already exist - * @returns {Boolean} True if created, false if it already existed - */ - createDirectory() { - if (!fs.existsSync(this.directory.path)) { - this.client.log.plugins(`Creating plugin directory for "${this.name}"`); - fs.mkdirSync(this.directory.path); - return true; - } else { - return false; - } - } - - /** - * Create the plugin config file if it doesn't already exist - * @param {Object} template The default config template - * @returns {Boolean} True if created, false if it already existed - */ - createConfig(template) { - this.createDirectory(); - const file = join(this.directory.path, 'config.json'); - if (!fs.existsSync(file)) { - this.client.log.plugins(`Creating plugin config file for "${this.name}"`); - fs.writeFileSync(file, JSON.stringify(template, null, 2)); - return true; - } else { - return false; - } - } - - /** - * Reset the plugin config file to the defaults - * @param {Object} template The default config template - */ - resetConfig(template) { - this.createDirectory(); - const file = join(this.directory.path, 'config.json'); - this.client.log.plugins(`Resetting plugin config file for "${this.name}"`); - fs.writeFileSync(file, JSON.stringify(template, null, 2)); - } - - /** - * The function where any code that needs to be executed before the client is ready should go. - * **This is executed _BEFORE_ the ready event** - * @abstract - */ - preload() { } - - /** - * The main function where your code should go. Create commands and event listeners here. - * **This is executed _after_ the ready event** - * @abstract - */ - load() {} -}; \ No newline at end of file diff --git a/src/modules/tickets/archives.js b/src/modules/tickets/archives.js deleted file mode 100644 index 8278c37..0000000 --- a/src/modules/tickets/archives.js +++ /dev/null @@ -1,202 +0,0 @@ -const { int2hex } = require('../../utils'); - -/** Manages ticket archiving */ -module.exports = class TicketArchives { - /** - * Create a TicketArchives instance - * @param {import('../..').Bot} client - */ - constructor(client) { - - /** The Discord Client */ - this.client = client; - - this.encrypt = this.client.cryptr.encrypt; - this.decrypt = this.client.cryptr.decrypt; - - } - - async addMessage(message) { - try { - // await this.client.db.transaction(async t => { - const t_row = await this.client.db.models.Ticket.findOne({ - where: { id: message.channel.id } - /* transaction: t */ - }); - - if (t_row) { - await this.client.db.models.Message.create({ - author: message.author.id, - createdAt: new Date(message.createdTimestamp), - data: this.encrypt(JSON.stringify({ - attachments: [...message.attachments.values()], - content: message.content, - embeds: message.embeds.map(embed => ({ embed })) - })), - id: message.id, - ticket: t_row.id - } /* { transaction: t } */); - - await this.updateEntities(message); - } - // }); - } catch (e) { - this.client.log.warn('Failed to add a message to the ticket archive'); - this.client.log.error(e); - } - } - - async updateMessage(message) { - try { - // await this.client.db.transaction(async t => { - const m_row = await this.client.db.models.Message.findOne({ - where: { id: message.id } - /* transaction: t */ - }); - - if (m_row) { - m_row.data = this.encrypt(JSON.stringify({ - attachments: [...message.attachments.values()], - content: message.content, - embeds: message.embeds.map(embed => ({ embed })) - })); - - if (message.editedTimestamp) { - m_row.edited = true; - await this.updateEntities(message); - } - - await m_row.save(/* { transaction: t } */); // save changes - } - // }); - } catch (e) { - this.client.log.warn('Failed to update message in the ticket archive'); - this.client.log.error(e); - } - } - - async deleteMessage(message) { - try { - // await this.client.db.transaction(async t => { - const msg = await this.client.db.models.Message.findOne({ - where: { id: message.id } - /* transaction: t */ - }); - - if (msg) { - msg.deleted = true; - await msg.save(/* { transaction: t } */); // save changes to message row - } - // }); - } catch (e) { - this.client.log.warn('Failed to delete message in ticket archive'); - this.client.log.error(e); - } - } - - async updateEntities(message) { - // message author - await this.updateMember(message.channel.id, message.member); - - // mentioned members - message.mentions.members.forEach(async member => { - await this.updateMember(message.channel.id, member); - }); - - // mentioned channels - message.mentions.channels.forEach(async channel => { - await this.updateChannel(message.channel.id, channel); - }); - - // mentioned roles - message.mentions.roles.forEach(async role => { - await this.updateRole(message.channel.id, role); - }); - } - - async updateMember(ticket_id, member) { - await this.updateRole(ticket_id, member.roles.highest); - - try { - // await this.client.db.transaction(async t => { - const u_model_data = { - ticket: ticket_id, - user: member.user.id - }; - - const [u_row] = await this.client.db.models.UserEntity.findOrCreate({ - defaults: { - ...u_model_data, - role: member.roles.highest.id - }, - where: u_model_data - /* transaction: t */ - }); - - await u_row.update({ - avatar: member.user.avatar, - bot: member.user.bot, - discriminator: member.user.discriminator, - display_name: this.encrypt(member.displayName), - role: member.roles.highest.id, - username: this.encrypt(member.user.username) - } /* { transaction: t } */); - - return u_row; - // }); - } catch (e) { - this.client.log.warn('Failed to update message author entity in ticket archive'); - this.client.log.error(e); - } - } - - async updateChannel(ticket_id, channel) { - try { - // await this.client.db.transaction(async t => { - const c_model_data = { - channel: channel.id, - ticket: ticket_id - }; - const [c_row] = await this.client.db.models.ChannelEntity.findOrCreate({ - defaults: c_model_data, - where: c_model_data - /* transaction: t */ - }); - - await c_row.update({ name: this.encrypt(channel.name) } /* { transaction: t } */); - - return c_row; - // }); - } catch (e) { - this.client.log.warn('Failed to update mentioned channels entities in ticket archive'); - this.client.log.error(e); - } - } - - async updateRole(ticket_id, role) { - try { - // await this.client.db.transaction(async t => { - const r_model_data = { - role: role.id, - ticket: ticket_id - }; - const [r_row] = await this.client.db.models.RoleEntity.findOrCreate({ - defaults: r_model_data, - where: r_model_data - /* transaction: t */ - }); - - await r_row.update({ - colour: role.color === 0 ? '7289DA' : int2hex(role.color), // 7289DA = 7506394 - name: this.encrypt(role.name) - } /* { transaction: t } */); - - return r_row; - // }); - } catch (e) { - this.client.log.warn('Failed to update mentioned roles entities in ticket archive'); - this.client.log.error(e); - } - } - -}; \ No newline at end of file diff --git a/src/modules/tickets/manager.js b/src/modules/tickets/manager.js deleted file mode 100644 index 8a57e84..0000000 --- a/src/modules/tickets/manager.js +++ /dev/null @@ -1,449 +0,0 @@ -/* eslint-disable max-lines */ -const EventEmitter = require('events'); -const TicketArchives = require('./archives'); -const { - MessageActionRow, - MessageButton, - MessageEmbed -} = require('discord.js'); - -/** Manages tickets */ -module.exports = class TicketManager extends EventEmitter { - /** - * Create a TicketManager instance - * @param {import('../..').Bot} client - */ - constructor(client) { - super(); - - /** The Discord Client */ - this.client = client; - - this.setMaxListeners(this.client.config.max_listeners); - - this.archives = new TicketArchives(this.client); - } - - /** - * Create a new ticket - * @param {string} guild_id - ID of the guild to create the ticket in - * @param {string} creator_id - ID of the ticket creator (user) - * @param {string} category_id - ID of the ticket category - * @param {string} [topic] - The ticket topic - */ - async create(guild_id, creator_id, category_id, topic) { - if (!topic) topic = ''; - - const cat_row = await this.client.db.models.Category.findOne({ where: { id: category_id } }); - - if (!cat_row) throw new Error('Ticket category does not exist'); - - const cat_channel = await this.client.channels.fetch(category_id); - - if (cat_channel.children.size >= 50) throw new Error('Ticket category has reached child channel limit (50)'); - - const number = (await this.client.db.models.Ticket.count({ where: { guild: guild_id } })) + 1; - - const guild = this.client.guilds.cache.get(guild_id); - const creator = await guild.members.fetch(creator_id); - const name = cat_row.name_format - .replace(/{+\s?(user)?name\s?}+/gi, creator.displayName) - .replace(/{+\s?num(ber)?\s?}+/gi, number); - - const t_channel = await guild.channels.create(name, { - parent: category_id, - reason: `${creator.user.tag} requested a new ticket channel`, - topic: `${creator}${topic.length > 0 ? ` | ${topic}` : ''}`, - type: 'GUILD_TEXT' - }); - - t_channel.permissionOverwrites.edit(creator_id, { - ATTACH_FILES: true, - READ_MESSAGE_HISTORY: true, - SEND_MESSAGES: true, - VIEW_CHANNEL: true - }, `Ticket channel created by ${creator.user.tag}`); - - const t_row = await this.client.db.models.Ticket.create({ - category: category_id, - creator: creator_id, - guild: guild_id, - id: t_channel.id, - number, - topic: topic.length === 0 ? null : this.client.cryptr.encrypt(topic) - }); - - (async () => { - const settings = await this.client.utils.getSettings(guild.id); - const i18n = this.client.i18n.getLocale(settings.locale); - - topic = t_row.topic - ? this.client.cryptr.decrypt(t_row.topic) - : ''; - - if (cat_row.image) { - await t_channel.send({ content: cat_row.image }); - } - - const description = cat_row.opening_message - .replace(/{+\s?(user)?name\s?}+/gi, creator.displayName) - .replace(/{+\s?(tag|ping|mention)?\s?}+/gi, creator.user.toString()); - const embed = new MessageEmbed() - .setColor(settings.colour) - .setAuthor(creator.user.username, creator.user.displayAvatarURL()) - .setDescription(description) - .setFooter(settings.footer, guild.iconURL()); - - if (topic) embed.addField(i18n('ticket.opening_message.fields.topic'), topic); - - const components = new MessageActionRow(); - - if (cat_row.claiming) { - components.addComponents( - new MessageButton() - .setCustomId('ticket.claim') - .setLabel(i18n('ticket.claim')) - .setEmoji('🙌') - .setStyle('SECONDARY') - ); - } - - if (settings.close_button) { - components.addComponents( - new MessageButton() - .setCustomId('ticket.close') - .setLabel(i18n('ticket.close')) - .setEmoji('✖️') - .setStyle('DANGER') - ); - } - - const mentions = cat_row.ping instanceof Array && cat_row.ping.length > 0 - ? cat_row.ping.map(id => id === 'everyone' - ? '@everyone' - : id === 'here' - ? '@here' - : `<@&${id}>`) - .join(', ') - : ''; - const sent = await t_channel.send({ - components: cat_row.claiming || settings.close_button ? [components] : [], - content: i18n('ticket.opening_message.content', mentions, creator.user.toString()), - embeds: [embed] - }); - await sent.pin({ reason: 'Ticket opening message' }); - - await t_row.update({ opening_message: sent.id }); - - const pinned = t_channel.messages.cache.last(); - - if (pinned.system) { - pinned - .delete({ reason: 'Cleaning up system message' }) - .catch(() => this.client.log.warn('Failed to delete system pin message')); - } - - let questions; - if (cat_row.opening_questions) { - questions = cat_row.opening_questions - .map((q, index) => `**${index + 1}.** ${q}`) - .join('\n\n'); - } - - if (cat_row.require_topic && topic.length === 0) { - const collector_message = await t_channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.new.request_topic.title')) - .setDescription(i18n('commands.new.request_topic.description')) - .setFooter(this.client.utils.footer(settings.footer, i18n('collector_expires_in', 120)), guild.iconURL()) - ] - }); - - const filter = message => message.author.id === t_row.creator; - const collector = t_channel.createMessageCollector({ - filter, - time: 120000 - }); - - collector.on('collect', async message => { - topic = message.content; - await t_row.update({ topic: this.client.cryptr.encrypt(topic) }); - await t_channel.setTopic(`${creator} | ${topic}`, { reason: 'User updated ticket topic' }); - await sent.edit( - new MessageEmbed() - .setColor(settings.colour) - .setAuthor(creator.user.username, creator.user.displayAvatarURL()) - .setDescription(description) - .addField(i18n('ticket.opening_message.fields.topic'), topic) - .setFooter(settings.footer, guild.iconURL()) - ); - await message.react('✅'); - collector.stop(); - }); - - collector.on('end', async () => { - collector_message - .delete() - .catch(() => this.client.log.warn('Failed to delete topic collector message')); - if (cat_row.opening_questions) { - await t_channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setDescription(i18n('ticket.questions', questions)) - .setFooter(settings.footer, guild.iconURL()) - ] - }); - } - }); - } else { - if (cat_row.opening_questions) { - await t_channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setDescription(i18n('ticket.questions', questions)) - .setFooter(settings.footer, guild.iconURL()) - ] - }); - } - } - })(); - - this.client.log.info(`${creator.user.tag} created a new ticket in "${guild.name}"`); - - this.emit('create', t_row.id, creator_id); - - return t_row; - } - - /** - * Close a ticket - * @param {(string|number)} ticket_id - The channel ID, or the ticket number - * @param {string?} closer_id - ID of the member who is closing the ticket, or null - * @param {string} [guild_id] - The ID of the ticket's guild (used if a ticket number is provided instead of ID) - * @param {string} [reason] - The reason for closing the ticket - */ - async close(ticket_id, closer_id, guild_id, reason) { - const t_row = await this.resolve(ticket_id, guild_id); - if (!t_row) throw new Error(`A ticket with the ID or number "${ticket_id}" could not be resolved`); - ticket_id = t_row.id; - - this.emit('beforeClose', ticket_id); - - const guild = this.client.guilds.cache.get(t_row.guild); - const settings = await this.client.utils.getSettings(guild.id); - const i18n = this.client.i18n.getLocale(settings.locale); - const channel = await this.client.channels.cache.find(channel => - channel.id === t_row.id - ) ? await this.client.channels.fetch(t_row.id) : null; - - const close = async () => { - let pinned; - if (channel) { - pinned = await channel.messages.fetchPinned(); - } - await t_row.update({ - closed_by: closer_id || null, - closed_reason: reason ? this.client.cryptr.encrypt(reason) : null, - open: false, - pinned_messages: pinned ? [...pinned.keys()] : [] - }); - - if (closer_id) { - const closer = await guild.members.fetch(closer_id); - - await this.archives.updateMember(ticket_id, closer); - - if (channel) { - const description = reason - ? i18n('ticket.closed_by_member_with_reason.description', closer.user.toString(), reason) - : i18n('ticket.closed_by_member.description', closer.user.toString()); - await channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setAuthor(closer.user.username, closer.user.displayAvatarURL()) - .setTitle(i18n('ticket.closed.title')) - .setDescription(description) - .setFooter(settings.footer, guild.iconURL()) - ] - }); - - setTimeout(async () => { - await channel.delete(`Ticket channel closed by ${closer.user.tag}${reason ? `: "${reason}"` : ''}`); - }, 5000); - } - - this.client.log.info(`${closer.user.tag} closed a ticket (${ticket_id})${reason ? `: "${reason}"` : ''}`); - } else { - if (channel) { - const description = reason - ? i18n('ticket.closed_with_reason.description') - : i18n('ticket.closed.description'); - await channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setTitle(i18n('ticket.closed.title')) - .setDescription(description) - .setFooter(settings.footer, guild.iconURL()) - ] - }); - - setTimeout(async () => { - await channel.delete(`Ticket channel closed${reason ? `: "${reason}"` : ''}`); - }, 5000); - } - - this.client.log.info(`A ticket was closed (${ticket_id})${reason ? `: "${reason}"` : ''}`); - } - }; - - if (channel) { - guild.members.fetch(t_row.creator) - .then(async creator => { - const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } }); - if (creator && cat_row.survey) { - const survey = await this.client.db.models.Survey.findOne({ - where: { - guild: t_row.guild, - name: cat_row.survey - } - }); - - if (survey) { - await channel.send({ - components: [ - new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId(`start_survey:${channel.id}`) - .setLabel(i18n('ticket.survey.start.buttons.start')) - .setEmoji('✅') - .setStyle('SUCCESS') - ) - .addComponents( - new MessageButton() - .setCustomId(`ignore_survey:${channel.id}`) - .setLabel(i18n('ticket.survey.start.buttons.ignore')) - .setEmoji('❌') - .setStyle('SECONDARY') - ) - ], - content: creator.toString(), - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('ticket.survey.start.title')) - .setDescription(i18n('ticket.survey.start.description', creator.toString(), survey.questions.length)) - .setFooter(i18n('collector_expires_in', 60)) - ] - }); - - const filter = i => i.user.id === creator.id && i.customId.includes(channel.id); - const collector = channel.createMessageComponentCollector({ - filter, - time: 60000 - }); - - collector.on('collect', async i => { - await i.deferUpdate(); - - if (i.customId === `start_survey:${channel.id}`) { - const filter = message => message.author.id === creator.id; - let answers = []; - let number = 1; - for (const question of survey.questions) { - await channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(`${number++}/${survey.questions.length}`) - .setDescription(question) - .setFooter(i18n('collector_expires_in', 60)) - ] - }); - - try { - const collected = await channel.awaitMessages({ - errors: ['time'], - filter, - max: 1, - time: 60000 - }); - answers.push(collected.first().content); - } catch (collected) { - await close(); - } - } - - await channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setTitle(i18n('ticket.survey.complete.title')) - .setDescription(i18n('ticket.survey.complete.description')) - .setFooter(settings.footer, guild.iconURL()) - ] - }); - - answers = answers.map(a => this.client.cryptr.encrypt(a)); - await this.client.db.models.SurveyResponse.create({ - answers, - survey: survey.id, - ticket: t_row.id - }); - await close(); - } else { - await close(); - } - - collector.stop(); - }); - - collector.on('end', async collected => { - if (collected.size === 0) { - await close(); - } - }); - } - } else { - await close(); - } - }) - .catch(async error => { - this.client.log.debug(error); - await close(); - }); - } else { - await close(); - } - - this.emit('close', ticket_id); - return t_row; - } - - /** - * - * @param {(string|number)} ticket_id - ID or number of the ticket - * @param {string} [guild_id] - The ID of the ticket's guild (used if a ticket number is provided instead of ID) - */ - async resolve(ticket_id, guild_id) { - let t_row; - - if (!(t_row = await this.client.db.models.Ticket.findOne({ where: { id: ticket_id } }))) { - t_row = await this.client.db.models.Ticket.findOne({ - where: { - guild: guild_id, - number: ticket_id - } - }); - } - - return t_row; - } - -}; \ No newline at end of file diff --git a/src/routes/api/admin/guilds/[guild]/categories/[category]/index.js b/src/routes/api/admin/guilds/[guild]/categories/[category]/index.js new file mode 100644 index 0000000..413cfad --- /dev/null +++ b/src/routes/api/admin/guilds/[guild]/categories/[category]/index.js @@ -0,0 +1,198 @@ +const { logAdminEvent } = require('../../../../../../../lib/logging'); +const { updateStaffRoles } = require('../../../../../../../lib/users'); +const { randomUUID } = require('crypto'); +const { ApplicationCommandPermissionType } = require('discord.js'); + +module.exports.delete = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + const guild = client.guilds.cache.get(req.params.guild); + const categoryId = Number(req.params.category); + const original = categoryId && await client.prisma.category.findUnique({ where: { id: categoryId } }); + if (!original || original.guildId !== guild.id) return res.status(404).send(new Error('Not Found')); + const category = await client.prisma.category.delete({ where: { id: categoryId } }); + + await updateStaffRoles(guild); + + logAdminEvent(client, { + action: 'delete', + guildId: req.params.guild, + target: { + id: category.id, + name: category.name, + type: 'category', + }, + userId: req.user.id, + }); + + return category; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); + +module.exports.get = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + const guildId = req.params.guild; + const categoryId = Number(req.params.category); + const category = await client.prisma.category.findUnique({ + include: { + questions: { + select: { + // createdAt: true, + id: true, + label: true, + maxLength: true, + minLength: true, + options: true, + order: true, + placeholder: true, + required: true, + style: true, + type: true, + value: true, + }, + }, + }, + where: { id: categoryId }, + }); + + if (!category || category.guildId !== guildId) return res.status(404).send(new Error('Not Found')); + + return category; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); + +module.exports.patch = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + const guildId = req.params.guild; + const categoryId = Number(req.params.category); + /** @type {import('discord.js').Guild} */ + const guild = client.guilds.cache.get(req.params.guild); + const data = req.body; + + const select = { + channelName: true, + claiming: true, + // createdAt: true, + description: true, + discordCategory: true, + emoji: true, + enableFeedback: true, + guildId: true, + id: true, + image: true, + memberLimit: true, + name: true, + openingMessage: true, + pingRoles: true, + questions: { + select: { + // createdAt: true, + id: true, + label: true, + maxLength: true, + minLength: true, + options: true, + order: true, + placeholder: true, + required: true, + style: true, + type: true, + value: true, + }, + }, + ratelimit: true, + requireTopic: true, + requiredRoles: true, + staffRoles: true, + totalLimit: true, + }; + + const original = req.params.category && await client.prisma.category.findUnique({ + select, + where: { id: categoryId }, + }); + + if (!original || original.guildId !== guildId) return res.status(404).send(new Error('Not Found')); + + if (data.hasOwnProperty('id')) delete data.id; + if (data.hasOwnProperty('createdAt')) delete data.createdAt; + + const category = await client.prisma.category.update({ + data: { + ...data, + questions: { + upsert: data.questions?.map(q => { + if (!q.id) q.id = randomUUID(); + return { + create: q, + update: q, + where: { id: q.id }, + }; + }), + }, + }, + select, + where: { id: categoryId }, + }); + + // update caches + await client.tickets.getCategory(categoryId, true); + await updateStaffRoles(guild); + + if (req.user.accessToken && JSON.stringify(category.staffRoles) !== JSON.stringify(original.staffRoles)) { + Promise.all([ + 'Create ticket for user', + 'claim', + 'force-close', + 'move', + 'priority', + 'release', + ].map(name => + client.application.commands.permissions.set({ + command: client.application.commands.cache.find(cmd => cmd.name === name), + guild, + permissions: [ + { + id: guild.id, // @everyone + permission: false, + type: ApplicationCommandPermissionType.Role, + }, + ...category.staffRoles.map(id => ({ + id, + permission: true, + type: ApplicationCommandPermissionType.Role, + })), + ], + token: req.user.accessToken, + }), + )) + .then(() => client.log.success('Updated application command permissions in "%s"', guild.name)) + .catch(error => client.log.error(error)); + } + + logAdminEvent(client, { + action: 'update', + diff: { + original, + updated: category, + }, + guildId: guild.id, + target: { + id: category.id, + name: category.name, + type: 'category', + }, + userId: req.user.id, + }); + + return category; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); diff --git a/src/routes/api/admin/guilds/[guild]/categories/[category]/questions/[question].js b/src/routes/api/admin/guilds/[guild]/categories/[category]/questions/[question].js new file mode 100644 index 0000000..160c20b --- /dev/null +++ b/src/routes/api/admin/guilds/[guild]/categories/[category]/questions/[question].js @@ -0,0 +1,29 @@ +const { logAdminEvent } = require('../../../../../../../../lib/logging'); + +module.exports.delete = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + const guildId = req.params.guild; + const categoryId = Number(req.params.category); + const questionId = req.params.question; + const original = questionId && await client.prisma.question.findUnique({ where: { id: questionId } }); + const category = categoryId && await client.prisma.category.findUnique({ where: { id: categoryId } }); + if (original?.categoryId !== categoryId || category.guildId !== guildId) return res.status(404).send(new Error('Not Found')); + const question = await client.prisma.question.delete({ where: { id: questionId } }); + + logAdminEvent(client, { + action: 'delete', + guildId: req.params.guild, + target: { + id: question.id, + name: question.label, + type: 'question', + }, + userId: req.user.id, + }); + + return question; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); diff --git a/src/routes/api/admin/guilds/[guild]/categories/index.js b/src/routes/api/admin/guilds/[guild]/categories/index.js new file mode 100644 index 0000000..409baa2 --- /dev/null +++ b/src/routes/api/admin/guilds/[guild]/categories/index.js @@ -0,0 +1,153 @@ +const { logAdminEvent } = require('../../../../../../lib/logging'); +const { updateStaffRoles } = require('../../../../../../lib/users'); +const emoji = require('node-emoji'); +const { + ApplicationCommandPermissionType, + ChannelType: { GuildCategory }, +} = require('discord.js'); +const ms = require('ms'); +const { + getAvgResolutionTime, + getAvgResponseTime, +} = require('../../../../../../lib/stats'); + +module.exports.get = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + + let { categories } = await client.prisma.guild.findUnique({ + select: { + categories: { + select: { + createdAt: true, + description: true, + discordCategory: true, + emoji: true, + id: true, + image: true, + name: true, + requiredRoles: true, + staffRoles: true, + tickets: { where: { open: false } }, + }, + }, + }, + where: { id: req.params.guild }, + }); + categories = categories.map(c => { + const closedTickets = c.tickets.filter(t => t.firstResponseAt && t.closedAt); + c = { + ...c, + stats: { + avgResolutionTime: ms(getAvgResolutionTime(closedTickets)), + avgResponseTime: ms(getAvgResponseTime(closedTickets)), + }, + }; + delete c.tickets; + return c; + }); + + return categories; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); + +module.exports.post = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + + const user = await client.users.fetch(req.user.id); + const guild = client.guilds.cache.get(req.params.guild); + const data = req.body; + const allow = ['ViewChannel', 'ReadMessageHistory', 'SendMessages', 'EmbedLinks', 'AttachFiles']; + + if (!data.discordCategory) { + let name = data.name; + if (emoji.hasEmoji(data.emoji)) name = `${emoji.get(data.emoji)} ${name}`; + const channel = await guild.channels.create({ + name, + permissionOverwrites: [ + ...[ + { + deny: ['ViewChannel'], + id: guild.roles.everyone, + }, + { + allow: allow, + id: client.user.id, + }, + ], + ...data.staffRoles.map(id => ({ + allow: allow, + id, + })), + ], + position: 1, + reason: `Tickets category created by ${user.tag}`, + type: GuildCategory, + }); + data.discordCategory = channel.id; + } + + data.channelName ||= 'ticket-{num}'; // not ??=, expect empty string + + const category = await client.prisma.category.create({ + data: { + guild: { connect: { id: guild.id } }, + ...data, + questions: { createMany: { data: data.questions ?? [] } }, + }, + }); + + // update caches + await client.tickets.getCategory(category.id, true); + await updateStaffRoles(guild); + + if (req.user.accessToken) { + Promise.all([ + 'Create ticket for user', + 'claim', + 'force-close', + 'move', + 'priority', + 'release', + ].map(name => + client.application.commands.permissions.set({ + command: client.application.commands.cache.find(cmd => cmd.name === name), + guild, + permissions: [ + { + id: guild.id, // @everyone + permission: false, + type: ApplicationCommandPermissionType.Role, + }, + ...category.staffRoles.map(id => ({ + id, + permission: true, + type: ApplicationCommandPermissionType.Role, + })), + ], + token: req.user.accessToken, + }), + )) + .then(() => client.log.success('Updated application command permissions in "%s"', guild.name)) + .catch(error => client.log.error(error)); + } + + logAdminEvent(client, { + action: 'create', + guildId: guild.id, + target: { + id: category.id, + name: category.name, + type: 'category', + }, + userId: req.user.id, + }); + + return category; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); diff --git a/src/routes/api/admin/guilds/[guild]/data.js b/src/routes/api/admin/guilds/[guild]/data.js new file mode 100644 index 0000000..0a3e8c8 --- /dev/null +++ b/src/routes/api/admin/guilds/[guild]/data.js @@ -0,0 +1,13 @@ +module.exports.get = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + const id = req.params.guild; + const guild = client.guilds.cache.get(id) ?? {}; + const { query } = req.query; + if (!query) return {}; + const data = query.split(/\./g).reduce((acc, part) => acc && acc[part], guild); + return data; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); diff --git a/src/routes/api/admin/guilds/[guild]/index.js b/src/routes/api/admin/guilds/[guild]/index.js new file mode 100644 index 0000000..bb787fd --- /dev/null +++ b/src/routes/api/admin/guilds/[guild]/index.js @@ -0,0 +1,60 @@ +/* eslint-disable no-underscore-dangle */ +const { + getAvgResolutionTime, + getAvgResponseTime, +} = require('../../../../../lib/stats'); +const ms = require('ms'); + +module.exports.get = fastify => ({ + handler: async (req, res) => { + /** @type {import("client")} */ + const client = res.context.config.client; + const id = req.params.guild; + const cacheKey = `cache/stats/guild:${id}`; + let cached = await client.keyv.get(cacheKey); + + if (!cached) { + const guild = client.guilds.cache.get(id); + const settings = await client.prisma.guild.findUnique({ where: { id } }) ?? + await client.prisma.guild.create({ data: { id } }); + const categories = await client.prisma.category.findMany({ + select: { + _count: { select: { tickets: true } }, + id: true, + name: true, + }, + where: { guildId: id }, + }); + const tickets = await client.prisma.ticket.findMany({ + select: { + closedAt: true, + createdAt: true, + firstResponseAt: true, + }, + where: { guildId: id }, + }); + const closedTickets = tickets.filter(t => t.firstResponseAt && t.closedAt); + cached = { + createdAt: settings.createdAt, + id: guild.id, + logo: guild.iconURL(), + name: guild.name, + stats: { + avgResolutionTime: ms(getAvgResolutionTime(closedTickets)), + avgResponseTime: ms(getAvgResponseTime(closedTickets)), + categories: categories.map(c => ({ + id: c.id, + name: c.name, + tickets: c._count.tickets, + })), + tags: await client.prisma.tag.count({ where: { guildId: id } }), + tickets: tickets.length, + }, + }; + await client.keyv.set(cacheKey, cached, ms('5m')); + } + + return cached; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); diff --git a/src/routes/api/admin/guilds/[guild]/panels.js b/src/routes/api/admin/guilds/[guild]/panels.js new file mode 100644 index 0000000..50991c4 --- /dev/null +++ b/src/routes/api/admin/guilds/[guild]/panels.js @@ -0,0 +1,143 @@ +const { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle: { + Primary, + Secondary, + }, + ChannelType: { GuildText }, + EmbedBuilder, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, +} = require('discord.js'); +const emoji = require('node-emoji'); +const { logAdminEvent } = require('../../../../../lib/logging'); + +module.exports.post = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + const guild = client.guilds.cache.get(req.params.guild); + const data = req.body; + + const settings = await client.prisma.guild.findUnique({ + select: { + categories: true, + footer: true, + locale: true, + primaryColour: true, + }, + where: { id: guild.id }, + }); + const getMessage = client.i18n.getLocale(settings.locale); + const categories = settings.categories.filter(c => data.categories.includes(c.id)); + if (categories.length === 0) throw new Error('No categories'); + if (categories.length !== 1 && data.type === 'MESSAGE') throw new Error('Invalid number of categories for panel type'); + + let channel; + if (data.channel) { + channel = await client.channels.fetch(data.channel); + } else { + const allow = ['ViewChannel', 'ReadMessageHistory']; + if (data.type === 'MESSAGE') allow.push('SendMessages'); + channel = await guild.channels.create({ + name: 'create-a-ticket', + permissionOverwrites: [ + { + allow, + deny: ['AddReactions', 'AttachFiles'], + id: guild.roles.everyone, + }, + ], + position: 1, + rateLimitPerUser: 15, + reason: 'New ticket panel', + type: GuildText, + }); + } + + const embed = new EmbedBuilder() + .setColor(settings.primaryColour) + .setTitle(data.title); + + if (settings.footer) { + embed.setFooter({ + iconURL: guild.iconURL(), + text: settings.footer, + }); + } + + if (data.description) embed.setDescription(data.description); + if (data.image) embed.setImage(data.image); + if (data.thumbnail) embed.setThumbnail(data.thumbnail); + + if (data.type === 'MESSAGE') { + await channel.send({ embeds: [embed] }); + } else { + const components = []; + + if (categories.length === 1) { + components.push( + new ButtonBuilder() + .setCustomId(JSON.stringify({ + action: 'create', + target: categories[0].id, + })) + .setStyle(Primary) + .setLabel(getMessage('buttons.create.text')) + .setEmoji(getMessage('buttons.create.emoji')), + ); + } else if (data.type === 'BUTTON') { + components.push( + ...categories.map(category => + new ButtonBuilder() + .setCustomId(JSON.stringify({ + action: 'create', + target: category.id, + })) + .setStyle(Secondary) + .setLabel(category.name) + .setEmoji(emoji.hasEmoji(category.emoji) ? emoji.get(category.emoji) : { id: category.emoji }), + ), + ); + } else { + components.push( + new StringSelectMenuBuilder() + .setCustomId(JSON.stringify({ action: 'create' })) + .setPlaceholder(getMessage('menus.category.placeholder')) + .setOptions( + categories.map(category => + new StringSelectMenuOptionBuilder() + .setValue(String(category.id)) + .setLabel(category.name) + .setDescription(category.description) + .setEmoji(emoji.hasEmoji(category.emoji) ? emoji.get(category.emoji) : { id: category.emoji }), + ), + ), + ); + + } + + await channel.send({ + components: [ + new ActionRowBuilder() + .setComponents(components), + ], + embeds: [embed], + }); + } + + logAdminEvent(client, { + action: 'create', + guildId: guild.id, + target: { + id: channel.toString(), + type: 'panel', + }, + userId: req.user.id, + }); + + return true; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); diff --git a/src/routes/api/admin/guilds/[guild]/problems.js b/src/routes/api/admin/guilds/[guild]/problems.js new file mode 100644 index 0000000..130d6de --- /dev/null +++ b/src/routes/api/admin/guilds/[guild]/problems.js @@ -0,0 +1,34 @@ +const { PermissionsBitField } = require('discord.js'); + +module.exports.get = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + const id = req.params.guild; + const guild = client.guilds.cache.get(id); + const settings = await client.prisma.guild.findUnique({ where: { id } }) ?? + await client.prisma.guild.create({ data: { id } }); + const problems = []; + + if (settings.logChannel) { + const permissions = guild.members.me.permissionsIn(settings.logChannel); + + if (!permissions.has(PermissionsBitField.Flags.SendMessages)) { + problems.push({ + id: 'logChannelMissingPermission', + permission: 'SendMessages', + }); + } + + if (!permissions.has(PermissionsBitField.Flags.EmbedLinks)) { + problems.push({ + id: 'logChannelMissingPermission', + permission: 'EmbedLinks', + }); + } + } + + return problems; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); diff --git a/src/routes/api/admin/guilds/[guild]/settings.js b/src/routes/api/admin/guilds/[guild]/settings.js new file mode 100644 index 0000000..2c14ad9 --- /dev/null +++ b/src/routes/api/admin/guilds/[guild]/settings.js @@ -0,0 +1,84 @@ +const { logAdminEvent } = require('../../../../../lib/logging.js'); +const { Colors } = require('discord.js'); + +module.exports.delete = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + const id = req.params.guild; + await client.prisma.guild.delete({ where: { id } }); + const settings = await client.prisma.guild.create({ data: { id } }); + logAdminEvent(client, { + action: 'delete', + guildId: id, + target: { + id, + name: client.guilds.cache.get(id), + type: 'settings', + }, + userId: req.user.id, + }); + return settings; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); + +module.exports.get = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + const id = req.params.guild; + const settings = await client.prisma.guild.findUnique({ where: { id } }) ?? + await client.prisma.guild.create({ data: { id } }); + + return settings; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); + +module.exports.patch = fastify => ({ + handler: async (req, res) => { + const data = req.body; + if (data.hasOwnProperty('id')) delete data.id; + if (data.hasOwnProperty('createdAt')) delete data.createdAt; + const colours = ['errorColour', 'primaryColour', 'successColour']; + for (const c of colours) { + if (data[c] && !data[c].startsWith('#') && !(data[c] in Colors)) { // if not null/empty and not hex + throw new Error(`${data[c]} is not a valid colour. Valid colours are HEX and: ${Object.keys(Colors).join(', ')}`); + } + } + + /** @type {import('client')} */ + const client = res.context.config.client; + const id = req.params.guild; + const original = await client.prisma.guild.findUnique({ where: { id } }); + const settings = await client.prisma.guild.update({ + data: data, + include: { categories: { select: { id: true } } }, + where: { id }, + }); + + // Update cached categories, which include guild settings + for (const { id } of settings.categories) await client.tickets.getCategory(id, true); + + // don't log the categories + delete settings.categories; + + logAdminEvent(client, { + action: 'update', + diff: { + original, + updated: settings, + }, + guildId: id, + target: { + id, + name: client.guilds.cache.get(id).name, + type: 'settings', + }, + userId: req.user.id, + }); + return settings; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); diff --git a/src/routes/api/admin/guilds/[guild]/tags/[tag].js b/src/routes/api/admin/guilds/[guild]/tags/[tag].js new file mode 100644 index 0000000..13fb25d --- /dev/null +++ b/src/routes/api/admin/guilds/[guild]/tags/[tag].js @@ -0,0 +1,106 @@ +const ms = require('ms'); +const { logAdminEvent } = require('../../../../../../lib/logging'); + +module.exports.delete = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + const guildId = req.params.guild; + const tagId = Number(req.params.tag); + const original = tagId && await client.prisma.tag.findUnique({ where: { id: tagId } }); + if (original.guildId !== guildId) return res.status(404).send(new Error('Not Found')); + const tag = await client.prisma.tag.delete({ where: { id: tagId } }); + + const cacheKey = `cache/guild-tags:${guildId}`; + client.keyv.set(cacheKey, await client.prisma.tag.findMany({ + select: { + content: true, + id: true, + name: true, + regex: true, + }, + where: { guildId: guildId }, + }), ms('1h')); + + logAdminEvent(client, { + action: 'delete', + guildId: req.params.guild, + target: { + id: tag.id, + name: tag.name, + type: 'tag', + }, + userId: req.user.id, + }); + + return tag; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); + +module.exports.get = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + const guildId = req.params.guild; + const tagId = Number(req.params.tag); + const tag = await client.prisma.tag.findUnique({ where: { id: tagId } }); + + if (!tag || tag.guildId !== guildId) return res.status(404).send(new Error('Not Found')); + + return tag; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); + +module.exports.patch = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + const guildId = req.params.guild; + const tagId = Number(req.params.tag); + const guild = client.guilds.cache.get(req.params.guild); + const data = req.body; + + const original = req.params.tag && await client.prisma.tag.findUnique({ where: { id: tagId } }); + + if (!original || original.guildId !== guildId) return res.status(404).send(new Error('Not Found')); + + if (data.hasOwnProperty('id')) delete data.id; + if (data.hasOwnProperty('createdAt')) delete data.createdAt; + + const tag = await client.prisma.tag.update({ + data, + where: { id: tagId }, + }); + + const cacheKey = `cache/guild-tags:${guildId}`; + client.keyv.set(cacheKey, await client.prisma.tag.findMany({ + select: { + content: true, + id: true, + name: true, + regex: true, + }, + where: { guildId: guildId }, + }), ms('1h')); + + logAdminEvent(client, { + action: 'update', + diff: { + original, + updated: tag, + }, + guildId: guild.id, + target: { + id: tag.id, + name: tag.name, + type: 'tag', + }, + userId: req.user.id, + }); + + return tag; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); diff --git a/src/routes/api/admin/guilds/[guild]/tags/index.js b/src/routes/api/admin/guilds/[guild]/tags/index.js new file mode 100644 index 0000000..46a84e8 --- /dev/null +++ b/src/routes/api/admin/guilds/[guild]/tags/index.js @@ -0,0 +1,65 @@ +const ms = require('ms'); +const { logAdminEvent } = require('../../../../../../lib/logging'); + +module.exports.get = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + + const { tags } = await client.prisma.guild.findUnique({ + select: { tags: true }, + where: { id: req.params.guild }, + }); + + return tags; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); + + +module.exports.post = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + const guild = client.guilds.cache.get(req.params.guild); + const data = req.body; + const tag = await client.prisma.tag.create({ + data: { + guild: { connect: { id: guild.id } }, + ...data, + }, + }); + + const cacheKey = `cache/guild-tags:${guild.id}`; + let tags = await client.keyv.get(cacheKey); + if (!tags) { + tags = await client.prisma.tag.findMany({ + select: { + content: true, + id: true, + name: true, + regex: true, + }, + where: { guildId: guild.id }, + }); + client.keyv.set(cacheKey, tags, ms('1h')); + } else { + tags.push(tag); + client.keyv.set(cacheKey, tags, ms('1h')); + } + + logAdminEvent(client, { + action: 'create', + guildId: guild.id, + target: { + id: tag.id, + name: tag.name, + type: 'tag', + }, + userId: req.user.id, + }); + + return tag; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); diff --git a/src/routes/api/admin/guilds/index.js b/src/routes/api/admin/guilds/index.js new file mode 100644 index 0000000..9b6dc06 --- /dev/null +++ b/src/routes/api/admin/guilds/index.js @@ -0,0 +1,19 @@ +const { PermissionsBitField } = require('discord.js'); + +module.exports.get = fastify => ({ + handler: async (req, res) => { + const { client } = res.context.config; + const guilds = await (await fetch('https://discordapp.com/api/users/@me/guilds', { headers: { 'Authorization': `Bearer ${req.user.accessToken}` } })).json(); + res.send( + guilds + .filter(guild => guild.owner || new PermissionsBitField(guild.permissions.toString()).has(PermissionsBitField.Flags.ManageGuild)) + .map(guild => ({ + added: client.guilds.cache.has(guild.id), + id: guild.id, + logo: `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp`, + name: guild.name, + })), + ); + }, + onRequest: [fastify.authenticate], +}); diff --git a/src/routes/api/client.js b/src/routes/api/client.js new file mode 100644 index 0000000..e18c1df --- /dev/null +++ b/src/routes/api/client.js @@ -0,0 +1,46 @@ +const { + getAvgResolutionTime, getAvgResponseTime, +} = require('../../lib/stats'); +const ms = require('ms'); + +module.exports.get = () => ({ + handler: async (req, res) => { + /** @type {import("client")} */ + const client = res.context.config.client; + const cacheKey = 'cache/stats/client'; + let cached = await client.keyv.get(cacheKey); + + if (!cached) { + const tickets = await client.prisma.ticket.findMany({ + select: { + closedAt: true, + createdAt: true, + firstResponseAt: true, + }, + }); + const closedTickets = tickets.filter(t => t.firstResponseAt && t.closedAt); + const users = await client.prisma.user.findMany({ select: { messageCount: true } }); + cached = { + avatar: client.user.avatarURL(), + discriminator: client.user.discriminator, + id: client.user.id, + public: (process.env.PUBLIC_BOT === 'true'), + stats: { + activatedUsers: users.length, + archivedMessages: users.reduce((total, user) => total + user.messageCount, 0), // don't count archivedMessage table rows, they can be deleted + avgResolutionTime: ms(getAvgResolutionTime(closedTickets)), + avgResponseTime: ms(getAvgResponseTime(closedTickets)), + categories: await client.prisma.category.count(), + guilds: client.guilds.cache.size, + members: client.guilds.cache.reduce((t, g) => t + g.memberCount, 0), + tags: await client.prisma.tag.count(), + tickets: tickets.length, + }, + username: client.user.username, + }; + await client.keyv.set(cacheKey, cached, ms('15m')); + } + + return cached; + }, +}); diff --git a/src/routes/api/locales.js b/src/routes/api/locales.js new file mode 100644 index 0000000..2da51c1 --- /dev/null +++ b/src/routes/api/locales.js @@ -0,0 +1,7 @@ +module.exports.get = () => ({ + handler: async (req, res) => { + /** @type {import("client")} */ + const client = res.context.config.client; + return client.i18n.locales; + }, +}); \ No newline at end of file diff --git a/src/routes/api/users/@me/index.js b/src/routes/api/users/@me/index.js new file mode 100644 index 0000000..8608fc1 --- /dev/null +++ b/src/routes/api/users/@me/index.js @@ -0,0 +1,4 @@ +module.exports.get = fastify => ({ + handler: req => req.user, + onRequest: [fastify.authenticate], +}); diff --git a/src/routes/api/users/@me/key.js b/src/routes/api/users/@me/key.js new file mode 100644 index 0000000..7d8b202 --- /dev/null +++ b/src/routes/api/users/@me/key.js @@ -0,0 +1,19 @@ +module.exports.get = fastify => ({ + handler: async function (req, res) { // MUST NOT use arrow function syntax + if (process.env.PUBLIC_BOT === 'true') { + return res.code(400).send({ + error: 'Bad Request', + message: 'API keys are not available on public bots.', + statusCode: 400, + }); + } else { + return { + token: this.jwt.sign({ + createdAt: Date.now(), + id: req.user.id, + }), + }; + } + }, + onRequest: [fastify.authenticate], +}); diff --git a/src/routes/auth/callback.js b/src/routes/auth/callback.js new file mode 100644 index 0000000..12c0622 --- /dev/null +++ b/src/routes/auth/callback.js @@ -0,0 +1,31 @@ +const { domain } = require('../../lib/http'); + +module.exports.get = () => ({ + handler: async function (req, res) { // MUST NOT use arrow function syntax + const { + access_token: accessToken, + expires_in: expiresIn, + } = await this.discord.getAccessTokenFromAuthorizationCodeFlow(req); + const redirect = this.states.get(req.query.state) || '/'; + this.states.delete(req.query.state); + const user = await (await fetch('https://discordapp.com/api/users/@me', { headers: { 'Authorization': `Bearer ${accessToken}` } })).json(); + const token = this.jwt.sign({ + accessToken, + avatar: user.avatar, + discriminator: user.discriminator, + expiresAt: Date.now() + (expiresIn * 1000), + id: user.id, + locale: user.locale, + username: user.username, + }); + res.setCookie('token', token, { + domain, + httpOnly: true, + maxAge: expiresIn, + path: '/', + sameSite: 'Lax', + secure: false, + }); + return res.redirect(303, redirect); + }, +}); diff --git a/src/routes/auth/logout.js b/src/routes/auth/logout.js new file mode 100644 index 0000000..3e8b164 --- /dev/null +++ b/src/routes/auth/logout.js @@ -0,0 +1,22 @@ +const { domain } = require('../../lib/http'); + +module.exports.get = fastify => ({ + handler: async function (req, res) { + const { accessToken } = req.user; + + await fetch('https://discord.com/api/oauth2/token/revoke', { + body: new URLSearchParams({ token: accessToken }).toString(), + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + method: 'POST', + }); + + res.clearCookie('token', { + domain, + httpOnly: true, + path: '/', + sameSite: 'Lax', + secure: false, + }).send('The token has been revoked.'); + }, + onRequest: [fastify.authenticate], +}); diff --git a/src/schemas/settings.js b/src/schemas/settings.js new file mode 100644 index 0000000..9b7694f --- /dev/null +++ b/src/schemas/settings.js @@ -0,0 +1,26 @@ +// TODO +/* eslint-disable no-undef */ +module.exports = joi.object({ + archive: joi.boolean().optional(), + autoClose: joi.number().min(3_600_000).optional(), + autoTag: [joi.array(), joi.string().valid('ticket', '!ticket', 'all')].optional(), + blocklist: joi.array().optional(), + createdAt: joi.string().optional(), + errorColour: joi.string().optional(), + footer: joi.string().optional(), + id: joi.string().optional(), + logChannel: joi.string().optional(), + primaryColour: joi.string().optional(), + staleAfter: joi.number().min(60_000).optional(), + successColour: joi.string().optional(), + workingHours: joi.array().length(8).items( + joi.string(), + joi.array().items(joi.string().required(), joi.string().required()), + joi.array().items(joi.string().required(), joi.string().required()), + joi.array().items(joi.string().required(), joi.string().required()), + joi.array().items(joi.string().required(), joi.string().required()), + joi.array().items(joi.string().required(), joi.string().required()), + joi.array().items(joi.string().required(), joi.string().required()), + joi.array().items(joi.string().required(), joi.string().required()), + ).optional(), +}); \ No newline at end of file diff --git a/src/stdin/commands.js b/src/stdin/commands.js new file mode 100644 index 0000000..c408ffb --- /dev/null +++ b/src/stdin/commands.js @@ -0,0 +1,31 @@ +const { StdinCommand } = require('@eartharoid/dbf'); +const { inspect } = require('util'); + +module.exports = class Commands extends StdinCommand { + constructor(client, options) { + super(client, { + ...options, + id: 'commands', + }); + } + + async run(args) { + switch (args[0]) { + case 'publish': { + this.client.commands.publish() + .then(commands => this.client.log.success('Published %d commands', commands?.size)) + .catch(error => { + this.client.log.warn('Failed to publish commands'); + this.client.log.error(error); + this.client.log.error(inspect(error.rawError?.errors, { depth: Infinity })); + }); + break; + } + default: { + this.client.log.info('subcommands: \n' + [ + '> commands publish', + ].join('\n')); + } + } + } +}; \ No newline at end of file diff --git a/src/stdin/eval.js b/src/stdin/eval.js new file mode 100644 index 0000000..6a5de5c --- /dev/null +++ b/src/stdin/eval.js @@ -0,0 +1,22 @@ +const { StdinCommand } = require('@eartharoid/dbf'); + +module.exports = class extends StdinCommand { + constructor(client, options) { + super(client, { + ...options, + id: 'eval', + }); + } + + async run(input) { + const toEval = input.join(' '); + try { + const res = await eval(toEval); + console.log(res); // eslint-disable-line no-console + return true; + } catch (error) { + this.client.log.error(error); + return false; + } + } +}; \ No newline at end of file diff --git a/src/stdin/exit.js b/src/stdin/exit.js new file mode 100644 index 0000000..e58317e --- /dev/null +++ b/src/stdin/exit.js @@ -0,0 +1,15 @@ +const { StdinCommand } = require('@eartharoid/dbf'); + +module.exports = class extends StdinCommand { + constructor(client, options) { + super(client, { + ...options, + id: 'exit', + }); + } + + async run() { + this.client.log.info('Exiting'); + process.exit(); + } +}; \ No newline at end of file diff --git a/src/stdin/help.js b/src/stdin/help.js new file mode 100644 index 0000000..109494c --- /dev/null +++ b/src/stdin/help.js @@ -0,0 +1,17 @@ +const { StdinCommand } = require('@eartharoid/dbf'); +const { homepage } = require('../../package.json'); + +module.exports = class extends StdinCommand { + constructor(client, options) { + super(client, { + ...options, + id: 'help', + }); + } + + async run() { + this.client.log.info('Documentation:', homepage); + this.client.log.info('Support: https://lnk.earth/discord'); + this.client.log.info('stdin commands:\n' + this.client.stdin.components.map(c => `> ${c.id}`).join('\n')); + } +}; \ No newline at end of file diff --git a/src/stdin/npx.js b/src/stdin/npx.js new file mode 100644 index 0000000..992eebc --- /dev/null +++ b/src/stdin/npx.js @@ -0,0 +1,18 @@ +const { StdinCommand } = require('@eartharoid/dbf'); +const { spawn } = require('child_process'); + +module.exports = class extends StdinCommand { + constructor(client, options) { + super(client, { + ...options, + id: 'npx', + }); + } + + async run(input) { + if (!input[0]) return this.client.log.warn('Usage: npx [args]'); + const child = spawn('npx', input, { shell: true }); + for await (const data of child.stdout) this.client.log.info('npx:', data.toString()); + for await (const data of child.stderr) this.client.warn.info('npx:', data.toString()); + } +}; \ No newline at end of file diff --git a/src/stdin/settings.js b/src/stdin/settings.js new file mode 100644 index 0000000..187f972 --- /dev/null +++ b/src/stdin/settings.js @@ -0,0 +1,14 @@ +const { StdinCommand } = require('@eartharoid/dbf'); + +module.exports = class extends StdinCommand { + constructor(client, options) { + super(client, { + ...options, + id: 'settings', + }); + } + + async run() { + this.client.log.info.settings(process.env.HTTP_EXTERNAL + '/settings'); + } +}; \ No newline at end of file diff --git a/src/stdin/sync.js b/src/stdin/sync.js new file mode 100644 index 0000000..b022b93 --- /dev/null +++ b/src/stdin/sync.js @@ -0,0 +1,15 @@ +const { StdinCommand } = require('@eartharoid/dbf'); +const sync = require('../lib/sync'); + +module.exports = class extends StdinCommand { + constructor(client, options) { + super(client, { + ...options, + id: 'sync', + }); + } + + async run() { + await sync(this.client); + } +}; \ No newline at end of file diff --git a/src/stdin/version.js b/src/stdin/version.js new file mode 100644 index 0000000..d857b72 --- /dev/null +++ b/src/stdin/version.js @@ -0,0 +1,17 @@ +const { StdinCommand } = require('@eartharoid/dbf'); +const { version } = require('../../package.json'); +const checkForUpdates = require('../lib/updates'); + +module.exports = class extends StdinCommand { + constructor(client, options) { + super(client, { + ...options, + id: 'version', + }); + } + + async run() { + this.client.log.info('Current version:', version); + checkForUpdates(this.client); + } +}; diff --git a/src/update/notifier.js b/src/update/notifier.js deleted file mode 100644 index 00e215f..0000000 --- a/src/update/notifier.js +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-disable no-console */ -const fetch = require('node-fetch'); -const boxen = require('boxen'); -const link = require('terminal-link'); -const semver = require('semver'); -const { format } = require('leekslazylogger'); - -const { version: current } = require('../../package.json'); - -module.exports = async client => { - if (!client.config.update_notice) return; - const json = await (await fetch('https://api.github.com/repos/discord-tickets/bot/releases')).json(); - const update = json[0]; - - const latest = semver.coerce(update.tag_name); - - if (!semver.valid(latest)) return; - - if (semver.lt(current, latest)) { - client.log.notice(`There is an update available for Discord Tickets (${current} -> ${update.tag_name})`); - - const lines = [ - `&k&6You are currently using &c${current}&6, the latest is &a${update.tag_name}&6.&r`, - `&k&6Download "&f${update.name}&6" from&r`, - link('&k&6the GitHub releases page.&r&6', 'https://github.com/discord-tickets/bot/releases/') - ]; - - console.log( - boxen(format(lines.join('\n')), { - align: 'center', - borderColor: 'yellow', - borderStyle: 'round', - margin: 1, - padding: 1 - }) - ); - } -}; \ No newline at end of file diff --git a/src/utils/discord.js b/src/utils/discord.js deleted file mode 100644 index 77d4800..0000000 --- a/src/utils/discord.js +++ /dev/null @@ -1,96 +0,0 @@ - -const { GuildMember } = require('discord.js'); // eslint-disable-line no-unused-vars -const { Model } = require('sequelize'); // eslint-disable-line no-unused-vars -const config = require('../../user/config'); -let current_presence = -1; - -module.exports = class DiscordUtils { - constructor(client) { - this.client = client; - } - - /** - * Generate embed footer text - * @param {string} text - * @param {string} [additional] - * @returns {string} - */ - footer(text, additional) { - if (text && additional) return `${text} | ${additional}`; - else return additional || text || ''; - } - - /** - * Check if a guild member is staff - * @param {GuildMember} member - the guild member - * @returns {boolean} - */ - async isStaff(member) { - const guild_categories = await this.client.db.models.Category.findAll({ where: { guild: member.guild.id } }); - return guild_categories.some(cat => cat.roles.some(r => member.roles.cache.has(r))); - } - - /** - * Fet a guild's settings - * @param {string} id - The guild's ID - * @returns {Promise} - */ - async getSettings(id) { - const data = { id }; - const [settings] = await this.client.db.models.Guild.findOrCreate({ - defaults: data, - where: data - }); - return settings; - } - - /** - * Delete a guild's settings - * @param {string} id - The guild ID - * @returns {Promise} - */ - async deleteSettings(id) { - const row = await this.getSettings(id); - return await row.destroy(); - } - - /** - * Select a presence from the config - * @returns {PresenceData} - */ - static selectPresence() { - const length = config.presence.presences.length; - if (length === 0) return {}; - - let num; - if (length === 1) { - num = 0; - } else if (config.presence.randomise) { - num = Math.floor(Math.random() * length); - } else { - current_presence = current_presence + 1; // ++ doesn't work on negative numbers - if (current_presence === length) { - current_presence = 0; - } - num = current_presence; - } - - const { - activity: name, - status, - type, - url - } = config.presence.presences[num]; - - return { - activities: [ - { - name, - type, - url - } - ], - status - }; - } -}; \ No newline at end of file diff --git a/src/utils/emoji.js b/src/utils/emoji.js deleted file mode 100644 index 8ee6000..0000000 --- a/src/utils/emoji.js +++ /dev/null @@ -1,43 +0,0 @@ -module.exports = { - letters: { - A: '🇦', - B: '🇧', - C: '🇨', - D: '🇩', - E: '🇪', - F: '🇫', - G: '🇬', - H: '🇭', - I: '🇮', - J: '🇯', - K: '🇰', - L: '🇱', - M: '🇲', - N: '🇳', - O: '🇴', - P: '🇵', - Q: '🇶', - R: '🇷', - S: '🇸', - T: '🇹', - U: '🇺', - V: '🇻', - W: '🇼', - X: '🇽', - Y: '🇾', - Z: '🇿' - }, - numbers: { - 0: '0️⃣', - 1: '1️⃣', - 2: '2️⃣', - 3: '3️⃣', - 4: '4️⃣', - 5: '5️⃣', - 6: '6️⃣', - 7: '7️⃣', - 8: '8️⃣', - 9: '9️⃣', - 10: '🔟' - } -}; \ No newline at end of file diff --git a/src/utils/fs.js b/src/utils/fs.js deleted file mode 100644 index 5f76ab3..0000000 --- a/src/utils/fs.js +++ /dev/null @@ -1,10 +0,0 @@ -const { join } = require('path'); - -module.exports = { - /** - * Make a relative path absolute - * @param {string} path - A path relative to the root of the project (like "./user/config.js") - * @returns {string} absolute path - */ - path: path => join(__dirname, '../../', path) -}; \ No newline at end of file diff --git a/src/utils/index.js b/src/utils/index.js deleted file mode 100644 index f702860..0000000 --- a/src/utils/index.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - int2hex: int => int.toString(16).toUpperCase(), - some: async (array, func) => { - for (const element of array) { - if (await func(element)) return true; - } - return false; - }, - wait: time => new Promise(res => setTimeout(res, time)) -}; \ No newline at end of file diff --git a/user/plugins/.gitkeep b/user/avatars/.gitkeep similarity index 100% rename from user/plugins/.gitkeep rename to user/avatars/.gitkeep diff --git a/user/example.config.js b/user/example.config.js deleted file mode 100644 index 406af9e..0000000 --- a/user/example.config.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * ############################################################################################### - * ____ _ _____ _ _ - * | _ \ (_) ___ ___ ___ _ __ __| | |_ _| (_) ___ | | __ ___ | |_ ___ - * | | | | | | / __| / __| / _ \ | '__| / _` | | | | | / __| | |/ / / _ \ | __| / __| - * | |_| | | | \__ \ | (__ | (_) | | | | (_| | | | | | | (__ | < | __/ | |_ \__ \ - * |____/ |_| |___/ \___| \___/ |_| \__,_| |_| |_| \___| |_|\_\ \___| \__| |___/ - * - * --------------------- - * Support - * --------------------- - * - * > Documentation: https://discordtickets.app - * > Discord support server: https://go.eartharoid.me/discord - * - * ############################################################################################### - */ - - -module.exports = { - defaults: { - colour: '#009999', - log_messages: true, - name_format: 'ticket-{number}', - opening_message: 'Hello {name}, thank you for creating a ticket. A member of staff will soon be available to assist you.\n\n__All messages in this channel are stored for future reference.__' - }, - developer: { debug: false }, - locale: 'en-GB', - logs: { - enabled: true, - keep_for: 30, - split: true - }, - max_listeners: 10, - plugins: [], - presence: { - duration: 60, - presences: [ - { - activity: '/new', - type: 'PLAYING' - }, - { - activity: 'with tickets', - type: 'PLAYING' - }, - { - activity: 'tickets', - type: 'WATCHING' - } - ], - randomise: true - }, - super_secret_setting: true, - update_notice: true -}; \ No newline at end of file diff --git a/user/example.config.yml b/user/example.config.yml new file mode 100644 index 0000000..d60910f --- /dev/null +++ b/user/example.config.yml @@ -0,0 +1,36 @@ +##################################################### +## ____ _ ## +## | _ \ (_) ___ ___ ___ _ __ __| | ## +## | | | | | | / __| / __| / _ \ | '__| / _` | ## +## | |_| | | | \__ \ | (__ | (_) | | | | (_| | ## +## |____/ |_| |___/ \___| \___/ |_| \__,_| ## +## _____ _ _ ## +## |_ _| (_) ___ | | __ ___ | |_ ___ ## +## | | | | / __| | |/ / / _ \ | __| / __| ## +## | | | | | (__ | < | __/ | |_ \__ \ ## +## |_| |_| \___| |_|\_\ \___| \__| |___/ ## +## ## +## Documentation: https://discordtickets.app ## +## Support: https://lnk.earth/discord ## +##################################################### + +logs: + files: + directory: ./logs + enabled: true + keepFor: 30 + level: info +presence: + activities: + - name: /new + - name: with {totalTickets} tickets + - name: "{openTickets} tickets" + type: 3 + - name: "{avgResponseTime} response time" + type: 3 + interval: 20 + status: online +stats: true +templates: + transcript: transcript.md +updates: true diff --git a/user/templates/transcript.md.mustache b/user/templates/transcript.md.mustache new file mode 100644 index 0000000..cb69b1d --- /dev/null +++ b/user/templates/transcript.md.mustache @@ -0,0 +1,30 @@ +#{{ channelName }} ticket transcript + +--- + +* ID: {{ ticket.id }} ({{ guildName }}) +* Number: {{ ticket.category.name }} #{{ ticket.number }} +* Topic: {{ #ticket.topic }}{{ . }}{{ /ticket.topic }}{{ ^ticket.topic }}(no topic){{ /ticket.topic }} +* Created on: {{ #ticket }}{{ createdAtFull }}{{ /ticket }} +* Created by: {{ #ticket.createdBy }}"{{ displayName }}" @{{ username }}#{{ discriminator }}{{ /ticket.createdBy }} +* Closed on: {{ #ticket }}{{ closedAtFull }}{{ /ticket }} +* Closed by: {{ #ticket.closedBy }}"{{ displayName }}" @{{ username }}#{{ discriminator }}{{ /ticket.closedBy }}{{ ^ticket.closedBy }}(automated){{ /ticket.closedBy }} +* Closed because: {{ #ticket.closedReason }}{{ ticket.closedReason }}{{ /ticket.closedReason }}{{ ^ticket.closedReason }}(no reason){{ /ticket.closedReason }} +* Claimed by: {{ #ticket.claimedBy }}"{{ displayName }}" @{{ username }}#{{ discriminator }}{{ /ticket.claimedBy }}{{ ^ticket.claimedBy }}(not claimed){{ /ticket.claimedBy }} +{{ #ticket.feedback }} +* Feedback: + * Rating: {{ rating }}/5 + * Comment: {{ comment }}{{ ^comment }}(no comment){{ /comment }} +{{ /ticket.feedback }} +* Participants: +{{ #ticket.archivedUsers }} + * "{{ displayName }}" @{{ username }}#{{ discriminator }} ({{ userId }}) +{{ /ticket.archivedUsers }} +* Pinned messages: {{ #pinned }}{{ . }}{{ /pinned }} + +--- + +{{ #ticket.archivedMessages }} +<{{ number }}> [{{ createdAtTimestamp }}] {{author.displayName}}: {{ text }} + +{{ /ticket.archivedMessages }} \ No newline at end of file diff --git a/user/uploads/.gitkeep b/user/uploads/.gitkeep new file mode 100644 index 0000000..e69de29