diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b7fa9cd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +# Folders +/.cache +/.git +/dist +/data +/logs +/node_modules + +# Files +/npm-debug.log + +# Configuration +config.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2c8e77b --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# NODE_ENV=staging + +# This was inserted by `prisma init`: +# Environment variables declared in this file are automatically made available to Prisma. +# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema + +# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. +# See the documentation for all the connection string options: https://pris.ly/d/connection-strings + +DATABASE_URL="mongodb+srv://:@/" \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..feebf80 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,72 @@ +{ + "env": { + "es6": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "no-console": 0, + "indent": [ + "error", + 2 + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ], + "max-len": [ + "warn", + { + "code": 120, + "tabWidth": 2, + "comments": 120, + "ignoreUrls": true, + "ignoreTemplateLiterals": false, + "ignoreRegExpLiterals": true + } + ], + "comma-dangle": [ + "error", + { + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "only-multiline" + } + ], + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_" + } + ], + "eol-last": [ + "error", + "always" + ], + "no-multiple-empty-lines": [ + "error", + { + "max": 2, + "maxEOF": 1, + "maxBOF": 0 + } + ] + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..314766e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..0bedceb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "npm" # See documentation for possible values + directory: "/" # Location of package manifests + target-branch: dev # Branch to create PRs against + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..9e66419 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,82 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + schedule: + - cron: '41 4 * * 6' + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ javascript-typescript ] + # CodeQL supports [ $supported-codeql-languages ] + # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ā„¹ļø Command-line programs to run using the OS shell. + # šŸ“š See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..2d464f2 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,83 @@ +name: Deploy + +on: + workflow_dispatch: + workflow_run: + workflows: [Release] + types: + - completed + +jobs: + notify-start: + runs-on: ubuntu-latest + steps: + - name: Discord Webhook - Notify Start Deploy + uses: tsickert/discord-webhook@v5.3.0 + continue-on-error: true + with: + username: "Mirasaki Development CI/CD" + avatar-url: "https://mirasaki.dev/logo.png" + webhook-url: ${{ secrets.DEPLOYMENT_WEBHOOK_URL }} + embed-author-name: "Continuous Deployment by Mirasaki Development" + embed-author-url: "https://mirasaki.dev" + embed-author-icon-url: "https://mirasaki.dev/logo.png" + embed-color: 14228765 + embed-title: "ā¬‡ļø Rhidium Template" + embed-description: "āŒ› Deploying **`@${{ github.repository }}`**...\nšŸ“¤ Service is now temporarily unavailable." + + deploy: + needs: notify-start + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Copy repository contents via scp + uses: appleboy/scp-action@master + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + passphrase: ${{ secrets.SSH_PASSPHRASE }} + port: ${{ secrets.SSH_PORT }} + source: "." + target: "/var/www/@rhidium/template" + + - name: Deploy to Production + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + passphrase: ${{ secrets.SSH_PASSPHRASE }} + port: ${{ secrets.SSH_PORT }} + script: | + export NVM_DIR=~/.nvm + source ~/.nvm/nvm.sh + + cd /var/www/@rhidium/template + pm2 stop ecosystem.config.js + + npm install + npm run build + + cd /var/www/@rhidium/template + pm2 restart ecosystem.config.js + + notify-finish: + needs: deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Discord Webhook - Notify Finish Deploy + uses: tsickert/discord-webhook@v5.3.0 + with: + username: "Mirasaki Development CI/CD" + avatar-url: "https://mirasaki.dev/logo.png" + webhook-url: ${{ secrets.DEPLOYMENT_WEBHOOK_URL }} + filename: "CHANGELOG.md" + embed-author-name: "Continuous Deployment by Mirasaki Development" + embed-author-url: "https://mirasaki.dev" + embed-author-icon-url: "https://mirasaki.dev/logo.png" + embed-color: 9559538 + embed-title: "ā¬†ļø Rhidium Template" + embed-description: "āœ… Finished deploying **`@${{ github.repository }}`**\nšŸ“„ <@1174745346719625216> is back online ([docs]())" \ No newline at end of file diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..052cae9 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,13 @@ +name: Docker Image CI + +on: + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag @rhidium/template:$(date +%s) diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml new file mode 100644 index 0000000..9c9b337 --- /dev/null +++ b/.github/workflows/eslint.yml @@ -0,0 +1,48 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# ESLint is a tool for identifying and reporting on patterns +# found in ECMAScript/JavaScript code. +# More details at https://github.com/eslint/eslint +# and https://eslint.org + +name: ESLint + +on: + push: + branches: [ main ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + +jobs: + eslint: + name: Run eslint scanning + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install Sarif Formatter + run: | + npm install @microsoft/eslint-formatter-sarif@2.1.7 + + + - name: Run ESLint + run: npx eslint . + --config .eslintrc.json + --ext .js,.jsx,.ts,.tsx + --format @microsoft/eslint-formatter-sarif + --output-file eslint-results.sarif + continue-on-error: true + + - name: Upload analysis results to GitHub + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: eslint-results.sarif + wait-for-processing: true \ No newline at end of file diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml new file mode 100644 index 0000000..ed21965 --- /dev/null +++ b/.github/workflows/publish-docker.yml @@ -0,0 +1,44 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# GitHub recommends pinning actions to a commit SHA. +# To get a newer version, you will need to update the SHA. +# You can also reference a tag or branch, but the action may change without warning. + +name: Publish Docker image + +on: + workflow_run: + workflows: [Release] + types: + - completed + +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: "@rhidium/template" + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c3f3148 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: Release + +on: + workflow_dispatch: + +jobs: + release: + permissions: + contents: write + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 18 + - run: npm install + - run: | + npm i -D semantic-release \ + @semantic-release/changelog \ + @semantic-release/commit-analyzer \ + @semantic-release/git \ + @semantic-release/github \ + @semantic-release/npm \ + @semantic-release/release-notes-generator + - run: npx semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e51bd35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,147 @@ +# Developers +src/**/development +TODO + +# Data +/data +INDEV + +# Config +/config.json +/config.json.bak + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.dev +.env.development.local +.env.test +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Build distribution directory +dist/ diff --git a/.husky/.prepare-commit-msg b/.husky/.prepare-commit-msg new file mode 100755 index 0000000..72ed02f --- /dev/null +++ b/.husky/.prepare-commit-msg @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +exec < /dev/tty && npx cz --hook || true + +# This hook has been disabled in favor of commit-msg (commitlint) hook. +# Use 'npm run commit' or 'npx cz' to commit instead. + +# In a perfect world, we would re-enable this hook +# but only run cz if the commit message provided is +# entirely empty. diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..86890fd --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx commitlint --edit ${1} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..fc09221 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +# Run formatter (write), and linting (read) - only on files that have actually changed +npx lint-staged \ No newline at end of file diff --git a/.lintstagedrc b/.lintstagedrc new file mode 100644 index 0000000..e69de29 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f8665ed --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +/node_modules +/dist +/coverage +/types \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..94eacea --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "arrowParens": "always", + "endOfLine": "lf", + "semi": true, + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..acf0289 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": + [ + "dbaeumer.vscode-eslint", + "streetsidesoftware.code-spell-checker" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d6de5fe --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,30 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": ["/**", "node_modules/**"], + "program": "${workspaceFolder}/dist/src/index.js", + "preLaunchTask": "npm: build", + "console": "integratedTerminal" + }, + { + "type": "node", + "request": "launch", + "name": "Mocha Tests", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": [ + "-u", + "bdd", + "--timeout", + "999999", + "--colors", + "${workspaceFolder}/dist/test/**/*.js" + ], + "internalConsoleOptions": "openOnSessionStart", + "preLaunchTask": "npm: build" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a1687fc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,27 @@ +{ + "typescript.format.enable": true, + "typescript.referencesCodeLens.enabled": true, + "typescript.implementationsCodeLens.enabled": true, + "cSpell.enabled": true, + "cSpell.words": [ + "Greyple", + "Rhidium" + ], + "eslint.enable": true, + "eslint.format.enable": true, + "eslint.lintTask.enable": true, + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.detectIndentation": false, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.fixAll.eslint": true, + "source.organizeImports": false + }, + "eslint.codeActionsOnSave.mode": "all", + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact" ], + "eslint.useESLintClass": true, +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..76a21e8 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,15 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "build", + "problemMatcher": [ + "$tsc" + ], + "group": "build", + "label": "npm: build", + "detail": "tsc" + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8670dbb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM node:21-alpine + +# Create app/working/bot directory +RUN mkdir -p /app +WORKDIR /app + +# Install app production dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ +RUN npm ci + +# Bundle app source +COPY . ./ + +# Build the project +RUN npx prisma generate +RUN npm run build + +# Run the start command +CMD [ "npm", "start" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..245f2c1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2023 Richard Hillebrand (Mirasaki) + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1bda9b8 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +
+ logo + + ![Font_PNG](https://github.com/rhidium/core/assets/57721238/9ccc5763-8336-4d1e-8187-a738bafdc519) + +

+ Discord server + npm version + npm downloads +

+
+ +# @rhidium/template + +This is a Discord bot template that fully utilizes the [rhidium framework](https://rhidium.xyz). From essential system related commands to detailed command usage/statistics and integrated re-usable embed and placeholder functionality - this template aims to provide everything you need to develop powerful, scalable Discord bots fast and efficiently. + +> With Rhidium, you can focus on what's really important: **Creating meaningful features** + +Excited to begin? [Get started!](#installation) + +## Features (non-exhaustive) + +We've compromised a list a list of + +- Powerful, dynamic [middleware](https://rhidium.xyz/classes/Middleware.CommandMiddleware.html) system +- Type-safe, re-usable [controllers](https://rhidium.xyz/modules/Commands.Controllers.html) +- Dynamic [Component](https://rhidium.xyz/modules/Commands.html) Handlers/File-Loaders +- Synchronized local & API commands, automatic refreshes +- Fully localized, convenience localization for components +- Colorful console logging & verbose, organized file logging +- Wide range of everyday utilities +- CRON and interval jobs + +### Developer Friendly + +This template focuses on simplicity and robustness for developers. The primary example of this is that components are resolved from directories, not extended from a class and imported elsewhere - as commonly seen across other templates. It's our mission to get you started on your project fast, so you can focus on the important things: implementing meaningful features. + +- Check out the [API Reference](https://rhidium.xyz/) +- Made with [TypeScript](https://www.typescriptlang.org/) and [discord.js](https://discord.js.org/) +- [PM2](https://pm2.io/), [Docker](https://www.docker.com/), [docker-compose](https://docs.docker.com/compose/) configurations provided + +### Functionality + +- Detailed command usage statistics and metrics +- User configurable embed utilities, easily extendable to new functionality +- Dynamic placeholder structure, easily extendable to new data +- Discord logging for errors, guild join/leave etc. +- Administrator and Moderator permission level roles and channels +- Member Join messages, uses both Embed and Placeholder functionality as a complete integration +- Developer utility commands +- User information commands + +### Scaling + +This template grows with you and your bot. Opt-in to sharding & clustering (`x` shards for `n` processes) as you reach new milestones, all functionality included is sharding + clustering friendly by design. + +- Supports [sharding](https://discordjs.guide/sharding), which is required when your bot reaches 2500+ servers +- Supports [clustering](https://www.npmjs.com/package/discord-hybrid-sharding), which allows you to seamlessly run your bot over multiple processes + +## Database + +This TypeScript project uses [Prisma](https://www.prisma.io/docs/getting-started/quickstart) TypeORM with `mongodb` adapter. The MongoDB adapter was chosen to minimize required setup for this project. Converting to SQL adapters should (mostly) only include removing the `_id` `@map`'ing and removing `@db.ObjectId` for `@id`'s (an up-to-date sql schema file is included for your convenience). + +Available adapters: `cockroachdb`, `mongodb`, `mysql`, `postgresql`, `sqlite`, and `sqliteserver` + +- If you make changes to the schema, use the `prisma db push` command to push the new schema state to the database. +- If you want to use an existing database (pull the schema from existing database), use the `prisma db pull` command. +- Use `prisma migrate dev` to create migrations, apply them to the database, and generate artifacts (e.g. Prisma Client) + +## Configuration + +The full configuration for this project can be found [here](./config.example.json). + +- `npm run config-editor` - starts the configuration editor, edit [the script](./scripts/config-editor.mjs) if needed +- `npm run update-schema` - generate a new JSON Schema, `config-editor` automatically does this +- `npm run generate-schema` - CLI alternative to `update-schema`, included for completeness + +The `.env` file is not required, but overwrites by developers in these files are still respected. For example, you can run the production build with sharding and clustering in development mode by setting `NODE_ENV` in `.env` + +## Installation + +Are you familiar with Docker? If so, you can use the `docker-compose.yml` or `Dockerfile` to get started quickly. Otherwise, follow the steps below. + +### Pre-requisites + +- A [Discord Application](https://wiki.mirasaki.dev/docs/discord-create-application#go-to-discord-developer-portal) +- [Node.js](https://nodejs.org/en/) v16.6.0 or newer +- [MongoDB](https://www.mongodb.com/) v4.4 or newer + +### Setup Instructions + +- Download the [latest release]() or `git clone git@github.com:rhidium/template.git` the repo +- Run `npm run setup:linux` or `npm run setup:windows` in the newly created folder +- Edit the newly created `config.json` file and provide your configuration + - Alternatively, use `npm run setup:config` if you prefer a web-based editor + - Hit `ctrl+c` to stop the application once you've clicked "Save" +- Start the application: `npm run start` + +> Forever Free, open-source, and ISC licensed, meaning you're in full control diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..e685be3 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], + ignores: [ + (message) => /^Bumps \[.+]\(.+\) from .+ to .+\.$/m.test(message), + (message) => message.startsWith('chore(release): '), + ], +}; diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..a93efee --- /dev/null +++ b/config.example.json @@ -0,0 +1,70 @@ +{ + "$schema": "./config.schema.json", + "client": { + "id": "835856785512464436", + "token": "YOUR_BOT_TOKEN", + "refuse_unknown_command_interactions": true, + "development_server_id": "FOR_DEVELOPERS_ONLY" + }, + "permissions": { + "owner_id": "1148597817498140774", + "developer_ids": [ + "1148597817498140774" + ], + "system_administrator_ids": [ + "1148597817498140774" + ] + }, + "api": { + "enabled": true, + "port": 9000 + }, + "cooldown": { + "default_cooldown_enabled": true, + "default_cooldown_type": "User", + "default_cooldown_usages": 2, + "default_cooldown_duration": 15, + "default_cooldown_persistent": false + }, + "cluster": { + "enabled": false, + "total_shards": 2, + "shards_per_clusters": 1, + "total_clusters": 2, + "mode": "process", + "respawn": true, + "restarts": { + "max": 1, + "interval": 5000 + } + }, + "colors": { + "primary": "#91DDF2", + "secondary": "#99aab5", + "success": "#57F287", + "error": "#dd2e44", + "info": "#3b88c3", + "warning": "#ffcc4d", + "debug": "#9266cc", + "waiting": "#f7b977" + }, + "emojis": { + "success": "āœ…", + "error": "āŒ", + "info": ":information_source:", + "warning": "āš ļø", + "debug": "šŸ›", + "waiting": "ā³", + "separator": "ā€¢" + }, + "debug": { + "debug_mode_enabled": true, + "chat_input_command_api_data": true, + "interactions": true, + "autocomplete_execution_time": true, + "modal_submit_execution_time": true, + "command_throttling": true, + "localizations": false, + "command_data": true + } +} \ No newline at end of file diff --git a/config.schema.json b/config.schema.json new file mode 100644 index 0000000..30d21d5 --- /dev/null +++ b/config.schema.json @@ -0,0 +1 @@ +{"$schema":"http://json-schema.org/draft-07/schema#","$ref":"#/definitions/UserConfigOptions","definitions":{"UserConfigOptions":{"type":"object","properties":{"debug":{"type":"object","properties":{"debug_mode_enabled":{"type":"boolean","description":"Should we perform general debugging (everything that isn't specified in other options)"},"chat_input_command_api_data":{"type":"boolean","description":"Should the Discord API Command data be displayed when registering?"},"interactions":{"type":"boolean","description":"Should interaction objects be debugged when received?"},"autocomplete_execution_time":{"type":"boolean","description":"Should the time it takes to query auto-completes be displayed"},"modal_submit_execution_time":{"type":"boolean","description":"Should the time it takes to respond to modal submit interactions be displayed? Useful because these interaction can't be deferred"},"command_throttling":{"type":"boolean","description":"Should command usage throttling be debugged?"},"localizations":{"type":"boolean","description":"Should language localization be debugged?"},"command_data":{"type":"boolean","description":"Should a table with a complete overview of commands be displayed on boot?"}},"additionalProperties":false,"description":"Debug configuration for this instance, useful for developers"},"colors":{"type":"object","properties":{"primary":{"$ref":"#/definitions/HexColorString"},"secondary":{"$ref":"#/definitions/HexColorString"},"success":{"$ref":"#/definitions/HexColorString"},"error":{"$ref":"#/definitions/HexColorString"},"info":{"$ref":"#/definitions/HexColorString"},"warning":{"$ref":"#/definitions/HexColorString"},"debug":{"$ref":"#/definitions/HexColorString"},"waiting":{"$ref":"#/definitions/HexColorString"}},"additionalProperties":false,"description":"Color customization for embeds, and other user feedback/interactions"},"emojis":{"type":"object","properties":{"success":{"type":"string"},"error":{"type":"string"},"info":{"type":"string"},"warning":{"type":"string"},"debug":{"type":"string"},"waiting":{"type":"string"},"separator":{"type":"string"}},"additionalProperties":false,"description":"Emoji customization for embeds, and other user feedback/interactions"},"urls":{"type":"object","properties":{"github":{"type":"string","description":"The URL to the project repository"},"docs":{"type":"string","description":"The URL to the project documentation"},"website":{"type":"string","description":"The URL to the project website"},"support_server":{"type":"string","description":"The URL to the project support server"}},"additionalProperties":false,"description":"URL configuration for the client"},"logging":{"type":"object","properties":{"max_size":{"type":"string","description":"Whats the max size of a file before it should rotate"},"max_files":{"type":"string","description":"Whats the max amount of files to keep before deleting the oldest"},"zipped_archive":{"type":"boolean","description":"Should rotated log files be archived, instead of deleted?"},"directory":{"type":"string","description":"What directory should log files be stored in?"},"date_pattern":{"type":"string","description":"What date pattern should be used for log file names"},"combined_logging":{"type":"boolean","description":"Should there be a combined log file that holds everything?"},"error_logging":{"type":"boolean","description":"Should there be an error log file?"},"warn_logging":{"type":"boolean","description":"Should there be a warning log file?"},"info_logging":{"type":"boolean","description":"Should there be an info log file?"},"http_logging":{"type":"boolean","description":"Should there be an http log file?"},"verbose_logging":{"type":"boolean","description":"Should there be a verbose log file?"},"debug_logging":{"type":"boolean","description":"Should there be a debug log file?"},"silly_logging":{"type":"boolean","description":"Should there be a silly log file?"}},"additionalProperties":false,"description":"File logging (winston) configuration"},"$schema":{"type":"string","description":"JSON schema used for validation and type-ahead/Intellisense, you should never change this"},"api":{"$ref":"#/definitions/IClientAPI"},"permissions":{"$ref":"#/definitions/IPermissions","description":"Client internal permission level configuration"},"client":{"$ref":"#/definitions/IClient","description":"Discord client/bot configuration for this instance"},"cooldown":{"$ref":"#/definitions/ICommandCooldown","description":"Default command throttle configuration"},"cluster":{"$ref":"#/definitions/IClusterManagerOptions","description":"Change the entry file of this application to further fine-tune sharding and clustering settings"}},"required":["$schema","api","permissions","client","cooldown","cluster"],"additionalProperties":false},"HexColorString":{"type":"string"},"IClientAPI":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Whether the API should be enabled"},"port":{"type":["number","null"],"description":"Which port the server application should run on, between `0` and `65535` (disabled if `null`)\n\nNote: Port range `[0-1024]` should be considered reserved as they're well-known ports, these require SuperUser/Administrator access\n\nNote: Port range `[49152-65535]` should be considered reserved for Ephemeral Ports (Unix based devices, configurable)\n\nReference: {@link https://www.ncftp.com/ncftpd/doc/misc/ephemeral_ports.html }"}},"required":["enabled","port"],"additionalProperties":false},"IPermissions":{"type":"object","properties":{"owner_id":{"$ref":"#/definitions/Snowflake","description":"The bot owner's Discord user id - represents the highest permission level"},"developer_ids":{"type":"array","items":{"$ref":"#/definitions/Snowflake"},"description":"Array of Discord user id's that have the Developer permission level"},"system_administrator_ids":{"type":"array","items":{"$ref":"#/definitions/Snowflake"},"description":"Array of Discord user id's that should be able to manage the bot - like restarting, viewing logs, audits, etc."}},"required":["owner_id","developer_ids","system_administrator_ids"],"additionalProperties":false},"Snowflake":{"type":"string","description":"https://discord.com/developers/docs/reference#snowflakes"},"IClient":{"type":"object","properties":{"id":{"$ref":"#/definitions/Snowflake","description":"This client's application/user ID. You can get one here: https://discord.com/developers/applications"},"token":{"type":"string","description":"The secret token used to log in to this bot account. You can get one here: https://discord.com/developers/applications"},"development_server_id":{"$ref":"#/definitions/Snowflake","description":"The server/guild id where test commands should be registered to - development only"},"refuse_unknown_command_interactions":{"type":"boolean","description":"Should we refuse, and reply to, interactions that belong to unknown commands? You should disable this if you run multiple applications/processes under the same bot account used for this client"},"error_channel_id":{"$ref":"#/definitions/Snowflake","description":"The id of the channel where internal errors should be logged to"},"command_usage_channel_id":{"$ref":"#/definitions/Snowflake","description":"Should we log command/component usage in a Discord channel (id)?"}},"required":["id","token"],"additionalProperties":false},"ICommandCooldown":{"type":"object","properties":{"default_cooldown_enabled":{"type":"boolean","description":"Whether command throttling is enabled by default"},"default_cooldown_type":{"type":"string","enum":["User","Member","Guild","Channel","Global"],"description":"What resource/identifier this cooldown should apply to"},"default_cooldown_usages":{"type":"number","description":"The amount of time the command can be used before being throttled"},"default_cooldown_duration":{"type":"number","description":"The duration (in ms) usages last for"},"default_cooldown_persistent":{"type":"boolean","description":"Whether the cooldown is persistent (i.e. does it persist between bot restarts)? If true, uses prisma data to store cooldown (slower). If false, uses internal memory TTL cache to store cooldown for as long as they're relevant (faster, uses more RAM)"}},"required":["default_cooldown_enabled","default_cooldown_type","default_cooldown_usages","default_cooldown_duration","default_cooldown_persistent"],"additionalProperties":false,"description":"Represents a command throttling configuration that is unique to our config.json file"},"IClusterManagerOptions":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Is clustering enabled"},"total_shards":{"type":["number","null"],"description":"Total amount of shards to spawn, use null for auto"},"shards_per_clusters":{"type":"number","description":"Amount of shards per cluster"},"total_clusters":{"type":["number","null"],"description":"Amount of clusters to spawn, use null for auto"},"mode":{"type":"string","enum":["worker","process"],"description":"Mode to spawn clusters in"},"respawn":{"type":"boolean","description":"Should the process respawn on exit"},"restarts":{"$ref":"#/definitions/IClusterRestartOptions","description":"Additional cluster restart options"}},"required":["enabled","total_shards","shards_per_clusters","total_clusters","mode","respawn","restarts"],"additionalProperties":false},"IClusterRestartOptions":{"type":"object","properties":{"max":{"type":"number","description":"Maximum amount of restarts a cluster can have in the interval"},"interval":{"type":"number","description":"Interval in milliseconds on which the current restarts amount of a cluster will be resetted"}},"required":["max","interval"],"additionalProperties":false,"description":"Represents essential options to configure clustering & sharding\n\nUnfortunately we still can't use this to generate our json schema as it's not supported by the generator (bigint)"}}} \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..8f1e8ba --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,41 @@ +version: '3.1' + +services: + client: + depends_on: + - db + build: + context: . + dockerfile: Dockerfile + image: nodejs + container_name: client + restart: unless-stopped + env_file: .env + environment: + - NODE_ENV=development + volumes: + - ./:/app + - node_modules:/app/node_modules + networks: + - app-network + + db: + image: mongo:latest + container_name: db + restart: unless-stopped + env_file: .env + environment: + - MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME + - MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD + volumes: + - dbdata:/data/db + networks: + - app-network + +networks: + app-network: + driver: bridge + +volumes: + dbdata: + node_modules: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..26872a5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.1' + +services: + client: + build: + context: . + dockerfile: Dockerfile + container_name: client + restart: unless-stopped + env_file: .env + environment: + - NODE_ENV=production + volumes: + - ./:/app \ No newline at end of file diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..156a3f8 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,11 @@ +module.exports = { + apps: [ + { + name: 'rhidium-template', + script: './dist/client/index.js', + env_production: { + NODE_ENV: 'production', + }, + }, + ], +}; diff --git a/locales/en/client.json b/locales/en/client.json new file mode 100644 index 0000000..eb27b9d --- /dev/null +++ b/locales/en/client.json @@ -0,0 +1,16 @@ +{ + "initialize": { + "start": "Initializing client...", + "success": "Finished initializing client in {{duration}}, registered/loaded {{commandSize}} commands" + }, + "ready": "Client logged in as {{username}} after {{duration}}", + "clustering": { + "config": { + "disabled": "CLUSTERING is disabled/null, cannot launch cluster - use ~/client/index instead", + "invalid": "Invalid clustering configuration, cannot launch cluster" + }, + "cluster": { + "launch": "Launching cluster {{id}}" + } + } +} diff --git a/locales/en/commands.json b/locales/en/commands.json new file mode 100644 index 0000000..25e5529 --- /dev/null +++ b/locales/en/commands.json @@ -0,0 +1,9 @@ +{ + "help": { + "name": "help" + }, + "deploy": { + "name": "deploy", + "description": "Deploy commands globally or in a guild/server" + } +} \ No newline at end of file diff --git a/locales/nl/client.json b/locales/nl/client.json new file mode 100644 index 0000000..9611a1f --- /dev/null +++ b/locales/nl/client.json @@ -0,0 +1,16 @@ +{ + "initialize": { + "start": "Client initializeren...", + "success": "Client ge-initializeerd in {{duration}}, {{commandSize}} commands geladen/geregistreerd" + }, + "ready": "Client ingelogd als {{username}} na {{duration}}", + "clustering": { + "config": { + "disabled": "CLUSTERING is niet actief, kan cluster niet started - gebruik ~/client/index", + "invalid": "Ongeldige cluster configuratie, kan cluster niet started" + }, + "cluster": { + "launch": "Cluster {{id}} opstarten..." + } + } +} diff --git a/locales/nl/commands.json b/locales/nl/commands.json new file mode 100644 index 0000000..a1b0b34 --- /dev/null +++ b/locales/nl/commands.json @@ -0,0 +1,9 @@ +{ + "help": { + "name": "help" + }, + "deploy": { + "name": "commands-registreren", + "description": "Registreer commands globaal of per guild/server" + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cf8d996 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6502 @@ +{ + "name": "@rhidium/template", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@rhidium/template", + "version": "0.0.1", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.4.1", + "@rhidium/core": "^1.0.0", + "common-tags": "^1.8.2", + "discord-api-types": "^0.37.63", + "discord-hybrid-sharding": "^2.1.4", + "discord.js": "^14.14.1", + "i18next": "23.6.0", + "module-alias": "^2.2.3" + }, + "devDependencies": { + "@commitlint/cli": "^17.7.1", + "@commitlint/config-conventional": "^17.7.0", + "@types/common-tags": "^1.8.1", + "@types/express": "^4.17.18", + "@types/module-alias": "^2.0.2", + "@types/node": "^18.18.3", + "@typescript-eslint/eslint-plugin": "^6.7.5", + "@typescript-eslint/parser": "^6.7.5", + "cz-conventional-changelog": "^3.3.0", + "eslint": "^8.51.0", + "husky": "^8.0.3", + "lint-staged": "^14.0.1", + "prisma": "^5.4.1", + "rimraf": "^5.0.1", + "ts-json-schema-generator": "^1.4.0", + "ts-node-dev": "^2.0.0", + "typescript": "^5.2.2" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@commitlint/cli": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-17.8.1.tgz", + "integrity": "sha512-ay+WbzQesE0Rv4EQKfNbSMiJJ12KdKTDzIt0tcK4k11FdsWmtwP0Kp1NWMOUswfIWo6Eb7p7Ln721Nx9FLNBjg==", + "dev": true, + "dependencies": { + "@commitlint/format": "^17.8.1", + "@commitlint/lint": "^17.8.1", + "@commitlint/load": "^17.8.1", + "@commitlint/read": "^17.8.1", + "@commitlint/types": "^17.8.1", + "execa": "^5.0.0", + "lodash.isfunction": "^3.0.9", + "resolve-from": "5.0.0", + "resolve-global": "1.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-17.8.1.tgz", + "integrity": "sha512-NxCOHx1kgneig3VLauWJcDWS40DVjg7nKOpBEEK9E5fjJpQqLCilcnKkIIjdBH98kEO1q3NpE5NSrZ2kl/QGJg==", + "dev": true, + "dependencies": { + "conventional-changelog-conventionalcommits": "^6.1.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-17.8.1.tgz", + "integrity": "sha512-UUgUC+sNiiMwkyiuIFR7JG2cfd9t/7MV8VB4TZ+q02ZFkHoduUS4tJGsCBWvBOGD9Btev6IecPMvlWUfJorkEA==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.8.1", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/ensure": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-17.8.1.tgz", + "integrity": "sha512-xjafwKxid8s1K23NFpL8JNo6JnY/ysetKo8kegVM7c8vs+kWLP8VrQq+NbhgVlmCojhEDbzQKp4eRXSjVOGsow==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.8.1", + "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" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-17.8.1.tgz", + "integrity": "sha512-JHVupQeSdNI6xzA9SqMF+p/JjrHTcrJdI02PwesQIDCIGUrv04hicJgCcws5nzaoZbROapPs0s6zeVHoxpMwFQ==", + "dev": true, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/format": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-17.8.1.tgz", + "integrity": "sha512-f3oMTyZ84M9ht7fb93wbCKmWxO5/kKSbwuYvS867duVomoOsgrgljkGGIztmT/srZnaiGbaK8+Wf8Ik2tSr5eg==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.8.1", + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-17.8.1.tgz", + "integrity": "sha512-UshMi4Ltb4ZlNn4F7WtSEugFDZmctzFpmbqvpyxD3la510J+PLcnyhf9chs7EryaRFJMdAKwsEKfNK0jL/QM4g==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.8.1", + "semver": "7.5.4" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/lint": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-17.8.1.tgz", + "integrity": "sha512-aQUlwIR1/VMv2D4GXSk7PfL5hIaFSfy6hSHV94O8Y27T5q+DlDEgd/cZ4KmVI+MWKzFfCTiTuWqjfRSfdRllCA==", + "dev": true, + "dependencies": { + "@commitlint/is-ignored": "^17.8.1", + "@commitlint/parse": "^17.8.1", + "@commitlint/rules": "^17.8.1", + "@commitlint/types": "^17.8.1" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/load": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-17.8.1.tgz", + "integrity": "sha512-iF4CL7KDFstP1kpVUkT8K2Wl17h2yx9VaR1ztTc8vzByWWcbO/WaKwxsnCOqow9tVAlzPfo1ywk9m2oJ9ucMqA==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^17.8.1", + "@commitlint/execute-rule": "^17.8.1", + "@commitlint/resolve-extends": "^17.8.1", + "@commitlint/types": "^17.8.1", + "@types/node": "20.5.1", + "chalk": "^4.1.0", + "cosmiconfig": "^8.0.0", + "cosmiconfig-typescript-loader": "^4.0.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0", + "resolve-from": "^5.0.0", + "ts-node": "^10.8.1", + "typescript": "^4.6.4 || ^5.2.2" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/load/node_modules/@types/node": { + "version": "20.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.1.tgz", + "integrity": "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==", + "dev": true + }, + "node_modules/@commitlint/message": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-17.8.1.tgz", + "integrity": "sha512-6bYL1GUQsD6bLhTH3QQty8pVFoETfFQlMn2Nzmz3AOLqRVfNNtXBaSY0dhZ0dM6A2MEq4+2d7L/2LP8TjqGRkA==", + "dev": true, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/parse": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-17.8.1.tgz", + "integrity": "sha512-/wLUickTo0rNpQgWwLPavTm7WbwkZoBy3X8PpkUmlSmQJyWQTj0m6bDjiykMaDt41qcUbfeFfaCvXfiR4EGnfw==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.8.1", + "conventional-changelog-angular": "^6.0.0", + "conventional-commits-parser": "^4.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/read": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-17.8.1.tgz", + "integrity": "sha512-Fd55Oaz9irzBESPCdMd8vWWgxsW3OWR99wOntBDHgf9h7Y6OOHjWEdS9Xzen1GFndqgyoaFplQS5y7KZe0kO2w==", + "dev": true, + "dependencies": { + "@commitlint/top-level": "^17.8.1", + "@commitlint/types": "^17.8.1", + "fs-extra": "^11.0.0", + "git-raw-commits": "^2.0.11", + "minimist": "^1.2.6" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-17.8.1.tgz", + "integrity": "sha512-W/ryRoQ0TSVXqJrx5SGkaYuAaE/BUontL1j1HsKckvM6e5ZaG0M9126zcwL6peKSuIetJi7E87PRQF8O86EW0Q==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^17.8.1", + "@commitlint/types": "^17.8.1", + "import-fresh": "^3.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/rules": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-17.8.1.tgz", + "integrity": "sha512-2b7OdVbN7MTAt9U0vKOYKCDsOvESVXxQmrvuVUZ0rGFMCrCPJWWP1GJ7f0lAypbDAhaGb8zqtdOr47192LBrIA==", + "dev": true, + "dependencies": { + "@commitlint/ensure": "^17.8.1", + "@commitlint/message": "^17.8.1", + "@commitlint/to-lines": "^17.8.1", + "@commitlint/types": "^17.8.1", + "execa": "^5.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-17.8.1.tgz", + "integrity": "sha512-LE0jb8CuR/mj6xJyrIk8VLz03OEzXFgLdivBytoooKO5xLt5yalc8Ma5guTWobw998sbR3ogDd+2jed03CFmJA==", + "dev": true, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/top-level": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-17.8.1.tgz", + "integrity": "sha512-l6+Z6rrNf5p333SHfEte6r+WkOxGlWK4bLuZKbtf/2TXRN+qhrvn1XE63VhD8Oe9oIHQ7F7W1nG2k/TJFhx2yA==", + "dev": true, + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/types": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-17.8.1.tgz", + "integrity": "sha512-PXDQXkAmiMEG162Bqdh9ChML/GJZo6vU+7F03ALKDK8zYc6SuAr47LjG7hGYRqUOz+WK0dU7bQ0xzuqFMdxzeQ==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.7.0.tgz", + "integrity": "sha512-GDtbKMkg433cOZur8Dv6c25EHxduNIBsxeHrsRoIM8+AwmEZ8r0tEpckx/sHwTLwQPOF3e2JWloZh9ofCaMfAw==", + "dependencies": { + "@discordjs/formatters": "^0.3.3", + "@discordjs/util": "^1.0.2", + "@sapphire/shapeshift": "^3.9.3", + "discord-api-types": "0.37.61", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/builders/node_modules/discord-api-types": { + "version": "0.37.61", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", + "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.3.tgz", + "integrity": "sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==", + "dependencies": { + "discord-api-types": "0.37.61" + }, + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters/node_modules/discord-api-types": { + "version": "0.37.61", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", + "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" + }, + "node_modules/@discordjs/rest": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.1.0.tgz", + "integrity": "sha512-5gFWFkZX2JCFSRzs8ltx8bWmyVi0wPMk6pBa9KGIQSDPMmrP+uOrZ9j9HOwvmVWGe+LmZ5Bov0jMnQd6/jVReg==", + "dependencies": { + "@discordjs/collection": "^2.0.0", + "@discordjs/util": "^1.0.2", + "@sapphire/async-queue": "^1.5.0", + "@sapphire/snowflake": "^3.5.1", + "@vladfrangu/async_event_emitter": "^2.2.2", + "discord-api-types": "0.37.61", + "magic-bytes.js": "^1.5.0", + "tslib": "^2.6.2", + "undici": "5.27.2" + }, + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.0.0.tgz", + "integrity": "sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@discordjs/rest/node_modules/discord-api-types": { + "version": "0.37.61", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", + "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" + }, + "node_modules/@discordjs/util": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.0.2.tgz", + "integrity": "sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.0.2.tgz", + "integrity": "sha512-+XI82Rm2hKnFwAySXEep4A7Kfoowt6weO6381jgW+wVdTpMS/56qCvoXyFRY0slcv7c/U8My2PwIB2/wEaAh7Q==", + "dependencies": { + "@discordjs/collection": "^2.0.0", + "@discordjs/rest": "^2.1.0", + "@discordjs/util": "^1.0.2", + "@sapphire/async-queue": "^1.5.0", + "@types/ws": "^8.5.9", + "@vladfrangu/async_event_emitter": "^2.2.2", + "discord-api-types": "0.37.61", + "tslib": "^2.6.2", + "ws": "^8.14.2" + }, + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.0.0.tgz", + "integrity": "sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@discordjs/ws/node_modules/discord-api-types": { + "version": "0.37.61", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", + "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@eslint/js": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", + "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/client": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.6.0.tgz", + "integrity": "sha512-mUDefQFa1wWqk4+JhKPYq8BdVoFk9NFMBXUI8jAkBfQTtgx8WPx02U2HB/XbAz3GSUJpeJOKJQtNvaAIDs6sug==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee" + }, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.6.0.tgz", + "integrity": "sha512-Mt2q+GNJpU2vFn6kif24oRSBQv1KOkYaterQsi0k2/lA+dLvhRX6Lm26gon6PYHwUM8/h8KRgXIUMU0PCLB6bw==", + "devOptional": true, + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee.tgz", + "integrity": "sha512-UoFgbV1awGL/3wXuUK3GDaX2SolqczeeJ5b4FVec9tzeGbSWJboPSbT0psSrmgYAKiKnkOPFSLlH6+b+IyOwAw==" + }, + "node_modules/@rhidium/core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rhidium/core/-/core-1.0.0.tgz", + "integrity": "sha512-2le4Sns6XL4JbxaYJQEM8F6N6G3178nuIDLGx6boHFM+wSbmEu2fFVmMgJty8k6Ex2OAuxBRtaq9KLgcXpSF7g==", + "dependencies": { + "colors": "^1.4.0", + "common-tags": "^1.8.2", + "cron": "^3.1.6", + "deep-object-diff": "^1.1.9", + "discord-hybrid-sharding": "^2.1.4", + "discord.js": "^14.14.1", + "fs-extra": "^11.1.1", + "i18next": "^23.6.0", + "winston": "^3.11.0", + "winston-daily-rotate-file": "^4.7.1" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", + "integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.3.tgz", + "integrity": "sha512-WzKJSwDYloSkHoBbE8rkRW8UNKJiSRJ/P8NqJ5iVq7U2Yr/kriIBx2hW+wj2Z5e5EnXL1hgYomgaFsdK6b+zqQ==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.1.tgz", + "integrity": "sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/common-tags": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/@types/common-tags/-/common-tags-1.8.4.tgz", + "integrity": "sha512-S+1hLDJPjWNDhcGxsxEbepzaxWqURP/o+3cP4aa2w7yBXgdcmKGQtZzP8JbyfOd0m+33nh+8+kvxYE2UJtBDkg==", + "dev": true + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/luxon": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.4.tgz", + "integrity": "sha512-H9OXxv4EzJwE75aTPKpiGXJq+y4LFxjpsdgKwSmr503P5DkWc3AG7VAFYrFNVvqemT5DfgZJV9itYhqBHSGujA==" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true + }, + "node_modules/@types/module-alias": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/module-alias/-/module-alias-2.0.4.tgz", + "integrity": "sha512-5+G/QXO/DvHZw60FjvbDzO4JmlD/nG5m2/vVGt25VN1eeP3w2bCoks1Wa7VuptMPM1TxJdx6RjO70N9Fw0nZPA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.18.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz", + "integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.10", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", + "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", + "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, + "node_modules/@types/ws": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz", + "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.11.0.tgz", + "integrity": "sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.11.0", + "@typescript-eslint/type-utils": "6.11.0", + "@typescript-eslint/utils": "6.11.0", + "@typescript-eslint/visitor-keys": "6.11.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.11.0.tgz", + "integrity": "sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.11.0", + "@typescript-eslint/types": "6.11.0", + "@typescript-eslint/typescript-estree": "6.11.0", + "@typescript-eslint/visitor-keys": "6.11.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.11.0.tgz", + "integrity": "sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.11.0", + "@typescript-eslint/visitor-keys": "6.11.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.11.0.tgz", + "integrity": "sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.11.0", + "@typescript-eslint/utils": "6.11.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.11.0.tgz", + "integrity": "sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.11.0.tgz", + "integrity": "sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.11.0", + "@typescript-eslint/visitor-keys": "6.11.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.11.0.tgz", + "integrity": "sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.11.0", + "@typescript-eslint/types": "6.11.0", + "@typescript-eslint/typescript-estree": "6.11.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.11.0.tgz", + "integrity": "sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.11.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.2.tgz", + "integrity": "sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", + "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/cachedir": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", + "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz", + "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/commander": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", + "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/commitizen": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.3.0.tgz", + "integrity": "sha512-H0iNtClNEhT0fotHvGV3E9tDejDeS04sN1veIebsKYGMuGscFaswRoYJKmT3eW85eIJAs0F28bG2+a/9wCOfPw==", + "dev": true, + "dependencies": { + "cachedir": "2.3.0", + "cz-conventional-changelog": "3.3.0", + "dedent": "0.7.0", + "detect-indent": "6.1.0", + "find-node-modules": "^2.1.2", + "find-root": "1.1.0", + "fs-extra": "9.1.0", + "glob": "7.2.3", + "inquirer": "8.2.5", + "is-utf8": "^0.2.1", + "lodash": "4.17.21", + "minimist": "1.2.7", + "strip-bom": "4.0.0", + "strip-json-comments": "3.1.1" + }, + "bin": { + "commitizen": "bin/commitizen", + "cz": "bin/git-cz", + "git-cz": "bin/git-cz" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/commitizen/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/commitizen/node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/conventional-changelog-angular": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-6.0.0.tgz", + "integrity": "sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-6.1.0.tgz", + "integrity": "sha512-3cS3GEtR78zTfMzk0AizXKKIdN4OvSh7ibNz6/DPbhWWQu7LqE/8+/GqSodV+sywUR2gpJAdP/1JFf4XtN7Zpw==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/conventional-commit-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-3.0.0.tgz", + "integrity": "sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==", + "dev": true + }, + "node_modules/conventional-commits-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-4.0.0.tgz", + "integrity": "sha512-WRv5j1FsVM5FISJkoYMR6tPk07fkKT0UodruX4je86V4owk451yjXAKzKAPOs9l7y59E2viHUS9eQ+dfUA9NSg==", + "dev": true, + "dependencies": { + "is-text-path": "^1.0.1", + "JSONStream": "^1.3.5", + "meow": "^8.1.2", + "split2": "^3.2.2" + }, + "bin": { + "conventional-commits-parser": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/conventional-commits-parser/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-commits-parser/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-commits-parser/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-commits-parser/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-commits-parser/node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conventional-commits-parser/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-commits-parser/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conventional-commits-parser/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-commits-parser/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-commits-parser/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conventional-commits-parser/node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-commits-parser/node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/conventional-commits-parser/node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/conventional-commits-parser/node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/conventional-commits-parser/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-commits-parser/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/conventional-commits-parser/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/conventional-commits-parser/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.4.0.tgz", + "integrity": "sha512-BabizFdC3wBHhbI4kJh0VkQP9GkBfoHPydD0COMce1nJ1kJAB3F2TmJ/I7diULBKtmEWSwEbuN/KDtgnmUUVmw==", + "dev": true, + "engines": { + "node": ">=v14.21.3" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=7", + "ts-node": ">=10", + "typescript": ">=4" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cron": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.6.tgz", + "integrity": "sha512-cvFiQCeVzsA+QPM6fhjBtlKGij7tLLISnTSvFxVdnFGLdz+ZdXN37kNe0i2gefmdD17XuZA6n2uPVwzl4FxW/w==", + "dependencies": { + "@types/luxon": "~3.3.0", + "luxon": "~3.4.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cz-conventional-changelog": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz", + "integrity": "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "commitizen": "^4.0.3", + "conventional-commit-types": "^3.0.0", + "lodash.map": "^4.5.1", + "longest": "^2.0.1", + "word-wrap": "^1.0.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@commitlint/load": ">6.1.1" + } + }, + "node_modules/cz-conventional-changelog/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cz-conventional-changelog/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cz-conventional-changelog/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/cz-conventional-changelog/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/cz-conventional-changelog/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/cz-conventional-changelog/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/cz-conventional-changelog/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deep-object-diff": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", + "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==" + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.63", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.63.tgz", + "integrity": "sha512-WbEDWj/1JGCIC1oCMIC4z9XbYY8PrWpV5eqFFQymJhJlHMqgIjqoYbU812X5oj5cwbRrEh6Va4LNLumB2Nt6IQ==" + }, + "node_modules/discord-hybrid-sharding": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/discord-hybrid-sharding/-/discord-hybrid-sharding-2.1.4.tgz", + "integrity": "sha512-GI6RtF9kEpDEHitqNnxImBf2iArkpfWN7xPQLjxelTDql2sklKRs6lNz1vbE+bkvUmxnqZM9j2uzBBeWRppbIA==", + "dependencies": { + "node-fetch": "^2.6.7" + } + }, + "node_modules/discord.js": { + "version": "14.14.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.14.1.tgz", + "integrity": "sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==", + "dependencies": { + "@discordjs/builders": "^1.7.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.3.3", + "@discordjs/rest": "^2.1.0", + "@discordjs/util": "^1.0.2", + "@discordjs/ws": "^1.0.2", + "@sapphire/snowflake": "3.5.1", + "@types/ws": "8.5.9", + "discord-api-types": "0.37.61", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "tslib": "2.6.2", + "undici": "5.27.2", + "ws": "8.14.2" + }, + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/discord.js/node_modules/discord-api-types": { + "version": "0.37.61", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", + "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", + "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.53.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "dependencies": { + "moment": "^2.29.1" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-node-modules": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/find-node-modules/-/find-node-modules-2.1.3.tgz", + "integrity": "sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==", + "dev": true, + "dependencies": { + "findup-sync": "^4.0.0", + "merge": "^2.1.1" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/findup-sync": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", + "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^4.0.2", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/git-raw-commits": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", + "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", + "dev": true, + "dependencies": { + "dargs": "^7.0.0", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/git-raw-commits/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/git-raw-commits/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/git-raw-commits/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/git-raw-commits/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/git-raw-commits/node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-raw-commits/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/git-raw-commits/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-raw-commits/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/git-raw-commits/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/git-raw-commits/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-raw-commits/node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/git-raw-commits/node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/git-raw-commits/node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/git-raw-commits/node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/git-raw-commits/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/git-raw-commits/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/git-raw-commits/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/git-raw-commits/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", + "dev": true, + "dependencies": { + "ini": "^1.3.4" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/i18next": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.6.0.tgz", + "integrity": "sha512-z0Cxr0MGkt+kli306WS4nNNM++9cgt2b2VCMprY92j+AIab/oclgPxdwtTZVLP1zn5t5uo8M6uLsZmYrcjr3HA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.22.5" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/inquirer": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", + "dev": true, + "dependencies": { + "text-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lint-staged": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-14.0.1.tgz", + "integrity": "sha512-Mw0cL6HXnHN1ag0mN/Dg4g6sr8uf8sn98w2Oc1ECtFto9tvRF7nkXGJRbx8gPlHyoR0pLyBr2lQHbWwmUHe1Sw==", + "dev": true, + "dependencies": { + "chalk": "5.3.0", + "commander": "11.0.0", + "debug": "4.3.4", + "execa": "7.2.0", + "lilconfig": "2.1.0", + "listr2": "6.6.1", + "micromatch": "4.0.5", + "pidtree": "0.6.0", + "string-argv": "0.3.2", + "yaml": "2.3.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "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" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-6.6.1.tgz", + "integrity": "sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg==", + "dev": true, + "dependencies": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^5.0.1", + "rfdc": "^1.3.0", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/listr2/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true + }, + "node_modules/lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", + "integrity": "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==", + "dev": true, + "dependencies": { + "ansi-escapes": "^5.0.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^5.0.0", + "strip-ansi": "^7.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "dev": true, + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/logform": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", + "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/longest": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz", + "integrity": "sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", + "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-bytes.js": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.5.0.tgz", + "integrity": "sha512-wJkXvutRbNWcc37tt5j1HyOK1nosspdh3dj6LUYYAvF6JYNqs53IfRvK9oEpcwiDA1NdoIi64yAMfdivPeVAyw==" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz", + "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimist-options/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/module-alias": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", + "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==" + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prisma": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.6.0.tgz", + "integrity": "sha512-EEaccku4ZGshdr2cthYHhf7iyvCcXqwJDvnoQRAJg5ge2Tzpv0e2BaMCp+CbbDUwoVTzwgOap9Zp+d4jFa2O9A==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.6.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "dev": true, + "dependencies": { + "global-dirs": "^0.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "dev": true + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-json-schema-generator": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-1.4.0.tgz", + "integrity": "sha512-wm8vyihmGgYpxrqRshmYkWGNwEk+sf3xV2rUgxv8Ryeh7bSpMO7pZQOht+2rS002eDkFTxR7EwRPXVzrS0WJTg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.12", + "commander": "^11.0.0", + "glob": "^8.0.3", + "json5": "^2.2.3", + "normalize-path": "^3.0.0", + "safe-stable-stringify": "^2.4.3", + "typescript": "~5.2.2" + }, + "bin": { + "ts-json-schema-generator": "bin/ts-json-schema-generator" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/ts-json-schema-generator/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ts-json-schema-generator/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ts-json-schema-generator/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", + "integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==" + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "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 + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tsconfig/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "5.27.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", + "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz", + "integrity": "sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-4.7.1.tgz", + "integrity": "sha512-7LGPiYGBPNyGHLn9z33i96zx/bd71pjBn9tqQzO3I4Tayv94WPmBNwKC7CO1wPHdP9uvu+Md/1nr6VSH9h0iaA==", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^2.0.1", + "triple-beam": "^1.3.0", + "winston-transport": "^4.4.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "node_modules/winston-transport": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.6.0.tgz", + "integrity": "sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==", + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "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.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..12a9e97 --- /dev/null +++ b/package.json @@ -0,0 +1,91 @@ +{ + "name": "@rhidium/template", + "version": "0.0.1", + "description": "A TypeScript Discord bot template that fully utilizes the Rhidium framework.", + "main": "dist/src/index.js", + "license": "ISC", + "author": { + "name": "Mirasaki (Richard Hillebrand)", + "email": "me@mirasaki.dev", + "url": "https://mirasaki.dev" + }, + "scripts": { + "setup:linux": "npm install && cp .env.example .env && cp config.example.json config.json", + "setup:windows": "npm install && copy .env.example .env && copy config.example.json config.json", + "generate-schema": "npx ts-json-schema-generator --path 'src/config/*.ts' --type 'UserConfigOptions' --tsconfig tsconfig.json", + "update-schema": "node scripts/create-config-schema.mjs", + "preconfig-editor": "npm run update-schema", + "config-editor": "node scripts/config-editor.mjs", + "setup:config": "npm i @rhidium/json-editor && npm run config-editor", + "clean:dist": "rimraf ./dist", + "clean:modules": "rimraf ./node_modules", + "clean": "npm run clean:dist && npm run clean:modules", + "prebuild": "npm run clean:dist", + "build": "tsc", + "build:watch": "tsc -w", + "dev": "npm run start:dev", + "prestart": "npm run build", + "start": "node --enable-source-maps dist/src/index.js --NODE_ENV=production", + "start:dev": "ts-node-dev --respawn --notify --transpile-only src/index.ts --NODE_ENV=development", + "pretest": "npm run build", + "test": "mocha dist/test/**/*.js", + "test:dev": "ts-mocha test/**/*.spec.ts -w --watch-files '**/*.ts'", + "lint": "eslint 'src/**/*.{ts,tsx,js,jsx}'", + "lint:fix": "eslint --fix 'src/**/*.{ts,tsx,js,jsx}'", + "pm2:start": "pm2 start", + "pm2:stop": "pm2 stop", + "pm2:restart": "pm2 stop", + "pm2:reset": "pm2 reset", + "pm2:purge": "npm run pm2:stop && npm run pm2:reset && npm run pm2:delete", + "pm2:delete": "pm2 delete", + "docker:build": "docker build --tag rhidium-template .", + "docker:start": "docker run -it -p 3000:3000 --env-file ./.env -d --name my-discord-bot rhidium-template", + "docker:stop": "docker stop my-discord-bot", + "docker:restart": "docker restart my-discord-bot", + "docker:kill": "docker rm -f my-discord-bot", + "docker:purge": "docker rm -fv my-discord-bot", + "docker:shell": "docker run -it --rm my-discord-bot sh", + "docker:logs": "docker logs my-discord-bot -f", + "commit": "cz", + "prepare": "husky install" + }, + "dependencies": { + "@prisma/client": "^5.4.1", + "@rhidium/core": "^1.0.0", + "common-tags": "^1.8.2", + "discord-api-types": "^0.37.63", + "discord-hybrid-sharding": "^2.1.4", + "discord.js": "^14.14.1", + "i18next": "23.6.0", + "module-alias": "^2.2.3" + }, + "devDependencies": { + "@commitlint/cli": "^17.7.1", + "@commitlint/config-conventional": "^17.7.0", + "@types/common-tags": "^1.8.1", + "@types/express": "^4.17.18", + "@types/module-alias": "^2.0.2", + "@types/node": "^18.18.3", + "@typescript-eslint/eslint-plugin": "^6.7.5", + "@typescript-eslint/parser": "^6.7.5", + "cz-conventional-changelog": "^3.3.0", + "eslint": "^8.51.0", + "husky": "^8.0.3", + "lint-staged": "^14.0.1", + "prisma": "^5.4.1", + "rimraf": "^5.0.1", + "ts-json-schema-generator": "^1.4.0", + "ts-node-dev": "^2.0.0", + "typescript": "^5.2.2" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + }, + "lint-staged": { + "src/**/*.{js,jsx,ts,tsx,json}": [ + "eslint --fix" + ] + } +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..3aab457 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,97 @@ +generator client { + provider = "prisma-client-js" + // Docker image for the generator binary + binaryTargets = ["native", "linux-musl-openssl-3.0.x"] +} + +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(auto()) @map("_id") @db.ObjectId + userId String @unique + guildId String @db.ObjectId + guild Guild @relation(fields: [guildId], references: [id]) +} + +model Guild { + id String @id @default(auto()) @map("_id") @db.ObjectId + guildId String @unique + Users User[] + + adminRoleId String? + adminLogChannelId String? + modRoleId String? + modLogChannelId String? + + autoRoleIds String[] + + memberJoinChannelId String? + memberJoinEmbed Embed? @relation("MemberJoinEmbed", fields: [memberJoinEmbedId], references: [id]) + memberJoinEmbedId String? @db.ObjectId + + memberLeaveChannelId String? + memberLeaveEmbed Embed? @relation("MemberLeaveEmbed", fields: [memberLeaveEmbedId], references: [id]) + memberLeaveEmbedId String? @db.ObjectId +} + +model CommandCooldown { + id String @id @default(auto()) @map("_id") @db.ObjectId + cooldownId String @unique + duration Int + usages DateTime[] +} + +// Configurable Messages for users +model Embed { + id String @id @default(auto()) @map("_id") @db.ObjectId + messageText String? + color Int? + authorName String? + authorIconURL String? + authorURL String? + title String? + description String? + url String? + imageURL String? + thumbnailURL String? + footerText String? + footerIconURL String? + fields EmbedField[] + + memberJoinEmbed Guild[] @relation("MemberJoinEmbed") + memberLeaveEmbed Guild[] @relation("MemberLeaveEmbed") +} + +model EmbedField { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + value String + inline Boolean + Embed Embed? @relation(fields: [embedId], references: [id]) + embedId String? @db.ObjectId +} + +model CommandStatistics { + id String @id @default(auto()) @map("_id") @db.ObjectId + type Int + commandId String @unique + + usages DateTime[] @default([]) + lastUsedAt DateTime? + firstUsedAt DateTime? + + errorCount Int @default(0) + lastError String? + lastErrorAt DateTime? + + runtimeTotal Float? + runtimeMax Float? + runtimeMin Float? + runtimeMean Float? + runtimeMedian Float? + runtimeVariance Float? + runtimeStdDeviation Float? +} diff --git a/prisma/schema.sql.prisma b/prisma/schema.sql.prisma new file mode 100644 index 0000000..71d9cc7 --- /dev/null +++ b/prisma/schema.sql.prisma @@ -0,0 +1,95 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @unique + userId String @unique + guildId String + guild Guild @relation(fields: [guildId], references: [id]) +} + +model Guild { + id String @id @unique + guildId String @unique + Users User[] + + adminRoleId String? + adminLogChannelId String? + modRoleId String? + modLogChannelId String? + + autoRoleIds String[] + + memberJoinChannelId String? + memberJoinEmbed Embed? @relation("MemberJoinEmbed", fields: [memberJoinEmbedId], references: [id]) + memberJoinEmbedId String? + + memberLeaveChannelId String? + memberLeaveEmbed Embed? @relation("MemberLeaveEmbed", fields: [memberLeaveEmbedId], references: [id]) + memberLeaveEmbedId String? +} + +model CommandCooldown { + id String @id @unique + cooldownId String @unique + duration Int + usages DateTime[] +} + +// Configurable Messages for users +model Embed { + id String @id @unique + messageText String? + color Int? + authorName String? + authorIconURL String? + authorURL String? + title String? + description String? + url String? + imageURL String? + thumbnailURL String? + footerText String? + footerIconURL String? + fields EmbedField[] + + memberJoinEmbed Guild[] @relation("MemberJoinEmbed") + memberLeaveEmbed Guild[] @relation("MemberLeaveEmbed") +} + +model EmbedField { + id String @id @unique + name String + value String + inline Boolean + Embed Embed? @relation(fields: [embedId], references: [id]) + embedId String? +} + +model CommandStatistics { + id String @id @unique + type Int + commandId String @unique + + usages DateTime[] @default([]) + lastUsedAt DateTime? + firstUsedAt DateTime? + + errorCount Int @default(0) + lastError String? + lastErrorAt DateTime? + + runtimeTotal Float? + runtimeMax Float? + runtimeMin Float? + runtimeMean Float? + runtimeMedian Float? + runtimeVariance Float? + runtimeStdDeviation Float? +} diff --git a/release.config.js b/release.config.js new file mode 100644 index 0000000..1d4b3c8 --- /dev/null +++ b/release.config.js @@ -0,0 +1,24 @@ +const config = { + branches: [ 'main' ], + plugins: [ + '@semantic-release/commit-analyzer', + '@semantic-release/release-notes-generator', + '@semantic-release/changelog', + [ '@semantic-release/npm', { 'npmPublish': false } ], + [ + '@semantic-release/git', + { + 'assets': [ + 'CHANGELOG.md', + 'package.json', + 'package-lock.json', + 'npm-shrinkwrap.json', + ], + 'message': 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', + }, + ], + '@semantic-release/github', + ], +}; + +module.exports = config; diff --git a/scripts/config-editor.mjs b/scripts/config-editor.mjs new file mode 100644 index 0000000..cd646b7 --- /dev/null +++ b/scripts/config-editor.mjs @@ -0,0 +1,25 @@ +import { readFileSync } from 'fs'; + +(async () => { + let jsonEditor; + try { + jsonEditor = await import('@rhidium/json-editor'); + } + catch { + console.error('Please install @rhidium/json-editor: "npm i -D @rhidium/json-editor"'); + process.exit(1); + } + + const { + startJSONEditor, + } = jsonEditor; + + const jsonSchema = readFileSync('./config.schema.json', { encoding: 'utf-8' }); + + startJSONEditor({ + port: 3000, + dataFilePath: './config.json', + createBackup: true, + schemaString: jsonSchema, + }); +})(); diff --git a/scripts/create-config-schema.mjs b/scripts/create-config-schema.mjs new file mode 100644 index 0000000..a8d1e64 --- /dev/null +++ b/scripts/create-config-schema.mjs @@ -0,0 +1,13 @@ +import { writeFileSync } from 'fs'; +import { createGenerator } from 'ts-json-schema-generator'; + +const generatorOptions = { + path: './src/config/types.ts', + tsconfig: './tsconfig.json', + type: 'UserConfigOptions', +}; + +const schema = createGenerator(generatorOptions).createSchema(generatorOptions.type); +const schemaString = JSON.stringify(schema); + +writeFileSync('./config.schema.json', schemaString); diff --git a/showcase/Code_qEbP8RNlgf.png b/showcase/Code_qEbP8RNlgf.png new file mode 100644 index 0000000..3a03dd9 Binary files /dev/null and b/showcase/Code_qEbP8RNlgf.png differ diff --git a/showcase/Code_vMC8Tb5jBy.png b/showcase/Code_vMC8Tb5jBy.png new file mode 100644 index 0000000..bd19c76 Binary files /dev/null and b/showcase/Code_vMC8Tb5jBy.png differ diff --git a/showcase/Discord_fJJrwxRMlE.png b/showcase/Discord_fJJrwxRMlE.png new file mode 100644 index 0000000..a68ea0c Binary files /dev/null and b/showcase/Discord_fJJrwxRMlE.png differ diff --git a/src/auto-completes/command-statistic.ts b/src/auto-completes/command-statistic.ts new file mode 100644 index 0000000..1e30da1 --- /dev/null +++ b/src/auto-completes/command-statistic.ts @@ -0,0 +1,39 @@ +import { stringCommandTypeFromInteger } from '@/chat-input/command-usage/helpers'; +import { + COMMAND_STATISTICS_ROOT_ID, + CommandStatisticsPayload, + commandStatisticsTTLCache, +} from '@/database/CommandStatistics'; +import { AutoCompleteOption } from '@rhidium/core'; + +const CommandStatisticOption = new AutoCompleteOption({ + name: 'command-statistic', + description: 'Select the command to display statistics for', + required: true, + resolveValue: async (rawValue) => { + const allStats = await commandStatisticsTTLCache.getWithFetch(COMMAND_STATISTICS_ROOT_ID); + if (!allStats) return null; + + const stat = allStats.find((s) => s.commandId === rawValue); + if (!stat) return null; + return stat; + }, + run: async (query) => { + const allStats = await commandStatisticsTTLCache.getWithFetch(COMMAND_STATISTICS_ROOT_ID); + if (!allStats) return []; + + const stats = allStats.filter((s) => s.commandId.startsWith(query)); + if (!stats.length) return []; + + return stats.map((stat) => { + const [commandName, type] = stat.commandId.split('@'); + return { + name: `${commandName} (${stringCommandTypeFromInteger(Number(type)).replaceAll('*', '')})`, + value: stat.commandId, + description: `Used ${stat.usages.length} times`, + }; + }); + }, +}); + +export default CommandStatisticOption; diff --git a/src/auto-completes/command.ts b/src/auto-completes/command.ts new file mode 100644 index 0000000..54358f7 --- /dev/null +++ b/src/auto-completes/command.ts @@ -0,0 +1,61 @@ +import { APICommandType, AutoCompleteOption, PermissionUtils } from '@rhidium/core'; + +export enum CommandAutoCompleteQueryType { + CATEGORY = 'category', + SLASH = 'slash', + USER_CONTEXT = 'user_context', + MESSAGE_CONTEXT = 'message_context', +} + +const CommandOption = new AutoCompleteOption({ + name: 'command', + description: 'Select the command', + required: true, + run: async (query, client, interaction) => { + const { member, guild } = interaction; + const memberPermLevel = await PermissionUtils.resolveMemberPermLevel(client, member, guild); + const commands = client.commandManager.chatInput + .filter((c) => + c.data.name.indexOf(query) >= 0 + && client.commandManager.isAppropriateCommandFilter(c, member, memberPermLevel) + ); + const userCtxCommands = client.commandManager.userContextMenus + .filter((c) => + c.data.name.indexOf(query) >= 0 + && client.commandManager.isAppropriateCommandFilter(c, member, memberPermLevel) + ); + const messageCtxCommands = client.commandManager.messageContextMenus + .filter((c) => + c.data.name.indexOf(query) >= 0 + && client.commandManager.isAppropriateCommandFilter(c, member, memberPermLevel) + ); + + const categories = [ ...new Set( + commands.map((c) => c.category)), + ].filter((e) => + e !== null + && e.indexOf(query) >= 0 + ); + + return [ + ...categories.map((c) => ({ + name: `Category: ${c}`, + value: `${CommandAutoCompleteQueryType.CATEGORY}@${c}`, + })), + ...commands.map((c) => ({ + name: `Slash Command: ${c.data.name}`, + value: `${CommandAutoCompleteQueryType.SLASH}@${c.data.name}`, + })), + ...userCtxCommands.map((c) => ({ + name: `User Context Menu: ${c.data.name}`, + value: `${CommandAutoCompleteQueryType.USER_CONTEXT}@${c.data.name}`, + })), + ...messageCtxCommands.map((c) => ({ + name: `Message Context Menu: ${c.data.name}`, + value: `${CommandAutoCompleteQueryType.MESSAGE_CONTEXT}@${c.data.name}`, + })), + ]; + }, +}); + +export default CommandOption; diff --git a/src/auto-completes/placeholder-group.ts b/src/auto-completes/placeholder-group.ts new file mode 100644 index 0000000..2e8908a --- /dev/null +++ b/src/auto-completes/placeholder-group.ts @@ -0,0 +1,19 @@ +import { groupedDiscordPlaceholders } from '@/placeholders'; +import { CommandType, AutoCompleteOption } from '@rhidium/core'; + +const PlaceholderGroupOption = new AutoCompleteOption({ + name: 'placeholder-group', + description: 'Select the placeholder group', + required: true, + lowercaseQuery: true, + run: async (query) => { + return Object.keys(groupedDiscordPlaceholders) + .filter((placeholderGroup) => placeholderGroup.toLowerCase().indexOf(query) >= 0) + .map((placeholderGroup) => ({ + name: placeholderGroup, + value: placeholderGroup, + })); + }, +}); + +export default PlaceholderGroupOption; diff --git a/src/auto-completes/placeholder.ts b/src/auto-completes/placeholder.ts new file mode 100644 index 0000000..949a9d8 --- /dev/null +++ b/src/auto-completes/placeholder.ts @@ -0,0 +1,22 @@ +import { discordPlaceholderStrings, lowercaseDiscordPlaceholderStrings } from '@/placeholders'; +import { CommandType, AutoCompleteOption } from '@rhidium/core'; + +const PlaceholderOption = new AutoCompleteOption({ + name: 'placeholder', + description: 'Select the placeholder', + required: true, + lowercaseQuery: true, + run: async (query) => { + return lowercaseDiscordPlaceholderStrings + .filter((placeholder) => placeholder.indexOf(query) >= 0) + .map((placeholder) => { + const original = discordPlaceholderStrings.find((e) => e.toLowerCase() === placeholder) as string; + return { + name: original, + value: original, + }; + }); + }, +}); + +export default PlaceholderOption; diff --git a/src/buttons/eval-accept.ts b/src/buttons/eval-accept.ts new file mode 100644 index 0000000..00206c9 --- /dev/null +++ b/src/buttons/eval-accept.ts @@ -0,0 +1,130 @@ +import { inspect } from 'util'; +import { + ActionRowBuilder, + AttachmentBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, +} from 'discord.js'; +import { ButtonCommand, EmbedConstants, Embeds, PermLevel, TimeUtils } from '@rhidium/core'; +import EvalConstants from '../enums/eval'; + +const EvalAcceptCommand = new ButtonCommand({ + customId: EvalConstants.ACCEPT_CODE_EVALUATION, + permLevel: PermLevel['Bot Administrator'], + run: async (client, interaction) => { + const evalEmbed = interaction.message.embeds[0]; + if (!evalEmbed) { + EvalAcceptCommand.reply(interaction, client.embeds.error( + 'The code to evaluate could not be resolved from origin message, please try again', + )); + return; + } + + const input = evalEmbed.description; + if (!input || input.length === 0) { + EvalAcceptCommand.reply(interaction, client.embeds.error( + 'No code was provided, please try again', + )); + return; + } + const evalEmbedClone = EmbedBuilder.from(evalEmbed); + const codeInput = Embeds.extractCodeblockDescription(evalEmbedClone); + if (!codeInput) { + EvalAcceptCommand.reply(interaction, client.embeds.error( + 'No code was provided, please try again', + )); + return; + } + + const inputWithCodeblock = Embeds.extractDescription(evalEmbedClone); + if (!inputWithCodeblock) { + EvalAcceptCommand.reply(interaction, client.embeds.error( + 'No code was provided, please try again', + )); + return; + } + + await EvalAcceptCommand.deferReplyInternal(interaction); + + let evaluated: unknown; + const startEval = process.hrtime.bigint(); + try { + evaluated = eval(codeInput); + if (evaluated instanceof Promise) evaluated = await evaluated; + } + catch (err) { + EvalAcceptCommand.reply(interaction, client.embeds.error( + // Note: We show the full error here at is is a development command + `Error encountered while evaluating code: \`\`\`${err}\`\`\``, + )); + const errorEmbed = EmbedBuilder.from(evalEmbed); + errorEmbed.setColor(client.colors.error); + errorEmbed.setTitle(`${client.clientEmojis.error} This code evaluation errored`); + errorEmbed.setDescription(inputWithCodeblock); + await interaction.message.edit({ + embeds: [errorEmbed], + components: [ evalAcceptedRow ], + }).catch(() => null); + return; + } + + const files: AttachmentBuilder[] = []; + const runtime = TimeUtils.runTime(startEval); + const output = inspect(evaluated, { + depth: 0, + showHidden: false, + }); + const embed = client.embeds.success({ + title: 'Code evaluation successful', + description: `**Input:**\n\`\`\`js\n${codeInput}\n\`\`\``, + }); + + if (output.length > EmbedConstants.FIELD_VALUE_MAX_LENGTH) { + embed.addFields({ + name: ':outbox_tray: Output', + value: '```Output was too large to display, see file attachment```', + inline: false, + }); + files.push(new AttachmentBuilder(output).setName('output.txt')); + } + else { + embed.addFields({ + name: ':outbox_tray: Output', + value: `\`\`\`${output}\`\`\``, + inline: false, + }); + } + + embed.addFields({ + name: ':stopwatch: Runtime', + value: `\`\`\`${runtime}\`\`\``, + inline: false, + }); + + EvalAcceptCommand.reply(interaction, { + embeds: [ embed ], + files, + components: [ evalAcceptedRow ], + }); + + const newEmbed = client.embeds.success({ + title: 'This code has been evaluated', + description: inputWithCodeblock, + }); + await interaction.message.edit({ + embeds: [newEmbed], + components: [ evalAcceptedRow ], + }).catch(() => null); + }, +}); + +export default EvalAcceptCommand; + +export const evalAcceptedRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(EvalConstants.ACCEPT_CODE_EVALUATION) + .setLabel('Evaluated') + .setDisabled(true) + .setStyle(ButtonStyle.Success), +); diff --git a/src/buttons/eval-decline.ts b/src/buttons/eval-decline.ts new file mode 100644 index 0000000..c13f500 --- /dev/null +++ b/src/buttons/eval-decline.ts @@ -0,0 +1,48 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Colors, EmbedBuilder } from 'discord.js'; +import { ButtonCommand, Embeds, PermLevel } from '@rhidium/core'; +import EvalConstants from '../enums/eval'; + +const EvalDeclineCommand = new ButtonCommand({ + customId: EvalConstants.CANCEL_CODE_EVALUATION, + permLevel: PermLevel['Bot Administrator'], + run: async (client, interaction) => { + await EvalDeclineCommand.reply(interaction, 'Cancelling code evaluation...'); + + const evalEmbed = interaction.message.embeds[0]; + if (!evalEmbed) { + EvalDeclineCommand.reply(interaction, client.embeds.error( + 'The code to evaluate could not be resolved from origin message, please try again', + )); + return; + } + + const embed = EmbedBuilder.from(evalEmbed); + const inputWithCodeblock = Embeds.extractDescription(embed); + if (!inputWithCodeblock) { + EvalDeclineCommand.reply(interaction, client.embeds.error( + 'No code was provided, please try again', + )); + return; + } + + embed.setColor(Colors.Red); + embed.setTitle(`${client.clientEmojis.error} This code evaluation was cancelled by ${interaction.user.username}`); + embed.setDescription(inputWithCodeblock); + + await interaction.message.edit({ + embeds: [embed], + components: [ evalDeclinedRow ], + }).catch(() => null); + EvalDeclineCommand.reply(interaction, 'Code evaluation cancelled'); + }, +}); + +export default EvalDeclineCommand; + +export const evalDeclinedRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(EvalConstants.CANCEL_CODE_EVALUATION) + .setLabel('Cancelled') + .setDisabled(true) + .setStyle(ButtonStyle.Secondary), +); diff --git a/src/chat-input/administrator/admin-log-channel.ts b/src/chat-input/administrator/admin-log-channel.ts new file mode 100644 index 0000000..f28e3fd --- /dev/null +++ b/src/chat-input/administrator/admin-log-channel.ts @@ -0,0 +1,103 @@ +import { guildSettingsFromCache, updateGuildSettings } from '@/database'; +import { LoggingServices } from '@/services'; +import { ChannelType, SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommand, InteractionUtils, PermLevel } from '@rhidium/core'; + +const AdminLogChannelCommand = new ChatInputCommand({ + permLevel: PermLevel.Administrator, + isEphemeral: true, + guildOnly: true, + data: new SlashCommandBuilder() + .setDescription('Set the channel to send admin log (audit) messages to') + .addChannelOption((option) => option + .setName('channel') + .setDescription('The channel to send admin log (audit) messages to') + .setRequired(false) + .addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement), + ) + .addBooleanOption((option) => option + .setName('disable') + .setDescription('Disable admin log messages') + .setRequired(false), + ), + run: async (client, interaction) => { + const { options } = interaction; + const channel = options.getChannel('channel'); + const disable = options.getBoolean('disable') ?? false; + + const guildAvailable = InteractionUtils.requireAvailableGuild(client, interaction); + if (!guildAvailable) return; + + await AdminLogChannelCommand.deferReplyInternal(interaction); + + const guildSettings = await guildSettingsFromCache(interaction.guildId); + if (!guildSettings) { + AdminLogChannelCommand.reply( + interaction, + client.embeds.error('Guild settings not found, please try again later'), + ); + return; + } + + if (disable) { + guildSettings.adminLogChannelId = null; + await updateGuildSettings(guildSettings, { + data: { adminLogChannelId: null }, + }); + AdminLogChannelCommand.reply( + interaction, + client.embeds.success('Admin logging disabled'), + ); + LoggingServices.adminLog( + interaction.guild, + client.embeds.info({ + title: 'Admin Logging Disabled', + description: `Admin logging has been disabled by ${interaction.user}`, + }), + ); + return; + } + + if (!channel) { + AdminLogChannelCommand.reply( + interaction, + client.embeds.branding({ + fields: [{ + name: 'Admin Logging Channel', + value: guildSettings.adminLogChannelId + ? `<#${guildSettings.adminLogChannelId}>` + : 'Not set', + }], + }) + ); + return; + } + + guildSettings.adminLogChannelId = channel.id; + await updateGuildSettings(guildSettings, { + data: { adminLogChannelId: channel.id }, + }); + AdminLogChannelCommand.reply( + interaction, + client.embeds.success(`Admin logging channel set to ${channel}`), + ); + LoggingServices.adminLog( + interaction.guild, + client.embeds.info({ + title: 'Admin Logging Channel Changed', + fields: [{ + name: 'Channel', + value: `<#${channel.id}>`, + inline: true, + }, { + name: 'Member', + value: interaction.user.toString(), + inline: true, + }], + }), + ); + return; + }, +}); + +export default AdminLogChannelCommand; diff --git a/src/chat-input/administrator/admin-role.ts b/src/chat-input/administrator/admin-role.ts new file mode 100644 index 0000000..177c570 --- /dev/null +++ b/src/chat-input/administrator/admin-role.ts @@ -0,0 +1,102 @@ +import { guildSettingsFromCache, updateGuildSettings } from '@/database'; +import { LoggingServices } from '@/services'; +import { SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommand, InteractionUtils, PermLevel } from '@rhidium/core'; + +const AdministratorRoleCommand = new ChatInputCommand({ + permLevel: PermLevel.Administrator, + isEphemeral: true, + guildOnly: true, + data: new SlashCommandBuilder() + .setDescription('Set the role that determines who can use Administrator commands') + .addRoleOption((option) => option + .setName('role') + .setDescription('The role that should be able to use Administrator commands') + .setRequired(false) + ) + .addBooleanOption((option) => option + .setName('remove') + .setDescription('Remove the Administrator role') + .setRequired(false), + ), + run: async (client, interaction) => { + const { options } = interaction; + const role = options.getRole('role'); + const remove = options.getBoolean('remove') ?? false; + + const guildAvailable = InteractionUtils.requireAvailableGuild(client, interaction); + if (!guildAvailable) return; + + await AdministratorRoleCommand.deferReplyInternal(interaction); + + const guildSettings = await guildSettingsFromCache(interaction.guildId); + if (!guildSettings) { + AdministratorRoleCommand.reply( + interaction, + client.embeds.error('Guild settings not found, please try again later'), + ); + return; + } + + if (remove) { + guildSettings.adminRoleId = null; + await updateGuildSettings(guildSettings, { + data: { adminRoleId: null }, + }); + AdministratorRoleCommand.reply( + interaction, + client.embeds.success('Administrator role removed/unset'), + ); + LoggingServices.adminLog( + interaction.guild, + client.embeds.info({ + title: 'Administrator Role Removed', + description: `The Administrator role has been removed by ${interaction.user}`, + }), + ); + return; + } + + if (!role) { + AdministratorRoleCommand.reply( + interaction, + client.embeds.branding({ + fields: [{ + name: 'Administrator Role', + value: guildSettings.adminRoleId + ? `<@&${guildSettings.adminRoleId}>` + : 'Not set', + }], + }) + ); + return; + } + + guildSettings.adminRoleId = role.id; + await updateGuildSettings(guildSettings, { + data: { adminRoleId: role.id }, + }); + AdministratorRoleCommand.reply( + interaction, + client.embeds.success(`Administrator role changed to ${role}`), + ); + LoggingServices.adminLog( + interaction.guild, + client.embeds.info({ + title: 'Administrator Role Changed', + fields: [{ + name: 'Role', + value: `<@&${role.id}>`, + inline: true, + }, { + name: 'Member', + value: interaction.user.toString(), + inline: true, + }], + }), + ); + return; + }, +}); + +export default AdministratorRoleCommand; diff --git a/src/chat-input/administrator/embeds/components.ts b/src/chat-input/administrator/embeds/components.ts new file mode 100644 index 0000000..7690aac --- /dev/null +++ b/src/chat-input/administrator/embeds/components.ts @@ -0,0 +1,33 @@ +import { ButtonStyle, SlashCommandSubcommandBuilder } from 'discord.js'; +import { configureEmbedOptions, embedCommandOption } from './options'; +import { ActionRowBuilder, ButtonBuilder } from '@discordjs/builders'; +import { EmbedConfigurationConstants } from './enums'; + +export const configureEmbedSubcommand = () => { + const subcommand = new SlashCommandSubcommandBuilder() + .setName(EmbedConfigurationConstants.CONFIGURE_SUBCOMMAND_NAME) + .setDescription('Configure an embed, use special value "none" to remove existing field/value') + .addIntegerOption(embedCommandOption); + configureEmbedOptions.forEach((option) => subcommand.addStringOption(option)); + return subcommand; +}; + +export const configureEmbedControlRow = + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(EmbedConfigurationConstants.CONFIGURE_CONTINUE) + .setLabel('Continue') + .setStyle(ButtonStyle.Success), + new ButtonBuilder() + .setCustomId(EmbedConfigurationConstants.CONFIGURE_CANCEL) + .setLabel('Cancel') + .setStyle(ButtonStyle.Secondary), + ); + +export const configureEmbedAcceptedRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(EmbedConfigurationConstants.CONFIGURE_CONTINUE) + .setLabel('Saved!') + .setDisabled(true) + .setStyle(ButtonStyle.Success), +); diff --git a/src/chat-input/administrator/embeds/controllers.ts b/src/chat-input/administrator/embeds/controllers.ts new file mode 100644 index 0000000..e3bc0af --- /dev/null +++ b/src/chat-input/administrator/embeds/controllers.ts @@ -0,0 +1,576 @@ +import ConfigureEmbedsCommand from '.'; +import { configureEmbedOptions } from './options'; +import { + configureEmbedInputToEmbedData, + embedFromEmbedModel, + resolveConfigureEmbedData, + settingsKeyFromEmbedOption, +} from './helpers'; +import { EmbedController, EmbedFieldController } from './types'; +import { + ComponentType, + EmbedBuilder, + EmbedField, + escapeCodeBlock, + resolveColor, +} from 'discord.js'; +import { + configureEmbedAcceptedRow, + configureEmbedControlRow, +} from './components'; +import { Prisma } from '@prisma/client'; +import { EmbedConfigurationConstants } from './enums'; +import { LoggingServices } from '@/services'; +import { + buildDiscordPlaceholders, + replacePlaceholders, + replacePlaceholdersAcrossEmbed, +} from '@/placeholders'; +import { EmbedConstants, StringUtils, UnitConstants } from '@rhidium/core'; +import { guildTTLCache, prisma } from '@/database'; + +const jsonCodeBlockOffset = 12; + +export const configureEmbedController: EmbedController = async ( + client, + interaction, + guildSettings, +) => { + const options = interaction.options; + const embedOptionInput = options.getInteger( + EmbedConfigurationConstants.EMBED_COMMAND_OPTION_NAME, + true, + ); + + const settingKey = settingsKeyFromEmbedOption(embedOptionInput); + const setting = guildSettings[settingKey]; + const humanFriendlySettingKey = StringUtils.titleCase( + StringUtils.splitOnUppercase(settingKey), + ); + + const nullableByNone = (value: string | null) => + value === 'none' ? null : value ?? undefined; + + const embedData = Object.fromEntries( + configureEmbedOptions.map((option) => { + const value = options.getString(option.name); + return [option.name, nullableByNone(value)]; + }), + ); + + const { + embed: configureEmbedData, + message: configureEmbedMessage, + } = configureEmbedInputToEmbedData(embedData); + const embedFromSetting = setting + ? embedFromEmbedModel(setting) + : new EmbedBuilder(); + const rawEmbed = resolveConfigureEmbedData(configureEmbedData, embedFromSetting); + + const placeholders = buildDiscordPlaceholders( + interaction.channel, + interaction.guild, + interaction.member, + interaction.user + ); + const embed = replacePlaceholdersAcrossEmbed(rawEmbed, placeholders); + const resolvedMessage = configureEmbedMessage ? replacePlaceholders( + configureEmbedMessage, + placeholders, + ) : null; + + const messageSuffix = resolvedMessage ? `\n\n${resolvedMessage}` : ''; + const msg = await ConfigureEmbedsCommand.reply(interaction, { + content: `This is what the embed will look like, do you want to continue?${messageSuffix}`, + embeds: [embed], + components: [configureEmbedControlRow], + fetchReply: true, + }); + + if (!msg) { + ConfigureEmbedsCommand.reply( + interaction, + client.embeds.error( + 'Failed to send embed preview, please try again later', + ), + ); + return; + } + + let i; + try { + i = await msg.awaitMessageComponent({ + componentType: ComponentType.Button, + time: UnitConstants.MS_IN_ONE_MINUTE * 5, + filter: (i) => + i.customId === EmbedConfigurationConstants.CONFIGURE_CONTINUE || + i.customId === EmbedConfigurationConstants.CONFIGURE_CANCEL, + }); + } catch { + ConfigureEmbedsCommand.reply( + interaction, + client.embeds.error('Embed configuration expired'), + ); + return; + } + + if (i.user.id !== interaction.user.id) { + i.reply({ + content: + 'You cannot interact with this component as it was created for someone else', + ephemeral: true, + }); + return; + } + + if (i.customId === EmbedConfigurationConstants.CONFIGURE_CANCEL) { + i.update({ + content: 'Embed configuration cancelled', + components: [], + }); + return; + } + + const upsertId = guildSettings[`${settingKey}Id`] ?? null; + if (upsertId === null) { + i.update({ + content: + 'Embed configuration failed, settingKey id reference field ' + + 'couldn\'t be resolved - please try again later', + components: [], + }); + return; + } + + await ConfigureEmbedsCommand.deferReplyInternal(i); + + const fields = + configureEmbedData.fields + .filter((field) => { + const [name, value] = field.split(';'); + return name && value; + }) + .map((field) => { + const [name, value, inline] = field.split(';'); + return { + name: name as string, + value: value as string, + inline: inline === 'true', + }; + }) ?? []; + + const upsertData: { + messageText?: string | null; + color?: number | null; + authorName?: string | null; + authorIconURL?: string | null; + authorURL?: string | null; + title?: string | null; + description?: string | null; + url?: string | null; + imageURL?: string | null; + thumbnailURL?: string | null; + footerText?: string | null; + footerIconURL?: string | null; + fields?: { create: EmbedField[] }; + } = {}; + + if (rawEmbed.data.color) upsertData.color = rawEmbed.data.color; + upsertData.messageText = configureEmbedMessage ?? null; + upsertData.authorName = rawEmbed.data.author?.name ?? null; + upsertData.authorIconURL = rawEmbed.data.author?.icon_url ?? null; + upsertData.authorURL = rawEmbed.data.author?.url ?? null; + upsertData.title = rawEmbed.data.title ?? null; + upsertData.description = rawEmbed.data.description ?? null; + upsertData.url = rawEmbed.data.url ?? null; + upsertData.imageURL = rawEmbed.data.image?.url ?? null; + upsertData.thumbnailURL = rawEmbed.data.thumbnail?.url ?? null; + upsertData.footerText = rawEmbed.data.footer?.text ?? null; + upsertData.footerIconURL = rawEmbed.data.footer?.icon_url ?? null; + if (fields.length > 0) upsertData.fields = { create: fields }; + + const newTotalFields = (setting?.fields?.length ?? 0) + fields.length; + if (newTotalFields > EmbedConstants.MAX_FIELDS_LENGTH) { + i.editReply({ + content: `Embed configuration failed, embed fields length exceeds maximum of ${EmbedConstants.MAX_FIELDS_LENGTH}`, + }); + interaction.editReply({ + components: [configureEmbedAcceptedRow], + }); + return; + } + + const createEmbedColor = configureEmbedData.color + ? resolveColor(`#${configureEmbedData.color.replaceAll('#', '')}`) + : null; + const createEmbedData: Prisma.EmbedCreateInput = { + memberJoinEmbed: { + connect: { + guildId: interaction.guildId, + }, + }, + messageText: configureEmbedMessage ?? null, + color: createEmbedColor ?? null, + authorName: configureEmbedData.authorName ?? null, + authorIconURL: configureEmbedData.authorIconUrl ?? null, + authorURL: configureEmbedData.authorUrl ?? null, + title: configureEmbedData.title ?? null, + description: configureEmbedData.description ?? null, + url: configureEmbedData.url ?? null, + imageURL: configureEmbedData.imageUrl ?? null, + thumbnailURL: configureEmbedData.thumbnailUrl ?? null, + footerText: configureEmbedData.footerText ?? null, + footerIconURL: configureEmbedData.footerIconUrl ?? null, + fields: { create: fields }, + }; + + guildTTLCache.delete(interaction.guildId); + const updatedEmbed = await prisma.embed.upsert({ + update: upsertData, + create: createEmbedData, + where: { + id: upsertId!, + }, + include: { fields: true }, + }); + + i.editReply({ + content: 'Embed configuration successful', + }); + interaction.editReply({ + components: [configureEmbedAcceptedRow], + }); + + const newEmbedData = msg.embeds[0]; + if (!newEmbedData) return; + const jsonOutput = escapeCodeBlock( + JSON.stringify(updatedEmbed, null, 2) + ).slice(0, EmbedConstants.FIELD_VALUE_MAX_LENGTH - jsonCodeBlockOffset); + LoggingServices.adminLog( + interaction.guild, + client.embeds.info({ + title: 'Embed Configuration Changed', + fields: [ + { + name: 'Member', + value: interaction.user.toString(), + inline: true, + }, + { + name: 'Embed', + value: `\`\`\`${humanFriendlySettingKey}\`\`\``, + inline: true, + }, + { + name: 'Fields', + value: `\`\`\`${newEmbedData.fields?.length ?? 0}\`\`\``, + inline: true, + }, + { + name: 'JSON', + value: `\`\`\`json\n${jsonOutput}\n\`\`\``, + }, + ], + }), + ); +}; + +export const manageEmbedFieldsController: EmbedFieldController = async ( + client, + interaction, + _guildSettings, + setting, +) => { + const { options } = interaction; + const subcommand = options.getSubcommand(true); + const embedOptionInput = options.getInteger( + EmbedConfigurationConstants.EMBED_COMMAND_OPTION_NAME, + true, + ); + const settingKey = settingsKeyFromEmbedOption(embedOptionInput); + const humanFriendlySettingKey = StringUtils.titleCase( + StringUtils.splitOnUppercase(settingKey), + ); + + switch (subcommand) { + case EmbedConfigurationConstants.MANAGE_FIELDS_ADD: { + if (!setting) { + ConfigureEmbedsCommand.reply( + interaction, + client.embeds.error( + 'Embed not configured, nothing to add to - please create the embed first', + ), + ); + return; + } + + const name = options.getString('name', true); + const value = options.getString('value', true); + const inline = options.getBoolean('inline') ?? true; + + if (setting.fields.length === EmbedConstants.MAX_FIELDS_LENGTH) { + ConfigureEmbedsCommand.reply( + interaction, + client.embeds.error( + `Embed fields length exceeds maximum of ${EmbedConstants.MAX_FIELDS_LENGTH}`, + ), + ); + return; + } + + const newField: EmbedField = { + name, + value, + inline, + }; + + guildTTLCache.delete(interaction.guildId); + const updatedSetting = await prisma.embed.update({ + where: { id: setting.id }, + include: { fields: true }, + data: { + fields: { + create: [newField], + }, + }, + }); + + const rawEmbed = embedFromEmbedModel(updatedSetting); + const placeholders = buildDiscordPlaceholders( + interaction.channel, + interaction.guild, + interaction.member, + interaction.user + ); + const embed = replacePlaceholdersAcrossEmbed(rawEmbed, placeholders); + const resolvedMessage = updatedSetting.messageText + ? replacePlaceholders(updatedSetting.messageText, placeholders) + : null; + const messageSuffix = resolvedMessage ? `\n\n${resolvedMessage}` : ''; + + ConfigureEmbedsCommand.reply(interaction, { + content: `Field added to embed successfully, here is a preview:${messageSuffix}`, + embeds: [embed], + }); + + const jsonOutput = escapeCodeBlock( + JSON.stringify(newField, null, 2), + ).slice(0, EmbedConstants.FIELD_VALUE_MAX_LENGTH - jsonCodeBlockOffset); + LoggingServices.adminLog( + interaction.guild, + client.embeds.info({ + title: 'Embed Field Added', + fields: [ + { + name: 'Member', + value: `\`\`\`${interaction.user.username}\`\`\``, + inline: true, + }, + { + name: 'Embed', + value: `\`\`\`${humanFriendlySettingKey}\`\`\``, + inline: true, + }, + { + name: 'Field', + value: `\`\`\`json\n${jsonOutput}\n\`\`\``, + }, + ], + }), + ); + + break; + } + + case EmbedConfigurationConstants.MANAGE_FIELDS_REMOVE: { + if (!setting) { + ConfigureEmbedsCommand.reply( + interaction, + client.embeds.error( + 'Embed not configured, nothing to remove from - please create the embed first', + ), + ); + return; + } + + const index = options.getInteger('index', true); + if (index < 1 || index > setting.fields.length) { + ConfigureEmbedsCommand.reply( + interaction, + client.embeds.error( + `Index must be between 1 and ${setting.fields.length}`, + ), + ); + return; + } + + const targetField = setting.fields[index - 1]; + if (!targetField) { + ConfigureEmbedsCommand.reply( + interaction, + client.embeds.error( + `Field at index ${index} not found, provide a valid index between 1 and ${setting.fields.length}`, + ), + ); + return; + } + + guildTTLCache.delete(interaction.guildId); + const updatedSetting = await prisma.embed.update({ + where: { id: setting.id }, + include: { fields: true }, + data: { + fields: { delete: { id: targetField.id } }, + }, + }); + + const rawEmbed = embedFromEmbedModel(updatedSetting); + const placeholders = buildDiscordPlaceholders( + interaction.channel, + interaction.guild, + interaction.member, + interaction.user + ); + const embed = replacePlaceholdersAcrossEmbed(rawEmbed, placeholders); + const resolvedMessage = updatedSetting.messageText + ? replacePlaceholders(updatedSetting.messageText, placeholders) + : null; + const messageSuffix = resolvedMessage ? `\n\n${resolvedMessage}` : ''; + + + ConfigureEmbedsCommand.reply(interaction, { + content: `Field removed from embed successfully, here is a preview:${messageSuffix}`, + embeds: [embed], + }); + + const jsonOutput = escapeCodeBlock( + JSON.stringify(targetField, null, 2), + ).slice(0, EmbedConstants.FIELD_VALUE_MAX_LENGTH - jsonCodeBlockOffset); + LoggingServices.adminLog( + interaction.guild, + client.embeds.info({ + title: 'Embed Field Removed', + fields: [ + { + name: 'Member', + value: `\`\`\`${interaction.user.username}\`\`\``, + inline: true, + }, + { + name: 'Embed', + value: `\`\`\`${humanFriendlySettingKey}\`\`\``, + inline: true, + }, + { + name: 'Field', + value: `\`\`\`json\n${jsonOutput}\n\`\`\``, + }, + ], + }), + ); + + break; + } + + case EmbedConfigurationConstants.MANAGE_FIELDS_RESET: { + if (!setting) { + ConfigureEmbedsCommand.reply( + interaction, + client.embeds.error( + 'Embed not configured, nothing to reset - please create the embed first', + ), + ); + return; + } + + const confirm = options.getBoolean('confirm') ?? false; + if (!confirm) { + ConfigureEmbedsCommand.reply( + interaction, + client.embeds.error( + 'Please confirm that you want to reset the embed fields in the command options', + ), + ); + return; + } + + guildTTLCache.delete(interaction.guildId); + const updatedSetting = await prisma.embed.update({ + where: { id: setting.id }, + include: { fields: true }, + data: { + fields: { deleteMany: {} }, + }, + }); + + const rawEmbed = embedFromEmbedModel(updatedSetting); + const placeholders = buildDiscordPlaceholders( + interaction.channel, + interaction.guild, + interaction.member, + interaction.user + ); + const embed = replacePlaceholdersAcrossEmbed(rawEmbed, placeholders); + const resolvedMessage = updatedSetting.messageText + ? replacePlaceholders(updatedSetting.messageText, placeholders) + : null; + const messageSuffix = resolvedMessage ? `\n\n${resolvedMessage}` : ''; + + ConfigureEmbedsCommand.reply(interaction, { + content: `Fields reset successfully, here is a preview:${messageSuffix}`, + embeds: [embed], + }); + + LoggingServices.adminLog( + interaction.guild, + client.embeds.info({ + title: 'Embed Fields Reset', + fields: [ + { + name: 'Member', + value: `\`\`\`${interaction.user.username}\`\`\``, + inline: true, + }, + { + name: 'Embed', + value: `\`\`\`${humanFriendlySettingKey}\`\`\``, + inline: true, + }, + ], + }), + ); + + break; + } + + case EmbedConfigurationConstants.MANAGE_FIELDS_LIST: + default: { + if (!setting) { + ConfigureEmbedsCommand.reply( + interaction, + client.embeds.error('Embed not configured, nothing to show'), + ); + return; + } + + if (setting.fields.length === 0) { + ConfigureEmbedsCommand.reply( + interaction, + client.embeds.error('Embed has no fields, nothing to show'), + ); + return; + } + + const embed = client.embeds.branding({ + fields: setting.fields.map((e, ind) => ({ + name: `#${ind + 1} | ${e.name}`, + value: e.value, + inline: e.inline, + })), + }); + + ConfigureEmbedsCommand.reply(interaction, { embeds: [embed] }); + break; + } + } +}; diff --git a/src/chat-input/administrator/embeds/enums.ts b/src/chat-input/administrator/embeds/enums.ts new file mode 100644 index 0000000..40487e8 --- /dev/null +++ b/src/chat-input/administrator/embeds/enums.ts @@ -0,0 +1,17 @@ +export enum AvailableEmbedConfiguration { + MEMBER_JOIN = 0, + MEMBER_LEAVE = 1, +} + +export enum EmbedConfigurationConstants { + EMBED_COMMAND_OPTION_NAME = 'embed', + SHOW_SUBCOMMAND_NAME = 'show', + CONFIGURE_SUBCOMMAND_NAME = 'configure', + CONFIGURE_CONTINUE = '@embed-config-continue', + CONFIGURE_CANCEL = '@embed-config-cancel', + MANAGE_FIELDS_SUBCOMMAND_NAME = 'fields', + MANAGE_FIELDS_ADD = 'add', + MANAGE_FIELDS_REMOVE = 'remove', + MANAGE_FIELDS_LIST = 'list', + MANAGE_FIELDS_RESET = 'reset', +} diff --git a/src/chat-input/administrator/embeds/helpers.ts b/src/chat-input/administrator/embeds/helpers.ts new file mode 100644 index 0000000..3a3c0ed --- /dev/null +++ b/src/chat-input/administrator/embeds/helpers.ts @@ -0,0 +1,163 @@ +import { ConfigureEmbedData, EmbedWithFields } from './types'; +import { EmbedBuilder } from 'discord.js'; +import { AvailableEmbedConfiguration } from './enums'; +import { EmbedConstants, StringUtils } from '@rhidium/core'; +import { appConfig } from '@/config'; + +export const fieldIdentificationLength = 6; +export const maxFieldNameLength = EmbedConstants.FIELD_NAME_MAX_LENGTH - fieldIdentificationLength; + +export const settingsKeyFromEmbedOption = ( + embedOption: AvailableEmbedConfiguration, +) => + embedOption === AvailableEmbedConfiguration.MEMBER_JOIN + ? 'memberJoinEmbed' + : 'memberLeaveEmbed'; + +export const configureEmbedInputToEmbedData = (options: { + [k: string]: string | undefined | null; +}): { + embed: ConfigureEmbedData, + message: string | null, +} => { + const embedData: ConfigureEmbedData = { + title: options['title'], + color: options['color'], + authorName: options['author-name'], + authorIconUrl: options['author-icon-url'], + authorUrl: options['author-url'], + description: options['description'], + url: options['url'], + imageUrl: options['image-url'], + thumbnailUrl: options['thumbnail-url'], + footerText: options['footer-text'], + footerIconUrl: options['footer-icon-url'], + fields: [], + }; + + for (let i = 1; i <= EmbedConstants.MAX_FIELDS_LENGTH; i++) { + const field = options[`field-${i}`]; + if (!field) continue; + embedData.fields.push(field); + } + + return { + embed: embedData, + message: options['message'] ?? null, + }; +}; + +export const embedDataUrlProperties: (keyof ConfigureEmbedData)[] = [ + 'url', + 'imageUrl', + 'thumbnailUrl', + 'authorIconUrl', + 'footerIconUrl', +]; + +export const filterInvalidUrls = (data: ConfigureEmbedData) => { + const resolvedData = { ...data }; + for (const property of embedDataUrlProperties) { + const url = resolvedData[property]; + if (!url || typeof url !== 'string') continue; + const isURL = StringUtils.isUrl(url); + if (!isURL) delete resolvedData[property]; + } + return resolvedData; +}; + +/** + * Constructs an EmbedBuilder from the given ConfigureEmbedData, + * and validates and filters out invalid properties, like color and urls + */ +export const resolveConfigureEmbedData = ( + data: ConfigureEmbedData, + initialEmbed = new EmbedBuilder(), +): EmbedBuilder => { + const embed = initialEmbed; + const resolvedData = filterInvalidUrls(data); + + if (resolvedData.color) embed.setColor(resolvedData.color ? parseInt(resolvedData.color, 16) : null); + if (resolvedData.title || resolvedData.title === null) embed.setTitle(resolvedData.title); + if (resolvedData.description || resolvedData.description === null) embed.setDescription(resolvedData.description); + if (resolvedData.url || resolvedData.url === null) embed.setURL(resolvedData.url); + if (resolvedData.imageUrl || resolvedData.imageUrl === null) embed.setImage(resolvedData.imageUrl); + if (resolvedData.thumbnailUrl || resolvedData.thumbnailUrl === null) embed.setThumbnail(resolvedData.thumbnailUrl); + + if (resolvedData.authorName === null) embed.setAuthor(null); + else if (resolvedData.authorName) { + const author: { + name: string; + icon_url?: string; + url?: string; + } = { name: resolvedData.authorName }; + if (resolvedData.authorIconUrl) author.icon_url = resolvedData.authorIconUrl; + if (resolvedData.authorUrl) author.url = resolvedData.authorUrl; + embed.setAuthor(author); + } + + if (resolvedData.footerText === null) embed.setFooter(null); + else if (resolvedData.footerText) { + const footer: { + text: string; + icon_url?: string; + } = { text: resolvedData.footerText }; + if (resolvedData.footerIconUrl) footer.icon_url = resolvedData.footerIconUrl; + embed.setFooter(footer); + } + + for (const field of resolvedData.fields) { + const [name, value, inline] = field.split(';'); + if (!name || !value) continue; + embed.addFields({ + name, + value, + inline: inline === 'true', + }); + } + + return embed; +}; + +export const embedFromEmbedModel = ( + embed: EmbedWithFields | null, + baseEmbed = new EmbedBuilder(), +) => { + const embedBuilder = baseEmbed.setColor(embed?.color ?? appConfig.colors.primary); + + if (embed?.title) embedBuilder.setTitle(embed.title); + if (embed?.description) embedBuilder.setDescription(embed.description); + if (embed?.url) embedBuilder.setURL(embed.url); + if (embed?.imageURL) embedBuilder.setImage(embed.imageURL); + if (embed?.thumbnailURL) embedBuilder.setThumbnail(embed.thumbnailURL); + + if (embed?.authorName) { + const author: { + name: string; + icon_url?: string; + url?: string; + } = { name: embed?.authorName }; + if (embed?.authorIconURL) author.icon_url = embed.authorIconURL; + if (embed?.authorURL) author.url = embed.authorURL; + embedBuilder.setAuthor(author); + } + + if (embed?.footerText) { + const footer: { + text: string; + icon_url?: string; + } = { text: embed?.footerText }; + if (embed?.footerIconURL) footer.icon_url = embed.footerIconURL; + embedBuilder.setFooter(footer); + } + + if (embed?.fields) for (const field of embed.fields) { + embedBuilder.addFields({ + name: field.name, + value: field.value, + inline: field.inline, + }); + } + + return embedBuilder; +}; diff --git a/src/chat-input/administrator/embeds/index.ts b/src/chat-input/administrator/embeds/index.ts new file mode 100644 index 0000000..69a00c5 --- /dev/null +++ b/src/chat-input/administrator/embeds/index.ts @@ -0,0 +1,85 @@ +import { + SlashCommandBuilder, +} from 'discord.js'; +import { embedCommandOption, manageEmbedFieldsSubcommandGroup } from './options'; +import { configureEmbedSubcommand } from './components'; +import { embedFromEmbedModel, settingsKeyFromEmbedOption } from './helpers'; +import { configureEmbedController, manageEmbedFieldsController } from './controllers'; +import { EmbedConfigurationConstants } from './enums'; +import { ChatInputCommand, InteractionUtils, PermLevel } from '@rhidium/core'; +import { guildSettingsFromCache } from '@/database'; + +const ConfigureEmbedsCommand = new ChatInputCommand({ + permLevel: PermLevel.Administrator, + guildOnly: true, + data: new SlashCommandBuilder() + .setDescription('Configure embeds used throughout the bot') + .addSubcommand((subcommand) => + subcommand + .setName(EmbedConfigurationConstants.SHOW_SUBCOMMAND_NAME) + .setDescription('Display/render an embed') + .addIntegerOption(embedCommandOption), + ) + .addSubcommand(configureEmbedSubcommand()) + .addSubcommandGroup(manageEmbedFieldsSubcommandGroup), + run: async (client, interaction) => { + const { options } = interaction; + const subcommand = options.getSubcommand(true); + const subcommandGroup = options.getSubcommandGroup(false); + const embedOptionInput = options.getInteger( + EmbedConfigurationConstants.EMBED_COMMAND_OPTION_NAME, + true, + ); + + const guildAvailable = InteractionUtils.requireAvailableGuild(client, interaction); + if (!guildAvailable) return; + + await ConfigureEmbedsCommand.deferReplyInternal(interaction); + + const guildSettings = await guildSettingsFromCache(interaction.guild.id); + if (!guildSettings) { + ConfigureEmbedsCommand.reply( + interaction, + client.embeds.error('Guild settings not found'), + ); + return; + } + + const settingKey = settingsKeyFromEmbedOption(embedOptionInput); + const setting = guildSettings[settingKey]; + + if (subcommandGroup) { + switch (subcommandGroup) { + case EmbedConfigurationConstants.MANAGE_FIELDS_SUBCOMMAND_NAME: + default: { + manageEmbedFieldsController(client, interaction, guildSettings, setting); + break; + }} + return; + } + + + switch (subcommand) { + case EmbedConfigurationConstants.CONFIGURE_SUBCOMMAND_NAME: { + configureEmbedController(client, interaction, guildSettings); + break; + } + + case EmbedConfigurationConstants.SHOW_SUBCOMMAND_NAME: + default: { + if (setting === null) { + ConfigureEmbedsCommand.reply( + interaction, + client.embeds.error('Embed not configured, nothing to show'), + ); + return; + } + + const embed = embedFromEmbedModel(setting); + ConfigureEmbedsCommand.reply(interaction, { embeds: [embed] }); + break; + }} + }, +}); + +export default ConfigureEmbedsCommand; diff --git a/src/chat-input/administrator/embeds/options.ts b/src/chat-input/administrator/embeds/options.ts new file mode 100644 index 0000000..9478d6f --- /dev/null +++ b/src/chat-input/administrator/embeds/options.ts @@ -0,0 +1,153 @@ +import { SlashCommandIntegerOption, SlashCommandStringOption, SlashCommandSubcommandGroupBuilder } from 'discord.js'; +import { AvailableEmbedConfiguration, EmbedConfigurationConstants } from './enums'; +import { maxFieldNameLength } from './helpers'; +import { EmbedConstants } from '@rhidium/core'; + +export const embedOptions = [ + { + name: 'Member Join', + value: AvailableEmbedConfiguration.MEMBER_JOIN, + }, + { + name: 'Member Leave', + value: AvailableEmbedConfiguration.MEMBER_LEAVE, + }, +]; +export const embedCommandOption = new SlashCommandIntegerOption() + .setName(EmbedConfigurationConstants.EMBED_COMMAND_OPTION_NAME) + .setDescription('The embed to configure') + .setRequired(true) + .addChoices(...embedOptions); + +export const staticConfigureEmbedOptions = [ + new SlashCommandStringOption() + .setName('message') + .setDescription('Text to send with the embed, can ping users & roles (suppressed in embeds)') + .setRequired(false), + new SlashCommandStringOption() + .setName('color') + .setDescription('The color of the embed') + .setRequired(false), + new SlashCommandStringOption() + .setName('author-name') + .setDescription('The name of the embed author') + .setRequired(false), + new SlashCommandStringOption() + .setName('author-icon-url') + .setDescription('The icon url of the embed author') + .setRequired(false), + new SlashCommandStringOption() + .setName('author-url') + .setDescription('The url of the embed author') + .setRequired(false), + new SlashCommandStringOption() + .setName('title') + .setDescription('The title of the embed') + .setRequired(false), + new SlashCommandStringOption() + .setName('description') + .setDescription('The description of the embed') + .setRequired(false), + new SlashCommandStringOption() + .setName('url') + .setDescription('The url of the embed') + .setRequired(false), + new SlashCommandStringOption() + .setName('image-url') + .setDescription('The image url of the embed') + .setRequired(false), + new SlashCommandStringOption() + .setName('thumbnail-url') + .setDescription('The thumbnail url of the embed') + .setRequired(false), + new SlashCommandStringOption() + .setName('footer-text') + .setDescription('The footer text of the embed') + .setRequired(false), + new SlashCommandStringOption() + .setName('footer-icon-url') + .setDescription('The footer icon url of the embed') + .setRequired(false), +]; +export const configureEmbedOptions = [ + ...staticConfigureEmbedOptions, + ...Array.from( + { + length: + EmbedConstants.MAX_FIELDS_LENGTH - + staticConfigureEmbedOptions.length - + 1, + }, + (_, i) => { + const index = i + 1; + const minPossibleLength = 3; + const commaLength = 2; + const minInlineLength = 4; + return new SlashCommandStringOption() + .setName(`field-${index}`) + .setDescription( + `The field ${index} of the embed, \`name;value;inline\` format`, + ) + .setRequired(false) + .setMinLength(minPossibleLength) + .setMaxLength( + maxFieldNameLength + EmbedConstants.FIELD_VALUE_MAX_LENGTH + commaLength + minInlineLength, + ); + }, + ), +]; + +export const manageEmbedFieldsSubcommandGroup = new SlashCommandSubcommandGroupBuilder() + .setName(EmbedConfigurationConstants.MANAGE_FIELDS_SUBCOMMAND_NAME) + .setDescription('Manage the fields of the embed') + .addSubcommand((subcommand) => + subcommand + .setName(EmbedConfigurationConstants.MANAGE_FIELDS_ADD) + .setDescription('Add a field to the embed') + .addIntegerOption(embedCommandOption) + .addStringOption((option) => option + .setName('name') + .setDescription('The name of this field') + .setRequired(true) + .setMaxLength(maxFieldNameLength), + ) + .addStringOption((option) => option + .setName('value') + .setDescription('The value of this field') + .setRequired(true) + .setMaxLength(EmbedConstants.FIELD_VALUE_MAX_LENGTH), + ) + .addBooleanOption((option) => option + .setName('inline') + .setDescription('Whether this field should be inline') + .setRequired(false), + ), + ) + .addSubcommand((subcommand) => + subcommand + .setName(EmbedConfigurationConstants.MANAGE_FIELDS_REMOVE) + .setDescription('Remove a field from the embed') + .addIntegerOption(embedCommandOption) + .addIntegerOption((option) => option + .setName('index') + .setDescription('The index of the field to remove') + .setRequired(true), + ), + ) + .addSubcommand((subcommand) => + subcommand + .addIntegerOption(embedCommandOption) + .setName(EmbedConfigurationConstants.MANAGE_FIELDS_LIST) + .setDescription('List the fields of the embed'), + ) + .addSubcommand((subcommand) => + subcommand + .addIntegerOption(embedCommandOption) + .setName(EmbedConfigurationConstants.MANAGE_FIELDS_RESET) + .setDescription('Reset the fields of the embed') + .addBooleanOption((option) => option + .setName('confirm') + .setDescription('Whether to confirm the reset') + .setRequired(false), + ), + ); diff --git a/src/chat-input/administrator/embeds/types.ts b/src/chat-input/administrator/embeds/types.ts new file mode 100644 index 0000000..ed79bbb --- /dev/null +++ b/src/chat-input/administrator/embeds/types.ts @@ -0,0 +1,36 @@ +import { GuildWithEmbeds } from '@/database'; +import { Prisma } from '@prisma/client'; +import { ChatInputCommandInteraction } from 'discord.js'; +import { GuildCommandController } from '@rhidium/core'; + +/** + * Nothing is required in this data structure, all fields are optional + * and will be set to null if not provided - when constructing an embed + * from this data, make sure either title or description has a default + */ +export type ConfigureEmbedData = { + title: string | null | undefined; + color: string | null | undefined; + authorName: string | null | undefined; + authorIconUrl: string | null | undefined; + authorUrl: string | null | undefined; + description: string | null | undefined; + url: string | null | undefined; + imageUrl: string | null | undefined; + thumbnailUrl: string | null | undefined; + footerText: string | null | undefined; + footerIconUrl: string | null | undefined; + fields: string[]; +}; + +export type EmbedWithFields = Prisma.EmbedGetPayload<{ + include: { fields: true }; +}>; + +export type EmbedController = GuildCommandController< + ChatInputCommandInteraction, [ GuildWithEmbeds ] +> + +export type EmbedFieldController = GuildCommandController< + ChatInputCommandInteraction, [ GuildWithEmbeds, EmbedWithFields | null ] +> diff --git a/src/chat-input/administrator/member-join-channel.ts b/src/chat-input/administrator/member-join-channel.ts new file mode 100644 index 0000000..001b6d1 --- /dev/null +++ b/src/chat-input/administrator/member-join-channel.ts @@ -0,0 +1,103 @@ +import { guildSettingsFromCache, updateGuildSettings } from '@/database'; +import { LoggingServices } from '@/services'; +import { ChannelType, SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommand, InteractionUtils, PermLevel } from '@rhidium/core'; + +const MemberJoinChannelCommand = new ChatInputCommand({ + permLevel: PermLevel.Administrator, + isEphemeral: true, + guildOnly: true, + data: new SlashCommandBuilder() + .setDescription('Set the channel to send member join messages to') + .addChannelOption((option) => option + .setName('channel') + .setDescription('The channel to send member join messages to') + .setRequired(false) + .addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement), + ) + .addBooleanOption((option) => option + .setName('disable') + .setDescription('Disable member join messages') + .setRequired(false), + ), + run: async (client, interaction) => { + const { options } = interaction; + const channel = options.getChannel('channel'); + const disable = options.getBoolean('disable') ?? false; + + const guildAvailable = InteractionUtils.requireAvailableGuild(client, interaction); + if (!guildAvailable) return; + + await MemberJoinChannelCommand.deferReplyInternal(interaction); + + const guildSettings = await guildSettingsFromCache(interaction.guildId); + if (!guildSettings) { + MemberJoinChannelCommand.reply( + interaction, + client.embeds.error('Guild settings not found, please try again later'), + ); + return; + } + + if (disable) { + guildSettings.memberJoinChannelId = null; + await updateGuildSettings(guildSettings, { + data: { memberJoinChannelId: null }, + }); + MemberJoinChannelCommand.reply( + interaction, + client.embeds.success('Member join messages disabled'), + ); + LoggingServices.adminLog( + interaction.guild, + client.embeds.info({ + title: 'Member Join Messages Disabled', + description: `Member join messages have been disabled by ${interaction.user}`, + }), + ); + return; + } + + if (!channel) { + MemberJoinChannelCommand.reply( + interaction, + client.embeds.branding({ + fields: [{ + name: 'Member Join Channel', + value: guildSettings.memberJoinChannelId + ? `<#${guildSettings.memberJoinChannelId}>` + : 'Not set', + }], + }) + ); + return; + } + + guildSettings.memberJoinChannelId = channel.id; + await updateGuildSettings(guildSettings, { + data: { memberJoinChannelId: channel.id }, + }); + MemberJoinChannelCommand.reply( + interaction, + client.embeds.success(`Member join channel set to ${channel}`), + ); + LoggingServices.adminLog( + interaction.guild, + client.embeds.info({ + title: 'Member Join Channel Changed', + fields: [{ + name: 'Channel', + value: `<#${channel.id}>`, + inline: true, + }, { + name: 'Member', + value: interaction.user.toString(), + inline: true, + }], + }), + ); + return; + }, +}); + +export default MemberJoinChannelCommand; diff --git a/src/chat-input/administrator/mod-log-channel.ts b/src/chat-input/administrator/mod-log-channel.ts new file mode 100644 index 0000000..7177cce --- /dev/null +++ b/src/chat-input/administrator/mod-log-channel.ts @@ -0,0 +1,103 @@ +import { guildSettingsFromCache, updateGuildSettings } from '@/database'; +import { LoggingServices } from '@/services'; +import { ChannelType, SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommand, InteractionUtils, PermLevel } from '@rhidium/core'; + +const ModLogChannelCommand = new ChatInputCommand({ + permLevel: PermLevel.Administrator, + isEphemeral: true, + guildOnly: true, + data: new SlashCommandBuilder() + .setDescription('Set the channel to send moderator log messages to') + .addChannelOption((option) => option + .setName('channel') + .setDescription('The channel to send moderator log messages to') + .setRequired(false) + .addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement), + ) + .addBooleanOption((option) => option + .setName('disable') + .setDescription('Disable moderator log messages') + .setRequired(false), + ), + run: async (client, interaction) => { + const { options } = interaction; + const channel = options.getChannel('channel'); + const disable = options.getBoolean('disable') ?? false; + + const guildAvailable = InteractionUtils.requireAvailableGuild(client, interaction); + if (!guildAvailable) return; + + await ModLogChannelCommand.deferReplyInternal(interaction); + + const guildSettings = await guildSettingsFromCache(interaction.guildId); + if (!guildSettings) { + ModLogChannelCommand.reply( + interaction, + client.embeds.error('Guild settings not found, please try again later'), + ); + return; + } + + if (disable) { + guildSettings.modLogChannelId = null; + await updateGuildSettings(guildSettings, { + data: { modLogChannelId: null }, + }); + ModLogChannelCommand.reply( + interaction, + client.embeds.success('Moderator logging disabled'), + ); + LoggingServices.adminLog( + interaction.guild, + client.embeds.info({ + title: 'Moderator Logging Disabled', + description: `Moderator logging has been disabled by ${interaction.user}`, + }), + ); + return; + } + + if (!channel) { + ModLogChannelCommand.reply( + interaction, + client.embeds.branding({ + fields: [{ + name: 'Moderator Logging Channel', + value: guildSettings.modLogChannelId + ? `<#${guildSettings.modLogChannelId}>` + : 'Not set', + }], + }) + ); + return; + } + + guildSettings.modLogChannelId = channel.id; + await updateGuildSettings(guildSettings, { + data: { modLogChannelId: channel.id }, + }); + ModLogChannelCommand.reply( + interaction, + client.embeds.success(`Moderator logging channel set to ${channel}`), + ); + LoggingServices.adminLog( + interaction.guild, + client.embeds.info({ + title: 'Moderator Logging Channel Changed', + fields: [{ + name: 'Channel', + value: `<#${channel.id}>`, + inline: true, + }, { + name: 'Member', + value: interaction.user.toString(), + inline: true, + }], + }), + ); + return; + }, +}); + +export default ModLogChannelCommand; diff --git a/src/chat-input/administrator/mod-role.ts b/src/chat-input/administrator/mod-role.ts new file mode 100644 index 0000000..991814c --- /dev/null +++ b/src/chat-input/administrator/mod-role.ts @@ -0,0 +1,102 @@ +import { guildSettingsFromCache, updateGuildSettings } from '@/database'; +import { LoggingServices } from '@/services'; +import { SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommand, InteractionUtils, PermLevel } from '@rhidium/core'; + +const ModRoleCommand = new ChatInputCommand({ + permLevel: PermLevel.Administrator, + isEphemeral: true, + guildOnly: true, + data: new SlashCommandBuilder() + .setDescription('Set the role that determines who can use Moderator commands') + .addRoleOption((option) => option + .setName('role') + .setDescription('The role that should be able to use Moderator commands') + .setRequired(false) + ) + .addBooleanOption((option) => option + .setName('remove') + .setDescription('Remove the Moderator role') + .setRequired(false), + ), + run: async (client, interaction) => { + const { options } = interaction; + const role = options.getRole('role'); + const remove = options.getBoolean('remove') ?? false; + + const guildAvailable = InteractionUtils.requireAvailableGuild(client, interaction); + if (!guildAvailable) return; + + await ModRoleCommand.deferReplyInternal(interaction); + + const guildSettings = await guildSettingsFromCache(interaction.guildId); + if (!guildSettings) { + ModRoleCommand.reply( + interaction, + client.embeds.error('Guild settings not found, please try again later'), + ); + return; + } + + if (remove) { + guildSettings.modRoleId = null; + await updateGuildSettings(guildSettings, { + data: { modRoleId: null }, + }); + ModRoleCommand.reply( + interaction, + client.embeds.success('Moderator role removed/unset'), + ); + LoggingServices.adminLog( + interaction.guild, + client.embeds.info({ + title: 'Moderator Role Removed', + description: `The Moderator role has been removed by ${interaction.user}`, + }), + ); + return; + } + + if (!role) { + ModRoleCommand.reply( + interaction, + client.embeds.branding({ + fields: [{ + name: 'Moderator Role', + value: guildSettings.modRoleId + ? `<@&${guildSettings.modRoleId}>` + : 'Not set', + }], + }) + ); + return; + } + + guildSettings.modRoleId = role.id; + await updateGuildSettings(guildSettings, { + data: { modRoleId: role.id }, + }); + ModRoleCommand.reply( + interaction, + client.embeds.success(`Moderator role changed to ${role}`), + ); + LoggingServices.adminLog( + interaction.guild, + client.embeds.info({ + title: 'Moderator Role Changed', + fields: [{ + name: 'Role', + value: `<@&${role.id}>`, + inline: true, + }, { + name: 'Member', + value: interaction.user.toString(), + inline: true, + }], + }), + ); + return; + }, +}); + +export default ModRoleCommand; diff --git a/src/chat-input/administrator/placeholders/enums.ts b/src/chat-input/administrator/placeholders/enums.ts new file mode 100644 index 0000000..f5da998 --- /dev/null +++ b/src/chat-input/administrator/placeholders/enums.ts @@ -0,0 +1,6 @@ +export enum PlaceholderConstants { + LIST_SUBCOMMAND_GROUP_NAME = 'list', + LIST_PLACEHOLDER_GROUPS_SUBCOMMAND_NAME = 'group', + LIST_PLACEHOLDERS_SUBCOMMAND_NAME = 'placeholders', + PLACEHOLDER_INFO_SUBCOMMAND_NAME = 'info', +} diff --git a/src/chat-input/administrator/placeholders/index.ts b/src/chat-input/administrator/placeholders/index.ts new file mode 100644 index 0000000..970bc23 --- /dev/null +++ b/src/chat-input/administrator/placeholders/index.ts @@ -0,0 +1,103 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { PlaceholderConstants } from './enums'; +import { discordPlaceholders, groupedDiscordPlaceholders } from '@/placeholders'; +import { listPlaceholderSubcommandGroup, placeholderInfoSubcommand } from './options'; +import PlaceholderGroupOption from '@/auto-completes/placeholder-group'; +import PlaceholderOption from '@/auto-completes/placeholder'; +import { stripIndents } from 'common-tags'; +import ConfigureEmbedsCommand from '../embeds'; +import { ChatInputCommand, InteractionUtils, PermLevel } from '@rhidium/core'; + +const PlaceholdersCommand = new ChatInputCommand({ + permLevel: PermLevel.Administrator, + guildOnly: true, + data: new SlashCommandBuilder() + .setDescription('Manage placeholders used throughout the bot') + .addSubcommand(placeholderInfoSubcommand) + .addSubcommandGroup(listPlaceholderSubcommandGroup), + run: async (client, interaction) => { + const { options } = interaction; + const subcommand = options.getSubcommand(true); + const subcommandGroup = options.getSubcommandGroup(false); + + const guildAvailable = InteractionUtils.requireAvailableGuild(client, interaction); + if (!guildAvailable) return; + + const manageEmbedCommand = await client.commandManager.commandLink(ConfigureEmbedsCommand.data.name); + if (subcommand === PlaceholderConstants.PLACEHOLDER_INFO_SUBCOMMAND_NAME) { + const embed = client.embeds.branding({ + title: 'Placeholder Information', + description: stripIndents` + Placeholders can be used in messages to dynamically replace the placeholder with a value. + + For example, \`{{user}}\` will be replaced with the user's name, like **${interaction.member.user.username}**. + + These placeholders can be applied to any message that supports them, + like the welcome and leave messages. + + You can start customizing placeholders by using the **${manageEmbedCommand}** command, + which will display previews of what your final embeds will look like. + `, + fields: [{ + name: 'Placeholder Groups', + value: `\`\`\`${Object.keys(groupedDiscordPlaceholders).length}\`\`\``, + inline: true, + }, { + name: 'Placeholders', + value: `\`\`\`${Object.keys(discordPlaceholders).length}\`\`\``, + inline: true, + }], + }); + PlaceholdersCommand.reply(interaction, embed); + return; // escape early + } + + switch (subcommandGroup) { + case PlaceholderConstants.LIST_SUBCOMMAND_GROUP_NAME: + default: { + switch (subcommand) { + case PlaceholderConstants.LIST_PLACEHOLDER_GROUPS_SUBCOMMAND_NAME: { + const placeholderGroup = options.getString(PlaceholderGroupOption.name, true); + const resolvedGroup = groupedDiscordPlaceholders[placeholderGroup]; + if (!resolvedGroup) { + const embed = client.embeds.error(`Placeholder group \`${placeholderGroup}\` not found`); + PlaceholdersCommand.reply(interaction, embed); + return; + } + + const longestKey = Object.keys(resolvedGroup).reduce((a, b) => a.length > b.length ? a : b); + const centerPadKey = (key: string) => key.padStart((longestKey.length + key.length) / 2, ' ') + .padEnd(longestKey.length, ' '); + + const embed = client.embeds.branding({ + title: `Available Placeholders for ${placeholderGroup}`, + description: Object.entries(resolvedGroup) + .map(([k, v]) => `**\`{{${centerPadKey(k)}}}\`** ${v}`) + .join('\n'), + }); + PlaceholdersCommand.reply(interaction, embed); + break; + } + case PlaceholderConstants.LIST_PLACEHOLDERS_SUBCOMMAND_NAME: + default: { + const placeholder = options.getString(PlaceholderOption.name, true); + const cleanPlaceholder = placeholder.replace(/{{|}}/g, ''); + const placeholderValue = discordPlaceholders[cleanPlaceholder as keyof typeof discordPlaceholders]; + if (!placeholderValue) { + const embed = client.embeds.error(`Placeholder \`${placeholder}\` not found`); + PlaceholdersCommand.reply(interaction, embed); + return; + } + + const embed = client.embeds.branding({ + title: `Placeholder Definition for ${cleanPlaceholder}`, + description: `**\`${placeholder}\`**\n\`\`\`${placeholderValue}\`\`\``, + }); + PlaceholdersCommand.reply(interaction, embed); + break; + }} + }} + }, +}); + +export default PlaceholdersCommand; diff --git a/src/chat-input/administrator/placeholders/options.ts b/src/chat-input/administrator/placeholders/options.ts new file mode 100644 index 0000000..97d2538 --- /dev/null +++ b/src/chat-input/administrator/placeholders/options.ts @@ -0,0 +1,24 @@ +import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from 'discord.js'; +import { PlaceholderConstants } from './enums'; +import PlaceholderOption from '@/auto-completes/placeholder'; +import PlaceholderGroupOption from '@/auto-completes/placeholder-group'; + +export const listPlaceholderSubcommandGroup = new SlashCommandSubcommandGroupBuilder() + .setName(PlaceholderConstants.LIST_SUBCOMMAND_GROUP_NAME) + .setDescription('List all available placeholders') + .addSubcommand(subcommand => + subcommand + .setName(PlaceholderConstants.LIST_PLACEHOLDER_GROUPS_SUBCOMMAND_NAME) + .setDescription('List all available placeholder groups') + .addStringOption(PlaceholderGroupOption.addOptionHandler) + ) + .addSubcommand(subcommand => + subcommand + .setName(PlaceholderConstants.LIST_PLACEHOLDERS_SUBCOMMAND_NAME) + .setDescription('List all available placeholders') + .addStringOption(PlaceholderOption.addOptionHandler) + ); + +export const placeholderInfoSubcommand = new SlashCommandSubcommandBuilder() + .setName(PlaceholderConstants.PLACEHOLDER_INFO_SUBCOMMAND_NAME) + .setDescription('Display information about placeholders'); diff --git a/src/chat-input/command-usage/enums.ts b/src/chat-input/command-usage/enums.ts new file mode 100644 index 0000000..e4980db --- /dev/null +++ b/src/chat-input/command-usage/enums.ts @@ -0,0 +1,5 @@ +export enum CommandUsageConstants { + LEADERBOARD_SUBCOMMAND_NAME = 'leaderboard', + ITEM_SUBCOMMAND_NAME = 'for', + DELETE_ITEM_SUBCOMMAND_NAME = 'delete', +} diff --git a/src/chat-input/command-usage/helpers.ts b/src/chat-input/command-usage/helpers.ts new file mode 100644 index 0000000..7b36807 --- /dev/null +++ b/src/chat-input/command-usage/helpers.ts @@ -0,0 +1,139 @@ +import { ApplicationCommandType } from 'discord.js'; +import { stripIndents } from 'common-tags'; +import { Client, ComponentCommandType, TimeUtils, UnitConstants } from '@rhidium/core'; +import { CommandStatisticsPayload } from '@/database/CommandStatistics'; + +export const stringCommandTypeFromInteger = (type: number) => { + if (type === ApplicationCommandType.ChatInput) return '**Slash** Command'; + if (type === ApplicationCommandType.User) return '**User Context Menu** Command'; + if (type === ApplicationCommandType.Message) return '**Message Context Menu** Command'; + if (type === ComponentCommandType.BUTTON) return '**Button** Component'; + if (type === ComponentCommandType.SELECT_MENU) return '**Select Menu** Component'; + if (type === ComponentCommandType.MODAL) return '**Modal** Component'; + return '**Unknown** Command'; +}; + +export const embedFromUsageStatistics = (client: Client, stats: CommandStatisticsPayload) => { + const [commandId] = stats.commandId.split('@'); + + const runtimeAverage = stats.runtimeMean ? `${stats.runtimeMean.toFixed(2)}ms` : 'Unknown'; + const runtimeMedian = stats.runtimeMedian ? `${stats.runtimeMedian.toFixed(2)}ms` : 'Unknown'; + const runtimeVariance = stats.runtimeVariance ? `${stats.runtimeVariance.toFixed(2)}ms` : 'Unknown'; + const runtimeDeviation = stats.runtimeStdDeviation ? `${stats.runtimeStdDeviation.toFixed(2)}ms` : 'Unknown'; + + const runtimeTotal = stats.runtimeTotal ? TimeUtils.msToHumanReadableTime(stats.runtimeTotal) : 'Unknown'; + const firstUsedAt = stats.firstUsedAt ? TimeUtils.discordInfoTimestamp(stats.firstUsedAt.valueOf()) : 'Never'; + const lastUsedAt = stats.lastUsedAt ? TimeUtils.discordInfoTimestamp(stats.lastUsedAt.valueOf()) : 'Never'; + const lastErrorAt = stats.lastErrorAt ? TimeUtils.discordInfoTimestamp(stats.lastErrorAt.valueOf()) : 'Never'; + + const usagesLastHour = stats.usages.filter((u) => u.valueOf() > Date.now() - UnitConstants.MS_IN_ONE_HOUR); + const usagesLastDay = stats.usages.filter((u) => u.valueOf() > Date.now() - UnitConstants.MS_IN_ONE_DAY); + const usagesLastWeek = stats.usages.filter((u) => u.valueOf() > Date.now() - UnitConstants.MS_IN_ONE_DAY * 7); + const usagesLastMonth = stats.usages.filter((u) => u.valueOf() > Date.now() - UnitConstants.MS_IN_ONE_DAY * 30); + const usagesLastYear = stats.usages.filter((u) => u.valueOf() > Date.now() - UnitConstants.MS_IN_ONE_DAY * 365); + + const averagePerHour = TimeUtils.calculateAverageFromDateArr(stats.usages, UnitConstants.MS_IN_ONE_HOUR); + const averagePerDay = TimeUtils.calculateAverageFromDateArr(stats.usages, UnitConstants.MS_IN_ONE_DAY); + const averagePerWeek = TimeUtils.calculateAverageFromDateArr(stats.usages, UnitConstants.MS_IN_ONE_DAY * 7); + const averagePerMonth = TimeUtils.calculateAverageFromDateArr(stats.usages, UnitConstants.MS_IN_ONE_DAY * 30); + const averagePerYear = TimeUtils.calculateAverageFromDateArr(stats.usages, UnitConstants.MS_IN_ONE_DAY * 365); + + const embed = client.embeds.branding({ + title: `Command Statistics for ${commandId}`, + description: stripIndents` + **${commandId}** has been used **${stats.usages.length}** times: + + **Last Hour**: ${usagesLastHour.length} (~${averagePerHour.toFixed(2)}/h) + **Last Day**: ${usagesLastDay.length} (~${averagePerDay.toFixed(2)}/d) + **Last Week**: ${usagesLastWeek.length} (~${averagePerWeek.toFixed(2)}/w) + **Last Month**: ${usagesLastMonth.length} (~${averagePerMonth.toFixed(2)}/M) + **Last Year**: ${usagesLastYear.length} (~${averagePerYear.toFixed(2)}/y) + `, + fields: [ + { + name: 'Command Type', + value: stringCommandTypeFromInteger(stats.type), + inline: true, + }, + { + name: 'Total Usages', + value: stats.usages.length.toString(), + inline: true, + }, + { + name: 'Total Runtime', + value: `${runtimeTotal}`, + inline: true, + }, + + { + name: 'Average Runtime', + value: `${runtimeAverage}`, + inline: true, + }, + { + name: 'Median Runtime', + value: `${runtimeMedian}`, + inline: true, + }, + { + name: 'Runtime Deviation', + value: `${runtimeDeviation} (variance: ${runtimeVariance})`, + inline: true, + }, + + { + name: 'Total Errors', + value: stats.errorCount.toString(), + inline: true, + }, + { + name: 'Last Error', + value: lastErrorAt, + inline: true, + }, + { + name: 'Last Error Message', + value: stats.lastError ? `\`\`\`\n${stats.lastError}\`\`\`` : 'Never', + inline: !stats.lastError, + }, + + { + name: 'First Used', + value: firstUsedAt, + inline: true, + }, + { + name: 'Last Used', + value: lastUsedAt, + inline: true, + }, + ], + }); + return embed; +}; + +export const compactEmbedFromUsageStatistics = ( + client: Client, + stats: CommandStatisticsPayload[], + indexOffset = 0, +) => { + const embed = client.embeds.branding({ + title: 'Command Usage Leaderboard', + description: 'The top most used commands', + fields: stats.map((stat, index) => { + const [ commandId ] = stat.commandId.split('@'); + const runtimeAverage = stat.runtimeMean ? `${stat.runtimeMean.toFixed(2)}ms` : 'Unknown'; + const totalRuntime = stat.runtimeTotal ? `${TimeUtils.msToHumanReadableTime(stat.runtimeTotal)}` : 'Unknown'; + return { + name: `${index + 1 + indexOffset}. ${commandId} (${stringCommandTypeFromInteger(stat.type)})`, + value: `Used ${stat.usages.length} time${stat.usages.length === 1 ? '' : 's'}` + + `, ${runtimeAverage} average runtime, ${totalRuntime} total runtime` + , + inline: false, + }; + }), + }); + + return embed; +}; diff --git a/src/chat-input/command-usage/index.ts b/src/chat-input/command-usage/index.ts new file mode 100644 index 0000000..3ab2ebf --- /dev/null +++ b/src/chat-input/command-usage/index.ts @@ -0,0 +1,66 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { CommandUsageConstants } from './enums'; +import { deleteItemSubcommand, itemSubcommand, leaderboardSubcommand } from './options'; +import { compactEmbedFromUsageStatistics, embedFromUsageStatistics, stringCommandTypeFromInteger } from './helpers'; +import CommandStatisticOption from '@/auto-completes/command-statistic'; +import { ArrayUtils, ChatInputCommand, InteractionUtils, PermLevel, isAutoCompleteResponseType } from '@rhidium/core'; +import { COMMAND_STATISTICS_ROOT_ID, commandStatisticsTTLCache } from '@/database/CommandStatistics'; +import { prisma } from '@/database'; + +export const compactEntriesPerPage = 10; + +const CommandUsageCommand = new ChatInputCommand({ + category: 'Developer', // Could alternatively use folder name + permLevel: PermLevel['Bot Administrator'], + data: new SlashCommandBuilder() + .setDescription('View detailed command usage information') + .addSubcommand(leaderboardSubcommand) + .addSubcommand(itemSubcommand) + .addSubcommand(deleteItemSubcommand), + run: async (client, interaction) => { + const { options } = interaction; + const compact = options.getBoolean('compact') ?? false; + + const subcommand = options.getSubcommand(); + if (subcommand === CommandUsageConstants.LEADERBOARD_SUBCOMMAND_NAME) { + const allStats = await commandStatisticsTTLCache.getWithFetch(COMMAND_STATISTICS_ROOT_ID); + if (!allStats || !allStats[0]) { + CommandUsageCommand.reply(interaction, 'No command usage statistics were found'); + return; + } + + const stats = allStats.sort((a, b) => b.usages.length - a.usages.length); + const allEmbeds = compact + ? ArrayUtils.chunk(stats, compactEntriesPerPage) + .map((stat, ind) => compactEmbedFromUsageStatistics(client, stat, ind * compactEntriesPerPage)) + : stats.map((stat) => embedFromUsageStatistics(client, stat)); + + InteractionUtils.paginator( + 'command-usage-leaderboard', + client, + allEmbeds.map((e) => ({ embeds: Array.isArray(e) ? e : [e] })), + interaction, + ); + } + + else if (subcommand === CommandUsageConstants.ITEM_SUBCOMMAND_NAME) { + const value = await CommandStatisticOption.getValue(interaction, true); + if (isAutoCompleteResponseType(value)) return; + const embed = embedFromUsageStatistics(client, value); + CommandUsageCommand.reply(interaction, embed); + } + + else if (subcommand === CommandUsageConstants.DELETE_ITEM_SUBCOMMAND_NAME) { + const value = await CommandStatisticOption.getValue(interaction, true); + if (isAutoCompleteResponseType(value)) return; + await prisma.commandStatistics.delete({ where: { id: value.id } }); + commandStatisticsTTLCache.delete(COMMAND_STATISTICS_ROOT_ID); + CommandUsageCommand.reply( + interaction, + `Successfully deleted usage statistics for \`${value.commandId}\` (${stringCommandTypeFromInteger(value.type)})` + ); + } + }, +}); + +export default CommandUsageCommand; diff --git a/src/chat-input/command-usage/options.ts b/src/chat-input/command-usage/options.ts new file mode 100644 index 0000000..5f84189 --- /dev/null +++ b/src/chat-input/command-usage/options.ts @@ -0,0 +1,23 @@ +import { SlashCommandSubcommandBuilder } from 'discord.js'; +import { CommandUsageConstants } from './enums'; +import CommandStatisticOption from '@/auto-completes/command-statistic'; + +export const leaderboardSubcommand = new SlashCommandSubcommandBuilder() + .setName(CommandUsageConstants.LEADERBOARD_SUBCOMMAND_NAME) + .setDescription('View the leaderboard for most used commands & components') + .addBooleanOption((option) => + option + .setName('compact') + .setDescription('Wether or not to display the leaderboard in a compact format, default false') + .setRequired(false) + ); + +export const itemSubcommand = new SlashCommandSubcommandBuilder() + .setName(CommandUsageConstants.ITEM_SUBCOMMAND_NAME) + .setDescription('View detailed usage information for a specific command/component') + .addStringOption(CommandStatisticOption.addOptionHandler); + +export const deleteItemSubcommand = new SlashCommandSubcommandBuilder() + .setName(CommandUsageConstants.DELETE_ITEM_SUBCOMMAND_NAME) + .setDescription('Delete a specific command/component from the database') + .addStringOption(CommandStatisticOption.addOptionHandler); diff --git a/src/chat-input/developer/change-avatar.ts b/src/chat-input/developer/change-avatar.ts new file mode 100644 index 0000000..0c07c9e --- /dev/null +++ b/src/chat-input/developer/change-avatar.ts @@ -0,0 +1,47 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommand, CommandCooldownType, PermLevel, UnitConstants } from '@rhidium/core'; + +const ChangeAvatarCommand = new ChatInputCommand({ + permLevel: PermLevel['Bot Administrator'], + cooldown: { // Global Rate Limit of 5 per hour + type: CommandCooldownType.Global, + usages: 5, + duration: UnitConstants.MS_IN_ONE_HOUR, + persistent: true, + }, + data: new SlashCommandBuilder() + .setDescription('Change the bot\'s avatar') + .addAttachmentOption((option) => + option.setName('avatar') + .setDescription('The avatar to set') + .setRequired(true) + ), + run: async (client, interaction) => { + const {options} = interaction; + const avatarAttachment = options.getAttachment('avatar', true); + + if (!avatarAttachment.contentType?.startsWith('/image')) { + ChangeAvatarCommand.reply(interaction, client.embeds.error( + 'The provided attachment is not an image', + )); + return; + } + + try { + await client.user.setAvatar(avatarAttachment.url); + } + catch (err) { + ChangeAvatarCommand.reply(interaction, client.embeds.error( + `Failed to set avatar: ${err instanceof Error ? err.message : err}`, + )); + return; + } + + ChangeAvatarCommand.reply(interaction, client.embeds.success({ + description: 'Successfully set avatar', + thumbnail: { url: avatarAttachment.url }, + })); + }, +}); + +export default ChangeAvatarCommand; diff --git a/src/chat-input/developer/change-name.ts b/src/chat-input/developer/change-name.ts new file mode 100644 index 0000000..725e38c --- /dev/null +++ b/src/chat-input/developer/change-name.ts @@ -0,0 +1,37 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommand, CommandCooldownType, PermLevel, UnitConstants } from '@rhidium/core'; + +const ChangeNameCommand = new ChatInputCommand({ + permLevel: PermLevel['Bot Administrator'], + cooldown: { // Global Rate Limit of 2 per hour + type: CommandCooldownType.Global, + usages: 2, + duration: UnitConstants.MS_IN_ONE_HOUR, + persistent: true, + }, + data: new SlashCommandBuilder() + .setDescription('Change the bot\'s username') + .addStringOption((option) => + option.setName('name') + .setDescription('The new name to set') + .setRequired(true) + ), + run: async (client, interaction) => { + const {options} = interaction; + const newName = options.getString('name', true); + + try { + await client.user.setUsername(newName); + } + catch (err) { + ChangeNameCommand.reply(interaction, client.embeds.error( + `Failed to set new name: ${err instanceof Error ? err.message : err}`, + )); + return; + } + + ChangeNameCommand.reply(interaction, client.embeds.success(`Successfully set name to **\`${newName}\`**`)); + }, +}); + +export default ChangeNameCommand; diff --git a/src/chat-input/developer/change-presence.ts b/src/chat-input/developer/change-presence.ts new file mode 100644 index 0000000..339e292 --- /dev/null +++ b/src/chat-input/developer/change-presence.ts @@ -0,0 +1,95 @@ +import { ChatInputCommand, CommandCooldownType, PermLevel, UnitConstants } from '@rhidium/core'; +import { ActivitiesOptions, ActivityType, PresenceData, SlashCommandBuilder } from 'discord.js'; + +enum PresenceStatusData { + Online = 'online', + Idle = 'idle', + Dnd = 'dnd', + Invisible = 'invisible' +} + +const ChangeStatusCommand = new ChatInputCommand({ + cooldown: { + duration: UnitConstants.MS_IN_ONE_HOUR, + usages: 2, + type: CommandCooldownType.Global, + persistent: true, + }, + permLevel: PermLevel['Bot Administrator'], + data: new SlashCommandBuilder() + .setDescription('Change the bot\'s status') + .addStringOption((option) => + option.setName('activity') + .setDescription('The activity text/name to set') + .setRequired(true) + .setMinLength(1) + ) + .addStringOption((option) => + option.setName('activity-type') + .setDescription('The activity-type to use') + .setRequired(false) + .setChoices( + ...Object.entries(ActivityType) + .filter((e) => typeof e[0] === 'string') + .map(([key, value]) => ({ name: key, value: `${value}` })) + ) + ) + .addStringOption((option) => + option.setName('activity-url') + .setDescription('The activity URL to set, only used when type is `STREAMING`, supports Twitch and Youtube') + .setRequired(false) + ) + .addStringOption((option) => + option.setName('activity-state') + .setDescription('The activity state to use') + .setRequired(false) + ) + .addStringOption((option) => + option.setName('status') + .setDescription('The status to set') + .setRequired(false) + .setChoices( + ...Object.entries(PresenceStatusData).map(([key, value]) => ({ name: key, value })) + ) + ), + run: async (client, interaction) => { + const { options } = interaction; + const activity = options.getString('activity', true); + const activityType = options.getString('activity-type'); + const activityUrl = options.getString('activity-url'); + const activityState = options.getString('activity-state'); + const status = options.getString('status'); + const resolvedStatus = PresenceStatusData[status as keyof typeof PresenceStatusData]; + const resolvedActivityType = activityType + ? ActivityType[activityType as keyof typeof ActivityType] + : ActivityType.Playing; + + await ChangeStatusCommand.deferReplyInternal(interaction); + + const activityOptions: ActivitiesOptions = { + name: activity, + type: resolvedActivityType, + }; + if (activityState) activityOptions.state = activityState; + if (activityUrl && resolvedActivityType === ActivityType.Streaming) { + activityOptions.url = activityUrl; + } + + const presenceOptions: PresenceData = { + status: resolvedStatus, + afk: false, + activities: [activityOptions], + }; + if (client.cluster) { + await client.cluster.broadcastEval((c, { presenceOptions }) => { + presenceOptions.shardId = c.cluster ? [...c.cluster.ids.keys()] : 0; + c.user.presence.set(presenceOptions); + }, { context: { presenceOptions } }); + } + else client.user.presence.set(presenceOptions); + + ChangeStatusCommand.reply(interaction, client.embeds.success(`Activity changed to **\`${activity}\`**`)); + }, +}); + +export default ChangeStatusCommand; diff --git a/src/chat-input/developer/deploy.ts b/src/chat-input/developer/deploy.ts new file mode 100644 index 0000000..f832d60 --- /dev/null +++ b/src/chat-input/developer/deploy.ts @@ -0,0 +1,74 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommand, PermLevel } from '@rhidium/core'; + +const DeployCommand = new ChatInputCommand({ + permLevel: PermLevel['Bot Administrator'], + data: new SlashCommandBuilder() + .addStringOption((option) => + option + .setName('scope') + .setDescription('The scope to deploy to') + .setRequired(true) + .addChoices( + { name: 'Global', value: 'global' }, + { name: 'Server', value: 'guild' }, + ), + ) + .addStringOption((option) => + option + .setName('server') + .setDescription( + 'The server to deploy to, only used if scope is "Server"', + ) + .setRequired(false), + ), + run: async (client, interaction) => { + const { options } = interaction; + const scope = options.getString('scope', true); + const guildId = options.getString('server', false); + + await DeployCommand.reply(interaction, { + embeds: [ + client.embeds.waiting( + `Deploying commands to ${ + scope === 'global' ? 'global' : `guild ${guildId}` + }`, + ), + ], + }); + + let res; + if (scope === 'global') { + // If global, deploy global commands right away + res = await client.commandManager.deployCommands({ type: 'global' }); + } else { + // Make sure a guild ID was provided + if (!guildId) { + DeployCommand.reply(interaction, { + embeds: [ + client.embeds.error( + 'No guild/server ID was provided - this command has been cancelled', + ), + ], + }); + return; + } + + // Deploy development commands to specific (overwrite) guild/server + else + res = await client.commandManager.deployCommands({ + type: 'development', + guildId, + }); + } + + // User feedback + DeployCommand.reply(interaction, { + content: `Deployed a total of ${res.commands.length} commands ${ + scope === 'global' ? 'globally' : `to guild ${guildId}` + }`, + }); + }, +}); + +export default DeployCommand; diff --git a/src/chat-input/developer/eval.ts b/src/chat-input/developer/eval.ts new file mode 100644 index 0000000..9e09775 --- /dev/null +++ b/src/chat-input/developer/eval.ts @@ -0,0 +1,26 @@ +import { ActionRowBuilder, ModalBuilder, SlashCommandBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; +import { ChatInputCommand, PermLevel } from '@rhidium/core'; +import EvalConstants from '../../enums/eval'; + +const EvalCommand = new ChatInputCommand({ + permLevel: PermLevel['Bot Administrator'], + data: new SlashCommandBuilder() + .setDescription('Evaluate arbitrary JavaScript code'), + run: async (_client, interaction) => { + interaction.showModal(codeModal); + }, +}); + +export const codeModal = new ModalBuilder() + .setCustomId(EvalConstants.CODE_MODAL_ID) + .setTitle('Evaluate JavaScript code') + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId(EvalConstants.CODE_MODAL_INPUT_ID) + .setLabel('The JavaScript code to evaluate') + .setStyle(TextInputStyle.Paragraph) + ) + ); + +export default EvalCommand; diff --git a/src/chat-input/developer/exec.ts b/src/chat-input/developer/exec.ts new file mode 100644 index 0000000..e913a36 --- /dev/null +++ b/src/chat-input/developer/exec.ts @@ -0,0 +1,83 @@ +import { exec } from 'child_process'; +import { BaseMessageOptions, SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommand, EmbedConstants, PermLevel, TimeUtils, UnitConstants } from '@rhidium/core'; + +const ExecCommand = new ChatInputCommand({ + permLevel: PermLevel['Bot Administrator'], + data: new SlashCommandBuilder() + .setDescription('Execute a console/terminal command, rejects after 15 minutes') + .addStringOption((option) => + option + .setName('command') + .setDescription('The command to execute') + .setRequired(true) + .setMinLength(1) + ), + run: async (client, interaction) => { + const {options} = interaction; + const command = options.getString('command', true); + const execStartTime = process.hrtime.bigint(); + + await ExecCommand.deferReplyInternal(interaction); + + let output: string; + try { + output = await new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error('Command execution timed out')); + }, 15 * UnitConstants.MS_IN_ONE_SECOND); + exec(command, (error, stdout, stderr) => { + if (error || stderr) reject(error || new Error(stderr)); + else resolve(stdout); + }); + }); + } + catch (err) { + const error = err instanceof Error ? err : new Error(`${err}`); + const execTime = TimeUtils.runTime(execStartTime); + ExecCommand.reply(interaction, client.embeds.error({ + title: 'Command execution failed', + description: `Error:\n\`\`\`${error.message}\`\`\``, + fields: [{ + name: 'Time Taken', + value: `\`\`\`fix\n${execTime}\`\`\``, + }], + })); + return; + } + + const execTime = TimeUtils.runTime(execStartTime); + const stdout = output; + const contentTooLong = stdout.length > (EmbedConstants.FIELD_VALUE_MAX_LENGTH - 6); + const ctx: BaseMessageOptions = { + embeds: [ + client.embeds.success({ + title: 'Command execution successful', + fields: [{ + name: ':inbox_tray: Command', + value: `\`\`\`${command}\`\`\``, + inline: false, + }, { + name: ':outbox_tray: Output', + value: `\`\`\`${contentTooLong ? 'See File Attachment' : stdout}\`\`\``, + inline: false, + }, { + name: 'Time Taken', + value: `\`\`\`fix\n${execTime}\`\`\``, + }], + }), + ], + files: [], + }; + if (stdout.length > EmbedConstants.FIELD_VALUE_MAX_LENGTH) { + ctx.files = [{ + name: 'stdout.txt', + attachment: Buffer.from(stdout), + }]; + } + + ExecCommand.reply(interaction, ctx); + }, +}); + +export default ExecCommand; diff --git a/src/chat-input/developer/reload.ts b/src/chat-input/developer/reload.ts new file mode 100644 index 0000000..24a769c --- /dev/null +++ b/src/chat-input/developer/reload.ts @@ -0,0 +1,63 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommand, PermLevel, isAutoCompleteResponseType, isCommand } from '@rhidium/core'; +import CommandOption from '../../auto-completes/command'; + +/** + * Reload doesn't work very well for TS in general (since you have + * to re-build and load a command from an updated code-base), but I know + * people will ask for it so here it is. + * + * Only relevant in production mode. + */ + +const ReloadCommand = new ChatInputCommand({ + disabled: process.env.NODE_ENV !== 'production', + permLevel: PermLevel['Bot Administrator'], + data: new SlashCommandBuilder() + .setDescription('Reloads a command') + .addStringOption(CommandOption.addOptionHandler), + run: async (client, interaction) => { + await interaction.deferReply({ ephemeral: ReloadCommand.isEphemeral }); + + const command = await CommandOption.getValue(interaction, true); + if (isAutoCompleteResponseType(command)) return; + const commandCollection = client.commandManager.chatInput; + + try { + const filePath = command.sourceFile; + command.unInitialize(); + commandCollection.delete(command.data.name); + delete require.cache[require.resolve(filePath)]; + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const newCommand = require(filePath); + if (!isCommand(newCommand.default)) { + await ReloadCommand.reply(interaction, client.embeds.error( + `Command at path **\`${filePath}\`** is not a valid command`, + )); + return; + } + + command.load(filePath, client, client.commandManager); + if (!command.initialized) { + await ReloadCommand.reply(interaction, client.embeds.error( + `Command at path **\`${filePath}\`** failed to initialize`, + )); + return; + } + } + catch (err) { + client.logger.error(`Error encountered while reloading command ${command.data.name}:`, err); + await ReloadCommand.reply(interaction, client.embeds.error( + `Command at path **\`${command.sourceFile}\`** failed to reload`, + )); + return; + } + + await ReloadCommand.reply(interaction, client.embeds.success( + `Command **\`${command.data.name}\`** was reloaded`, + )); + }, +}); + +export default ReloadCommand; diff --git a/src/chat-input/system/commands.ts b/src/chat-input/system/commands.ts new file mode 100644 index 0000000..057c415 --- /dev/null +++ b/src/chat-input/system/commands.ts @@ -0,0 +1,58 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommand, CommandType, PermissionUtils } from '@rhidium/core'; +import CommandOption, { CommandAutoCompleteQueryType } from '../../auto-completes/command'; + +const CommandsHelpCommand = new ChatInputCommand({ + isEphemeral: true, + data: new SlashCommandBuilder().setDescription( + 'Receive detailed information and usage help for commands', + ).addStringOption(CommandOption.addOptionHandler), + run: async (client, interaction) => { + // Declarations + const { member, guild } = interaction; + const [ group, name ] = CommandOption.getRawValue(interaction).split('@'); + const isCategoryQuery = group === CommandAutoCompleteQueryType.CATEGORY; + const queryOutput = typeof name === 'undefined' ? (group ?? '""') : `${name} (${group})`; + + let cmd: CommandType | null = null; + if (group === CommandAutoCompleteQueryType.CATEGORY) { + cmd = client.commandManager.apiCommands.find((c) => c.category === name) ?? null; + } + else if (group === CommandAutoCompleteQueryType.SLASH) { + cmd = client.commandManager.chatInput.find((c) => c.data.name === name) ?? null; + } + else if (group === CommandAutoCompleteQueryType.USER_CONTEXT) { + cmd = client.commandManager.userContextMenus.find((c) => c.data.name === name) ?? null; + } + else if (group === CommandAutoCompleteQueryType.MESSAGE_CONTEXT) { + cmd = client.commandManager.messageContextMenus.find((c) => c.data.name === name) ?? null; + } + + // Make sure an option was selected + if (!cmd) { + CommandsHelpCommand.reply(interaction, client.embeds.error( + `No command found for query: **\`${queryOutput}\`**`, + )); + return; + } + + // Only use commands usable by the member + const memberPermLevel = await PermissionUtils.resolveMemberPermLevel(client, member, guild); + const commands = client.commandManager.apiCommands + .filter((c) => client.commandManager.isAppropriateCommandFilter(c, member, memberPermLevel)); + + // Overview of Category + if (isCategoryQuery) { + const categoryCommands = commands.filter((c) => c.category === name); + const embed = await client.commandManager.categoryEmbed(name ?? '', categoryCommands); + await CommandsHelpCommand.reply(interaction, embed); + return; + } + + // Overview of selected command + const embed = client.commandManager.commandEmbed(cmd); + await CommandsHelpCommand.reply(interaction, embed); + }, +}); + +export default CommandsHelpCommand; diff --git a/src/chat-input/system/help.ts b/src/chat-input/system/help.ts new file mode 100644 index 0000000..d0c6ec9 --- /dev/null +++ b/src/chat-input/system/help.ts @@ -0,0 +1,76 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + SlashCommandBuilder, +} from 'discord.js'; +import { ChatInputCommand, Client, StringUtils } from '@rhidium/core'; +import { appConfig } from '@/config'; + +export const helpDescription = async (client: Client) => { + const commandLink = client.commandManager.commandLink; + return [ + '## Introduction\n', + 'I am a bot designed to help you get started on your projects quickly, ', + 'without having to worry about any project bootstrapping.\n', + '### Features\n', + 'I have a lot of built-in features by default, here are some of them:\n', + '1. **Auto-roles** - Automatically assign roles to new members\n', + '2. **Welcome/Goodbye messages** - Send messages when a member joins or leaves\n', + '3. **Moderation** - Kick, ban, mute, and more\n', + '4. **Custom commands** - Create custom commands for your server\n', + '5. **Customizable** - Customize me to your liking\n', + '6. **Auto-completion** - Auto-complete commands and arguments\n', + '7. **Slash commands** - Use slash commands to interact with me\n', + '8. **Permissions** - Use permissions to control who can do what\n', + '9. **Logging** - Log moderation actions and more\n', + '10. **And more!** - There are a lot more features, too many to list here\n', + '### Getting Started\n', + `1. Configure your Administrator role and logging-channel using ${await commandLink('admin')}\n`, + `2. Configure your Moderation resources using ${await commandLink('moderator')}\n`, + `3. Set-up some auto-roles with ${await commandLink('auto-roles')}\n`, + `4. Configure your welcome messages using ${await commandLink('welcome')}\n`, + `5. Configure your goodbye messages using ${await commandLink('goodbye')}\n`, + '\n', + `**Note**: You can use ${await commandLink('commands')} to learn `, + 'more about a command and what it does.\n', + ].join(''); +}; + +const HelpCommand = new ChatInputCommand({ + isEphemeral: true, + data: new SlashCommandBuilder() + .setDescription('Display general information about the bot, and how to use it'), + run: async (client, interaction) => { + // Defer our reply internally, uses cmd#deferReply + await HelpCommand.deferReplyInternal(interaction); + + // Create overview embed + const embed = client.embeds.branding({ + description: await helpDescription(client), + footer: { + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }, + }); + + // Resolve our link components + const components = []; + const linksRow = new ActionRowBuilder().addComponents( + ...Object.entries(appConfig.urls ?? {}).map(([key, value]) => new ButtonBuilder() + .setStyle(ButtonStyle.Link) + .setLabel(StringUtils.titleCase(key.replaceAll('_', ' '))) + .setURL(value) + ) + ); + + if (linksRow.components.length >= 1) components.push(linksRow); + + await HelpCommand.reply(interaction, { + embeds: [embed], + components, + }); + }, +}); + +export default HelpCommand; diff --git a/src/chat-input/system/invite.ts b/src/chat-input/system/invite.ts new file mode 100644 index 0000000..a34d720 --- /dev/null +++ b/src/chat-input/system/invite.ts @@ -0,0 +1,25 @@ +import { OAuth2Scopes, SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommand, CommandCooldownType, PermissionUtils, UnitConstants } from '@rhidium/core'; + +const InviteCommand = new ChatInputCommand({ + data: new SlashCommandBuilder() + .setDescription('Invite the bot to your server'), + cooldown: { + type: CommandCooldownType.Channel, + usages: 1, + duration: 30 * UnitConstants.MS_IN_ONE_SECOND, + }, + run: async (client, interaction) => { + const commands = client.commandManager.apiCommands; + const allUniqueClientPermissions = PermissionUtils.uniqueCommandPermissions(commands.toJSON()); + const inviteLink = client.generateInvite({ + scopes: [ OAuth2Scopes.ApplicationsCommands, OAuth2Scopes.Bot ], + permissions: allUniqueClientPermissions, + }); + await InviteCommand.reply(interaction, client.embeds.branding({ + description: `You can invite me to your server using [this link](${inviteLink})`, + })); + }, +}); + +export default InviteCommand; diff --git a/src/chat-input/system/perm-level.ts b/src/chat-input/system/perm-level.ts new file mode 100644 index 0000000..7a6dc5a --- /dev/null +++ b/src/chat-input/system/perm-level.ts @@ -0,0 +1,37 @@ +import { SlashCommandBuilder, SlashCommandUserOption } from 'discord.js'; +import { ChatInputCommand, PermissionUtils, resolvePermLevel } from '@rhidium/core'; + +const PermLevelCommand = new ChatInputCommand({ + isEphemeral: true, + // aliases: ['pl'], + data: new SlashCommandBuilder() + .setDescription('Display your internal permission level') + .addUserOption( + new SlashCommandUserOption() + .setName('user') + .setDescription('The user to check the permission level of') + .setRequired(false), + ), + requiredResourceIds: { + guilds: [ 'something' ], + }, + run: async (client, interaction) => { + const { guild, options } = interaction; + const targetUser = options.getUser('user', false); + const member = targetUser + ? await guild?.members.fetch(targetUser.id).catch(() => null) ?? null + : interaction.member; + const memberPermLevel = await PermissionUtils.resolveMemberPermLevel( + client, + member, + guild, + ); + const memberPermLevelName = resolvePermLevel(memberPermLevel); + const prefix = targetUser ? `${targetUser}'s` : 'Your'; + PermLevelCommand.reply(interaction, { + content: `${prefix} internal permission level is: ${memberPermLevel} - ${memberPermLevelName}`, + }); + }, +}); + +export default PermLevelCommand; diff --git a/src/chat-input/system/stats.ts b/src/chat-input/system/stats.ts new file mode 100644 index 0000000..ebc07cf --- /dev/null +++ b/src/chat-input/system/stats.ts @@ -0,0 +1,148 @@ +import { ChatInputCommand, TimeUtils, UnitConstants } from '@rhidium/core'; +import { stripIndents } from 'common-tags'; +import { getInfo } from 'discord-hybrid-sharding'; +import { SlashCommandBuilder, version } from 'discord.js'; + +const discordVersion = version.indexOf('dev') < 0 ? version : version.slice(0, version.indexOf('dev') + 3); +const discordVersionDocLink = 'https://discord.js.org/#/docs/discord.js/main/general/welcome'; +const nodeVersionDocLink = `https://nodejs.org/docs/latest-${ process.version.split('.')[0] }.x/api/#`; + +const StatsCommand = new ChatInputCommand({ + // aliases: ['ping'], + data: new SlashCommandBuilder() + .setDescription('Display detailed bot statistics'), + run: async (client, interaction) => { + // Latency + const reply = await interaction.reply({ + content: 'Pinging...', + fetchReply: true, + }); + const fullCircleLatency = reply.createdTimestamp - interaction.createdTimestamp; + const latencyEmoji = (ms: number) => { + let emoji; + if (ms < 150) emoji = 'šŸŸ¢'; + else if (ms < 250) emoji = 'šŸŸ”'; + else emoji = 'šŸ”“'; + return emoji; + }; + + // Counts + const guildCount = client.guilds.cache.size; + const memberCount = client.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0); + const channelCount = client.channels.cache.size; + const roleCount = client.guilds.cache.reduce((acc, guild) => acc + guild.roles.cache.size, 0); + + // Memory + const memoryUsage = process.memoryUsage(); + const memoryUsedInMB = memoryUsage.heapUsed / UnitConstants.BYTES_IN_KIB / UnitConstants.BYTES_IN_KIB; + const memoryAvailableInMB = memoryUsage.heapTotal / UnitConstants.BYTES_IN_KIB / UnitConstants.BYTES_IN_KIB; + const objCacheSizeInMB = memoryUsage.external / UnitConstants.BYTES_IN_KIB / UnitConstants.BYTES_IN_KIB; + + // Create our embed + const embed = client.embeds.branding({ + title: 'Statistics', + fields: [{ + name: 'Latency', + value: stripIndents` + ${latencyEmoji(Math.round(client.ws.ping))} **API Latency:** ${Math.round(client.ws.ping)}ms + ${latencyEmoji(fullCircleLatency)} **Full Circle Latency:** ${Math.round(fullCircleLatency)}ms + `, + inline: true, + }, { + name: 'Memory', + value: stripIndents` + šŸ’¾ **Memory Usage:** ${ memoryUsedInMB.toFixed(2) }/${ memoryAvailableInMB.toFixed(2) } MB + ā™»ļø **Cache Size:** ${ objCacheSizeInMB.toFixed(2) } MB + `, + inline: true, + }, { + name: 'Uptime', + value: `šŸ• ${TimeUtils.msToHumanReadableTime(client.uptime ?? 0)}`, + inline: false, + }, { + name: 'Counts', + value: [ + `šŸ‘Ŗ **Servers:** ${guildCount.toLocaleString()}`, + `šŸ™‹ **Members:** ${memberCount.toLocaleString()}`, + `#ļøāƒ£ **Channels:** ${channelCount.toLocaleString()}`, + `šŸŖŖ **Roles:** ${roleCount.toLocaleString()}`, + ].join('\n'), + inline: true, + }, { + name: 'System', + value: stripIndents` + āš™ļø **Discord.js Version:** [v${ discordVersion }](${ discordVersionDocLink }) + āš™ļø **Node Version:** [${ process.version }](${ nodeVersionDocLink }) + `, + inline: true, + }], + }); + + // Let's not forget about our clustering/sharding information + if (client.cluster) { + let shardsOutput; + try { + const shardStatuses = await client.cluster.broadcastEval((ctx) => { + return { + shards: ctx.cluster? [...ctx.cluster.ids.keys()].length : 0, + readyAt: ctx.readyAt, + }; + }); + const shardStatusArr = shardStatuses.map(({ shards, readyAt }) => { + if (shards === 0) return 'šŸŸ„'.repeat(shards); + if (!readyAt) return 'šŸŸØ'.repeat(shards); + return 'šŸŸ©'.repeat(shards); + }); + if (shardStatusArr.length < getInfo().TOTAL_SHARDS) { + shardStatusArr.push(...Array(getInfo().TOTAL_SHARDS - shardStatusArr.length).fill('šŸŸ„')); + } + shardsOutput = stripIndents` + ${shardStatusArr.join('')} + + šŸŸ© = Shard is online and responsive + šŸŸØ = Shard spawned, but hasn't logged in + šŸŸ„ = Shard has not been spawned + `; + } catch { + shardsOutput = 'Shards are still being spawned, please try again later'; + } + + const totalMemberCount = client.cluster + ? await client.cluster + .broadcastEval(c => c.guilds.cache.reduce((a, b) => a + b.memberCount ?? 0, 0)) + .then(results => results.reduce((prev, val) => prev + val, 0)) + : client.guilds.cache.reduce((a, b) => a + b.memberCount ?? 0, 0); + const totalGuildCount = client.cluster + ? await client.cluster + .broadcastEval(c => c.guilds.cache.size) + .then(results => results.reduce((prev, val) => prev + val, 0)) + : client.guilds.cache.size; + embed.addFields({ + name: '\u200b', + value: '\u200b', + inline: false, + }, { + name: 'Cluster', + value: stripIndents` + šŸ“” **Shards:** ${getInfo().TOTAL_SHARDS.toLocaleString()} + šŸ“” **Clusters:** ${client.cluster.count.toLocaleString()} + šŸ™‹ **Total Members:** ${totalMemberCount.toLocaleString()} + šŸ‘Ŗ **Total Guilds:** ${totalGuildCount.toLocaleString()} + `, + inline: true, + }, { + name: 'Shard Status', + value: shardsOutput, + inline: true, + }); + } + + // Reply with our embed + await StatsCommand.reply(interaction, { + content: '', + embeds: [embed], + }); + }, +}); + +export default StatsCommand; diff --git a/src/chat-input/system/support.ts b/src/chat-input/system/support.ts new file mode 100644 index 0000000..4cca0cb --- /dev/null +++ b/src/chat-input/system/support.ts @@ -0,0 +1,32 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommand, CommandCooldownType, UnitConstants } from '@rhidium/core'; +import { appConfig } from '@/config'; + +const SupportCommand = new ChatInputCommand({ + data: new SlashCommandBuilder() + .setDescription('Receive support for the bot'), + cooldown: { + type: CommandCooldownType.Channel, + usages: 1, + duration: 30 * UnitConstants.MS_IN_ONE_SECOND, + }, + run: async (client, interaction) => { + if (!appConfig.urls?.support_server) { + await SupportCommand.reply(interaction, client.embeds.error( + 'The support server URL is currently not available, please try again later', + )); + return; + } + + await SupportCommand.reply(interaction, client.embeds.branding({ + description: [ + 'If you need help with the bot, have any questions or feature requests, you can join the ', + `[support server](${ + appConfig.urls.support_server + }) and ask in the appropriate channel.`, + ].join(''), + })); + }, +}); + +export default SupportCommand; diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..f89f154 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,130 @@ +import './shared'; + +import { ClusterClient, getInfo } from 'discord-hybrid-sharding'; +import { GatewayIntentBits } from 'discord.js'; +import path from 'path'; +import Lang from './i18n/i18n'; +import { Client, GlobalMiddlewareOptions, logger, initializeLocalization } from '@rhidium/core'; +import { permConfig } from './permissions'; +import { appConfig } from './config'; +import { locales } from './i18n'; +import pkg from '../package.json'; +import { processUsageStatisticsMiddleware } from './middleware/process-usage-statictics'; +import { persistentCooldownMiddleware } from './middleware/persistent-cooldown'; +import { clientExtensions } from './extensions'; + +/** + * This is our client/bot file - it's not the entry point of our application, + * but it's the entry point of our bot. + * This file is spawned by the cluster manager, and is responsible for + * initializing our client, and logging in to Discord. + * Since we're using discord-hybrid-sharding, this file will be spawned + * multiple times, depending on the number of shards we're using. + * We should perform required initialization here (but also in our cluster). + */ + +export const main = async () => { + // Timestamp log start + logger._info(Lang.t('client:initialize.start')); + const startInitialing = process.hrtime(); + + // Initialize our client - scoped because of clustering + // It looks like a lot boilerplate, but we only have few + // required options and can opt-into a lot of powerful features + const client = new Client({ + suppressVanity: false, + globalMiddleware, + token: appConfig.client.token, + intents: [GatewayIntentBits.Guilds], + extensions: clientExtensions, + applicationId: appConfig.client.id, + errorChannelId: appConfig.client.error_channel_id ?? null, + commandUsageChannelId: appConfig.client.command_usage_channel_id ?? null, + developmentServerId: appConfig.client.development_server_id, + shards: appConfig.cluster.enabled ? getInfo()?.SHARD_LIST ?? 'auto' : 'auto', + shardCount: appConfig.cluster.enabled ? getInfo()?.TOTAL_SHARDS ?? 1 : 1, + internalPermissions: clientPermissions!, + debug: clientDebugging!, + directories: clientDirectories!, + logging: clientLoggingConfig!, + colors: appConfig.colors, + emojis: appConfig.emojis, + I18N: Lang, + locales, + pkg, + }); + + // Let's initialize the library language localization + initializeLocalization(appConfig.debug.localizations); + + // Initialize our cluster if we're using one + if (appConfig.cluster.enabled) client.cluster = new ClusterClient(client); + + // Timestamp log finished initializing + const endInitializing = client.logger.consoleExecutionTime(startInitialing); + client.logger.success( + Lang.t('client:initialize.success', { + commandSize: client.commandManager.commandSize, + duration: endInitializing, + }), + ); + + // Log in to our client + await client.login(appConfig.client.token); +}; + +export const globalMiddleware: GlobalMiddlewareOptions = { + postRunExecution: [ + processUsageStatisticsMiddleware, + ], + preRunThrottle: [ + persistentCooldownMiddleware, + ], +}; + +export const clientLoggingConfig: Client['extendedOptions']['logging'] = { + directory: appConfig.logging?.directory, + maxFiles: appConfig.logging?.max_files, + maxSize: appConfig.logging?.max_size, + datePattern: appConfig.logging?.date_pattern, + zippedArchive: appConfig.logging?.zipped_archive, + combinedLogging: appConfig.logging?.combined_logging, + errorLogging: appConfig.logging?.error_logging, + warnLogging: appConfig.logging?.warn_logging, + infoLogging: appConfig.logging?.info_logging, + httpLogging: appConfig.logging?.http_logging, + verboseLogging: appConfig.logging?.verbose_logging, + debugLogging: appConfig.logging?.debug_logging, + sillyLogging: appConfig.logging?.silly_logging, +}; + +export const clientPermissions: Client['extendedOptions']['internalPermissions'] = { + ownerId: appConfig.permissions.owner_id, + systemAdministrators: appConfig.permissions.system_administrator_ids, + developers: appConfig.permissions.developer_ids, + permConfig: permConfig, +}; + +export const clientDirectories: Client['extendedOptions']['directories'] = { + listeners: [path.resolve(__dirname, './listeners')], + autoCompletes: [path.resolve(__dirname, './auto-completes')], + chatInputs: [path.resolve(__dirname, './chat-input')], + messageContextMenus: [path.resolve(__dirname, './message-context')], + userContextMenus: [path.resolve(__dirname, './user-context')], + componentCommands: [ + path.resolve(__dirname, './buttons'), + path.resolve(__dirname, './modals'), + path.resolve(__dirname, './select-menus'), + ], + jobs: [path.resolve(__dirname, './jobs')], +}; + +export const clientDebugging: Client['extendedOptions']['debug'] = { + enabled: appConfig.debug.debug_mode_enabled, + commandData: appConfig.debug.command_data, +}; + +// If we're initializing a shard, run main +const args = process.argv.slice(2); +const initializeShard = args.some((arg) => arg.startsWith('--INITIALIZE=')); +if (initializeShard) main(); diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..569656b --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,26 @@ +import { existsSync, readFileSync } from 'fs'; +import { UserConfigOptions } from '.'; +import { logger } from '@rhidium/core'; + +const configFileExists = existsSync('./config.json'); + +// [DEV] - Consider cli argument to force config.example.json +// to be used instead of config.json for Docker builds etc. +if (!configFileExists) { + logger._warn([ + 'config.json does not exist, did you forget to create it?', + 'You can use our web-based editor to create a new one', + 'and configure it: `npm run config-editor` - falling back', + 'to config.example.json - this will NOT start your bot!', + ].join(' ')); +} + +const configData = configFileExists + ? readFileSync('./config.json', 'utf8') + : readFileSync('./config.example.json', 'utf8'); +const userConfig = JSON.parse(configData) as UserConfigOptions; + +export default userConfig; + +export * from './internal-config'; +export * from './types'; diff --git a/src/config/internal-config.ts b/src/config/internal-config.ts new file mode 100644 index 0000000..8c8c173 --- /dev/null +++ b/src/config/internal-config.ts @@ -0,0 +1,129 @@ +import { Colors, resolveColor } from 'discord.js'; +import userConfig from '.'; +import { AppConfig, NodeEnvValues, UserConfigOptions } from './types'; +import { validateConfig } from './validate'; +import { resolveColorConfig } from '@rhidium/core'; + +/** + * Resolve our active NODE_ENV, which can be overwritten through cli arguments + * and .env files + * + * @returns {NodeEnvValues} The resolved NODE_ENV value + */ +export const resolveNodeEnvironment = ( + userConfig: UserConfigOptions, +): NodeEnvValues => { + // Resolve CLI arguments + const cliArguments = process.argv.slice(2); + const envCliArg = cliArguments.find((arg) => arg.startsWith('--NODE_ENV=')); + const [, envCliArgValue] = envCliArg?.split('=') ?? []; + + // CLI Argument > .env file value > default(production) + const userValue = envCliArgValue ?? process.env.NODE_ENV; + + // Only allow whitelisted values + const whitelistedValue = + typeof userValue === 'undefined' || userValue === 'production' + ? 'production' + : userValue === 'staging' + ? 'staging' + : 'development'; + + // Sync our CLI argument with our process.env.NODE_ENV + if (!process.env.NODE_ENV && whitelistedValue) + process.env.NODE_ENV = whitelistedValue; + + // Sync our process debugging environmental variable + // This is used when appConfig#debug.debug_mode_enabled is true + // to avoid creating circular imports in essential, re-usable modules + // like the logger + if (!process.env.DEBUG_ENABLED && userConfig.debug?.debug_mode_enabled) + process.env.DEBUG_ENABLED = 'true'; + + return whitelistedValue; +}; + +/** + * Apply sensible defaults to our user config and include internal config + */ +export const resolveAppConfig = (userConfig: UserConfigOptions): AppConfig => { + const appConfig: AppConfig = { + ...userConfig, + // Resolve our NODE_ENV from cli arguments and .env files + // for reference in config files + NODE_ENV: resolveNodeEnvironment(userConfig), + // Set defaults for any optional/missing values + debug: { + debug_mode_enabled: userConfig.debug?.debug_mode_enabled ?? false, + command_throttling: userConfig.debug?.command_throttling ?? false, + localizations: userConfig.debug?.localizations ?? false, + chat_input_command_api_data: + userConfig.debug?.chat_input_command_api_data ?? false, + command_data: userConfig.debug?.command_data ?? false, + interactions: userConfig.debug?.interactions ?? false, + modal_submit_execution_time: + userConfig.debug?.modal_submit_execution_time ?? false, + autocomplete_execution_time: + userConfig.debug?.autocomplete_execution_time ?? false, + }, + emojis: { + success: userConfig.emojis?.success ?? 'ā˜‘ļø', + error: userConfig.emojis?.error ?? 'āŒ', + info: userConfig.emojis?.info ?? 'ā„¹ļø', + warning: userConfig.emojis?.warning ?? 'āš ļø', + debug: userConfig.emojis?.debug ?? 'šŸ›', + waiting: userConfig.emojis?.waiting ?? 'ā³', + separator: userConfig.emojis?.separator ?? 'ā€¢', + }, + colors: { + primary: resolveColorConfig(userConfig.colors?.primary, Colors.Blurple), + secondary: resolveColorConfig( + userConfig.colors?.secondary, + Colors.Greyple, + ), + success: resolveColorConfig(userConfig.colors?.success, Colors.Green), + debug: resolveColorConfig( + userConfig.colors?.debug, + resolveColor('#af86fc'), + ), + error: resolveColorConfig(userConfig.colors?.error, Colors.Red), + warning: resolveColorConfig( + userConfig.colors?.warning, + resolveColor('#ffc61b'), + ), + info: resolveColorConfig( + userConfig.colors?.info, + resolveColor('#77c8d5'), + ), + waiting: resolveColorConfig( + userConfig.colors?.waiting, + resolveColor('#f7b977'), + ), + }, + }; + validateConfig(appConfig); + return appConfig; +}; + +/** + * Singleton that represent our local/runtime configuration - enums + * have to be manually resolved due to JSON schema not supporting them + */ +export const appConfig = resolveAppConfig({ + ...userConfig, + $schema: userConfig.$schema, + api: userConfig.api, + client: userConfig.client, + permissions: userConfig.permissions, + debug: userConfig.debug ?? {}, + emojis: userConfig.emojis ?? {}, + colors: userConfig.colors ?? {}, + cooldown: userConfig.cooldown ?? {}, + cluster: userConfig.cluster ?? {}, +}); + +export const userColors = appConfig.colors; + +export const userEmojis = appConfig.emojis; + +export default appConfig; diff --git a/src/config/types.ts b/src/config/types.ts new file mode 100644 index 0000000..adcbdc4 --- /dev/null +++ b/src/config/types.ts @@ -0,0 +1,231 @@ +import { Snowflake } from 'discord.js'; +import { CommandCooldownType, IColors, IEmojis, UserColors } from '@rhidium/core'; + +/** + * Represents essential options to configure clustering & sharding + * + * Unfortunately we still can't use this to generate + * our json schema as it's not supported by the generator (bigint) + */ +// export type RequiredClusterManagerOptions = Required> & Pick; + +export interface IClusterRestartOptions { + /** Maximum amount of restarts a cluster can have in the interval */ + max: number; + /** Interval in milliseconds on which the current restarts amount of a cluster will be resetted */ + interval: number; +} + +export interface IClusterManagerOptions { + /** Is clustering enabled */ + enabled: boolean; + /** Total amount of shards to spawn, use null for auto */ + total_shards: number | null; + /** Amount of shards per cluster */ + shards_per_clusters: number; + /** Amount of clusters to spawn, use null for auto */ + total_clusters: number | null; + /** Mode to spawn clusters in */ + mode: 'worker' | 'process'; + /** Should the process respawn on exit */ + respawn: boolean; + /** Additional cluster restart options */ + restarts: IClusterRestartOptions; +} + +export interface IClientAPI { + /** Whether the API should be enabled */ + enabled: boolean; + /** + * Which port the server application should run on, between `0` and `65535` (disabled if `null`) + * + * Note: Port range `[0-1024]` should be considered reserved as they're well-known ports, + * these require SuperUser/Administrator access + * + * Note: Port range `[49152-65535]` should be considered reserved for Ephemeral Ports + * (Unix based devices, configurable) + * + * Reference: {@link https://www.ncftp.com/ncftpd/doc/misc/ephemeral_ports.html} + */ + port: number | null; +} + +/** Available runtime environments */ +export type NodeEnvValues = 'production' | 'development' | 'staging'; + +export interface UserConfigOptions extends OptionalUserConfig { + /** JSON schema used for validation and type-ahead/Intellisense, you should never change this */ + readonly $schema: string; + api: IClientAPI; + /** Client internal permission level configuration */ + permissions: IPermissions; + /** Discord client/bot configuration for this instance */ + client: IClient; + /** Default command throttle configuration */ + cooldown: ICommandCooldown; + /** + * Change the entry file of this application to + * further fine-tune sharding and clustering settings + */ + cluster: IClusterManagerOptions; +} + + +export type FullUserConfigOptions = UserConfigOptions & + Required<{ + debug: Required; + colors: Required; + emojis: Required; + }>; + +export interface OptionalUserConfig { + /** Debug configuration for this instance, useful for developers */ + debug?: Partial; + /** Color customization for embeds, and other user feedback/interactions */ + colors?: Partial; + /** Emoji customization for embeds, and other user feedback/interactions */ + emojis?: Partial; + /** URL configuration for the client */ + urls?: Partial; + /** File logging (winston) configuration */ + logging?: Partial; + // Note: We need the relative path to the client/commands directory + // between ts-node-dev and tsc, so we need path.relative for + // directory structure - can't be provided in user json file + // Instead, use in client options only + /** Configure the project/application structure */ + // PROJECT_STRUCTURE?: IProjectStructure; +} + +export interface LoggingOptions { + /** Whats the max size of a file before it should rotate */ + max_size?: string; + /** Whats the max amount of files to keep before deleting the oldest */ + max_files?: string; + /** Should rotated log files be archived, instead of deleted? */ + zipped_archive?: boolean; + /** What directory should log files be stored in? */ + directory?: string; + /** What date pattern should be used for log file names */ + date_pattern?: string; + /** Should there be a combined log file that holds everything? */ + combined_logging?: boolean; + /** Should there be an error log file? */ + error_logging?: boolean; + /** Should there be a warning log file? */ + warn_logging?: boolean; + /** Should there be an info log file? */ + info_logging?: boolean; + /** Should there be an http log file? */ + http_logging?: boolean; + /** Should there be a verbose log file? */ + verbose_logging?: boolean; + /** Should there be a debug log file? */ + debug_logging?: boolean; + /** Should there be a silly log file? */ + silly_logging?: boolean; +} + +export interface IPermissions { + /** The bot owner's Discord user id - represents the highest permission level */ + owner_id: Snowflake; + /** Array of Discord user id's that have the Developer permission level */ + developer_ids: Snowflake[]; + /** + * Array of Discord user id's that should be able to manage + * the bot - like restarting, viewing logs, audits, etc. + */ + system_administrator_ids: Snowflake[]; +} + +export interface IClient { + /** This client's application/user ID. You can get one here: https://discord.com/developers/applications */ + id: Snowflake; + /** The secret token used to log in to this bot account. You can get one here: https://discord.com/developers/applications */ + token: string; + /** The server/guild id where test commands should be registered to - development only */ + development_server_id?: Snowflake; + /** + * Should we refuse, and reply to, interactions that belong to unknown commands? + * You should disable this if you run multiple applications/processes under the + * same bot account used for this client + */ + refuse_unknown_command_interactions?: boolean; + /** + * The id of the channel where internal errors should be logged to + */ + error_channel_id?: Snowflake; + /** + * Should we log command/component usage in a Discord channel (id)? + */ + command_usage_channel_id?: Snowflake; +} + +/** + * Represents a command throttling configuration that + * is unique to our config.json file + */ +export interface ICommandCooldown { + /** Whether command throttling is enabled by default */ + default_cooldown_enabled: boolean; + /** What resource/identifier this cooldown should apply to */ + default_cooldown_type: keyof typeof CommandCooldownType; + /** The amount of time the command can be used before being throttled */ + default_cooldown_usages: number; + /** The duration (in ms) usages last for */ + default_cooldown_duration: number; + /** + * Whether the cooldown is persistent (i.e. does it persist between bot restarts)? + * If true, uses prisma data to store cooldown (slower). If false, + * uses internal memory TTL cache to store cooldown for as long as + * they're relevant (faster, uses more RAM) + */ + default_cooldown_persistent: boolean; +} + +export interface URLOptions { + /** The URL to the project repository */ + github: string; + /** The URL to the project documentation */ + docs: string; + /** The URL to the project website */ + website: string; + /** The URL to the project support server */ + support_server: string; +} + +export interface IDebug { + /** Should we perform general debugging (everything that isn't specified in other options) */ + debug_mode_enabled?: boolean; + /** Should the Discord API Command data be displayed when registering? */ + chat_input_command_api_data?: boolean; + /** Should interaction objects be debugged when received? */ + interactions?: boolean; + /** Should the time it takes to query auto-completes be displayed */ + autocomplete_execution_time?: boolean; + /** + * Should the time it takes to respond to modal submit + * interactions be displayed? Useful because these interaction + * can't be deferred + */ + modal_submit_execution_time?: boolean; + /** Should command usage throttling be debugged? */ + command_throttling?: boolean; + /** Should language localization be debugged? */ + localizations?: boolean; + /** Should a table with a complete overview of commands be displayed on boot? */ + command_data?: boolean; +} + +export type InternalAppConfig = { + /** Which environment are we running in resolved from cli arg .env value */ + NODE_ENV: NodeEnvValues; +}; + +export type AppConfig = Omit & + InternalAppConfig & { + colors: UserColors; + }; diff --git a/src/config/validate.ts b/src/config/validate.ts new file mode 100644 index 0000000..15ef46c --- /dev/null +++ b/src/config/validate.ts @@ -0,0 +1,37 @@ +import { stripIndents } from 'common-tags'; +import { AppConfig } from '.'; +import { PortConstants } from '@/enums/ports'; +import { logger } from '@rhidium/core'; + +/** + * Validates the resolved `appConfig`, warning users when + * necessary, and existing if we can't operate due to bad configuration + * + * [DEV] - Implement other non-type validation here + */ +export const validateConfig = (appConfig: AppConfig) => { + if (appConfig.api.port !== null) { + if ( + typeof appConfig.api.port !== 'number' || + appConfig.api.port < 0 || + appConfig.api.port > PortConstants.VALID_PORT_RANGE_MAX + ) { + logger._warn(stripIndents` + [Configuration] "api#port" is not in range 0-${PortConstants.VALID_PORT_RANGE_MAX} + `); + process.exit(1); + } + + if (appConfig.api.port <= PortConstants.WELL_KNOWN_PORT_RANGE) { + logger._warn(stripIndents` + [Configuration] "api#port" is in the reserved well-known port range of 0-${PortConstants.WELL_KNOWN_PORT_RANGE} + `); + } + + if (appConfig.api.port >= PortConstants.EPHEMERAL_PORT_RANGE_START) { + logger._warn(stripIndents` + [Configuration] "api#port" is in the reserved ephemeral port range of 0-${PortConstants.WELL_KNOWN_PORT_RANGE} + `); + } + } +}; diff --git a/src/database/CommandCooldown.ts b/src/database/CommandCooldown.ts new file mode 100644 index 0000000..f769bc2 --- /dev/null +++ b/src/database/CommandCooldown.ts @@ -0,0 +1,28 @@ +import { AsyncTTLCacheManager, UnitConstants } from '@rhidium/core'; +import { prisma } from '.'; +import { CommandCooldown, Prisma } from '@prisma/client'; + +const cooldownFromDb = async (cooldownId: string) => await prisma.commandCooldown.findUnique({ + where: { cooldownId }, +}); + +export const cooldownTTLCache = new AsyncTTLCacheManager({ + fetchFunction: cooldownFromDb, + ttl: UnitConstants.MS_IN_ONE_DAY, +}); + +export const cooldownFromCache = async (cooldownId: string) => { + return cooldownTTLCache.getWithFetch(cooldownId); +}; + +export const updateCooldown = async ( + cooldown: CommandCooldown, + updateArgs: Omit +) => { + const updatedCooldown = await prisma.commandCooldown.update({ + where: { cooldownId: cooldown.cooldownId }, + ...updateArgs, + }); + cooldownTTLCache.set(cooldown.cooldownId, updatedCooldown); + return updatedCooldown; +}; diff --git a/src/database/CommandStatistics.ts b/src/database/CommandStatistics.ts new file mode 100644 index 0000000..b89e446 --- /dev/null +++ b/src/database/CommandStatistics.ts @@ -0,0 +1,27 @@ +import { ApplicationCommandType } from 'discord.js'; +import { prisma } from '.'; +import { Prisma } from '@prisma/client'; +import { AsyncTTLCacheManager, ComponentCommandType, UnitConstants } from '@rhidium/core'; + +export type CommandStatisticsPayload = Prisma.CommandStatisticsGetPayload>; + +export const COMMAND_STATISTICS_ROOT_ID = 'ignored-param'; + +// Database is only updated once every 30 minutes +// and this cache is only used to get the full collection +export const commandStatisticsTTLCache = new AsyncTTLCacheManager< + CommandStatisticsPayload[] +>({ + fetchFunction: async () => prisma.commandStatistics.findMany(), + capacity: 1, + ttl: UnitConstants.MS_IN_ONE_MINUTE * 30, +}); + +export const dbCommandByName = async ( + name: string, + type: ApplicationCommandType | ComponentCommandType +) => prisma.commandStatistics.upsert({ + where: { commandId: name, type }, + create: { commandId: name, type }, + update: {}, +}); diff --git a/src/database/Guilds.ts b/src/database/Guilds.ts new file mode 100644 index 0000000..f9024d9 --- /dev/null +++ b/src/database/Guilds.ts @@ -0,0 +1,79 @@ +import { Prisma } from '@prisma/client'; +import { prisma } from '.'; +import { AsyncTTLCacheManager, UnitConstants } from '@rhidium/core'; + +export type GuildWithEmbeds = Prisma.GuildGetPayload<{ + include: { + memberJoinEmbed: { + include: { fields: true }, + }, + memberLeaveEmbed: { + include: { fields: true }, + }, + }, +}>; + +// Note: Unfortunately, we can't use a guildIncludes constant here +// to avoid code repetition, not 100% sure why + +export const guildSettingsFromDb = async (guildId: string): Promise => { + const guild = await prisma.guild.findUnique({ + where: { guildId: guildId }, + include: { + memberJoinEmbed: { + include: { fields: true }, + }, + memberLeaveEmbed: { + include: { fields: true }, + }, + }, + }); + return ( + guild ?? + (await prisma.guild.create({ + data: { guildId }, + include: { + memberJoinEmbed: { + include: { fields: true }, + }, + memberLeaveEmbed: { + include: { fields: true }, + }, + }, + })) + ); +}; + +export const guildTTLCache = new AsyncTTLCacheManager({ + fetchFunction: guildSettingsFromDb, + capacity: 500, + ttl: UnitConstants.MS_IN_ONE_DAY, +}); + +export const guildSettingsFromCache = async (guildId: string) => { + return guildTTLCache.getWithFetch(guildId); +}; + +/** Convenience method */ +export const updateGuildSettings = async ( + guildSettings: GuildWithEmbeds, + updateArgs: Omit, +): Promise => { + const updatedGuild = await prisma.guild.update({ + ...updateArgs, + where: { + guildId: guildSettings.guildId, + id: guildSettings.id, + }, + include: { + memberJoinEmbed: { + include: { fields: true }, + }, + memberLeaveEmbed: { + include: { fields: true }, + }, + }, + }); + guildTTLCache.set(guildSettings.guildId, updatedGuild); + return updatedGuild; +}; diff --git a/src/database/index.ts b/src/database/index.ts new file mode 100644 index 0000000..2c2b551 --- /dev/null +++ b/src/database/index.ts @@ -0,0 +1,7 @@ +import { PrismaClient } from '@prisma/client'; + +/** Singleton prisma client */ +export const prisma = new PrismaClient(); + +export * from './CommandCooldown'; +export * from './Guilds'; diff --git a/src/enums/eval.ts b/src/enums/eval.ts new file mode 100644 index 0000000..d83126d --- /dev/null +++ b/src/enums/eval.ts @@ -0,0 +1,8 @@ +enum EvalConstants { + CODE_MODAL_ID = 'code-modal', + CODE_MODAL_INPUT_ID = 'code-modal-input', + ACCEPT_CODE_EVALUATION = 'accept-code-evaluation', + CANCEL_CODE_EVALUATION = 'cancel-code-evaluation', +} + +export default EvalConstants; diff --git a/src/enums/ports.ts b/src/enums/ports.ts new file mode 100644 index 0000000..5367c48 --- /dev/null +++ b/src/enums/ports.ts @@ -0,0 +1,6 @@ +export class PortConstants { + static readonly WELL_KNOWN_PORT_RANGE = 1024; + static readonly VALID_PORT_RANGE_MAX = 65535; + static readonly EPHEMERAL_PORT_RANGE_START = 49152; + static readonly EPHEMERAL_PORT_RANGE_END = PortConstants.VALID_PORT_RANGE_MAX; +} diff --git a/src/extensions.ts b/src/extensions.ts new file mode 100644 index 0000000..cbd840c --- /dev/null +++ b/src/extensions.ts @@ -0,0 +1,17 @@ +import { Client } from '@rhidium/core'; +import { usageStatisticsQueue } from './jobs/process-usage-statistics'; + +export const clientExtensions = { usageStatisticsQueue }; + +export type ClientWithUsageStatistics = Client & { + extensions: { + usageStatisticsQueue: typeof usageStatisticsQueue; + }; +}; + +export class ClientExtensions { + static readonly extensions = clientExtensions; + static readonly hasUsageStatisticsQueue = (client: Client): client is ClientWithUsageStatistics => { + return !!client.extensions.usageStatisticsQueue; + }; +} diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts new file mode 100644 index 0000000..715a4e2 --- /dev/null +++ b/src/i18n/i18n.ts @@ -0,0 +1,27 @@ +import Lang from 'i18next'; + +import enClient from '../../locales/en/client.json'; +import enCommands from '../../locales/en/commands.json'; +import nlClient from '../../locales/nl/client.json'; +import nlCommands from '../../locales/nl/commands.json'; + +export const defaultNS = 'client'; + +export const init = (debugEnabled = false) => + Lang.init({ + debug: debugEnabled, + fallbackLng: 'en', + defaultNS, + resources: { + en: { + client: enClient, + commands: enCommands, + }, + nl: { + client: nlClient, + commands: nlCommands, + }, + }, + }); + +export default Lang; diff --git a/src/i18n/i18next-resources.d.ts b/src/i18n/i18next-resources.d.ts new file mode 100644 index 0000000..6232597 --- /dev/null +++ b/src/i18n/i18next-resources.d.ts @@ -0,0 +1,25 @@ +interface Resources { + client: { + ready: 'Client logged in as {{username}}'; + initialize: { + start: 'Initializing client...'; + success: 'Finished initializing client in {{duration}}, registered/loaded {{commandSize}} commands'; + }; + clustering: { + config: { + disabled: 'CLUSTERING is disabled/null, cannot launch cluster - use ~/client/index instead'; + invalid: 'Invalid cluster configuration, cannot launch cluster'; + }; + cluster: { + launch: 'Launching cluster {{id}}'; + }; + }; + }; + commands: { + help: { + name: 'help'; + }; + }; +} + +export default Resources; diff --git a/src/i18n/i18next.d.ts b/src/i18n/i18next.d.ts new file mode 100644 index 0000000..987e020 --- /dev/null +++ b/src/i18n/i18next.d.ts @@ -0,0 +1,9 @@ +import { defaultNS } from './i18n'; +import Resources from './i18next-resources'; + +declare module 'i18next' { + interface CustomTypeOptions { + defaultNS: typeof defaultNS; + resources: Resources; + } +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..ddc807b --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,2 @@ +export * from './locales'; +export * from './i18n'; diff --git a/src/i18n/locales.ts b/src/i18n/locales.ts new file mode 100644 index 0000000..dc731bb --- /dev/null +++ b/src/i18n/locales.ts @@ -0,0 +1,3 @@ +import { LocaleString } from 'discord.js'; + +export const locales: LocaleString[] = ['en-GB', 'nl']; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..8ff71df --- /dev/null +++ b/src/index.ts @@ -0,0 +1,65 @@ +import './shared'; + +import { stripIndents } from 'common-tags'; +import { ClusterManager } from 'discord-hybrid-sharding'; +import { existsSync } from 'fs'; +import path from 'path'; +import { main } from './client'; +import Lang from './i18n/i18n'; +import { appConfig } from './config'; + +// Make sure we're not using ts-node-dev when clustering +if (process.env.TS_NODE_DEV === 'true' && appConfig.cluster.enabled) { + // I'm not exactly sure if there's a way to make this work, + // or if the issue is with discord-hybrid-sharding, ts-node-dev, + // or our ts config altogether (and/or package type) + throw new Error([ + 'The package "discord-hybrid-sharding" does not currently work with ts-node-dev,', + 'please create a dedicated build to run cluster.', + ].join(' ')); +} + +// Try to use source-code file if it exists +// Otherwise, use compiled file +const sourcePath = path.resolve(__dirname, './client.ts'); +const botFilePath = existsSync(sourcePath) + ? sourcePath + : path.resolve(__dirname, './client.js'); + +// If clustering is not enabled, +// or we're in development mode, (ts-node-dev doesn't play well with discord-hybrid-sharding) +// just start the bot normally +if (appConfig.cluster.enabled === false) { + main(); +} + +// Otherwise, start the cluster manager +else { + const manager = new ClusterManager(botFilePath, { + ...appConfig.cluster, + token: appConfig.client.token, + totalShards: appConfig.cluster.total_shards ?? 'auto', + shardsPerClusters: appConfig.cluster.shards_per_clusters, + totalClusters: appConfig.cluster.total_clusters ?? 'auto', + mode: appConfig.cluster.mode, + respawn: appConfig.cluster.respawn, + restarts: { + max: appConfig.cluster.restarts.max, + interval: appConfig.cluster.restarts.interval, + }, + shardArgs: [ + // Always overwrite shard node_env for consistency + `--NODE_ENV=${appConfig.NODE_ENV}`, + '--INITIALIZE=true', + ], + }); + + manager.on('clusterCreate', (cluster) => { + console.info(stripIndents`-------------------------------------------------------- + + ${Lang.t('client:clustering.cluster.launch', { id: cluster.id })} + + --------------------------------------------------------`); + }); + manager.spawn({ timeout: -1 }); +} diff --git a/src/jobs/clean-cooldown-data.ts b/src/jobs/clean-cooldown-data.ts new file mode 100644 index 0000000..6d88fa3 --- /dev/null +++ b/src/jobs/clean-cooldown-data.ts @@ -0,0 +1,34 @@ +import { cooldownTTLCache, prisma } from '@/database'; +import { Job } from '@rhidium/core'; + +const CleanCooldownData = new Job({ + id: 'clean-cooldown-data', + schedule: '0 0 * * *', // Every day at midnight + run: async () => { + cooldownTTLCache.clear(); + const data = await prisma.commandCooldown.findMany(); + for (const entry of data) { + const { duration } = entry; + const now = Date.now(); + const initialLength = entry.usages.length; + + entry.usages = entry.usages.filter((usage) => { + const diff = now - usage.valueOf(); + return diff < duration; + }); + + const finalLength = entry.usages.length; + if (finalLength !== initialLength) { + if (finalLength === 0) { + await prisma.commandCooldown.delete({ where: { id: entry.id } }); + } + else await prisma.commandCooldown.update({ + where: { id: entry.id }, + data: { usages: entry.usages }, + }); + } + } + }, +}); + +export default CleanCooldownData; diff --git a/src/jobs/process-usage-statistics.ts b/src/jobs/process-usage-statistics.ts new file mode 100644 index 0000000..d686ea1 --- /dev/null +++ b/src/jobs/process-usage-statistics.ts @@ -0,0 +1,157 @@ +import { prisma } from '@/database'; +import { dbCommandByName } from '@/database/CommandStatistics'; +import { ClientExtensions } from '@/extensions'; +import { Prisma } from '@prisma/client'; +import { + ClientWithCluster, + ClusterUtils, + ComponentCommandType, + Job, + NumberUtils, + QueueCallbackFunction, + QueueManager, + UnitConstants, +} from '@rhidium/core'; +import { ApplicationCommandType } from 'discord.js'; + +export type CommandUsageStatistics = { + commandId: string; + type: ApplicationCommandType | ComponentCommandType; + usages: Date[]; + errorCount: number; + lastUsed: Date | null; + lastError: string | null; + lastErrorAt: Date | null; + runtimeDurations: number[]; +} + +export const processUsageStatistics: QueueCallbackFunction = async (item) => { + const dataToSave: Prisma.CommandStatisticsGetPayload>[] = []; + + for await (const stat of item) { + const data = dataToSave.find((d) => d.commandId === stat.commandId && d.type === stat.type) + ?? await dbCommandByName(stat.commandId, stat.type); + + data.usages = [...data.usages, ...stat.usages]; + data.lastUsedAt = stat.lastUsed; + + const firstUsage = data.usages[0]; + if (!data.firstUsedAt && firstUsage) data.firstUsedAt = firstUsage; + + if (data.errorCount > 0) { + data.errorCount += stat.errorCount; + data.lastError = stat.lastError; + data.lastErrorAt = stat.lastErrorAt; + } + + const totalRuntime = stat.runtimeDurations.reduce((a, b) => a + b, 0); + const lowestRuntime = Math.min(...stat.runtimeDurations); + const highestRuntime = Math.max(...stat.runtimeDurations); + const averageRuntime = stat.runtimeDurations.reduce((a, b) => a + b, 0) / stat.runtimeDurations.length; + + data.runtimeTotal = data.runtimeTotal ? data.runtimeTotal + totalRuntime : totalRuntime; + data.runtimeMax = data.runtimeMax ? Math.max(data.runtimeMax, highestRuntime) : highestRuntime; + data.runtimeMin = data.runtimeMin ? Math.min(data.runtimeMin, lowestRuntime) : lowestRuntime; + data.runtimeMean = data.runtimeMean ? (data.runtimeMean + averageRuntime) / 2 : averageRuntime; + data.runtimeMedian = NumberUtils.calculateMedian([ + data.runtimeMean ?? averageRuntime, + ...stat.runtimeDurations, + ]); + data.runtimeVariance = NumberUtils.calculateVariance([ + data.runtimeMean ?? averageRuntime, + ...stat.runtimeDurations, + ]); + data.runtimeStdDeviation = NumberUtils.calculateStandardDeviation([ + data.runtimeMean ?? averageRuntime, + ...stat.runtimeDurations, + ]); + + if (dataToSave.find((d) => d.commandId === stat.commandId)) continue; + else dataToSave.push(data); + } + + await Promise.all( + dataToSave.map((data) => prisma.commandStatistics.update({ + where: { commandId: data.commandId, type: data.type }, + data: { + errorCount: data.errorCount, + firstUsedAt: data.firstUsedAt, + lastError: data.lastError, + lastErrorAt: data.lastErrorAt, + lastUsedAt: data.lastUsedAt, + runtimeTotal: data.runtimeTotal, + runtimeMax: data.runtimeMax, + runtimeMin: data.runtimeMin, + runtimeMean: data.runtimeMean, + runtimeMedian: data.runtimeMedian, + runtimeVariance: data.runtimeVariance, + runtimeStdDeviation: data.runtimeStdDeviation, + usages: { set: data.usages }, + }, + })), + ); +}; + +export const usageStatisticsQueue = new QueueManager({ + maxQueueSize: 1, + stopOnEmpty: true, + nextDelay: UnitConstants.MS_IN_ONE_MINUTE * 30, + waitOnEmpty: UnitConstants.MS_IN_ONE_MINUTE * 5, + processFunction: processUsageStatistics, +}); + +export const clusterProcessUsageStatistics = async (client: ClientWithCluster) => { + const noQueueMsg = 'No usage statistics queue found to cluster usage statistics tracking'; + if (!ClientExtensions.hasUsageStatisticsQueue(client)) throw new Error(noQueueMsg); + + const cluster = client.cluster; + let combinedStatistics: CommandUsageStatistics[] = []; + const originalProcessFunction = client.extensions.usageStatisticsQueue.processFunction; + if (!originalProcessFunction) throw new Error('No process function found to cluster usage statistics tracking'); + + const results = await cluster.broadcastEval(async (c, { noQueueMsg }) => { + const stats: CommandUsageStatistics[] = []; + if ( + !c.extensions.usageStatisticsQueue + || typeof c.extensions.usageStatisticsQueue !== 'object' + ) throw new Error(noQueueMsg); + // Type cast to avoid type errors, doesn't compile, so we can use out of scope reference + const usageStatsQueue = c.extensions.usageStatisticsQueue as typeof usageStatisticsQueue; + usageStatsQueue.stopOnEmpty = true; + usageStatsQueue.processFunction = (item) => { + stats.push(...item); + }; + await usageStatsQueue.process(); + return stats; + }, { context: { noQueueMsg } }); + + results.forEach((stats) => { + combinedStatistics.push(...stats.map((e) => ({ + commandId: e.commandId, + type: e.type, + usages: e.usages.map((e) => new Date(e)), + errorCount: e.errorCount, + lastUsed: e.lastUsed ? new Date(e.lastUsed) : null, + lastError: e.lastError, + lastErrorAt: e.lastErrorAt ? new Date(e.lastErrorAt) : null, + runtimeDurations: e.runtimeDurations, + }))); + }); + + originalProcessFunction(combinedStatistics); + combinedStatistics = []; +}; + +const ProcessUsageStatisticsJob = new Job({ + id: 'process-usage-statistics', + run: async (client) => { + if (ClusterUtils.hasCluster(client)) { + if (client.cluster.id === 0) await clusterProcessUsageStatistics(client); + else return; // Only process on cluster 0 + } + else usageStatisticsQueue.process(); + }, + interval: usageStatisticsQueue.nextDelay, +}); + +export default ProcessUsageStatisticsJob; diff --git a/src/jobs/update-server-count.ts b/src/jobs/update-server-count.ts new file mode 100644 index 0000000..d596d94 --- /dev/null +++ b/src/jobs/update-server-count.ts @@ -0,0 +1,15 @@ +import { ClusterServices } from '@/services'; +import { Job, UnitConstants } from '@rhidium/core'; + +const UpdateServerCountJob = new Job({ + id: 'update-server-count', + // Let's update every 30 minutes + interval: UnitConstants.MS_IN_ONE_MINUTE * 30, + // Set timeout, as we need some time for our clusters and/or shards to come online + timeout: UnitConstants.MS_IN_ONE_MINUTE * 5, + // Don't retry, rate-limit sensitive + maxRetries: 0, + run: ClusterServices.updateServerCount, +}); + +export default UpdateServerCountJob; diff --git a/src/listeners/client/ready.ts b/src/listeners/client/ready.ts new file mode 100644 index 0000000..6d36e9d --- /dev/null +++ b/src/listeners/client/ready.ts @@ -0,0 +1,24 @@ +import Lang from '@/i18n/i18n'; +import { Events } from 'discord.js'; +import { ClientEventListener } from '@rhidium/core'; + +export default new ClientEventListener({ + once: true, + event: Events.ClientReady, + async run(client) { + // Log time since client initialization + const timeSinceInit = client.logger.consoleExecutionTime( + client.startInitializeTs, + ); + client.logger.success( + Lang.t('client:ready', { + username: client.logger.consoleColors.cyan(client.user.username), + duration: timeSinceInit, + }), + ); + + // Always make sure commands are synced + // This checks if anything has changed, and updates accordingly + await client.commandManager.refreshCommandData(); + }, +}); diff --git a/src/listeners/guild/guildMemberAdd.ts b/src/listeners/guild/guildMemberAdd.ts new file mode 100644 index 0000000..44323ac --- /dev/null +++ b/src/listeners/guild/guildMemberAdd.ts @@ -0,0 +1,106 @@ +import { embedFromEmbedModel } from '@/chat-input/administrator/embeds/helpers'; +import { guildSettingsFromCache } from '@/database'; +import { buildDiscordPlaceholders, replacePlaceholders, replacePlaceholdersAcrossEmbed } from '@/placeholders'; +import { LoggingServices } from '@/services'; +import { EmbedBuilder, Events, PermissionFlagsBits } from 'discord.js'; +import { ClientEventListener, PermissionUtils, TimeUtils } from '@rhidium/core'; + +const requiredPermissions = [ + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.EmbedLinks, +]; + +export default new ClientEventListener({ + event: Events.GuildMemberAdd, + run: async (client, member) => { + const { logger } = client; + const { guild } = member; + + const guildSettings = await guildSettingsFromCache(guild.id); + if (!guildSettings || !guildSettings.memberJoinChannelId) return; + + const channel = guild.channels.cache.get(guildSettings.memberJoinChannelId); + if (!channel) { + LoggingServices.adminLog( + guild, + client.embeds.error({ + title: 'Member Join Message Error', + description: `No channel found with ID ${guildSettings.memberJoinChannelId}`, + }), + ); + return; + } + + if (!channel.permissionsFor(client.user.id)?.has(requiredPermissions)) { + LoggingServices.adminLog( + guild, + client.embeds.error({ + title: 'Member Join Message Error', + description: `No permissions to send member join message in specified channel ${channel}` + + `, missing: permissions: ${PermissionUtils.bigIntPermOutput( + requiredPermissions.filter((permission) => !channel.permissionsFor(client.user.id)?.has(permission)) + )}`, + }), + ); + return; + } + + if (!channel.isTextBased()) { + LoggingServices.adminLog( + guild, + client.embeds.error({ + title: 'Member Join Message Error', + description: `Channel ${channel} is not text-based`, + }), + ); + return; + } + + const accountCreatedOutput = TimeUtils.discordInfoTimestamp(member.user.createdTimestamp); + const { memberJoinEmbed } = guildSettings; + const baseEmbed = new EmbedBuilder() + .setColor(client.colors.primary) + .setAuthor({ + name: member.user.username, + iconURL: member.user.displayAvatarURL({ forceStatic: false }), + }) + .setTitle('Member Joined') + .setDescription(`Welcome ${member.user} to ${guild.name}`) + .setThumbnail(member.user.displayAvatarURL({ forceStatic: false })) + .addFields({ + name: 'Account Created', + value: accountCreatedOutput, + inline: true, + }, { + name: 'Member Count', + value: guild.memberCount.toLocaleString(), + inline: true, + }); + + + const rawEmbed = embedFromEmbedModel(memberJoinEmbed, baseEmbed); + const placeholders = buildDiscordPlaceholders( + channel, + guild, + member, + member.user + ); + const embed = replacePlaceholdersAcrossEmbed(rawEmbed, placeholders); + const resolvedMessage = guildSettings.memberJoinEmbed?.messageText + ? replacePlaceholders(guildSettings.memberJoinEmbed.messageText, placeholders) + : ''; + + channel.send({ content: resolvedMessage, embeds: [embed] }) + .catch((error) => { + logger.error('Error encountered while sending member join message, after checking permissions', error); + LoggingServices.adminLog( + guild, + client.embeds.error({ + title: 'Member Join Message Error', + description: 'An error occurred while sending the member join message, please try again later,' + + ' the developers have been notified of this error', + }), + ); + }); + }, +}); diff --git a/src/listeners/guild/guildMemberRemove.ts b/src/listeners/guild/guildMemberRemove.ts new file mode 100644 index 0000000..9569bfa --- /dev/null +++ b/src/listeners/guild/guildMemberRemove.ts @@ -0,0 +1,112 @@ +import { embedFromEmbedModel } from '@/chat-input/administrator/embeds/helpers'; +import { guildSettingsFromCache } from '@/database'; +import { buildDiscordPlaceholders, replacePlaceholders, replacePlaceholdersAcrossEmbed } from '@/placeholders'; +import { LoggingServices } from '@/services'; +import { EmbedBuilder, Events, PermissionFlagsBits } from 'discord.js'; +import { ClientEventListener, PermissionUtils, TimeUtils } from '@rhidium/core'; + +const requiredPermissions = [ + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.EmbedLinks, +]; + +export default new ClientEventListener({ + event: Events.GuildMemberRemove, + run: async (client, member) => { + const { logger } = client; + const { guild } = member; + + const guildSettings = await guildSettingsFromCache(guild.id); + if (!guildSettings || !guildSettings.memberLeaveChannelId) return; + + const channel = guild.channels.cache.get(guildSettings.memberLeaveChannelId); + if (!channel) { + LoggingServices.adminLog( + guild, + client.embeds.error({ + title: 'Member Leave Message Error', + description: `No channel found with ID ${guildSettings.memberLeaveChannelId}`, + }), + ); + return; + } + + if (!channel.permissionsFor(client.user.id)?.has(requiredPermissions)) { + LoggingServices.adminLog( + guild, + client.embeds.error({ + title: 'Member Leave Message Error', + description: `No permissions to send member leave message in specified channel ${channel}, ` + + `missing: permissions: ${PermissionUtils.bigIntPermOutput( + requiredPermissions.filter((permission) => !channel.permissionsFor(client.user.id)?.has(permission)) + )}`, + }), + ); + return; + } + + if (!channel.isTextBased()) { + LoggingServices.adminLog( + guild, + client.embeds.error({ + title: 'Member Leave Message Error', + description: `Channel ${channel} is not text-based`, + }), + ); + return; + } + + const accountCreatedOutput = TimeUtils.discordInfoTimestamp(member.user.createdTimestamp); + const joinedAtOutput = member.joinedTimestamp + ? TimeUtils.discordInfoTimestamp(member.joinedTimestamp) + : 'Unknown'; + const { memberLeaveEmbed } = guildSettings; + const baseEmbed = new EmbedBuilder() + .setColor(client.colors.primary) + .setAuthor({ + name: member.user.username, + iconURL: member.user.displayAvatarURL({ forceStatic: false }), + }) + .setTitle('Member Left') + .setDescription(`${member.user} has left ${guild.name}`) + .setThumbnail(member.user.displayAvatarURL({ forceStatic: false })) + .addFields({ + name: 'Account Created', + value: accountCreatedOutput, + inline: true, + }, { + name: 'Joined At', + value: joinedAtOutput, + inline: true, + }, { + name: 'Member Count', + value: guild.memberCount.toLocaleString(), + inline: true, + }); + + const rawEmbed = embedFromEmbedModel(memberLeaveEmbed, baseEmbed); + const placeholders = buildDiscordPlaceholders( + channel, + guild, + member, + member.user + ); + const embed = replacePlaceholdersAcrossEmbed(rawEmbed, placeholders); + const resolvedMessage = guildSettings.memberJoinEmbed?.messageText + ? replacePlaceholders(guildSettings.memberJoinEmbed.messageText, placeholders) + : ''; + + channel.send({ content: resolvedMessage, embeds: [embed] }) + .catch((error) => { + logger.error('Error encountered while sending member leave message, after checking permissions', error); + LoggingServices.adminLog( + guild, + client.embeds.error({ + title: 'Member Leave Message Error', + description: 'An error occurred while sending the member leave message, please try again later,' + + ' the developers have been notified of this error', + }), + ); + }); + }, +}); diff --git a/src/message-context/print-embeds.ts b/src/message-context/print-embeds.ts new file mode 100644 index 0000000..3723807 --- /dev/null +++ b/src/message-context/print-embeds.ts @@ -0,0 +1,42 @@ +import { AttachmentBuilder, InteractionReplyOptions, escapeCodeBlock } from 'discord.js'; +import { EmbedConstants, MessageContextCommand } from '@rhidium/core'; + +const PrintEmbedCommand = new MessageContextCommand({ + run: async (client, interaction) => { + const { targetMessage } = interaction; + const hasEmbeds = targetMessage.embeds.length > 0; + + if (!hasEmbeds) { // Might be missing MessageContent scope + await PrintEmbedCommand.reply(interaction, client.embeds.error( + 'Message has no embeds to print', + )); + return; + } + + const { embeds } = targetMessage; + const codeblockFormattingLength = 10; + const context: InteractionReplyOptions = { + embeds: [], + files: [], + }; + + for (const embed of embeds) { + const output = escapeCodeBlock(JSON.stringify(embed, null, 2)); + const outputLength = output.length; + if (outputLength > EmbedConstants.DESCRIPTION_MAX_LENGTH - codeblockFormattingLength) { + context.files!.push( + new AttachmentBuilder(Buffer.from(output)) + .setName('embed.json') + .setSpoiler(true) + ); + } + else context.embeds!.push(client.embeds.branding({ + description: `\`\`\`json\n${output}\n\`\`\``, + })); + } + + await PrintEmbedCommand.reply(interaction, context); + }, +}); + +export default PrintEmbedCommand; diff --git a/src/middleware/persistent-cooldown.ts b/src/middleware/persistent-cooldown.ts new file mode 100644 index 0000000..681799f --- /dev/null +++ b/src/middleware/persistent-cooldown.ts @@ -0,0 +1,73 @@ +import { cooldownFromCache, prisma, updateCooldown } from '@/database'; +import { + CommandCooldownType, + CommandMiddlewareFunction, + InteractionUtils, + TimeUtils, + cooldownResourceId, +} from '@rhidium/core'; + +export const persistentCooldownMiddleware: CommandMiddlewareFunction = async ({ + client, + interaction, + next, +}) => { + const { commandManager } = client; + const commandId = commandManager.resolveCommandId(interaction); + const command = commandManager.commandById(commandId); + + if (!command) return next(); + if ( + !command.cooldown + || !command.cooldown.enabled + || !command.cooldown.persistent + ) return next(); + + await command.deferReplyInternal(interaction); + + const now = Date.now(); + const { cooldown } = command; + const resourceId = cooldownResourceId(cooldown.type, interaction); + const cooldownId = `${command.sourceHash}@${resourceId}`; + const durationInMS = cooldown.duration; + + // Not cached because we have an expired-usage cleaning job + let cooldownEntry = await cooldownFromCache(cooldownId); + if (!cooldownEntry) { + cooldownEntry = await prisma.commandCooldown.create({ + data: { cooldownId, usages: [], duration: durationInMS }, + }); + } + + // Is on cooldown + const nonExpiredUsages = cooldownEntry.usages.filter( + (e) => e.valueOf() + durationInMS > now, + ); + const activeUsages = nonExpiredUsages.length; + if (nonExpiredUsages.length >= 1 && activeUsages >= cooldown.usages) { + const firstNonExpired = nonExpiredUsages[0] as Date; + const firstUsageExpires = new Date( + firstNonExpired.valueOf() + durationInMS, + ); + const remaining = firstUsageExpires.valueOf() - now; + const expiresIn = TimeUtils.msToHumanReadableTime(remaining); + const relativeOutput = expiresIn === '0 seconds' ? '1 second' : expiresIn; + InteractionUtils.replyDynamic(client, interaction, { + content: `You are on cooldown (type ${ + CommandCooldownType[cooldown.type] + }) for this command - please wait **${relativeOutput}** before using this command again`, + ephemeral: true, + }); + // Don't go next =) + // Doesn't continue to next middleware, command is not executed + return; + } + + // Increment usages + cooldownEntry.usages.push(new Date(now)); + await updateCooldown(cooldownEntry, { + data: { usages: cooldownEntry.usages }, + }); + + next(); +}; diff --git a/src/middleware/process-usage-statictics.ts b/src/middleware/process-usage-statictics.ts new file mode 100644 index 0000000..e147552 --- /dev/null +++ b/src/middleware/process-usage-statictics.ts @@ -0,0 +1,69 @@ +import { ApplicationCommandType } from 'discord.js'; +import { + ChatInputCommand, + CommandMiddlewareFunctionWithResult, + MessageContextCommand, + UserContextCommand, +} from '@rhidium/core'; +import { ClientExtensions } from '@/extensions'; + +export const processUsageStatisticsMiddleware: CommandMiddlewareFunctionWithResult = async ({ + client, + interaction, + startRunTs, + invokedAt, + next, + error, +}) => { + const commandId = client.commandManager.resolveCommandId(interaction); + const command = client.commandManager.commandById(commandId); + + if (!command) return next(); + if (!ClientExtensions.hasUsageStatisticsQueue(client)) throw new Error( + 'No usage statistics queue found to process usage statistics' + ); + + // Update usage statistics + const endRunTs = process.hrtime(startRunTs); + const runTimeMs = endRunTs[0] * 1000 + endRunTs[1] / 1000000; + const { usageStatisticsQueue } = client.extensions; + const usageStatistics = usageStatisticsQueue.peek(); + const cmdType = command instanceof ChatInputCommand + ? ApplicationCommandType.ChatInput + : command instanceof UserContextCommand + ? ApplicationCommandType.User + : command instanceof MessageContextCommand + ? ApplicationCommandType.Message + : command.type; + + const commandIdWithType = `${commandId}@${cmdType}`; + const createStatisticsData = { + commandId: commandIdWithType, + type: cmdType, + usages: [invokedAt], + errorCount: error ? 1 : 0, + lastUsed: new Date(), + lastError: error?.message ?? null, + lastErrorAt: error ? new Date() : null, + runtimeDurations: [ runTimeMs ], + }; + if (!usageStatistics) { + usageStatisticsQueue.add([createStatisticsData]); + } + else { + const cmdEntry = usageStatistics.find((x) => x.commandId === commandIdWithType); + if (cmdEntry) { + cmdEntry.usages.push(invokedAt); + if (error) { + cmdEntry.errorCount++; + cmdEntry.lastError = error.message; + cmdEntry.lastErrorAt = new Date(); + } + cmdEntry.lastUsed = new Date(); + cmdEntry.runtimeDurations.push(runTimeMs); + } + else usageStatistics.push(createStatisticsData); + } + + next(); +}; diff --git a/src/modals/eval-modal.ts b/src/modals/eval-modal.ts new file mode 100644 index 0000000..a83f31e --- /dev/null +++ b/src/modals/eval-modal.ts @@ -0,0 +1,40 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; +import { ModalCommand, PermLevel } from '@rhidium/core'; +import EvalConstants from '../enums/eval'; + +const CodeModalCommand = new ModalCommand({ + customId: EvalConstants.CODE_MODAL_ID, + permLevel: PermLevel['Bot Administrator'], + run: async (client, interaction) => { + const input = interaction.fields.getTextInputValue(EvalConstants.CODE_MODAL_INPUT_ID); + if (!input || input.length === 0) { + CodeModalCommand.reply(interaction, client.embeds.error( + 'No code was provided, please try again', + )); + return; + } + + const embed = client.embeds.waiting({ + title: 'Are you sure you want to evaluate this code?', + description: `\`\`\`js\n${input}\n\`\`\``, + }); + + CodeModalCommand.reply(interaction, { + embeds: [ embed ], + components: [ evalControlRow ], + }); + }, +}); + +export const evalControlRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(EvalConstants.ACCEPT_CODE_EVALUATION) + .setLabel('Accept') + .setStyle(ButtonStyle.Success), + new ButtonBuilder() + .setCustomId(EvalConstants.CANCEL_CODE_EVALUATION) + .setLabel('Cancel') + .setStyle(ButtonStyle.Secondary) +); + +export default CodeModalCommand; diff --git a/src/module-aliases.ts b/src/module-aliases.ts new file mode 100644 index 0000000..acde927 --- /dev/null +++ b/src/module-aliases.ts @@ -0,0 +1,11 @@ +import moduleAlias from 'module-alias'; + +// Note: We can't use _moduleAliases in package.json +// as it doesn't work with ts-node-dev + +// Changes should be reflected in tsconfig.json +// tsconfig.json = Intellisense +// module-alias = Runtime +moduleAlias.addAliases({ + '@': `${__dirname}/`, +}); diff --git a/src/permissions.ts b/src/permissions.ts new file mode 100644 index 0000000..592ba3c --- /dev/null +++ b/src/permissions.ts @@ -0,0 +1,70 @@ +import { ClientPermissionLevel } from '@rhidium/core'; +import { guildSettingsFromCache } from './database'; + +export const permConfig: ClientPermissionLevel[] = [ + { + name: 'User', + level: 0, + hasLevel: () => true, + }, + + { + name: 'Moderator', + level: 1, + /** + * Note: This doesn't check if the member has Kick/Ban member perms + * as that would mean people could have access to sensitive commands + * without the owner/admin explicitly setting the required role first + */ + hasLevel: async (_config, member) => { + const guildSettings = await guildSettingsFromCache(member.guild.id); + if (!guildSettings) return false; + return member.roles.cache.some( + (role) => guildSettings.modRoleId === role.id, + ); + }, + }, + + { + name: 'Administrator', + level: 2, + hasLevel: async (_config, member) => { + const guildSettings = await guildSettingsFromCache(member.guild.id); + if (!guildSettings) return false; + return member.roles.cache.some( + (role) => guildSettings.adminRoleId === role.id, + ); + }, + }, + + { + name: 'Server Owner', + level: 3, + hasLevel: (_config, member) => { + if (member.guild?.ownerId) { + return member.guild.ownerId === member.id; + } + return false; + }, + }, + + { + name: 'Bot Administrator', + level: 4, + hasLevel(config, member) { + return config.systemAdministrators.includes(member.id); + }, + }, + + { + name: 'Developer', + level: 5, + hasLevel: (config, member) => config.developers.includes(member.id), + }, + + { + name: 'Bot Owner', + level: 6, + hasLevel: (config, member) => config.ownerId === member.id, + }, +]; diff --git a/src/placeholders/discord.ts b/src/placeholders/discord.ts new file mode 100644 index 0000000..9359f10 --- /dev/null +++ b/src/placeholders/discord.ts @@ -0,0 +1,432 @@ +import { ArrayUtils, InteractionUtils, TimeUtils } from '@rhidium/core'; +import { + APIInteractionGuildMember, + ChannelType, + Guild, + GuildBasedChannel, + GuildExplicitContentFilter, + GuildMFALevel, + GuildMember, + GuildNSFWLevel, + GuildVerificationLevel, + PartialGuildMember, + User, +} from 'discord.js'; +import { PlaceholderString } from '.'; + +export type ChannelPlaceholders = { + '#channel': string; + channelCreatedAt: string; + channelCreatedTS: string; + channelId: string; + channelName: string; + channelNSFW: string; + channelParentId: string; + channelParentName: string; + channelPosition: string; + channelTopic: string; + channelType: string; + channelURL: string; + channelMemberCount: string; +}; + +export type GuildPlaceholders = { + serverAFKChannelId: string; + serverAFKChannelName: string; + serverAFKTimeout: string; + serverBannerHash: string; + serverBannerURL: string; + serverChannelCount: string; + serverCreatedAt: string; + serverCreatedTS: string; + serverDescription: string; + serverDiscoverySplashHash: string; + serverDiscoverySplashURL: string; + serverEmojiCount: string; + serverExplicitContentFilter: string; + serverFeatures: string; + serverIconHash: string; + serverIconURL: string; + serverId: string; + serverMaxMembers: string; + serverMemberCount: string; + serverMFALevel: string; + serverName: string; + serverNameAcronym: string; + serverNSFWLevel: string; + serverOwnerId: string; + '@serverOwner': string; + serverPartnered: string; + serverPreferredLocale: string; + serverPremiumTier: string; + serverBoostCount: string; + serverRoleCount: string; + serverRulesChannelId: string; + serverRulesChannelName: string; + '#serverRulesChannel': string; + serverAutoModChannelId: string; + serverAutoModChannelName: string; + '#serverAutoModChannel': string; + serverShardId: string; + serverSplashHash: string; + serverSplashURL: string; + serverStickerCount: string; + serverSystemChannelId: string; + serverSystemChannelName: string; + '#serverSystemChannel': string; + serverVanityURLCode: string; + serverVerificationLevel: string; + serverVerified: string; + serverWidgetChannelId: string; + serverWidgetChannelName: string; + '#serverWidgetChannel': string; +} + +export type MemberPlaceholders = { + '@member': string; + memberAvatarHash: string; + memberAvatarURL: string; + memberTimedOutUntil: string; + memberTimedOutUntilTS: string; + memberDisplayColor: string; + memberDisplayHexColor: string; + memberDisplayName: string; + memberId: string; + memberJoinedAt: string; + memberJoinedTS: string; + memberNickname: string; + memberPending: string; + memberPermissions: string; + memberPremiumSince: string; + memberPremiumSinceTS: string; + memberRoleCount: string; + memberRoles: string; +} + +export type UserPlaceholders = { + '@user': string; + userAccentColor: string; + userAccentColorHex: string; + userAvatarHash: string; + userAvatarURL: string; + userAvatarDecoration: string; + userBannerHash: string; + userBannerURL: string; + userBot: string; + userCreatedAt: string; + userCreatedTS: string; + userDefaultAvatarURL: string; + userDisplayName: string; + userGlobalName: string; + userId: string; + userUsername: string; +} + +export type DiscordPlaceholders = ChannelPlaceholders & + GuildPlaceholders & + MemberPlaceholders & + UserPlaceholders + +export type DiscordPlaceholderKey = keyof DiscordPlaceholders; + +export type DiscordPlaceholderString = PlaceholderString + +/** + * Every group should have a maximum of 25 placeholders + */ +export const organizedDiscordPlaceholders: Record< + DiscordPlaceholderKey, + string +> & Record< + `{{SEPARATOR-${string}}}`, + null +> = { + '{{SEPARATOR-CHANNEL}}': null, + + '#channel': 'Mention/tag the channel (clickable)', + channelCreatedAt: 'When the channel was created', + channelCreatedTS: 'Timestamp of when the channel was created', + channelId: 'The ID of the channel', + channelName: 'The name of the channel', + channelNSFW: 'Whether the channel is NSFW or not', + channelParentId: 'The ID of the parent channel', + channelParentName: 'The name of the parent channel', + channelPosition: 'The position of the channel', + channelTopic: 'The topic of the channel', + channelType: 'The type of the channel', + channelURL: 'The URL of the channel', + channelMemberCount: 'The number of members in the channel', + + '{{SEPARATOR-MEMBER}}': null, + + '@member': 'Mention/tag the member (clickable)', + memberAvatarHash: 'The avatar hash of the member', + memberAvatarURL: 'The avatar URL of the member', + memberTimedOutUntil: 'When the member\'s communication is disabled until', + memberTimedOutUntilTS: 'When the member\'s communication is disabled until', + memberDisplayColor: 'The display color of the member', + memberDisplayHexColor: 'The display color of the member in Hex format', + memberDisplayName: 'The display name of the member', + memberId: 'The ID of the member', + memberJoinedAt: 'When the member joined the server', + memberJoinedTS: 'Timestamp of when the member joined the server', + memberNickname: 'The nickname of the member', + memberPending: 'Whether the member verification is pending or not', + memberPermissions: 'The permissions of the member', + memberPremiumSince: 'When the member boosted the server', + memberPremiumSinceTS: 'Timestamp of when the member boosted the server', + memberRoleCount: 'The number of roles the member has', + memberRoles: 'The roles the member has', + + '{{SEPARATOR-SERVER}}': null, + + serverAFKChannelId: 'The ID of the AFK channel', + serverAFKChannelName: 'The name of the AFK channel', + serverAFKTimeout: 'The AFK timeout of the server', + serverBannerHash: 'The banner hash of the server', + serverBannerURL: 'The banner URL of the server', + serverChannelCount: 'The number of channels in the server', + serverCreatedAt: 'When the server was created', + serverCreatedTS: 'Timestamp of when the server was created', + serverDescription: 'The description of the server', + serverDiscoverySplashHash: 'The discovery splash hash of the server', + serverDiscoverySplashURL: 'The discovery splash URL of the server', + serverEmojiCount: 'The number of emojis in the server', + serverExplicitContentFilter: 'The explicit content filter of the server', + serverFeatures: 'The features of the server', + serverIconHash: 'The icon hash of the server', + serverIconURL: 'The icon URL of the server', + serverId: 'The ID of the server', + serverMaxMembers: 'The maximum number of members in the server', + serverMemberCount: 'The number of members in the server', + serverMFALevel: 'The MFA level of the server', + serverName: 'The name of the server', + serverNameAcronym: 'The name acronym of the server', + serverNSFWLevel: 'The NSFW level of the server', + serverOwnerId: 'The ID of the owner of the server', + '@serverOwner': 'Mention/tag the owner of the server (clickable)', + + '{{SEPARATOR-SERVER-2}}': null, + + serverPartnered: 'Whether the server is partnered or not', + serverPreferredLocale: 'The preferred locale of the server', + serverPremiumTier: 'The premium tier of the server', + serverBoostCount: 'The number of premium subscriptions of the server', + serverRoleCount: 'The number of roles in the server', + serverRulesChannelId: 'The ID of the rules channel', + serverRulesChannelName: 'The name of the rules channel', + '#serverRulesChannel': 'Mention/tag the rules channel (clickable)', + serverAutoModChannelId: 'The ID of the safety alerts channel', + serverAutoModChannelName: 'The name of the safety alerts channel', + '#serverAutoModChannel': 'Mention/tag the safety alerts channel (clickable)', + serverShardId: 'The shard ID of the server', + serverSplashHash: 'The splash hash of the server', + serverSplashURL: 'The splash URL of the server', + serverStickerCount: 'The number of stickers in the server', + serverSystemChannelId: 'The ID of the system channel', + serverSystemChannelName: 'The name of the system channel', + '#serverSystemChannel': 'Mention/tag the system channel (clickable)', + serverVanityURLCode: 'The vanity URL code of the server', + serverVerificationLevel: 'The verification level of the server', + serverVerified: 'Whether the server is verified or not', + serverWidgetChannelId: 'The ID of the widget channel', + serverWidgetChannelName: 'The name of the widget channel', + '#serverWidgetChannel': 'Mention/tag the widget channel (clickable)', + + '{{SEPARATOR-USER}}': null, + + '@user': 'Mention/tag the user (clickable)', + userAccentColor: 'The accent color of the user', + userAccentColorHex: 'The accent color of the user in Hex format', + userAvatarHash: 'The avatar hash of the user', + userAvatarURL: 'The avatar URL of the user', + userAvatarDecoration: 'The avatar decoration of the user', + userBannerHash: 'The banner hash of the user', + userBannerURL: 'The banner URL of the user', + userBot: 'Whether the user is a bot or not', + userCreatedAt: 'When the user was created', + userCreatedTS: 'Timestamp of when the user was created', + userDefaultAvatarURL: 'The default avatar URL of the user', + userDisplayName: 'The display name of the user', + userGlobalName: 'The global name of the user', + userId: 'The ID of the user', + userUsername: 'The username of the user', +}; + +export const discordPlaceholders = Object.fromEntries( + Object.entries(organizedDiscordPlaceholders).filter(([,value]) => value !== null), +) as Record; + +export const discordPlaceholderStrings = Object.keys( + discordPlaceholders +).map((e) => `{{${e}}}` as DiscordPlaceholderString); + +export const lowercaseDiscordPlaceholderStrings = discordPlaceholderStrings + .map((placeholder) => placeholder.toLowerCase()); + +export const groupedDiscordPlaceholders = Object.fromEntries( + Object.entries(organizedDiscordPlaceholders).reduce( + (acc, [key, value]) => { + const sep = '{{SEPARATOR-'; + const sepEnd = '}}'; + if (key.startsWith(sep)) { + const groupName = key.substring(sep.length, key.length - sepEnd.length); + acc.push([groupName, {} as Record]); + } else { + const group = acc[acc.length - 1]![1]; + if (value !== null) { + group[key as keyof typeof group] = value; + } + } + return acc; + }, + [] as [string, Record][], + ), +); + +export const buildDiscordPlaceholders = ( + channel: GuildBasedChannel | null, + guild: Guild, + member: GuildMember | APIInteractionGuildMember | PartialGuildMember, + user: User, +): Required => { + const threadOrCategory = channel && (channel.type === ChannelType.GuildCategory || channel.isThread()); + const resolvedMember = member instanceof GuildMember + ? member + : guild.members.resolve(member.user.id); + + return { + '#channel': channel?.toString() ?? 'n/a', + channelCreatedAt: channel?.createdAt ? TimeUtils.discordInfoTimestamp(channel?.createdAt.valueOf()) : 'n/a', + channelCreatedTS: channel?.createdTimestamp?.toString() ?? 'n/a', + channelId: channel?.id ?? 'n/a', + channelName: channel?.name ?? 'n/a', + channelNSFW: channel ? threadOrCategory ? 'n/a' : `${channel?.nsfw ?? 'n/a'}` : 'n/a', + channelParentId: channel?.parentId ?? 'None', + channelParentName: channel?.parent?.name ?? 'None', + channelPosition: + channel && !threadOrCategory + ? channel.position.toString() + : 'n/a', + channelTopic: channel && (threadOrCategory || channel.isVoiceBased()) ? 'n/a' : channel?.topic ?? 'None', + channelType: channel ? InteractionUtils.channelTypeToString(channel.type) : 'n/a', + channelURL: channel?.toString() ?? 'n/a', + channelMemberCount: !channel + ? 'n/a' + : channel.isThread() + ? channel.members.cache.size.toString() + : channel.members.size.toString(), + + '@member': resolvedMember?.toString() ?? 'Unknown', + memberAvatarHash: resolvedMember?.user.avatar ?? 'None', + memberAvatarURL: resolvedMember?.user.avatarURL() ?? 'None', + memberTimedOutUntil: resolvedMember?.communicationDisabledUntil + ? TimeUtils.discordInfoTimestamp(resolvedMember?.communicationDisabledUntil.valueOf()) + : 'None', + memberTimedOutUntilTS: resolvedMember?.communicationDisabledUntil + ?.valueOf().toString() ?? 'Unknown', + memberDisplayColor: resolvedMember?.displayColor.toString() ?? 'Unknown', + memberDisplayHexColor: resolvedMember?.displayHexColor ?? 'Unknown', + memberDisplayName: resolvedMember?.displayName ?? 'Unknown', + memberId: resolvedMember?.id ?? 'Unknown', + memberJoinedAt: resolvedMember?.joinedAt ? TimeUtils.discordInfoTimestamp(resolvedMember?.joinedAt.valueOf()) : '', + memberJoinedTS: resolvedMember?.joinedTimestamp?.toString() ?? 'Unknown', + memberNickname: resolvedMember?.nickname ?? 'None', + memberPending: resolvedMember?.pending.toString() ?? 'Unknown', + memberPermissions : resolvedMember?.permissions.toArray().join(', ') ?? 'Unknown', + memberPremiumSince: resolvedMember?.premiumSince + ? TimeUtils.discordInfoTimestamp(resolvedMember?.premiumSince.valueOf()) : '', + memberPremiumSinceTS: resolvedMember?.premiumSince?.valueOf().toString() ?? 'Unknown', + memberRoleCount: resolvedMember?.roles.cache.size.toString() ?? 'Unknown', + memberRoles: ArrayUtils.joinWithLimit(resolvedMember?.roles.cache.map(role => role.toString()) ?? [], 10), + + serverAFKChannelId: guild.afkChannelId ?? 'None', + serverAFKChannelName: guild.afkChannel?.name ?? 'None', + serverAFKTimeout: guild.afkTimeout.toString(), + serverBannerHash: guild.banner ?? 'None', + serverBannerURL: guild.bannerURL() ?? 'None', + serverChannelCount: guild.channels.cache.size.toString(), + serverCreatedAt: guild.createdAt.toString(), + serverCreatedTS: guild.createdTimestamp.toString(), + serverDescription: guild.description ?? 'None', + serverDiscoverySplashHash: guild.discoverySplash ?? 'None', + serverDiscoverySplashURL: guild.discoverySplashURL() ?? 'None', + serverEmojiCount: guild.emojis.cache.size.toString(), + serverExplicitContentFilter: guild.explicitContentFilter === GuildExplicitContentFilter.AllMembers + ? 'All Members' + : guild.explicitContentFilter === GuildExplicitContentFilter.MembersWithoutRoles + ? 'Members Without Roles' + : 'Disabled', + serverFeatures: guild.features.join(', '), + serverIconHash: guild.icon ?? 'None', + serverIconURL: guild.iconURL() ?? 'None', + serverId: guild.id, + serverMaxMembers: guild.maximumMembers ? guild.maximumMembers.toString() : 'Unknown', + serverMemberCount: guild.memberCount.toString(), + serverMFALevel: guild.mfaLevel === GuildMFALevel.Elevated + ? 'Elevated' + : 'None', + serverName: guild.name, + serverNameAcronym: guild.nameAcronym, + serverNSFWLevel: guild.nsfwLevel === GuildNSFWLevel.AgeRestricted + ? 'Age Restricted' + : guild.nsfwLevel === GuildNSFWLevel.Explicit + ? 'Explicit' + : guild.nsfwLevel === GuildNSFWLevel.Safe + ? 'Safe' + : 'Default', + serverOwnerId: guild.ownerId, + '@serverOwner': `<@${guild.ownerId}`, + serverPartnered: guild.partnered.toString(), + serverPreferredLocale: guild.preferredLocale, + serverPremiumTier: guild.premiumTier === 0 + ? 'None' + : `Tier ${guild.premiumTier}`, + serverBoostCount: guild.premiumSubscriptionCount ? guild.premiumSubscriptionCount.toString() : 'None', + serverRoleCount: guild.roles.cache.size.toString(), + serverRulesChannelId: guild.rulesChannelId ?? 'None', + serverRulesChannelName: guild.rulesChannel?.name ?? 'None', + '#serverRulesChannel': guild.rulesChannel?.toString() ?? 'None', + serverAutoModChannelId: guild.publicUpdatesChannelId ?? 'None', + serverAutoModChannelName: guild.publicUpdatesChannel?.name ?? 'None', + '#serverAutoModChannel': guild.publicUpdatesChannel?.toString() ?? 'None', + serverShardId: guild.shardId.toString(), + serverSplashHash: guild.splash ?? 'None', + serverSplashURL: guild.splashURL() ?? 'None', + serverStickerCount: guild.stickers.cache.size.toString(), + serverSystemChannelId: guild.systemChannelId ?? 'None', + serverSystemChannelName: guild.systemChannel?.name ?? 'None', + serverVanityURLCode: guild.vanityURLCode ?? 'None', + serverVerificationLevel: guild.verificationLevel === GuildVerificationLevel.VeryHigh + ? 'Very High' + : guild.verificationLevel === GuildVerificationLevel.High + ? 'High' + : guild.verificationLevel === GuildVerificationLevel.Medium + ? 'Medium' + : guild.verificationLevel === GuildVerificationLevel.Low + ? 'Low' + : 'None', + serverVerified: guild.verified.toString(), + serverWidgetChannelId: guild.widgetChannelId ?? 'None', + serverWidgetChannelName: guild.widgetChannel?.name ?? 'None', + '#serverSystemChannel': guild.systemChannel?.toString() ?? 'None', + '#serverWidgetChannel': guild.widgetChannel?.toString() ?? 'None', + + '@user': user.toString(), + userAccentColor: user.accentColor ? `${user.accentColor}` : 'None', + userAccentColorHex: user.hexAccentColor ?? 'None', + userAvatarDecoration: user.avatarDecoration ?? 'None', + userAvatarHash: user.avatar ?? 'None', + userAvatarURL: user.avatarURL() ?? 'None', + userBannerHash: user.banner ?? 'None', + userBannerURL: user.bannerURL() ?? 'None', + userBot: user.bot.toString(), + userCreatedAt: TimeUtils.discordInfoTimestamp(user.createdAt.valueOf()), + userCreatedTS: user.createdTimestamp.toString(), + userDefaultAvatarURL: user.defaultAvatarURL ?? 'None', + userDisplayName: user.username, + userGlobalName: user.globalName ?? 'None', + userId: user.id, + userUsername: user.username, + }; +}; diff --git a/src/placeholders/index.ts b/src/placeholders/index.ts new file mode 100644 index 0000000..4792e21 --- /dev/null +++ b/src/placeholders/index.ts @@ -0,0 +1,49 @@ +import { EmbedBuilder } from 'discord.js'; + +export * from './discord'; + +export type PlaceholderString = `{{${T}}}`; + +export type Placeholders< + S extends string = string, + T extends PlaceholderString = PlaceholderString, +> = { + [key in T]: string; +}; + +export const placeholderRegex = /(?{{\s*[a-zA-Z0-9@#]+\s*}})/g; + +export const isPlaceholder = (str: string): boolean => + placeholderRegex.test(str); + +export const replacePlaceholders =

( + str: string, + placeholders: Required

, +): string => { + const matches = str.matchAll(placeholderRegex); + const replacedPlaceholders: string[] = []; + let newStr: string = str; + for (const match of matches) { + const placeholder = match.groups?.placeholder; + const cleanPlaceholder = placeholder?.replace(/{{\s*|\s*}}/g, ''); + if (!placeholder || !cleanPlaceholder) continue; + if ( + !(cleanPlaceholder in placeholders) || + replacedPlaceholders.includes(placeholder) + ) continue; + + const value = placeholders[cleanPlaceholder as keyof typeof placeholders]; + newStr = newStr.replace(placeholder, `${value}`); + replacedPlaceholders.push(placeholder); + } + + return newStr; +}; + +export const replacePlaceholdersAcrossEmbed =

( + embed: EmbedBuilder, + placeholders: P, +): EmbedBuilder => { + const embedStr = replacePlaceholders(JSON.stringify(embed), placeholders); + return EmbedBuilder.from(JSON.parse(embedStr)); +}; diff --git a/src/select-menus/.gitkeep b/src/select-menus/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/services/cluster-services.ts b/src/services/cluster-services.ts new file mode 100644 index 0000000..030fd77 --- /dev/null +++ b/src/services/cluster-services.ts @@ -0,0 +1,36 @@ +import { Client, ClusterUtils } from '@rhidium/core'; +import { ActivityType } from 'discord.js'; + +const updateServerCount = async (client: Client): Promise => { + if (ClusterUtils.hasCluster(client)) { + const results = await client.cluster.broadcastEval((c) => { + const serverCount = c.guilds.cache.size; + const shardIds = c.cluster ? [...c.cluster.ids.keys()] : []; + if (!c.user) return 0; + c.user.presence.set({ + status: 'online', + activities: [ + { + name: `${serverCount.toLocaleString()} server${serverCount === 1 ? '' : 's'}`, + type: 3, // Can't use ActivityType.Watching here because of broadcastEval + }, + ], + shardId: shardIds, + }); + return serverCount; + }); + return results.reduce((acc, val) => acc + val, 0); + } + else { + const serverCount = client.guilds.cache.size; + client.user.setActivity( + `${serverCount.toLocaleString()} server${serverCount === 1 ? '' : 's'}`, + { type: ActivityType.Watching } + ); + return serverCount; + } +}; + +export class ClusterServices { + static readonly updateServerCount = updateServerCount; +} diff --git a/src/services/discord-logging.ts b/src/services/discord-logging.ts new file mode 100644 index 0000000..4a2dae1 --- /dev/null +++ b/src/services/discord-logging.ts @@ -0,0 +1,77 @@ +import { appConfig } from '@/config'; +import { guildSettingsFromCache } from '@/database'; +import { + EmbedBuilder, + Guild, + GuildMember, + MessageCreateOptions, + MessagePayload, + PermissionFlagsBits, +} from 'discord.js'; +import { Client } from '@rhidium/core'; + +/** + * Perform logging of a mod action to a specific server, + * this function does not notify if missing permissions + */ +const modLog = async ( + client: Client, + guild: Guild, + action: string, + target: GuildMember, + moderator: GuildMember, + reason: string = 'No reason provided.', +) => { + const settings = await guildSettingsFromCache(guild.id); + if (!settings || !settings.modLogChannelId) return; + + const modLogChannel = guild.channels.cache.get( + settings.modLogChannelId, + ); + if (!modLogChannel || !modLogChannel.isTextBased()) return; + + const hasPerms = modLogChannel.permissionsFor(appConfig.client.id)?.has([ + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.EmbedLinks, + ]); + if (!hasPerms) return; + + const embed = client.embeds.info({ + title: `${action} | ${target.user.tag}`, + description: `**Reason:** ${reason}\n**Moderator:** ${moderator.user.tag}`, + }); + embed.setThumbnail(target.user.displayAvatarURL({ forceStatic: false })); + + modLogChannel.send({ embeds: [embed] }); +}; + +/** + * Perform logging of anything internal, can be considered + * ad audit log - this function does not notify if missing permissions + */ +const adminLog = async ( + guild: Guild, + msg: string | MessagePayload | MessageCreateOptions | EmbedBuilder +) => { + const settings = await guildSettingsFromCache(guild.id); + if (!settings || !settings.adminLogChannelId) return; + + const adminLogChannel = guild.channels.cache.get( + settings.adminLogChannelId, + ); + if (!adminLogChannel || !adminLogChannel.isTextBased()) return; + + const hasPerms = adminLogChannel.permissionsFor(appConfig.client.id)?.has([ + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.EmbedLinks, + ]); + if (!hasPerms) return; + + const resolvedMsg = msg instanceof EmbedBuilder ? { embeds: [msg] } : msg; + adminLogChannel.send(resolvedMsg); +}; + +export class LoggingServices { + static readonly modLog = modLog; + static readonly adminLog = adminLog; +} diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..f5045db --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,2 @@ +export * from './cluster-services'; +export * from './discord-logging'; diff --git a/src/shared.ts b/src/shared.ts new file mode 100644 index 0000000..fe93e3a --- /dev/null +++ b/src/shared.ts @@ -0,0 +1,33 @@ +import { logger } from '@rhidium/core'; + +import './module-aliases'; +import { init as initLang } from './i18n/i18n'; +import { appConfig } from './config'; + +/** + * This file is used to initialize any shared code - + * code that should be executed both when spawning a cluster, + * and when starting the client/bot normally. + * + * Importing this file as early as possible is crucial, + * as it will initialize our config and language localization + * across the entire application and all shared processes. + */ + +// Instantiate our singleton config as early as possible +// used in initLang, would otherwise need unused variable + +// Initialize our language localization +initLang(appConfig.debug.localizations); + +// Error handling / keep alive ONLY in development. You shouldn't have any +// unhandledRejection or uncaughtException errors in production +// as these should be addressed in development +if (process.env.NODE_ENV !== 'production') { + process.on('unhandledRejection', (reason, promise) => { + logger._error('Encountered unhandledRejection error (catch):', reason, promise); + }); + process.on('uncaughtException', (err, origin) => { + logger._error('Encountered uncaughtException error:', err, origin); + }); +} diff --git a/src/user-context/info.ts b/src/user-context/info.ts new file mode 100644 index 0000000..1bc8d71 --- /dev/null +++ b/src/user-context/info.ts @@ -0,0 +1,84 @@ +import { UserContextCommand, ArrayUtils, TimeUtils } from '@rhidium/core'; + +const UserInfoCommand = new UserContextCommand({ + disabled: process.env.NODE_ENV === 'production', + run: async (client, interaction) => { + const { guild, targetUser } = interaction; + + if (!guild) { + UserInfoCommand.reply(interaction, { + content: 'This command can only be used on server members, it\'s not available in DM\'s.', + ephemeral: true, + }); + return; + } + + await UserInfoCommand.deferReplyInternal(interaction); + + const target = await guild.members.fetch(targetUser.id); + if (!target) { + UserInfoCommand.reply(interaction, { + content: 'Failed to fetch target member.', + ephemeral: true, + }); + return; + } + + const maxRoles = 25; + const roles = target.roles.cache + .filter((role) => role.id !== guild.roles.everyone.id) + .toJSON() + .map((e) => e.toString()); + const joinedServer = target.joinedAt ? TimeUtils.discordInfoTimestamp(target.joinedAt.valueOf()) : 'Unknown'; + const joinedDiscord = TimeUtils.discordInfoTimestamp(targetUser.createdAt.valueOf()); + const roleOutput = ArrayUtils.joinWithLimit(roles, maxRoles, 'None'); + const hasServerAvatar = target.displayAvatarURL() !== null + && target.displayAvatarURL() !== targetUser.displayAvatarURL(); + const serverAvatarOutput = hasServerAvatar + ? `[link](<${target.displayAvatarURL({ forceStatic: false, size: 4096 })}>)` + : 'None'; + const boostingOutput = target.premiumSinceTimestamp !== null + ? TimeUtils.discordInfoTimestamp(target.premiumSinceTimestamp) + : 'Member is **not** currently boosting'; + + const embed = client.embeds.branding({ + description: roleOutput, + author: { + name: target.user.username, + iconURL: targetUser.displayAvatarURL({ forceStatic: false }), + }, + fields: [ + { + name: 'Nickname', + value: target.nickname ?? 'None', + inline: true, + }, + { + name: 'Server Avatar', + value: serverAvatarOutput, + }, + { + name: 'Boost', + value: boostingOutput, + inline: false, + }, + { + name: 'Joined Server', + value: joinedServer, + inline: false, + }, + { + name: 'Joined Discord', + value: joinedDiscord, + inline: false, + }, + ], + }); + + if (hasServerAvatar) embed.setThumbnail(target.displayAvatarURL({ forceStatic: false, size: 1024 })); + + UserInfoCommand.reply(interaction, embed); + }, +}); + +export default UserInfoCommand; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3f54e61 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,116 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + "experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */, + "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */, + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "NodeNext" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "NodeNext" /* Specify how TypeScript looks up a file from a given module specifier. */, + + "traceResolution": false, + // "baseUrl": "." /* Specify the base directory to resolve non-relative module names. */, + "paths": { + "@/*": [ "./src/*" ], + "@rhidium/system": [ "../system" ] + } /* Specify a set of entries that re-map imports to additional lookup locations. */, + + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "resolveJsonModule": true /* Enable importing .json files. */, + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, + // "declarationMap": true /* Create sourcemaps for d.ts files. */, + // "declarationDir": "./types" /* Specify the output directory for generated declaration files. */, + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + "sourceMap": true /* Create source map files for emitted JavaScript files. */, + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + "importHelpers": true /* Allow importing helper functions from tslib once per project, instead of including them per-file. */, + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, + "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, + "strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */, + "strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */, + "strictBindCallApply": true /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */, + "strictPropertyInitialization": true /* Check for class properties that are declared but not set in the constructor. */, + "noImplicitThis": true /* Enable error reporting when 'this' is given the type 'any'. */, + "useUnknownInCatchVariables": true /* Default catch clause variables as 'unknown' instead of 'any'. */, + "alwaysStrict": true /* Ensure 'use strict' is always emitted. */, + "noUnusedLocals": true /* Enable error reporting when local variables aren't read. */, + "noUnusedParameters": true /* Raise an error when a function parameter isn't read. */, + "exactOptionalPropertyTypes": true /* Interpret optional property types as written, rather than adding 'undefined'. */, + "noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */, + "noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */, + "noUncheckedIndexedAccess": true /* Add 'undefined' to a type when accessed using an index. */, + "noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an override modifier. */, + "noPropertyAccessFromIndexSignature": false /* Enforces using indexed accessors for keys declared using an indexed type. */, + "allowUnusedLabels": false /* Disable error reporting for unused labels. */, + "allowUnreachableCode": false /* Disable error reporting for unreachable code. */, + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": [ + "src/**/*", + "test/**/*", + "scripts", + "test", + "locales/**/*.json" + ] +}