diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8fa1c7c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x, 19.x, 20.x, 21.x, 22.x] + fail-fast: true + + name: Test TypeScript build + steps: + - uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Build the package + run: pnpm build + - name: Run tests + run: pnpm run coverage diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index ea35190..418e70c 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -11,12 +11,13 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v4 with: - version: 9 - - uses: actions/setup-node@v3 + version: 10 + - name: Use Node.js + uses: actions/setup-node@v4 with: node-version: 20 registry-url: https://registry.npmjs.org/ @@ -29,12 +30,13 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v4 with: - version: 9 - - uses: actions/setup-node@v3 + version: 10 + - name: Use Node.js + uses: actions/setup-node@v4 with: node-version: 20 registry-url: https://registry.npmjs.org/ @@ -43,11 +45,7 @@ jobs: run: pnpm install --frozen-lock - name: Build the package run: pnpm build - - name: Move files - run: rm -r ./src && mv ./dist/src/* ./ && rm -r ./dist - - name: Edit package.json imports - run: sed -i 's#../../package.json#../package.json#' discordjs/index.js && sed -i 's#../../package.json#../package.json#' discordjs-light/index.js && sed -i 's#../../package.json#../package.json#' eris/index.js && sed -i 's#../../package.json#../package.json#' oceanic/index.js - name: Publish to NPM - run: pnpm publish --no-git-checks + run: pnpm publish --recursive --no-git-checks env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.github/workflows/test-ts.yml b/.github/workflows/test-ts.yml deleted file mode 100644 index a2d3daa..0000000 --- a/.github/workflows/test-ts.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Test TS - -on: - pull_request: - types: [opened, reopened, synchronize] - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [16.x, 17.x, 18.x, 19.x, 20.x, 21.x, 22.x] - - name: Test TypeScript build - steps: - - name: Check node version - run: node --version - - uses: actions/checkout@v3.5.2 - - name: Install NodeJS - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - name: Install dependencies - run: npm install - - name: Compile typescript - uses: icrawl/action-tsc@v1 diff --git a/.gitignore b/.gitignore index ff8c00e..7f69862 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules dist +coverage .idea # Files diff --git a/.npmignore b/.npmignore index 23acda7..988eeca 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,9 @@ node_modules src -dist/test +test +examples +coverage +tsconfig.json +*.ts .idea -.github -test \ No newline at end of file +.github \ No newline at end of file diff --git a/README.md b/README.md index 0f63a1b..a986d4a 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,22 @@ # Discord Analytics -## Installing the package +## Overview +Discord Analytics is a powerful tool that allows you to track and analyze your Discord bot's performance and user engagement. This repository contains packages for different Discord libraries, including Discord.js, Eris, Oceanic.js, and Discord.js-light. +These packages provide a simple and efficient way to integrate Discord Analytics into your bot, enabling you to gather valuable insights and improve your bot's functionality. + +## Installing the packages +> **Note:** Use the package manager of your choice (npm, yarn, pnpm) to install the packages. ```bash -npm install discord-analytics +npm install @discordanalytics/core # Core package +npm install @discordanalytics/discordjs # For Discord.js +npm install @discordanalytics/discordjs-light # For Discord.js-light +npm install @discordanalytics/eris # For Eris +npm install @discordanalytics/oceanic # For Oceanic.js ``` ## Usage -> **Note:** To use Discord Analytics, you need to have an API token. Check the docs for more informations : https://docs.discordanalytics.xyz/get-started/bot-registration - -**With Discord.js:** - -```js -// Import Discord.js's client and intents -const { Client, IntentsBitField } = require("discord.js") -// import discord-analytics -const { default: DiscordAnalytics } = require("discord-analytics/discordjs") - -// Create Discord client -const client = new Client({ - intents: [IntentsBitField.Flags.Guilds] // This intent is required -}); - -// Create Discord Analytics instance -// Don't forget to replace YOUR_API_TOKEN by your Discord Analytics token ! -const analytics = new DiscordAnalytics({ - client: client, - apiToken: 'YOUR_API_TOKEN', - sharded: false // Set it to true if your bot use shards -}); - -// start tracking selected events -analytics.trackEvents(); - -// When Discord client is ready -client.on('ready', () => { - console.log("Bot is ready!"); -}); - -// Login to Discord -// Don't forget to replace token by your Discord bot token ! -client.login('token'); -``` - -**With Eris:** - -```js -const {Client} = require("eris"); -const {default: DiscordAnalytics} = require("discord-analytics/eris"); - -// Create Eris client. -// Don't forget to replace token by your Discord bot token ! -const bot = new Client("token"); - -bot.on("ready", () => { - // Create Discord Analytics instance - // Don't forget to replace YOUR_API_TOKEN by your Discord Analytics token ! - const analytics = new DiscordAnalytics({ - client: client, - apiToken: 'YOUR_API_TOKEN' - }); - - // start tracking selected events - analytics.trackEvents(); - - console.log("Ready!"); -}); - -// Login to Discord -bot.connect(); -``` - -**With Oceanic.js:** -```js -// Import Discord.js's client and intents -const { Client } = require("oceanic.js") -// import discord-analytics -const { default: DiscordAnalytics } = require("discord-analytics/oceanic") - -// Create Discord client -const client = new Client({ - auth: "Bot ", - gateway: { - intents: ["GUILDS"] // This intent is required - } -}) - -// Create Discord Analytics instance -// Don't forget to replace YOUR_API_TOKEN by your Discord Analytics token ! -const analytics = new DiscordAnalytics({ - client: client, - apiToken: 'YOUR_API_TOKEN' -}); - -// start tracking selected events -analytics.trackEvents(); - -// When Discord client is ready -client.on('ready', () => { - console.log("Bot is ready!"); -}); - -// Login to Discord -// Don't forget to replace token by your Discord bot token ! -client.login('token'); -``` +[Discord.js](./packages/discordjs/README.md) | [Discord.js-light](./packages/discordjs-light/README.md) | [Eris](./packages/eris/README.md) | [Oceanic.js](./packages/oceanic/README.md) -> For advanced usages and updated docs, please check https://docs.discordanalytics.xyz/get-started/installation \ No newline at end of file +> For advanced usages and updated docs, please check https://discordanalytics.xyz/docs/main/get-started/advanced-usage \ No newline at end of file diff --git a/package.json b/package.json index 2c278d3..1c24ca0 100644 --- a/package.json +++ b/package.json @@ -1,55 +1,30 @@ { "name": "discord-analytics", - "version": "2.5.0", - "description": "A discord bot analytics to send your bot data", - "main": "dist/index.js", + "version": "0.0.0", + "author": "Discord Analytics", + "homepage": "https://discordanalytics.xyz", + "license": "MIT", + "private": true, + "workspaces": [ + "packages/*" + ], "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "init": "tsc --init", - "test:djs": "nodemon src/test/discordjs/index.ts --dev", - "test:eris": "nodemon src/test/eris/index.ts --dev", - "test:oceanic": "nodemon src/test/oceanic/index.ts --dev", - "build": "tsc" + "build": "pnpm -r run build", + "test": "vitest", + "coverage": "vitest run --coverage" }, "repository": { "type": "git", "url": "git+https://github.com/DiscordAnalytics/node-package.git" }, - "keywords": [ - "Javascript", - "Discord", - "Discord.js", - "Discord.js-Light", - "Eris", - "API", - "Analytics", - "Oceanic" - ], - "author": "Discord Analytics", - "license": "MIT", "bugs": { "url": "https://github.com/DiscordAnalytics/node-package/issues" }, - "homepage": "https://discordanalytics.xyz", "devDependencies": { - "@types/node": "^20.17.11", - "@types/node-fetch": "^2.6.12", - "discord.js": "^14.17.2", - "discord.js-light": "^4.10.0", - "dotenv": "^16.4.7", - "eris": "^0.17.2", - "nodemon": "^3.1.9", - "oceanic.js": "^1.11.2", - "ts-node": "^10.9.2", - "typescript": "^5.7.2" - }, - "dependencies": { - "node-fetch": "^2.7.0" - }, - "peerDependencies": { - "discord.js": "14.x", - "discord.js-light": "4.10.0", - "eris": ">=0.17.2", - "oceanic.js": ">= 1.11.0 < 1.12.0" + "@types/node": "^22.15.2", + "@vitest/coverage-v8": "3.1.2", + "dotenv": "^16.5.0", + "typescript": "^5.8.3", + "vitest": "^3.1.2" } } diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..d047a1c --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,24 @@ +# Discord Analytics + +## Overview +This package is a wrapper for the Discord Analytics API. It allows you to track events and send them to the Discord Analytics API. + +## Installation +You can install the package using npm: + +```bash +npm install @discordanalytics/core +# or +yarn add @discordanalytics/core +# or +pnpm add @discordanalytics/core +``` + +### Usage +Discord Analytics core package is used by other packages like `@discordanalytics/discordjs`, `@discordanalytics/discordjs-light`, `@discordanalytics/eris`, and `@discordanalytics/oceanic`. You don't need to use this package directly unless you want to use the core features without any wrapper. + +## API +### `AnalyticsBase` +The main class of the package. It is used to track events and send them to the Discord Analytics API. +### `CustomEvent` +A class that represents a custom event. It is used to track custom events and send them to the Discord Analytics API. diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..d3db009 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,27 @@ +{ + "name": "@discordanalytics/core", + "version": "3.5.0", + "description": "Core package to work with Discord Analytics", + "main": "dist/index.js", + "author": "Discord Analytics", + "homepage": "https://discordanalytics.xyz", + "readme": "README.md", + "repository": { + "type": "git", + "url": "git+https://github.com/DiscordAnalytics/node-package.git" + }, + "bugs": { + "url": "https://github.com/DiscordAnalytics/node-package/issues" + }, + "scripts": { + "build": "tsc" + }, + "dependencies": { + "node-fetch": "^2.7.0" + }, + "devDependencies": { + "@types/node": "*", + "@types/node-fetch": "2.6.12", + "typescript": "*" + } +} \ No newline at end of file diff --git a/packages/core/pnpm-lock.yaml b/packages/core/pnpm-lock.yaml new file mode 100644 index 0000000..1bb409b --- /dev/null +++ b/packages/core/pnpm-lock.yaml @@ -0,0 +1,244 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + node-fetch: + specifier: ^2.7.0 + version: 2.7.0 + devDependencies: + '@types/node': + specifier: 22.15.2 + version: 22.15.2 + '@types/node-fetch': + specifier: 2.6.12 + version: 2.6.12 + typescript: + specifier: 5.8.3 + version: 5.8.3 + +packages: + + '@types/node-fetch@2.6.12': + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + + '@types/node@22.15.2': + resolution: {integrity: sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + +snapshots: + + '@types/node-fetch@2.6.12': + dependencies: + '@types/node': 22.15.2 + form-data: 4.0.2 + + '@types/node@22.15.2': + dependencies: + undici-types: 6.21.0 + + asynckit@0.4.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + tr46@0.0.3: {} + + typescript@5.8.3: {} + + undici-types@6.21.0: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..262a038 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,302 @@ +import { ApiEndpoints, ErrorCodes, GuildsStatsData, InteractionData, LocaleData, TrackGuildType } from './types'; +import fetch from 'node-fetch'; + +/** + * DiscordAnalytics Base Class + * @class AnalyticsBase + * @description Base class for DiscordAnalytics + * @param {string} api_key The API key for DiscordAnalytics + * @param {boolean} debug Optional flag to enable debug mode /!\ MUST BE USED ONLY FOR DEBUGGING PURPOSES + * @returns {AnalyticsBase} An instance of the AnalyticsBase class + * @example + * const analytics = new AnalyticsBase('YOUR_API_KEY'); + * analytics.sendStats('YOUR_CLIENT_ID', 0, 0); + */ +export class AnalyticsBase { + private readonly _api_key: string; + private readonly _headers: { 'Content-Type': string; Authorization: string; }; + private readonly debug_mode: boolean = false; + public stats_data = { + date: new Date().toISOString().slice(0, 10), + guilds: 0, + users: 0, + interactions: [] as InteractionData[], + locales: [] as LocaleData[], + guildsLocales: [] as LocaleData[], + guildMembers: { + little: 0, + medium: 0, + big: 0, + huge: 0, + }, + guildsStats: [] as GuildsStatsData[], + addedGuilds: 0, + removedGuilds: 0, + users_type: { + admin: 0, + moderator: 0, + new_member: 0, + other: 0, + private_message: 0, + }, + custom_events: {} as Record, + }; + public client_id: string = ''; + + constructor(api_key: string, debug: boolean = false) { + this._api_key = api_key; + this.debug_mode = debug; + this._headers = { + 'Content-Type': 'application/json', + Authorization: `Bot ${this._api_key}`, + }; + } + + public debug(...args: any[]): void { + if (this.debug_mode) console.debug(...args); + } + + public error(content: any, exit: boolean = false): void { + console.error(content) + if (exit) process.exit(1); + } + + /** + * Custom events + * /!\ Advanced users only + * /!\ You need to initialize the class first + * @param event_key The event key to track + * @returns {CustomEvent} The CustomEvent instance + * @example + * const event = analytics.events('my_custom_event'); + * event.increment(1); + * event.decrement(1); + * event.set(10); + */ + public events(event_key: string): CustomEvent { + this.debug(`[DISCORDANALYTICS] Getting event ${event_key}`); + + if (typeof event_key !== 'string') throw new Error(`[DISCORDANALYTICS] ${ErrorCodes.INVALID_VALUE_TYPE}`); + + return new CustomEvent(this, event_key); + } + + public updateOrInsert( + array: T[], + match: (item: T) => boolean, + update: (item: T) => void, + insert: () => T, + ): void { + const item = array.find(match); + if (item) update(item); + else array.push(insert()); + } + + public calculateGuildMembers(guildMembers: number[]): { little: number; medium: number; big: number; huge: number } { + return guildMembers.reduce((acc, count) => { + if (count <= 100) acc.little++; + else if (count <= 500) acc.medium++; + else if (count <= 1500) acc.big++; + else acc.huge++; + return acc; + }, { little: 0, medium: 0, big: 0, huge: 0 }); + } + + /** + * Track guilds + * /!\ Advanced users only + * /!\ You need to initialize the class first + * @param {TrackGuildType} type 'create' if the event is guildCreate and 'delete' if is guildDelete + */ + public trackGuilds(type: TrackGuildType): void { + this.debug(`[DISCORDANALYTICS] trackGuilds(${type}) triggered`); + if (type === 'create') this.stats_data.addedGuilds++; + else this.stats_data.removedGuilds++; + } + + /** + * API call with retries + * @param method The HTTP method to use (GET, POST, PUT, DELETE) + * @param url The URL to call + * @param body The body to send (optional) + * @param max_retries The maximum number of retries (default: 5) + * @param backoff_factor The backoff factor to use (default: 0.5) + * @returns {Promise} The response from the API + */ + public async api_call_with_retries( + method: string, + url: string, + body?: string, + max_retries: number = 5, + backoff_factor: number = 0.5, + ): Promise { + let retries = 0; + let response: fetch.Response; + + while (retries < max_retries) { + try { + response = await fetch(url, { + method, + headers: this._headers, + body, + }); + + if (response.ok) return response; + else if (response.status === 401) return this.error(`[DISCORDANALYTICS] ${ErrorCodes.INVALID_API_TOKEN}`); + else if (response.status === 423) return this.error(`[DISCORDANALYTICS] ${ErrorCodes.SUSPENDED_BOT}`); + else if (response.status === 404 && url.includes('events')) return this.error(`[DISCORDANALYTICS] ${ErrorCodes.INVALID_EVENT_KEY}`, true); + else if (response.status !== 200) return this.error(`[DISCORDANALYTICS] ${ErrorCodes.INVALID_RESPONSE}`); + } catch (error) { + retries++; + const retry_after = Math.pow(2, retries) * backoff_factor; + this.error(`[DISCORDANALYTICS] Error: ${error}. Retrying in ${retry_after} seconds...`); + if (retries >= max_retries) return this.error(`[DISCORDANALYTICS] ${ErrorCodes.MAX_RETRIES_EXCEEDED}`); + await new Promise((resolve) => setTimeout(resolve, retry_after * 1000)); + } + } + } + + /** + * Send stats to the API + * @param client_id The client ID of the bot + * @param guild_count The number of guilds the bot is in (default: 0) + * @param user_count The number of users the bot is in (default: 0) + * @param guild_members The number of members in each guild (optional) + * @returns {Promise} A promise that resolves when the stats are sent + */ + public async sendStats( + client_id: string, + guild_count: number = 0, + user_count: number = 0, + guild_members: number[] = [], + ): Promise { + this.debug('[DISCORDANALYTICS] Sending stats...'); + + const url = ApiEndpoints.EDIT_STATS_URL.replace(':id', client_id); + const body = JSON.stringify(this.stats_data); + + await this.api_call_with_retries('POST', url, body); + + this.debug('[DISCORDANALYTICS] Stats sent to the API', body); + + this.stats_data = { + date: new Date().toISOString().slice(0, 10), + guilds: guild_count, + users: user_count, + interactions: [], + locales: [], + guildsLocales: [], + guildMembers: this.calculateGuildMembers(guild_members), + guildsStats: [], + addedGuilds: 0, + removedGuilds: 0, + users_type: { + admin: 0, + moderator: 0, + new_member: 0, + other: 0, + private_message: 0, + }, + custom_events: this.stats_data.custom_events, + } + } +} + +/** + * CustomEvent class + * @class CustomEvent + * @description Class for custom events + * @param {AnalyticsBase} analytics The AnalyticsBase instance + * @param {string} event_key The event key to track + * @returns {CustomEvent} An instance of the CustomEvent class + * @example + * const event = analytics.events('my_custom_event'); + * event.increment(1); + * event.decrement(1); + * event.set(10); + * event.get(); + */ +export class CustomEvent { + private readonly _analytics: AnalyticsBase; + private readonly _event_key: string; + private _last_action: string; + + constructor(analytics: AnalyticsBase, event_key: string) { + this._analytics = analytics; + this._event_key = event_key; + this._last_action = ""; + + this.ensure(); + } + + private async ensure() { + if (typeof this._analytics.stats_data.custom_events[this._event_key] !== 'number') { + this._analytics.debug(`[DISCORDANALYTICS] Fetching value for event ${this._event_key}`); + const url = ApiEndpoints.EVENT_URL.replace(':id', this._analytics.client_id).replace(':event', this._event_key); + const res = await this._analytics.api_call_with_retries('GET', url); + + if (res instanceof fetch.Response && this._last_action !== 'set') { + const data = await res.json() + this._analytics.stats_data.custom_events[this._event_key] = (this._analytics.stats_data.custom_events[this._event_key] || 0) + (data.today_value || 0) + } + this._analytics.debug(`[DISCORDANALYTICS] Value fetched for event ${this._event_key}`); + } + } + + /** + * Increment the event by a value + * @param value The value to increment the event by (default: 1) + */ + public increment(value: number = 1): void { + this._analytics.debug(`[DISCORDANALYTICS] Incrementing event ${this._event_key} by ${value}`); + + if (typeof value !== 'number') throw new Error(`[DISCORDANALYTICS] ${ErrorCodes.INVALID_VALUE_TYPE}`); + + if (value < 0) throw new Error(`[DISCORDANALYTICS] ${ErrorCodes.INVALID_EVENTS_COUNT}`); + + this._analytics.stats_data.custom_events[this._event_key] = (this._analytics.stats_data.custom_events[this._event_key] || 0) + value; + this._last_action = 'increment'; + } + + /** + * Decrement the event by a value + * @param value The value to decrement the event by (default: 1) + */ + public decrement(value: number = 1): void { + this._analytics.debug(`[DISCORDANALYTICS] Decrementing event ${this._event_key} by ${value}`); + + if (typeof value !== 'number') throw new Error(`[DISCORDANALYTICS] ${ErrorCodes.INVALID_VALUE_TYPE}`); + + if (value < 0 || this.get() - value < 0) throw new Error(`[DISCORDANALYTICS] ${ErrorCodes.INVALID_EVENTS_COUNT}`); + + this._analytics.stats_data.custom_events[this._event_key] -= value; + this._last_action = 'decrement'; + } + + /** + * Set the event to a value + * @param value The value to set the event to + */ + public set(value: number): void { + this._analytics.debug(`[DISCORDANALYTICS] Setting event ${this._event_key} to ${value}`); + + if (typeof value !== 'number') throw new Error(`[DISCORDANALYTICS] ${ErrorCodes.INVALID_VALUE_TYPE}`); + + if (value < 0) throw new Error(`[DISCORDANALYTICS] ${ErrorCodes.INVALID_EVENTS_COUNT}`); + + this._analytics.stats_data.custom_events[this._event_key] = value; + this._last_action = 'set'; + } + + /** + * Get the event value + * @returns {number} The event value + */ + public get(): number { + this._analytics.debug(`[DISCORDANALYTICS] Getting event ${this._event_key}`); + + return this._analytics.stats_data.custom_events[this._event_key]; + } +} + +export * from './types'; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts new file mode 100644 index 0000000..180660c --- /dev/null +++ b/packages/core/src/types.ts @@ -0,0 +1,65 @@ +export const api_url = 'https://discordanalytics.xyz/api'; +export const ApiEndpoints = { + EDIT_SETTINGS_URL: `${api_url}/bots/:id`, + EDIT_STATS_URL: `${api_url}/bots/:id/stats`, + EVENT_URL: `${api_url}/bots/:id/events/:event`, +} + +export const ErrorCodes = { + INVALID_CLIENT_TYPE: 'Invalid client type, please use a valid client.', + CLIENT_NOT_READY: 'Client is not ready, please start the client first.', + INVALID_RESPONSE: 'Invalid response from the API, please try again later.', + INVALID_API_TOKEN: 'Invalid API token, please get one at ' + api_url.split('/api')[0] + ' and try again.', + DATA_NOT_SENT: 'Data cannot be sent to the API, I will try again in a minute.', + SUSPENDED_BOT: 'Your bot has been suspended, please check your mailbox for more information.', + INSTANCE_NOT_INITIALIZED: 'It seem that you didn\'t initialize your instance. Please check the docs for more informations.', + INVALID_EVENTS_COUNT: 'invalid events count', + INVALID_VALUE_TYPE: 'invalid value type', + INVALID_EVENT_KEY: 'invalid event key', + MAX_RETRIES_EXCEEDED: 'Max retries exceeded, please check your network connection or the API status.', +} + +export type Locale = 'id' | 'en-US' | 'en-GB' | 'bg' | 'zh-CN' | 'zh-TW' | 'hr' | 'cs' | 'da' | 'nl' | 'fi' | 'fr' | 'de' | 'el' | 'hi' | 'hu' | 'it' | 'ja' | 'ko' | 'lt' | 'no' | 'pl' | 'pt-BR' | 'ro' | 'ru' | 'es-ES' | 'sv-SE' | 'th' | 'tr' | 'uk' | 'vi'; + +export enum InteractionType { + Ping = 1, + ApplicationCommand, + MessageComponent, + ApplicationCommandAutocomplete, + ModalSubmit, +} + +export enum ApplicationCommandType { + ChatInputCommand = 1, + UserCommand, + MessageCommand, +} + +export interface InteractionData { + name: string; + number: number; + type: InteractionType; + command_type?: ApplicationCommandType; +} + +export interface LocaleData { + locale: Locale; + number: number; +} + +export interface GuildsStatsData { + guildId: string; + name: string; + icon: string | null; + members: number; + interactions: number; +} + +export interface AnalyticsOptions { + client: any; + api_key: string; + sharded?: boolean; + debug?: boolean; +} + +export type TrackGuildType = 'create' | 'delete'; diff --git a/packages/core/tests/core.test.ts b/packages/core/tests/core.test.ts new file mode 100644 index 0000000..17841b0 --- /dev/null +++ b/packages/core/tests/core.test.ts @@ -0,0 +1,69 @@ +import { expect, test, vi } from 'vitest'; +import { AnalyticsBase, CustomEvent, ErrorCodes } from '../src'; + +test('should create an Analytics instance', () => { + const instance = new AnalyticsBase('test_api_key', true); + expect(instance).toBeInstanceOf(AnalyticsBase); +}); + +test('debug should log messages when debug is enabled', () => { + const instance = new AnalyticsBase('test_api_key', true); + const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); + instance.debug('Test message'); + expect(consoleSpy).toHaveBeenCalledWith('Test message'); + consoleSpy.mockRestore(); +}); + +test('error should throw an error when debug is enabled', () => { + const instance = new AnalyticsBase('test_api_key', true); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); + instance.error('Test error'); + expect(consoleSpy).toHaveBeenCalledWith('Test error'); + consoleSpy.mockRestore(); +}); + +test('should return a CustomEvent instance', () => { + const instance = new AnalyticsBase('test_api_key', true); + const event = instance.events('my_custom_event'); + expect(event).toBeInstanceOf(CustomEvent); +}); + +test('should update the event value', () => { + const instance = new AnalyticsBase('test_api_key', true); + const event = instance.events('my_custom_event'); + event.set(42); + event.increment(8); + event.decrement(1); + expect(event.get()).toBe(49); +}); + +test('should throw an error if the event key is not a string', () => { + const instance = new AnalyticsBase('test_api_key', true); + expect(() => instance.events(123 as any)).toThrow(`[DISCORDANALYTICS] ${ErrorCodes.INVALID_VALUE_TYPE}`); +}); + +test('should update or insert an item in the array', () => { + const instance = new AnalyticsBase('test_api_key', true); + const array = [{ id: 1, value: 10 }]; + const update = (item: { value: number }) => { item.value += 5; }; + const insert = () => ({ id: 2, value: 20 }); + + instance.updateOrInsert(array, (item) => item.id === 1, update, insert); + expect(array).toEqual([{ id: 1, value: 15 }]); + + instance.updateOrInsert(array, (item) => item.id === 2, update, insert); + expect(array).toEqual([{ id: 1, value: 15 }, { id: 2, value: 20 }]); +}); + +test('should return an object of guild sizes', () => { + const instance = new AnalyticsBase('test_api_key', true); + const guilds_member_count = [50, 200, 1000, 3000, 150]; + const result = instance.calculateGuildMembers(guilds_member_count); + expect(result).toEqual({ little: 1, medium: 2, big: 1, huge: 1 }); +}); + +test('should update the added and removed guilds', () => { + const instance = new AnalyticsBase('test_api_key', true); + expect(instance.trackGuilds('create')); + expect(instance.trackGuilds('delete')); +}); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..16894b3 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + }, + "include": ["src"] +} \ No newline at end of file diff --git a/packages/discordjs-light/README.md b/packages/discordjs-light/README.md new file mode 100644 index 0000000..79bdcae --- /dev/null +++ b/packages/discordjs-light/README.md @@ -0,0 +1,50 @@ +# DiscordAnalytics for Discord.js-light + +## Overview +This package is a wrapper for the Discord Analytics API made for Discord.js-light. It allows you to track events and send them to the Discord Analytics API. + +## Installation +```bash +npm install @discordanalytics/discordjs-light +# or +yarn add @discordanalytics/discordjs-light +# or +pnpm add @discordanalytics/discordjs-light +``` + +## Usage +> **Note:** To use Discord Analytics, you need to have an API key. Check the docs for more informations : https://discordanalytics.xyz/docs/main/get-started/bot-registration + +### Example +```js +// Import Discord.js's client +const { Client } = require("discord.js-light") +// import discord-analytics +const { default: DiscordAnalytics } = require("@discordanalytics/discordjs-light") + +// Create Discord client +const client = new Client({ + intents: ["GUILD"] // This intent is required +}); + +// Create Discord Analytics instance +// Don't forget to replace YOUR_API_KEY by your Discord Analytics key ! +const analytics = new DiscordAnalytics({ + client: client, + api_key: 'YOUR_API_KEY', + sharded: false // Set it to true if your bot use shards +}); + +// start tracking selected events + +// When Discord client is ready +client.on('ready', () => { + console.log("Bot is ready!"); + analytics.init(); // Initialize the analytics + analytics.trackEvents(); // Start tracking events +}); + +// Login to Discord +// Don't forget to replace YOUR_BOT_TOKEN by your Discord bot token ! +client.login('YOUR_BOT_TOKEN'); +``` diff --git a/packages/discordjs-light/examples/index.ts b/packages/discordjs-light/examples/index.ts new file mode 100644 index 0000000..eb2f0b1 --- /dev/null +++ b/packages/discordjs-light/examples/index.ts @@ -0,0 +1,140 @@ +import { InteractionType } from '@discordanalytics/core'; +import DiscordAnalytics from '../src/index'; // Replace it with @discordanalytics/discordjs-light in your project +import { Client, Message, MessageActionRow, Modal, TextInputComponent } from 'discord.js-light'; +import 'dotenv/config'; + +const client = new Client({ + intents: ['GUILDS'], +}); + +client.on('error', (error) => { + console.error('Client error:', error); +}); + +const analytics = new DiscordAnalytics({ + client, + api_key: process.env.DISCORD_ANALYTICS_API_KEY as string, + sharded: false, // Set to true if using ShardingManager + debug: true, +}); + +client.on('ready', async () => { + client.application?.commands.set([{ + name: 'test', + description: 'Send a test message', + dmPermission: true, + options: [{ + name: 'test', + description: 'Test option', + type: 3, + required: false, + autocomplete: true, + }] + }]); + + console.log('Client is ready!'); + + await analytics.init(); +}); + +client.on('interactionCreate', async (interaction) => { + await analytics.trackInteractions(interaction, (interaction) => { + if (interaction.type === InteractionType.ApplicationCommand) + return interaction.commandName; + else if ( + interaction.type === InteractionType.MessageComponent + || interaction.type === InteractionType.ModalSubmit + ) { + if ((/\d{17,19}/g).test(interaction.customId)) return 'this_awesome_button'; + else return interaction.customId; + } + return ''; + }); + + if (interaction.isCommand()) { + if (interaction.commandName === 'test') { + const option = interaction.options.getString('test'); + + if (option === 'button') interaction.reply({ + content: 'Test button', + components: [{ + type: 1, + components: [{ + type: 2, + style: 1, + label: 'Test button', + customId: `button_${interaction.user.id}`, + }], + }], + }); + + else if (option === 'select') interaction.reply({ + content: 'Test select', + components: [{ + type: 1, + components: [{ + type: 3, + customId: 'test_select', + options: [{ + label: 'Test select', + value: 'test_select', + }], + }], + }], + }); + + else if (option === 'modal') { + const modal = new Modal() + .setCustomId(`my_modal`) + .setTitle('My modal'); + + const favoriteColorInput = new TextInputComponent() + .setCustomId('favorite_color_input') + .setLabel('What\'s your favorite color?') + .setStyle('SHORT'); + + const actionRow = new MessageActionRow().addComponents(favoriteColorInput); + modal.addComponents(actionRow); + + await interaction.showModal(modal); + } + + else interaction.reply({ + content: 'This is a test message', + ephemeral: true, + }); + } + } + + if (interaction.isAutocomplete()) { + const focusedValue = interaction.options.getFocused(); + const choices = ['button', 'select', 'modal']; + const filtered = choices.filter((choice) => choice.startsWith(focusedValue)); + await interaction.respond( + filtered.map((choice) => ({ name: choice, value: choice })), + ); + } + + if (interaction.isButton()) interaction.reply({ + content: `You clicked the button with ID: ${interaction.customId}`, + ephemeral: true, + }); + + if (interaction.isSelectMenu()) await (interaction.message as Message).edit({ + content: `You selected: ${interaction.values[0]}`, + components: [], + }); + + if (interaction.isModalSubmit()) { + const favoriteColor = interaction.fields.getTextInputValue('favorite_color_input'); + await interaction.reply({ + content: `Your favorite color is: ${favoriteColor}`, + ephemeral: true, + }); + } +}); + +client.on('guildCreate', (_) => analytics.trackGuilds('create')); +client.on('guildDelete', (_) => analytics.trackGuilds('delete')); + +client.login(process.env.DISCORD_TOKEN); diff --git a/packages/discordjs-light/examples/sharded.ts b/packages/discordjs-light/examples/sharded.ts new file mode 100644 index 0000000..927687f --- /dev/null +++ b/packages/discordjs-light/examples/sharded.ts @@ -0,0 +1,8 @@ +import { ShardingManager } from 'discord.js-light'; +import 'dotenv/config'; + +const manager = new ShardingManager('./index.ts', { token: process.env.DISCORD_TOKEN}); + +manager.on('shardCreate', (shard) => console.log(`Launched shard ${shard.id}`)); + +manager.spawn(); diff --git a/packages/discordjs-light/package.json b/packages/discordjs-light/package.json new file mode 100644 index 0000000..cd68030 --- /dev/null +++ b/packages/discordjs-light/package.json @@ -0,0 +1,30 @@ +{ + "name": "@discordanalytics/discordjs-light", + "version": "3.5.0", + "description": "Discord.js light package for working with Discord Analytics", + "main": "dist/index.js", + "author": "Discord Analytics", + "homepage": "https://discordanalytics.xyz", + "readme": "README.md", + "repository": { + "type": "git", + "url": "git+https://github.com/DiscordAnalytics/node-package.git" + }, + "bugs": { + "url": "https://github.com/DiscordAnalytics/node-package/issues" + }, + "scripts": { + "prebuild": "pnpm --filter @discordanalytics/core build", + "build": "tsc" + }, + "dependencies": { + "@discordanalytics/core": "workspace:*" + }, + "devDependencies": { + "discord.js-light": "^4.10.0", + "typescript": "*" + }, + "peerDependencies": { + "discord.js-light": "4.x" + } +} \ No newline at end of file diff --git a/packages/discordjs-light/src/index.ts b/packages/discordjs-light/src/index.ts new file mode 100644 index 0000000..8d0e1eb --- /dev/null +++ b/packages/discordjs-light/src/index.ts @@ -0,0 +1,211 @@ +import { AnalyticsBase, ApiEndpoints, ApplicationCommandType, AnalyticsOptions, ErrorCodes } from '@discordanalytics/core'; +import npmPackageData from '../package.json'; + +/** + * @class DiscordAnalytics + * @description The Discord.js-light class for the DiscordAnalytics library. + * @param {AnalyticsOptions} options - Configuration options. + * @property {any} options.client The Discord.js-light client to track events for. + * @property {string} options.api_key The API token for DiscordAnalytics. + * @property {boolean} options.sharded Whether the Discord.js-light client is sharded. + * @property {boolean} options.debug Enable or not the debug mode /!\ MUST BE USED ONLY FOR DEVELOPMENT PURPOSES /!\ + * @example + * const { default: DiscordAnalytics } = require('discord-analytics/discord.js-light'); + * const { Client, IntentsBitField } = require('discord.js-light'); + * const client = new Client({ + * intents: ['GUILDS'] + * }) + * client.on('ready', () => { + * const analytics = new DiscordAnalytics({ + * client: client, + * api_key: 'YOUR_API_TOKEN' + * }); + * analytics.init(); + * analytics.trackEvents(); + * }); + * client.login('YOUR_BOT_TOKEN'); + * @link https://discordanalytics.xyz/docs/main/get-started/installation/discord.js Check docs for more informations about advanced usages + */ +export default class DiscordAnalytics extends AnalyticsBase { + private readonly _client: any; + private readonly _sharded: boolean = false; + private _isReady: boolean = false; + + constructor(options: AnalyticsOptions) { + super(options.api_key, options.debug); + this._client = options.client; + this._sharded = options.sharded || false; + } + + /** + * Initialize DiscordAnalytics on your bot + * /!\ Advanced users only + * /!\ Required to use DiscordAnalytics + * /!\ Must be used when the client is ready (recommended to use in ready event to prevent problems) + */ + public async init(): Promise { + if (process.env.NODE_ENV !== 'production') return console.log("[DISCORDANALYTICS] NODE_ENV != 'production', initialization skipped") + + const url = ApiEndpoints.EDIT_SETTINGS_URL.replace(':id', this._client.user.id); + const body = JSON.stringify({ + username: this._client.user.username, + avatar: this._client.user.avatar, + framework: 'discord.js-light', + version: npmPackageData.version, + team: this._client.application.owner + ? this._client.application.owner.hasOwnProperty('members') + ? this._client.application.owner.members.map((member: any) => member.user.id) + : [this._client.application.owner.id] + : [], + }); + await this.api_call_with_retries('PATCH', url, body); + + this.debug('[DISCORDANALYTICS] Instance successfully initialized'); + this.client_id = this._client.user.id; + this._isReady = true; + + const fast_mode = process.argv[2] === '--fast'; + this.debug(`[DISCORDANALYTICS] Fast mode is ${fast_mode ? 'enabled' : 'disabled'}. Stats will be sent every ${fast_mode ? '30s' : '5min'}.`); + + setInterval(async () => { + this.debug('[DISCORDANALYTICS] Sending stats...'); + + const guildCount = this._sharded + ? ((await this._client.shard?.broadcastEval((c: any) => c.guilds.cache.size))?.reduce((a: number, b: number) => a + b, 0) || 0) + : this._client.guilds.cache.size; + + const userCount = this._sharded + ? ((await this._client.shard?.broadcastEval((c: any) => c.guilds.cache.reduce((a: number, g: any) => a + (g.memberCount || 0), 0)))?.reduce((a: number, b: number) => a + b, 0) || 0) + : this._client.guilds.cache.reduce((a: number, g: any) => a + (g.memberCount || 0), 0); + + const guildMembers: number[] = !this._sharded + ? this._client.guilds.cache.map((guild: any) => guild.memberCount) + : ((await this._client.shard?.broadcastEval( + (c: any) => c.guilds.cache.map((guild: any) => guild.memberCount) + ))?.flat() ?? []); + + await this.sendStats(this._client.user.id, guildCount, userCount, guildMembers); + }, fast_mode ? 30000 : 300000); + } + + /** + * Track interactions + * /!\ Advanced users only + * /!\ You need to initialize the class first + * @param interaction BaseInteraction class and its extensions only + * @param interactionNameResolver A function that will resolve the name of the interaction + */ + public async trackInteractions(interaction: any, interactionNameResolver?: (interaction: any) => string): Promise { + this.debug(`[DISCORDANALYTICS] trackInteractions(${interaction.type}) triggered`); + if (!this._isReady) return this.error(ErrorCodes.INSTANCE_NOT_INITIALIZED); + + this.updateOrInsert( + this.stats_data.guildsLocales, + (x) => x.locale === interaction.guild?.preferredLocale, + (x) => x.number++, + () => ({ locale: interaction.guild?.preferredLocale, number: 1 }), + ); + + this.updateOrInsert( + this.stats_data.locales, + (x) => x.locale === interaction.locale, + (x) => x.number++, + () => ({ locale: interaction.locale, number: 1 }), + ); + + if (interaction.isCommand()) { + const commandType = interaction.command + ? interaction.command.type === 'USER' + ? ApplicationCommandType.UserCommand + : interaction.command.type === 'MESSAGE' + ? ApplicationCommandType.MessageCommand + : ApplicationCommandType.ChatInputCommand + : ApplicationCommandType.ChatInputCommand; + const commandName = interactionNameResolver + ? interactionNameResolver(interaction) + : interaction.commandName; + this.updateOrInsert( + this.stats_data.interactions, + (x) => + x.name === commandName + && x.type === interaction.type + && x.command_type === commandType, + (x) => x.number++, + () => ({ + name: commandName, + number: 1, + type: interaction.type, + command_type: commandType, + }), + ); + } else if (interaction.isMessageComponent() || interaction.isModalSubmit()) { + const interactionName = interactionNameResolver + ? interactionNameResolver(interaction) + : interaction.customId; + this.updateOrInsert( + this.stats_data.interactions, + (x) => x.name === interactionName && x.type === interaction.type, + (x) => x.number++, + () => ({ + name: interactionName, + number: 1, + type: interaction.type, + }), + ); + } + + this.updateOrInsert( + this.stats_data.guildsStats, + (x) => x.guildId === (interaction.guild ? interaction.guild.id : 'dm'), + (x) => x.interactions++, + () => ({ + guildId: interaction.guild ? interaction.guild.id : 'dm', + name: interaction.guild ? interaction.guild.name : 'DM', + icon: interaction.guild && interaction.guild.icon ? interaction.guild.icon : undefined, + interactions: 1, + members: interaction.guild ? interaction.guild.memberCount : 0, + }), + ); + + const oneWeekAgo = new Date(); + oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); + + if (!interaction.inGuild()) ++this.stats_data.users_type.private_message; + else if ( + interaction.member + && interaction.member.permissions + && interaction.member.permissions.has(8n) + || interaction.member.permissions.has(32n) + ) ++this.stats_data.users_type.admin; + else if ( + interaction.member + && interaction.member.permissions + && interaction.member.permissions.has(8192n) + || interaction.member.permissions.has(2n) + || interaction.member.permissions.has(4n) + || interaction.member.permissions.has(4194304n) + || interaction.member.permissions.has(8388608n) + || interaction.member.permissions.has(16777216n) + || interaction.member.permissions.has(1099511627776n) + ) ++this.stats_data.users_type.moderator; + else if ( + interaction.member + && interaction.member.joinedAt + && interaction.member.joinedAt > oneWeekAgo + ) ++this.stats_data.users_type.new_member; + } + + /** + * Let DiscordAnalytics declare the events necessary for its operation. + * /!\ Not recommended for big bots + * /!\ Not compatible with other functions + * @param interactionNameResolver A function that will resolve the name of the interaction + */ + public trackEvents(interactionNameResolver?: (interaction: any) => string): void { + this.debug('[DISCORDANALYTICS] trackEvents() triggered'); + if (!this._isReady) return this.error(ErrorCodes.INSTANCE_NOT_INITIALIZED); + this._client.on('interactionCreate', async (interaction: any) => await this.trackInteractions(interaction, interactionNameResolver)); + this._client.on('guildCreate', (guild: any) => this.trackGuilds('create')); + this._client.on('guildDelete', (guild: any) => this.trackGuilds('delete')); + } +} diff --git a/packages/discordjs-light/tsconfig.json b/packages/discordjs-light/tsconfig.json new file mode 100644 index 0000000..16894b3 --- /dev/null +++ b/packages/discordjs-light/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + }, + "include": ["src"] +} \ No newline at end of file diff --git a/packages/discordjs/README.md b/packages/discordjs/README.md new file mode 100644 index 0000000..9e26522 --- /dev/null +++ b/packages/discordjs/README.md @@ -0,0 +1,50 @@ +# DiscordAnalytics for Discord.js + +## Overview +This package is a wrapper for the Discord Analytics API made for Discord.js. It allows you to track events and send them to the Discord Analytics API. + +## Installation +```bash +npm install @discordanalytics/discordjs +# or +yarn add @discordanalytics/discordjs +# or +pnpm add @discordanalytics/discordjs +``` + +## Usage +> **Note:** To use Discord Analytics, you need to have an API key. Check the docs for more informations : https://discordanalytics.xyz/docs/main/get-started/bot-registration + +### Example +```js +// Import Discord.js's client and intents +const { Client, IntentsBitField } = require("discord.js") +// import discord-analytics +const { default: DiscordAnalytics } = require("@discordanalytics/discordjs") + +// Create Discord client +const client = new Client({ + intents: [IntentsBitField.Flags.Guilds] // This intent is required +}); + +// Create Discord Analytics instance +// Don't forget to replace YOUR_API_KEY by your Discord Analytics key ! +const analytics = new DiscordAnalytics({ + client: client, + api_key: 'YOUR_API_KEY', + sharded: false // Set it to true if your bot use shards +}); + +// start tracking selected events + +// When Discord client is ready +client.on('ready', () => { + console.log("Bot is ready!"); + analytics.init(); // Initialize the analytics + analytics.trackEvents(); // Start tracking events +}); + +// Login to Discord +// Don't forget to replace YOUR_BOT_TOKEN by your Discord bot token ! +client.login('YOUR_BOT_TOKEN'); +``` diff --git a/packages/discordjs/examples/index.ts b/packages/discordjs/examples/index.ts new file mode 100644 index 0000000..e316a1a --- /dev/null +++ b/packages/discordjs/examples/index.ts @@ -0,0 +1,152 @@ +import DiscordAnalytics from '../src/index'; // Replace it with @discordanalytics/discordjs in your project +import { + ActionRowBuilder, Client, IntentsBitField, InteractionType, ModalBuilder, + TextInputBuilder, TextInputStyle +} from 'discord.js'; +import 'dotenv/config'; + +const client = new Client({ + intents: [ + IntentsBitField.Flags.Guilds, + IntentsBitField.Flags.GuildMessageReactions, + IntentsBitField.Flags.GuildMessages + ], +}); + +client.on('error', (error) => { + console.error('Client error:', error); +}); + +const analytics = new DiscordAnalytics({ + client, + api_key: process.env.DISCORD_ANALYTICS_API_KEY as string, + sharded: false, // Set to true if using ShardingManager + debug: true, +}); + +client.on('ready', async () => { + client.application?.commands.set([{ + name: 'test', + description: 'Send a test message', + dmPermission: true, + options: [{ + name: 'test', + description: 'Test option', + type: 3, + required: false, + autocomplete: true, + }] + }]); + + console.log('Client is ready!'); + + await analytics.init(); +}); + +client.on('interactionCreate', async (interaction) => { + await analytics.trackInteractions(interaction, (interaction) => { + if (interaction.type === InteractionType.ApplicationCommand) + return interaction.commandName; + else if ( + interaction.type === InteractionType.MessageComponent + || interaction.type === InteractionType.ModalSubmit + ) { + if ((/\d{17,19}/g).test(interaction.customId)) return 'this_awesome_button'; + else return interaction.customId; + } + return ''; + }); + + if (interaction.isChatInputCommand()) { + if (interaction.commandName === 'test') { + const option = interaction.options.getString('test'); + + if (option === 'button') interaction.reply({ + content: 'Test button', + components: [{ + type: 1, + components: [{ + type: 2, + style: 1, + label: 'Test button', + customId: `button_${interaction.user.id}`, + }], + }], + }); + + else if (option === 'select') interaction.reply({ + content: 'Test select', + components: [{ + type: 1, + components: [{ + type: 3, + customId: 'test_select', + options: [{ + label: 'Test select', + value: 'test_select', + }], + }], + }], + }); + + else if (option === 'modal') { + const modal = new ModalBuilder() + .setCustomId(`my_modal`) + .setTitle('My modal'); + + const favoriteColorInput = new TextInputBuilder() + .setCustomId('favorite_color_input') + .setLabel('What\'s your favorite color?') + .setStyle(TextInputStyle.Short); + + const actionRow = new ActionRowBuilder().addComponents(favoriteColorInput); + modal.addComponents(actionRow); + + await interaction.showModal(modal); + } + + else interaction.reply({ + content: 'This is a test message', + ephemeral: true, + }); + } + } + + if (interaction.isAutocomplete()) { + const focusedValue = interaction.options.getFocused(); + const choices = ['button', 'select', 'modal']; + const filtered = choices.filter((choice) => choice.startsWith(focusedValue)); + await interaction.respond( + filtered.map((choice) => ({ name: choice, value: choice })), + ); + } + + if (interaction.isButton()) interaction.reply({ + content: `You clicked the button with ID: ${interaction.customId}`, + ephemeral: true, + }); + + if (interaction.isStringSelectMenu()) await interaction.message.edit({ + content: `You selected: ${interaction.values[0]}`, + components: [], + }); + + if (interaction.isModalSubmit()) { + const favoriteColor = interaction.fields.getTextInputValue('favorite_color_input'); + await interaction.reply({ + content: `Your favorite color is: ${favoriteColor}`, + ephemeral: true, + }); + } +}); + +client.on('guildCreate', (_) => analytics.trackGuilds('create')); +client.on('guildDelete', (_) => analytics.trackGuilds('delete')); + +client.on('messageReactionAdd', (reaction) => { + if (reaction.emoji.name === '❤️') { + analytics.events('heart_reaction').increment() + } +}) + +client.login(process.env.DISCORD_TOKEN); diff --git a/packages/discordjs/examples/sharded.ts b/packages/discordjs/examples/sharded.ts new file mode 100644 index 0000000..06f61d5 --- /dev/null +++ b/packages/discordjs/examples/sharded.ts @@ -0,0 +1,8 @@ +import { ShardingManager } from 'discord.js'; +import 'dotenv/config'; + +const manager = new ShardingManager('./index.ts', { token: process.env.DISCORD_TOKEN}); + +manager.on('shardCreate', (shard) => console.log(`Launched shard ${shard.id}`)); + +manager.spawn(); diff --git a/packages/discordjs/package.json b/packages/discordjs/package.json new file mode 100644 index 0000000..9e63571 --- /dev/null +++ b/packages/discordjs/package.json @@ -0,0 +1,30 @@ +{ + "name": "@discordanalytics/discordjs", + "version": "3.5.0", + "description": "Discord.js package for working with Discord Analytics", + "main": "dist/index.js", + "author": "Discord Analytics", + "homepage": "https://discordanalytics.xyz", + "readme": "README.md", + "repository": { + "type": "git", + "url": "git+https://github.com/DiscordAnalytics/node-package.git" + }, + "bugs": { + "url": "https://github.com/DiscordAnalytics/node-package/issues" + }, + "scripts": { + "prebuild": "pnpm --filter @discordanalytics/core build", + "build": "tsc" + }, + "dependencies": { + "@discordanalytics/core": "workspace:*" + }, + "devDependencies": { + "discord.js": "14.18.0", + "typescript": "*" + }, + "peerDependencies": { + "discord.js": "14.x" + } +} \ No newline at end of file diff --git a/packages/discordjs/src/index.ts b/packages/discordjs/src/index.ts new file mode 100644 index 0000000..6eda221 --- /dev/null +++ b/packages/discordjs/src/index.ts @@ -0,0 +1,208 @@ +import { AnalyticsBase, ApiEndpoints, ApplicationCommandType, AnalyticsOptions, ErrorCodes, InteractionType } from '@discordanalytics/core'; +import npmPackageData from '../package.json'; + +/** + * @class DiscordAnalytics + * @description The Discord.js class for the DiscordAnalytics library. + * @param {AnalyticsOptions} options Configuration options. + * @property {any} options.client The Discord.js client to track events for. + * @property {string} options.api_key The API token for DiscordAnalytics. + * @property {boolean} options.sharded Whether the Discord.js client is sharded. + * @property {boolean} options.debug Enable or not the debug mode /!\ MUST BE USED ONLY FOR DEVELOPMENT PURPOSES /!\ + * @example + * const { default: DiscordAnalytics } = require('discord-analytics/discordjs'); + * const { Client, IntentsBitField } = require('discord.js'); + * const client = new Client({ + * intents: [IntentsBitField.Flags.Guilds] + * }) + * client.on('ready', () => { + * const analytics = new DiscordAnalytics({ + * client: client, + * api_key: 'YOUR_API_TOKEN' + * }); + * analytics.init(); + * analytics.trackEvents(); + * }); + * client.login('YOUR_BOT_TOKEN'); + * @link https://discordanalytics.xyz/docs/main/get-started/installation/discord.js Check docs for more informations about advanced usages + */ +export default class DiscordAnalytics extends AnalyticsBase { + private readonly _client: any; + private readonly _sharded: boolean = false; + private _isReady: boolean = false; + + constructor(options: AnalyticsOptions) { + super(options.api_key, options.debug); + this._client = options.client; + this._sharded = options.sharded || false; + } + + /** + * Initialize DiscordAnalytics on your bot + * /!\ Advanced users only + * /!\ Required to use DiscordAnalytics + * /!\ Must be used when the client is ready (recommended to use in ready event to prevent problems) + */ + public async init(): Promise { + if (process.env.NODE_ENV !== 'production') return console.log("[DISCORDANALYTICS] NODE_ENV != 'production', initialization skipped") + + const url = ApiEndpoints.EDIT_SETTINGS_URL.replace(':id', this._client.user.id); + const body = JSON.stringify({ + username: this._client.user.username, + avatar: this._client.user.avatar, + framework: 'discord.js', + version: npmPackageData.version, + team: this._client.application.owner + ? this._client.application.owner.hasOwnProperty('members') + ? this._client.application.owner.members.map((member: any) => member.user.id) + : [this._client.application.owner.id] + : [], + }); + + await this.api_call_with_retries('PATCH', url, body); + + this.debug('[DISCORDANALYTICS] Instance successfully initialized'); + this.client_id = this._client.user.id; + this._isReady = true; + + const fast_mode = process.argv[2] === '--fast'; + this.debug(`[DISCORDANALYTICS] Fast mode is ${fast_mode ? 'enabled' : 'disabled'}. Stats will be sent every ${fast_mode ? '30s' : '5min'}.`); + + setInterval(async () => { + this.debug('[DISCORDANALYTICS] Sending stats...'); + + const guildCount = this._sharded + ? ((await this._client.shard?.broadcastEval((c: any) => c.guilds.cache.size))?.reduce((a: number, b: number) => a + b, 0) || 0) + : this._client.guilds.cache.size; + + const userCount = this._sharded + ? ((await this._client.shard?.broadcastEval((c: any) => c.guilds.cache.reduce((a: number, g: any) => a + (g.memberCount || 0), 0)))?.reduce((a: number, b: number) => a + b, 0) || 0) + : this._client.guilds.cache.reduce((a: number, g: any) => a + (g.memberCount || 0), 0); + + const guildMembers: number[] = !this._sharded + ? this._client.guilds.cache.map((guild: any) => guild.memberCount) + : ((await this._client.shard?.broadcastEval( + (c: any) => c.guilds.cache.map((guild: any) => guild.memberCount) + ))?.flat() ?? []); + + await this.sendStats(this._client.user.id, guildCount, userCount, guildMembers); + }, fast_mode ? 30000 : 300000); + } + + /** + * Track interactions + * /!\ Advanced users only + * /!\ You need to initialize the class first + * @param interaction BaseInteraction class and its extensions only + * @param interactionNameResolver A function that will resolve the name of the interaction + */ + public async trackInteractions(interaction: any, interactionNameResolver?: (interaction: any) => string): Promise { + this.debug(`[DISCORDANALYTICS] trackInteractions(${interaction.type}) triggered`); + if (!this._isReady) return this.error(ErrorCodes.INSTANCE_NOT_INITIALIZED); + + this.updateOrInsert( + this.stats_data.guildsLocales, + (x) => x.locale === interaction.guild?.preferredLocale, + (x) => x.number++, + () => ({ locale: interaction.guild?.preferredLocale, number: 1 }) + ); + + this.updateOrInsert( + this.stats_data.locales, + (x) => x.locale === interaction.locale, + (x) => x.number++, + () => ({ locale: interaction.locale, number: 1 }) + ); + + if (interaction.type === InteractionType.ApplicationCommand) { + const commandType = interaction.command + ? interaction.command.type + : ApplicationCommandType.ChatInputCommand; + const commandName = interactionNameResolver + ? interactionNameResolver(interaction) + : interaction.commandName; + this.updateOrInsert( + this.stats_data.interactions, + (x) => x.name === commandName + && x.type === interaction.type + && x.command_type === commandType, + (x) => x.number++, + () => ({ + name: commandName, + number: 1, + type: interaction.type, + command_type: commandType, + }), + ); + } else if (interaction.type === InteractionType.MessageComponent || interaction.type === InteractionType.ModalSubmit) { + const interactionName = interactionNameResolver + ? interactionNameResolver(interaction) + : interaction.customId; + this.updateOrInsert( + this.stats_data.interactions, + (x) => x.name === interactionName && x.type === interaction.type, + (x) => x.number++, + () => ({ + name: interactionName, + number: 1, + type: interaction.type, + }), + ); + } + + this.updateOrInsert( + this.stats_data.guildsStats, + (x) => x.guildId === (interaction.guild ? interaction.guild.id : 'dm'), + (x) => x.interactions++, + () => ({ + guildId: interaction.guild ? interaction.guild.id : 'dm', + name: interaction.guild ? interaction.guild.name : 'DM', + icon: interaction.guild && interaction.guild.icon ? interaction.guild.icon : undefined, + interactions: 1, + members: interaction.guild ? interaction.guild.memberCount : 0, + }), + ); + + const oneWeekAgo = new Date(); + oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); + + if (!interaction.inGuild()) ++this.stats_data.users_type.private_message; + else if ( + interaction.member + && interaction.member.permissions + && interaction.member.permissions.has(8n) + || interaction.member.permissions.has(32n) + ) ++this.stats_data.users_type.admin; + else if ( + interaction.member + && interaction.member.permissions + && interaction.member.permissions.has(8192n) + || interaction.member.permissions.has(2n) + || interaction.member.permissions.has(4n) + || interaction.member.permissions.has(4194304n) + || interaction.member.permissions.has(8388608n) + || interaction.member.permissions.has(16777216n) + || interaction.member.permissions.has(1099511627776n) + ) ++this.stats_data.users_type.moderator; + else if ( + interaction.member + && interaction.member.joinedAt + && interaction.member.joinedAt > oneWeekAgo + ) ++this.stats_data.users_type.new_member; + } + + /** + * Let DiscordAnalytics declare the events necessary for its operation. + * /!\ Not recommended for big bots + * /!\ Not compatible with other functions + * @param interactionNameResolver A function that will resolve the name of the interaction + */ + public trackEvents(interactionNameResolver?: (interaction: any) => string): void { + this.debug('[DISCORDANALYTICS] trackEvents() triggered'); + if (!this._isReady) return this.error(ErrorCodes.INSTANCE_NOT_INITIALIZED); + + this._client.on('interactionCreate', async (interaction: any) => await this.trackInteractions(interaction, interactionNameResolver)); + this._client.on('guildCreate', (guild: any) => this.trackGuilds('create')); + this._client.on('guildDelete', (guild: any) => this.trackGuilds('delete')); + } +} diff --git a/packages/discordjs/tsconfig.json b/packages/discordjs/tsconfig.json new file mode 100644 index 0000000..16894b3 --- /dev/null +++ b/packages/discordjs/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + }, + "include": ["src"] +} \ No newline at end of file diff --git a/packages/eris/README.md b/packages/eris/README.md new file mode 100644 index 0000000..357fe2a --- /dev/null +++ b/packages/eris/README.md @@ -0,0 +1,44 @@ +# DiscordAnalytics for Eris + +## Overview +This package is a wrapper for the Discord Analytics API made for Eris. It allows you to track events and send them to the Discord Analytics API. + +## Installation +```bash +npm install @discordanalytics/eris +# or +yarn add @discordanalytics/eris +# or +pnpm add @discordanalytics/eris +``` + +## Usage +> **Note:** To use Discord Analytics, you need to have an API key. Check the docs for more informations : https://discordanalytics.xyz/docs/main/get-started/bot-registration + +### Example +```js +const {Client} = require("eris"); +const {default: DiscordAnalytics} = require("@discordanalytics/eris"); + +// Create Eris client. +// Don't forget to replace YOUR_BOT_TOKEN by your Discord bot token ! +const bot = new Client("YOUR_BOT_TOKEN"); + +bot.on("ready", () => { + // Create Discord Analytics instance + // Don't forget to replace YOUR_API_KEY by your Discord Analytics key ! + const analytics = new DiscordAnalytics({ + client: client, + api_key: 'YOUR_API_KEY' + }); + + // start tracking selected events + analytics.init(); // Initialize the analytics + analytics.trackEvents(); + + console.log("Ready!"); +}); + +// Login to Discord +bot.connect(); +``` diff --git a/packages/eris/examples/index.ts b/packages/eris/examples/index.ts new file mode 100644 index 0000000..e022eda --- /dev/null +++ b/packages/eris/examples/index.ts @@ -0,0 +1,139 @@ +import DiscordAnalytics from '../src/index'; // Replace it with @discordanalytics/discordjs-light in your project +import { Client, CommandInteraction, ComponentInteraction, Constants, ModalSubmitInteraction } from 'eris'; +import 'dotenv/config'; + +const client = new Client(process.env.DISCORD_TOKEN as string, { + intents: ['guilds'], +}); + +client.on('error', (error) => { + console.error('Client error:', error); +}); + +const analytics = new DiscordAnalytics({ + client, + api_key: process.env.DISCORD_ANALYTICS_API_KEY as string, + debug: true, +}); + +client.on('ready', async () => { + client.createCommand({ + name: 'test', + description: 'Send a test message', + dmPermission: true, + type: Constants.ApplicationCommandTypes.CHAT_INPUT, + options: [{ + name: 'test', + description: 'Test option', + type: Constants.ApplicationCommandOptionTypes.STRING, + required: false, + choices: [ + { name: 'button', value: 'button' }, + { name: 'select', value: 'select' }, + { name: 'modal', value: 'modal' }, + ], + }], + }); + + console.log('Client is ready!'); + + await analytics.init(); +}); + +client.on('interactionCreate', async (interaction) => { + await analytics.trackInteractions(interaction, (interaction) => { + if (interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND) + return interaction.commandName; + else if ( + interaction.type === Constants.InteractionTypes.MESSAGE_COMPONENT + || interaction.type === Constants.InteractionTypes.MODAL_SUBMIT + ) { + if ((/\d{17,19}/g).test(interaction.customId)) return 'this_awesome_button'; + else return interaction.customId; + } + return ''; + }); + + + if (interaction instanceof CommandInteraction) { + if (interaction.data.name === 'test') { + const option = interaction.data.options?.find((o) => o.name === 'test') as { value: string, type: number, name: string } | undefined; + + if (option && option.value === 'button') await interaction.createMessage({ + content: 'Test button', + components: [{ + type: 1, + components: [{ + type: 2, + style: 1, + label: 'Test button', + custom_id: `button_${interaction.user?.id}`, + }], + }], + }); + + else if (option && option.value === 'select') await interaction.createMessage({ + content: 'Test select', + components: [{ + type: 1, + components: [{ + type: 3, + custom_id: 'test_select', + options: [{ + label: 'Test select', + value: 'test_select', + }], + }], + }], + }); + + else if (option && option.value === 'modal') await interaction.createModal({ + custom_id: `my_modal`, + title: 'My modal', + components: [{ + type: 1, + components: [{ + type: 4, + custom_id: 'favorite_color_input', + label: 'What\'s your favorite color?', + style: 1, + }], + }], + }); + + else await interaction.createMessage({ + content: 'This is a test message', + flags: Constants.MessageFlags.EPHEMERAL, + }); + } + } + + if (interaction instanceof ComponentInteraction) { + if (interaction.data && interaction.data.component_type) { + if (interaction.data.component_type === 2) { + await interaction.createMessage({ + content: `You clicked the button with ID: ${interaction.data.custom_id}`, + flags: Constants.MessageFlags.EPHEMERAL, + }); + } else if (interaction.data.component_type === 3) { + await interaction.editOriginalMessage({ + content: `You selected: ${interaction.data.values[0]}`, + components: [], + }); + } + } + } + + if (interaction instanceof ModalSubmitInteraction) { + const favoriteColor = interaction.data.components[0].components[0].value; + await interaction.createMessage({ + content: `Your favorite color is: ${favoriteColor}`, + flags: Constants.MessageFlags.EPHEMERAL, + }); + } +}); + +client.on('guildCreate', (_) => analytics.trackGuilds('create')); +client.on('guildDelete', (_) => analytics.trackGuilds('delete')); + +client.connect(); diff --git a/packages/eris/package.json b/packages/eris/package.json new file mode 100644 index 0000000..2fd3014 --- /dev/null +++ b/packages/eris/package.json @@ -0,0 +1,30 @@ +{ + "name": "@discordanalytics/eris", + "version": "3.5.0", + "description": "Eris package for working with Discord Analytics", + "main": "dist/index.js", + "author": "Discord Analytics", + "homepage": "https://discordanalytics.xyz", + "readme": "README.md", + "repository": { + "type": "git", + "url": "git+https://github.com/DiscordAnalytics/node-package.git" + }, + "bugs": { + "url": "https://github.com/DiscordAnalytics/node-package/issues" + }, + "scripts": { + "prebuild": "pnpm --filter @discordanalytics/core build", + "build": "tsc" + }, + "dependencies": { + "@discordanalytics/core": "workspace:*" + }, + "devDependencies": { + "eris": "^0.18.0", + "typescript": "*" + }, + "peerDependencies": { + "eris": ">=0.17.x" + } +} \ No newline at end of file diff --git a/packages/eris/src/index.ts b/packages/eris/src/index.ts new file mode 100644 index 0000000..80e3422 --- /dev/null +++ b/packages/eris/src/index.ts @@ -0,0 +1,171 @@ +import { AnalyticsBase, ApiEndpoints, AnalyticsOptions, ErrorCodes, InteractionType } from '@discordanalytics/core'; +import npmPackageData from '../package.json'; + +/** + * @class DiscordAnalytics + * @description The Eris class for the DiscordAnalytics library. + * @param {AnalyticsOptions} options - Configuration options. + * @property {any} options.client The Eris client to track events for. + * @property {string} options.api_key The API token for DiscordAnalytics. + * @property {boolean} options.debug Enable or not the debug mode /!\ MUST BE USED ONLY FOR DEVELOPMENT PURPOSES /!\ + * @example + * const { default: DiscordAnalytics } = require('discord-analytics/eris'); + * const Eris = require('eris'); + * const client = new Client('YOUR_BOT_TOKEN', { + * intents: ['guilds'] + * }) + * client.on('ready', () => { + * const analytics = new DiscordAnalytics({ + * client: client, + * api_key: 'YOUR_API_TOKEN' + * }); + * analytics.init(); + * analytics.trackEvents(); + * }); + * client.connect() + * + * @link https://discordanalytics.xyz/docs/main/get-started/installation/eris - Check docs for more informations about advanced usages + */ +export default class DiscordAnalytics extends AnalyticsBase { + private readonly _client: any; + private _isReady: boolean = false; + + constructor(options: Omit) { + super(options.api_key, options.debug); + this._client = options.client; + } + + /** + * Initialize DiscordAnalytics on your bot + * /!\ Advanced users only + * /!\ Required to use DiscordAnalytics + * /!\ Must be used when the client is ready (recommended to use in ready event to prevent problems) + */ + public async init(): Promise { + if (process.env.NODE_ENV !== 'production') return console.log("[DISCORDANALYTICS] NODE_ENV != 'production', initialization skipped") + + const url = ApiEndpoints.EDIT_SETTINGS_URL.replace(':id', this._client.user.id); + const body = JSON.stringify({ + username: this._client.user.username, + avatar: this._client.user.avatar, + framework: 'eris', + version: npmPackageData.version, + team: this._client.application.owner + ? this._client.application.owner.hasOwnProperty('members') + ? this._client.application.owner.members.map((member: any) => member.user.id) + : [this._client.application.owner.id] + : [], + }); + + await this.api_call_with_retries('PATCH', url, body); + + this.debug('[DISCORDANALYTICS] Instance successfully initialized'); + this.client_id = this._client.user.id; + this._isReady = true; + + const fast_mode = process.argv[2] === '--fast'; + this.debug(`[DISCORDANALYTICS] Fast mode is ${fast_mode ? 'enabled' : 'disabled'}. Stats will be sent every ${fast_mode ? '30s' : '5min'}.`); + + setInterval(async () => { + this.debug('[DISCORDANALYTICS] Sending stats...'); + + const guildCount = this._client.guilds.size; + const userCount = this._client.guilds.reduce((a: number, g: any) => a + g.memberCount, 0); + const guildMembers: number[] = this._client.guilds.map((guild: any) => guild.memberCount); + + await this.sendStats(this._client.user.id, guildCount, userCount, guildMembers); + }, fast_mode ? 30000 : 300000); + } + + /** + * Track interactions + * /!\ Advanced users only + * /!\ You need to initialize the class first + * @param interaction BaseInteraction class and its extensions only + * @param interactionNameResolver A function that will resolve the name of the interaction + */ + public async trackInteractions(interaction: any, interactionNameResolver?: (interaction: any) => string): Promise { + this.debug('[DISCORDANALYTICS] trackInteractions() triggered'); + if (!this._isReady) return this.error(ErrorCodes.INSTANCE_NOT_INITIALIZED); + + this.updateOrInsert( + this.stats_data.guildsLocales, + (x) => x.locale === interaction.guild?.preferredLocale, + (x) => x.number++, + () => ({ locale: interaction.guild?.preferredLocale, number: 1 }) + ); + + if (interaction.type === InteractionType.ApplicationCommand) { + const name = interactionNameResolver ? interactionNameResolver(interaction) : interaction.data.name; + this.updateOrInsert( + this.stats_data.interactions, + (x) => x.name === name && x.type === interaction.type, + (x) => x.number++, + () => ({ name, number: 1, type: interaction.type }) + ); + } else if (interaction.type === InteractionType.MessageComponent) { + const name = interactionNameResolver ? interactionNameResolver(interaction) : interaction.data.custom_id; + this.updateOrInsert( + this.stats_data.interactions, + (x) => x.name === name && x.type === interaction.type, + (x) => x.number++, + () => ({ name, number: 1, type: interaction.type }) + ); + } + + this.updateOrInsert( + this.stats_data.guildsStats, + (x) => x.guildId === (interaction.guildID || 'dm'), + (x) => x.interactions++, + () => ({ + guildId: interaction.guildID || 'dm', + name: interaction.guildID ? this._client.guilds.get(interaction.guildID)?.name : 'DM', + icon: interaction.guildID ? this._client.guilds.get(interaction.guildID)?.icon : undefined, + interactions: 1, + members: interaction.guildID ? this._client.guilds.get(interaction.guildID)?.memberCount : 0, + }) + ); + + const oneWeekAgo = new Date(); + oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); + + if (!interaction.guildID) ++this.stats_data.users_type.private_message; + else if ( + interaction.member + && interaction.member.permissions + && interaction.member.permissions.has(8n) + || interaction.member.permissions.has(32n) + ) ++this.stats_data.users_type.admin; + else if ( + interaction.member + && interaction.member.permissions + && interaction.member.permissions.has(8192n) + || interaction.member.permissions.has(2n) + || interaction.member.permissions.has(4n) + || interaction.member.permissions.has(4194304n) + || interaction.member.permissions.has(8388608n) + || interaction.member.permissions.has(16777216n) + || interaction.member.permissions.has(1099511627776n) + ) ++this.stats_data.users_type.moderator; + else if ( + interaction.member + && interaction.member.joinedAt + && interaction.member.joinedAt > oneWeekAgo + ) ++this.stats_data.users_type.new_member; + } + + /** + * Let DiscordAnalytics declare the events necessary for its operation. + * /!\ Not recommended for big bots + * /!\ Not compatible with other functions + * @param interactionNameResolver A function that will resolve the name of the interaction + */ + public trackEvents(interactionNameResolver?: (interaction: any) => string): void { + this.debug('[DISCORDANALYTICS] trackEvents() triggered'); + if (!this._isReady) return this.error(ErrorCodes.INSTANCE_NOT_INITIALIZED); + + this._client.on('interactionCreate', async (interaction: any) => await this.trackInteractions(interaction, interactionNameResolver)); + this._client.on('guildCreate', (guild: any) => this.trackGuilds('create')); + this._client.on('guildDelete', (guild: any) => this.trackGuilds('delete')); + } +} \ No newline at end of file diff --git a/packages/eris/tsconfig.json b/packages/eris/tsconfig.json new file mode 100644 index 0000000..16894b3 --- /dev/null +++ b/packages/eris/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + }, + "include": ["src"] +} \ No newline at end of file diff --git a/packages/oceanic/README.md b/packages/oceanic/README.md new file mode 100644 index 0000000..a7744be --- /dev/null +++ b/packages/oceanic/README.md @@ -0,0 +1,51 @@ +# DiscordAnalytics for Oceanic.js + +## Overview +This package is a wrapper for the Discord Analytics API made for Oceanic.js. It allows you to track events and send them to the Discord Analytics API. + +## Installation +```bash +npm install @discordanalytics/oceanic +# or +yarn add @discordanalytics/oceanic +# or +pnpm add @discordanalytics/oceanic +``` + +## Usage +> **Note:** To use Discord Analytics, you need to have an API key. Check the docs for more informations : https://discordanalytics.xyz/docs/main/get-started/bot-registration + +### Example +```js +// Import Discord.js's client and intents +const { Client } = require("oceanic.js") +// import discord-analytics +const { default: DiscordAnalytics } = require("@discordanalytics/oceanic") + +// Create Discord client +// Don't forget to replace YOUR_BOT_TOKEN by your Discord bot token ! +const client = new Client({ + auth: "Bot ", + gateway: { + intents: ["GUILDS"] // This intent is required + } +}) + +// Create Discord Analytics instance +// Don't forget to replace YOUR_API_KEY by your Discord Analytics key ! +const analytics = new DiscordAnalytics({ + client: client, + api_key: 'YOUR_API_KEY' +}); + +// When Discord client is ready +client.on('ready', () => { + console.log("Bot is ready!"); + + analytics.init(); // Initialize the analytics + analytics.trackEvents(); // Start tracking events +}); + +// Login to Discord +client.connect(); +``` diff --git a/packages/oceanic/examples/index.ts b/packages/oceanic/examples/index.ts new file mode 100644 index 0000000..ff37f95 --- /dev/null +++ b/packages/oceanic/examples/index.ts @@ -0,0 +1,139 @@ +import DiscordAnalytics from '../src/index'; +import { ApplicationCommandOptionTypes, ApplicationCommandTypes, ButtonStyles, Client, ComponentTypes, InteractionTypes, TextInputStyles } from 'oceanic.js'; +import 'dotenv/config'; + +const client = new Client({ + auth: `Bot ${process.env.DISCORD_TOKEN}`, + gateway: { + intents: ['GUILDS'], + }, +}); + +const analytics = new DiscordAnalytics({ + client, + api_key: process.env.DISCORD_ANALYTICS_API_KEY as string, + debug: true, +}); + +client.on('error', (error) => { + console.error('Client error:', error); +}); + +client.on('ready', async () => { + await client.application.createGlobalCommand({ + type: ApplicationCommandTypes.CHAT_INPUT, + name: 'test', + description: 'Do some tests', + options: [{ + type: ApplicationCommandOptionTypes.STRING, + name: 'type', + description: 'Type of test', + required: false, + choices: [ + { + name: 'Button', + value: 'button', + }, + { + name: 'Select Menu', + value: 'select_menu', + }, + { + name: 'Modal', + value: 'modal', + }, + ], + }], + }); + + console.log('Client is ready!'); + + await analytics.init(); +}); + +client.on('interactionCreate', async (interaction) => { + await analytics.trackInteractions(interaction, (interaction) => { + if (interaction.type === InteractionTypes.APPLICATION_COMMAND) + return interaction.commandName; + else if ( + interaction.type === InteractionTypes.MESSAGE_COMPONENT + || interaction.type === InteractionTypes.MODAL_SUBMIT + ) { + if ((/\d{17,19}/g).test(interaction.customId)) return 'this_awesome_button'; + else return interaction.customId; + } + return ''; + }); + + if (interaction.isCommandInteraction() && interaction.data.name === 'test') { + const option = interaction.data.options.getString('type', false); + + if (option === 'button') interaction.reply({ + content: 'Here is the button!', + components: [{ + type: ComponentTypes.ACTION_ROW, + components: [{ + type: ComponentTypes.BUTTON, + style: ButtonStyles.PRIMARY, + label: 'Test button', + customID: `button_${interaction.user.id}`, + }], + }], + }); + + else if (option === 'select_menu') interaction.reply({ + content: 'Here is the select menu!', + components: [{ + type: ComponentTypes.ACTION_ROW, + components: [{ + type: ComponentTypes.STRING_SELECT, + placeholder: 'Select an option', + customID: 'test_select', + options: [{ + label: 'Test select', + value: 'test_select', + }], + }], + }], + }); + + else if (option === 'modal') interaction.createModal({ + title: 'This is a test modal', + customID: 'test_modal', + components: [{ + type: ComponentTypes.ACTION_ROW, + components: [{ + type: ComponentTypes.TEXT_INPUT, + customID: 'test_input', + label: 'Test input', + style: TextInputStyles.SHORT, + placeholder: 'Test input', + required: true, + }], + }], + }); + + else interaction.reply({ + content: 'Please select a type of test', + }); + } else if (interaction.isComponentInteraction()) { + if (interaction.data.componentType === ComponentTypes.BUTTON) interaction.reply({ + content: `You clicked the button! ${interaction.data.customID}`, + }); + + else if (interaction.data.componentType === ComponentTypes.STRING_SELECT) await interaction.message.edit({ + content: `You selected the option! ${interaction.data.values.raw[0]}`, + components: [], + }); + } else if (interaction.isModalSubmitInteraction()) { + const input = interaction.data.components.getComponents()[0].value; + interaction.reply({ + content: `You submitted the modal! ${input}`, + }); + } +}); + +client.on('guildCreate', (_) => analytics.trackGuilds('create')); +client.on('guildDelete', (_) => analytics.trackGuilds('delete')); + +client.connect(); diff --git a/packages/oceanic/package.json b/packages/oceanic/package.json new file mode 100644 index 0000000..35cc66d --- /dev/null +++ b/packages/oceanic/package.json @@ -0,0 +1,30 @@ +{ + "name": "@discordanalytics/oceanic", + "version": "3.5.0", + "description": "Oceanic package for working with Discord Analytics", + "main": "dist/index.js", + "author": "Discord Analytics", + "homepage": "https://discordanalytics.xyz", + "readme": "README.md", + "repository": { + "type": "git", + "url": "git+https://github.com/DiscordAnalytics/node-package.git" + }, + "bugs": { + "url": "https://github.com/DiscordAnalytics/node-package/issues" + }, + "scripts": { + "prebuild": "pnpm --filter @discordanalytics/core build", + "build": "tsc" + }, + "dependencies": { + "@discordanalytics/core": "workspace:*" + }, + "devDependencies": { + "oceanic.js": "^1.12.0", + "typescript": "*" + }, + "peerDependencies": { + "oceanic.js": ">=1.12.x" + } +} \ No newline at end of file diff --git a/packages/oceanic/src/index.ts b/packages/oceanic/src/index.ts new file mode 100644 index 0000000..1b13240 --- /dev/null +++ b/packages/oceanic/src/index.ts @@ -0,0 +1,196 @@ +import { AnalyticsBase, ApiEndpoints, AnalyticsOptions, ErrorCodes, InteractionType } from '@discordanalytics/core'; +import npmPackageData from '../package.json'; + +/** + * @class DiscordAnalytics + * @description The Oceanic.js class for the DiscordAnalytics library. + * @param {AnalyticsOptions} options - Configuration options. + * @property {any} options.client The Oceanic.js client to track events for. + * @property {string} options.api_key The API token for DiscordAnalytics. + * @property {boolean} options.debug Enable or not the debug mode /!\ MUST BE USED ONLY FOR DEVELOPMENT PURPOSES /!\ + * @example + * const { default: DiscordAnalytics } = require('discord-analytics/oceanic'); + * const { Client } = require('oceanic.js'); + * const client = new Client({ + * auth: 'Bot ', + * gateway: { + * intents: ['GUILDS'] + * } + * }) + * client.on('ready', () => { + * const analytics = new DiscordAnalytics({ + * client: client, + * api_key: 'YOUR_API_TOKEN' + * }); + * analytics.init(); + * analytics.trackEvents(); + * }); + * client.connect(); + * @link https://discordanalytics.xyz/docs/main/get-started/installation/oceanic.js Check docs for more informations about advanced usages + */ +export default class DiscordAnalytics extends AnalyticsBase { + private readonly _client: any; + private _isReady: boolean = false; + + constructor(options: Omit) { + super(options.api_key, options.debug); + this._client = options.client; + } + + /** + * Initialize DiscordAnalytics on your bot + * /!\ Advanced users only + * /!\ Required to use DiscordAnalytics + * /!\ Must be used when the client is ready (recommended to use in ready event to prevent problems) + */ + public async init(): Promise { + if (process.env.NODE_ENV !== 'production') return console.log("[DISCORDANALYTICS] NODE_ENV != 'production', initialization skipped") + + const app = await this._client.rest.applications.getCurrent(); + const url = ApiEndpoints.EDIT_SETTINGS_URL.replace(':id', this._client.user.id); + const body = JSON.stringify({ + username: this._client.user.username, + avatar: this._client.user.avatar, + framework: 'oceanic', + version: npmPackageData.version, + team: app.team + ? app.team.members.map((member: any) => member.user.id) + : app.owner + ? [app.owner.id] + : [], + }); + + await this.api_call_with_retries('PATCH', url, body); + + this.debug('[DISCORDANALYTICS] Instance successfully initialized'); + this.client_id = this._client.user.id; + this._isReady = true; + + const fast_mode = process.argv[2] === '--fast'; + this.debug(`[DISCORDANALYTICS] Fast mode is ${fast_mode ? 'enabled' : 'disabled'}. Stats will be sent every ${fast_mode ? '30s' : '5min'}.`); + + setInterval(async () => { + this.debug('[DISCORDANALYTICS] Sending stats...'); + + const guildCount = this._client.guilds.toArray().length; + const userCount = this._client.guilds.reduce((a: number, g: any) => a + (g.memberCount || 0), 0); + const guildMembers: number[] = this._client.guilds.map((guild: any) => guild.memberCount); + + await this.sendStats(this._client.user.id, guildCount, userCount, guildMembers); + }, fast_mode ? 30000 : 300000); + } + + /** + * Track interactions + * /!\ Advanced users only + * /!\ You need to initialize the class first + * @param interaction BaseInteraction class and its extensions only + * @param interactionNameResolver A function that will resolve the name of the interaction + */ + public async trackInteractions(interaction: any, interactionNameResolver?: (interaction: any) => string): Promise { + this.debug('[DISCORDANALYTICS] trackInteractions() triggered'); + if (!this._isReady) return this.error(ErrorCodes.INSTANCE_NOT_INITIALIZED); + + this.updateOrInsert( + this.stats_data.guildsLocales, + (x) => x.locale === interaction.guild?.preferredLocale, + (x) => x.number++, + () => ({ locale: interaction.guild?.preferredLocale, number: 1 }), + ); + + this.updateOrInsert( + this.stats_data.locales, + (x) => x.locale === interaction.locale, + (x) => x.number++, + () => ({ locale: interaction.locale, number: 1 }), + ); + + if (interaction.type === InteractionType.ApplicationCommand) { + const commandType = interaction.data.type; + const commandName = interactionNameResolver + ? interactionNameResolver(interaction) + : interaction.data.name; + this.updateOrInsert( + this.stats_data.interactions, + (x) => x.name === commandName + && x.type === interaction.type + && x.command_type === commandType, + (x) => x.number++, + () => ({ + name: commandName, + number: 1, + type: interaction.type, + command_type: commandType, + }), + ); + } else if (interaction.type === InteractionType.MessageComponent || interaction.type === InteractionType.ModalSubmit) { + const interactionName = interactionNameResolver + ? interactionNameResolver(interaction) + : interaction.data.customID; + this.updateOrInsert( + this.stats_data.interactions, + (x) => x.name === interactionName && x.type === interaction.type, + (x) => x.number++, + () => ({ + name: interactionName, + number: 1, + type: interaction.type, + }), + ); + } + + this.updateOrInsert( + this.stats_data.guildsStats, + (x) => x.guildId === (interaction.guild ? interaction.guildID : 'dm'), + (x) => x.interactions++, + () => ({ + guildId: interaction.guild ? interaction.guildID : 'dm', + name: interaction.guild ? interaction.guild.name : 'DM', + icon: interaction.guild && interaction.guild.icon ? interaction.guild.icon : undefined, + interactions: 1, + members: interaction.guild ? interaction.guild.memberCount : 0, + }), + ); + + const oneWeekAgo = new Date(); + oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); + + if (!interaction.guild) ++this.stats_data.users_type.private_message + else if ( + interaction.member + && interaction.member.permissions + && interaction.member.permissions.has(8n) + || interaction.member.permissions.has(32n) + ) ++this.stats_data.users_type.admin + else if ( + interaction.member + && interaction.member.permissions + && interaction.member.permissions.has(8192n) + || interaction.member.permissions.has(2n) + || interaction.member.permissions.has(4194304n) + || interaction.member.permissions.has(8388608n) + || interaction.member.permissions.has(16777216n) + || interaction.member.permissions.has(1099511627776n) + ) ++this.stats_data.users_type.moderator + else if ( + interaction.member + && interaction.member.joinedAt + && interaction.member.joinedAt > oneWeekAgo + ) ++this.stats_data.users_type.new_member + } + + /** + * Let DiscordAnalytics declare the events necessary for its operation. + * /!\ Not recommended for big bots + * /!\ Not compatible with other functions + * @param interactionNameResolver A function that will resolve the name of the interaction + */ + public trackEvents(interactionNameResolver?: (interaction: any) => string): void { + this.debug('[DISCORDANALYTICS] trackEvents() triggered'); + if (!this._isReady) return this.error(ErrorCodes.INSTANCE_NOT_INITIALIZED); + + this._client.on('interactionCreate', async (interaction: any) => await this.trackInteractions(interaction, interactionNameResolver)); + this._client.on('guildCreate', async (guild: any) => this.trackGuilds('create')); + this._client.on('guildDelete', async (guild: any) => this.trackGuilds('delete')); + } +} diff --git a/packages/oceanic/tsconfig.json b/packages/oceanic/tsconfig.json new file mode 100644 index 0000000..16894b3 --- /dev/null +++ b/packages/oceanic/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + }, + "include": ["src"] +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7440050..aefa8ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,47 +7,117 @@ settings: importers: .: + devDependencies: + '@types/node': + specifier: ^22.15.2 + version: 22.15.2 + '@vitest/coverage-v8': + specifier: 3.1.2 + version: 3.1.2(vitest@3.1.2(@types/node@22.15.2)) + dotenv: + specifier: ^16.5.0 + version: 16.5.0 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vitest: + specifier: ^3.1.2 + version: 3.1.2(@types/node@22.15.2) + + packages/core: dependencies: node-fetch: specifier: ^2.7.0 version: 2.7.0 devDependencies: '@types/node': - specifier: ^20.17.11 - version: 20.17.28 + specifier: '*' + version: 22.15.2 '@types/node-fetch': - specifier: ^2.6.12 + specifier: 2.6.12 version: 2.6.12 + typescript: + specifier: '*' + version: 5.8.3 + + packages/discordjs: + dependencies: + '@discordanalytics/core': + specifier: workspace:* + version: link:../core + devDependencies: discord.js: - specifier: ^14.17.2 + specifier: 14.18.0 version: 14.18.0 + typescript: + specifier: '*' + version: 5.8.3 + + packages/discordjs-light: + dependencies: + '@discordanalytics/core': + specifier: workspace:* + version: link:../core + devDependencies: discord.js-light: specifier: ^4.10.0 version: 4.10.0 - dotenv: - specifier: ^16.4.7 - version: 16.4.7 + typescript: + specifier: '*' + version: 5.8.3 + + packages/eris: + dependencies: + '@discordanalytics/core': + specifier: workspace:* + version: link:../core + devDependencies: eris: - specifier: ^0.17.2 - version: 0.17.2 - nodemon: - specifier: ^3.1.9 - version: 3.1.9 + specifier: ^0.18.0 + version: 0.18.0 + typescript: + specifier: '*' + version: 5.8.3 + + packages/oceanic: + dependencies: + '@discordanalytics/core': + specifier: workspace:* + version: link:../core + devDependencies: oceanic.js: - specifier: ^1.11.2 - version: 1.11.2(opusscript@0.0.8) - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@20.17.28)(typescript@5.8.2) + specifier: ^1.12.0 + version: 1.12.0(opusscript@0.0.8) typescript: - specifier: ^5.7.2 - version: 5.8.2 + specifier: '*' + version: 5.8.3 packages: - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.0': + resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.27.0': + resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} '@discordjs/builders@0.16.0': resolution: {integrity: sha512-9/NCiZrLivgRub2/kBc0Vm5pMBE5AUdYbdXsLu/yg9ANgvnaJ0bZKTY8yYnLbsEc/LYUP79lEIdC73qEYhWq7A==} @@ -83,24 +153,293 @@ packages: resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==} engines: {node: '>=18'} - '@discordjs/voice@0.17.0': - resolution: {integrity: sha512-hArn9FF5ZYi1IkxdJEVnJi+OxlwLV0NJYWpKXsmNOojtGtAZHxmsELA+MZlu2KW1F/K1/nt7lFOfcMXNYweq9w==} - engines: {node: '>=16.11.0'} - deprecated: This version uses deprecated encryption modes. Please use a newer version. + '@discordjs/voice@0.18.0': + resolution: {integrity: sha512-BvX6+VJE5/vhD9azV9vrZEt9hL1G+GlOdsQaVl5iv9n87fkXjf3cSwllhR3GdaUC8m6dqT8umXIWtn3yCu4afg==} + engines: {node: '>=18'} '@discordjs/ws@1.2.1': resolution: {integrity: sha512-PBvenhZG56a6tMWF/f4P6f4GxZKJTBG95n7aiGSPTnodmz4N5g60t79rSIAq7ywMbv8A4jFtexMruH+oe51aQQ==} engines: {node: '>=16.11.0'} + '@esbuild/aix-ppc64@0.25.3': + resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.3': + resolution: {integrity: sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.3': + resolution: {integrity: sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.3': + resolution: {integrity: sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.3': + resolution: {integrity: sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.3': + resolution: {integrity: sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.3': + resolution: {integrity: sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.3': + resolution: {integrity: sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.3': + resolution: {integrity: sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.3': + resolution: {integrity: sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.3': + resolution: {integrity: sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.3': + resolution: {integrity: sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.3': + resolution: {integrity: sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.3': + resolution: {integrity: sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.3': + resolution: {integrity: sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.3': + resolution: {integrity: sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.3': + resolution: {integrity: sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.3': + resolution: {integrity: sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.3': + resolution: {integrity: sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.3': + resolution: {integrity: sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.3': + resolution: {integrity: sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.3': + resolution: {integrity: sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.3': + resolution: {integrity: sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.3': + resolution: {integrity: sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.3': + resolution: {integrity: sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/rollup-android-arm-eabi@4.40.0': + resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.40.0': + resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.40.0': + resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.40.0': + resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.40.0': + resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.40.0': + resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.40.0': + resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.40.0': + resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.40.0': + resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.40.0': + resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.40.0': + resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': + resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.40.0': + resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.40.0': + resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.40.0': + resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.40.0': + resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.40.0': + resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.40.0': + resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.40.0': + resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.40.0': + resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==} + cpu: [x64] + os: [win32] '@sapphire/async-queue@1.5.5': resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} @@ -118,49 +457,79 @@ packages: resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - '@tsconfig/node10@1.0.11': - resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} - - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - - '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} '@types/node-fetch@2.6.12': resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} - '@types/node@20.17.28': - resolution: {integrity: sha512-DHlH/fNL6Mho38jTy7/JT7sn2wnXI+wULR6PV4gy4VHLVvnrV/d3pHAMQHhc4gjdLmK2ZiPoMxzp6B3yRajLSQ==} - - '@types/ws@8.5.13': - resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} + '@types/node@22.15.2': + resolution: {integrity: sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==} '@types/ws@8.5.14': resolution: {integrity: sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==} + '@vitest/coverage-v8@3.1.2': + resolution: {integrity: sha512-XDdaDOeaTMAMYW7N63AqoK32sYUWbXnTkC6tEbVcu3RlU1bB9of32T+PGf8KZvxqLNqeXhafDFqCkwpf2+dyaQ==} + peerDependencies: + '@vitest/browser': 3.1.2 + vitest: 3.1.2 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@3.1.2': + resolution: {integrity: sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==} + + '@vitest/mocker@3.1.2': + resolution: {integrity: sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.1.2': + resolution: {integrity: sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==} + + '@vitest/runner@3.1.2': + resolution: {integrity: sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==} + + '@vitest/snapshot@3.1.2': + resolution: {integrity: sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==} + + '@vitest/spy@3.1.2': + resolution: {integrity: sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==} + + '@vitest/utils@3.1.2': + resolution: {integrity: sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==} + '@vladfrangu/async_event_emitter@2.4.6': resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} - engines: {node: '>=0.4.0'} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} - engines: {node: '>=0.4.0'} - hasBin: true + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -168,30 +537,35 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + chai@5.2.0: + resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} + engines: {node: '>=12'} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} @@ -202,14 +576,14 @@ packages: supports-color: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - discord-api-types@0.33.5: resolution: {integrity: sha512-dvO5M52v7m7Dy96+XUnzXNsQ/0npsYpU6dL205kAtEDueswoz3aU3bh1UMoK4cQmcGtB1YRyLKqp+DXi05lzFg==} @@ -219,9 +593,6 @@ packages: discord-api-types@0.37.119: resolution: {integrity: sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg==} - discord-api-types@0.37.83: - resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==} - discord.js-light@4.10.0: resolution: {integrity: sha512-IFpwVeeyUtopu77pWUfzZwOmhQqWLFbNhNqvRZZBAtRQ7t4m6LKjJjfAsHr4QgQiHcGUjNAvEUnpPnMOmZyuQg==} engines: {node: '>=16.6.0'} @@ -234,20 +605,52 @@ packages: resolution: {integrity: sha512-SvU5kVUvwunQhN2/+0t55QW/1EHfB1lp0TtLZUSXVHDmyHTrdOj5LRKdR0zLcybaA15F+NtdWuWmGOX9lE+CAw==} engines: {node: '>=18'} - dotenv@16.4.7: - resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} - eris@0.17.2: - resolution: {integrity: sha512-OzffDSL81VooFXfRn56ienSg7J6fagoN8WdkmCHjn0B9a4jUA3bVouMztQiaXT54jHV5jaX9D0mJ/b9/0mFoVg==} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + eris@0.18.0: + resolution: {integrity: sha512-zv2hjAfs4SYjR/EHUwBGNoKClctbqki1jPMed4SiQ4KKfafV1MIBLqKodQSGDgT9qkSUI5+MQ4EQW/8MkbAPkg==} engines: {node: '>=10.4.0'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.25.3: + resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.2.1: + resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} + engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} + fdir@6.4.4: + resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} form-data@4.0.1: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} @@ -258,32 +661,42 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} - ignore-by-default@1.0.1: - resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} @@ -291,11 +704,24 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loupe@3.1.3: + resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + magic-bytes.js@1.10.0: resolution: {integrity: sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==} - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} @@ -305,12 +731,22 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -320,25 +756,41 @@ packages: encoding: optional: true - nodemon@3.1.9: - resolution: {integrity: sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==} - engines: {node: '>=10'} - hasBin: true - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - oceanic.js@1.11.2: - resolution: {integrity: sha512-kXMoZiIrIFq0QCfsZGJ+eOK+1IFkVsTpvwcQ3nfY6ioDKGVdHQjVIXNJAYvJb5c5la//2OU8WGpqsmp/bAP8aw==} + oceanic.js@1.12.0: + resolution: {integrity: sha512-e3h5ptwBfbX4/gDz13UFiIfAFsaZrXDwJbfTdV7YEKdQG6d0A6of2KGzkBbqXrVC5wmM/7TWAYeh2fe9zuxGYA==} engines: {node: '>=18.13.0'} opusscript@0.0.8: resolution: {integrity: sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} prism-media@1.3.5: resolution: {integrity: sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==} @@ -357,33 +809,86 @@ packages: opusscript: optional: true - pstree.remy@1.1.8: - resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} + rollup@4.40.0: + resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} hasBin: true - simple-update-notifier@2.0.0: - resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} - engines: {node: '>=10'} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - touch@3.1.1: - resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} - hasBin: true + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.13: + resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} + engines: {node: '>=12.0.0'} + + tinypool@1.0.2: + resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -391,43 +896,96 @@ packages: ts-mixer@6.0.4: resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} - ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} tweetnacl@1.0.3: resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} - typescript@5.8.2: - resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} hasBin: true - undefsafe@2.0.5: - resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} - - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} undici@6.21.1: resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==} engines: {node: '>=18.17'} - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + vite-node@3.1.2: + resolution: {integrity: sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@6.3.3: + resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.1.2: + resolution: {integrity: sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.1.2 + '@vitest/ui': 3.1.2 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -435,6 +993,24 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -447,15 +1023,39 @@ packages: utf-8-validate: optional: true - yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} + ws@8.18.1: + resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} + 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 snapshots: - '@cspotcode/source-map-support@0.8.1': + '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/trace-mapping': 0.3.9 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/parser@7.27.0': + dependencies: + '@babel/types': 7.27.0 + + '@babel/types@7.27.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@bcoe/v8-coverage@1.0.2': {} '@discordjs/builders@0.16.0': dependencies: @@ -499,13 +1099,13 @@ snapshots: '@discordjs/util@1.1.1': {} - '@discordjs/voice@0.17.0(opusscript@0.0.8)': + '@discordjs/voice@0.18.0(opusscript@0.0.8)': dependencies: - '@types/ws': 8.5.13 - discord-api-types: 0.37.83 + '@types/ws': 8.5.14 + discord-api-types: 0.37.119 prism-media: 1.3.5(opusscript@0.0.8) tslib: 2.8.1 - ws: 8.18.0 + ws: 8.18.1 transitivePeerDependencies: - '@discordjs/opus' - bufferutil @@ -530,15 +1130,172 @@ snapshots: - bufferutil - utf-8-validate + '@esbuild/aix-ppc64@0.25.3': + optional: true + + '@esbuild/android-arm64@0.25.3': + optional: true + + '@esbuild/android-arm@0.25.3': + optional: true + + '@esbuild/android-x64@0.25.3': + optional: true + + '@esbuild/darwin-arm64@0.25.3': + optional: true + + '@esbuild/darwin-x64@0.25.3': + optional: true + + '@esbuild/freebsd-arm64@0.25.3': + optional: true + + '@esbuild/freebsd-x64@0.25.3': + optional: true + + '@esbuild/linux-arm64@0.25.3': + optional: true + + '@esbuild/linux-arm@0.25.3': + optional: true + + '@esbuild/linux-ia32@0.25.3': + optional: true + + '@esbuild/linux-loong64@0.25.3': + optional: true + + '@esbuild/linux-mips64el@0.25.3': + optional: true + + '@esbuild/linux-ppc64@0.25.3': + optional: true + + '@esbuild/linux-riscv64@0.25.3': + optional: true + + '@esbuild/linux-s390x@0.25.3': + optional: true + + '@esbuild/linux-x64@0.25.3': + optional: true + + '@esbuild/netbsd-arm64@0.25.3': + optional: true + + '@esbuild/netbsd-x64@0.25.3': + optional: true + + '@esbuild/openbsd-arm64@0.25.3': + optional: true + + '@esbuild/openbsd-x64@0.25.3': + optional: true + + '@esbuild/sunos-x64@0.25.3': + optional: true + + '@esbuild/win32-arm64@0.25.3': + optional: true + + '@esbuild/win32-ia32@0.25.3': + optional: true + + '@esbuild/win32-x64@0.25.3': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/set-array@1.2.1': {} + '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/trace-mapping@0.3.9': + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/rollup-android-arm-eabi@4.40.0': + optional: true + + '@rollup/rollup-android-arm64@4.40.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.40.0': + optional: true + + '@rollup/rollup-darwin-x64@4.40.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.40.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.40.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.40.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.40.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.40.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.40.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.40.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.40.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.40.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.40.0': + optional: true + '@sapphire/async-queue@1.5.5': {} '@sapphire/shapeshift@3.9.7': @@ -553,90 +1310,136 @@ snapshots: '@sapphire/snowflake@3.5.3': {} - '@tsconfig/node10@1.0.11': {} + '@types/estree@1.0.7': {} - '@tsconfig/node12@1.0.11': {} + '@types/node-fetch@2.6.12': + dependencies: + '@types/node': 22.15.2 + form-data: 4.0.1 - '@tsconfig/node14@1.0.3': {} + '@types/node@22.15.2': + dependencies: + undici-types: 6.21.0 - '@tsconfig/node16@1.0.4': {} + '@types/ws@8.5.14': + dependencies: + '@types/node': 22.15.2 - '@types/node-fetch@2.6.12': + '@vitest/coverage-v8@3.1.2(vitest@3.1.2(@types/node@22.15.2))': dependencies: - '@types/node': 20.17.28 - form-data: 4.0.1 + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + debug: 4.4.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.17 + magicast: 0.3.5 + std-env: 3.9.0 + test-exclude: 7.0.1 + tinyrainbow: 2.0.0 + vitest: 3.1.2(@types/node@22.15.2) + transitivePeerDependencies: + - supports-color - '@types/node@20.17.28': + '@vitest/expect@3.1.2': dependencies: - undici-types: 6.19.8 + '@vitest/spy': 3.1.2 + '@vitest/utils': 3.1.2 + chai: 5.2.0 + tinyrainbow: 2.0.0 - '@types/ws@8.5.13': + '@vitest/mocker@3.1.2(vite@6.3.3(@types/node@22.15.2))': dependencies: - '@types/node': 20.17.28 + '@vitest/spy': 3.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 6.3.3(@types/node@22.15.2) - '@types/ws@8.5.14': + '@vitest/pretty-format@3.1.2': dependencies: - '@types/node': 20.17.28 + tinyrainbow: 2.0.0 - '@vladfrangu/async_event_emitter@2.4.6': {} + '@vitest/runner@3.1.2': + dependencies: + '@vitest/utils': 3.1.2 + pathe: 2.0.3 - acorn-walk@8.3.4: + '@vitest/snapshot@3.1.2': dependencies: - acorn: 8.14.0 + '@vitest/pretty-format': 3.1.2 + magic-string: 0.30.17 + pathe: 2.0.3 - acorn@8.14.0: {} + '@vitest/spy@3.1.2': + dependencies: + tinyspy: 3.0.2 - anymatch@3.1.3: + '@vitest/utils@3.1.2': dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 + '@vitest/pretty-format': 3.1.2 + loupe: 3.1.3 + tinyrainbow: 2.0.0 + + '@vladfrangu/async_event_emitter@2.4.6': {} - arg@4.1.3: {} + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + assertion-error@2.0.1: {} asynckit@0.4.0: {} balanced-match@1.0.2: {} - binary-extensions@2.3.0: {} - - brace-expansion@1.1.11: + brace-expansion@2.0.1: dependencies: balanced-match: 1.0.2 - concat-map: 0.0.1 - braces@3.0.3: + cac@6.7.14: {} + + chai@5.2.0: dependencies: - fill-range: 7.1.1 + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.3 + pathval: 2.0.0 - chokidar@3.6.0: + check-error@2.1.1: {} + + color-convert@2.0.1: dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 + color-name: 1.1.4 + + color-name@1.1.4: {} combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 - concat-map@0.0.1: {} - - create-require@1.1.1: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 - debug@4.4.0(supports-color@5.5.0): + debug@4.4.0: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 5.5.0 - delayed-stream@1.0.0: {} + deep-eql@5.0.2: {} - diff@4.0.2: {} + delayed-stream@1.0.0: {} discord-api-types@0.33.5: {} @@ -644,9 +1447,6 @@ snapshots: discord-api-types@0.37.119: {} - discord-api-types@0.37.83: - optional: true - discord.js-light@4.10.0: dependencies: discord.js: 13.17.1 @@ -661,7 +1461,7 @@ snapshots: '@discordjs/collection': 0.7.0 '@sapphire/async-queue': 1.5.5 '@types/node-fetch': 2.6.12 - '@types/ws': 8.5.13 + '@types/ws': 8.5.14 discord-api-types: 0.33.5 form-data: 4.0.1 node-fetch: 2.7.0 @@ -689,9 +1489,15 @@ snapshots: - bufferutil - utf-8-validate - dotenv@16.4.7: {} + dotenv@16.5.0: {} + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} - eris@0.17.2: + eris@0.18.0: dependencies: ws: 8.18.0 optionalDependencies: @@ -701,11 +1507,52 @@ snapshots: - bufferutil - utf-8-validate + es-module-lexer@1.7.0: {} + + esbuild@0.25.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.3 + '@esbuild/android-arm': 0.25.3 + '@esbuild/android-arm64': 0.25.3 + '@esbuild/android-x64': 0.25.3 + '@esbuild/darwin-arm64': 0.25.3 + '@esbuild/darwin-x64': 0.25.3 + '@esbuild/freebsd-arm64': 0.25.3 + '@esbuild/freebsd-x64': 0.25.3 + '@esbuild/linux-arm': 0.25.3 + '@esbuild/linux-arm64': 0.25.3 + '@esbuild/linux-ia32': 0.25.3 + '@esbuild/linux-loong64': 0.25.3 + '@esbuild/linux-mips64el': 0.25.3 + '@esbuild/linux-ppc64': 0.25.3 + '@esbuild/linux-riscv64': 0.25.3 + '@esbuild/linux-s390x': 0.25.3 + '@esbuild/linux-x64': 0.25.3 + '@esbuild/netbsd-arm64': 0.25.3 + '@esbuild/netbsd-x64': 0.25.3 + '@esbuild/openbsd-arm64': 0.25.3 + '@esbuild/openbsd-x64': 0.25.3 + '@esbuild/sunos-x64': 0.25.3 + '@esbuild/win32-arm64': 0.25.3 + '@esbuild/win32-ia32': 0.25.3 + '@esbuild/win32-x64': 0.25.3 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.7 + + expect-type@1.2.1: {} + fast-deep-equal@3.1.3: {} - fill-range@7.1.1: + fdir@6.4.4(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + foreground-child@3.3.1: dependencies: - to-regex-range: 5.0.1 + cross-spawn: 7.0.6 + signal-exit: 4.1.0 form-data@4.0.1: dependencies: @@ -716,33 +1563,73 @@ snapshots: fsevents@2.3.3: optional: true - glob-parent@5.1.2: + glob@10.4.5: dependencies: - is-glob: 4.0.3 + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + has-flag@4.0.0: {} - has-flag@3.0.0: {} + html-escaper@2.0.2: {} - ignore-by-default@1.0.1: {} + is-fullwidth-code-point@3.0.0: {} - is-binary-path@2.1.0: + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: dependencies: - binary-extensions: 2.3.0 + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 - is-extglob@2.1.1: {} + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + debug: 4.4.0 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color - is-glob@4.0.3: + istanbul-reports@3.1.7: dependencies: - is-extglob: 2.1.1 + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 - is-number@7.0.0: {} + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 lodash.snakecase@4.1.1: {} lodash@4.17.21: {} + loupe@3.1.3: {} + + lru-cache@10.4.3: {} + magic-bytes.js@1.10.0: {} - make-error@1.3.6: {} + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.1 mime-db@1.52.0: {} @@ -750,37 +1637,26 @@ snapshots: dependencies: mime-db: 1.52.0 - minimatch@3.1.2: + minimatch@9.0.5: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 2.0.1 + + minipass@7.1.2: {} ms@2.1.3: {} + nanoid@3.3.11: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 - nodemon@3.1.9: - dependencies: - chokidar: 3.6.0 - debug: 4.4.0(supports-color@5.5.0) - ignore-by-default: 1.0.1 - minimatch: 3.1.2 - pstree.remy: 1.1.8 - semver: 7.6.3 - simple-update-notifier: 2.0.0 - supports-color: 5.5.0 - touch: 3.1.1 - undefsafe: 2.0.5 - - normalize-path@3.0.0: {} - - oceanic.js@1.11.2(opusscript@0.0.8): + oceanic.js@1.12.0(opusscript@0.0.8): dependencies: tslib: 2.8.1 - ws: 8.18.0 + ws: 8.18.1 optionalDependencies: - '@discordjs/voice': 0.17.0(opusscript@0.0.8) + '@discordjs/voice': 0.18.0(opusscript@0.0.8) transitivePeerDependencies: - '@discordjs/opus' - bufferutil @@ -792,71 +1668,209 @@ snapshots: opusscript@0.0.8: optional: true - picomatch@2.3.1: {} + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + pathe@2.0.3: {} + + pathval@2.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.2: {} + + postcss@8.5.3: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 prism-media@1.3.5(opusscript@0.0.8): optionalDependencies: opusscript: 0.0.8 optional: true - pstree.remy@1.1.8: {} + rollup@4.40.0: + dependencies: + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.40.0 + '@rollup/rollup-android-arm64': 4.40.0 + '@rollup/rollup-darwin-arm64': 4.40.0 + '@rollup/rollup-darwin-x64': 4.40.0 + '@rollup/rollup-freebsd-arm64': 4.40.0 + '@rollup/rollup-freebsd-x64': 4.40.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.40.0 + '@rollup/rollup-linux-arm-musleabihf': 4.40.0 + '@rollup/rollup-linux-arm64-gnu': 4.40.0 + '@rollup/rollup-linux-arm64-musl': 4.40.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.40.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0 + '@rollup/rollup-linux-riscv64-gnu': 4.40.0 + '@rollup/rollup-linux-riscv64-musl': 4.40.0 + '@rollup/rollup-linux-s390x-gnu': 4.40.0 + '@rollup/rollup-linux-x64-gnu': 4.40.0 + '@rollup/rollup-linux-x64-musl': 4.40.0 + '@rollup/rollup-win32-arm64-msvc': 4.40.0 + '@rollup/rollup-win32-ia32-msvc': 4.40.0 + '@rollup/rollup-win32-x64-msvc': 4.40.0 + fsevents: 2.3.3 + + semver@7.7.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} - readdirp@3.6.0: + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.9.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + strip-ansi@6.0.1: dependencies: - picomatch: 2.3.1 + ansi-regex: 5.0.1 - semver@7.6.3: {} + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 - simple-update-notifier@2.0.0: + supports-color@7.2.0: dependencies: - semver: 7.6.3 + has-flag: 4.0.0 - supports-color@5.5.0: + test-exclude@7.0.1: dependencies: - has-flag: 3.0.0 + '@istanbuljs/schema': 0.1.3 + glob: 10.4.5 + minimatch: 9.0.5 - to-regex-range@5.0.1: + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.13: dependencies: - is-number: 7.0.0 + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + + tinypool@1.0.2: {} + + tinyrainbow@2.0.0: {} - touch@3.1.1: {} + tinyspy@3.0.2: {} tr46@0.0.3: {} ts-mixer@6.0.4: {} - ts-node@10.9.2(@types/node@20.17.28)(typescript@5.8.2): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 20.17.28 - acorn: 8.14.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.8.2 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - tslib@2.8.1: {} tweetnacl@1.0.3: optional: true - typescript@5.8.2: {} - - undefsafe@2.0.5: {} + typescript@5.8.3: {} - undici-types@6.19.8: {} + undici-types@6.21.0: {} undici@6.21.1: {} - v8-compile-cache-lib@3.0.1: {} + vite-node@3.1.2(@types/node@22.15.2): + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.3.3(@types/node@22.15.2) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@6.3.3(@types/node@22.15.2): + dependencies: + esbuild: 0.25.3 + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.3 + rollup: 4.40.0 + tinyglobby: 0.2.13 + optionalDependencies: + '@types/node': 22.15.2 + fsevents: 2.3.3 + + vitest@3.1.2(@types/node@22.15.2): + dependencies: + '@vitest/expect': 3.1.2 + '@vitest/mocker': 3.1.2(vite@6.3.3(@types/node@22.15.2)) + '@vitest/pretty-format': 3.1.2 + '@vitest/runner': 3.1.2 + '@vitest/snapshot': 3.1.2 + '@vitest/spy': 3.1.2 + '@vitest/utils': 3.1.2 + chai: 5.2.0 + debug: 4.4.0 + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.13 + tinypool: 1.0.2 + tinyrainbow: 2.0.0 + vite: 6.3.3(@types/node@22.15.2) + vite-node: 3.1.2(@types/node@22.15.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.15.2 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml webidl-conversions@3.0.1: {} @@ -865,6 +1879,27 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + ws@8.18.0: {} - yn@3.1.1: {} + ws@8.18.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..4340350 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'packages/*' \ No newline at end of file diff --git a/src/discordjs-light/index.ts b/src/discordjs-light/index.ts deleted file mode 100644 index 4510129..0000000 --- a/src/discordjs-light/index.ts +++ /dev/null @@ -1,270 +0,0 @@ -import npmPackageData from "../../package.json"; -import fetch from "node-fetch"; -import { ApiEndpoints, ApplicationCommandType, DiscordAnalyticsOptions, ErrorCodes, InteractionData, InteractionType, Locale, TrackGuildType } from "../utils/types"; - -/** - * @class DiscordAnalytics - * @description The Discord.js-light class for the DiscordAnalytics library. - * @param {DiscordAnalyticsOptions} options - Configuration options. - * @property {any} options.client - The Discord.js-light client to track events for. - * @property {string} options.apiToken - The API token for DiscordAnalytics. - * @property {boolean} options.sharded - Whether the Discord.js-light client is sharded. - * @property {boolean} options.debug - Enable or not the debug mode /!\ MUST BE USED ONLY FOR DEVELOPMENT PURPOSES /!\ - * @example - * const { default: DiscordAnalytics } = require('discord-analytics/discord.js-light'); - * const { Client, IntentsBitField } = require('discord.js-light'); - * const client = new Client({ - * intents: ["GUILDS"] - * }) - * client.on('ready', () => { - * const analytics = new DiscordAnalytics({ - * client: client, - * apiToken: 'YOUR_API_TOKEN' - * }); - * analytics.trackEvents(); - * }); - * client.login('YOUR_BOT_TOKEN'); - * - * // Check docs for more informations about advanced usages : https://docs.discordanalytics.xyz/get-started/installation/discord.js - */ -export default class DiscordAnalytics { - private readonly _client: any; - private readonly _apiToken: string; - private readonly _sharded: boolean = false; - private readonly _debug: boolean = false - private readonly _headers: { 'Content-Type': string; Authorization: string; }; - private _isReady: boolean - - constructor(options: DiscordAnalyticsOptions) { - this._client = options.client; - this._apiToken = options.apiToken; - this._headers = { - 'Content-Type': 'application/json', - 'Authorization': `Bot ${this._apiToken}` - } - this._sharded = options.sharded || false; - this._isReady = false - this._debug = options.debug || false - } - - /** - * Initialize DiscordAnalytics on your bot - * /!\ Advanced users only - * /!\ Required to use DiscordAnalytics (except if you use the trackEvents function) - * /!\ Must be used when the client is ready (recommended to use in ready event to prevent problems) - */ - public async init() { - fetch(`${ApiEndpoints.BASE_URL}${ApiEndpoints.EDIT_SETTINGS_URL.replace(':id', this._client.user.id)}`, { - headers: this._headers, - body: JSON.stringify({ - username: this._client.user.username, - avatar: this._client.user.avatar, - framework: "discord.js-light", - version: npmPackageData.version, - team: this._client.application.owner - ? this._client.application.owner.hasOwnProperty("members") - ? this._client.application.owner.members.map((member: any) => member.user.id) - : [this._client.application.owner.id] - : [], - }), - method: "PATCH" - }).then(async (res) => { - if (res.status === 401) return console.error(ErrorCodes.INVALID_API_TOKEN) - else if (res.status === 423) return console.error(ErrorCodes.SUSPENDED_BOT) - else if (res.status !== 200) return console.error(ErrorCodes.INVALID_RESPONSE) - - if (this._debug) console.debug("[DISCORDANALYTICS] Instance successfully initialized") - this._isReady = true - - if (this._debug) { - if (process.argv[2] === "--dev") console.debug("[DISCORDANALYTICS] DevMode is enabled. Stats will be sent every 30s.") - else console.debug("[DISCORDANALYTICS] DevMode is disabled. Stats will be sent every 5min.") - } - - setInterval(async () => { - if (this._debug) console.debug("[DISCORDANALYTICS] Sending stats...") - - let guildCount = this._sharded ? - ((await this._client.shard?.broadcastEval((c: any) => c.guilds.cache.size))?.reduce((a: number, b: number) => a + b, 0) || 0) : - this._client.guilds.cache.size; - - let userCount = this._sharded ? - ((await this._client.shard?.broadcastEval((c: any) => c.guilds.cache.reduce((a: number, g: any) => a + (g.memberCount || 0), 0)))?.reduce((a: number, b: number) => a + b, 0) || 0) : - this._client.guilds.cache.reduce((a: number, g: any) => a + (g.memberCount || 0), 0); - - fetch(`${ApiEndpoints.BASE_URL}${ApiEndpoints.EDIT_STATS_URL.replace(':id', this._client.user!.id)}`, { - headers: this._headers, - body: JSON.stringify(this.statsData), - method: "POST" - }).then(async (res) => { - if (res.status === 401) return console.error(ErrorCodes.INVALID_API_TOKEN) - else if (res.status === 423) return console.error(ErrorCodes.SUSPENDED_BOT) - else if (res.status !== 200) return console.error(ErrorCodes.INVALID_RESPONSE) - if (res.status === 200) { - if (this._debug) console.debug(`[DISCORDANALYTICS] Stats ${JSON.stringify(this.statsData)} sent to the API`) - - this.statsData = { - date: new Date().toISOString().slice(0, 10), - guilds: guildCount, - users: userCount, - interactions: [], - locales: [], - guildsLocales: [], - guildMembers: await this.calculateGuildMembersRepartition(), - guildsStats: [], - addedGuilds: 0, - removedGuilds: 0, - users_type: { - admin: 0, - moderator: 0, - new_member: 0, - other: 0, - private_message: 0 - } - } - } - }).catch(e => { - if (this._debug) { - console.debug("[DISCORDANALYTICS] " + ErrorCodes.DATA_NOT_SENT); - console.error(e) - } - }); - }, process.argv[2] === "--dev" ? 30000 : 5 * 60000); - }) - } - - private statsData = { - date: new Date().toISOString().slice(0, 10), - guilds: 0, - users: 0, - interactions: [] as InteractionData[], - locales: [] as { locale: Locale, number: number }[], - guildsLocales: [] as { locale: Locale, number: number }[], - guildMembers: { - little: 0, - medium: 0, - big: 0, - huge: 0 - }, - guildsStats: [] as { guildId: string, name: string, icon: string | undefined, members: number, interactions: number }[], - addedGuilds: 0, - removedGuilds: 0, - users_type: { - admin: 0, - moderator: 0, - new_member: 0, - other: 0, - private_message: 0 - } - } - - private async calculateGuildMembersRepartition(): Promise<{ little: number, medium: number, big: number, huge: number }> { - const res = { - little: 0, - medium: 0, - big: 0, - huge: 0 - } - - let guildsMembers: number[] = [] - - if (!this._sharded) guildsMembers = this._client.guilds.cache.map((guild: any) => guild.memberCount) - else guildsMembers = [].concat(await this._client.shard?.broadcastEval((c: any) => c.guilds.cache.map((guild: any) => guild.memberCount))) - - for (const guild of guildsMembers) { - if (guild <= 100) res.little++ - else if (guild > 100 && guild <= 500) res.medium++ - else if (guild > 500 && guild <= 1500) res.big++ - else if (guild > 1500) res.huge++ - } - - return res - } - - /** - * Track interactions - * /!\ Advanced users only - * /!\ You need to initialize the class first - * @param interaction - BaseInteraction class and its extensions only - */ - public async trackInteractions(interaction: any) { - if (this._debug) console.log("[DISCORDANALYTICS] trackInteractions() triggered") - if (!this._isReady) throw new Error(ErrorCodes.INSTANCE_NOT_INITIALIZED) - - let guilds: { locale: Locale, number: number }[] = [] - this._client.guilds.cache.map((current: any) => guilds.find((x) => x.locale === current.preferredLocale) ? - ++guilds.find((x) => x.locale === current.preferredLocale)!.number : - guilds.push({ locale: current.preferredLocale, number: 1 })); - - this.statsData.guildsLocales = guilds - - this.statsData.locales.find((x) => x.locale === interaction.locale) ? - ++this.statsData.locales.find((x) => x.locale === interaction.locale)!.number : - this.statsData.locales.push({ locale: interaction.locale as Locale, number: 1 }); - - if (interaction.isCommand()) { - const commandType = interaction.command ? - interaction.command.type === "USER" - ? ApplicationCommandType.UserCommand - : interaction.command.type === "MESSAGE" - ? ApplicationCommandType.MessageCommand - : ApplicationCommandType.ChatInputCommand - : ApplicationCommandType.ChatInputCommand - this.statsData.interactions.find((x) => x.name === interaction.commandName && x.type === InteractionType.ApplicationCommand && x.command_type === commandType) ? - ++this.statsData.interactions.find((x) => x.name === interaction.commandName && x.type === InteractionType.ApplicationCommand)!.number : - this.statsData.interactions.push({ name: interaction.commandName, number: 1, type: InteractionType.ApplicationCommand, command_type: commandType }); - } else if (interaction.isMessageComponent()) { - this.statsData.interactions.find((x) => x.name === interaction.customId && x.type === InteractionType.MessageComponent) ? - ++this.statsData.interactions.find((x) => x.name === interaction.customId && x.type === InteractionType.MessageComponent)!.number : - this.statsData.interactions.push({ name: interaction.customId, number: 1, type: InteractionType.MessageComponent }); - } else if (interaction.isModalSubmit()) { - this.statsData.interactions.find((x) => x.name === interaction.customId && x.type === InteractionType.ModalSubmit) ? - ++this.statsData.interactions.find((x) => x.name === interaction.customId && x.type === InteractionType.ModalSubmit)!.number : - this.statsData.interactions.push({ name: interaction.customId, number: 1, type: InteractionType.ModalSubmit }); - } - - const guildData = this.statsData.guildsStats.find(guild => interaction.guild ? guild.guildId === interaction.guild.id : guild.guildId === "dm") - if (guildData) this.statsData.guildsStats = this.statsData.guildsStats.filter(guild => guild.guildId !== guildData.guildId) - this.statsData.guildsStats.push({ - guildId: interaction.guild ? interaction.guild.id : "dm", - name: interaction.guild ? interaction.guild.name : "DM", - icon: interaction.guild && interaction.guild.icon ? interaction.guild.icon : undefined, - interactions: guildData ? guildData.interactions + 1 : 1, - members: interaction.guild ? interaction.guild.memberCount : 0 - }) - - const oneWeekAgo = new Date() - oneWeekAgo.setDate(oneWeekAgo.getDate() - 7) - - let member = interaction.member - if (!interaction.inGuild()) ++this.statsData.users_type.private_message - else if (member && member.permissions && member.permissions.has(8n) || member.permissions.has(32n)) ++this.statsData.users_type.admin - else if (member && member.permissions && member.permissions.has(8192n) || member.permissions.has(2n) || member.permissions.has(4n) || member.permissions.has(4194304n) || member.permissions.has(8388608n) || member.permissions.has(16777216n) || member.permissions.has(1099511627776n)) ++this.statsData.users_type.moderator - else if (member && member.joinedAt && member.joinedAt > oneWeekAgo) ++this.statsData.users_type.new_member - } - - /** - * Track guilds - * /!\ Advanced users only - * /!\ You need to initialize the class first - * @param guild - The Guild instance only - * @param {TrackGuildType} type - "create" if the event is guildCreate and "delete" if is guildDelete - */ - public async trackGuilds(guild: any, type: TrackGuildType) { - if (this._debug) console.log(`[DISCORDANALYTICS] trackGuilds(${type}) triggered`) - if (type === "create") this.statsData.addedGuilds++ - else this.statsData.removedGuilds++ - } - /** - * Let DiscordAnalytics declare the events necessary for its operation. - * /!\ Not recommended for big bots - * /!\ Not compatible with other functions - */ - public trackEvents() { - if (!this._client.isReady()) this._client.on("ready", async () => await this.init()) - else this.init() - this._client.on("interactionCreate", async (interaction: any) => await this.trackInteractions(interaction)) - this._client.on("guildCreate", (guild: any) => this.trackGuilds(guild, "create")) - this._client.on("guildDelete", (guild: any) => this.trackGuilds(guild, "delete")) - } -} diff --git a/src/discordjs/index.ts b/src/discordjs/index.ts deleted file mode 100644 index 2573e90..0000000 --- a/src/discordjs/index.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { - ApiEndpoints, - ApplicationCommandType, - DiscordAnalyticsOptions, - ErrorCodes, - InteractionData, - InteractionType, - Locale, - TrackGuildType -} from "../utils/types"; -import npmPackageData from "../../package.json"; -import fetch from "node-fetch"; - -/** - * @class DiscordAnalytics - * @description The Discord.js class for the DiscordAnalytics library. - * @param {DiscordAnalyticsOptions} options - Configuration options. - * @property {any} options.client - The Discord.js client to track events for. - * @property {string} options.apiToken - The API token for DiscordAnalytics. - * @property {boolean} options.sharded - Whether the Discord.js client is sharded. - * @property {boolean} options.debug - Enable or not the debug mode /!\ MUST BE USED ONLY FOR DEVELOPMENT PURPOSES /!\ - * @example - * const { default: DiscordAnalytics } = require('discord-analytics/discordjs'); - * const { Client, IntentsBitField } = require('discord.js'); - * const client = new Client({ - * intents: [IntentsBitField.Flags.Guilds] - * }) - * client.on('ready', () => { - * const analytics = new DiscordAnalytics({ - * client: client, - * apiToken: 'YOUR_API_TOKEN' - * }); - * analytics.trackEvents(); - * }); - * client.login('YOUR_BOT_TOKEN'); - * - * // Check docs for more informations about advanced usages : https://docs.discordanalytics.xyz/get-started/installation/discord.js - */ -export default class DiscordAnalytics { - private readonly _client: any; - private readonly _apiToken: string; - private readonly _sharded: boolean = false; - private readonly _debug: boolean = false - private readonly _headers: { 'Content-Type': string; Authorization: string; }; - private _isReady: boolean - - constructor(options: DiscordAnalyticsOptions) { - this._client = options.client; - this._apiToken = options.apiToken; - this._headers = { - 'Content-Type': 'application/json', - 'Authorization': `Bot ${this._apiToken}` - } - this._sharded = options.sharded || false; - this._isReady = false - this._debug = options.debug || false - } - - /** - * Initialize DiscordAnalytics on your bot - * /!\ Advanced users only - * /!\ Required to use DiscordAnalytics (except if you use the trackEvents function) - * /!\ Must be used when the client is ready (recommended to use in ready event to prevent problems) - */ - public async init() { - fetch(`${ApiEndpoints.BASE_URL}${ApiEndpoints.EDIT_SETTINGS_URL.replace(':id', this._client.user.id)}`, { - headers: this._headers, - body: JSON.stringify({ - username: this._client.user.username, - avatar: this._client.user.avatar, - framework: "discord.js", - version: npmPackageData.version, - team: this._client.application.owner - ? this._client.application.owner.hasOwnProperty("members") - ? this._client.application.owner.members.map((member: any) => member.user.id) - : [this._client.application.owner.id] - : [], - }), - method: "PATCH" - }).then(async (res) => { - if (res.status === 401) return console.error(ErrorCodes.INVALID_API_TOKEN) - else if (res.status === 423) return console.error(ErrorCodes.SUSPENDED_BOT) - else if (res.status !== 200) return console.error(ErrorCodes.INVALID_RESPONSE) - - if (this._debug) console.debug("[DISCORDANALYTICS] Instance successfully initialized") - this._isReady = true - - if (this._debug) { - if (process.argv[2] === "--dev") console.debug("[DISCORDANALYTICS] DevMode is enabled. Stats will be sent every 30s.") - else console.debug("[DISCORDANALYTICS] DevMode is disabled. Stats will be sent every 5min.") - } - - setInterval(async () => { - if (this._debug) console.debug("[DISCORDANALYTICS] Sending stats...") - - let guildCount = this._sharded ? - ((await this._client.shard?.broadcastEval((c: any) => c.guilds.cache.size))?.reduce((a: number, b: number) => a + b, 0) || 0) : - this._client.guilds.cache.size; - - let userCount = this._sharded ? - ((await this._client.shard?.broadcastEval((c: any) => c.guilds.cache.reduce((a: number, g: any) => a + (g.memberCount || 0), 0)))?.reduce((a: number, b: number) => a + b, 0) || 0) : - this._client.guilds.cache.reduce((a: number, g: any) => a + (g.memberCount || 0), 0); - - fetch(`${ApiEndpoints.BASE_URL}${ApiEndpoints.EDIT_STATS_URL.replace(':id', this._client.user!.id)}`, { - headers: this._headers, - body: JSON.stringify(this.statsData), - method: "POST" - }).then(async (res) => { - if (res.status === 401) return console.error(ErrorCodes.INVALID_API_TOKEN) - else if (res.status === 423) return console.error(ErrorCodes.SUSPENDED_BOT) - else if (res.status !== 200) return console.error(ErrorCodes.INVALID_RESPONSE) - if (res.status === 200) { - if (this._debug) console.debug(`[DISCORDANALYTICS] Stats ${JSON.stringify(this.statsData)} sent to the API`) - - this.statsData = { - date: new Date().toISOString().slice(0, 10), - guilds: guildCount, - users: userCount, - interactions: [], - locales: [], - guildsLocales: [], - guildMembers: await this.calculateGuildMembersRepartition(), - guildsStats: [], - addedGuilds: 0, - removedGuilds: 0, - users_type: { - admin: 0, - moderator: 0, - new_member: 0, - other: 0, - private_message: 0 - } - } - } - }).catch(e => { - if (this._debug) { - console.debug("[DISCORDANALYTICS] " + ErrorCodes.DATA_NOT_SENT); - console.error(e) - } - }); - }, process.argv[2] === "--dev" ? 30000 : 5 * 60000); - }) - } - - private statsData = { - date: new Date().toISOString().slice(0, 10), - guilds: 0, - users: 0, - interactions: [] as InteractionData[], - locales: [] as { locale: Locale, number: number }[], - guildsLocales: [] as { locale: Locale, number: number }[], - guildMembers: { - little: 0, - medium: 0, - big: 0, - huge: 0 - }, - guildsStats: [] as { guildId: string, name: string, icon: string, members: number, interactions: number }[], - addedGuilds: 0, - removedGuilds: 0, - users_type: { - admin: 0, - moderator: 0, - new_member: 0, - other: 0, - private_message: 0 - } - } - - private async calculateGuildMembersRepartition(): Promise<{ little: number, medium: number, big: number, huge: number }> { - const res = { - little: 0, - medium: 0, - big: 0, - huge: 0 - } - - let guildsMembers: number[] = [] - - if (!this._sharded) guildsMembers = this._client.guilds.cache.map((guild: any) => guild.memberCount) - else guildsMembers = [].concat(await this._client.shard?.broadcastEval((c: any) => c.guilds.cache.map((guild: any) => guild.memberCount))) - - for (const guild of guildsMembers) { - if (guild <= 100) res.little++ - else if (guild > 100 && guild <= 500) res.medium++ - else if (guild > 500 && guild <= 1500) res.big++ - else if (guild > 1500) res.huge++ - } - - return res - } - - /** - * Track interactions - * /!\ Advanced users only - * /!\ You need to initialize the class first - * @param interaction - BaseInteraction class and its extensions only - * @param interactionNameResolver - A function that will resolve the name of the interaction - */ - public async trackInteractions(interaction: any, interactionNameResolver?: (interaction: any) => string) { - if (this._debug) console.log("[DISCORDANALYTICS] trackInteractions() triggered") - if (!this._isReady) return console.error(ErrorCodes.INSTANCE_NOT_INITIALIZED) - - let guilds: { locale: Locale, number: number }[] = [] - this._client.guilds.cache.map((current: any) => guilds.find((x) => x.locale === current.preferredLocale) ? - ++guilds.find((x) => x.locale === current.preferredLocale)!.number : - guilds.push({ locale: current.preferredLocale, number: 1 })); - - this.statsData.guildsLocales = guilds - - this.statsData.locales.find((x) => x.locale === interaction.locale) ? - ++this.statsData.locales.find((x) => x.locale === interaction.locale)!.number : - this.statsData.locales.push({ locale: interaction.locale, number: 1 }); - - if (interaction.type === InteractionType.ApplicationCommand) { - const commandType = interaction.command ? interaction.command.type : ApplicationCommandType.ChatInputCommand; - const commandName = interactionNameResolver ? interactionNameResolver(interaction) : interaction.commandName; - this.statsData.interactions.find((x) => x.name === commandName && x.type === interaction.type && x.command_type === commandType) ? - ++this.statsData.interactions.find((x) => x.name === commandName && x.type === interaction.type && x.command_type === commandType)!.number : - this.statsData.interactions.push({ name: commandName, number: 1, type: interaction.type as InteractionType, command_type: commandType }); - } - - else if (interaction.type === InteractionType.MessageComponent || interaction.type === InteractionType.ModalSubmit) { - const interactionName = interactionNameResolver ? interactionNameResolver(interaction) : interaction.customId; - - this.statsData.interactions.find((x) => x.name === interactionName && x.type === interaction.type) ? - ++this.statsData.interactions.find((x) => x.name === interactionName && x.type === interaction.type)!.number : - this.statsData.interactions.push({name: interactionName, number: 1, type: interaction.type}); - } - - const guildData = this.statsData.guildsStats.find(guild => interaction.guild ? guild.guildId === interaction.guild.id : guild.guildId === "dm") - if (guildData) this.statsData.guildsStats = this.statsData.guildsStats.filter(guild => guild.guildId !== guildData.guildId) - this.statsData.guildsStats.push({ - guildId: interaction.guild ? interaction.guild.id : "dm", - name: interaction.guild ? interaction.guild.name : "DM", - icon: interaction.guild && interaction.guild.icon ? interaction.guild.icon : undefined, - interactions: guildData ? guildData.interactions + 1 : 1, - members: interaction.guild ? interaction.guild.memberCount : 0 - }) - - const oneWeekAgo = new Date() - oneWeekAgo.setDate(oneWeekAgo.getDate() - 7) - - if (!interaction.inGuild()) ++this.statsData.users_type.private_message - else if (interaction.member && interaction.member.permissions && interaction.member.permissions.has(8n) || interaction.member.permissions.has(32n)) ++this.statsData.users_type.admin - else if (interaction.member && interaction.member.permissions && interaction.member.permissions.has(8192n) || interaction.member.permissions.has(2n) || interaction.member.permissions.has(4n) || interaction.member.permissions.has(4194304n) || interaction.member.permissions.has(8388608n) || interaction.member.permissions.has(16777216n) || interaction.member.permissions.has(1099511627776n)) ++this.statsData.users_type.moderator - else if (interaction.member && interaction.member.joinedAt && interaction.member.joinedAt > oneWeekAgo) ++this.statsData.users_type.new_member - } - - /** - * Track guilds - * /!\ Advanced users only - * /!\ You need to initialize the class first - * @param guild - The Guild instance only - * @param {TrackGuildType} type - "create" if the event is guildCreate and "delete" if is guildDelete - */ - public async trackGuilds(guild: any, type: TrackGuildType) { - if (this._debug) console.log(`[DISCORDANALYTICS] trackGuilds(${type}) triggered`) - if (type === "create") this.statsData.addedGuilds++ - else this.statsData.removedGuilds++ - } - - /** - * Let DiscordAnalytics declare the events necessary for its operation. - * /!\ Not recommended for big bots - * /!\ Not compatible with other functions - * @param interactionNameResolver - A function that will resolve the name of the interaction - */ - public trackEvents(interactionNameResolver?: (interaction: any) => string) { - if (!this._client.isReady()) this._client.on("ready", async () => await this.init()) - else this.init() - this._client.on("interactionCreate", async (interaction: any) => await this.trackInteractions(interaction, interactionNameResolver)) - this._client.on("guildCreate", (guild: any) => this.trackGuilds(guild, "create")) - this._client.on("guildDelete", (guild: any) => this.trackGuilds(guild, "delete")) - } -} diff --git a/src/eris/index.ts b/src/eris/index.ts deleted file mode 100644 index fc51ad7..0000000 --- a/src/eris/index.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { - ApiEndpoints, - DiscordAnalyticsOptions, - ErrorCodes, - InteractionType, - Locale, - TrackGuildType -} from "../utils/types"; -import npmPackageData from "../../package.json"; -import fetch from "node-fetch"; - -/** - * @class DiscordAnalytics - * @description The Eris class for the DiscordAnalytics library. - * @param {DiscordAnalyticsOptions} options - Configuration options. - * @property {any} options.client - The Eris client to track events for. - * @property {string} options.apiToken - The API token for DiscordAnalytics. - * @property {boolean} options.sharded - /!\ Not compatible with Eris - * @property {boolean} options.debug - Enable or not the debug mode /!\ MUST BE USED ONLY FOR DEVELOPMENT PURPOSES /!\ - * @example - * const { default: DiscordAnalytics } = require('discord-analytics/eris'); - * const Eris = require('eris'); - * const client = new Client("YOUR_BOT_TOKEN", { - * intents: ["guilds"] - * }) - * client.on('ready', () => { - * const analytics = new DiscordAnalytics({ - * client: client, - * apiToken: 'YOUR_API_TOKEN' - * }); - * analytics.trackEvents(); - * }); - * client.connect() - * - * // Check docs for more informations about advanced usages : https://docs.discordanalytics.xyz/get-started/installation/eris - */ -export default class DiscordAnalytics { - private readonly _client: any; - private readonly _apiToken: string; - private readonly _sharded: boolean = false; - private readonly _debug: boolean - private readonly _headers: { 'Content-Type': string; Authorization: string; }; - private _isReady: boolean - - constructor(options: Omit) { - this._client = options.client; - this._apiToken = options.apiToken; - this._headers = { - 'Content-Type': 'application/json', - 'Authorization': `Bot ${this._apiToken}` - } - this._isReady = false - this._debug = options.debug || false - } - - /** - * Initialize DiscordAnalytics on your bot - * /!\ Advanced users only - * /!\ Required to use DiscordAnalytics (except if you use the trackEvents function) - * /!\ Must be used when the client is ready (recommended to use in ready event to prevent problems) - */ - public async init () { - fetch(`${ApiEndpoints.BASE_URL}${ApiEndpoints.EDIT_SETTINGS_URL.replace(':id', this._client.user.id)}`, { - headers: this._headers, - body: JSON.stringify({ - username: this._client.user.username, - avatar: this._client.user.avatar, - framework: "eris", - version: npmPackageData.version, - }), - method: "PATCH" - }).then(async (res) => { - if (res.status === 401) return console.error(ErrorCodes.INVALID_API_TOKEN) - else if (res.status === 423) return console.error(ErrorCodes.SUSPENDED_BOT) - else if (res.status !== 200) return console.error(ErrorCodes.INVALID_RESPONSE) - - if (this._debug) console.debug("[DISCORDANALYTICS] Instance successfully initialized") - this._isReady = true - - if (this._debug) { - if (process.argv[2] === "--dev") console.debug("[DISCORDANALYTICS] DevMode is enabled. Stats will be sent every 30s.") - else console.debug("[DISCORDANALYTICS] DevMode is disabled. Stats will be sent every 5min.") - } - - setInterval(async () => { - if (this._debug) console.debug("[DISCORDANALYTICS] Sending stats...") - - let guildCount = this._client.guilds.size - let userCount = this._client.guilds.reduce((a: number, g: any) => a + g.memberCount, 0) - - fetch(`${ApiEndpoints.BASE_URL}${ApiEndpoints.EDIT_STATS_URL.replace(':id', this._client.user!.id)}`, { - headers: this._headers, - body: JSON.stringify(this.statsData), - method: "POST" - }).then(async (res) => { - if (res.status === 401) return console.error(ErrorCodes.INVALID_API_TOKEN) - else if (res.status === 423) return console.error(ErrorCodes.SUSPENDED_BOT) - else if (res.status !== 200) return console.error(ErrorCodes.INVALID_RESPONSE) - if (res.status === 200) { - if (this._debug) console.debug(`[DISCORDANALYTICS] Stats ${JSON.stringify(this.statsData)} sent to the API`) - - this.statsData = { - date: new Date().toISOString().slice(0, 10), - guilds: guildCount, - users: userCount, - interactions: [], - locales: [], - guildsLocales: [], - guildMembers: await this.calculateGuildMembersRepartition(), - guildsStats: [], - addedGuilds: 0, - removedGuilds: 0, - users_type: { - admin: 0, - moderator: 0, - new_member: 0, - other: 0, - private_message: 0 - } - } - } - }).catch(e => { - if (this._debug) { - console.debug("[DISCORDANALYTICS] " + ErrorCodes.DATA_NOT_SENT); - console.error(e) - } - }); - }, process.argv[2] === "--dev" ? 30000 : 5 * 60000); - }) - } - - private statsData = { - date: new Date().toISOString().slice(0, 10), - guilds: 0, - users: 0, - interactions: [] as { name: string, number: number, type: InteractionType }[], - locales: [] as { locale: Locale, number: number }[], - guildsLocales: [] as { locale: Locale, number: number }[], - guildMembers: { - little: 0, - medium: 0, - big: 0, - huge: 0 - }, - guildsStats: [] as { guildId: string, name: string, icon: string, members: number, interactions: number }[], - addedGuilds: 0, - removedGuilds: 0, - users_type: { - admin: 0, - moderator: 0, - new_member: 0, - other: 0, - private_message: 0 - } - } - - private async calculateGuildMembersRepartition (): Promise<{ little: number, medium: number, big: number, huge: number }> { - const res = { - little: 0, - medium: 0, - big: 0, - huge: 0 - } - - let guildsMembers: number[] = this._client.guilds.map((guild: any) => guild.memberCount) - - - for (const guild of guildsMembers) { - if (guild <= 100) res.little++ - else if (guild > 100 && guild <= 500) res.medium++ - else if (guild > 500 && guild <= 1500) res.big++ - else if (guild > 1500) res.huge++ - } - - return res - } - - /** - * Track interactions - * /!\ Advanced users only - * /!\ You need to initialize the class first - * @param interaction - BaseInteraction class and its extensions only - */ - public async trackInteractions (interaction: any) { - if (this._debug) console.log("[DISCORDANALYTICS] trackInteractions() triggered") - if (!this._isReady) throw new Error(ErrorCodes.INSTANCE_NOT_INITIALIZED) - - let guilds: { locale: string, number: number }[] = [] - this._client.guilds.map((current: any) => guilds.find((x) => x.locale === current.preferredLocale) ? - ++guilds.find((x) => x.locale === current.preferredLocale)!.number : - guilds.push({ locale: current.preferredLocale, number: 1 })); - - this.statsData.guildsLocales = guilds as { locale: Locale, number: number }[]; - - if (interaction.type === 2) this.statsData.interactions.find((x) => x.name === interaction.data.name && x.type === interaction.type) ? - ++this.statsData.interactions.find((x) => x.name === interaction.data.name && x.type === interaction.type)!.number : - this.statsData.interactions.push({name: interaction.data.name, number: 1, type: interaction.type}); - else if (interaction.type === 3) this.statsData.interactions.find((x) => x.name === interaction.data.custom_id && x.type === interaction.type) ? - ++this.statsData.interactions.find((x) => x.name === interaction.data.custom_id && x.type === interaction.type)!.number : - this.statsData.interactions.push({ name: interaction.data.custom_id, number: 1, type: interaction.type }); - - const guildData = this.statsData.guildsStats.find(guild => interaction.guildID ? guild.guildId === interaction.guildID : guild.guildId === "dm") - if (guildData) this.statsData.guildsStats = this.statsData.guildsStats.filter(guild => guild.guildId === guildData.guildId) - - const guild = this._client.guilds.get(interaction.guildID) - this.statsData.guildsStats.push({ - guildId: interaction.guildID || "dm", - name: guild ? guild.name : "DM", - icon: guild && guild.icon ? guild.icon : undefined, - interactions: guildData ? guildData.interactions + 1 : 1, - members: guild ? guild.memberCount : 0 - }) - } - - /** - * Track guilds - * /!\ Advanced users only - * /!\ You need to initialize the class first - * @param guild - The Guild instance only - * @param {TrackGuildType} type - "create" if the event is guildCreate and "delete" if is guildDelete - */ - public async trackGuilds (guild: any, type: TrackGuildType) { - if (this._debug) console.log(`[DISCORDANALYTICS] trackGuilds(${type}) triggered`) - if (type === "create") this.statsData.addedGuilds++ - else this.statsData.removedGuilds++ - } - - /** - * Let DiscordAnalytics declare the events necessary for its operation. - * /!\ Not recommended for big bots - * /!\ Not compatible with other functions - */ - public trackEvents () { - if (!this._client.ready) this._client.on("ready", async () => await this.init()) - else this.init() - this._client.on("interactionCreate", async (interaction: any) => await this.trackInteractions(interaction)) - this._client.on("guildCreate", (guild: any) => this.trackGuilds(guild, "create")) - this._client.on("guildDelete", (guild: any) => this.trackGuilds(guild, "delete")) - } -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index dc5ff51..0000000 --- a/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { default as discordjs } from "./discordjs"; -export { default as eris } from "./eris"; -export { default as oceanic } from "./oceanic" - -export { ErrorCodes } from "./utils/types"; -export type { EventsToTrack } from "./utils/types"; \ No newline at end of file diff --git a/src/oceanic/index.ts b/src/oceanic/index.ts deleted file mode 100644 index 6c16f4e..0000000 --- a/src/oceanic/index.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { - ApiEndpoints, - DiscordAnalyticsOptions, - ErrorCodes, - InteractionData, - InteractionType, - Locale, - TrackGuildType -} from "../utils/types"; -import npmPackageData from "../../package.json"; -import fetch from "node-fetch"; - -/** - * @class DiscordAnalytics - * @description The Oceanic.js class for the DiscordAnalytics library. - * @param {DiscordAnalyticsOptions} options - Configuration options. - * @property {any} options.client - The Oceanic.js client to track events for. - * @property {string} options.apiToken - The API token for DiscordAnalytics. - * @property {boolean} options.sharded - /!\ Not compatible with Oceanic.js - * @property {boolean} options.debug - Enable or not the debug mode /!\ MUST BE USED ONLY FOR DEVELOPMENT PURPOSES /!\ - * @example - * const { default: DiscordAnalytics } = require('discord-analytics/oceanic'); - * const { Client } = require('oceanic.js'); - * const client = new Client({ - * auth: "Bot ", - * gateway: { - * intents: ["GUILDS"] - * } - * }) - * client.on('ready', () => { - * const analytics = new DiscordAnalytics({ - * client: client, - * apiToken: 'YOUR_API_TOKEN' - * }); - * analytics.trackEvents(); - * }); - * client.connect(); - * - * // Check docs for more informations about advanced usages : https://docs.discordanalytics.xyz/get-started/installation/oceanic.js - */ -export default class DiscordAnalytics { - private readonly _client: any; - private readonly _apiToken: string; - private readonly _sharded: boolean = false; - private readonly _debug: boolean = false - private readonly _headers: { 'Content-Type': string; Authorization: string; }; - private _isReady: boolean - - constructor(options: Omit) { - this._client = options.client; - this._apiToken = options.apiToken; - this._headers = { - 'Content-Type': 'application/json', - 'Authorization': `Bot ${this._apiToken}` - } - this._isReady = false - this._debug = options.debug || false - } - - /** - * Initialize DiscordAnalytics on your bot - * /!\ Advanced users only - * /!\ Required to use DiscordAnalytics (except if you use the trackEvents function) - * /!\ Must be used when the client is ready (recommended to use in ready event to prevent problems) - */ - public async init() { - const app = await this._client.rest.applications.getCurrent(); - - fetch(`${ApiEndpoints.BASE_URL}${ApiEndpoints.EDIT_SETTINGS_URL.replace(':id', this._client.user.id)}`, { - headers: this._headers, - body: JSON.stringify({ - username: this._client.user.username, - avatar: this._client.user.avatar, - framework: "oceanic", - version: npmPackageData.version, - team: app.team - ? app.team.members.map((member: any) => member.user.id) - : app.owner - ? [app.owner.id] - : [], - }), - method: "PATCH" - }).then(async (res) => { - if (res.status === 401) return console.error(ErrorCodes.INVALID_API_TOKEN) - else if (res.status === 423) return console.error(ErrorCodes.SUSPENDED_BOT) - else if (res.status !== 200) return console.error(ErrorCodes.INVALID_RESPONSE) - - if (this._debug) console.debug("[DISCORDANALYTICS] Instance successfully initialized") - this._isReady = true - - if (this._debug) { - if (process.argv[2] === "--dev") console.debug("[DISCORDANALYTICS] DevMode is enabled. Stats will be sent every 30s.") - else console.debug("[DISCORDANALYTICS] DevMode is disabled. Stats will be sent every 5min.") - } - - setInterval(async () => { - if (this._debug) console.debug("[DISCORDANALYTICS] Sending stats...") - - let guildCount = this._client.guilds.toArray().length; - - let userCount = this._client.guilds.reduce((a: number, g: any) => a + (g.memberCount || 0), 0); - - fetch(`${ApiEndpoints.BASE_URL}${ApiEndpoints.EDIT_STATS_URL.replace(':id', this._client.user!.id)}`, { - headers: this._headers, - body: JSON.stringify(this.statsData), - method: "POST" - }).then(async (res) => { - if (res.status === 401) return console.error(ErrorCodes.INVALID_API_TOKEN) - else if (res.status === 423) return console.error(ErrorCodes.SUSPENDED_BOT) - else if (res.status !== 200) return console.error(ErrorCodes.INVALID_RESPONSE) - if (res.status === 200) { - if (this._debug) console.debug(`[DISCORDANALYTICS] Stats ${JSON.stringify(this.statsData)} sent to the API`) - - this.statsData = { - date: new Date().toISOString().slice(0, 10), - guilds: guildCount, - users: userCount, - interactions: [], - locales: [], - guildsLocales: [], - guildMembers: await this.calculateGuildMembersRepartition(), - guildsStats: [], - addedGuilds: 0, - removedGuilds: 0, - users_type: { - admin: 0, - moderator: 0, - new_member: 0, - other: 0, - private_message: 0 - } - } - } - }).catch(e => { - if (this._debug) { - console.debug("[DISCORDANALYTICS] " + ErrorCodes.DATA_NOT_SENT); - console.error(e) - } - }); - }, process.argv[2] === "--dev" ? 30000 : 5 * 60000); - }) - } - - private statsData = { - date: new Date().toISOString().slice(0, 10), - guilds: 0, - users: 0, - interactions: [] as InteractionData[], - locales: [] as { locale: Locale, number: number }[], - guildsLocales: [] as { locale: Locale, number: number }[], - guildMembers: { - little: 0, - medium: 0, - big: 0, - huge: 0 - }, - guildsStats: [] as { guildId: string, name: string, icon: string, members: number, interactions: number }[], - addedGuilds: 0, - removedGuilds: 0, - users_type: { - admin: 0, - moderator: 0, - new_member: 0, - other: 0, - private_message: 0 - } - } - - private async calculateGuildMembersRepartition(): Promise<{ little: number, medium: number, big: number, huge: number }> { - const res = { - little: 0, - medium: 0, - big: 0, - huge: 0 - } - - let guildsMembers: number[] = this._client.guilds.map((guild: any) => guild.memberCount) - - for (const guild of guildsMembers) { - if (guild <= 100) res.little++ - else if (guild > 100 && guild <= 500) res.medium++ - else if (guild > 500 && guild <= 1500) res.big++ - else if (guild > 1500) res.huge++ - } - - return res - } - - /** - * Track interactions - * /!\ Advanced users only - * /!\ You need to initialize the class first - * @param interaction - BaseInteraction class and its extensions only - */ - public async trackInteractions(interaction: any) { - if (this._debug) console.log("[DISCORDANALYTICS] trackInteractions() triggered") - if (!this._isReady) throw new Error(ErrorCodes.INSTANCE_NOT_INITIALIZED) - - let guilds: { locale: Locale, number: number }[] = [] - this._client.guilds.map((current: any) => guilds.find((x) => x.locale === current.preferredLocale) ? - ++guilds.find((x) => x.locale === current.preferredLocale)!.number : - guilds.push({ locale: current.preferredLocale, number: 1 })); - - this.statsData.guildsLocales = guilds - - this.statsData.locales.find((x) => x.locale === interaction.locale) ? - ++this.statsData.locales.find((x) => x.locale === interaction.locale)!.number : - this.statsData.locales.push({ locale: interaction.locale, number: 1 }); - - if (interaction.type === InteractionType.ApplicationCommand) { - const commandType = interaction.data.type; - this.statsData.interactions.find((x) => x.name === interaction.data.name && x.type === interaction.type && x.command_type === commandType) ? - ++this.statsData.interactions.find((x) => x.name === interaction.data.name && x.type === interaction.type && x.command_type === commandType)!.number : - this.statsData.interactions.push({ name: interaction.data.name, number: 1, type: interaction.type, command_type: commandType }); - } - - else if (interaction.type === InteractionType.MessageComponent || interaction.type === InteractionType.ModalSubmit) - this.statsData.interactions.find((x) => x.name === interaction.data.customID && x.type === interaction.type) ? - ++this.statsData.interactions.find((x) => x.name === interaction.data.customID && x.type === interaction.type)!.number : - this.statsData.interactions.push({ name: interaction.data.customID, number: 1, type: interaction.type }); - - const guildData = this.statsData.guildsStats.find(guild => interaction.guildID ? guild.guildId === interaction.guildID : guild.guildId === "dm") - if (guildData) this.statsData.guildsStats = this.statsData.guildsStats.filter(guild => guild.guildId === guildData.guildId) - - const guild = this._client.guilds.find((guild: any) => guild.id === interaction.guildID) - this.statsData.guildsStats.push({ - guildId: interaction.guildID || "dm", - name: guild ? guild.name : "DM", - icon: guild && guild.icon ? guild.icon : undefined, - interactions: guildData ? guildData.interactions + 1 : 1, - members: guild ? guild.memberCount : 0 - }) - - const oneWeekAgo = new Date() - oneWeekAgo.setDate(oneWeekAgo.getDate() - 7) - - if (!interaction.guild) ++this.statsData.users_type.private_message - else if (interaction.member && interaction.member.permissions && interaction.member.permissions.has(8n) || interaction.member.permissions.has(32n)) ++this.statsData.users_type.admin - else if (interaction.member && interaction.member.permissions && interaction.member.permissions.has(8192n) || interaction.member.permissions.has(2n) || interaction.member.permissions.has(4n) || interaction.member.permissions.has(4194304n) || interaction.member.permissions.has(8388608n) || interaction.member.permissions.has(16777216n) || interaction.member.permissions.has(1099511627776n)) ++this.statsData.users_type.moderator - else if (interaction.member && interaction.member.joinedAt && interaction.member.joinedAt > oneWeekAgo) ++this.statsData.users_type.new_member - } - - /** - * Track guilds - * /!\ Advanced users only - * /!\ You need to initialize the class first - * @param guild - The Guild instance only - * @param {TrackGuildType} type - "create" if the event is guildCreate and "delete" if is guildDelete - */ - public async trackGuilds(guild: any, type: TrackGuildType) { - if (this._debug) console.log(`[DISCORDANALYTICS] trackGuilds(${type}) triggered`) - if (type === "create") this.statsData.addedGuilds++ - else this.statsData.removedGuilds++ - } - - /** - * Let DiscordAnalytics declare the events necessary for its operation. - * /!\ Not recommended for big bots - * /!\ Not compatible with other functions - */ - public trackEvents() { - if (!this._client.ready) this._client.on("ready", async () => await this.init()) - else this.init() - this._client.on("interactionCreate", async (interaction: any) => await this.trackInteractions(interaction)) - } -} \ No newline at end of file diff --git a/src/test/discordjs-light/index.ts b/src/test/discordjs-light/index.ts deleted file mode 100644 index 8e6898d..0000000 --- a/src/test/discordjs-light/index.ts +++ /dev/null @@ -1,139 +0,0 @@ -// @ts-nocheck - -import DiscordAnalytics from "../../discordjs-light"; -import { Client, Interaction, Message, MessageActionRow, Modal, TextInputComponent, TextInputStyle } from "discord.js-light"; -import { config } from "dotenv"; - -config() - -const client = new Client({ - intents: ["GUILDS"] -}) - -client.on("error", (e) => { - console.log(e) -}) - -const analytics = new DiscordAnalytics({ - client: client, - apiToken: process.env.DA_TOKEN, - sharded: false, - debug: true -}); - -client.on("ready", async () => { - client.application?.commands.set([{ - name: "test", - description: "Send test message", - dmPermission: true, - options: [{ - name: "test", - description: "Test option", - type: 3, - required: false, - autocomplete: true - }] - }]) - - console.log("Bot is ready!"); - - await analytics.init() -}); - -client.on("interactionCreate", async (interaction: Interaction) => { - await analytics.trackInteractions(interaction) - if (interaction.isCommand()) { - if (interaction.commandName === "test") { - const option = interaction.options.getString("test"); - if (option === "button") interaction.reply({ - content: "Test button", - components: [{ - type: 1, - components: [{ - type: 2, - style: 1, - label: "Test button", - custom_id: "✅" - }] - }] - }) - else if (option === "select") interaction.reply({ - content: "Test select", - components: [{ - type: 1, - components: [{ - type: 3, - custom_id: "test_select", - options: [{ - label: "Test select", - value: "test_select" - }] - }] - }] - }) - else if (option === "modal") { - const modal = new Modal() - .setCustomId('myModal') - .setTitle('My Modal'); - - // Add components to modal - - // Create the text input components - const favoriteColorInput = new TextInputComponent() - .setCustomId('favoriteColorInput') - // The label is the prompt the user sees for this input - .setLabel("What's your favorite color?") - // Short means only a single line of text - .setStyle("SHORT"); - - // An action row only holds one text input, - // so you need one action row per text input. - const firstActionRow: MessageActionRow = new MessageActionRow().addComponents(favoriteColorInput); - - // Add inputs to the modal - modal.addComponents(firstActionRow); - - // Show the modal to the user - await interaction.showModal(modal); - } else interaction.reply({ - content: "Test message", - ephemeral: true - }) - } - } - - if (interaction.isAutocomplete()) { - const focusedValue = interaction.options.getFocused(); - const choices = ["button", "select", "modal"]; - const filtered = choices.filter(choice => choice.startsWith(focusedValue)); - await interaction.respond( - filtered.map(choice => ({ name: choice, value: choice })), - ); - } - - if (interaction.isButton()) interaction.reply({ - content: "Button clicked!", - ephemeral: true - }) - - if (interaction.isSelectMenu()) { - await interaction.reply({ - content: "Select clicked!", - ephemeral: true - }) - await (interaction.message as Message).edit({ - components: [] - }) - } - - if (interaction.isModalSubmit()) interaction.reply({ - content: "Modal submitted!", - ephemeral: true - }) -}) - -client.on("guildCreate", async (guild) => await analytics.trackGuilds(guild, "create")) - -client.on("guildDelete", async (guild) => await analytics.trackGuilds(guild, "delete")) - -client.login(process.env.DISCORD_TOKEN); \ No newline at end of file diff --git a/src/test/discordjs-light/shards.ts b/src/test/discordjs-light/shards.ts deleted file mode 100644 index 796bb8a..0000000 --- a/src/test/discordjs-light/shards.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ShardingManager } from 'discord.js-light'; -import {config} from "dotenv"; - -config() - -const manager = new ShardingManager('./dist/src/test/discordjs-light/index.js', { token: process.env.DISCORD_TOKEN }); - -manager.on('shardCreate', shard => console.log(`Launched shard ${shard.id}`)); - -manager.spawn(); \ No newline at end of file diff --git a/src/test/discordjs/index.ts b/src/test/discordjs/index.ts deleted file mode 100644 index b4802c9..0000000 --- a/src/test/discordjs/index.ts +++ /dev/null @@ -1,146 +0,0 @@ -// @ts-nocheck - -import DiscordAnalytics from "../../discordjs"; -import { ActionRowBuilder, Client, IntentsBitField, Interaction, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; -import {config} from "dotenv"; -import {InteractionType} from "../../utils/types"; - -config() - -const client = new Client({ - intents: [IntentsBitField.Flags.Guilds] -}) - -client.on("error", (e) => { - console.log(e) -}) - -const analytics = new DiscordAnalytics({ - client: client, - apiToken: process.env.DA_TOKEN, - sharded: false, - debug: true -}); - -client.on("ready", async () => { - client.application?.commands.set([{ - name: "test", - description: "Send test message", - dmPermission: true, - options: [{ - name: "test", - description: "Test option", - type: 3, - required: false, - autocomplete: true - }] - }]) - - console.log("Bot is ready!"); - - await analytics.init() -}); - -client.on("interactionCreate", async (interaction: Interaction) => { - await analytics.trackInteractions(interaction, (int) => { - if (interaction.type === InteractionType.ApplicationCommand) - return interaction.commandName - else if (interaction.type === InteractionType.MessageComponent || interaction.type === InteractionType.ModalSubmit) { - if ((/\d{17,19}/g).test(interaction.customId)) return "this_awesome_button" - else return interaction.customId - } - return "" - }) - if (interaction.isChatInputCommand()) { - if (interaction.commandName === "test") { - const option = interaction.options.getString("test"); - if (option === "button") interaction.reply({ - content: "Test button", - components: [{ - type: 1, - components: [{ - type: 2, - style: 1, - label: "Test button", - custom_id: `button_${interaction.user.id}` - }] - }] - }) - else if (option === "select") interaction.reply({ - content: "Test select", - components: [{ - type: 1, - components: [{ - type: 3, - custom_id: "test_select", - options: [{ - label: "Test select", - value: "test_select" - }] - }] - }] - }) - else if (option === "modal") { - const modal = new ModalBuilder() - .setCustomId('myModal') - .setTitle('My Modal'); - - // Add components to modal - - // Create the text input components - const favoriteColorInput = new TextInputBuilder() - .setCustomId('favoriteColorInput') - // The label is the prompt the user sees for this input - .setLabel("What's your favorite color?") - // Short means only a single line of text - .setStyle(TextInputStyle.Short); - - // An action row only holds one text input, - // so you need one action row per text input. - const firstActionRow = new ActionRowBuilder().addComponents(favoriteColorInput) as ActionRowBuilder; - - // Add inputs to the modal - modal.addComponents(firstActionRow); - - // Show the modal to the user - await interaction.showModal(modal); - } else interaction.reply({ - content: "Test message", - ephemeral: true - }) - } - } - - if (interaction.isAutocomplete()) { - const focusedValue = interaction.options.getFocused(); - const choices = ["button", "select", "modal"]; - const filtered = choices.filter(choice => choice.startsWith(focusedValue)); - await interaction.respond( - filtered.map(choice => ({ name: choice, value: choice })), - ); - } - - if (interaction.isButton()) interaction.reply({ - content: "Button clicked!", - ephemeral: true - }) - - if (interaction.isStringSelectMenu()) { - await interaction.reply({ - content: "Select clicked!", - ephemeral: true - }) - await interaction.message.edit() - } - - if (interaction.isModalSubmit()) interaction.reply({ - content: "Modal submitted!", - ephemeral: true - }) -}) - -client.on("guildCreate", async (guild) => await analytics.trackGuilds(guild, "create")) - -client.on("guildDelete", async (guild) => await analytics.trackGuilds(guild, "delete")) - -client.login(process.env.DISCORD_TOKEN); \ No newline at end of file diff --git a/src/test/discordjs/shards.ts b/src/test/discordjs/shards.ts deleted file mode 100644 index aca227a..0000000 --- a/src/test/discordjs/shards.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ShardingManager } from 'discord.js'; -import {config} from "dotenv"; - -config() - -const manager = new ShardingManager('./dist/src/test/discordjs/index.js', { token: process.env.DISCORD_TOKEN }); - -manager.on('shardCreate', shard => console.log(`Launched shard ${shard.id}`)); - -manager.spawn(); \ No newline at end of file diff --git a/src/test/eris/index.ts b/src/test/eris/index.ts deleted file mode 100644 index c0eacc9..0000000 --- a/src/test/eris/index.ts +++ /dev/null @@ -1,106 +0,0 @@ -// @ts-nocheck - -import {Client, Constants, CommandInteraction, ComponentInteraction} from "eris"; -import DiscordAnalytics from "../../eris"; -import {config} from "dotenv"; - -config() - -const bot = new Client(process.env.DISCORD_TOKEN, { - intents: ["guilds"] -}); - -const analytics = new DiscordAnalytics({ - client: bot, - apiToken: process.env.DA_TOKEN, - debug: true -}); - -bot.on("ready", () => { - bot.createCommand({ - name: "test", - description: "Send test message", - type: Constants.ApplicationCommandTypes.CHAT_INPUT, - options: [{ - name: "test", - description: "Test option", - type: Constants.ApplicationCommandOptionTypes.STRING, - required: false, - choices: [{ - name: "Button", - value: "button" - },{ - name: "Select", - value: "select" - }] - }] - }) - - analytics.init() - - console.log("Ready!"); -}); - -bot.connect(); - -bot.on("interactionCreate", async (interaction) => { - await analytics.trackInteractions(interaction) - if (interaction instanceof CommandInteraction) { - if (interaction.data.name == "test") { - if (interaction.data.options) { - const option = interaction.data.options.find((option) => option.name === "test") as { value: string, type: number, name: string } | undefined; - if (option) { - if (option.value === "button") await interaction.createMessage({ - content: "Test button", - components: [{ - type: 1, - components: [{ - type: 2, - style: 1, - label: "Test button", - custom_id: "test_button" - }] - }] - }) - else if (option.value === "select") await interaction.createMessage({ - content: "Test select", - components: [{ - type: 1, - components: [{ - type: 3, - custom_id: "test_select", - options: [{ - label: "Test select", - value: "test_select" - }] - }] - }] - }) - } else await interaction.createMessage({ - content: "Test message", - flags: 64 - }) - } else await interaction.createMessage({ - content: "Test message", - flags: 64 - }) - } - } - - if (interaction instanceof ComponentInteraction) { - if (interaction.data && interaction.data.component_type) { - if (interaction.data.component_type === 2) await interaction.createMessage({ - content: "Button clicked!", - flags: 64 - }) - else if (interaction.data.component_type === 3) await interaction.createMessage({ - content: "Select clicked!", - flags: 64 - }) - } - } -}) - -bot.on("guildCreate", async (guild) => await analytics.trackGuilds(guild, "create")) - -bot.on("guildDelete", async (guild) => await analytics.trackGuilds(guild, "delete")) \ No newline at end of file diff --git a/src/test/oceanic/index.ts b/src/test/oceanic/index.ts deleted file mode 100644 index b6aa44b..0000000 --- a/src/test/oceanic/index.ts +++ /dev/null @@ -1,152 +0,0 @@ -// @ts-nocheck - -import { - ApplicationCommandOptionTypes, - ApplicationCommandTypes, - ButtonStyles, - Client, - ComponentTypes, - TextInputStyles, -} from "oceanic.js"; -import DiscordAnalytics from "../../oceanic"; -import {config} from "dotenv"; - -config() - -const client = new Client({ - auth: `Bot ${process.env.DISCORD_TOKEN}`, - gateway: { - intents: ["GUILDS"] - } -}) - -const analytics = new DiscordAnalytics({ - client: client, - apiToken: process.env.DA_TOKEN, - debug: true -}) - -analytics.trackEvents() - -client.on("ready", async () => { - await client.application.createGlobalCommand({ - type: ApplicationCommandTypes.CHAT_INPUT, - name: "test", - description: "Do some tests", - options: [ - { - type: ApplicationCommandOptionTypes.STRING, - name: "type", - description: "The type of the test.", - choices: [ - { - name: "button", - value: "button" - }, - { - name: "select_menu", - value: "select" - }, - { - name: "modal", - value: "modal" - } - ], - required: false - } - ] - }) - - console.log(`Connected as ${client.user.tag}`) -}) - -client.on("interactionCreate", async (interaction) => { - if (interaction.isCommandInteraction() && interaction.data.name === "test") { - const option = interaction.data.options.getString("test", false) - if (option === "button") interaction.reply({ - content: "Here is the button!", - components: [ - { - type: ComponentTypes.ACTION_ROW, - components: [ - { - type: ComponentTypes.BUTTON, - customID: "test_button", - label: "Test button", - style: ButtonStyles.PRIMARY - } - ] - } - ] - }) - else if (option === "select") interaction.reply({ - content: "Here is the button!", - components: [ - { - type: ComponentTypes.ACTION_ROW, - components: [ - { - type: ComponentTypes.STRING_SELECT, - placeholder: "Test select", - customID: "test_select", - options: [ - { - label: "test select", - value: "test_select" - } - ] - } - ] - } - ] - }) - else if (option === "modal") { - interaction.createModal({ - title: "This is the modal", - components: [ - { - type: ComponentTypes.ACTION_ROW, - components: [ - { - type: ComponentTypes.TEXT_INPUT, - customID: "text-input", - label: "Some Label Here", - placeholder: "Some Placeholder Here", - required: true, - style: TextInputStyles.SHORT - } - ] - } - ], - customID: "modal" - }) - } - else interaction.reply({ - content: "Test command!" - }) - } else if (interaction.isComponentInteraction()) { - if (interaction.data.componentType === ComponentTypes.BUTTON) interaction.reply({ - content: "Button clicked!" - }) - else if (interaction.data.componentType === ComponentTypes.STRING_SELECT) { - interaction.reply({ - content: "Select menu clicked!" - }) - - await interaction.message.edit({ - content: interaction.message.content, - components: interaction.message.components - }) - } - } else if (interaction.isModelSubmitInteraction()) { - interaction.reply({ - content: "Modal submitted!" - }) - } -}) - -client.on("guildCreate", async (guild) => await analytics.trackGuilds(guild, "create")) - -client.on("guildDelete", async (guild) => await analytics.trackGuilds(guild, "delete")) - -client.connect() \ No newline at end of file diff --git a/src/utils/types.ts b/src/utils/types.ts deleted file mode 100644 index e3eed9f..0000000 --- a/src/utils/types.ts +++ /dev/null @@ -1,55 +0,0 @@ -export interface EventsToTrack { - trackInteractions: boolean; - trackGuilds: boolean; - trackUserCount: boolean; - trackUserLanguage: boolean; - trackGuildsLocale: boolean; -} -export const ApiEndpoints = { - BASE_URL: 'https://discordanalytics.xyz/api', - EDIT_SETTINGS_URL: '/bots/:id', - EDIT_STATS_URL: '/bots/:id/stats', -} - -export const ErrorCodes = { - INVALID_CLIENT_TYPE: 'Invalid client type, please use a valid client.', - CLIENT_NOT_READY: 'Client is not ready, please start the client first.', - INVALID_RESPONSE: 'Invalid response from the API, please try again later.', - INVALID_API_TOKEN: 'Invalid API token, please get one at ' + ApiEndpoints.BASE_URL.split('/api')[0] + ' and try again.', - DATA_NOT_SENT: "Data cannot be sent to the API, I will try again in a minute.", - SUSPENDED_BOT: "Your bot has been suspended, please check your mailbox for more information.", - INSTANCE_NOT_INITIALIZED: "It seem that you didn't initialize your instance. Please check the docs for more informations.", - INVALID_EVENTS_COUNT: "invalid events count" -} - -export type Locale = 'id' | 'en-US' | 'en-GB' | 'bg' | 'zh-CN' | 'zh-TW' | 'hr' | 'cs' | 'da' | 'nl' | 'fi' | 'fr' | 'de' | 'el' | 'hi' | 'hu' | 'it' | 'ja' | 'ko' | 'lt' | 'no' | 'pl' | 'pt-BR' | 'ro' | 'ru' | 'es-ES' | 'sv-SE' | 'th' | 'tr' | 'uk' | 'vi'; - -export enum InteractionType { - Ping = 1, - ApplicationCommand, - MessageComponent, - ApplicationCommandAutocomplete, - ModalSubmit, -} - -export enum ApplicationCommandType { - ChatInputCommand = 1, - UserCommand, - MessageCommand, -} - -export interface InteractionData { - name: string; - number: number; - type: InteractionType; - command_type?: ApplicationCommandType; -} - -export interface DiscordAnalyticsOptions { - client: any; - apiToken: string; - sharded?: boolean; - debug?: boolean; -} - -export type TrackGuildType = "create" | "delete" \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 13d91c0..411b1a2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,109 +1,13 @@ { "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 legacy experimental 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": "commonjs", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* 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. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "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. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "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. */ - // "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. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "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. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "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": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* 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. */ + "target": "es2022", + "module": "commonjs", + "resolveJsonModule": true, + "declaration": true, + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true } } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..7aecda3 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + reporters: process.env.GITHUB_ACTIONS ? ['dot', 'github-actions'] : ['dot'], + coverage: { + include: ['packages/*/src/**/*.ts'], + }, + }, +}); diff --git a/vitest.workspace.ts b/vitest.workspace.ts new file mode 100644 index 0000000..ab8a7cf --- /dev/null +++ b/vitest.workspace.ts @@ -0,0 +1,5 @@ +import { defineWorkspace } from 'vitest/config'; + +export default defineWorkspace([ + 'packages/*', +]);