diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 90e1cb1..f2c8edc 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -41,3 +41,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HUSKY: 0 + diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6106558..cf2761a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,37 +1,37 @@ -name: Publish - -on: - push: - branches: - - build - -jobs: - build: - name: Publish - runs-on: ubuntu-latest - steps: - - name: check out code - uses: actions/checkout@v4 - - - name: Install Node - uses: actions/setup-node@v4 - with: - node-version: '20.x' - registry-url: 'https://registry.npmjs.org' - - - uses: pnpm/action-setup@v3 - with: - version: 8 - - - name: Install dependencies - run: pnpm install - - - name: Create Release Pull Request - uses: changesets/action@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - with: - commit: "chore: release packages" - publish: npm publish - title: "chore: release packages" +name: Publish + +on: + push: + branches: + - build + +jobs: + build: + name: Publish + runs-on: ubuntu-latest + steps: + - name: check out code + uses: actions/checkout@v4 + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: '20.x' + registry-url: 'https://registry.npmjs.org' + + - uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Create Release Pull Request + uses: changesets/action@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + with: + commit: "chore: release packages" + publish: npm publish + title: "chore: release packages" diff --git a/.github/workflows/transpile.yml b/.github/workflows/transpile.yml index 9c0ba2f..4cd2ea7 100644 --- a/.github/workflows/transpile.yml +++ b/.github/workflows/transpile.yml @@ -1,33 +1,33 @@ -name: Transpile code - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - build: - name: Transpile code - runs-on: ubuntu-latest - steps: - - name: check out code - uses: actions/checkout@v4 - - - name: Install Node - uses: actions/setup-node@v4 - with: - node-version: '20.x' - registry-url: 'https://registry.npmjs.org' - - - uses: pnpm/action-setup@v3 - with: - version: 8 - - - name: Install dependencies - run: pnpm install - - - name: Build - run: npx tsc +name: Transpile code + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + name: Transpile code + runs-on: ubuntu-latest + steps: + - name: check out code + uses: actions/checkout@v4 + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: '20.x' + registry-url: 'https://registry.npmjs.org' + + - uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: npx tsc diff --git a/package.json b/package.json index bd5cdd0..5301469 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,10 @@ "license": "Apache-2.0", "dependencies": { "chokidar": "^3.6.0", - "discord-api-types": "^0.37.88", + "discord-api-types": "^0.37.90", "magic-bytes.js": "^1.10.0", "ts-mixer": "^6.0.4", - "ws": "^8.17.0" + "ws": "^8.17.1" }, "lint-staged": { "*.ts": [ @@ -35,11 +35,11 @@ "@biomejs/biome": "1.8.1", "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", - "@types/node": "^20.14.2", + "@types/node": "^20.14.6", "@types/ws": "^8.5.10", "husky": "^9.0.11", - "lint-staged": "^15.2.6", - "typescript": "^5.4.5" + "lint-staged": "^15.2.7", + "typescript": "^5.5.2" }, "optionalDependencies": { "ioredis": "^5.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66576ec..47277f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^3.6.0 version: 3.6.0 discord-api-types: - specifier: ^0.37.88 - version: 0.37.88 + specifier: ^0.37.90 + version: 0.37.90 magic-bytes.js: specifier: ^1.10.0 version: 1.10.0 @@ -21,8 +21,8 @@ importers: specifier: ^6.0.4 version: 6.0.4 ws: - specifier: ^8.17.0 - version: 8.17.0 + specifier: ^8.17.1 + version: 8.17.1 optionalDependencies: ioredis: specifier: ^5.4.1 @@ -39,13 +39,13 @@ importers: version: 1.8.1 '@commitlint/cli': specifier: ^19.3.0 - version: 19.3.0(@types/node@20.14.2)(typescript@5.4.5) + version: 19.3.0(@types/node@20.14.6)(typescript@5.5.2) '@commitlint/config-conventional': specifier: ^19.2.2 version: 19.2.2 '@types/node': - specifier: ^20.14.2 - version: 20.14.2 + specifier: ^20.14.6 + version: 20.14.6 '@types/ws': specifier: ^8.5.10 version: 8.5.10 @@ -53,11 +53,11 @@ importers: specifier: ^9.0.11 version: 9.0.11 lint-staged: - specifier: ^15.2.6 - version: 15.2.6 + specifier: ^15.2.7 + version: 15.2.7 typescript: - specifier: ^5.4.5 - version: 5.4.5 + specifier: ^5.5.2 + version: 5.5.2 packages: @@ -201,8 +201,8 @@ packages: '@types/conventional-commits-parser@5.0.0': resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} - '@types/node@20.14.2': - resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==} + '@types/node@20.14.6': + resolution: {integrity: sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==} '@types/ws@8.5.10': resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} @@ -362,8 +362,8 @@ packages: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} - discord-api-types@0.37.88: - resolution: {integrity: sha512-Yrj5S3JXzouPc6WLA8svgXCw3Q7IkNdW/Y/IfkJF+CsgUBsgECQ8qVoOaseJJ8Atj2TEvabut4rGmzq6PRi1/Q==} + discord-api-types@0.37.90: + resolution: {integrity: sha512-lpOJSGrqHuXoM4FV/2HtjoaJpCClGFHpRNIdZEW8zPINlsCHNSfIwA2EQ8dxeE6k1QhhTuM9ZlOGVYXoU7FLgA==} dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} @@ -540,13 +540,13 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lint-staged@15.2.6: - resolution: {integrity: sha512-M/3PdijFXT/A5lnbSK3EQNLbIIrkE00JZaD39r7t4kfFOqT1Ly9LgSZSMMtvQ3p2/C8Nyj/ou0vkNHmEwqoB8g==} + lint-staged@15.2.7: + resolution: {integrity: sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw==} engines: {node: '>=18.12.0'} hasBin: true - listr2@8.2.1: - resolution: {integrity: sha512-irTfvpib/rNiD637xeevjO2l3Z5loZmuaRi0L0YE5LfijwVY96oyVn0DFD3o/teAok7nfobMG1THvvcHh/BP6g==} + listr2@8.2.2: + resolution: {integrity: sha512-sy0dq+JPS+RAFiFk2K8Nbub7khNmeeoFALNUJ4Wzk34wZKAzaOhEXqGWs4RA5aui0RaM6Hgn7VEKhCj0mlKNLA==} engines: {node: '>=18.0.0'} locate-path@7.2.0: @@ -710,8 +710,8 @@ packages: resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - rfdc@1.3.1: - resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} semver@7.6.2: resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} @@ -793,8 +793,8 @@ packages: tweetnacl@1.0.3: resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} - typescript@5.4.5: - resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + typescript@5.5.2: + resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==} engines: {node: '>=14.17'} hasBin: true @@ -825,8 +825,8 @@ packages: resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} engines: {node: '>=18'} - ws@8.17.0: - resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==} + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -909,11 +909,11 @@ snapshots: '@biomejs/cli-win32-x64@1.8.1': optional: true - '@commitlint/cli@19.3.0(@types/node@20.14.2)(typescript@5.4.5)': + '@commitlint/cli@19.3.0(@types/node@20.14.6)(typescript@5.5.2)': dependencies: '@commitlint/format': 19.3.0 '@commitlint/lint': 19.2.2 - '@commitlint/load': 19.2.0(@types/node@20.14.2)(typescript@5.4.5) + '@commitlint/load': 19.2.0(@types/node@20.14.6)(typescript@5.5.2) '@commitlint/read': 19.2.1 '@commitlint/types': 19.0.3 execa: 8.0.1 @@ -960,15 +960,15 @@ snapshots: '@commitlint/rules': 19.0.3 '@commitlint/types': 19.0.3 - '@commitlint/load@19.2.0(@types/node@20.14.2)(typescript@5.4.5)': + '@commitlint/load@19.2.0(@types/node@20.14.6)(typescript@5.5.2)': dependencies: '@commitlint/config-validator': 19.0.3 '@commitlint/execute-rule': 19.0.0 '@commitlint/resolve-extends': 19.1.0 '@commitlint/types': 19.0.3 chalk: 5.3.0 - cosmiconfig: 9.0.0(typescript@5.4.5) - cosmiconfig-typescript-loader: 5.0.0(@types/node@20.14.2)(cosmiconfig@9.0.0(typescript@5.4.5))(typescript@5.4.5) + cosmiconfig: 9.0.0(typescript@5.5.2) + cosmiconfig-typescript-loader: 5.0.0(@types/node@20.14.6)(cosmiconfig@9.0.0(typescript@5.5.2))(typescript@5.5.2) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -1025,15 +1025,15 @@ snapshots: '@types/conventional-commits-parser@5.0.0': dependencies: - '@types/node': 20.14.2 + '@types/node': 20.14.6 - '@types/node@20.14.2': + '@types/node@20.14.6': dependencies: undici-types: 5.26.5 '@types/ws@8.5.10': dependencies: - '@types/node': 20.14.2 + '@types/node': 20.14.6 JSONStream@1.3.5: dependencies: @@ -1154,21 +1154,21 @@ snapshots: meow: 12.1.1 split2: 4.2.0 - cosmiconfig-typescript-loader@5.0.0(@types/node@20.14.2)(cosmiconfig@9.0.0(typescript@5.4.5))(typescript@5.4.5): + cosmiconfig-typescript-loader@5.0.0(@types/node@20.14.6)(cosmiconfig@9.0.0(typescript@5.5.2))(typescript@5.5.2): dependencies: - '@types/node': 20.14.2 - cosmiconfig: 9.0.0(typescript@5.4.5) + '@types/node': 20.14.6 + cosmiconfig: 9.0.0(typescript@5.5.2) jiti: 1.21.6 - typescript: 5.4.5 + typescript: 5.5.2 - cosmiconfig@9.0.0(typescript@5.4.5): + cosmiconfig@9.0.0(typescript@5.5.2): dependencies: env-paths: 2.2.1 import-fresh: 3.3.0 js-yaml: 4.1.0 parse-json: 5.2.0 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.2 cross-spawn@7.0.3: dependencies: @@ -1185,7 +1185,7 @@ snapshots: denque@2.1.0: optional: true - discord-api-types@0.37.88: {} + discord-api-types@0.37.90: {} dot-prop@5.3.0: dependencies: @@ -1334,14 +1334,14 @@ snapshots: lines-and-columns@1.2.4: {} - lint-staged@15.2.6: + lint-staged@15.2.7: dependencies: chalk: 5.3.0 commander: 12.1.0 debug: 4.3.5 execa: 8.0.1 lilconfig: 3.1.2 - listr2: 8.2.1 + listr2: 8.2.2 micromatch: 4.0.7 pidtree: 0.6.0 string-argv: 0.3.2 @@ -1349,13 +1349,13 @@ snapshots: transitivePeerDependencies: - supports-color - listr2@8.2.1: + listr2@8.2.2: dependencies: cli-truncate: 4.0.0 colorette: 2.0.20 eventemitter3: 5.0.1 log-update: 6.0.0 - rfdc: 1.3.1 + rfdc: 1.4.1 wrap-ansi: 9.0.0 locate-path@7.2.0: @@ -1485,7 +1485,7 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - rfdc@1.3.1: {} + rfdc@1.4.1: {} semver@7.6.2: {} @@ -1555,7 +1555,7 @@ snapshots: tweetnacl@1.0.3: optional: true - typescript@5.4.5: {} + typescript@5.5.2: {} uWebSockets.js@https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/f40213ec0a97d0d8721d9d32d92d6eb6ddcd22e7: optional: true @@ -1584,7 +1584,7 @@ snapshots: string-width: 7.1.0 strip-ansi: 7.1.0 - ws@8.17.0: {} + ws@8.17.1: {} y18n@5.0.8: {} diff --git a/src/builders/Button.ts b/src/builders/Button.ts index 95343f8..77f8069 100644 --- a/src/builders/Button.ts +++ b/src/builders/Button.ts @@ -1,26 +1,22 @@ import { - type APIButtonComponentWithCustomId, - type APIButtonComponentWithURL, type APIMessageComponentEmoji, type ButtonStyle, ComponentType, + type APIButtonComponent, } from 'discord-api-types/v10'; -import { throwError } from '..'; -import type { EmojiResolvable, When } from '../common'; +import type { EmojiResolvable } from '../common'; import { resolvePartialEmoji } from '../structures/extra/functions'; -export type ButtonStylesForID = Exclude; - /** * Represents a button component. * @template Type - The type of the button component. */ -export class Button { +export class Button { /** * Creates a new Button instance. * @param data - The initial data for the button. */ - constructor(public data: Partial> = {}) { + constructor(public data: Partial = {}) { this.data.type = ComponentType.Button; } @@ -30,8 +26,7 @@ export class Button { * @returns The modified Button instance. */ setCustomId(id: string) { - // @ts-expect-error - this.data.custom_id = id; + (this.data as Extract).custom_id = id; return this; } @@ -41,8 +36,7 @@ export class Button { * @returns The modified Button instance. */ setURL(url: string) { - // @ts-expect-error - this.data.url = url; + (this.data as Extract).url = url; return this; } @@ -52,7 +46,7 @@ export class Button { * @returns The modified Button instance. */ setLabel(label: string) { - this.data.label = label; + (this.data as Extract).label = label; return this; } @@ -63,8 +57,9 @@ export class Button { */ setEmoji(emoji: EmojiResolvable) { const resolve = resolvePartialEmoji(emoji); - if (!resolve) return throwError('Invalid Emoji'); - this.data.emoji = resolve as APIMessageComponentEmoji; + if (!resolve) throw new Error('Invalid Emoji'); + (this.data as Extract).emoji = + resolve as APIMessageComponentEmoji; return this; } @@ -83,11 +78,16 @@ export class Button { return this; } + setSKUId(skuId: string) { + (this.data as Extract).sku_id = skuId; + return this; + } + /** * Converts the Button instance to its JSON representation. * @returns The JSON representation of the Button instance. */ toJSON() { - return { ...this.data } as When; + return { ...this.data } as Partial; } } diff --git a/src/cache/adapters/default.ts b/src/cache/adapters/default.ts index 1f5ec82..0da75bb 100644 --- a/src/cache/adapters/default.ts +++ b/src/cache/adapters/default.ts @@ -20,13 +20,7 @@ export class MemoryAdapter implements Adapter { return values; } - get(keys: string): any; - get(keys: string[]): any[]; - get(keys: string | string[]) { - if (!Array.isArray(keys)) { - const data = this.storage.get(keys); - return data ? JSON.parse(data) : null; - } + bulkGet(keys: string[]) { return keys .map(x => { const data = this.storage.get(x); @@ -35,44 +29,45 @@ export class MemoryAdapter implements Adapter { .filter(x => x); } - set(keys: string, data: any): void; - set(keys: [string, any][]): void; - set(keys: string | [string, any][], data?: any): void { - if (Array.isArray(keys)) { - for (const [key, value] of keys) { - this.storage.set(key, JSON.stringify(value)); - } - } else { - this.storage.set(keys, JSON.stringify(data)); + get(keys: string) { + const data = this.storage.get(keys); + return data ? JSON.parse(data) : null; + } + + bulkSet(keys: [string, any][]) { + for (const [key, value] of keys) { + this.storage.set(key, JSON.stringify(value)); } } - patch(updateOnly: boolean, keys: string, data: any): void; - patch(updateOnly: boolean, keys: [string, any][]): void; - patch(updateOnly: boolean, keys: string | [string, any][], data?: any): void { - if (Array.isArray(keys)) { - for (const [key, value] of keys) { - const oldData = this.get(key); - if (updateOnly && !oldData) { - continue; - } - this.storage.set( - key, - Array.isArray(value) ? JSON.stringify(value) : JSON.stringify({ ...(oldData ?? {}), ...value }), - ); - } - } else { - const oldData = this.get(keys); + set(key: string, data: any) { + this.storage.set(key, JSON.stringify(data)); + } + + bulkPatch(updateOnly: boolean, keys: [string, any][]) { + for (const [key, value] of keys) { + const oldData = this.get(key); if (updateOnly && !oldData) { - return; + continue; } this.storage.set( - keys, - Array.isArray(data) ? JSON.stringify(data) : JSON.stringify({ ...(oldData ?? {}), ...data }), + key, + Array.isArray(value) ? JSON.stringify(value) : JSON.stringify({ ...(oldData ?? {}), ...value }), ); } } + patch(updateOnly: boolean, keys: string, data: any) { + const oldData = this.get(keys); + if (updateOnly && !oldData) { + return; + } + this.storage.set( + keys, + Array.isArray(data) ? JSON.stringify(data) : JSON.stringify({ ...(oldData ?? {}), ...data }), + ); + } + values(to: string) { const array: any[] = []; const data = this.keys(to); @@ -96,14 +91,16 @@ export class MemoryAdapter implements Adapter { return this.getToRelationship(to).length; } - remove(keys: string): void; - remove(keys: string[]): void; - remove(keys: string | string[]) { - for (const i of Array.isArray(keys) ? keys : [keys]) { + bulkRemove(keys: string[]) { + for (const i of keys) { this.storage.delete(i); } } + remove(key: string) { + this.storage.delete(key); + } + flush(): void { this.storage.clear(); this.relationships.clear(); diff --git a/src/cache/adapters/limited.ts b/src/cache/adapters/limited.ts index 1038127..1c15767 100644 --- a/src/cache/adapters/limited.ts +++ b/src/cache/adapters/limited.ts @@ -9,18 +9,23 @@ export interface ResourceLimitedMemoryAdapter { export interface LimitedMemoryAdapterOptions { default?: ResourceLimitedMemoryAdapter; + guild?: ResourceLimitedMemoryAdapter; user?: ResourceLimitedMemoryAdapter; + + ban?: ResourceLimitedMemoryAdapter; member?: ResourceLimitedMemoryAdapter; voice_state?: ResourceLimitedMemoryAdapter; + channel?: ResourceLimitedMemoryAdapter; emoji?: ResourceLimitedMemoryAdapter; - overwrite?: ResourceLimitedMemoryAdapter; presence?: ResourceLimitedMemoryAdapter; role?: ResourceLimitedMemoryAdapter; stage_instance?: ResourceLimitedMemoryAdapter; sticker?: ResourceLimitedMemoryAdapter; thread?: ResourceLimitedMemoryAdapter; + overwrite?: ResourceLimitedMemoryAdapter; + message?: ResourceLimitedMemoryAdapter; } export class LimitedMemoryAdapter implements Adapter { @@ -58,13 +63,7 @@ export class LimitedMemoryAdapter implements Adapter { return values; } - get(keys: string): any; - get(keys: string[]): any[]; - get(keys: string | string[]) { - if (!Array.isArray(keys)) { - const data = [...this.storage.values()].find(x => x.has(keys))?.get(keys); - return data ? JSON.parse(data) : null; - } + bulkGet(keys: string[]) { const iterator = [...this.storage.values()]; return keys .map(key => { @@ -74,6 +73,11 @@ export class LimitedMemoryAdapter implements Adapter { .filter(x => x); } + get(keys: string) { + const data = [...this.storage.values()].find(x => x.has(keys))?.get(keys); + return data ? JSON.parse(data) : null; + } + private __set(key: string, data: any) { const __guildId = Array.isArray(data) ? data[0].guild_id : data.guild_id; const namespace = `${key.split('.')[0]}${__guildId ? `.${__guildId}` : ''}`; @@ -96,6 +100,7 @@ export class LimitedMemoryAdapter implements Adapter { case 'user': self.removeToRelationship(namespace, k.split('.')[1]); break; + case 'ban': case 'member': case 'voice_state': { @@ -124,38 +129,34 @@ export class LimitedMemoryAdapter implements Adapter { this.storage.get(namespace)!.set(key, JSON.stringify(data)); } - set(keys: string, data: any): void; - set(keys: [string, any][]): void; - set(keys: string | [string, any][], data?: any): void { - if (Array.isArray(keys)) { - for (const [key, value] of keys) { - this.__set(key, value); - } - } else { - this.__set(keys, data); + bulkSet(keys: [string, any][]) { + for (const [key, value] of keys) { + this.__set(key, value); } } - patch(updateOnly: boolean, keys: string, data: any): void; - patch(updateOnly: boolean, keys: [string, any][]): void; - patch(updateOnly: boolean, keys: string | [string, any][], data?: any): void { - if (Array.isArray(keys)) { - for (const [key, value] of keys) { - const oldData = this.get(key); - if (updateOnly && !oldData) { - continue; - } - this.__set(key, Array.isArray(value) ? value : { ...(oldData ?? {}), ...value }); - } - } else { - const oldData = this.get(keys); + set(keys: string, data: any) { + this.__set(keys, data); + } + + bulkPatch(updateOnly: boolean, keys: [string, any][]) { + for (const [key, value] of keys) { + const oldData = this.get(key); if (updateOnly && !oldData) { - return; + continue; } - this.__set(keys, Array.isArray(data) ? data : { ...(oldData ?? {}), ...data }); + this.__set(key, Array.isArray(value) ? value : { ...(oldData ?? {}), ...value }); } } + patch(updateOnly: boolean, keys: string, data: any) { + const oldData = this.get(keys); + if (updateOnly && !oldData) { + return; + } + this.__set(keys, Array.isArray(data) ? data : { ...(oldData ?? {}), ...data }); + } + values(to: string) { const array: any[] = []; const data = this.keys(to); @@ -179,14 +180,16 @@ export class LimitedMemoryAdapter implements Adapter { return this.getToRelationship(to).length; } - remove(keys: string): void; - remove(keys: string[]): void; - remove(keys: string | string[]) { - for (const i of Array.isArray(keys) ? keys : [keys]) { + bulkRemove(keys: string[]) { + for (const i of keys) { this.storage.get(i.split('.')[0])?.delete(i); } } + remove(key: string) { + this.storage.get(key.split('.')[0])?.delete(key); + } + flush(): void { this.storage.clear(); this.relationships.clear(); diff --git a/src/cache/adapters/redis.ts b/src/cache/adapters/redis.ts index b9642af..be1c771 100644 --- a/src/cache/adapters/redis.ts +++ b/src/cache/adapters/redis.ts @@ -31,7 +31,7 @@ export class RedisAdapter implements Adapter { private __scanSets(query: string, returnKeys: true): Promise; private __scanSets(query: string, returnKeys = false) { const match = this.buildKey(query); - return new Promise((r, j) => { + return new Promise((r, j) => { const stream = this.client.scanStream({ match, type: 'set', @@ -39,7 +39,7 @@ export class RedisAdapter implements Adapter { const keys: string[] = []; stream .on('data', resultKeys => keys.push(...resultKeys)) - .on('end', () => (returnKeys ? r(keys.map(x => this.buildKey(x))) : r(this.get(keys)))) + .on('end', () => (returnKeys ? r(keys.map(x => this.buildKey(x))) : r(this.bulkGet(keys)))) .on('error', err => j(err)); }); } @@ -48,7 +48,7 @@ export class RedisAdapter implements Adapter { scan(query: string, returnKeys: true): Promise; scan(query: string, returnKeys = false) { const match = this.buildKey(query); - return new Promise((r, j) => { + return new Promise((r, j) => { const stream = this.client.scanStream({ match, // omit relationships @@ -57,22 +57,12 @@ export class RedisAdapter implements Adapter { const keys: string[] = []; stream .on('data', resultKeys => keys.push(...resultKeys)) - .on('end', () => (returnKeys ? r(keys.map(x => this.buildKey(x))) : r(this.get(keys)))) + .on('end', () => (returnKeys ? r(keys.map(x => this.buildKey(x))) : r(this.bulkGet(keys)))) .on('error', err => j(err)); }); } - async get(keys: string[]): Promise; - async get(keys: string): Promise; - async get(keys: string | string[]) { - if (!Array.isArray(keys)) { - const value = await this.client.hgetall(this.buildKey(keys)); - if (value) { - return toNormal(value); - } - return; - } - + async bulkGet(keys: string[]) { const pipeline = this.client.pipeline(); for (const key of keys) { @@ -82,46 +72,31 @@ export class RedisAdapter implements Adapter { return (await pipeline.exec())?.filter(x => !!x[1]).map(x => toNormal(x[1] as Record)) ?? []; } - async set(id: [string, any][]): Promise; - async set(id: string, data: any): Promise; - async set(id: string | [string, any][], data?: any): Promise { - if (!Array.isArray(id)) { - await this.client.hset(this.buildKey(id), toDb(data)); - return; + async get(keys: string): Promise { + const value = await this.client.hgetall(this.buildKey(keys)); + if (value) { + return toNormal(value); } + } + async bulkSet(data: [string, any][]) { const pipeline = this.client.pipeline(); - for (const [k, v] of id) { + for (const [k, v] of data) { pipeline.hset(this.buildKey(k), toDb(v)); } await pipeline.exec(); } - async patch(updateOnly: boolean, id: [string, any][]): Promise; - async patch(updateOnly: boolean, id: string, data: any): Promise; - async patch(updateOnly: boolean, id: string | [string, any][], data?: any): Promise { - if (!Array.isArray(id)) { - if (updateOnly) { - await this.client.eval( - `if redis.call('exists',KEYS[1]) == 1 then redis.call('hset', KEYS[1], ${Array.from( - { length: Object.keys(data).length * 2 }, - (_, i) => `ARGV[${i + 1}]`, - )}) end`, - 1, - this.buildKey(id), - ...Object.entries(toDb(data)).flat(), - ); - } else { - await this.client.hset(this.buildKey(id), toDb(data)); - } - return; - } + async set(id: string, data: any) { + await this.client.hset(this.buildKey(id), toDb(data)); + } + async bulkPatch(updateOnly: boolean, data: [string, any][]) { const pipeline = this.client.pipeline(); - for (const [k, v] of id) { + for (const [k, v] of data) { if (updateOnly) { pipeline.eval( `if redis.call('exists',KEYS[1]) == 1 then redis.call('hset', KEYS[1], ${Array.from( @@ -140,11 +115,27 @@ export class RedisAdapter implements Adapter { await pipeline.exec(); } + async patch(updateOnly: boolean, id: string, data: any): Promise { + if (updateOnly) { + await this.client.eval( + `if redis.call('exists',KEYS[1]) == 1 then redis.call('hset', KEYS[1], ${Array.from( + { length: Object.keys(data).length * 2 }, + (_, i) => `ARGV[${i + 1}]`, + )}) end`, + 1, + this.buildKey(id), + ...Object.entries(toDb(data)).flat(), + ); + } else { + await this.client.hset(this.buildKey(id), toDb(data)); + } + } + async values(to: string): Promise { const array: unknown[] = []; const data = await this.keys(to); if (data.length) { - const items = await this.get(data); + const items = await this.bulkGet(data); for (const item of items) { if (item) { array.push(item); @@ -164,22 +155,21 @@ export class RedisAdapter implements Adapter { return this.client.scard(`${this.buildKey(to)}:set`); } - async remove(keys: string | string[]): Promise { - if (!Array.isArray(keys)) { - await this.client.del(this.buildKey(keys)); - return; - } - + async bulkRemove(keys: string[]) { await this.client.del(...keys.map(x => this.buildKey(x))); } + async remove(keys: string): Promise { + await this.client.del(this.buildKey(keys)); + } + async flush(): Promise { const keys = await Promise.all([ this.scan(this.buildKey('*'), true), this.__scanSets(this.buildKey('*'), true), ]).then(x => x.flat()); if (!keys.length) return; - await this.remove(keys); + await this.bulkRemove(keys); } async contains(to: string, keys: string): Promise { diff --git a/src/cache/adapters/types.ts b/src/cache/adapters/types.ts index 926834a..a003215 100644 --- a/src/cache/adapters/types.ts +++ b/src/cache/adapters/types.ts @@ -7,17 +7,14 @@ export interface Adapter { scan(query: string, keys: true): Awaitable; scan(query: string, keys?: boolean): Awaitable<(any | string)[]>; - get(keys: string[]): Awaitable; + bulkGet(keys: string[]): Awaitable; get(keys: string): Awaitable; - get(keys: string | string[]): Awaitable; - set(keyValue: [string, any][]): Awaitable; + bulkSet(keyValue: [string, any][]): Awaitable; set(id: string, data: any): Awaitable; - set(id: string | [string, any][], data?: any): Awaitable; - patch(updateOnly: boolean, keyValue: [string, any][]): Awaitable; + bulkPatch(updateOnly: boolean, keyValue: [string, any][]): Awaitable; patch(updateOnly: boolean, id: string, data: any): Awaitable; - patch(updateOnly: boolean, id: string | [string, any][], data?: any): Awaitable; values(to: string): Awaitable; @@ -25,7 +22,8 @@ export interface Adapter { count(to: string): Awaitable; - remove(keys: string | string[]): Awaitable; + bulkRemove(keys: string[]): Awaitable; + remove(keys: string): Awaitable; flush(): Awaitable; diff --git a/src/cache/adapters/workeradapter.ts b/src/cache/adapters/workeradapter.ts index 7d95828..3b9e47d 100644 --- a/src/cache/adapters/workeradapter.ts +++ b/src/cache/adapters/workeradapter.ts @@ -45,14 +45,26 @@ export class WorkerAdapter implements Adapter { return this.send('scan', ...rest); } + bulkGet(...rest: any[]) { + return this.send('bulkGet', ...rest); + } + get(...rest: any[]) { return this.send('get', ...rest); } + bulkSet(...rest: any[]) { + return this.send('bulkSet', ...rest); + } + set(...rest: any[]) { return this.send('set', ...rest); } + bulkPatch(...rest: any[]) { + return this.send('bulkPatch', ...rest); + } + patch(...rest: any[]) { return this.send('patch', ...rest); } @@ -69,6 +81,10 @@ export class WorkerAdapter implements Adapter { return this.send('count', ...rest); } + bulkRemove(...rest: any[]) { + return this.send('bulkRemove', ...rest); + } + remove(...rest: any[]) { return this.send('remove', ...rest); } diff --git a/src/cache/index.ts b/src/cache/index.ts index 69a43f0..aa1dfb3 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -382,7 +382,7 @@ export class Cache { } await this.adapter.bulkAddToRelationShip(relationshipsData); - await this.adapter.patch(false, allData); + await this.adapter.bulkPatch(false, allData); } async bulkSet( @@ -474,7 +474,7 @@ export class Cache { } await this.adapter.bulkAddToRelationShip(relationshipsData); - await this.adapter.set(allData); + await this.adapter.bulkSet(allData); } async onPacket(event: GatewayDispatchPayload) { diff --git a/src/cache/resources/bans.ts b/src/cache/resources/bans.ts index 861c18f..d5c7b66 100644 --- a/src/cache/resources/bans.ts +++ b/src/cache/resources/bans.ts @@ -2,7 +2,7 @@ import type { APIBan } from 'discord-api-types/v10'; import type { ReturnCache } from '../..'; import { fakePromise } from '../../common'; import { GuildBasedResource } from './default/guild-based'; -import { GuildBan } from '../../structures/GuildBan'; +import { type GuildBanStructure, Transformers } from '../../client/transformers'; export class Bans extends GuildBasedResource { namespace = 'ban'; @@ -16,31 +16,31 @@ export class Bans extends GuildBasedResource { return rest; } - override get(id: string, guild: string): ReturnCache { + override get(id: string, guild: string): ReturnCache { return fakePromise(super.get(id, guild)).then(rawBan => - rawBan ? new GuildBan(this.client, rawBan, guild) : undefined, + rawBan ? Transformers.GuildBan(this.client, rawBan, guild) : undefined, ); } - override bulk(ids: string[], guild: string): ReturnCache { + override bulk(ids: string[], guild: string): ReturnCache { return fakePromise(super.bulk(ids, guild)).then( bans => bans .map(rawBan => { - return rawBan ? new GuildBan(this.client, rawBan, guild) : undefined; + return rawBan ? Transformers.GuildBan(this.client, rawBan, guild) : undefined; }) - .filter(Boolean) as GuildBan[], + .filter(Boolean) as GuildBanStructure[], ); } - override values(guild: string): ReturnCache { + override values(guild: string): ReturnCache { return fakePromise(super.values(guild)).then( bans => bans .map(rawBan => { - return rawBan ? new GuildBan(this.client, rawBan, guild) : undefined; + return rawBan ? Transformers.GuildBan(this.client, rawBan, guild) : undefined; }) - .filter(Boolean) as GuildBan[], + .filter(Boolean) as GuildBanStructure[], ); } } diff --git a/src/cache/resources/channels.ts b/src/cache/resources/channels.ts index 8703845..b5e6b68 100644 --- a/src/cache/resources/channels.ts +++ b/src/cache/resources/channels.ts @@ -13,6 +13,10 @@ export class Channels extends GuildRelatedResource { return rest; } + raw(id: string): ReturnCache { + return super.get(id); + } + override get(id: string): ReturnCache { return fakePromise(super.get(id)).then(rawChannel => rawChannel ? channelFrom(rawChannel, this.client) : undefined, diff --git a/src/cache/resources/default/base.ts b/src/cache/resources/default/base.ts index ac0248e..94f03e1 100644 --- a/src/cache/resources/default/base.ts +++ b/src/cache/resources/default/base.ts @@ -48,7 +48,7 @@ export class BaseResource { } bulk(ids: string[]): ReturnCache { - return fakePromise(this.adapter.get(ids.map(id => this.hashId(id)))).then(x => x.filter(y => y)); + return fakePromise(this.adapter.bulkGet(ids.map(id => this.hashId(id)))).then(x => x.filter(y => y)); } set(id: string, data: any) { diff --git a/src/cache/resources/default/guild-based.ts b/src/cache/resources/default/guild-based.ts index 1c29051..c92c650 100644 --- a/src/cache/resources/default/guild-based.ts +++ b/src/cache/resources/default/guild-based.ts @@ -54,7 +54,7 @@ export class GuildBasedResource { } bulk(ids: string[], guild: string): ReturnCache<(T & { guild_id: string })[]> { - return fakePromise(this.adapter.get(ids.map(id => this.hashGuildId(guild, id)))).then(x => x.filter(y => y)); + return fakePromise(this.adapter.bulkGet(ids.map(id => this.hashGuildId(guild, id)))).then(x => x.filter(y => y)); } set(__keys: string, guild: string, data: any): ReturnCache; @@ -71,7 +71,7 @@ export class GuildBasedResource { guild, ), ).then(() => - this.adapter.set( + this.adapter.bulkSet( keys.map(([key, value]) => { return [this.hashGuildId(guild, key), this.parse(value, key, guild)] as const; }), @@ -87,14 +87,14 @@ export class GuildBasedResource { any, ][]; - return fakePromise(this.adapter.get(keys.map(([key]) => this.hashGuildId(guild, key)))).then(oldDatas => + return fakePromise(this.adapter.bulkGet(keys.map(([key]) => this.hashGuildId(guild, key)))).then(oldDatas => fakePromise( this.addToRelationship( keys.map(x => x[0]), guild, ), ).then(() => - this.adapter.set( + this.adapter.bulkSet( keys.map(([key, value]) => { const oldData = oldDatas.find(x => x.id === key) ?? {}; return [this.hashGuildId(guild, key), this.parse({ ...oldData, ...value }, key, guild)]; @@ -107,7 +107,7 @@ export class GuildBasedResource { remove(id: string | string[], guild: string) { const ids = Array.isArray(id) ? id : [id]; return fakePromise(this.removeToRelationship(ids, guild)).then(() => - this.adapter.remove(ids.map(x => this.hashGuildId(guild, x))), + this.adapter.bulkRemove(ids.map(x => this.hashGuildId(guild, x))), ); } diff --git a/src/cache/resources/default/guild-related.ts b/src/cache/resources/default/guild-related.ts index 9357709..ea21c30 100644 --- a/src/cache/resources/default/guild-related.ts +++ b/src/cache/resources/default/guild-related.ts @@ -54,7 +54,7 @@ export class GuildRelatedResource { } bulk(ids: string[]): ReturnCache<(T & { guild_id: string })[]> { - return fakePromise(this.adapter.get(ids.map(x => this.hashId(x)))).then(x => x.filter(y => y)); + return fakePromise(this.adapter.bulkGet(ids.map(x => this.hashId(x)))).then(x => x.filter(y => y)); } set(__keys: string, guild: string, data: any): ReturnCache; @@ -72,7 +72,7 @@ export class GuildRelatedResource { ), ).then( () => - this.adapter.set( + this.adapter.bulkSet( keys.map(([key, value]) => { return [this.hashId(key), this.parse(value, key, guild)] as const; }), @@ -96,7 +96,7 @@ export class GuildRelatedResource { ), ).then( () => - this.adapter.patch( + this.adapter.bulkPatch( false, keys.map(([key, value]) => { return [this.hashId(key), this.parse(value, key, guild)] as const; @@ -105,7 +105,7 @@ export class GuildRelatedResource { ); } return fakePromise( - this.adapter.patch( + this.adapter.bulkPatch( true, keys.map(([key, value]) => { return [this.hashId(key), value]; @@ -117,7 +117,7 @@ export class GuildRelatedResource { remove(id: string | string[], guild: string) { const ids = Array.isArray(id) ? id : [id]; return fakePromise(this.removeToRelationship(ids, guild)).then(() => - this.adapter.remove(ids.map(x => this.hashId(x))), + this.adapter.bulkRemove(ids.map(x => this.hashId(x))), ); } @@ -133,7 +133,7 @@ export class GuildRelatedResource { return guild === '*' ? (fakePromise(this.adapter.scan(this.hashId(guild))).then(x => x) as (T & { guild_id: string })[]) : (fakePromise(this.adapter.getToRelationship(this.hashId(guild))).then(keys => - this.adapter.get(keys.map(x => `${this.namespace}.${x}`)), + this.adapter.bulkGet(keys.map(x => `${this.namespace}.${x}`)), ) as (T & { guild_id: string })[]); } diff --git a/src/cache/resources/emojis.ts b/src/cache/resources/emojis.ts index 59ac612..9d3981a 100644 --- a/src/cache/resources/emojis.ts +++ b/src/cache/resources/emojis.ts @@ -1,8 +1,8 @@ import type { APIEmoji } from 'discord-api-types/v10'; import type { ReturnCache } from '../..'; import { fakePromise } from '../../common'; -import { GuildEmoji } from '../../structures'; import { GuildRelatedResource } from './default/guild-related'; +import { type GuildEmojiStructure, Transformers } from '../../client/transformers'; export class Emojis extends GuildRelatedResource { namespace = 'emoji'; @@ -12,21 +12,21 @@ export class Emojis extends GuildRelatedResource { return true; } - override get(id: string): ReturnCache { + override get(id: string): ReturnCache { return fakePromise(super.get(id)).then(rawEmoji => - rawEmoji ? new GuildEmoji(this.client, rawEmoji, rawEmoji.guild_id) : undefined, + rawEmoji ? Transformers.GuildEmoji(this.client, rawEmoji, rawEmoji.guild_id) : undefined, ); } - override bulk(ids: string[]): ReturnCache { + override bulk(ids: string[]): ReturnCache { return fakePromise(super.bulk(ids) as (APIEmoji & { id: string; guild_id: string })[]).then(emojis => - emojis.map(rawEmoji => new GuildEmoji(this.client, rawEmoji, rawEmoji.guild_id)), + emojis.map(rawEmoji => Transformers.GuildEmoji(this.client, rawEmoji, rawEmoji.guild_id)), ); } - override values(guild: string): ReturnCache { + override values(guild: string): ReturnCache { return fakePromise(super.values(guild) as (APIEmoji & { id: string; guild_id: string })[]).then(emojis => - emojis.map(rawEmoji => new GuildEmoji(this.client, rawEmoji, rawEmoji.guild_id)), + emojis.map(rawEmoji => Transformers.GuildEmoji(this.client, rawEmoji, rawEmoji.guild_id)), ); } } diff --git a/src/cache/resources/guilds.ts b/src/cache/resources/guilds.ts index c1f6d23..7141485 100644 --- a/src/cache/resources/guilds.ts +++ b/src/cache/resources/guilds.ts @@ -1,8 +1,8 @@ import type { APIGuild } from 'discord-api-types/v10'; import type { Cache, ReturnCache } from '..'; import { fakePromise } from '../../common'; -import { Guild } from '../../structures'; import { BaseResource } from './default/base'; +import { type GuildStructure, Transformers } from '../../client/transformers'; export class Guilds extends BaseResource { namespace = 'guild'; @@ -12,24 +12,30 @@ export class Guilds extends BaseResource { return true; } - override get(id: string): ReturnCache | undefined> { - return fakePromise(super.get(id)).then(guild => (guild ? new Guild<'cached'>(this.client, guild) : undefined)); + raw(id: string): ReturnCache { + return super.get(id); } - override bulk(ids: string[]): ReturnCache[]> { - return fakePromise(super.bulk(ids) as APIGuild[]).then(guilds => - guilds.map(x => new Guild<'cached'>(this.client, x)), + override get(id: string): ReturnCache | undefined> { + return fakePromise(super.get(id)).then(guild => + guild ? Transformers.Guild<'cached'>(this.client, guild) : undefined, ); } - override values(): ReturnCache[]> { + override bulk(ids: string[]): ReturnCache[]> { + return fakePromise(super.bulk(ids) as APIGuild[]).then(guilds => + guilds.map(x => Transformers.Guild<'cached'>(this.client, x)), + ); + } + + override values(): ReturnCache[]> { return fakePromise(super.values() as APIGuild[]).then(guilds => - guilds.map(x => new Guild<'cached'>(this.client, x)), + guilds.map(x => Transformers.Guild<'cached'>(this.client, x)), ); } override async remove(id: string) { - await this.cache.adapter.remove( + await this.cache.adapter.bulkRemove( ( await Promise.all([ this.cache.members?.keys(id) ?? [], diff --git a/src/cache/resources/members.ts b/src/cache/resources/members.ts index 0a5cb80..112ca5a 100644 --- a/src/cache/resources/members.ts +++ b/src/cache/resources/members.ts @@ -1,8 +1,8 @@ import type { APIGuildMember } from 'discord-api-types/v10'; import type { ReturnCache } from '../..'; import { fakePromise } from '../../common'; -import { GuildMember } from '../../structures'; import { GuildBasedResource } from './default/guild-based'; +import { type GuildMemberStructure, Transformers } from '../../client/transformers'; export class Members extends GuildBasedResource { namespace = 'member'; @@ -16,38 +16,51 @@ export class Members extends GuildBasedResource { return rest; } - override get(id: string, guild: string): ReturnCache { + raw(id: string, guild: string): ReturnCache { + return fakePromise(super.get(id, guild) as Omit).then(rawMember => { + return fakePromise(this.client.cache.users?.raw(id)).then(user => + rawMember && user + ? { + ...rawMember, + user, + } + : undefined, + ); + }); + } + + override get(id: string, guild: string): ReturnCache { return fakePromise(super.get(id, guild)).then(rawMember => - fakePromise(this.client.cache.users?.get(id)).then(user => - rawMember && user ? new GuildMember(this.client, rawMember, user, guild) : undefined, + fakePromise(this.client.cache.users?.raw(id)).then(user => + rawMember && user ? Transformers.GuildMember(this.client, rawMember, user, guild) : undefined, ), ); } - override bulk(ids: string[], guild: string): ReturnCache { + override bulk(ids: string[], guild: string): ReturnCache { return fakePromise(super.bulk(ids, guild)).then(members => - fakePromise(this.client.cache.users?.bulk(ids)).then( + fakePromise(this.client.cache.users?.bulkRaw(ids)).then( users => members .map(rawMember => { const user = users?.find(x => x.id === rawMember.id); - return user ? new GuildMember(this.client, rawMember, user, guild) : undefined; + return user ? Transformers.GuildMember(this.client, rawMember, user, guild) : undefined; }) - .filter(Boolean) as GuildMember[], + .filter(Boolean) as GuildMemberStructure[], ), ); } - override values(guild: string): ReturnCache { + override values(guild: string): ReturnCache { return fakePromise(super.values(guild)).then(members => - fakePromise(this.client.cache.users?.values()).then( + fakePromise(this.client.cache.users?.valuesRaw()).then( users => members .map(rawMember => { const user = users?.find(x => x.id === rawMember.id); - return user ? new GuildMember(this.client, rawMember, user, rawMember.guild_id) : undefined; + return user ? Transformers.GuildMember(this.client, rawMember, user, rawMember.guild_id) : undefined; }) - .filter(Boolean) as GuildMember[], + .filter(Boolean) as GuildMemberStructure[], ), ); } diff --git a/src/cache/resources/messages.ts b/src/cache/resources/messages.ts index 61af361..e83a5e2 100644 --- a/src/cache/resources/messages.ts +++ b/src/cache/resources/messages.ts @@ -2,7 +2,7 @@ import type { APIMessage, APIUser } from 'discord-api-types/v10'; import { GuildRelatedResource } from './default/guild-related'; import type { MessageData, ReturnCache } from '../..'; import { fakePromise } from '../../common'; -import { Message } from '../../structures'; +import { type MessageStructure, Transformers } from '../../client/transformers'; export class Messages extends GuildRelatedResource { namespace = 'message'; @@ -19,19 +19,19 @@ export class Messages extends GuildRelatedResource { return rest; } - override get(id: string): ReturnCache { + override get(id: string): ReturnCache { return fakePromise(super.get(id) as APIMessageResource | undefined).then(rawMessage => { return this.cache.users && rawMessage?.user_id ? fakePromise(this.cache.adapter.get(this.cache.users.hashId(rawMessage.user_id)) as APIUser | undefined).then( user => { - return user ? new Message(this.client, { ...rawMessage!, author: user }) : undefined; + return user ? Transformers.Message(this.client, { ...rawMessage!, author: user }) : undefined; }, ) : undefined; }); } - override bulk(ids: string[]): ReturnCache { + override bulk(ids: string[]): ReturnCache { return fakePromise(super.bulk(ids) as APIMessageResource[]).then( messages => messages @@ -40,26 +40,26 @@ export class Messages extends GuildRelatedResource { ? fakePromise( this.cache.adapter.get(this.cache.users.hashId(rawMessage.user_id)) as APIUser | undefined, ).then(user => { - return user ? new Message(this.client, { ...rawMessage!, author: user }) : undefined; + return user ? Transformers.Message(this.client, { ...rawMessage!, author: user }) : undefined; }) : undefined; }) - .filter(Boolean) as Message[], + .filter(Boolean) as MessageStructure[], ); } - override values(guild: string): ReturnCache { + override values(guild: string): ReturnCache { return fakePromise(super.values(guild) as APIMessageResource[]).then(messages => { const hashes: (string | undefined)[] = this.cache.users ? messages.map(x => (x.user_id ? this.cache.users!.hashId(x.user_id) : undefined)) : []; - return fakePromise(this.cache.adapter.get(hashes.filter(Boolean) as string[]) as APIUser[]).then(users => { + return fakePromise(this.cache.adapter.bulkGet(hashes.filter(Boolean) as string[]) as APIUser[]).then(users => { return messages .map(message => { const user = users.find(user => user.id === message.user_id); - return user ? new Message(this.client, { ...message, author: user }) : undefined; + return user ? Transformers.Message(this.client, { ...message, author: user }) : undefined; }) - .filter(Boolean) as Message[]; + .filter(Boolean) as MessageStructure[]; }); }); } diff --git a/src/cache/resources/roles.ts b/src/cache/resources/roles.ts index 26f534c..9bec29e 100644 --- a/src/cache/resources/roles.ts +++ b/src/cache/resources/roles.ts @@ -1,8 +1,8 @@ import type { APIRole } from 'discord-api-types/v10'; import type { ReturnCache } from '../..'; import { fakePromise } from '../../common'; -import { GuildRole } from '../../structures'; import { GuildRelatedResource } from './default/guild-related'; +import { type GuildRoleStructure, Transformers } from '../../client/transformers'; export class Roles extends GuildRelatedResource { namespace = 'role'; @@ -12,21 +12,29 @@ export class Roles extends GuildRelatedResource { return true; } - override get(id: string): ReturnCache { + raw(id: string): ReturnCache { + return super.get(id); + } + + override get(id: string): ReturnCache { return fakePromise(super.get(id)).then(rawRole => - rawRole ? new GuildRole(this.client, rawRole, rawRole.guild_id) : undefined, + rawRole ? Transformers.GuildRole(this.client, rawRole, rawRole.guild_id) : undefined, ); } - override bulk(ids: string[]): ReturnCache { + override bulk(ids: string[]): ReturnCache { return fakePromise(super.bulk(ids)).then(roles => - roles.map(rawRole => new GuildRole(this.client, rawRole, rawRole.guild_id)), + roles.map(rawRole => Transformers.GuildRole(this.client, rawRole, rawRole.guild_id)), ); } - override values(guild: string): ReturnCache { + override values(guild: string): ReturnCache { return fakePromise(super.values(guild)).then(roles => - roles.map(rawRole => new GuildRole(this.client, rawRole, rawRole.guild_id)), + roles.map(rawRole => Transformers.GuildRole(this.client, rawRole, rawRole.guild_id)), ); } + + valuesRaw(guild: string): ReturnCache { + return super.values(guild); + } } diff --git a/src/cache/resources/stickers.ts b/src/cache/resources/stickers.ts index 208f4c0..aee0186 100644 --- a/src/cache/resources/stickers.ts +++ b/src/cache/resources/stickers.ts @@ -1,8 +1,8 @@ import type { APISticker } from 'discord-api-types/v10'; import type { ReturnCache } from '../..'; import { fakePromise } from '../../common'; -import { Sticker } from '../../structures'; import { GuildRelatedResource } from './default/guild-related'; +import { type StickerStructure, Transformers } from '../../client/transformers'; export class Stickers extends GuildRelatedResource { namespace = 'sticker'; @@ -12,21 +12,21 @@ export class Stickers extends GuildRelatedResource { return true; } - override get(id: string): ReturnCache { + override get(id: string): ReturnCache { return fakePromise(super.get(id)).then(rawSticker => - rawSticker ? new Sticker(this.client, rawSticker) : undefined, + rawSticker ? Transformers.Sticker(this.client, rawSticker) : undefined, ); } - override bulk(ids: string[]): ReturnCache { + override bulk(ids: string[]): ReturnCache { return fakePromise(super.bulk(ids) as APISticker[]).then(emojis => - emojis.map(rawSticker => new Sticker(this.client, rawSticker)), + emojis.map(rawSticker => Transformers.Sticker(this.client, rawSticker)), ); } - override values(guild: string): ReturnCache { + override values(guild: string): ReturnCache { return fakePromise(super.values(guild) as APISticker[]).then(emojis => - emojis.map(rawSticker => new Sticker(this.client, rawSticker)), + emojis.map(rawSticker => Transformers.Sticker(this.client, rawSticker)), ); } } diff --git a/src/cache/resources/threads.ts b/src/cache/resources/threads.ts index e83b885..b45d509 100644 --- a/src/cache/resources/threads.ts +++ b/src/cache/resources/threads.ts @@ -1,8 +1,8 @@ import type { APIThreadChannel } from 'discord-api-types/v10'; import type { ReturnCache } from '../..'; import { fakePromise } from '../../common'; -import { ThreadChannel } from '../../structures'; import { GuildRelatedResource } from './default/guild-related'; +import { type ThreadChannelStructure, Transformers } from '../../client/transformers'; export class Threads extends GuildRelatedResource { namespace = 'thread'; @@ -12,21 +12,21 @@ export class Threads extends GuildRelatedResource { return true; } - override get(id: string): ReturnCache { + override get(id: string): ReturnCache { return fakePromise(super.get(id)).then(rawThread => - rawThread ? new ThreadChannel(this.client, rawThread) : undefined, + rawThread ? Transformers.ThreadChannel(this.client, rawThread) : undefined, ); } - override bulk(ids: string[]): ReturnCache { + override bulk(ids: string[]): ReturnCache { return fakePromise(super.bulk(ids) as APIThreadChannel[]).then(threads => - threads.map(rawThread => new ThreadChannel(this.client, rawThread)), + threads.map(rawThread => Transformers.ThreadChannel(this.client, rawThread)), ); } - override values(guild: string): ReturnCache { + override values(guild: string): ReturnCache { return fakePromise(super.values(guild) as APIThreadChannel[]).then(threads => - threads.map(rawThread => new ThreadChannel(this.client, rawThread)), + threads.map(rawThread => Transformers.ThreadChannel(this.client, rawThread)), ); } } diff --git a/src/cache/resources/users.ts b/src/cache/resources/users.ts index b5f01d6..7fc44ea 100644 --- a/src/cache/resources/users.ts +++ b/src/cache/resources/users.ts @@ -1,8 +1,8 @@ import type { APIUser } from 'discord-api-types/v10'; import type { ReturnCache } from '../..'; import { fakePromise } from '../../common'; -import { User } from '../../structures'; import { BaseResource } from './default/base'; +import { Transformers, type UserStructure } from '../../client/transformers'; export class Users extends BaseResource { namespace = 'user'; @@ -12,17 +12,31 @@ export class Users extends BaseResource { return true; } - override get(id: string): ReturnCache { - return fakePromise(super.get(id)).then(rawUser => (rawUser ? new User(this.client, rawUser) : undefined)); + raw(id: string): ReturnCache { + return super.get(id); } - override bulk(ids: string[]): ReturnCache { + override get(id: string): ReturnCache { + return fakePromise(super.get(id)).then(rawUser => (rawUser ? Transformers.User(this.client, rawUser) : undefined)); + } + + override bulk(ids: string[]): ReturnCache { return fakePromise(super.bulk(ids) as APIUser[]).then(users => - users.map(rawUser => new User(this.client, rawUser)), + users.map(rawUser => Transformers.User(this.client, rawUser)), ); } - override values(): ReturnCache { - return fakePromise(super.values() as APIUser[]).then(users => users.map(rawUser => new User(this.client, rawUser))); + bulkRaw(ids: string[]): ReturnCache { + return super.bulk(ids); + } + + override values(): ReturnCache { + return fakePromise(super.values() as APIUser[]).then(users => + users.map(rawUser => Transformers.User(this.client, rawUser)), + ); + } + + valuesRaw(): ReturnCache { + return super.values(); } } diff --git a/src/cache/resources/voice-states.ts b/src/cache/resources/voice-states.ts index 6f84fd4..1ede0b3 100644 --- a/src/cache/resources/voice-states.ts +++ b/src/cache/resources/voice-states.ts @@ -1,8 +1,8 @@ import type { GatewayVoiceState } from 'discord-api-types/v10'; import type { ReturnCache } from '../..'; import { fakePromise } from '../../common'; -import { VoiceState } from '../../structures'; import { GuildBasedResource } from './default/guild-based'; +import { Transformers, type VoiceStateStructure } from '../../client/transformers'; export class VoiceStates extends GuildBasedResource { namespace = 'voice_state'; @@ -17,21 +17,25 @@ export class VoiceStates extends GuildBasedResource { return rest; } - override get(memberId: string, guildId: string): ReturnCache { + override get(memberId: string, guildId: string): ReturnCache { return fakePromise(super.get(memberId, guildId)).then(state => - state ? new VoiceState(this.client, state) : undefined, + state ? Transformers.VoiceState(this.client, state) : undefined, ); } - override bulk(ids: string[], guild: string): ReturnCache { + override bulk(ids: string[], guild: string): ReturnCache { return fakePromise(super.bulk(ids, guild)).then( states => - states.map(state => (state ? new VoiceState(this.client, state) : undefined)).filter(y => !!y) as VoiceState[], + states + .map(state => (state ? Transformers.VoiceState(this.client, state) : undefined)) + .filter(y => !!y) as VoiceStateStructure[], ); } - override values(guildId: string): ReturnCache { - return fakePromise(super.values(guildId)).then(states => states.map(state => new VoiceState(this.client, state))); + override values(guildId: string): ReturnCache { + return fakePromise(super.values(guildId)).then(states => + states.map(state => Transformers.VoiceState(this.client, state)), + ); } } diff --git a/src/client/base.ts b/src/client/base.ts index 90c2dfb..ddb5760 100644 --- a/src/client/base.ts +++ b/src/client/base.ts @@ -5,9 +5,11 @@ import { Cache, MemoryAdapter } from '../cache'; import type { Command, CommandContext, + ContextMenuCommand, ExtraProps, - OnOptionsReturnObject, + MenuCommandContext, RegisteredMiddlewares, + SubCommand, UsingClient, } from '../commands'; import { IgnoreCommand, type InferWithPrefix, type MiddlewareContext } from '../commands/applications/shared'; @@ -40,7 +42,6 @@ import { LangsHandler } from '../langs/handler'; import type { ChatInputCommandInteraction, ComponentInteraction, - Message, MessageCommandInteraction, ModalSubmitInteraction, UserCommandInteraction, @@ -48,6 +49,8 @@ import type { import type { ComponentCommand, ComponentContext, ModalCommand, ModalContext } from '../components'; import { promises } from 'node:fs'; import { BanShorter } from '../common/shorters/bans'; +import { HandleCommand } from '../commands/handle'; +import type { MessageStructure } from './transformers'; export class BaseClient { rest!: ApiHandler; @@ -76,6 +79,7 @@ export class BaseClient { langs? = new LangsHandler(this.logger); commands? = new CommandHandler(this.logger, this); components? = new ComponentHandler(this.logger, this); + handleCommand!: HandleCommand; private _applicationId?: string; private _botId?: string; @@ -101,30 +105,30 @@ export class BaseClient { { commands: { defaults: { - onRunError(context: CommandContext, error: unknown): any { + onRunError(context, error): any { context.client.logger.fatal(`${context.command.name}.`, context.author.id, error); }, - onOptionsError(context: CommandContext<{}, never>, metadata: OnOptionsReturnObject): any { + onOptionsError(context, metadata): any { context.client.logger.fatal(`${context.command.name}.`, context.author.id, metadata); }, - onMiddlewaresError(context: CommandContext<{}, never>, error: string): any { + onMiddlewaresError(context, error: string): any { context.client.logger.fatal(`${context.command.name}.`, context.author.id, error); }, - onBotPermissionsFail(context: CommandContext<{}, never>, permissions: PermissionStrings): any { + onBotPermissionsFail(context, permissions): any { context.client.logger.fatal( `${context.command.name}.`, context.author.id, permissions, ); }, - onPermissionsFail(context: CommandContext<{}, never>, permissions: PermissionStrings): any { + onPermissionsFail(context, permissions): any { context.client.logger.fatal( `${context.command.name}.`, context.author.id, permissions, ); }, - onInternalError(client: UsingClient, command: Command, error?: unknown): any { + onInternalError(client: UsingClient, command, error?: unknown): any { client.logger.fatal(`${command.name}.`, error); }, }, @@ -180,7 +184,7 @@ export class BaseClient { return new Router(this.rest).createProxy(); } - setServices({ rest, cache, langs, middlewares, handlers }: ServicesOptions) { + setServices({ rest, cache, langs, middlewares, handlers, handleCommand }: ServicesOptions) { if (rest) { this.rest = rest; } @@ -201,9 +205,6 @@ export class BaseClient { this.components = undefined; } else if (typeof handlers.components === 'function') { this.components ??= new ComponentHandler(this.logger, this); - this.components.setHandlers({ - callback: handlers.components, - }); } else { this.components = handlers.components; } @@ -213,7 +214,6 @@ export class BaseClient { this.commands = undefined; } else if (typeof handlers.commands === 'object') { this.commands ??= new CommandHandler(this.logger, this); - this.commands.setHandlers(handlers.commands); } else { this.commands = handlers.commands; } @@ -233,6 +233,8 @@ export class BaseClient { if (langs.default) this.langs!.defaultLang = langs.default; if (langs.aliases) this.langs!.aliases = Object.entries(langs.aliases); } + + if (handleCommand) this.handleCommand = new handleCommand(this); } protected async execute(..._options: unknown[]) { @@ -275,6 +277,8 @@ export class BaseClient { } else { this.cache = new Cache(0, new MemoryAdapter(), [], this); } + + if (!this.handleCommand) this.handleCommand = new HandleCommand(this); } protected async onPacket(..._packet: unknown[]) { @@ -383,18 +387,25 @@ export interface BaseClientOptions { | MessageCommandInteraction | ComponentInteraction | ModalSubmitInteraction - | When, + | When, ) => {}; globalMiddlewares?: readonly (keyof RegisteredMiddlewares)[]; commands?: { defaults?: { - onRunError?: Command['onRunError']; + onRunError?: (context: MenuCommandContext | CommandContext, error: unknown) => unknown; onPermissionsFail?: Command['onPermissionsFail']; - onBotPermissionsFail?: Command['onBotPermissionsFail']; - onInternalError?: Command['onInternalError']; - onMiddlewaresError?: Command['onMiddlewaresError']; + onBotPermissionsFail?: ( + context: MenuCommandContext | CommandContext, + permissions: PermissionStrings, + ) => unknown; + onInternalError?: ( + client: UsingClient, + command: Command | SubCommand | ContextMenuCommand, + error?: unknown, + ) => unknown; + onMiddlewaresError?: (context: CommandContext | MenuCommandContext, error: string) => unknown; onOptionsError?: Command['onOptionsError']; - onAfterRun?: Command['onAfterRun']; + onAfterRun?: (context: CommandContext | MenuCommandContext, error: unknown) => unknown; props?: ExtraProps; }; }; @@ -480,7 +491,8 @@ export interface ServicesOptions { middlewares?: Record; handlers?: { components?: ComponentHandler | ComponentHandler['callback']; - commands?: CommandHandler | Parameters[0]; + commands?: CommandHandler; langs?: LangsHandler | LangsHandler['callback']; }; + handleCommand?: typeof HandleCommand; } diff --git a/src/client/client.ts b/src/client/client.ts index e53db36..05b6478 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -1,23 +1,7 @@ -import { - type APIApplicationCommandInteractionDataOption, - GatewayIntentBits, - type GatewayMessageCreateDispatchData, - type GatewayDispatchPayload, - type GatewayPresenceUpdateData, -} from 'discord-api-types/v10'; -import type { - Command, - CommandContext, - ContextOptionsResolved, - Message, - MessageCommandOptionErrors, - SubCommand, - UsingClient, -} from '..'; +import { GatewayIntentBits, type GatewayDispatchPayload, type GatewayPresenceUpdateData } from 'discord-api-types/v10'; +import type { CommandContext, Message } from '..'; import { type Awaitable, - type MakeRequired, - MergeOptions, lazyLoadPackage, type DeepPartial, type If, @@ -25,24 +9,22 @@ import { type WatcherSendToShard, } from '../common'; import { EventHandler } from '../events'; -import { ClientUser } from '../structures'; import { ShardManager, properties, type ShardManagerOptions } from '../websocket'; import { MemberUpdateHandler } from '../websocket/discord/events/memberUpdate'; import { PresenceUpdateHandler } from '../websocket/discord/events/presenceUpdate'; import type { BaseClientOptions, InternalRuntimeConfig, ServicesOptions, StartOptions } from './base'; import { BaseClient } from './base'; -import { onInteractionCreate } from './oninteractioncreate'; -import { defaultArgsParser, defaultOptionsParser, onMessageCreate } from './onmessagecreate'; import { Collectors } from './collectors'; +import { type ClientUserStructure, Transformers, type MessageStructure } from './transformers'; let parentPort: import('node:worker_threads').MessagePort; export class Client extends BaseClient { private __handleGuilds?: Set = new Set(); gateway!: ShardManager; - me!: If; + me!: If; declare options: Omit & { - commands: MakeRequired, 'argsParser' | 'optionsParser'>; + commands: NonNullable; }; memberUpdateHandler = new MemberUpdateHandler(); presenceUpdateHandler = new PresenceUpdateHandler(); @@ -51,15 +33,6 @@ export class Client extends BaseClient { constructor(options?: ClientOptions) { super(options); - this.options = MergeOptions( - { - commands: { - argsParser: options?.commands?.argsParser ?? defaultArgsParser, - optionsParser: options?.commands?.optionsParser ?? defaultOptionsParser, - }, - } satisfies ClientOptions, - this.options, - ); } setServices({ @@ -86,9 +59,6 @@ export class Client extends BaseClient { this.events = undefined; } else if (typeof rest.handlers.events === 'function') { this.events = new EventHandler(this); - this.events.setHandlers({ - callback: rest.handlers.events, - }); } else { this.events = rest.handlers.events; } @@ -169,7 +139,10 @@ export class Client extends BaseClient { } protected async onPacket(shardId: number, packet: GatewayDispatchPayload) { - await this.events?.runEvent('RAW', this, packet, shardId); + Promise.allSettled([ + this.events?.runEvent('RAW', this, packet, shardId, false), + this.collectors.run('RAW', packet), + ]); //ignore promise switch (packet.t) { //// Cases where we must obtain the old data before updating case 'GUILD_MEMBER_UPDATE': @@ -179,7 +152,7 @@ export class Client extends BaseClient { await this.events?.execute(packet.t, packet, this as Client, shardId); break; case 'PRESENCE_UPDATE': - if (!this.presenceUpdateHandler.check(packet.d as any)) { + if (!this.presenceUpdateHandler.check(packet.d)) { return; } await this.events?.execute(packet.t, packet, this as Client, shardId); @@ -198,24 +171,25 @@ export class Client extends BaseClient { } //rest of the events default: { - await this.events?.execute(packet.t, packet, this as Client, shardId); + await this.events?.execute(packet.t as never, packet, this as Client, shardId); switch (packet.t) { case 'INTERACTION_CREATE': - await onInteractionCreate(this, packet.d, shardId); + await this.handleCommand.interaction(packet.d, shardId); break; case 'MESSAGE_CREATE': - await onMessageCreate(this, packet.d, shardId); + await this.handleCommand.message(packet.d, shardId); break; case 'READY': + if (!this.__handleGuilds) this.__handleGuilds = new Set(); for (const g of packet.d.guilds) { - this.__handleGuilds?.add(g.id); + this.__handleGuilds.add(g.id); } this.botId = packet.d.user.id; this.applicationId = packet.d.application.id; - this.me = new ClientUser(this, packet.d.user, packet.d.application) as never; + this.me = Transformers.ClientUser(this, packet.d.user, packet.d.application) as never; if ( !( - this.__handleGuilds?.size && + this.__handleGuilds.size && (this.gateway.options.intents & GatewayIntentBits.Guilds) === GatewayIntentBits.Guilds ) ) { @@ -245,24 +219,9 @@ export interface ClientOptions extends BaseClientOptions { compress?: ShardManagerOptions['compress']; }; commands?: BaseClientOptions['commands'] & { - prefix?: (message: Message) => Promise | string[]; + prefix?: (message: MessageStructure) => Awaitable; deferReplyResponse?: (ctx: CommandContext) => Parameters[0]; reply?: (ctx: CommandContext) => boolean; - argsParser?: (content: string, command: SubCommand | Command, message: Message) => Record; - optionsParser?: ( - self: UsingClient, - command: Command | SubCommand, - message: GatewayMessageCreateDispatchData, - args: Partial>, - resolved: MakeRequired, - ) => Awaitable<{ - errors: { - name: string; - error: string; - fullError: MessageCommandOptionErrors; - }[]; - options: APIApplicationCommandInteractionDataOption[]; - }>; }; handlePayload?: ShardManagerOptions['handlePayload']; } diff --git a/src/client/httpclient.ts b/src/client/httpclient.ts index b830b32..86ed7c6 100644 --- a/src/client/httpclient.ts +++ b/src/client/httpclient.ts @@ -11,7 +11,6 @@ import { isBufferLike } from '../api/utils/utils'; import { MergeOptions, isCloudfareWorker, type DeepPartial } from '../common'; import type { BaseClientOptions, InternalRuntimeConfigHTTP, StartOptions } from './base'; import { BaseClient } from './base'; -import { onInteractionCreate } from './oninteractioncreate'; let UWS: typeof import('uWebSockets.js') | undefined; let nacl: typeof import('tweetnacl') | undefined; @@ -164,10 +163,11 @@ export class HttpClient extends BaseClient { default: return new Promise(r => { if (isCloudfareWorker()) - return onInteractionCreate(this, rawBody, -1) + return this.handleCommand + .interaction(rawBody, -1) .then(() => r(new Response())) .catch(() => r(new Response())); - return onInteractionCreate(this, rawBody, -1, async ({ body, files }) => { + return this.handleCommand.interaction(rawBody, -1, async ({ body, files }) => { let response: FormData | APIInteractionResponse; const headers: { 'Content-Type'?: string } = {}; @@ -224,7 +224,7 @@ export class HttpClient extends BaseClient { .end(JSON.stringify({ type: InteractionResponseType.Pong })); break; default: - await onInteractionCreate(this, rawBody, -1, async ({ body, files }) => { + await this.handleCommand.interaction(rawBody, -1, async ({ body, files }) => { res.cork(() => { let response: FormData | APIInteractionResponse; const headers: { 'Content-Type'?: string } = {}; diff --git a/src/client/index.ts b/src/client/index.ts index 7f43f5e..8524207 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -2,3 +2,4 @@ export type { RuntimeConfig, RuntimeConfigHTTP } from './base'; export * from './client'; export * from './httpclient'; export * from './workerclient'; +export * from './transformers'; diff --git a/src/client/oninteractioncreate.ts b/src/client/oninteractioncreate.ts deleted file mode 100644 index e730476..0000000 --- a/src/client/oninteractioncreate.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { ApplicationCommandType, InteractionType, type APIInteraction } from 'discord-api-types/v10'; -import { - BaseCommand, - CommandContext, - MenuCommandContext, - OptionResolver, - type RegisteredMiddlewares, - type Command, - type ContextMenuCommand, - type ContextOptionsResolved, - type UsingClient, -} from '../commands'; -import type { - ChatInputCommandInteraction, - ComponentInteraction, - MessageCommandInteraction, - ModalSubmitInteraction, - UserCommandInteraction, - __InternalReplyFunction, -} from '../structures'; -import { AutocompleteInteraction, BaseInteraction } from '../structures'; -import { ComponentContext, ModalContext } from '../components'; - -export async function onInteractionCreate( - self: UsingClient, - body: APIInteraction, - shardId: number, - __reply?: __InternalReplyFunction, -) { - self.debugger?.debug(`[${InteractionType[body.type] ?? body.type}] Interaction received.`); - switch (body.type) { - case InteractionType.ApplicationCommandAutocomplete: - { - const parentCommand = self.commands?.values.find(x => { - if (body.data.guild_id) { - return x.guildId?.includes(body.data.guild_id) && x.name === body.data.name; - } - return x.name === body.data.name; - }); - const optionsResolver = new OptionResolver( - self, - body.data.options ?? [], - parentCommand as Command, - body.guild_id, - body.data.resolved as ContextOptionsResolved, - ); - const interaction = new AutocompleteInteraction(self, body, optionsResolver, __reply); - const command = optionsResolver.getAutocomplete(); - - // idc, is a YOU problem - if (!command?.autocomplete) - return self.logger.warn( - `${optionsResolver.fullCommandName} ${command?.name} command does not have 'autocomplete' callback`, - ); - try { - try { - await command.autocomplete(interaction); - } catch (error) { - if (!command.onAutocompleteError) - return self.logger.error( - `${optionsResolver.fullCommandName} ${command.name} just threw an error, ${ - error ? (typeof error === 'object' && 'message' in error ? error.message : error) : 'Unknown' - }`, - ); - await command.onAutocompleteError(interaction, error); - } - } catch (error) { - try { - await optionsResolver.getCommand()?.onInternalError?.(self, optionsResolver.getCommand()!, error); - } catch { - // supress error - } - } - } - break; - case InteractionType.ApplicationCommand: - { - switch (body.data.type) { - case ApplicationCommandType.Message: - case ApplicationCommandType.User: - { - const command = self.commands?.values.find(x => { - if (body.data.guild_id) { - return x.guildId?.includes(body.data.guild_id) && x.name === body.data.name; - } - return x.name === body.data.name; - }) as ContextMenuCommand; - const interaction = BaseInteraction.from(self, body, __reply) as - | UserCommandInteraction - | MessageCommandInteraction; - // idc, is a YOU problem - if (!command?.run) - return self.logger.warn(`${command.name ?? 'Unknown'} command does not have 'run' callback`); - const context = new MenuCommandContext(self, interaction, shardId, command); - const extendContext = self.options?.context?.(interaction) ?? {}; - Object.assign(context, extendContext); - try { - if (command.botPermissions && interaction.appPermissions) { - const permissions = interaction.appPermissions.missings( - ...interaction.appPermissions.values([command.botPermissions]), - ); - if (!interaction.appPermissions.has('Administrator') && permissions.length) { - return command.onBotPermissionsFail(context, interaction.appPermissions.keys(permissions)); - } - } - const resultRunGlobalMiddlewares = await BaseCommand.__runMiddlewares( - context, - (self.options?.globalMiddlewares ?? []) as keyof RegisteredMiddlewares, - true, - ); - if (resultRunGlobalMiddlewares.pass) { - return; - } - if ('error' in resultRunGlobalMiddlewares) { - return command.onMiddlewaresError(context, resultRunGlobalMiddlewares.error ?? 'Unknown error'); - } - - const resultRunMiddlewares = await BaseCommand.__runMiddlewares( - context, - command.middlewares as keyof RegisteredMiddlewares, - false, - ); - if (resultRunMiddlewares.pass) { - return; - } - if ('error' in resultRunMiddlewares) { - return command.onMiddlewaresError(context, resultRunMiddlewares.error ?? 'Unknown error'); - } - - try { - await command.run(context); - await command.onAfterRun?.(context, undefined); - } catch (error) { - await command.onRunError(context, error); - await command.onAfterRun?.(context, error); - } - } catch (error) { - try { - await command.onInternalError(self, error); - } catch { - // supress error - } - } - } - break; - case ApplicationCommandType.ChatInput: - { - const parentCommand = self.commands?.values.find(x => { - if (body.data.guild_id) { - return x.guildId?.includes(body.data.guild_id) && x.name === body.data.name; - } - return x.name === body.data.name; - }); - const optionsResolver = new OptionResolver( - self, - body.data.options ?? [], - parentCommand as Command, - body.guild_id, - body.data.resolved as ContextOptionsResolved, - ); - const interaction = BaseInteraction.from(self, body, __reply) as ChatInputCommandInteraction; - const command = optionsResolver.getCommand(); - if (!command?.run) - return self.logger.warn(`${optionsResolver.fullCommandName} command does not have 'run' callback`); - const context = new CommandContext(self, interaction, optionsResolver, shardId, command); - const extendContext = self.options?.context?.(interaction) ?? {}; - Object.assign(context, extendContext); - try { - if (command.botPermissions && interaction.appPermissions) { - const permissions = interaction.appPermissions.missings( - ...interaction.appPermissions.values([command.botPermissions]), - ); - if (!interaction.appPermissions.has('Administrator') && permissions.length) { - return command.onBotPermissionsFail?.(context, interaction.appPermissions.keys(permissions)); - } - } - const [erroredOptions, result] = await command.__runOptions(context, optionsResolver); - if (erroredOptions) { - return command.onOptionsError?.(context, result); - } - const resultRunGlobalMiddlewares = await command.__runGlobalMiddlewares(context); - if (resultRunGlobalMiddlewares.pass) { - return; - } - if ('error' in resultRunGlobalMiddlewares) { - return command.onMiddlewaresError?.(context, resultRunGlobalMiddlewares.error ?? 'Unknown error'); - } - const resultRunMiddlewares = await command.__runMiddlewares(context); - if (resultRunMiddlewares.pass) { - return; - } - if ('error' in resultRunMiddlewares) { - return command.onMiddlewaresError?.(context, resultRunMiddlewares.error ?? 'Unknown error'); - } - - try { - await command.run(context); - await command.onAfterRun?.(context, undefined); - } catch (error) { - await command.onRunError?.(context, error); - await command.onAfterRun?.(context, error); - } - } catch (error) { - try { - await command.onInternalError?.(self, context.command, error); - } catch { - // supress error - } - } - } - break; - } - } - break; - - case InteractionType.ModalSubmit: - { - const interaction = BaseInteraction.from(self, body, __reply) as ModalSubmitInteraction; - if (self.components?.hasModal(interaction)) { - await self.components.onModalSubmit(interaction); - } else { - const context = new ModalContext(self, interaction); - const extended = self.options?.context?.(interaction) ?? {}; - Object.assign(context, extended); - await self.components?.executeModal(context); - } - } - break; - case InteractionType.MessageComponent: - { - const interaction = BaseInteraction.from(self, body, __reply) as ComponentInteraction; - if (self.components?.hasComponent(body.message.id, interaction.customId)) { - await self.components.onComponent(body.message.id, interaction); - } else { - //@ts-expect-error - const context = new ComponentContext(self, interaction); - const extended = self.options?.context?.(interaction) ?? {}; - Object.assign(context, extended); - await self.components?.executeComponent(context); - } - } - break; - } -} diff --git a/src/client/onmessagecreate.ts b/src/client/onmessagecreate.ts deleted file mode 100644 index 3f84053..0000000 --- a/src/client/onmessagecreate.ts +++ /dev/null @@ -1,471 +0,0 @@ -import { - ApplicationCommandOptionType, - ChannelType, - InteractionContextType, - type APIApplicationCommandInteractionDataOption, - type GatewayMessageCreateDispatchData, -} from 'discord-api-types/v10'; -import { - Command, - CommandContext, - IgnoreCommand, - type MessageCommandOptionErrors, - OptionResolver, - SubCommand, - User, - type UsingClient, - type Client, - type CommandOption, - type ContextOptionsResolved, - type SeyfertChannelOption, - type SeyfertIntegerOption, - type SeyfertNumberOption, - type SeyfertStringOption, - type WorkerClient, -} from '..'; -import type { MakeRequired } from '../common'; -import { Message } from '../structures'; - -function getCommandFromContent( - commandRaw: string[], - self: Client | WorkerClient, -): { - command?: Command | SubCommand; - parent?: Command; - fullCommandName: string; -} { - const rawParentName = commandRaw[0]; - const rawGroupName = commandRaw.length === 3 ? commandRaw[1] : undefined; - const rawSubcommandName = rawGroupName ? commandRaw[2] : commandRaw[1]; - const parent = self.commands!.values.find( - x => - (!('ignore' in x) || x.ignore !== IgnoreCommand.Message) && - (x.name === rawParentName || ('aliases' in x ? x.aliases?.includes(rawParentName) : false)), - ); - const fullCommandName = `${rawParentName}${ - rawGroupName ? ` ${rawGroupName} ${rawSubcommandName}` : `${rawSubcommandName ? ` ${rawSubcommandName}` : ''}` - }`; - - if (!(parent instanceof Command)) return { fullCommandName }; - - if (rawGroupName && !parent.groups?.[rawGroupName] && !parent.groupsAliases?.[rawGroupName]) - return getCommandFromContent([rawParentName, rawGroupName], self); - if ( - rawSubcommandName && - !parent.options?.some( - x => x instanceof SubCommand && (x.name === rawSubcommandName || x.aliases?.includes(rawSubcommandName)), - ) - ) - return getCommandFromContent([rawParentName], self); - - const groupName = rawGroupName ? parent.groupsAliases?.[rawGroupName] || rawGroupName : undefined; - - const command = - groupName || rawSubcommandName - ? (parent.options?.find(opt => { - if (opt instanceof SubCommand) { - if (groupName) { - if (opt.group !== groupName) return false; - } - if (opt.group && !groupName) return false; - return rawSubcommandName === opt.name || opt.aliases?.includes(rawSubcommandName); - } - return false; - }) as SubCommand) - : parent; - - return { - command, - fullCommandName, - parent, - }; -} - -export async function onMessageCreate( - self: Client | WorkerClient, - rawMessage: GatewayMessageCreateDispatchData, - shardId: number, -) { - if (!self.options?.commands?.prefix) return; - const message = new Message(self, rawMessage); - const prefixes = (await self.options.commands.prefix(message)).sort((a, b) => b.length - a.length); - const prefix = prefixes.find(x => message.content.startsWith(x)); - - if (!(prefix !== undefined && message.content.startsWith(prefix))) return; - - const content = message.content.slice(prefix.length).trimStart(); - const { fullCommandName, command, parent } = getCommandFromContent( - content - .split(' ') - .filter(x => x) - .slice(0, 3), - self, - ); - - if (!command) return; - if (!command.run) return self.logger.warn(`${fullCommandName} command does not have 'run' callback`); - - if (!(command.contexts.includes(InteractionContextType.BotDM) || message.guildId)) return; - if (!command.contexts.includes(InteractionContextType.Guild) && message.guildId) return; - if (command.guildId && !command.guildId?.includes(message.guildId!)) return; - - const resolved: MakeRequired = { - channels: {}, - roles: {}, - users: {}, - members: {}, - attachments: {}, - }; - - let newContent = content; - for (const i of fullCommandName.split(' ')) { - newContent = newContent.slice(newContent.indexOf(i) + i.length); - } - - const args = self.options.commands.argsParser(newContent.slice(1), command, message); - const { options, errors } = await self.options.commands.optionsParser(self, command, rawMessage, args, resolved); - const optionsResolver = new OptionResolver(self, options, parent as Command, message.guildId, resolved); - const context = new CommandContext(self, message, optionsResolver, shardId, command); - //@ts-expect-error - const extendContext = self.options?.context?.(message) ?? {}; - Object.assign(context, extendContext); - try { - if (errors.length) { - return command.onOptionsError?.( - context, - Object.fromEntries( - errors.map(x => { - return [ - x.name, - { - failed: true, - value: x.error, - parseError: x.fullError, - }, - ]; - }), - ), - ); - } - if (command.defaultMemberPermissions && message.guildId) { - const memberPermissions = await self.members.permissions(message.guildId, message.author.id); - const permissions = memberPermissions.missings(...memberPermissions.values([command.defaultMemberPermissions])); - if ( - !memberPermissions.has('Administrator') && - permissions.length && - (await message.guild())!.ownerId !== message.author.id - ) { - return command.onPermissionsFail?.(context, memberPermissions.keys(permissions)); - } - } - if (command.botPermissions && message.guildId) { - const meMember = await self.cache.members?.get(self.botId, message.guildId); - if (!meMember) return; //enable member cache and "Guilds" intent, lol - const appPermissions = await meMember.fetchPermissions(); - const permissions = appPermissions.missings(...appPermissions.values([command.botPermissions])); - if (!appPermissions.has('Administrator') && permissions.length) { - return command.onBotPermissionsFail?.(context, appPermissions.keys(permissions)); - } - } - const [erroredOptions, result] = await command.__runOptions(context, optionsResolver); - if (erroredOptions) { - return command.onOptionsError?.(context, result); - } - const resultRunGlobalMiddlewares = await command.__runGlobalMiddlewares(context); - if (resultRunGlobalMiddlewares.pass) { - return; - } - if ('error' in resultRunGlobalMiddlewares) { - return command.onMiddlewaresError?.(context, resultRunGlobalMiddlewares.error ?? 'Unknown error'); - } - const resultRunMiddlewares = await command.__runMiddlewares(context); - if (resultRunMiddlewares.pass) { - return; - } - if ('error' in resultRunMiddlewares) { - return command.onMiddlewaresError?.(context, resultRunMiddlewares.error ?? 'Unknown error'); - } - - try { - await command.run?.(context); - await command.onAfterRun?.(context, undefined); - } catch (error) { - await command.onRunError?.(context, error); - await command.onAfterRun?.(context, error); - } - } catch (error) { - try { - await command.onInternalError?.(self, context.command, error); - } catch { - // supress error - } - } -} - -export async function defaultOptionsParser( - self: UsingClient, - command: Command | SubCommand, - message: GatewayMessageCreateDispatchData, - args: Partial>, - resolved: MakeRequired, -) { - const options: APIApplicationCommandInteractionDataOption[] = []; - const errors: { name: string; error: string; fullError: MessageCommandOptionErrors }[] = []; - for (const i of (command.options ?? []) as (CommandOption & { type: ApplicationCommandOptionType })[]) { - try { - let value: string | boolean | number | undefined; - let indexAttachment = -1; - switch (i.type) { - case ApplicationCommandOptionType.Attachment: - if (message.attachments[++indexAttachment]) { - value = message.attachments[indexAttachment].id; - resolved.attachments[value] = message.attachments[indexAttachment]; - } - break; - case ApplicationCommandOptionType.Boolean: - if (args[i.name]) { - value = ['yes', 'y', 'true', 'treu'].includes(args[i.name]!.toLowerCase()); - } - break; - case ApplicationCommandOptionType.Channel: - { - const rawId = - message.content.match(/(?<=<#)[0-9]{17,19}(?=>)/g)?.find(x => args[i.name]?.includes(x)) || - args[i.name]?.match(/[0-9]{17,19}/g)?.[0]; - if (rawId) { - const channel = - (await self.cache.channels?.get(rawId)) ?? (i.required ? await self.channels.fetch(rawId) : undefined); - if (channel) { - if ('channel_types' in i) { - if (!(i as SeyfertChannelOption).channel_types!.includes(channel.type)) { - errors.push({ - name: i.name, - error: `The entered channel type is not one of ${(i as SeyfertChannelOption) - .channel_types!.map(t => ChannelType[t]) - .join(', ')}`, - fullError: ['CHANNEL_TYPES', (i as SeyfertChannelOption).channel_types!], - }); - break; - } - } - value = rawId; - resolved.channels[rawId] = channel; - } - } - } - break; - case ApplicationCommandOptionType.Mentionable: - { - const matches = message.content.match(/<@[0-9]{17,19}(?=>)|<@&[0-9]{17,19}(?=>)/g) ?? []; - for (const match of matches) { - if (match.includes('&')) { - const rawId = match.slice(3); - if (rawId) { - const role = - (await self.cache.roles?.get(rawId)) ?? - (i.required ? (await self.roles.list(message.guild_id!)).find(x => x.id === rawId) : undefined); - if (role) { - value = rawId; - resolved.roles[rawId] = role; - break; - } - } - } else { - const rawId = match.slice(2); - const raw = message.mentions.find(x => rawId === x.id); - if (raw) { - const { member, ...user } = raw; - value = raw.id; - resolved.users[raw.id] = user; - if (member) resolved.members[raw.id] = member; - break; - } - } - } - } - break; - case ApplicationCommandOptionType.Role: - { - const rawId = - message.mention_roles.find(x => args[i.name]?.includes(x)) || args[i.name]?.match(/[0-9]{17,19}/g)?.[0]; - if (rawId) { - const role = - (await self.cache.roles?.get(rawId)) ?? - (i.required ? (await self.roles.list(message.guild_id!)).find(x => x.id === rawId) : undefined); - - if (role) { - value = rawId; - resolved.roles[rawId] = role; - } - } - } - break; - case ApplicationCommandOptionType.User: - { - const rawId = - message.mentions.find(x => args[i.name]?.includes(x.id))?.id || args[i.name]?.match(/[0-9]{17,19}/g)?.[0]; - if (rawId) { - const raw = - message.mentions.find(x => args[i.name]?.includes(x.id)) ?? - (await self.cache.users?.get(rawId)) ?? - (i.required ? await self.users.fetch(rawId) : undefined); - if (raw) { - value = raw.id; - if (raw instanceof User) { - resolved.users[raw.id] = raw; - if (message.guild_id) { - const member = - message.mentions.find(x => args[i.name]?.includes(x.id))?.member ?? - (await self.cache.members?.get(rawId, message.guild_id)) ?? - (i.required ? await self.members.fetch(rawId, message.guild_id) : undefined); - if (member) resolved.members[raw.id] = member; - } - } else { - const { member, ...user } = raw; - resolved.users[user.id] = user; - if (member) resolved.members[user.id] = member; - } - } - } - } - break; - case ApplicationCommandOptionType.String: - { - value = args[i.name]; - const option = i as SeyfertStringOption; - if (!value) break; - if (option.min_length) { - if (value.length < option.min_length) { - value = undefined; - errors.push({ - name: i.name, - error: `The entered string has less than ${option.min_length} characters. The minimum required is ${option.min_length} characters.`, - fullError: ['STRING_MIN_LENGTH', option.min_length], - }); - break; - } - } - if (option.max_length) { - if (value.length > option.max_length) { - value = undefined; - errors.push({ - name: i.name, - error: `The entered string has more than ${option.max_length} characters. The maximum required is ${option.max_length} characters.`, - fullError: ['STRING_MAX_LENGTH', option.max_length], - }); - break; - } - } - if (option.choices?.length) { - const choice = option.choices.find(x => x.name === value); - if (!choice) { - value = undefined; - errors.push({ - name: i.name, - error: `The entered choice is invalid. Please choose one of the following options: ${option.choices - .map(x => x.name) - .join(', ')}.`, - fullError: ['STRING_INVALID_CHOICE', option.choices], - }); - break; - } - value = choice.value; - } - } - break; - case ApplicationCommandOptionType.Number: - case ApplicationCommandOptionType.Integer: - { - const option = i as SeyfertNumberOption | SeyfertIntegerOption; - if (!option.choices?.length) { - value = Number(args[i.name]); - if (args[i.name] === undefined) { - value = undefined; - break; - } - if (Number.isNaN(value)) { - value = undefined; - errors.push({ - name: i.name, - error: 'The entered choice is an invalid number.', - fullError: ['NUMBER_NAN', args[i.name]], - }); - break; - } - if (option.min_value) { - if (value < option.min_value) { - value = undefined; - errors.push({ - name: i.name, - error: `The entered number is less than ${option.min_value}. The minimum allowed is ${option.min_value}`, - fullError: ['NUMBER_MIN_VALUE', option.min_value], - }); - break; - } - } - if (option.max_value) { - if (value > option.max_value) { - value = undefined; - errors.push({ - name: i.name, - error: `The entered number is greater than ${option.max_value}. The maximum allowed is ${option.max_value}`, - fullError: ['NUMBER_MAX_VALUE', option.max_value], - }); - break; - } - } - break; - } - const choice = option.choices.find(x => x.name === args[i.name]); - if (!choice) { - value = undefined; - errors.push({ - name: i.name, - error: `The entered choice is invalid. Please choose one of the following options: ${option.choices - .map(x => x.name) - .join(', ')}.`, - fullError: ['NUMBER_INVALID_CHOICE', option.choices], - }); - break; - } - value = choice.value; - } - break; - default: - break; - } - if (value !== undefined) { - options.push({ - name: i.name, - type: i.type, - value, - } as APIApplicationCommandInteractionDataOption); - } else if (i.required) - if (!errors.some(x => x.name === i.name)) - errors.push({ - error: 'Option is required but returned undefined', - name: i.name, - fullError: ['OPTION_REQUIRED'], - }); - } catch (e) { - errors.push({ - error: e && typeof e === 'object' && 'message' in e ? (e.message as string) : `${e}`, - name: i.name, - fullError: ['UNKNOWN', e], - }); - } - } - - return { errors, options }; -} - -export function defaultArgsParser(content: string) { - const args: Record = {}; - for (const i of content.match(/-(.*?)(?=\s-|$)/gs) ?? []) { - args[i.slice(1).split(' ')[0]] = i.split(' ').slice(1).join(' '); - } - return args; -} - -//-(.*?)(?=\s-|$)/gs -//-(?[^-]*)/gm diff --git a/src/client/transformers.ts b/src/client/transformers.ts new file mode 100644 index 0000000..eebc38a --- /dev/null +++ b/src/client/transformers.ts @@ -0,0 +1,193 @@ +import type { ChannelType } from 'discord-api-types/v10'; +import { type CustomStructures, OptionResolver } from '../commands'; +import { + AnonymousGuild, + AutoModerationRule, + BaseChannel, + BaseGuildChannel, + CategoryChannel, + ClientUser, + DMChannel, + DirectoryChannel, + ForumChannel, + Guild, + GuildEmoji, + GuildMember, + GuildRole, + GuildTemplate, + InteractionGuildMember, + MediaChannel, + Message, + NewsChannel, + Poll, + StageChannel, + Sticker, + TextGuildChannel, + ThreadChannel, + User, + VoiceChannel, + VoiceState, + Webhook, + WebhookMessage, +} from '../structures'; +import type { StructStates } from '../common/types/util'; +import { GuildBan } from '../structures/GuildBan'; + +export type PollStructure = InferCustomStructure; +export type ClientUserStructure = InferCustomStructure; +export type AnonymousGuildStructure = InferCustomStructure; +export type AutoModerationRuleStructure = InferCustomStructure; +export type BaseChannelStructure = InferCustomStructure, 'BaseChannel'>; +export type BaseGuildChannelStructure = InferCustomStructure; +export type TextGuildChannelStructure = InferCustomStructure; +export type DMChannelStructure = InferCustomStructure; +export type VoiceChannelStructure = InferCustomStructure; +export type StageChannelStructure = InferCustomStructure; +export type MediaChannelStructure = InferCustomStructure; +export type ForumChannelStructure = InferCustomStructure; +export type ThreadChannelStructure = InferCustomStructure; +export type CategoryChannelStructure = InferCustomStructure; +export type NewsChannelStructure = InferCustomStructure; +export type DirectoryChannelStructure = InferCustomStructure; +export type GuildStructure = InferCustomStructure, 'Guild'>; +export type GuildBanStructure = InferCustomStructure; +export type GuildEmojiStructure = InferCustomStructure; +export type GuildMemberStructure = InferCustomStructure; +export type InteractionGuildMemberStructure = InferCustomStructure; +export type GuildRoleStructure = InferCustomStructure; +export type GuildTemplateStructure = InferCustomStructure; +export type MessageStructure = InferCustomStructure; +export type WebhookMessageStructure = InferCustomStructure; +export type StickerStructure = InferCustomStructure; +export type UserStructure = InferCustomStructure; +export type VoiceStateStructure = InferCustomStructure; +export type WebhookStructure = InferCustomStructure; +export type OptionResolverStructure = InferCustomStructure; + +export class Transformers { + static AnonymousGuild(...args: ConstructorParameters): AnonymousGuildStructure { + return new AnonymousGuild(...args); + } + + static AutoModerationRule(...args: ConstructorParameters): AutoModerationRuleStructure { + return new AutoModerationRule(...args); + } + + static BaseChannel(...args: ConstructorParameters): BaseChannelStructure { + return new BaseChannel(...args); + } + + static BaseGuildChannel(...args: ConstructorParameters): BaseGuildChannelStructure { + return new BaseGuildChannel(...args); + } + + static TextGuildChannel(...args: ConstructorParameters): TextGuildChannelStructure { + return new TextGuildChannel(...args); + } + + static DMChannel(...args: ConstructorParameters): DMChannelStructure { + return new DMChannel(...args); + } + + static VoiceChannel(...args: ConstructorParameters): VoiceChannelStructure { + return new VoiceChannel(...args); + } + + static StageChannel(...args: ConstructorParameters): StageChannelStructure { + return new StageChannel(...args); + } + + static MediaChannel(...args: ConstructorParameters): MediaChannelStructure { + return new MediaChannel(...args); + } + + static ForumChannel(...args: ConstructorParameters): ForumChannelStructure { + return new ForumChannel(...args); + } + + static ThreadChannel(...args: ConstructorParameters): ThreadChannelStructure { + return new ThreadChannel(...args); + } + + static CategoryChannel(...args: ConstructorParameters): CategoryChannelStructure { + return new CategoryChannel(...args); + } + + static NewsChannel(...args: ConstructorParameters): NewsChannelStructure { + return new NewsChannel(...args); + } + + static DirectoryChannel(...args: ConstructorParameters): DirectoryChannelStructure { + return new DirectoryChannel(...args); + } + + static ClientUser(...args: ConstructorParameters): ClientUserStructure { + return new ClientUser(...args); + } + + static Guild( + ...args: ConstructorParameters + ): GuildStructure { + return new Guild(...args); + } + + static GuildBan(...args: ConstructorParameters): GuildBanStructure { + return new GuildBan(...args); + } + + static GuildEmoji(...args: ConstructorParameters): GuildEmojiStructure { + return new GuildEmoji(...args); + } + + static GuildMember(...args: ConstructorParameters): GuildMemberStructure { + return new GuildMember(...args); + } + + static InteractionGuildMember( + ...args: ConstructorParameters + ): InteractionGuildMemberStructure { + return new InteractionGuildMember(...args); + } + + static GuildRole(...args: ConstructorParameters): GuildRoleStructure { + return new GuildRole(...args); + } + + static GuildTemplate(...args: ConstructorParameters): GuildTemplateStructure { + return new GuildTemplate(...args); + } + + static Message(...args: ConstructorParameters): MessageStructure { + return new Message(...args); + } + + static WebhookMessage(...args: ConstructorParameters): WebhookMessageStructure { + return new WebhookMessage(...args); + } + + static Poll(...args: ConstructorParameters): PollStructure { + return new Poll(...args); + } + + static Sticker(...args: ConstructorParameters): StickerStructure { + return new Sticker(...args); + } + + static User(...args: ConstructorParameters): UserStructure { + return new User(...args); + } + + static VoiceState(...args: ConstructorParameters): VoiceStateStructure { + return new VoiceState(...args); + } + + static Webhook(...args: ConstructorParameters): WebhookStructure { + return new Webhook(...args); + } + + static OptionResolver(...args: ConstructorParameters): OptionResolverStructure { + return new OptionResolver(...args); + } +} + +export type InferCustomStructure = CustomStructures extends Record ? P : T; diff --git a/src/client/workerclient.ts b/src/client/workerclient.ts index 3038d21..62bf94c 100644 --- a/src/client/workerclient.ts +++ b/src/client/workerclient.ts @@ -3,9 +3,8 @@ import { randomUUID } from 'node:crypto'; import { ApiHandler, Logger } from '..'; import type { Cache } from '../cache'; import { WorkerAdapter } from '../cache'; -import { LogLevels, MergeOptions, lazyLoadPackage, type DeepPartial, type When } from '../common'; +import { LogLevels, lazyLoadPackage, type DeepPartial, type When } from '../common'; import { EventHandler } from '../events'; -import { ClientUser } from '../structures'; import { Shard, properties, type ShardManagerOptions, type WorkerData } from '../websocket'; import type { WorkerReady, @@ -23,9 +22,9 @@ import type { ManagerMessages } from '../websocket/discord/workermanager'; import type { BaseClientOptions, ServicesOptions, StartOptions } from './base'; import { BaseClient } from './base'; import type { Client, ClientOptions } from './client'; -import { onInteractionCreate } from './oninteractioncreate'; -import { defaultArgsParser, defaultOptionsParser, onMessageCreate } from './onmessagecreate'; + import { Collectors } from './collectors'; +import { type ClientUserStructure, Transformers } from './transformers'; let workerData: WorkerData; let manager: import('node:worker_threads').MessagePort; @@ -49,7 +48,7 @@ export class WorkerClient extends BaseClient { collectors = new Collectors(); events? = new EventHandler(this); - me!: When; + me!: When; promises = new Map void; timeout: NodeJS.Timeout }>(); shards = new Map(); @@ -58,15 +57,7 @@ export class WorkerClient extends BaseClient { constructor(options?: WorkerClientOptions) { super(options); - this.options = MergeOptions( - { - commands: { - argsParser: defaultArgsParser, - optionsParser: defaultOptionsParser, - }, - } satisfies Partial, - this.options, - ); + if (!process.env.SEYFERT_SPAWNING) { throw new Error('WorkerClient cannot spawn without manager'); } @@ -129,9 +120,6 @@ export class WorkerClient extends BaseClient { this.events = undefined; } else if (typeof rest.handlers.events === 'function') { this.events = new EventHandler(this); - this.events.setHandlers({ - callback: rest.handlers.events, - }); } else { this.events = rest.handlers.events; } @@ -336,7 +324,10 @@ export class WorkerClient extends BaseClient { } protected async onPacket(packet: GatewayDispatchPayload, shardId: number) { - await this.events?.execute('RAW', packet, this as WorkerClient, shardId); + Promise.allSettled([ + this.events?.runEvent('RAW', this, packet, shardId, false), + this.collectors.run('RAW', packet), + ]); //ignore promise switch (packet.t) { case 'GUILD_CREATE': { if (this.__handleGuilds?.has(packet.d.id)) { @@ -355,19 +346,19 @@ export class WorkerClient extends BaseClient { break; } default: { - await this.events?.execute(packet.t, packet, this, shardId); + await this.events?.execute(packet.t as never, packet, this, shardId); switch (packet.t) { case 'READY': + if (!this.__handleGuilds) this.__handleGuilds = new Set(); for (const g of packet.d.guilds) { - this.__handleGuilds?.add(g.id); + this.__handleGuilds.add(g.id); } this.botId = packet.d.user.id; this.applicationId = packet.d.application.id; - this.me = new ClientUser(this, packet.d.user, packet.d.application) as never; + this.me = Transformers.ClientUser(this, packet.d.user, packet.d.application) as never; if ( !( - this.__handleGuilds?.size && - (workerData.intents & GatewayIntentBits.Guilds) === GatewayIntentBits.Guilds + this.__handleGuilds.size && (workerData.intents & GatewayIntentBits.Guilds) === GatewayIntentBits.Guilds ) ) { if ([...this.shards.values()].every(shard => shard.data.session_id)) { @@ -382,10 +373,10 @@ export class WorkerClient extends BaseClient { this.debugger?.debug(`#${shardId} [${packet.d.user.username}](${this.botId}) is online...`); break; case 'INTERACTION_CREATE': - await onInteractionCreate(this, packet.d, shardId); + await this.handleCommand.interaction(packet.d, shardId); break; case 'MESSAGE_CREATE': - await onMessageCreate(this, packet.d, shardId); + await this.handleCommand.message(packet.d, shardId); break; } break; diff --git a/src/commands/applications/chat.ts b/src/commands/applications/chat.ts index 2cc6967..57a476a 100644 --- a/src/commands/applications/chat.ts +++ b/src/commands/applications/chat.ts @@ -17,10 +17,9 @@ import type { SeyfertStringOption, } from '../..'; import type { Attachment } from '../../builders'; -import { magicImport, type FlatObjectKeys } from '../../common'; -import type { AllChannels, AutocompleteInteraction, GuildRole, InteractionGuildMember, User } from '../../structures'; +import { type Awaitable, magicImport, type FlatObjectKeys } from '../../common'; +import type { AllChannels, AutocompleteInteraction } from '../../structures'; import type { Groups, RegisteredMiddlewares } from '../decorators'; -import type { OptionResolver } from '../optionresolver'; import type { CommandContext } from './chatcontext'; import type { DefaultLocale, @@ -33,6 +32,12 @@ import type { UsingClient, } from './shared'; import { inspect } from 'node:util'; +import type { + GuildRoleStructure, + InteractionGuildMemberStructure, + OptionResolverStructure, + UserStructure, +} from '../../client/transformers'; export interface ReturnOptionsTypes { 1: never; // subcommand @@ -40,10 +45,10 @@ export interface ReturnOptionsTypes { 3: string; 4: number; // integer 5: boolean; - 6: InteractionGuildMember | User; + 6: InteractionGuildMemberStructure | UserStructure; 7: AllChannels; - 8: GuildRole; - 9: GuildRole | AllChannels | User; + 8: GuildRoleStructure; + 9: GuildRoleStructure | AllChannels | UserStructure; 10: number; // number 11: Attachment; } @@ -58,7 +63,7 @@ type Wrap = N extends data: { context: CommandContext; value: ReturnOptionsTypes[N] }, ok: OKFunction, fail: StopFunction, - ): void; + ): Awaitable; } & { description: string; description_localizations?: APIApplicationCommandBasicOption['description_localizations']; @@ -144,7 +149,7 @@ export class BaseCommand { /** @internal */ async __runOptions( ctx: CommandContext<{}, never>, - resolver: OptionResolver, + resolver: OptionResolverStructure, ): Promise<[boolean, OnOptionsReturnObject]> { if (!this?.options?.length) { return [false, {}]; diff --git a/src/commands/applications/chatcontext.ts b/src/commands/applications/chatcontext.ts index 9823d49..afdc913 100644 --- a/src/commands/applications/chatcontext.ts +++ b/src/commands/applications/chatcontext.ts @@ -1,19 +1,21 @@ import { MessageFlags } from 'discord-api-types/v10'; -import type { AllChannels, Guild, InferWithPrefix, ReturnCache, WebhookMessage } from '../..'; +import type { AllChannels, InferWithPrefix, Message, ReturnCache } from '../..'; import type { Client, WorkerClient } from '../../client'; import type { If, UnionToTuple, When } from '../../common'; import type { InteractionCreateBodyRequest, InteractionMessageUpdateBodyRequest } from '../../common/types/write'; -import { - Message, - type ChatInputCommandInteraction, - type GuildMember, - type InteractionGuildMember, -} from '../../structures'; +import { ChatInputCommandInteraction } from '../../structures'; import { BaseContext } from '../basecontext'; import type { RegisteredMiddlewares } from '../decorators'; -import type { OptionResolver } from '../optionresolver'; import type { Command, ContextOptions, OptionsRecord, SubCommand } from './chat'; import type { CommandMetadata, ExtendContext, GlobalMetadata, UsingClient } from './shared'; +import type { + GuildMemberStructure, + GuildStructure, + InteractionGuildMemberStructure, + MessageStructure, + OptionResolverStructure, + WebhookMessageStructure, +} from '../../client/transformers'; export interface CommandContext extends BaseContext, @@ -23,22 +25,22 @@ export class CommandContext< T extends OptionsRecord = {}, M extends keyof RegisteredMiddlewares = never, > extends BaseContext { - message!: If; + message!: If; interaction!: If; - messageResponse?: If; + messageResponse?: If; constructor( readonly client: UsingClient, - data: ChatInputCommandInteraction | Message, - readonly resolver: OptionResolver, + data: ChatInputCommandInteraction | MessageStructure, + readonly resolver: OptionResolverStructure, readonly shardId: number, readonly command: Command | SubCommand, ) { super(client); - if (data instanceof Message) { - this.message = data as never; - } else { + if (data instanceof ChatInputCommandInteraction) { this.interaction = data; + } else { + this.message = data as never; } } @@ -61,7 +63,7 @@ export class CommandContext< async write( body: InteractionCreateBodyRequest, fetchReply?: FR, - ): Promise> { + ): Promise> { if (this.interaction) return this.interaction.write(body, fetchReply); const options = (this.client as Client | WorkerClient).options?.commands; return (this.messageResponse = await (this.message! as Message)[ @@ -90,7 +92,7 @@ export class CommandContext< editOrReply( body: InteractionCreateBodyRequest | InteractionMessageUpdateBodyRequest, fetchReply?: FR, - ): Promise> { + ): Promise> { if (this.interaction) return this.interaction.editOrReply(body as InteractionCreateBodyRequest, fetchReply); if (this.messageResponse) { return this.editResponse(body); @@ -99,7 +101,7 @@ export class CommandContext< } async fetchResponse(): Promise< - If + If > { if (this.interaction) return this.interaction.fetchResponse(); this.messageResponse = await this.messageResponse?.fetch(); @@ -122,8 +124,8 @@ export class CommandContext< } } - me(mode?: 'rest' | 'flow'): Promise; - me(mode?: 'cache'): ReturnCache; + me(mode?: 'rest' | 'flow'): Promise; + me(mode?: 'cache'): ReturnCache; me(mode: 'cache' | 'rest' | 'flow' = 'cache') { if (!this.guildId) return mode === 'cache' ? (this.client.cache.adapter.isAsync ? Promise.resolve() : undefined) : Promise.resolve(); @@ -138,8 +140,8 @@ export class CommandContext< } } - guild(mode?: 'rest' | 'flow'): Promise | undefined>; - guild(mode?: 'cache'): ReturnCache | undefined>; + guild(mode?: 'rest' | 'flow'): Promise | undefined>; + guild(mode?: 'cache'): ReturnCache | undefined>; guild(mode: 'cache' | 'rest' | 'flow' = 'cache') { if (!this.guildId) return (mode === 'cache' @@ -159,23 +161,23 @@ export class CommandContext< } get guildId() { - return this.interaction?.guildId || (this.message! as Message | undefined)?.guildId; + return this.interaction?.guildId || (this.message! as MessageStructure | undefined)?.guildId; } get channelId() { - return this.interaction?.channelId || (this.message! as Message).channelId; + return this.interaction?.channelId || (this.message! as MessageStructure).channelId; } get author() { - return this.interaction?.user || (this.message! as Message).author; + return this.interaction?.user || (this.message! as MessageStructure).author; } get member(): If< InferWithPrefix, - GuildMember | InteractionGuildMember | undefined, - InteractionGuildMember | undefined + GuildMemberStructure | InteractionGuildMemberStructure | undefined, + InteractionGuildMemberStructure | undefined > { - return this.interaction?.member || ((this.message! as Message)?.member as any); + return this.interaction?.member || ((this.message! as MessageStructure)?.member as any); } isChat(): this is CommandContext { diff --git a/src/commands/applications/menu.ts b/src/commands/applications/menu.ts index 2589cf7..5b34b6c 100644 --- a/src/commands/applications/menu.ts +++ b/src/commands/applications/menu.ts @@ -64,10 +64,10 @@ export abstract class ContextMenuCommand { onBotPermissionsFail(context: MenuCommandContext, permissions: PermissionStrings): any { context.client.logger.fatal(`${this.name}.`, context.author.id, permissions); } - onPermissionsFail(context: MenuCommandContext, permissions: PermissionStrings): any { - context.client.logger.fatal(`${this.name}.`, context.author.id, permissions); - } - onInternalError(client: UsingClient, error?: unknown): any { - client.logger.fatal(error); + // onPermissionsFail(context: MenuCommandContext, permissions: PermissionStrings): any { + // context.client.logger.fatal(`${this.name}.`, context.author.id, permissions); + // } + onInternalError(client: UsingClient, command: ContextMenuCommand, error?: unknown): any { + client.logger.fatal(command.name, error); } } diff --git a/src/commands/applications/menucontext.ts b/src/commands/applications/menucontext.ts index 2fc54e1..ec635c8 100644 --- a/src/commands/applications/menucontext.ts +++ b/src/commands/applications/menucontext.ts @@ -1,5 +1,5 @@ import { type APIMessage, ApplicationCommandType, MessageFlags } from 'discord-api-types/v10'; -import type { ContextMenuCommand, ReturnCache, WebhookMessage } from '../..'; +import type { ContextMenuCommand, ReturnCache } from '../..'; import { toSnakeCase, type InteractionCreateBodyRequest, @@ -8,20 +8,20 @@ import { type UnionToTuple, type When, } from '../../common'; -import { - Message, - User, - type AllChannels, - type Guild, - type GuildMember, - type MessageCommandInteraction, - type UserCommandInteraction, -} from '../../structures'; +import type { AllChannels, MessageCommandInteraction, UserCommandInteraction } from '../../structures'; import { BaseContext } from '../basecontext'; import type { RegisteredMiddlewares } from '../decorators'; import type { CommandMetadata, ExtendContext, GlobalMetadata, UsingClient } from './shared'; +import { + type GuildMemberStructure, + type GuildStructure, + type MessageStructure, + Transformers, + type UserStructure, + type WebhookMessageStructure, +} from '../../client/transformers'; -export type InteractionTarget = T extends MessageCommandInteraction ? Message : User; +export type InteractionTarget = T extends MessageCommandInteraction ? MessageStructure : UserStructure; export interface MenuCommandContext< T extends MessageCommandInteraction | UserCommandInteraction, @@ -50,11 +50,11 @@ export class MenuCommandContext< switch (this.interaction.data.type) { case ApplicationCommandType.Message: { const data = this.interaction.data.resolved.messages[this.interaction.data.targetId as Lowercase]; - return new Message(this.client, toSnakeCase(data) as APIMessage) as never; + return Transformers.Message(this.client, toSnakeCase(data) as APIMessage) as never; } case ApplicationCommandType.User: { const data = this.interaction.data.resolved.users[this.interaction.data.targetId as Lowercase]; - return new User(this.client, toSnakeCase(data)) as never; + return Transformers.User(this.client, toSnakeCase(data)) as never; } } } @@ -70,7 +70,7 @@ export class MenuCommandContext< write( body: InteractionCreateBodyRequest, fetchReply?: FR, - ): Promise> { + ): Promise> { return this.interaction.write(body, fetchReply); } @@ -93,7 +93,7 @@ export class MenuCommandContext< editOrReply( body: InteractionCreateBodyRequest | InteractionMessageUpdateBodyRequest, fetchReply?: FR, - ): Promise> { + ): Promise> { return this.interaction.editOrReply(body as InteractionCreateBodyRequest, fetchReply); } @@ -109,8 +109,8 @@ export class MenuCommandContext< return this.client.channels.fetch(this.channelId, mode === 'rest'); } - me(mode?: 'rest' | 'flow'): Promise; - me(mode?: 'cache'): ReturnCache; + me(mode?: 'rest' | 'flow'): Promise; + me(mode?: 'cache'): ReturnCache; me(mode: 'cache' | 'rest' | 'flow' = 'cache') { if (!this.guildId) return mode === 'cache' ? (this.client.cache.adapter.isAsync ? Promise.resolve() : undefined) : Promise.resolve(); @@ -122,8 +122,8 @@ export class MenuCommandContext< } } - guild(mode?: 'rest' | 'flow'): Promise | undefined>; - guild(mode?: 'cache'): ReturnCache | undefined>; + guild(mode?: 'rest' | 'flow'): Promise | undefined>; + guild(mode?: 'cache'): ReturnCache | undefined>; guild(mode: 'cache' | 'rest' | 'flow' = 'cache') { if (!this.guildId) return ( @@ -158,10 +158,10 @@ export class MenuCommandContext< } isMenuUser(): this is MenuCommandContext { - return this.target instanceof User; + return this.interaction.data.type === ApplicationCommandType.User; } isMenuMessage(): this is MenuCommandContext { - return this.target instanceof Message; + return this.interaction.data.type === ApplicationCommandType.Message; } } diff --git a/src/commands/applications/shared.ts b/src/commands/applications/shared.ts index 78d281d..0c668f3 100644 --- a/src/commands/applications/shared.ts +++ b/src/commands/applications/shared.ts @@ -17,6 +17,7 @@ export interface ExtraProps {} export interface UsingClient extends BaseClient {} export type ParseClient = T; export interface InternalOptions {} +export interface CustomStructures {} export type MiddlewareContext = (context: { context: C; diff --git a/src/commands/handle.ts b/src/commands/handle.ts new file mode 100644 index 0000000..995ff18 --- /dev/null +++ b/src/commands/handle.ts @@ -0,0 +1,806 @@ +import { + type APIApplicationCommandInteraction, + ApplicationCommandType, + InteractionType, + type APIInteraction, + type GatewayMessageCreateDispatchData, + InteractionContextType, + type APIApplicationCommandInteractionDataOption, + ApplicationCommandOptionType, + ChannelType, + type APIInteractionDataResolvedChannel, +} from 'discord-api-types/v10'; +import { + Command, + type ContextOptionsResolved, + type UsingClient, + type CommandAutocompleteOption, + type ContextMenuCommand, + MenuCommandContext, + BaseCommand, + CommandContext, + type RegisteredMiddlewares, + SubCommand, + IgnoreCommand, + type CommandOption, + type MessageCommandOptionErrors, + type SeyfertChannelOption, + type SeyfertIntegerOption, + type SeyfertNumberOption, + type SeyfertStringOption, +} from '.'; +import { + AutocompleteInteraction, + BaseInteraction, + type ComponentInteraction, + type ModalSubmitInteraction, + type ChatInputCommandInteraction, + type MessageCommandInteraction, + type UserCommandInteraction, + type __InternalReplyFunction, +} from '../structures'; +import type { PermissionsBitField } from '../structures/extra/Permissions'; +import { ComponentContext, ModalContext } from '../components'; +import type { Client, WorkerClient } from '../client'; +import type { MakeRequired } from '../common'; +import { type MessageStructure, Transformers, type OptionResolverStructure } from '../client/transformers'; + +export type CommandOptionWithType = CommandOption & { + type: ApplicationCommandOptionType; +}; + +export interface CommandFromContent { + command?: Command | SubCommand; + parent?: Command; + fullCommandName: string; +} + +export class HandleCommand { + constructor(public client: UsingClient) {} + + async autocomplete( + interaction: AutocompleteInteraction, + optionsResolver: OptionResolverStructure, + command?: CommandAutocompleteOption, + ) { + // idc, is a YOU problem + if (!command?.autocomplete) { + return this.client.logger.warn( + `${optionsResolver.fullCommandName} ${command?.name} command does not have 'autocomplete' callback`, + ); + } + + try { + try { + try { + await command.autocomplete(interaction); + } catch (error) { + if (!command.onAutocompleteError) + return this.client.logger.error( + `${optionsResolver.fullCommandName} ${command.name} just threw an error, ${ + error ? (typeof error === 'object' && 'message' in error ? error.message : error) : 'Unknown' + }`, + ); + await command.onAutocompleteError(interaction, error); + } + } catch (error) { + await optionsResolver.getCommand()?.onInternalError?.(this.client, optionsResolver.getCommand()!, error); + } + } catch (error) { + // pass + } + } + + async contextMenuMessage( + command: ContextMenuCommand, + interaction: MessageCommandInteraction, + context: MenuCommandContext, + ) { + // @ts-expect-error + return this.contextMenuUser(command, interaction, context); + } + + async contextMenuUser( + command: ContextMenuCommand, + interaction: UserCommandInteraction, + context: MenuCommandContext, + ) { + if (command.botPermissions && interaction.appPermissions) { + const permissions = this.checkPermissions(interaction.appPermissions, command.botPermissions); + if (permissions) return command.onBotPermissionsFail(context, permissions); + } + + const resultGlobal = await this.runGlobalMiddlewares(command, context); + if (typeof resultGlobal === 'boolean') return; + const resultMiddle = await this.runMiddlewares(command, context); + if (typeof resultMiddle === 'boolean') return; + + try { + try { + await command.run!(context); + await command.onAfterRun?.(context, undefined); + } catch (error) { + await command.onRunError(context, error); + await command.onAfterRun?.(context, error); + } + } catch (error) { + try { + await command.onInternalError(this.client, command, error); + } catch { + // pass + } + } + } + + async chatInput( + command: Command | SubCommand, + interaction: ChatInputCommandInteraction, + resolver: OptionResolverStructure, + context: CommandContext, + ) { + if (command.botPermissions && interaction.appPermissions) { + const permissions = this.checkPermissions(interaction.appPermissions, command.botPermissions); + if (permissions) return command.onBotPermissionsFail?.(context, permissions); + } + if (!(await this.runOptions(command, context, resolver))) return; + + const resultGlobal = await this.runGlobalMiddlewares(command, context); + if (typeof resultGlobal === 'boolean') return; + const resultMiddle = await this.runMiddlewares(command, context); + if (typeof resultMiddle === 'boolean') return; + + try { + try { + await command.run!(context); + await command.onAfterRun?.(context, undefined); + } catch (error) { + await command.onRunError?.(context, error); + await command.onAfterRun?.(context, error); + } + } catch (error) { + try { + await command.onInternalError?.(this.client, command, error); + } catch { + // pass + } + } + } + + async modal(interaction: ModalSubmitInteraction) { + const context = new ModalContext(this.client, interaction); + const extended = this.client.options?.context?.(interaction) ?? {}; + Object.assign(context, extended); + await this.client.components?.executeModal(context); + } + + async messageComponent(interaction: ComponentInteraction) { + //@ts-expect-error + const context = new ComponentContext(this.client, interaction); + const extended = this.client.options?.context?.(interaction) ?? {}; + Object.assign(context, extended); + await this.client.components?.executeComponent(context); + } + + async interaction(body: APIInteraction, shardId: number, __reply?: __InternalReplyFunction) { + this.client.debugger?.debug(`[${InteractionType[body.type] ?? body.type}] Interaction received.`); + switch (body.type) { + case InteractionType.ApplicationCommandAutocomplete: + { + const optionsResolver = this.makeResolver( + this.client, + body.data.options ?? [], + this.getCommand(body.data), + body.guild_id, + body.data.resolved as ContextOptionsResolved, + ); + const interaction = new AutocompleteInteraction(this.client, body, optionsResolver, __reply); + const command = optionsResolver.getAutocomplete(); + await this.autocomplete(interaction, optionsResolver, command); + } + break; + case InteractionType.ApplicationCommand: { + switch (body.data.type) { + case ApplicationCommandType.Message: { + const data = this.makeMenuCommand(body, shardId, __reply); + if (!data) return; + // @ts-expect-error + this.contextMenuMessage(data.command, data.interaction, data.context); + break; + } + case ApplicationCommandType.User: { + const data = this.makeMenuCommand(body, shardId, __reply); + if (!data) return; + // @ts-expect-error + this.contextMenuUser(data.command, data.interaction, data.context); + break; + } + case ApplicationCommandType.ChatInput: { + const parentCommand = this.getCommand(body.data); + const optionsResolver = this.makeResolver( + this.client, + body.data.options ?? [], + parentCommand, + body.guild_id, + body.data.resolved as ContextOptionsResolved, + ); + const interaction = BaseInteraction.from(this.client, body, __reply) as ChatInputCommandInteraction; + const command = optionsResolver.getCommand(); + if (!command?.run) + return this.client.logger.warn(`${optionsResolver.fullCommandName} command does not have 'run' callback`); + const context = new CommandContext(this.client, interaction, optionsResolver, shardId, command); + const extendContext = this.client.options?.context?.(interaction) ?? {}; + Object.assign(context, extendContext); + await this.chatInput(command, interaction, optionsResolver, context); + break; + } + } + break; + } + case InteractionType.ModalSubmit: + { + const interaction = BaseInteraction.from(this.client, body, __reply) as ModalSubmitInteraction; + if (this.client.components?.hasModal(interaction)) { + await this.client.components.onModalSubmit(interaction); + } else await this.modal(interaction); + } + break; + case InteractionType.MessageComponent: + { + const interaction = BaseInteraction.from(this.client, body, __reply) as ComponentInteraction; + if (this.client.components?.hasComponent(body.message.id, interaction.customId)) { + await this.client.components.onComponent(body.message.id, interaction); + } else await this.messageComponent(interaction); + } + break; + } + } + + async message(rawMessage: GatewayMessageCreateDispatchData, shardId: number) { + const self = this.client as Client | WorkerClient; + if (!self.options.commands?.prefix) return; + const message = Transformers.Message(this.client, rawMessage); + const prefixes = (await self.options.commands.prefix(message)).sort((a, b) => b.length - a.length); + const prefix = prefixes.find(x => rawMessage.content.startsWith(x)); + + if (!(prefix !== undefined && rawMessage.content.startsWith(prefix))) return; + + const content = rawMessage.content.slice(prefix.length).trimStart(); + + const { fullCommandName, command, parent, argsContent } = this.resolveCommandFromContent( + content, + prefix, + rawMessage, + ); + + if (!command || argsContent === undefined) return; + if (!command.run) return self.logger.warn(`${fullCommandName} command does not have 'run' callback`); + + if (!(command.contexts.includes(InteractionContextType.BotDM) || rawMessage.guild_id)) return; + if (!command.contexts.includes(InteractionContextType.Guild) && rawMessage.guild_id) return; + if (command.guildId && !command.guildId?.includes(rawMessage.guild_id!)) return; + + const resolved: MakeRequired = { + channels: {}, + roles: {}, + users: {}, + members: {}, + attachments: {}, + }; + + const args = this.argsParser(argsContent, command, message); + const { options, errors } = await this.argsOptionsParser(command, rawMessage, args, resolved); + const optionsResolver = this.makeResolver(self, options, parent as Command, rawMessage.guild_id, resolved); + const context = new CommandContext(self, message, optionsResolver, shardId, command); + //@ts-expect-error + const extendContext = self.options?.context?.(message) ?? {}; + Object.assign(context, extendContext); + + try { + if (errors.length) { + return command.onOptionsError?.( + context, + Object.fromEntries( + errors.map(x => { + return [ + x.name, + { + failed: true, + value: x.error, + parseError: x.fullError, + }, + ]; + }), + ), + ); + } + if (command.defaultMemberPermissions && rawMessage.guild_id) { + const memberPermissions = await self.members.permissions(rawMessage.guild_id, rawMessage.author.id); + const permissions = this.checkPermissions(memberPermissions, command.defaultMemberPermissions); + const guild = await this.client.guilds.raw(rawMessage.guild_id); + if (permissions && guild.owner_id !== rawMessage.author.id) { + return command.onPermissionsFail?.(context, memberPermissions.keys(permissions)); + } + } + + if (command.botPermissions && rawMessage.guild_id) { + const meMember = await self.cache.members?.get(self.botId, rawMessage.guild_id); + if (!meMember) return; //enable member cache and "Guilds" intent, lol + const appPermissions = await meMember.fetchPermissions(); + const permissions = this.checkPermissions(appPermissions, command.botPermissions); + if (!appPermissions.has('Administrator') && permissions) { + return command.onBotPermissionsFail?.(context, permissions); + } + } + + if (!(await this.runOptions(command, context, optionsResolver))) return; + + const resultGlobal = await this.runGlobalMiddlewares(command, context); + if (typeof resultGlobal === 'boolean') return; + const resultMiddle = await this.runMiddlewares(command, context); + if (typeof resultMiddle === 'boolean') return; + try { + await command.run!(context); + await command.onAfterRun?.(context, undefined); + } catch (error) { + await command.onRunError?.(context, error); + await command.onAfterRun?.(context, error); + } + } catch (error) { + try { + await command.onInternalError?.(this.client, command, error); + } catch {} + } + } + + argsParser(content: string, _command: SubCommand | Command, _message: MessageStructure): Record { + const args: Record = {}; + for (const i of content.match(/-(.*?)(?=\s-|$)/gs) ?? []) { + args[i.slice(1).split(' ')[0]] = i.split(' ').slice(1).join(' '); + } + return args; + } + + resolveCommandFromContent( + content: string, + _prefix: string, + _message: GatewayMessageCreateDispatchData, + ): CommandFromContent & { argsContent?: string } { + const result = this.getCommandFromContent( + content + .split(' ') + .filter(x => x) + .slice(0, 3), + ); + + if (!result.command) return result; + + let newContent = content; + for (const i of result.fullCommandName.split(' ')) { + newContent = newContent.slice(newContent.indexOf(i) + i.length); + } + + return { + ...result, + argsContent: newContent.slice(1), + }; + } + + getCommandFromContent(commandRaw: string[]): CommandFromContent { + const rawParentName = commandRaw[0]; + const rawGroupName = commandRaw.length === 3 ? commandRaw[1] : undefined; + const rawSubcommandName = rawGroupName ? commandRaw[2] : commandRaw[1]; + const parent = this.getParentMessageCommand(rawParentName); + const fullCommandName = `${rawParentName}${ + rawGroupName ? ` ${rawGroupName} ${rawSubcommandName}` : `${rawSubcommandName ? ` ${rawSubcommandName}` : ''}` + }`; + + if (!(parent instanceof Command)) return { fullCommandName }; + + if (rawGroupName && !parent.groups?.[rawGroupName] && !parent.groupsAliases?.[rawGroupName]) + return this.getCommandFromContent([rawParentName, rawGroupName]); + if ( + rawSubcommandName && + !parent.options?.some( + x => x instanceof SubCommand && (x.name === rawSubcommandName || x.aliases?.includes(rawSubcommandName)), + ) + ) + return this.getCommandFromContent([rawParentName]); + + const groupName = rawGroupName ? parent.groupsAliases?.[rawGroupName] || rawGroupName : undefined; + + const command = + groupName || rawSubcommandName + ? (parent.options?.find(opt => { + if (opt instanceof SubCommand) { + if (groupName) { + if (opt.group !== groupName) return false; + } + if (opt.group && !groupName) return false; + return rawSubcommandName === opt.name || opt.aliases?.includes(rawSubcommandName); + } + return false; + }) as SubCommand) + : parent; + + return { + command, + fullCommandName, + parent, + }; + } + + makeResolver(...args: Parameters<(typeof Transformers)['OptionResolver']>) { + return Transformers.OptionResolver(...args); + } + + getParentMessageCommand(rawParentName: string) { + return this.client.commands!.values.find( + x => + (!('ignore' in x) || x.ignore !== IgnoreCommand.Message) && + (x.name === rawParentName || ('aliases' in x ? x.aliases?.includes(rawParentName) : false)), + ); + } + + getCommand(data: { + guild_id?: string; + name: string; + }): T | undefined { + return this.client.commands!.values.find(command => { + if (data.guild_id) { + return command.guildId?.includes(data.guild_id) && command.name === data.name; + } + return command.name === data.name; + }) as T; + } + + checkPermissions(app: PermissionsBitField, bot: bigint) { + const permissions = app.missings(...app.values([bot])); + if (!app.has('Administrator') && permissions.length) { + return app.keys(permissions); + } + return false; + } + + async fetchChannel(_option: CommandOptionWithType, id: string) { + return this.client.channels.raw(id); + } + + async fetchUser(_option: CommandOptionWithType, id: string) { + return this.client.users.raw(id); + } + + async fetchMember(_option: CommandOptionWithType, id: string, guildId: string) { + return this.client.members.raw(guildId, id); + } + + async fetchRole(_option: CommandOptionWithType, id: string, guildId?: string) { + return guildId ? (await this.client.roles.listRaw(guildId)).find(x => x.id === id) : undefined; + } + + async runGlobalMiddlewares( + command: Command | ContextMenuCommand | SubCommand, + context: CommandContext<{}, never> | MenuCommandContext, + ) { + try { + const resultRunGlobalMiddlewares = await BaseCommand.__runMiddlewares( + context, + (this.client.options?.globalMiddlewares ?? []) as keyof RegisteredMiddlewares, + true, + ); + if (resultRunGlobalMiddlewares.pass) { + return false; + } + if ('error' in resultRunGlobalMiddlewares) { + await command.onMiddlewaresError?.(context as never, resultRunGlobalMiddlewares.error ?? 'Unknown error'); + return; + } + return resultRunGlobalMiddlewares; + } catch (e) { + try { + await command.onInternalError?.(this.client, command as never, e); + } catch {} + } + return false; + } + + async runMiddlewares( + command: Command | ContextMenuCommand | SubCommand, + context: CommandContext<{}, never> | MenuCommandContext, + ) { + try { + const resultRunMiddlewares = await BaseCommand.__runMiddlewares( + context, + command.middlewares as keyof RegisteredMiddlewares, + false, + ); + if (resultRunMiddlewares.pass) { + return false; + } + if ('error' in resultRunMiddlewares) { + await command.onMiddlewaresError?.(context as never, resultRunMiddlewares.error ?? 'Unknown error'); + return; + } + return resultRunMiddlewares; + } catch (e) { + try { + await command.onInternalError?.(this.client, command as never, e); + } catch {} + } + return false; + } + + makeMenuCommand(body: APIApplicationCommandInteraction, shardId: number, __reply?: __InternalReplyFunction) { + const command = this.getCommand(body.data); + const interaction = BaseInteraction.from(this.client, body, __reply) as + | UserCommandInteraction + | MessageCommandInteraction; + // idc, is a YOU problem + if (!command?.run) + return this.client.logger.warn(`${command?.name ?? 'Unknown'} command does not have 'run' callback`); + const context = new MenuCommandContext(this.client, interaction, shardId, command); + const extendContext = this.client.options?.context?.(interaction) ?? {}; + Object.assign(context, extendContext); + + return { command, interaction, context }; + } + + async runOptions(command: Command | SubCommand, context: CommandContext, resolver: OptionResolverStructure) { + const [erroredOptions, result] = await command.__runOptions(context, resolver); + if (erroredOptions) { + try { + await command.onOptionsError?.(context, result); + } catch (e) { + try { + await command.onInternalError?.(this.client, command, e); + } catch {} + } + return false; + } + return true; + } + + async argsOptionsParser( + command: Command | SubCommand, + message: GatewayMessageCreateDispatchData, + args: Partial>, + resolved: MakeRequired, + ) { + const options: APIApplicationCommandInteractionDataOption[] = []; + const errors: { name: string; error: string; fullError: MessageCommandOptionErrors }[] = []; + for (const i of (command.options ?? []) as (CommandOption & { type: ApplicationCommandOptionType })[]) { + try { + let value: string | boolean | number | undefined; + let indexAttachment = -1; + switch (i.type) { + case ApplicationCommandOptionType.Attachment: + if (message.attachments[++indexAttachment]) { + value = message.attachments[indexAttachment].id; + resolved.attachments[value] = message.attachments[indexAttachment]; + } + break; + case ApplicationCommandOptionType.Boolean: + if (args[i.name]) { + value = ['yes', 'y', 'true', 'treu'].includes(args[i.name]!.toLowerCase()); + } + break; + case ApplicationCommandOptionType.Channel: + { + const rawId = + message.content.match(/(?<=<#)[0-9]{17,19}(?=>)/g)?.find(x => args[i.name]?.includes(x)) || + args[i.name]?.match(/[0-9]{17,19}/g)?.[0]; + if (!rawId) continue; + const channel = (await this.client.cache.channels?.raw(rawId)) ?? (await this.fetchChannel(i, rawId)); + if (channel) { + if ('channel_types' in i) { + if (!(i as SeyfertChannelOption).channel_types!.includes(channel.type)) { + errors.push({ + name: i.name, + error: `The entered channel type is not one of ${(i as SeyfertChannelOption) + .channel_types!.map(t => ChannelType[t]) + .join(', ')}`, + fullError: ['CHANNEL_TYPES', (i as SeyfertChannelOption).channel_types!], + }); + break; + } + } + value = rawId; + //discord funny memoentnt!!!!!!!! + resolved.channels[rawId] = channel as APIInteractionDataResolvedChannel; + } + } + break; + case ApplicationCommandOptionType.Mentionable: + { + const matches = message.content.match(/<@[0-9]{17,19}(?=>)|<@&[0-9]{17,19}(?=>)/g) ?? []; + for (const match of matches) { + if (match.includes('&')) { + const rawId = match.slice(3); + if (rawId) { + const role = + (await this.client.cache.roles?.raw(rawId)) ?? (await this.fetchRole(i, rawId, message.guild_id)); + if (role) { + value = rawId; + resolved.roles[rawId] = role; + break; + } + } + } else { + const rawId = match.slice(2); + const raw = message.mentions.find(x => rawId === x.id); + if (raw) { + const { member, ...user } = raw; + value = raw.id; + resolved.users[raw.id] = user; + if (member) resolved.members[raw.id] = member; + break; + } + } + } + } + break; + case ApplicationCommandOptionType.Role: + { + const rawId = + message.mention_roles.find(x => args[i.name]?.includes(x)) || args[i.name]?.match(/[0-9]{17,19}/g)?.[0]; + if (!rawId) continue; + const role = + (await this.client.cache.roles?.raw(rawId)) ?? (await this.fetchRole(i, rawId, message.guild_id)); + if (role) { + value = rawId; + resolved.roles[rawId] = role; + } + } + break; + case ApplicationCommandOptionType.User: + { + const rawId = + message.mentions.find(x => args[i.name]?.includes(x.id))?.id || + args[i.name]?.match(/[0-9]{17,19}/g)?.[0]; + if (!rawId) continue; + const raw = + message.mentions.find(x => args[i.name]?.includes(x.id)) ?? + (await this.client.cache.users?.raw(rawId)) ?? + (await this.fetchUser(i, rawId)); + if (raw) { + value = raw.id; + resolved.users[raw.id] = raw; + if (message.guild_id) { + const member = + message.mentions.find(x => args[i.name]?.includes(x.id))?.member ?? + (await this.client.cache.members?.raw(rawId, message.guild_id)) ?? + (await this.fetchMember(i, rawId, message.guild_id)); + if (member) resolved.members[raw.id] = member; + } + } + } + break; + case ApplicationCommandOptionType.String: + { + value = args[i.name]; + const option = i as SeyfertStringOption; + if (!value) break; + if (option.min_length) { + if (value.length < option.min_length) { + value = undefined; + errors.push({ + name: i.name, + error: `The entered string has less than ${option.min_length} characters. The minimum required is ${option.min_length} characters.`, + fullError: ['STRING_MIN_LENGTH', option.min_length], + }); + break; + } + } + if (option.max_length) { + if (value.length > option.max_length) { + value = undefined; + errors.push({ + name: i.name, + error: `The entered string has more than ${option.max_length} characters. The maximum required is ${option.max_length} characters.`, + fullError: ['STRING_MAX_LENGTH', option.max_length], + }); + break; + } + } + if (option.choices?.length) { + const choice = option.choices.find(x => x.name === value); + if (!choice) { + value = undefined; + errors.push({ + name: i.name, + error: `The entered choice is invalid. Please choose one of the following options: ${option.choices + .map(x => x.name) + .join(', ')}.`, + fullError: ['STRING_INVALID_CHOICE', option.choices], + }); + break; + } + value = choice.value; + } + } + break; + case ApplicationCommandOptionType.Number: + case ApplicationCommandOptionType.Integer: + { + const option = i as SeyfertNumberOption | SeyfertIntegerOption; + if (!option.choices?.length) { + value = Number(args[i.name]); + if (args[i.name] === undefined) { + value = undefined; + break; + } + if (Number.isNaN(value)) { + value = undefined; + errors.push({ + name: i.name, + error: 'The entered choice is an invalid number.', + fullError: ['NUMBER_NAN', args[i.name]], + }); + break; + } + if (option.min_value) { + if (value < option.min_value) { + value = undefined; + errors.push({ + name: i.name, + error: `The entered number is less than ${option.min_value}. The minimum allowed is ${option.min_value}`, + fullError: ['NUMBER_MIN_VALUE', option.min_value], + }); + break; + } + } + if (option.max_value) { + if (value > option.max_value) { + value = undefined; + errors.push({ + name: i.name, + error: `The entered number is greater than ${option.max_value}. The maximum allowed is ${option.max_value}`, + fullError: ['NUMBER_MAX_VALUE', option.max_value], + }); + break; + } + } + break; + } + const choice = option.choices.find(x => x.name === args[i.name]); + if (!choice) { + value = undefined; + errors.push({ + name: i.name, + error: `The entered choice is invalid. Please choose one of the following options: ${option.choices + .map(x => x.name) + .join(', ')}.`, + fullError: ['NUMBER_INVALID_CHOICE', option.choices], + }); + break; + } + value = choice.value; + } + break; + default: + break; + } + if (value !== undefined) { + options.push({ + name: i.name, + type: i.type, + value, + } as APIApplicationCommandInteractionDataOption); + } else if (i.required) + if (!errors.some(x => x.name === i.name)) + errors.push({ + error: 'Option is required but returned undefined', + name: i.name, + fullError: ['OPTION_REQUIRED'], + }); + } catch (e) { + errors.push({ + error: e && typeof e === 'object' && 'message' in e ? (e.message as string) : `${e}`, + name: i.name, + fullError: ['UNKNOWN', e], + }); + } + } + + return { errors, options }; + } +} diff --git a/src/commands/handler.ts b/src/commands/handler.ts index 8449dc5..08ef53f 100644 --- a/src/commands/handler.ts +++ b/src/commands/handler.ts @@ -10,9 +10,9 @@ import { type APIApplicationCommandChannelOption, } from 'discord-api-types/v10'; import { basename, dirname } from 'node:path'; -import type { Logger } from '../common'; +import type { Logger, MakeRequired, NulleableCoalising, OmitInsert } from '../common'; import { BaseHandler } from '../common'; -import { Command, SubCommand } from './applications/chat'; +import { Command, type CommandOption, SubCommand } from './applications/chat'; import { ContextMenuCommand } from './applications/menu'; import type { UsingClient } from './applications/shared'; import { promises } from 'node:fs'; @@ -169,125 +169,100 @@ export class CommandHandler extends BaseHandler { return false; } - async load(commandsDir: string, client: UsingClient, instances?: { new (): Command | ContextMenuCommand }[]) { - const result = - instances?.map(x => { - const i = new x(); - return { name: i.name, file: x, path: i.__filePath ?? '*' }; - }) ?? - ( - await this.loadFilesK<{ new (): Command | SubCommand | ContextMenuCommand }>(await this.getFiles(commandsDir)) - ).filter(x => x.file); + async load(commandsDir: string, client: UsingClient) { + const result = await this.loadFilesK>(await this.getFiles(commandsDir)); this.values = []; - for (const command of result) { - let commandInstance; - try { - commandInstance = this.onCommand(command.file); - if (!commandInstance) continue; - } catch (e) { - if (e instanceof Error && e.message.includes('is not a constructor')) { - this.logger.warn( - `${command.path - .split(process.cwd()) - .slice(1) - .join(process.cwd())} doesn't export the class by \`export default \``, - ); - } else this.logger.warn(e, command); - continue; - } - if (commandInstance instanceof ContextMenuCommand) { - this.values.push(commandInstance); - commandInstance.__filePath = command.path; - this.__parseCommandLocales(commandInstance); + for (const { commands, file } of result.map(x => ({ commands: this.onFile(x.file), file: x }))) { + if (!commands) continue; + for (const command of commands) { + let commandInstance; + try { + commandInstance = this.onCommand(command); + if (!commandInstance) continue; + } catch (e) { + if (e instanceof Error && e.message.includes('is not a constructor')) { + this.logger.warn( + `${file.path + .split(process.cwd()) + .slice(1) + .join(process.cwd())} doesn't export the class by \`export default \``, + ); + } else this.logger.warn(e, command); + continue; + } + if (commandInstance instanceof SubCommand) continue; + + commandInstance.__filePath = file.path; commandInstance.props ??= client.options.commands?.defaults?.props ?? {}; - continue; - } - if (!(commandInstance instanceof Command)) { - continue; - } - commandInstance.onAfterRun ??= client.options.commands?.defaults?.onAfterRun; - commandInstance.onBotPermissionsFail ??= client.options.commands?.defaults?.onBotPermissionsFail; - commandInstance.onInternalError ??= client.options.commands?.defaults?.onInternalError; - commandInstance.onMiddlewaresError ??= client.options.commands?.defaults?.onMiddlewaresError; - commandInstance.onOptionsError ??= client.options.commands?.defaults?.onOptionsError; - commandInstance.onPermissionsFail ??= client.options.commands?.defaults?.onPermissionsFail; - commandInstance.onRunError ??= client.options.commands?.defaults?.onRunError; - commandInstance.__filePath = command.path; - commandInstance.options ??= [] as NonNullable; - commandInstance.props ??= client.options.commands?.defaults?.props ?? {}; - if (commandInstance.__autoload) { - //@AutoLoad - const options = await this.getFiles(dirname(command.path)); - for (const option of options) { - if (command.name === basename(option)) { - continue; - } - try { - const subCommand = this.onSubCommand(result.find(x => x.path === option)!.file as { new (): SubCommand }); - if (subCommand && subCommand instanceof SubCommand) { - subCommand.__filePath = option; - commandInstance.options.push(subCommand); + const isAvailableCommand = this.stablishCommandDefaults(commandInstance); + if (isAvailableCommand) { + commandInstance = isAvailableCommand; + if (commandInstance.__autoload) { + //@AutoLoad + const options = await this.getFiles(dirname(file.path)); + for (const option of options) { + if (file.name === basename(option)) { + continue; + } + try { + const fileSubCommands = this.onFile(result.find(x => x.path === option)!.file); + if (!fileSubCommands) { + this.logger.warn(`SubCommand returned (${fileSubCommands}) ignoring.`); + continue; + } + for (const fileSubCommand of fileSubCommands) { + const subCommand = this.onSubCommand(fileSubCommand as HandleableSubCommand); + if (subCommand && subCommand instanceof SubCommand) { + subCommand.__filePath = option; + commandInstance.options.push(subCommand); + } else { + this.logger.warn(subCommand ? 'SubCommand expected' : 'Invalid SubCommand', subCommand); + } + } + } catch { + //pass + } } - } catch { - //pass + } + for (const option of commandInstance.options ?? []) { + if (option instanceof SubCommand) this.stablishSubCommandDefaults(commandInstance, option); } } - } - - for (const option of commandInstance.options ?? []) { - if (option instanceof SubCommand) { - option.middlewares = (commandInstance.middlewares ?? []).concat(option.middlewares ?? []); - option.onMiddlewaresError = - option.onMiddlewaresError?.bind(option) ?? - commandInstance.onMiddlewaresError?.bind(commandInstance) ?? - this.client.options.commands?.defaults?.onMiddlewaresError; - option.onRunError = - option.onRunError?.bind(option) ?? - commandInstance.onRunError?.bind(commandInstance) ?? - this.client.options.commands?.defaults?.onRunError; - option.onOptionsError = - option.onOptionsError?.bind(option) ?? - commandInstance.onOptionsError?.bind(commandInstance) ?? - this.client.options.commands?.defaults?.onOptionsError; - option.onInternalError = - option.onInternalError?.bind(option) ?? - commandInstance.onInternalError?.bind(commandInstance) ?? - this.client.options.commands?.defaults?.onInternalError; - option.onAfterRun = - option.onAfterRun?.bind(option) ?? - commandInstance.onAfterRun?.bind(commandInstance) ?? - this.client.options.commands?.defaults?.onAfterRun; - option.onBotPermissionsFail = - option.onBotPermissionsFail?.bind(option) ?? - commandInstance.onBotPermissionsFail?.bind(commandInstance) ?? - this.client.options.commands?.defaults?.onBotPermissionsFail; - option.onPermissionsFail = - option.onPermissionsFail?.bind(option) ?? - commandInstance.onPermissionsFail?.bind(commandInstance) ?? - this.client.options.commands?.defaults?.onPermissionsFail; - option.botPermissions ??= commandInstance.botPermissions; - option.defaultMemberPermissions ??= commandInstance.defaultMemberPermissions; - option.contexts ??= commandInstance.contexts; - option.integrationTypes ??= commandInstance.integrationTypes; - option.props ??= commandInstance.props; - } - } - - this.values.push(commandInstance); - this.__parseCommandLocales(commandInstance); - - for (const i of commandInstance.options ?? []) { - if (i instanceof SubCommand) { - this.__parseCommandLocales(i); - } + this.stablishContextCommandDefaults(commandInstance); + this.values.push(commandInstance); + this.parseLocales(commandInstance); } } return this.values; } - private __parseCommandLocales(command: Command | SubCommand | ContextMenuCommand) { + parseLocales(command: Command | SubCommand | ContextMenuCommand) { + this.parseGlobalLocales(command); + if (command instanceof ContextMenuCommand) { + this.parseContextMenuLocales(command); + return command; + } + + if (command instanceof Command && command.__tGroups) { + this.parseCommandLocales(command); + for (const option of command.options ?? []) { + if (option instanceof SubCommand) { + this.parseSubCommandLocales(option); + continue; + } + // @ts-expect-error + if (option.locales) this.parseCommandOptionLocales(option); + } + } + if (command instanceof SubCommand) { + this.parseSubCommandLocales(command); + } + return command; + } + + parseGlobalLocales(command: Command | SubCommand | ContextMenuCommand) { if (command.__t) { command.name_localizations = {}; command.description_localizations = {}; @@ -310,60 +285,57 @@ export class CommandHandler extends BaseHandler { } } } + } - if (command instanceof ContextMenuCommand) return; + parseCommandOptionLocales(option: MakeRequired) { + option.name_localizations = {}; + option.description_localizations = {}; + for (const locale of Object.keys(this.client.langs!.values)) { + const locales = this.client.langs!.aliases.find(x => x[0] === locale)?.[1] ?? []; + if (Object.values(Locale).includes(locale)) locales.push(locale as LocaleString); - for (const options of command.options ?? []) { - if (options instanceof SubCommand || !options.locales) continue; - options.name_localizations = {}; - options.description_localizations = {}; - for (const locale of Object.keys(this.client.langs!.values)) { - const locales = this.client.langs!.aliases.find(x => x[0] === locale)?.[1] ?? []; - if (Object.values(Locale).includes(locale)) locales.push(locale as LocaleString); - - if (options.locales.name) { - for (const i of locales) { - const valueName = this.client.langs!.getKey(locale, options.locales.name!); - if (valueName) options.name_localizations[i] = valueName; - } + if (option.locales.name) { + for (const i of locales) { + const valueName = this.client.langs!.getKey(locale, option.locales.name!); + if (valueName) option.name_localizations[i] = valueName; } + } - if (options.locales.description) { - for (const i of locales) { - const valueKey = this.client.langs!.getKey(locale, options.locales.description!); - if (valueKey) options.description_localizations[i] = valueKey; - } + if (option.locales.description) { + for (const i of locales) { + const valueKey = this.client.langs!.getKey(locale, option.locales.description!); + if (valueKey) option.description_localizations[i] = valueKey; } } } + } - if (command instanceof Command && command.__tGroups) { - command.groups = {}; - for (const locale of Object.keys(this.client.langs!.values)) { - const locales = this.client.langs!.aliases.find(x => x[0] === locale)?.[1] ?? []; - if (Object.values(Locale).includes(locale)) locales.push(locale as LocaleString); - for (const group in command.__tGroups) { - command.groups[group] ??= { - defaultDescription: command.__tGroups[group].defaultDescription, - description: [], - name: [], - }; + parseCommandLocales(command: Command) { + command.groups = {}; + for (const locale of Object.keys(this.client.langs!.values)) { + const locales = this.client.langs!.aliases.find(x => x[0] === locale)?.[1] ?? []; + if (Object.values(Locale).includes(locale)) locales.push(locale as LocaleString); + for (const group in command.__tGroups) { + command.groups[group] ??= { + defaultDescription: command.__tGroups[group].defaultDescription, + description: [], + name: [], + }; - if (command.__tGroups[group].name) { - for (const i of locales) { - const valueName = this.client.langs!.getKey(locale, command.__tGroups[group].name!); - if (valueName) { - command.groups[group].name!.push([i, valueName]); - } + if (command.__tGroups[group].name) { + for (const i of locales) { + const valueName = this.client.langs!.getKey(locale, command.__tGroups[group].name!); + if (valueName) { + command.groups[group].name!.push([i, valueName]); } } + } - if (command.__tGroups[group].description) { - for (const i of locales) { - const valueKey = this.client.langs!.getKey(locale, command.__tGroups[group].description!); - if (valueKey) { - command.groups[group].description!.push([i, valueKey]); - } + if (command.__tGroups[group].description) { + for (const i of locales) { + const valueKey = this.client.langs!.getKey(locale, command.__tGroups[group].description!); + if (valueKey) { + command.groups[group].description!.push([i, valueKey]); } } } @@ -371,21 +343,103 @@ export class CommandHandler extends BaseHandler { } } - setHandlers({ - onCommand, - onSubCommand, - }: { - onCommand?: CommandHandler['onCommand']; - onSubCommand?: CommandHandler['onSubCommand']; - }) { - if (onCommand) this.onCommand = onCommand; - if (onSubCommand) this.onSubCommand = onSubCommand; + parseContextMenuLocales(command: ContextMenuCommand) { + return command; } - onCommand = (file: { new (): Command | SubCommand | ContextMenuCommand }): - | Command - | SubCommand - | ContextMenuCommand - | false => new file(); - onSubCommand = (file: { new (): SubCommand }): SubCommand | false => new file(); + parseSubCommandLocales(command: SubCommand) { + for (const i of command.options ?? []) { + // @ts-expect-error + if (i.locales) this.parseCommandOptionLocales(i); + } + return command; + } + + stablishContextCommandDefaults(commandInstance: InstanceType): ContextMenuCommand | false { + if (!(commandInstance instanceof ContextMenuCommand)) return false; + commandInstance.onAfterRun ??= this.client.options.commands?.defaults?.onAfterRun; + //@ts-expect-error magic. + commandInstance.onBotPermissionsFail ??= this.client.options.commands?.defaults?.onBotPermissionsFail; + //@ts-expect-error magic. + commandInstance.onInternalError ??= this.client.options.commands?.defaults?.onInternalError; + //@ts-expect-error magic. + commandInstance.onMiddlewaresError ??= this.client.options.commands?.defaults?.onMiddlewaresError; + //@ts-expect-error magic. + commandInstance.onPermissionsFail ??= this.client.options.commands?.defaults?.onPermissionsFail; + //@ts-expect-error magic. + commandInstance.onRunError ??= this.client.options.commands?.defaults?.onRunError; + return commandInstance; + } + + stablishCommandDefaults( + commandInstance: InstanceType, + ): OmitInsert }> | false { + if (!(commandInstance instanceof Command)) return false; + commandInstance.onAfterRun ??= this.client.options.commands?.defaults?.onAfterRun; + commandInstance.onBotPermissionsFail ??= this.client.options.commands?.defaults?.onBotPermissionsFail; + commandInstance.onInternalError ??= this.client.options.commands?.defaults?.onInternalError; + commandInstance.onMiddlewaresError ??= this.client.options.commands?.defaults?.onMiddlewaresError; + commandInstance.onOptionsError ??= this.client.options.commands?.defaults?.onOptionsError; + commandInstance.onPermissionsFail ??= this.client.options.commands?.defaults?.onPermissionsFail; + commandInstance.onRunError ??= this.client.options.commands?.defaults?.onRunError; + commandInstance.options ??= []; + return commandInstance as any; + } + + stablishSubCommandDefaults(commandInstance: Command, option: SubCommand): SubCommand { + option.middlewares = (commandInstance.middlewares ?? []).concat(option.middlewares ?? []); + option.onMiddlewaresError = + option.onMiddlewaresError?.bind(option) ?? + commandInstance.onMiddlewaresError?.bind(commandInstance) ?? + this.client.options.commands?.defaults?.onMiddlewaresError; + option.onRunError = + option.onRunError?.bind(option) ?? + commandInstance.onRunError?.bind(commandInstance) ?? + this.client.options.commands?.defaults?.onRunError; + option.onOptionsError = + option.onOptionsError?.bind(option) ?? + commandInstance.onOptionsError?.bind(commandInstance) ?? + this.client.options.commands?.defaults?.onOptionsError; + option.onInternalError = + option.onInternalError?.bind(option) ?? + commandInstance.onInternalError?.bind(commandInstance) ?? + this.client.options.commands?.defaults?.onInternalError; + option.onAfterRun = + option.onAfterRun?.bind(option) ?? + commandInstance.onAfterRun?.bind(commandInstance) ?? + this.client.options.commands?.defaults?.onAfterRun; + option.onBotPermissionsFail = + option.onBotPermissionsFail?.bind(option) ?? + commandInstance.onBotPermissionsFail?.bind(commandInstance) ?? + this.client.options.commands?.defaults?.onBotPermissionsFail; + option.onPermissionsFail = + option.onPermissionsFail?.bind(option) ?? + commandInstance.onPermissionsFail?.bind(commandInstance) ?? + this.client.options.commands?.defaults?.onPermissionsFail; + option.botPermissions ??= commandInstance.botPermissions; + option.defaultMemberPermissions ??= commandInstance.defaultMemberPermissions; + option.contexts ??= commandInstance.contexts; + option.integrationTypes ??= commandInstance.integrationTypes; + option.props ??= commandInstance.props; + return option; + } + + onFile(file: FileLoaded): HandleableCommand[] | undefined { + return file.default ? [file.default] : undefined; + } + + onCommand(file: HandleableCommand): Command | SubCommand | ContextMenuCommand | false { + return new file(); + } + + onSubCommand(file: HandleableSubCommand): SubCommand | false { + return new file(); + } } + +export type FileLoaded = { + default?: NulleableCoalising; +} & Record>; + +export type HandleableCommand = new () => Command | SubCommand | ContextMenuCommand; +export type HandleableSubCommand = new () => SubCommand; diff --git a/src/commands/optionresolver.ts b/src/commands/optionresolver.ts index 038871f..e383303 100644 --- a/src/commands/optionresolver.ts +++ b/src/commands/optionresolver.ts @@ -8,23 +8,26 @@ import { type APIUser, ApplicationCommandOptionType, } from 'discord-api-types/v10'; -import { Attachment, GuildMember } from '..'; +import { Attachment } from '..'; import type { MakeRequired } from '../common'; import type { AllChannels } from '../structures'; -import { GuildRole, InteractionGuildMember, User } from '../structures'; import channelFrom from '../structures/channels'; import type { Command, CommandAutocompleteOption, CommandOption, SubCommand } from './applications/chat'; import type { UsingClient } from './applications/shared'; +import { + type GuildMemberStructure, + type GuildRoleStructure, + type InteractionGuildMemberStructure, + Transformers, + type UserStructure, +} from '../client/transformers'; export type ContextOptionsResolved = { - members?: Record< - string, - APIGuildMember | Omit | APIInteractionGuildMember | GuildMember | InteractionGuildMember - >; - users?: Record; - roles?: Record; - channels?: Record; - attachments?: Record; + members?: Record | APIInteractionGuildMember>; + users?: Record; + roles?: Record; + channels?: Record; + attachments?: Record; }; export class OptionResolver { @@ -164,18 +167,16 @@ export class OptionResolver { const value = resolve.value as string; const user = resolved.users?.[value]; if (user) { - resolve.user = user instanceof User ? user : new User(this.client, user); + resolve.user = Transformers.User(this.client, user); } const member = resolved.members?.[value]; if (member) { resolve.member = - member instanceof GuildMember || member instanceof InteractionGuildMember - ? member - : 'permissions' in member - ? new InteractionGuildMember(this.client, member, user!, this.guildId!) - : new GuildMember(this.client, member, user!, this.guildId!); + 'permissions' in member + ? Transformers.InteractionGuildMember(this.client, member, user!, this.guildId!) + : Transformers.GuildMember(this.client, member, user!, this.guildId!); } const channel = resolved.channels?.[value]; @@ -185,7 +186,7 @@ export class OptionResolver { const role = resolved.roles?.[value]; if (role) { - resolve.role = role instanceof GuildRole ? role : new GuildRole(this.client, role, this.guildId!); + resolve.role = Transformers.GuildRole(this.client, role, this.guildId!); } const attachment = resolved.attachments?.[value]; @@ -203,11 +204,11 @@ export interface OptionResolved { type: ApplicationCommandOptionType; value?: string | number | boolean; options?: OptionResolved[]; - user?: User; - member?: GuildMember | InteractionGuildMember; + user?: UserStructure; + member?: GuildMemberStructure | InteractionGuildMemberStructure; attachment?: Attachment; channel?: AllChannels; - role?: GuildRole; + role?: GuildRoleStructure; focused?: boolean; } diff --git a/src/common/it/utils.ts b/src/common/it/utils.ts index 8ddb1f0..ede218a 100644 --- a/src/common/it/utils.ts +++ b/src/common/it/utils.ts @@ -130,7 +130,7 @@ export class BaseHandler { * @param paths The paths of the files to load. * @returns A Promise that resolves to an array of loaded files. */ - protected async loadFiles>(paths: string[]): Promise { + protected loadFiles>(paths: string[]): Promise { return Promise.all(paths.map(path => magicImport(path).then(file => file.default ?? file))); } @@ -139,13 +139,13 @@ export class BaseHandler { * @param paths The paths of the files to load. * @returns A Promise that resolves to an array of objects containing name, file, and path. */ - protected async loadFilesK(paths: string[]): Promise<{ name: string; file: T; path: string }[]> { + protected loadFilesK(paths: string[]): Promise<{ name: string; file: T; path: string }[]> { return Promise.all( paths.map(path => magicImport(path).then(file => { return { name: basename(path), - file: file.default ?? file, + file, path, }; }), diff --git a/src/common/shorters/bans.ts b/src/common/shorters/bans.ts index 176a02f..0502127 100644 --- a/src/common/shorters/bans.ts +++ b/src/common/shorters/bans.ts @@ -5,7 +5,7 @@ import type { RESTPutAPIGuildBanJSONBody, } from 'discord-api-types/v10'; import { BaseShorter } from './base'; -import { GuildBan } from '../../structures/GuildBan'; +import { Transformers } from '../../client/transformers'; export class BanShorter extends BaseShorter { /** @@ -58,7 +58,7 @@ export class BanShorter extends BaseShorter { ban = await this.client.proxy.guilds(guildId).bans(userId).get(); await this.client.cache.members?.set(ban.user!.id, guildId, ban); - return new GuildBan(this.client, ban, guildId); + return Transformers.GuildBan(this.client, ban, guildId); } /** @@ -81,6 +81,6 @@ export class BanShorter extends BaseShorter { bans.map<[string, APIBan]>(x => [x.user!.id, x]), guildId, ); - return bans.map(m => new GuildBan(this.client, m, guildId)); + return bans.map(m => Transformers.GuildBan(this.client, m, guildId)); } } diff --git a/src/common/shorters/channels.ts b/src/common/shorters/channels.ts index b8ff7a7..b0445be 100644 --- a/src/common/shorters/channels.ts +++ b/src/common/shorters/channels.ts @@ -1,15 +1,17 @@ import { + type APIChannel, PermissionFlagsBits, type RESTGetAPIChannelMessagesQuery, type RESTPatchAPIChannelJSONBody, type RESTPostAPIChannelThreadsJSONBody, type RESTPostAPIGuildForumThreadsJSONBody, } from 'discord-api-types/v10'; -import { BaseChannel, Message, type GuildMember, type GuildRole } from '../../structures'; +import { BaseChannel, type GuildRole, type GuildMember } from '../../structures'; import channelFrom, { type AllChannels } from '../../structures/channels'; import { PermissionsBitField } from '../../structures/extra/Permissions'; import { BaseShorter } from './base'; import { MergeOptions } from '../it/utils'; +import { type MessageStructure, Transformers } from '../../client/transformers'; export class ChannelShorter extends BaseShorter { /** @@ -19,15 +21,19 @@ export class ChannelShorter extends BaseShorter { * @returns A Promise that resolves to the fetched channel. */ async fetch(id: string, force?: boolean): Promise { + return channelFrom(await this.raw(id, force), this.client); + } + + async raw(id: string, force?: boolean): Promise { let channel; if (!force) { - channel = await this.client.cache.channels?.get(id); + channel = await this.client.cache.channels?.raw(id); if (channel) return channel; } channel = await this.client.proxy.channels(id).get(); await this.client.cache.channels?.patch(id, undefined, channel); - return channelFrom(channel, this.client); + return channel; } /** @@ -77,7 +83,7 @@ export class ChannelShorter extends BaseShorter { await this.client.proxy.channels(id).typing.post(); } - async pins(channelId: string): Promise { + async pins(channelId: string): Promise { const messages = await this.client.proxy.channels(channelId).pins.get(); await this.client.cache.messages?.patch( messages.map(x => { @@ -85,7 +91,7 @@ export class ChannelShorter extends BaseShorter { }) satisfies [string, any][], channelId, ); - return messages.map(message => new Message(this.client, message)); + return messages.map(message => Transformers.Message(this.client, message)); } /** @@ -194,7 +200,7 @@ export class ChannelShorter extends BaseShorter { }) satisfies [string, any][], channelId, ); - return result.map(message => new Message(this.client, message)); + return result.map(message => Transformers.Message(this.client, message)); } setVoiceStatus(channelId: string, status: string | null = null) { diff --git a/src/common/shorters/emojis.ts b/src/common/shorters/emojis.ts index 0c3b005..dfd04b6 100644 --- a/src/common/shorters/emojis.ts +++ b/src/common/shorters/emojis.ts @@ -1,9 +1,9 @@ import type { APIEmoji, RESTPatchAPIGuildEmojiJSONBody, RESTPostAPIGuildEmojiJSONBody } from 'discord-api-types/v10'; -import { GuildEmoji } from '../..'; import { resolveImage } from '../../builders'; import type { ImageResolvable } from '../types/resolvables'; import type { OmitInsert } from '../types/util'; import { BaseShorter } from './base'; +import { Transformers } from '../../client/transformers'; export class EmojiShorter extends BaseShorter { /** @@ -25,7 +25,7 @@ export class EmojiShorter extends BaseShorter { emojis.map<[string, APIEmoji]>(x => [x.id!, x]), guildId, ); - return emojis.map(m => new GuildEmoji(this.client, m, guildId)); + return emojis.map(m => Transformers.GuildEmoji(this.client, m, guildId)); } /** @@ -42,7 +42,7 @@ export class EmojiShorter extends BaseShorter { await this.client.cache.emojis?.setIfNI('GuildEmojisAndStickers', emoji.id!, guildId, emoji); - return new GuildEmoji(this.client, emoji, guildId); + return Transformers.GuildEmoji(this.client, emoji, guildId); } /** @@ -59,7 +59,7 @@ export class EmojiShorter extends BaseShorter { if (emoji) return emoji; } emoji = await this.client.proxy.guilds(guildId).emojis(emojiId).get(); - return new GuildEmoji(this.client, emoji, guildId); + return Transformers.GuildEmoji(this.client, emoji, guildId); } /** @@ -84,6 +84,6 @@ export class EmojiShorter extends BaseShorter { async edit(guildId: string, emojiId: string, body: RESTPatchAPIGuildEmojiJSONBody, reason?: string) { const emoji = await this.client.proxy.guilds(guildId).emojis(emojiId).patch({ body, reason }); await this.client.cache.emojis?.setIfNI('GuildEmojisAndStickers', emoji.id!, guildId, emoji); - return new GuildEmoji(this.client, emoji, guildId); + return Transformers.GuildEmoji(this.client, emoji, guildId); } } diff --git a/src/common/shorters/guilds.ts b/src/common/shorters/guilds.ts index 47821bc..859c075 100644 --- a/src/common/shorters/guilds.ts +++ b/src/common/shorters/guilds.ts @@ -12,17 +12,10 @@ import type { } from 'discord-api-types/v10'; import { toSnakeCase, type ObjectToLower } from '..'; import { resolveFiles } from '../../builders'; -import { - AnonymousGuild, - AutoModerationRule, - BaseChannel, - Guild, - GuildMember, - Sticker, - type CreateStickerBodyRequest, -} from '../../structures'; +import { BaseChannel, GuildMember, type CreateStickerBodyRequest } from '../../structures'; import channelFrom from '../../structures/channels'; import { BaseShorter } from './base'; +import { type GuildStructure, Transformers } from '../../client/transformers'; export class GuildShorter extends BaseShorter { /** @@ -30,10 +23,10 @@ export class GuildShorter extends BaseShorter { * @param body The data for creating the guild. * @returns A Promise that resolves to the created guild. */ - async create(body: RESTPostAPIGuildsJSONBody): Promise> { + async create(body: RESTPostAPIGuildsJSONBody): Promise> { const guild = await this.client.proxy.guilds.post({ body }); await this.client.cache.guilds?.setIfNI('Guilds', guild.id, guild); - return new Guild<'api'>(this.client, guild); + return Transformers.Guild<'api'>(this.client, guild); } /** @@ -43,14 +36,18 @@ export class GuildShorter extends BaseShorter { * @returns A Promise that resolves to the fetched guild. */ async fetch(id: string, force = false) { + return Transformers.Guild<'api'>(this.client, await this.raw(id, force)); + } + + async raw(id: string, force = false) { if (!force) { - const guild = await this.client.cache.guilds?.get(id); + const guild = await this.client.cache.guilds?.raw(id); if (guild) return guild; } const data = await this.client.proxy.guilds(id).get(); await this.client.cache.guilds?.patch(id, data); - return (await this.client.cache.guilds?.get(id)) ?? new Guild<'api'>(this.client, data); + return (await this.client.cache.guilds?.raw(id)) ?? data; } /** @@ -72,17 +69,17 @@ export class GuildShorter extends BaseShorter { return this.client.proxy .users('@me') .guilds.get({ query }) - .then(guilds => guilds.map(guild => new AnonymousGuild(this.client, { ...guild, splash: null }))); + .then(guilds => guilds.map(guild => Transformers.AnonymousGuild(this.client, { ...guild, splash: null }))); } async fetchSelf(id: string, force = false) { if (!force) { - const self = await this.client.cache.members?.get(this.client.botId, id); - if (self) return self; + const self = await this.client.cache.members?.raw(this.client.botId, id); + if (self?.user) return new GuildMember(this.client, self, self.user, id); } const self = await this.client.proxy.guilds(id).members(this.client.botId).get(); - await this.client.cache.members?.patch(self.user!.id, id, self); - return new GuildMember(this.client, self, self.user!, id); + await this.client.cache.members?.patch(self.user.id, id, self); + return new GuildMember(this.client, self, self.user, id); } leave(id: string) { @@ -211,7 +208,7 @@ export class GuildShorter extends BaseShorter { this.client.proxy .guilds(guildId) ['auto-moderation'].rules.get() - .then(rules => rules.map(rule => new AutoModerationRule(this.client, rule))), + .then(rules => rules.map(rule => Transformers.AutoModerationRule(this.client, rule))), /** * Creates a new auto-moderation rule in the guild. @@ -223,7 +220,7 @@ export class GuildShorter extends BaseShorter { this.client.proxy .guilds(guildId) ['auto-moderation'].rules.post({ body }) - .then(rule => new AutoModerationRule(this.client, rule)), + .then(rule => Transformers.AutoModerationRule(this.client, rule)), /** * Deletes an auto-moderation rule from the guild. @@ -247,7 +244,7 @@ export class GuildShorter extends BaseShorter { .guilds(guildId) ['auto-moderation'].rules(ruleId) .get() - .then(rule => new AutoModerationRule(this.client, rule)); + .then(rule => Transformers.AutoModerationRule(this.client, rule)); }, /** @@ -268,7 +265,7 @@ export class GuildShorter extends BaseShorter { .guilds(guildId) ['auto-moderation'].rules(ruleId) .patch({ body: toSnakeCase(body), reason }) - .then(rule => new AutoModerationRule(this.client, rule)); + .then(rule => Transformers.AutoModerationRule(this.client, rule)); }, }; } @@ -289,7 +286,7 @@ export class GuildShorter extends BaseShorter { stickers.map(st => [st.id, st] as any), guildId, ); - return stickers.map(st => new Sticker(this.client, st)); + return stickers.map(st => Transformers.Sticker(this.client, st)); }, /** @@ -305,7 +302,7 @@ export class GuildShorter extends BaseShorter { .guilds(guildId) .stickers.post({ reason, body: json, files: [{ ...fileResolve[0], key: 'file' }], appendToFormData: true }); await this.client.cache.stickers?.setIfNI('GuildEmojisAndStickers', sticker.id, guildId, sticker); - return new Sticker(this.client, sticker); + return Transformers.Sticker(this.client, sticker); }, /** @@ -319,7 +316,7 @@ export class GuildShorter extends BaseShorter { edit: async (guildId: string, stickerId: string, body: RESTPatchAPIGuildStickerJSONBody, reason?: string) => { const sticker = await this.client.proxy.guilds(guildId).stickers(stickerId).patch({ body, reason }); await this.client.cache.stickers?.setIfNI('GuildEmojisAndStickers', stickerId, guildId, sticker); - return new Sticker(this.client, sticker); + return Transformers.Sticker(this.client, sticker); }, /** @@ -337,7 +334,7 @@ export class GuildShorter extends BaseShorter { } sticker = await this.client.proxy.guilds(guildId).stickers(stickerId).get(); await this.client.cache.stickers?.patch(stickerId, guildId, sticker); - return new Sticker(this.client, sticker); + return Transformers.Sticker(this.client, sticker); }, /** diff --git a/src/common/shorters/interaction.ts b/src/common/shorters/interaction.ts index cc79631..57ea8e8 100644 --- a/src/common/shorters/interaction.ts +++ b/src/common/shorters/interaction.ts @@ -1,4 +1,5 @@ -import { BaseInteraction, WebhookMessage, resolveFiles, type ReplyInteractionBody, Modal } from '../..'; +import { BaseInteraction, resolveFiles, type ReplyInteractionBody, Modal } from '../..'; +import { Transformers } from '../../client/transformers'; import type { InteractionMessageUpdateBodyRequest, MessageWebhookCreateBodyRequest } from '../types/write'; import { BaseShorter } from './base'; @@ -42,7 +43,7 @@ export class InteractionShorter extends BaseShorter { body: BaseInteraction.transformBody(data, parsedFiles, this.client), files: parsedFiles, }); - return new WebhookMessage(this.client, apiMessage, this.client.applicationId, token); + return Transformers.WebhookMessage(this.client, apiMessage, this.client.applicationId, token); } editOriginal(token: string, body: InteractionMessageUpdateBodyRequest) { @@ -69,6 +70,6 @@ export class InteractionShorter extends BaseShorter { body: BaseInteraction.transformBody(body, parsedFiles, this.client), files: parsedFiles, }); - return new WebhookMessage(this.client, apiMessage, this.client.applicationId, token); + return Transformers.WebhookMessage(this.client, apiMessage, this.client.applicationId, token); } } diff --git a/src/common/shorters/members.ts b/src/common/shorters/members.ts index 6ce6654..20f1fde 100644 --- a/src/common/shorters/members.ts +++ b/src/common/shorters/members.ts @@ -7,10 +7,10 @@ import type { RESTPutAPIGuildBanJSONBody, RESTPutAPIGuildMemberJSONBody, } from 'discord-api-types/v10'; -import { GuildMember } from '../../structures'; import { PermissionsBitField } from '../../structures/extra/Permissions'; import type { GuildMemberResolvable } from '../types/resolvables'; import { BaseShorter } from './base'; +import { Transformers } from '../../client/transformers'; export class MemberShorter extends BaseShorter { /** @@ -52,7 +52,7 @@ export class MemberShorter extends BaseShorter { members.map(x => [x.user!.id, x]), guildId, ); - return members.map(m => new GuildMember(this.client, m, m.user!, guildId)); + return members.map(m => Transformers.GuildMember(this.client, m, m.user!, guildId)); } /** @@ -100,7 +100,7 @@ export class MemberShorter extends BaseShorter { async edit(guildId: string, memberId: string, body: RESTPatchAPIGuildMemberJSONBody, reason?: string) { const member = await this.client.proxy.guilds(guildId).members(memberId).patch({ body, reason }); await this.client.cache.members?.setIfNI('GuildMembers', memberId, guildId, member); - return new GuildMember(this.client, member, member.user!, guildId); + return Transformers.GuildMember(this.client, member, member.user!, guildId); } /** @@ -122,7 +122,7 @@ export class MemberShorter extends BaseShorter { await this.client.cache.members?.setIfNI('GuildMembers', member.user!.id, guildId, member); - return new GuildMember(this.client, member, member.user!, guildId); + return Transformers.GuildMember(this.client, member, member.user!, guildId); } /** @@ -133,15 +133,20 @@ export class MemberShorter extends BaseShorter { * @returns A Promise that resolves to the fetched member. */ async fetch(guildId: string, memberId: string, force = false) { + const member = await this.raw(guildId, memberId, force); + return Transformers.GuildMember(this.client, member, member.user, guildId); + } + + async raw(guildId: string, memberId: string, force = false) { let member; if (!force) { - member = await this.client.cache.members?.get(memberId, guildId); + member = await this.client.cache.members?.raw(memberId, guildId); if (member) return member; } member = await this.client.proxy.guilds(guildId).members(memberId).get(); - await this.client.cache.members?.set(member.user!.id, guildId, member); - return new GuildMember(this.client, member, member.user!, guildId); + await this.client.cache.members?.set(member.user.id, guildId, member); + return member; } /** @@ -161,7 +166,7 @@ export class MemberShorter extends BaseShorter { query, }); await this.client.cache.members?.set(members.map(x => [x.user!.id, x]) as [string, APIGuildMember][], guildId); - return members.map(m => new GuildMember(this.client, m, m.user!, guildId)); + return members.map(m => Transformers.GuildMember(this.client, m, m.user!, guildId)); } /** diff --git a/src/common/shorters/messages.ts b/src/common/shorters/messages.ts index f21575e..6af363c 100644 --- a/src/common/shorters/messages.ts +++ b/src/common/shorters/messages.ts @@ -4,11 +4,12 @@ import type { RESTPostAPIChannelMessagesThreadsJSONBody, } from 'discord-api-types/v10'; import { resolveFiles } from '../../builders'; -import { Message, MessagesMethods, User } from '../../structures'; +import { MessagesMethods } from '../../structures'; import type { MessageCreateBodyRequest, MessageUpdateBodyRequest } from '../types/write'; import { BaseShorter } from './base'; import type { ValidAnswerId } from '../../api/Routes/channels'; +import { Transformers } from '../../client/transformers'; export class MessageShorter extends BaseShorter { async write(channelId: string, { files, ...body }: MessageCreateBodyRequest) { @@ -27,7 +28,7 @@ export class MessageShorter extends BaseShorter { }) .then(async message => { await this.client.cache.messages?.setIfNI('GuildMessages', message.id, message.channel_id, message); - return new Message(this.client, message); + return Transformers.Message(this.client, message); }); } @@ -42,7 +43,7 @@ export class MessageShorter extends BaseShorter { }) .then(async message => { await this.client.cache.messages?.setIfNI('GuildMessages', message.id, message.channel_id, message); - return new Message(this.client, message); + return Transformers.Message(this.client, message); }); } @@ -53,7 +54,7 @@ export class MessageShorter extends BaseShorter { .crosspost.post({ reason }) .then(async m => { await this.client.cache.messages?.setIfNI('GuildMessages', m.id, m.channel_id, m); - return new Message(this.client, m); + return Transformers.Message(this.client, m); }); } @@ -75,7 +76,7 @@ export class MessageShorter extends BaseShorter { .get() .then(async x => { await this.client.cache.messages?.set(x.id, x.channel_id, x); - return new Message(this.client, x); + return Transformers.Message(this.client, x); }); } @@ -99,7 +100,7 @@ export class MessageShorter extends BaseShorter { .channels(channelId) .polls(messageId) .expire.post() - .then(message => new Message(this.client, message)); + .then(message => Transformers.Message(this.client, message)); } getAnswerVoters(channelId: string, messageId: string, answerId: ValidAnswerId) { @@ -108,6 +109,6 @@ export class MessageShorter extends BaseShorter { .polls(messageId) .answers(answerId) .get() - .then(data => data.users.map(user => new User(this.client, user))); + .then(data => data.users.map(user => Transformers.User(this.client, user))); } } diff --git a/src/common/shorters/reactions.ts b/src/common/shorters/reactions.ts index 2f6b3d4..a10a4d8 100644 --- a/src/common/shorters/reactions.ts +++ b/src/common/shorters/reactions.ts @@ -1,8 +1,8 @@ import type { RESTGetAPIChannelMessageReactionUsersQuery } from 'discord-api-types/v10'; -import { User } from '../../structures'; import { encodeEmoji, resolveEmoji } from '../../structures/extra/functions'; import type { EmojiResolvable } from '../types/resolvables'; import { BaseShorter } from './base'; +import { Transformers, type UserStructure } from '../../client/transformers'; export class ReactionShorter extends BaseShorter { async add(messageId: string, channelId: string, emoji: EmojiResolvable): Promise { @@ -30,7 +30,7 @@ export class ReactionShorter extends BaseShorter { channelId: string, emoji: EmojiResolvable, query?: RESTGetAPIChannelMessageReactionUsersQuery, - ): Promise { + ): Promise { const rawEmoji = await resolveEmoji(emoji, this.client.cache); if (!rawEmoji) { @@ -42,7 +42,7 @@ export class ReactionShorter extends BaseShorter { .messages(messageId) .reactions(encodeEmoji(rawEmoji)) .get({ query }) - .then(u => u.map(user => new User(this.client, user))); + .then(u => u.map(user => Transformers.User(this.client, user))); } async purge(messageId: string, channelId: string, emoji?: EmojiResolvable): Promise { diff --git a/src/common/shorters/roles.ts b/src/common/shorters/roles.ts index 04fd54f..fa2233b 100644 --- a/src/common/shorters/roles.ts +++ b/src/common/shorters/roles.ts @@ -4,8 +4,8 @@ import type { RESTPatchAPIGuildRolePositionsJSONBody, RESTPostAPIGuildRoleJSONBody, } from 'discord-api-types/v10'; -import { GuildRole } from '../../structures'; import { BaseShorter } from './base'; +import { Transformers } from '../../client/transformers'; export class RoleShorter extends BaseShorter { /** @@ -18,7 +18,7 @@ export class RoleShorter extends BaseShorter { async create(guildId: string, body: RESTPostAPIGuildRoleJSONBody, reason?: string) { const res = await this.client.proxy.guilds(guildId).roles.post({ body, reason }); await this.client.cache.roles?.setIfNI('Guilds', res.id, guildId, res); - return new GuildRole(this.client, res, guildId); + return Transformers.GuildRole(this.client, res, guildId); } /** @@ -28,9 +28,14 @@ export class RoleShorter extends BaseShorter { * @returns A Promise that resolves to an array of roles. */ async list(guildId: string, force = false) { + const roles = await this.listRaw(guildId, force); + return roles.map(r => Transformers.GuildRole(this.client, r, guildId)); + } + + async listRaw(guildId: string, force = false) { let roles: APIRole[] = []; if (!force) { - const cachedRoles = (await this.client.cache.roles?.values(guildId)) ?? []; + const cachedRoles = (await this.client.cache.roles?.valuesRaw(guildId)) ?? []; if (cachedRoles.length) { return cachedRoles; } @@ -40,7 +45,7 @@ export class RoleShorter extends BaseShorter { roles.map<[string, APIRole]>(r => [r.id, r]), guildId, ); - return roles.map(r => new GuildRole(this.client, r, guildId)); + return roles; } /** @@ -54,7 +59,7 @@ export class RoleShorter extends BaseShorter { async edit(guildId: string, roleId: string, body: RESTPatchAPIGuildRoleJSONBody, reason?: string) { const res = await this.client.proxy.guilds(guildId).roles(roleId).patch({ body, reason }); await this.client.cache.roles?.setIfNI('Guilds', roleId, guildId, res); - return new GuildRole(this.client, res, guildId); + return Transformers.GuildRole(this.client, res, guildId); } /** @@ -67,7 +72,7 @@ export class RoleShorter extends BaseShorter { async delete(guildId: string, roleId: string, reason?: string) { const res = await this.client.proxy.guilds(guildId).roles(roleId).delete({ reason }); this.client.cache.roles?.removeIfNI('Guilds', roleId, guildId); - return new GuildRole(this.client, res, guildId); + return Transformers.GuildRole(this.client, res, guildId); } /** @@ -86,6 +91,6 @@ export class RoleShorter extends BaseShorter { guildId, ); } - return roles.map(x => new GuildRole(this.client, x, guildId)); + return roles.map(x => Transformers.GuildRole(this.client, x, guildId)); } } diff --git a/src/common/shorters/templates.ts b/src/common/shorters/templates.ts index f27a1dd..50fb9d1 100644 --- a/src/common/shorters/templates.ts +++ b/src/common/shorters/templates.ts @@ -1,27 +1,27 @@ import type { RESTPatchAPIGuildTemplateJSONBody, RESTPostAPIGuildTemplatesJSONBody } from 'discord-api-types/v10'; import { BaseShorter } from './base'; -import { GuildTemplate } from '../..'; +import { Transformers } from '../../client/transformers'; export class TemplateShorter extends BaseShorter { fetch(code: string) { return this.client.proxy.guilds .templates(code) .get() - .then(template => new GuildTemplate(this.client, template)); + .then(template => Transformers.GuildTemplate(this.client, template)); } list(guildId: string) { return this.client.proxy .guilds(guildId) .templates.get() - .then(list => list.map(template => new GuildTemplate(this.client, template))); + .then(list => list.map(template => Transformers.GuildTemplate(this.client, template))); } create(guildId: string, body: RESTPostAPIGuildTemplatesJSONBody) { return this.client.proxy .guilds(guildId) .templates.post({ body }) - .then(template => new GuildTemplate(this.client, template)); + .then(template => Transformers.GuildTemplate(this.client, template)); } sync(guildId: string, code: string) { @@ -29,7 +29,7 @@ export class TemplateShorter extends BaseShorter { .guilds(guildId) .templates(code) .put({}) - .then(template => new GuildTemplate(this.client, template)); + .then(template => Transformers.GuildTemplate(this.client, template)); } edit(guildId: string, code: string, body: RESTPatchAPIGuildTemplateJSONBody) { @@ -37,7 +37,7 @@ export class TemplateShorter extends BaseShorter { .guilds(guildId) .templates(code) .patch({ body }) - .then(template => new GuildTemplate(this.client, template)); + .then(template => Transformers.GuildTemplate(this.client, template)); } delete(guildId: string, code: string) { @@ -45,6 +45,6 @@ export class TemplateShorter extends BaseShorter { .guilds(guildId) .templates(code) .delete() - .then(template => new GuildTemplate(this.client, template)); + .then(template => Transformers.GuildTemplate(this.client, template)); } } diff --git a/src/common/shorters/threads.ts b/src/common/shorters/threads.ts index 715e785..4f3e060 100644 --- a/src/common/shorters/threads.ts +++ b/src/common/shorters/threads.ts @@ -7,10 +7,10 @@ import type { RESTPostAPIChannelThreadsJSONBody, RESTPostAPIGuildForumThreadsJSONBody, } from 'discord-api-types/v10'; -import type { ThreadChannel } from '../../structures'; import channelFrom from '../../structures/channels'; import { BaseShorter } from './base'; import type { MakeRequired, When } from '../types/util'; +import type { ThreadChannelStructure } from '../../client/transformers'; export class ThreadShorter extends BaseShorter { /** @@ -29,7 +29,7 @@ export class ThreadShorter extends BaseShorter { .channels(channelId) .threads.post({ body, reason }) // When testing this, discord returns the thread object, but in discord api types it does not. - .then(thread => channelFrom(thread, this.client) as ThreadChannel) + .then(thread => channelFrom(thread, this.client) as ThreadChannelStructure) ); } @@ -44,7 +44,7 @@ export class ThreadShorter extends BaseShorter { .channels(channelId) .messages(messageId) .threads.post({ body, reason }) - .then(thread => channelFrom(thread, this.client) as ThreadChannel); + .then(thread => channelFrom(thread, this.client) as ThreadChannelStructure); } async join(threadId: string) { @@ -56,7 +56,7 @@ export class ThreadShorter extends BaseShorter { } async lock(threadId: string, locked = true, reason?: string) { - return this.edit(threadId, { locked }, reason).then(x => channelFrom(x, this.client) as ThreadChannel); + return this.edit(threadId, { locked }, reason).then(x => channelFrom(x, this.client) as ThreadChannelStructure); } async edit(threadId: string, body: RESTPatchAPIChannelJSONBody, reason?: string) { @@ -98,7 +98,7 @@ export class ThreadShorter extends BaseShorter { const data = await this.client.proxy.channels(channelId).threads.archived[type].get({ query }); return { - threads: data.threads.map(thread => channelFrom(thread, this.client) as ThreadChannel), + threads: data.threads.map(thread => channelFrom(thread, this.client) as ThreadChannelStructure), members: data.members as GetAPIChannelThreadMemberResult[], hasMore: data.has_more, }; @@ -107,7 +107,7 @@ export class ThreadShorter extends BaseShorter { async listJoinedArchivedPrivate(channelId: string, query?: RESTGetAPIChannelThreadsArchivedQuery) { const data = await this.client.proxy.channels(channelId).users('@me').threads.archived.private.get({ query }); return { - threads: data.threads.map(thread => channelFrom(thread, this.client) as ThreadChannel), + threads: data.threads.map(thread => channelFrom(thread, this.client) as ThreadChannelStructure), members: data.members as GetAPIChannelThreadMemberResult[], hasMore: data.has_more, }; diff --git a/src/common/shorters/users.ts b/src/common/shorters/users.ts index 92d3086..84de776 100644 --- a/src/common/shorters/users.ts +++ b/src/common/shorters/users.ts @@ -1,4 +1,5 @@ -import { BaseChannel, DMChannel, User } from '../../structures'; +import { type DMChannelStructure, Transformers } from '../../client/transformers'; +import { BaseChannel } from '../../structures'; import type { MessageCreateBodyRequest } from '../types/write'; import { BaseShorter } from './base'; @@ -6,30 +7,34 @@ export class UsersShorter extends BaseShorter { async createDM(userId: string, force = false) { if (!force) { const dm = await this.client.cache.channels?.get(userId); - if (dm) return dm as DMChannel; + if (dm) return dm as DMChannelStructure; } const data = await this.client.proxy.users('@me').channels.post({ body: { recipient_id: userId }, }); await this.client.cache.channels?.set(userId, '@me', data); - return new DMChannel(this.client, data); + return Transformers.DMChannel(this.client, data); } async deleteDM(userId: string, reason?: string) { const res = await this.client.proxy.channels(userId).delete({ reason }); await this.client.cache.channels?.removeIfNI(BaseChannel.__intent__('@me'), res.id, '@me'); - return new DMChannel(this.client, res); + return Transformers.DMChannel(this.client, res); } async fetch(userId: string, force = false) { + return Transformers.User(this.client, await this.raw(userId, force)); + } + + async raw(userId: string, force = false) { if (!force) { - const user = await this.client.cache.users?.get(userId); + const user = await this.client.cache.users?.raw(userId); if (user) return user; } const data = await this.client.proxy.users(userId).get(); await this.client.cache.users?.patch(userId, data); - return new User(this.client, data); + return data; } async write(userId: string, body: MessageCreateBodyRequest) { diff --git a/src/common/shorters/webhook.ts b/src/common/shorters/webhook.ts index ccf4850..34ce44c 100644 --- a/src/common/shorters/webhook.ts +++ b/src/common/shorters/webhook.ts @@ -7,19 +7,18 @@ import type { import { resolveFiles } from '../../builders'; import { MessagesMethods, - Webhook, - WebhookMessage, type MessageWebhookMethodEditParams, type MessageWebhookMethodWriteParams, } from '../../structures'; import { BaseShorter } from './base'; +import { Transformers } from '../../client/transformers'; export class WebhookShorter extends BaseShorter { async create(channelId: string, body: RESTPostAPIChannelWebhookJSONBody) { const webhook = await this.client.proxy.channels(channelId).webhooks.post({ body, }); - return new Webhook(this.client, webhook); + return Transformers.Webhook(this.client, webhook); } /** * Deletes a webhook. @@ -50,12 +49,12 @@ export class WebhookShorter extends BaseShorter { return this.client.proxy .webhooks(webhookId)(options.token) .patch({ body, reason: options.reason, auth: false }) - .then(webhook => new Webhook(this.client, webhook)); + .then(webhook => Transformers.Webhook(this.client, webhook)); } return this.client.proxy .webhooks(webhookId) .patch({ body, reason: options.reason }) - .then(webhook => new Webhook(this.client, webhook)); + .then(webhook => Transformers.Webhook(this.client, webhook)); } /** @@ -71,7 +70,7 @@ export class WebhookShorter extends BaseShorter { } else { webhook = await this.client.proxy.webhooks(webhookId).get(); } - return new Webhook(this.client, webhook); + return Transformers.Webhook(this.client, webhook); } /** @@ -92,7 +91,7 @@ export class WebhookShorter extends BaseShorter { return this.client.proxy .webhooks(webhookId)(token) .post({ ...payload, files: parsedFiles, body: transformedBody }) - .then(m => (m?.id ? new WebhookMessage(this.client, m, webhookId, token) : null)); + .then(m => (m?.id ? Transformers.WebhookMessage(this.client, m, webhookId, token) : null)); } /** @@ -119,7 +118,7 @@ export class WebhookShorter extends BaseShorter { .webhooks(webhookId)(token) .messages(messageId) .patch({ ...json, auth: false, files: parsedFiles, body: transformedBody }) - .then(m => new WebhookMessage(this.client, m, webhookId, token)); + .then(m => Transformers.WebhookMessage(this.client, m, webhookId, token)); } /** @@ -147,17 +146,17 @@ export class WebhookShorter extends BaseShorter { .webhooks(webhookId)(token) .messages(messageId) .get({ auth: false, query: { threadId } }); - return message ? new WebhookMessage(this.client, message, webhookId, token) : undefined; + return message ? Transformers.WebhookMessage(this.client, message, webhookId, token) : undefined; } async listFromGuild(guildId: string) { const webhooks = await this.client.proxy.guilds(guildId).webhooks.get(); - return webhooks.map(webhook => new Webhook(this.client, webhook)); + return webhooks.map(webhook => Transformers.Webhook(this.client, webhook)); } async listFromChannel(channelId: string) { const webhooks = await this.client.proxy.channels(channelId).webhooks.get(); - return webhooks.map(webhook => new Webhook(this.client, webhook)); + return webhooks.map(webhook => Transformers.Webhook(this.client, webhook)); } } diff --git a/src/common/types/util.ts b/src/common/types/util.ts index d1fffb4..93aa5dd 100644 --- a/src/common/types/util.ts +++ b/src/common/types/util.ts @@ -79,6 +79,10 @@ export type IsStrictlyUndefined = AuxIsStrictlyUndefined extends true export type If = T extends true ? A : B extends null ? A | null : B; +export type NulleableCoalising = NonFalsy extends never ? B : A; + +export type TupleOr = ValueOf extends never ? A : TupleOr, Tail>; + export type PickPartial = { [P in keyof T]?: T[P] | undefined; } & { diff --git a/src/components/ButtonComponent.ts b/src/components/ButtonComponent.ts index e0ce105..5be0da4 100644 --- a/src/components/ButtonComponent.ts +++ b/src/components/ButtonComponent.ts @@ -1,14 +1,20 @@ -import type { ButtonStyle, ComponentType } from 'discord-api-types/v10'; -import { Button, type ButtonStylesForID } from '../builders'; +import type { + APIButtonComponentWithCustomId, + APIButtonComponentWithSKUId, + APIButtonComponentWithURL, + ButtonStyle, + ComponentType, +} from 'discord-api-types/v10'; +import { Button } from '../builders'; import { BaseComponent } from './BaseComponent'; export class LinkButtonComponent extends BaseComponent { + declare data: APIButtonComponentWithURL; get style() { - return this.data.style as ButtonStyle.Link; + return this.data.style; } get url(): string { - // @ts-expect-error return this.data.url; } @@ -25,15 +31,20 @@ export class LinkButtonComponent extends BaseComponent { } toBuilder() { - return new Button(this.data as never); + return new Button(this.data); } } export type ButtonStyleExludeLink = Exclude; export class ButtonComponent extends BaseComponent { + declare data: APIButtonComponentWithCustomId; get style() { - return this.data.style as ButtonStylesForID; + return this.data.style; + } + + get customId() { + return this.data.custom_id; } get label() { @@ -49,6 +60,25 @@ export class ButtonComponent extends BaseComponent { } toBuilder() { - return new Button(this.data as never); + return new Button(this.data); + } +} + +export class SKUButtonComponent extends BaseComponent { + declare data: APIButtonComponentWithSKUId; + get style() { + return this.data.style; + } + + get skuId() { + return this.data.sku_id; + } + + get disabled() { + return this.data.disabled; + } + + toBuilder() { + return new Button(this.data as never); } } diff --git a/src/components/componentcontext.ts b/src/components/componentcontext.ts index fba0f83..1b51ef8 100644 --- a/src/components/componentcontext.ts +++ b/src/components/componentcontext.ts @@ -4,15 +4,11 @@ import type { ButtonInteraction, ChannelSelectMenuInteraction, ComponentCommand, - Guild, - GuildMember, MentionableSelectMenuInteraction, - Message, ReturnCache, RoleSelectMenuInteraction, StringSelectMenuInteraction, UserSelectMenuInteraction, - WebhookMessage, } from '..'; import type { CommandMetadata, ExtendContext, GlobalMetadata, RegisteredMiddlewares, UsingClient } from '../commands'; import { BaseContext } from '../commands/basecontext'; @@ -24,6 +20,12 @@ import type { UnionToTuple, When, } from '../common'; +import type { + GuildMemberStructure, + GuildStructure, + MessageStructure, + WebhookMessageStructure, +} from '../client/transformers'; export interface ComponentContext< Type extends keyof ContextComponentCommandInteractionMap = keyof ContextComponentCommandInteractionMap, @@ -109,7 +111,7 @@ export class ComponentContext< editOrReply( body: InteractionCreateBodyRequest | InteractionMessageUpdateBodyRequest, fetchReply?: FR, - ): Promise> { + ): Promise> { return this.interaction.editOrReply(body as InteractionCreateBodyRequest, fetchReply); } @@ -143,8 +145,8 @@ export class ComponentContext< * @param mode - The mode to fetch the member. * @returns A promise that resolves to the bot member. */ - me(mode?: 'rest' | 'flow'): Promise; - me(mode?: 'cache'): ReturnCache; + me(mode?: 'rest' | 'flow'): Promise; + me(mode?: 'cache'): ReturnCache; me(mode: 'cache' | 'rest' | 'flow' = 'cache') { if (!this.guildId) return mode === 'cache' ? (this.client.cache.adapter.isAsync ? Promise.resolve() : undefined) : Promise.resolve(); @@ -161,8 +163,8 @@ export class ComponentContext< * @param mode - The mode to fetch the guild. * @returns A promise that resolves to the guild. */ - guild(mode?: 'rest' | 'flow'): Promise | undefined>; - guild(mode?: 'cache'): ReturnCache | undefined>; + guild(mode?: 'rest' | 'flow'): Promise | undefined>; + guild(mode?: 'cache'): ReturnCache | undefined>; guild(mode: 'cache' | 'rest' | 'flow' = 'cache') { if (!this.guildId) return ( diff --git a/src/components/handler.ts b/src/components/handler.ts index 88211da..04c07cd 100644 --- a/src/components/handler.ts +++ b/src/components/handler.ts @@ -1,6 +1,7 @@ import type { ComponentCallback, ListenerOptions, ModalSubmitCallback } from '../builders/types'; import { LimitedCollection } from '../collection'; import { BaseCommand, type RegisteredMiddlewares, type UsingClient } from '../commands'; +import type { FileLoaded } from '../commands/handler'; import { BaseHandler, magicImport, type Logger, type OnFailCallback } from '../common'; import type { ComponentInteraction, ModalSubmitInteraction, StringSelectMenuInteraction } from '../structures'; import { ComponentCommand, InteractionCommandType } from './componentcommand'; @@ -17,12 +18,15 @@ type COMPONENTS = { __run: (customId: string | string[] | RegExp, callback: ComponentCallback) => any; }; +export type CollectorInteraction = ComponentInteraction | StringSelectMenuInteraction; +export type ComponentCommands = ComponentCommand | ModalCommand; + export class ComponentHandler extends BaseHandler { onFail: OnFailCallback = err => this.logger.warn('.components.onFail', err); readonly values = new Map(); // 10 minutes timeout, because discord dont send an event when the user cancel the modal readonly modals = new LimitedCollection({ expire: 60e3 * 10 }); - readonly commands: (ComponentCommand | ModalCommand)[] = []; + readonly commands: ComponentCommands[] = []; protected filter = (path: string) => path.endsWith('.js') || (!path.endsWith('.d.ts') && path.endsWith('.ts')); constructor( @@ -36,9 +40,10 @@ export class ComponentHandler extends BaseHandler { messageId: string, options: ListenerOptions = {}, ): { - run< - T extends ComponentInteraction | StringSelectMenuInteraction = ComponentInteraction | StringSelectMenuInteraction, - >(customId: string | string[] | RegExp, callback: ComponentCallback): any; + run( + customId: string | string[] | RegExp, + callback: ComponentCallback, + ): any; stop(reason?: string): any; } { this.values.set(messageId, { @@ -149,38 +154,41 @@ export class ComponentHandler extends BaseHandler { this.deleteValue(id, 'messageDelete'); } - async load(componentsDir: string, instances?: { new (): ModalCommand | ComponentCommand }[]) { - const paths = - instances?.map(x => { - const i = new x(); - return { file: x, path: i.__filePath ?? '*' }; - }) ?? (await this.loadFilesK<{ new (): ModalCommand | ComponentCommand }>(await this.getFiles(componentsDir))); + stablishDefaults(component: ComponentCommands) { + component.props ??= this.client.options.commands?.defaults?.props ?? {}; + const is = component instanceof ModalCommand ? 'modals' : 'components'; + component.onInternalError ??= this.client.options?.[is]?.defaults?.onInternalError; + component.onMiddlewaresError ??= this.client.options?.[is]?.defaults?.onMiddlewaresError; + component.onRunError ??= this.client.options?.[is]?.defaults?.onRunError; + component.onAfterRun ??= this.client.options?.[is]?.defaults?.onAfterRun; + } - for (const value of paths) { - let component; - try { - component = this.callback(value.file); - if (!component) continue; - } catch (e) { - if (e instanceof Error && e.message.includes('is not a constructor')) { - this.logger.warn( - `${value.path - .split(process.cwd()) - .slice(1) - .join(process.cwd())} doesn't export the class by \`export default \``, - ); - } else this.logger.warn(e, value); - continue; + async load(componentsDir: string) { + const paths = await this.loadFilesK ComponentCommands>>(await this.getFiles(componentsDir)); + + for (const { components, file } of paths.map(x => ({ components: this.onFile(x.file), file: x }))) { + if (!components) continue; + for (const value of components) { + let component; + try { + component = this.callback(value); + if (!component) continue; + } catch (e) { + if (e instanceof Error && e.message.includes('is not a constructor')) { + this.logger.warn( + `${file.path + .split(process.cwd()) + .slice(1) + .join(process.cwd())} doesn't export the class by \`export default \``, + ); + } else this.logger.warn(e, value); + continue; + } + if (!(component instanceof ModalCommand || component instanceof ComponentCommand)) continue; + this.stablishDefaults(component); + component.__filePath = file.path; + this.commands.push(component); } - if (!(component instanceof ModalCommand || component instanceof ComponentCommand)) continue; - component.props ??= this.client.options.commands?.defaults?.props ?? {}; - const is = component instanceof ModalCommand ? 'modals' : 'components'; - component.onInternalError ??= this.client.options?.[is]?.defaults?.onInternalError; - component.onMiddlewaresError ??= this.client.options?.[is]?.defaults?.onMiddlewaresError; - component.onRunError ??= this.client.options?.[is]?.defaults?.onRunError; - component.onAfterRun ??= this.client.options?.[is]?.defaults?.onAfterRun; - component.__filePath = value.path; - this.commands.push(component); } } @@ -217,6 +225,45 @@ export class ComponentHandler extends BaseHandler { } } + async execute(i: ComponentCommands, context: ComponentContext | ModalContext) { + try { + const resultRunGlobalMiddlewares = await BaseCommand.__runMiddlewares( + context, + (context.client.options?.globalMiddlewares ?? []) as keyof RegisteredMiddlewares, + true, + ); + if (resultRunGlobalMiddlewares.pass) { + return; + } + if ('error' in resultRunGlobalMiddlewares) { + return i.onMiddlewaresError?.(context as never, resultRunGlobalMiddlewares.error ?? 'Unknown error'); + } + + const resultRunMiddlewares = await BaseCommand.__runMiddlewares(context, i.middlewares, false); + if (resultRunMiddlewares.pass) { + return; + } + if ('error' in resultRunMiddlewares) { + return i.onMiddlewaresError?.(context as never, resultRunMiddlewares.error ?? 'Unknown error'); + } + + try { + await i.run(context as never); + await i.onAfterRun?.(context as never, undefined); + } catch (error) { + await i.onRunError?.(context as never, error); + await i.onAfterRun?.(context as never, error); + } + } catch (error) { + try { + await i.onInternalError?.(this.client, error); + } catch (e) { + // supress error + this.logger.error(e); + } + } + } + async executeComponent(context: ComponentContext) { for (const i of this.commands) { try { @@ -226,42 +273,7 @@ export class ComponentHandler extends BaseHandler { (await i.filter(context)) ) { context.command = i; - try { - const resultRunGlobalMiddlewares = await BaseCommand.__runMiddlewares( - context, - (context.client.options?.globalMiddlewares ?? []) as keyof RegisteredMiddlewares, - true, - ); - if (resultRunGlobalMiddlewares.pass) { - return; - } - if ('error' in resultRunGlobalMiddlewares) { - return i.onMiddlewaresError?.(context, resultRunGlobalMiddlewares.error ?? 'Unknown error'); - } - - const resultRunMiddlewares = await BaseCommand.__runMiddlewares(context, i.middlewares, false); - if (resultRunMiddlewares.pass) { - return; - } - if ('error' in resultRunMiddlewares) { - return i.onMiddlewaresError?.(context, resultRunMiddlewares.error ?? 'Unknown error'); - } - - try { - await i.run(context); - await i.onAfterRun?.(context, undefined); - } catch (error) { - await i.onRunError?.(context, error); - await i.onAfterRun?.(context, error); - } - } catch (error) { - try { - await i.onInternalError?.(this.client, error); - } catch { - // supress error - } - } - break; + await this.execute(i, context); } } catch (e) { await this.onFail(e); @@ -274,42 +286,7 @@ export class ComponentHandler extends BaseHandler { try { if (i.type === InteractionCommandType.MODAL && (await i.filter(context))) { context.command = i; - try { - const resultRunGlobalMiddlewares = await BaseCommand.__runMiddlewares( - context, - (context.client.options?.globalMiddlewares ?? []) as keyof RegisteredMiddlewares, - true, - ); - if (resultRunGlobalMiddlewares.pass) { - return; - } - if ('error' in resultRunGlobalMiddlewares) { - return i.onMiddlewaresError?.(context, resultRunGlobalMiddlewares.error ?? 'Unknown error'); - } - - const resultRunMiddlewares = await BaseCommand.__runMiddlewares(context, i.middlewares, false); - if (resultRunMiddlewares.pass) { - return; - } - if ('error' in resultRunMiddlewares) { - return i.onMiddlewaresError?.(context, resultRunMiddlewares.error ?? 'Unknown error'); - } - - try { - await i.run(context); - await i.onAfterRun?.(context, undefined); - } catch (error) { - await i.onRunError?.(context, error); - await i.onAfterRun?.(context, error); - } - } catch (error) { - try { - await i.onInternalError?.(this.client, error); - } catch { - // supress error - } - } - break; + await this.execute(i, context); } } catch (e) { await this.onFail(e); @@ -317,9 +294,11 @@ export class ComponentHandler extends BaseHandler { } } - setHandlers({ callback }: { callback: ComponentHandler['callback'] }) { - this.callback = callback; + onFile(file: FileLoaded ComponentCommands>): (new () => ComponentCommands)[] | undefined { + return file.default ? [file.default] : undefined; } - callback = (file: { new (): ModalCommand | ComponentCommand }): ModalCommand | ComponentCommand | false => new file(); + callback(file: { new (): ComponentCommands }): ComponentCommands | false { + return new file(); + } } diff --git a/src/components/index.ts b/src/components/index.ts index d7476a9..692d149 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,6 +1,6 @@ import { type APIMessageActionRowComponent, ButtonStyle, ComponentType } from 'discord-api-types/v10'; import { BaseComponent } from './BaseComponent'; -import { ButtonComponent, LinkButtonComponent } from './ButtonComponent'; +import { ButtonComponent, LinkButtonComponent, SKUButtonComponent } from './ButtonComponent'; import { ChannelSelectMenuComponent } from './ChannelSelectMenuComponent'; import { MentionableSelectMenuComponent } from './MentionableSelectMenuComponent'; import { RoleSelectMenuComponent } from './RoleSelectMenuComponent'; @@ -11,6 +11,7 @@ import { UserSelectMenuComponent } from './UserSelectMenuComponent'; export type MessageComponents = | ButtonComponent | LinkButtonComponent + | SKUButtonComponent | RoleSelectMenuComponent | UserSelectMenuComponent | StringSelectMenuComponent @@ -39,6 +40,9 @@ export function componentFactory( if (component.style === ButtonStyle.Link) { return new LinkButtonComponent(component); } + if (component.style === ButtonStyle.Premium) { + return new SKUButtonComponent(component); + } return new ButtonComponent(component); case ComponentType.ChannelSelect: return new ChannelSelectMenuComponent(component); diff --git a/src/components/modalcontext.ts b/src/components/modalcontext.ts index e72498e..8861436 100644 --- a/src/components/modalcontext.ts +++ b/src/components/modalcontext.ts @@ -1,14 +1,5 @@ import { MessageFlags } from 'discord-api-types/v10'; -import type { - AllChannels, - Guild, - GuildMember, - Message, - ModalCommand, - ModalSubmitInteraction, - ReturnCache, - WebhookMessage, -} from '..'; +import type { AllChannels, ModalCommand, ModalSubmitInteraction, ReturnCache } from '..'; import type { CommandMetadata, ExtendContext, GlobalMetadata, RegisteredMiddlewares, UsingClient } from '../commands'; import { BaseContext } from '../commands/basecontext'; import type { @@ -18,6 +9,12 @@ import type { UnionToTuple, When, } from '../common'; +import type { + GuildMemberStructure, + GuildStructure, + MessageStructure, + WebhookMessageStructure, +} from '../client/transformers'; export interface ModalContext extends BaseContext, ExtendContext {} @@ -90,7 +87,7 @@ export class ModalContext extends editOrReply( body: InteractionCreateBodyRequest | InteractionMessageUpdateBodyRequest, fetchReply?: FR, - ): Promise> { + ): Promise> { return this.interaction.editOrReply(body as InteractionCreateBodyRequest, fetchReply); } @@ -125,8 +122,8 @@ export class ModalContext extends * @param mode - The mode to fetch the member. * @returns A promise that resolves to the bot member. */ - me(mode?: 'rest' | 'flow'): Promise; - me(mode?: 'cache'): ReturnCache; + me(mode?: 'rest' | 'flow'): Promise; + me(mode?: 'cache'): ReturnCache; me(mode: 'cache' | 'rest' | 'flow' = 'cache') { if (!this.guildId) return mode === 'cache' ? (this.client.cache.adapter.isAsync ? Promise.resolve() : undefined) : Promise.resolve(); @@ -143,8 +140,8 @@ export class ModalContext extends * @param mode - The mode to fetch the guild. * @returns A promise that resolves to the guild. */ - guild(mode?: 'rest' | 'flow'): Promise | undefined>; - guild(mode?: 'cache'): ReturnCache | undefined>; + guild(mode?: 'rest' | 'flow'): Promise | undefined>; + guild(mode?: 'cache'): ReturnCache | undefined>; guild(mode: 'cache' | 'rest' | 'flow' = 'cache') { if (!this.guildId) return ( diff --git a/src/events/handler.ts b/src/events/handler.ts index cb3fee9..1bfc1d5 100644 --- a/src/events/handler.ts +++ b/src/events/handler.ts @@ -9,6 +9,7 @@ import { BaseHandler, ReplaceRegex, magicImport, type MakeRequired, type SnakeCa import type { ClientEvents } from '../events/hooks'; import * as RawEvents from '../events/hooks'; import type { ClientEvent, CustomEvents, CustomEventsKeys, ClientNameEvents } from './event'; +import type { FileLoaded } from '../commands/handler'; export type EventValue = MakeRequired & { fired?: boolean }; @@ -25,25 +26,29 @@ export class EventHandler extends BaseHandler { values: Partial> = {}; - async load(eventsDir: string, instances?: { file: ClientEvent; path: string }[]) { + async load(eventsDir: string) { const discordEvents = Object.keys(RawEvents).map(x => ReplaceRegex.camel(x.toLowerCase())) as ClientNameEvents[]; + const paths = await this.loadFilesK<{ file: ClientEvent }>(await this.getFiles(eventsDir)); - for (const i of instances ?? (await this.loadFilesK(await this.getFiles(eventsDir)))) { - const instance = this.callback(i.file); - if (!instance) continue; - if (typeof instance?.run !== 'function') { - this.logger.warn( - i.path.split(process.cwd()).slice(1).join(process.cwd()), - 'Missing run function, use `export default {...}` syntax', - ); - continue; + for (const { events, file } of paths.map(x => ({ events: this.onFile(x.file), file: x }))) { + if (!events) continue; + for (const i of events) { + const instance = this.callback(i); + if (!instance) continue; + if (typeof instance?.run !== 'function') { + this.logger.warn( + file.path.split(process.cwd()).slice(1).join(process.cwd()), + 'Missing run function, use `export default {...}` syntax', + ); + continue; + } + instance.__filePath = file.path; + this.values[ + discordEvents.includes(instance.data.name) + ? (ReplaceRegex.snake(instance.data.name).toUpperCase() as GatewayEvents) + : (instance.data.name as CustomEventsKeys) + ] = instance as EventValue; } - instance.__filePath = i.path; - this.values[ - discordEvents.includes(instance.data.name) - ? (ReplaceRegex.snake(instance.data.name).toUpperCase() as GatewayEvents) - : (instance.data.name as CustomEventsKeys) - ] = instance as EventValue; } } @@ -53,7 +58,7 @@ export class EventHandler extends BaseHandler { { const { d: data } = args[0] as GatewayMessageCreateDispatch; if (args[1].components?.values.has(data.interaction_metadata?.id ?? data.id)) { - args[1].components.values.get(data.interaction_metadata?.id ?? data.id)!.messageId = data.id; + args[1].components.values.get(data.interaction_metadata!.id ?? data.id)!.messageId = data.id; } } break; @@ -81,29 +86,33 @@ export class EventHandler extends BaseHandler { } await Promise.all([ - this.runEvent(args[0].t, args[1], args[0].d, args[2]), - this.client.collectors.run(args[0].t, args[0].d), + this.runEvent(args[0].t as never, args[1], args[0].d, args[2]), + this.client.collectors.run(args[0].t as never, args[0].d as never), ]); } - async runEvent(name: GatewayEvents, client: Client | WorkerClient, packet: any, shardId: number) { + async runEvent(name: GatewayEvents, client: Client | WorkerClient, packet: any, shardId: number, runCache = true) { const Event = this.values[name]; if (!Event) { - return this.client.cache.onPacket({ - t: name, - d: packet, - } as GatewayDispatchPayload); + return runCache + ? this.client.cache.onPacket({ + t: name, + d: packet, + } as GatewayDispatchPayload) + : undefined; } try { if (Event.data.once && Event.fired) { - return this.client.cache.onPacket({ - t: name, - d: packet, - } as GatewayDispatchPayload); + return runCache + ? this.client.cache.onPacket({ + t: name, + d: packet, + } as GatewayDispatchPayload) + : undefined; } Event.fired = true; const hook = await RawEvents[name]?.(client, packet as never); - if (name !== 'RAW') + if (runCache) await this.client.cache.onPacket({ t: name, d: packet, @@ -153,8 +162,8 @@ export class EventHandler extends BaseHandler { } } - setHandlers({ callback }: { callback: EventHandler['callback'] }) { - this.callback = callback; + onFile(file: FileLoaded): ClientEvent[] | undefined { + return file.default ? [file.default] : undefined; } callback = (file: ClientEvent): ClientEvent | false => file; diff --git a/src/events/hooks/auto_moderation.ts b/src/events/hooks/auto_moderation.ts index a7c21a3..ae561de 100644 --- a/src/events/hooks/auto_moderation.ts +++ b/src/events/hooks/auto_moderation.ts @@ -5,8 +5,8 @@ import type { GatewayAutoModerationRuleUpdateDispatchData, } from 'discord-api-types/v10'; import { toCamelCase } from '../../common'; -import { AutoModerationRule } from '../../structures'; import type { UsingClient } from '../../commands'; +import { Transformers } from '../../client/transformers'; export const AUTO_MODERATION_ACTION_EXECUTION = ( _self: UsingClient, @@ -16,13 +16,13 @@ export const AUTO_MODERATION_ACTION_EXECUTION = ( }; export const AUTO_MODERATION_RULE_CREATE = (self: UsingClient, data: GatewayAutoModerationRuleCreateDispatchData) => { - return new AutoModerationRule(self, data); + return Transformers.AutoModerationRule(self, data); }; export const AUTO_MODERATION_RULE_DELETE = (self: UsingClient, data: GatewayAutoModerationRuleDeleteDispatchData) => { - return new AutoModerationRule(self, data); + return Transformers.AutoModerationRule(self, data); }; export const AUTO_MODERATION_RULE_UPDATE = (self: UsingClient, data: GatewayAutoModerationRuleUpdateDispatchData) => { - return new AutoModerationRule(self, data); + return Transformers.AutoModerationRule(self, data); }; diff --git a/src/events/hooks/custom.ts b/src/events/hooks/custom.ts index cb003ed..c97199a 100644 --- a/src/events/hooks/custom.ts +++ b/src/events/hooks/custom.ts @@ -1,10 +1,10 @@ +import type { ClientUserStructure } from '../../client/transformers'; import type { UsingClient } from '../../commands'; -import type { ClientUser } from '../../structures'; -export const BOT_READY = (_self: UsingClient, me: ClientUser) => { +export const BOT_READY = (_self: UsingClient, me: ClientUserStructure) => { return me; }; -export const WORKER_READY = (_self: UsingClient, me: ClientUser) => { +export const WORKER_READY = (_self: UsingClient, me: ClientUserStructure) => { return me; }; diff --git a/src/events/hooks/dispatch.ts b/src/events/hooks/dispatch.ts index f1894d4..8504cdb 100644 --- a/src/events/hooks/dispatch.ts +++ b/src/events/hooks/dispatch.ts @@ -1,9 +1,9 @@ import type { GatewayDispatchPayload, GatewayReadyDispatchData, GatewayResumedDispatch } from 'discord-api-types/v10'; -import { ClientUser } from '../../structures'; import type { UsingClient } from '../../commands'; +import { Transformers } from '../../client/transformers'; export const READY = (self: UsingClient, data: GatewayReadyDispatchData) => { - return new ClientUser(self, data.user, data.application); + return Transformers.ClientUser(self, data.user, data.application); }; export const RESUMED = (_self: UsingClient, _data: GatewayResumedDispatch['d']) => { diff --git a/src/events/hooks/guild.ts b/src/events/hooks/guild.ts index 4e2eaf1..4538702 100644 --- a/src/events/hooks/guild.ts +++ b/src/events/hooks/guild.ts @@ -22,23 +22,28 @@ import type { GatewayGuildUpdateDispatchData, } from 'discord-api-types/v10'; import { toCamelCase } from '../../common'; -import { Guild, GuildEmoji, GuildMember, GuildRole, Sticker, User } from '../../structures'; import type { UsingClient } from '../../commands'; +import { + type GuildMemberStructure, + type GuildRoleStructure, + type GuildStructure, + Transformers, +} from '../../client/transformers'; export const GUILD_AUDIT_LOG_ENTRY_CREATE = (_self: UsingClient, data: GatewayGuildAuditLogEntryCreateDispatchData) => { return toCamelCase(data); }; export const GUILD_BAN_ADD = (self: UsingClient, data: GatewayGuildBanAddDispatchData) => { - return { ...toCamelCase(data), user: new User(self, data.user) }; + return { ...toCamelCase(data), user: Transformers.User(self, data.user) }; }; export const GUILD_BAN_REMOVE = (self: UsingClient, data: GatewayGuildBanRemoveDispatchData) => { - return { ...toCamelCase(data), user: new User(self, data.user) }; + return { ...toCamelCase(data), user: Transformers.User(self, data.user) }; }; export const GUILD_CREATE = (self: UsingClient, data: GatewayGuildCreateDispatchData) => { - return new Guild<'create'>(self, data); + return Transformers.Guild<'create'>(self, data); }; export const GUILD_DELETE = async (self: UsingClient, data: GatewayGuildDeleteDispatchData) => { @@ -48,7 +53,7 @@ export const GUILD_DELETE = async (self: UsingClient, data: GatewayGuildDeleteDi export const GUILD_EMOJIS_UPDATE = (self: UsingClient, data: GatewayGuildEmojisUpdateDispatchData) => { return { ...toCamelCase(data), - emojis: data.emojis.map(x => new GuildEmoji(self, x, data.guild_id)), + emojis: data.emojis.map(x => Transformers.GuildEmoji(self, x, data.guild_id)), }; }; @@ -57,26 +62,26 @@ export const GUILD_INTEGRATIONS_UPDATE = (_self: UsingClient, data: GatewayGuild }; export const GUILD_MEMBER_ADD = (self: UsingClient, data: GatewayGuildMemberAddDispatchData) => { - return new GuildMember(self, data, data.user!, data.guild_id); + return Transformers.GuildMember(self, data, data.user!, data.guild_id); }; export const GUILD_MEMBER_REMOVE = (self: UsingClient, data: GatewayGuildMemberRemoveDispatchData) => { - return { ...toCamelCase(data), user: new User(self, data.user) }; + return { ...toCamelCase(data), user: Transformers.User(self, data.user) }; }; export const GUILD_MEMBERS_CHUNK = (self: UsingClient, data: GatewayGuildMembersChunkDispatchData) => { return { ...toCamelCase(data), - members: data.members.map(x => new GuildMember(self, x, x.user!, data.guild_id)), + members: data.members.map(x => Transformers.GuildMember(self, x, x.user!, data.guild_id)), }; }; export const GUILD_MEMBER_UPDATE = async ( self: UsingClient, data: GatewayGuildMemberUpdateDispatchData, -): Promise<[member: GuildMember, old?: GuildMember]> => { +): Promise<[member: GuildMemberStructure, old?: GuildMemberStructure]> => { const oldData = await self.cache.members?.get(data.user.id, data.guild_id); - return [new GuildMember(self, data, data.user, data.guild_id), oldData]; + return [Transformers.GuildMember(self, data, data.user, data.guild_id), oldData]; }; export const GUILD_SCHEDULED_EVENT_CREATE = ( @@ -115,7 +120,7 @@ export const GUILD_SCHEDULED_EVENT_USER_REMOVE = ( }; export const GUILD_ROLE_CREATE = (self: UsingClient, data: GatewayGuildRoleCreateDispatchData) => { - return new GuildRole(self, data.role, data.guild_id); + return Transformers.GuildRole(self, data.role, data.guild_id); }; export const GUILD_ROLE_DELETE = async (self: UsingClient, data: GatewayGuildRoleDeleteDispatchData) => { @@ -125,20 +130,20 @@ export const GUILD_ROLE_DELETE = async (self: UsingClient, data: GatewayGuildRol export const GUILD_ROLE_UPDATE = async ( self: UsingClient, data: GatewayGuildRoleUpdateDispatchData, -): Promise<[role: GuildRole, old?: GuildRole]> => { - return [new GuildRole(self, data.role, data.guild_id), await self.cache.roles?.get(data.role.id)]; +): Promise<[role: GuildRoleStructure, old?: GuildRoleStructure]> => { + return [Transformers.GuildRole(self, data.role, data.guild_id), await self.cache.roles?.get(data.role.id)]; }; export const GUILD_STICKERS_UPDATE = (self: UsingClient, data: GatewayGuildStickersUpdateDispatchData) => { return { ...toCamelCase(data), - stickers: data.stickers.map(x => new Sticker(self, x)), + stickers: data.stickers.map(x => Transformers.Sticker(self, x)), }; }; export const GUILD_UPDATE = async ( self: UsingClient, data: GatewayGuildUpdateDispatchData, -): Promise<[guild: Guild, old?: Guild<'cached'>]> => { - return [new Guild(self, data), await self.cache.guilds?.get(data.id)]; +): Promise<[guild: GuildStructure, old?: GuildStructure<'cached'>]> => { + return [Transformers.Guild(self, data), await self.cache.guilds?.get(data.id)]; }; diff --git a/src/events/hooks/integration.ts b/src/events/hooks/integration.ts index d76094c..862234d 100644 --- a/src/events/hooks/integration.ts +++ b/src/events/hooks/integration.ts @@ -4,14 +4,14 @@ import type { GatewayIntegrationUpdateDispatchData, } from 'discord-api-types/v10'; import { toCamelCase } from '../../common'; -import { User } from '../../structures'; import type { UsingClient } from '../../commands'; +import { Transformers } from '../../client/transformers'; export const INTEGRATION_CREATE = (self: UsingClient, data: GatewayIntegrationCreateDispatchData) => { return data.user ? { ...toCamelCase(data), - user: new User(self, data.user!), + user: Transformers.User(self, data.user!), } : toCamelCase(data); }; @@ -20,7 +20,7 @@ export const INTEGRATION_UPDATE = (self: UsingClient, data: GatewayIntegrationUp return data.user ? { ...toCamelCase(data), - user: new User(self, data.user!), + user: Transformers.User(self, data.user!), } : toCamelCase(data); }; diff --git a/src/events/hooks/message.ts b/src/events/hooks/message.ts index 7b1f6ee..860273a 100644 --- a/src/events/hooks/message.ts +++ b/src/events/hooks/message.ts @@ -11,17 +11,17 @@ import type { GatewayMessageUpdateDispatchData, } from 'discord-api-types/v10'; import { type MakeRequired, type PartialClass, toCamelCase, type ObjectToLower } from '../../common'; -import { Message } from '../../structures'; import type { UsingClient } from '../../commands'; +import { type MessageStructure, Transformers } from '../../client/transformers'; export const MESSAGE_CREATE = (self: UsingClient, data: GatewayMessageCreateDispatchData) => { - return new Message(self, data); + return Transformers.Message(self, data); }; export const MESSAGE_DELETE = async ( self: UsingClient, data: GatewayMessageDeleteDispatchData, -): Promise> => { +): Promise> => { return (await self.cache.messages?.get(data.id)) ?? toCamelCase(data); }; @@ -57,7 +57,7 @@ export const MESSAGE_UPDATE = async ( ): Promise< [ message: MakeRequired< - PartialClass, + PartialClass, //sus | 'id' | 'channelId' | 'createdAt' @@ -71,14 +71,10 @@ export const MESSAGE_UPDATE = async ( | 'user' | 'author' >, - old: undefined | Message, + old: undefined | MessageStructure, ] > => { - return [new Message(self, data as APIMessage), await self.cache.messages?.get(data.id)]; -}; - -export const MESSAGE_POLL_VOTE_ADD = (_: UsingClient, data: GatewayMessagePollVoteDispatchData) => { - return toCamelCase(data); + return [Transformers.Message(self, data as APIMessage), await self.cache.messages?.get(data.id)]; }; export const MESSAGE_POLL_VOTE_REMOVE = (_: UsingClient, data: GatewayMessagePollVoteDispatchData) => { diff --git a/src/events/hooks/presence.ts b/src/events/hooks/presence.ts index d5f9bcc..e0b7491 100644 --- a/src/events/hooks/presence.ts +++ b/src/events/hooks/presence.ts @@ -3,5 +3,5 @@ import type { UsingClient } from '../../commands'; import { toCamelCase } from '../../common'; export const PRESENCE_UPDATE = async (self: UsingClient, data: GatewayPresenceUpdateDispatchData) => { - return [toCamelCase(data), await self.cache.presences?.get(data.user.id)]; + return [toCamelCase(data), await self.cache.presences?.get(data.user.id)] as const; }; diff --git a/src/events/hooks/thread.ts b/src/events/hooks/thread.ts index ce2348d..023de54 100644 --- a/src/events/hooks/thread.ts +++ b/src/events/hooks/thread.ts @@ -7,15 +7,15 @@ import type { GatewayThreadUpdateDispatchData, } from 'discord-api-types/v10'; import { toCamelCase } from '../../common'; -import { ThreadChannel } from '../../structures'; import type { UsingClient } from '../../commands'; +import { type ThreadChannelStructure, Transformers } from '../../client/transformers'; export const THREAD_CREATE = (self: UsingClient, data: GatewayThreadCreateDispatchData) => { - return new ThreadChannel(self, data); + return Transformers.ThreadChannel(self, data); }; export const THREAD_DELETE = (self: UsingClient, data: GatewayThreadDeleteDispatchData) => { - return new ThreadChannel(self, data); + return Transformers.ThreadChannel(self, data); }; export const THREAD_LIST_SYNC = (_self: UsingClient, data: GatewayThreadListSyncDispatchData) => { @@ -33,6 +33,6 @@ export const THREAD_MEMBERS_UPDATE = (_self: UsingClient, data: GatewayThreadMem export const THREAD_UPDATE = async ( self: UsingClient, data: GatewayThreadUpdateDispatchData, -): Promise<[thread: ThreadChannel, old?: ThreadChannel]> => { - return [new ThreadChannel(self, data), await self.cache.threads?.get(data.id)]; +): Promise<[thread: ThreadChannelStructure, old?: ThreadChannelStructure]> => { + return [Transformers.ThreadChannel(self, data), await self.cache.threads?.get(data.id)]; }; diff --git a/src/events/hooks/typing.ts b/src/events/hooks/typing.ts index 86915e2..9be61a3 100644 --- a/src/events/hooks/typing.ts +++ b/src/events/hooks/typing.ts @@ -1,13 +1,13 @@ import type { GatewayTypingStartDispatchData } from 'discord-api-types/v10'; import { toCamelCase } from '../../common'; -import { GuildMember } from '../../structures'; import type { UsingClient } from '../../commands'; +import { Transformers } from '../../client/transformers'; export const TYPING_START = (self: UsingClient, data: GatewayTypingStartDispatchData) => { return data.member ? { ...toCamelCase(data), - member: new GuildMember(self, data.member, data.member.user!, data.guild_id!), + member: Transformers.GuildMember(self, data.member, data.member.user!, data.guild_id!), } : toCamelCase(data); }; diff --git a/src/events/hooks/user.ts b/src/events/hooks/user.ts index 107073c..77b3ad0 100644 --- a/src/events/hooks/user.ts +++ b/src/events/hooks/user.ts @@ -1,10 +1,10 @@ import type { GatewayUserUpdateDispatchData } from 'discord-api-types/v10'; -import { User } from '../../structures'; import type { UsingClient } from '../../commands'; +import { Transformers, type UserStructure } from '../../client/transformers'; export const USER_UPDATE = async ( self: UsingClient, data: GatewayUserUpdateDispatchData, -): Promise<[user: User, old?: User]> => { - return [new User(self, data), await self.cache.users?.get(data.id)]; +): Promise<[user: UserStructure, old?: UserStructure]> => { + return [Transformers.User(self, data), await self.cache.users?.get(data.id)]; }; diff --git a/src/events/hooks/voice.ts b/src/events/hooks/voice.ts index 327f836..bc4608d 100644 --- a/src/events/hooks/voice.ts +++ b/src/events/hooks/voice.ts @@ -1,7 +1,7 @@ import type { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData } from '../../types'; import { toCamelCase } from '../../common'; -import { VoiceState } from '../../structures'; import type { UsingClient } from '../../commands'; +import { Transformers, type VoiceStateStructure } from '../../client/transformers'; export const VOICE_SERVER_UPDATE = (_self: UsingClient, data: GatewayVoiceServerUpdateDispatchData) => { return toCamelCase(data); @@ -10,7 +10,7 @@ export const VOICE_SERVER_UPDATE = (_self: UsingClient, data: GatewayVoiceServer export const VOICE_STATE_UPDATE = async ( self: UsingClient, data: GatewayVoiceStateUpdateDispatchData, -): Promise<[VoiceState] | [state: VoiceState, old?: VoiceState]> => { - if (!data.guild_id) return [new VoiceState(self, data)]; - return [new VoiceState(self, data), await self.cache.voiceStates?.get(data.user_id, data.guild_id)]; +): Promise<[state: VoiceStateStructure] | [state: VoiceStateStructure, old?: VoiceStateStructure]> => { + if (!data.guild_id) return [Transformers.VoiceState(self, data)]; + return [Transformers.VoiceState(self, data), await self.cache.voiceStates?.get(data.user_id, data.guild_id)]; }; diff --git a/src/index.ts b/src/index.ts index 41de3f3..427ec29 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,7 +26,7 @@ export { ShardManager, WorkerManager } from './websocket/discord'; export * from './structures'; // export * from './client'; -// +/// export function throwError(msg: string): never { throw new Error(msg); diff --git a/src/structures/GuildMember.ts b/src/structures/GuildMember.ts index 7a247b7..c532309 100644 --- a/src/structures/GuildMember.ts +++ b/src/structures/GuildMember.ts @@ -23,8 +23,8 @@ import type { UsingClient } from '../commands'; import { Formatter, type MessageCreateBodyRequest, type ObjectToLower, type ToClass } from '../common'; import type { ImageOptions, MethodContext } from '../common/types/options'; import type { GuildMemberResolvable } from '../common/types/resolvables'; -import { User } from './User'; import { PermissionsBitField } from './extra/Permissions'; +import { Transformers, type UserStructure } from '../client/transformers'; export interface BaseGuildMember extends DiscordBase, ObjectToLower> {} export class BaseGuildMember extends DiscordBase { @@ -127,17 +127,17 @@ export interface GuildMember extends ObjectToLower extends DiscordBase { - user: User; - member!: When; + user: UserStructure; + member!: When; channel?: AllChannels; - message?: Message; + message?: MessageStructure; replied?: Promise | boolean; appPermissions?: PermissionsBitField; @@ -96,15 +102,15 @@ export class BaseInteraction< ) { super(client, interaction); if (interaction.member) { - this.member = new InteractionGuildMember( + this.member = Transformers.InteractionGuildMember( client, interaction.member, - interaction.member!.user, + interaction.member.user, interaction.guild_id!, ) as never; } if (interaction.message) { - this.message = new Message(client, interaction.message); + this.message = Transformers.Message(client, interaction.message); } if (interaction.app_permissions) { this.appPermissions = new PermissionsBitField(Number(interaction.app_permissions)); @@ -112,7 +118,7 @@ export class BaseInteraction< if (interaction.channel) { this.channel = channelFrom(interaction.channel, client); } - this.user = this.member?.user ?? new User(client, interaction.user!); + this.user = this.member?.user ?? Transformers.User(client, interaction.user!); } static transformBodyRequest( @@ -315,17 +321,17 @@ export class AutocompleteInteraction extend > { declare type: InteractionType.ApplicationCommandAutocomplete; declare data: ObjectToLower; - options: OptionResolver; + options: OptionResolverStructure; constructor( client: UsingClient, interaction: APIApplicationCommandAutocompleteInteraction, - resolver?: OptionResolver, + resolver?: OptionResolverStructure, protected __reply?: __InternalReplyFunction, ) { super(client, interaction); this.options = resolver ?? - new OptionResolver( + Transformers.OptionResolver( client, interaction.data.options, undefined, @@ -363,7 +369,7 @@ export class Interaction< async write( body: InteractionCreateBodyRequest, fetchReply?: FR, - ): Promise> { + ): Promise> { (await this.reply({ type: InteractionResponseType.ChannelMessageWithSource, data: body, @@ -382,7 +388,7 @@ export class Interaction< async editOrReply( body: InteractionCreateBodyRequest, fetchReply?: FR, - ): Promise>; + ): Promise>; async editOrReply(body: InteractionMessageUpdateBodyRequest, fetchReply?: FR) { if (await this.replied) { const { content, embeds, allowed_mentions, components, files, attachments } = body; @@ -444,7 +450,7 @@ export class ComponentInteraction< declare channelId: string; declare channel: AllChannels; declare type: InteractionType.MessageComponent; - declare message: Message; + declare message: MessageStructure; update(data: ComponentInteractionMessageUpdate) { return this.reply({ @@ -536,9 +542,9 @@ export class ChannelSelectMenuInteraction extends SelectMenuInteraction { } export class MentionableSelectMenuInteraction extends SelectMenuInteraction { - roles: GuildRole[]; - members: InteractionGuildMember[]; - users: User[]; + roles: GuildRoleStructure[]; + members: InteractionGuildMemberStructure[]; + users: UserStructure[]; constructor( client: UsingClient, interaction: APIMessageComponentSelectMenuInteraction, @@ -547,25 +553,24 @@ export class MentionableSelectMenuInteraction extends SelectMenuInteraction { super(client, interaction); const resolved = (interaction.data as APIMessageMentionableSelectInteractionData).resolved; this.roles = resolved.roles - ? this.values.map(x => new GuildRole(this.client, resolved.roles![x], this.guildId!)) + ? this.values.map(x => Transformers.GuildRole(this.client, resolved.roles![x], this.guildId!)) : []; this.members = resolved.members - ? this.values.map( - x => - new InteractionGuildMember( - this.client, - resolved.members![x], - this.users!.find(u => u.id === x)!, - this.guildId!, - ), + ? this.values.map(x => + Transformers.InteractionGuildMember( + this.client, + resolved.members![x], + resolved.users![this.values!.find(u => u === x)!]!, + this.guildId!, + ), ) : []; - this.users = resolved.users ? this.values.map(x => new User(this.client, resolved.users![x])) : []; + this.users = resolved.users ? this.values.map(x => Transformers.User(this.client, resolved.users![x])) : []; } } export class RoleSelectMenuInteraction extends SelectMenuInteraction { - roles: GuildRole[]; + roles: GuildRoleStructure[]; constructor( client: UsingClient, interaction: APIMessageComponentSelectMenuInteraction, @@ -573,13 +578,13 @@ export class RoleSelectMenuInteraction extends SelectMenuInteraction { ) { super(client, interaction); const resolved = (interaction.data as APIMessageRoleSelectInteractionData).resolved; - this.roles = this.values.map(x => new GuildRole(this.client, resolved.roles[x], this.guildId!)); + this.roles = this.values.map(x => Transformers.GuildRole(this.client, resolved.roles[x], this.guildId!)); } } export class UserSelectMenuInteraction extends SelectMenuInteraction { - members: InteractionGuildMember[]; - users: User[]; + members: InteractionGuildMemberStructure[]; + users: UserStructure[]; constructor( client: UsingClient, interaction: APIMessageComponentSelectMenuInteraction, @@ -587,16 +592,15 @@ export class UserSelectMenuInteraction extends SelectMenuInteraction { ) { super(client, interaction); const resolved = (interaction.data as APIMessageUserSelectInteractionData).resolved; - this.users = this.values.map(x => new User(this.client, resolved.users[x])); + this.users = this.values.map(x => Transformers.User(this.client, resolved.users[x])); this.members = resolved.members - ? this.values.map( - x => - new InteractionGuildMember( - this.client, - resolved.members![x], - this.users!.find(u => u.id === x)!, - this.guildId!, - ), + ? this.values.map(x => + Transformers.InteractionGuildMember( + this.client, + resolved.members![x], + resolved.users[this.values!.find(u => u === x)!]!, + this.guildId!, + ), ) : []; } diff --git a/src/structures/Message.ts b/src/structures/Message.ts index f4d5d7f..8e683f0 100644 --- a/src/structures/Message.ts +++ b/src/structures/Message.ts @@ -13,12 +13,16 @@ import type { EmojiResolvable } from '../common/types/resolvables'; import type { MessageCreateBodyRequest, MessageUpdateBodyRequest } from '../common/types/write'; import type { ActionRowMessageComponents } from '../components'; import { MessageActionRowComponent } from '../components/ActionRow'; -import { GuildMember } from './GuildMember'; -import { User } from './User'; import type { MessageWebhookMethodEditParams, MessageWebhookMethodWriteParams } from './Webhook'; import { DiscordBase } from './extra/DiscordBase'; import { messageLink } from './extra/functions'; -import { Embed, Poll } from '..'; +import { Embed } from '..'; +import { + type PollStructure, + Transformers, + type GuildMemberStructure, + type UserStructure, +} from '../client/transformers'; export type MessageData = APIMessage | GatewayMessageCreateDispatchData; @@ -27,14 +31,14 @@ export interface BaseMessage ObjectToLower> { timestamp?: number; guildId?: string; - author: User; - member?: GuildMember; + author: UserStructure; + member?: GuildMemberStructure; components: MessageActionRowComponent[]; - poll?: Poll; + poll?: PollStructure; mentions: { roles: string[]; channels: APIChannelMention[]; - users: (GuildMember | User)[]; + users: (GuildMemberStructure | UserStructure)[]; }; } export class BaseMessage extends DiscordBase { @@ -83,32 +87,31 @@ export class BaseMessage extends DiscordBase { } if ('author' in data && data.author) { - this.author = new User(this.client, data.author); + this.author = Transformers.User(this.client, data.author); } if ('member' in data && data.member) { - this.member = new GuildMember(this.client, data.member, this.author, this.guildId!); + this.member = Transformers.GuildMember(this.client, data.member, data.author, this.guildId!); } if (data.mentions?.length) { this.mentions.users = this.guildId - ? data.mentions.map( - m => - new GuildMember( - this.client, - { - ...(m as APIUser & { member?: Omit }).member!, - user: m, - }, - m, - this.guildId!, - ), + ? data.mentions.map(m => + Transformers.GuildMember( + this.client, + { + ...(m as APIUser & { member?: Omit }).member!, + user: m, + }, + m, + this.guildId!, + ), ) - : data.mentions.map(u => new User(this.client, u)); + : data.mentions.map(u => Transformers.User(this.client, u)); } if (data.poll) { - this.poll = new Poll(this.client, data.poll, this.channelId, this.id); + this.poll = Transformers.Poll(this.client, data.poll, this.channelId, this.id); } } } diff --git a/src/structures/Sticker.ts b/src/structures/Sticker.ts index e1bb43b..633e6c0 100644 --- a/src/structures/Sticker.ts +++ b/src/structures/Sticker.ts @@ -6,17 +6,17 @@ import type { import type { RawFile, UsingClient } from '..'; import type { Attachment, AttachmentBuilder } from '../builders'; import type { MethodContext, ObjectToLower } from '../common'; -import { User } from './User'; import { DiscordBase } from './extra/DiscordBase'; +import { Transformers, type UserStructure } from '../client/transformers'; export interface Sticker extends DiscordBase, ObjectToLower> {} export class Sticker extends DiscordBase { - user?: User; + user?: UserStructure; constructor(client: UsingClient, data: APISticker) { super(client, data); if (data.user) { - this.user = new User(this.client, data.user); + this.user = Transformers.User(this.client, data.user); } } diff --git a/src/structures/VoiceState.ts b/src/structures/VoiceState.ts index ae2a2d2..6ed6c4c 100644 --- a/src/structures/VoiceState.ts +++ b/src/structures/VoiceState.ts @@ -1,5 +1,6 @@ -import { GuildMember, type UsingClient } from '../'; +import type { UsingClient } from '../'; import type { VoiceStateResource } from '../cache/resources/voice-states'; +import { type GuildMemberStructure, Transformers } from '../client/transformers'; import type { ObjectToLower } from '../common'; import type { GatewayVoiceState } from '../types'; import { Base } from './extra/Base'; @@ -7,12 +8,13 @@ import { Base } from './extra/Base'; export interface VoiceState extends Base, ObjectToLower> {} export class VoiceState extends Base { - protected withMember?: GuildMember; + protected withMember?: GuildMemberStructure; constructor(client: UsingClient, data: GatewayVoiceState) { super(client); const { member, ...rest } = data; this.__patchThis(rest); - if (member?.user && data.guild_id) this.withMember = new GuildMember(client, member, member.user, data.guild_id); + if (member?.user && data.guild_id) + this.withMember = Transformers.GuildMember(client, member, member.user, data.guild_id); } isMuted() { diff --git a/src/structures/Webhook.ts b/src/structures/Webhook.ts index 76e69cd..139cd4c 100644 --- a/src/structures/Webhook.ts +++ b/src/structures/Webhook.ts @@ -17,9 +17,8 @@ import type { MethodContext, ObjectToLower, } from '../common'; -import { AnonymousGuild } from './AnonymousGuild'; -import { User } from './User'; import { DiscordBase } from './extra/DiscordBase'; +import { type AnonymousGuildStructure, Transformers, type UserStructure } from '../client/transformers'; export interface Webhook extends DiscordBase, ObjectToLower> {} @@ -28,9 +27,9 @@ export interface Webhook extends DiscordBase, ObjectToLower; + sourceGuild?: Partial; /** Methods related to interacting with messages through the webhook. */ messages!: ReturnType; /** @@ -42,11 +41,11 @@ export class Webhook extends DiscordBase { super(client, data); if (data.user) { - this.user = new User(this.client, data.user); + this.user = Transformers.User(this.client, data.user); } if (data.source_guild) { - this.sourceGuild = new AnonymousGuild(this.client, data.source_guild); + this.sourceGuild = Transformers.AnonymousGuild(this.client, data.source_guild); } Object.assign(this, { diff --git a/src/structures/channels.ts b/src/structures/channels.ts index 89de706..c25fff6 100644 --- a/src/structures/channels.ts +++ b/src/structures/channels.ts @@ -42,6 +42,22 @@ import type { GuildRole } from './GuildRole'; import { DiscordBase } from './extra/DiscordBase'; import { channelLink } from './extra/functions'; import { Collection, Formatter, type RawFile } from '..'; +import { + type BaseChannelStructure, + type BaseGuildChannelStructure, + type CategoryChannelStructure, + type DMChannelStructure, + type DirectoryChannelStructure, + type ForumChannelStructure, + type GuildMemberStructure, + type MediaChannelStructure, + type NewsChannelStructure, + type StageChannelStructure, + type TextGuildChannelStructure, + type ThreadChannelStructure, + Transformers, + type VoiceChannelStructure, +} from '../client/transformers'; export class BaseChannel extends DiscordBase> { declare type: T; @@ -293,32 +309,32 @@ export class TextBaseGuildChannel extends BaseGuildChannel {} export default function channelFrom(data: APIChannelBase, client: UsingClient): AllChannels { switch (data.type) { case ChannelType.GuildStageVoice: - return new StageChannel(client, data); + return Transformers.StageChannel(client, data); case ChannelType.GuildMedia: - return new MediaChannel(client, data); + return Transformers.MediaChannel(client, data); case ChannelType.DM: - return new DMChannel(client, data); + return Transformers.DMChannel(client, data); case ChannelType.GuildForum: - return new ForumChannel(client, data); + return Transformers.ForumChannel(client, data); case ChannelType.AnnouncementThread: case ChannelType.PrivateThread: case ChannelType.PublicThread: - return new ThreadChannel(client, data); + return Transformers.ThreadChannel(client, data); case ChannelType.GuildDirectory: - return new DirectoryChannel(client, data); + return Transformers.DirectoryChannel(client, data); case ChannelType.GuildVoice: - return new VoiceChannel(client, data); + return Transformers.VoiceChannel(client, data); case ChannelType.GuildText: - return new TextGuildChannel(client, data as APIGuildChannel); + return Transformers.TextGuildChannel(client, data as APIGuildChannel); case ChannelType.GuildCategory: - return new CategoryChannel(client, data); + return Transformers.CategoryChannel(client, data); case ChannelType.GuildAnnouncement: - return new NewsChannel(client, data); + return Transformers.NewsChannel(client, data); default: if ('guild_id' in data) { - return new BaseGuildChannel(client, data as APIGuildChannel); + return Transformers.BaseGuildChannel(client, data as APIGuildChannel); } - return new BaseChannel(client, data); + return Transformers.BaseChannel(client, data); } } @@ -389,7 +405,7 @@ export class VoiceChannelMethods extends DiscordBase { } public async members(force?: boolean) { - const collection = new Collection(); + const collection = new Collection(); const states = await this.states(); @@ -571,30 +587,39 @@ export class NewsChannel extends BaseChannel { export class DirectoryChannel extends BaseChannel {} export type AllGuildChannels = - | TextGuildChannel - | VoiceChannel - | MediaChannel - | ForumChannel - | ThreadChannel - | CategoryChannel - | NewsChannel - | DirectoryChannel - | StageChannel; + | TextGuildChannelStructure + | VoiceChannelStructure + | MediaChannelStructure + | ForumChannelStructure + | ThreadChannelStructure + | CategoryChannelStructure + | NewsChannelStructure + | DirectoryChannelStructure + | StageChannelStructure; -export type AllTextableChannels = TextGuildChannel | VoiceChannel | DMChannel | NewsChannel | ThreadChannel; -export type AllGuildTextableChannels = TextGuildChannel | VoiceChannel | NewsChannel | ThreadChannel; -export type AllGuildVoiceChannels = VoiceChannel | StageChannel; +export type AllTextableChannels = + | TextGuildChannelStructure + | VoiceChannelStructure + | DMChannelStructure + | NewsChannelStructure + | ThreadChannelStructure; +export type AllGuildTextableChannels = + | TextGuildChannelStructure + | VoiceChannelStructure + | NewsChannelStructure + | ThreadChannelStructure; +export type AllGuildVoiceChannels = VoiceChannelStructure | StageChannelStructure; export type AllChannels = - | BaseChannel - | BaseGuildChannel - | TextGuildChannel - | DMChannel - | VoiceChannel - | MediaChannel - | ForumChannel - | ThreadChannel - | CategoryChannel - | NewsChannel - | DirectoryChannel - | StageChannel; + | BaseChannelStructure + | BaseGuildChannelStructure + | TextGuildChannelStructure + | DMChannelStructure + | VoiceChannelStructure + | MediaChannelStructure + | ForumChannelStructure + | ThreadChannelStructure + | CategoryChannelStructure + | NewsChannelStructure + | DirectoryChannelStructure + | StageChannelStructure; diff --git a/src/websocket/discord/events/presenceUpdate.ts b/src/websocket/discord/events/presenceUpdate.ts index 5be745b..c3a7ae2 100644 --- a/src/websocket/discord/events/presenceUpdate.ts +++ b/src/websocket/discord/events/presenceUpdate.ts @@ -1,22 +1,15 @@ -import type { APIUser, GatewayActivity, GatewayPresenceUpdateDispatchData } from 'discord-api-types/v10'; - -type FixedGatewayPresenceUpdateDispatchData = - | (Omit & { user_id: string; user: undefined }) - | (Omit & { - user: Partial & Pick; - user_id: undefined; - }); +import type { GatewayActivity, GatewayPresenceUpdateDispatchData } from 'discord-api-types/v10'; export class PresenceUpdateHandler { - presenceUpdate = new Map(); + presenceUpdate = new Map(); - check(presence: FixedGatewayPresenceUpdateDispatchData) { - if (!this.presenceUpdate.has(presence.user?.id ?? presence.user_id!)) { + check(presence: GatewayPresenceUpdateDispatchData) { + if (!this.presenceUpdate.has(presence.user.id)) { this.setPresence(presence); return true; } - const data = this.presenceUpdate.get(presence.user?.id ?? presence.user_id!)!; + const data = this.presenceUpdate.get(presence.user.id)!; if (this.presenceEquals(data.presence, presence)) { return false; @@ -29,19 +22,16 @@ export class PresenceUpdateHandler { return true; } - setPresence(presence: FixedGatewayPresenceUpdateDispatchData) { - this.presenceUpdate.set(presence.user?.id ?? presence.user_id!, { + setPresence(presence: GatewayPresenceUpdateDispatchData) { + this.presenceUpdate.set(presence.user.id, { presence, timeout: setTimeout(() => { - this.presenceUpdate.delete(presence.user?.id ?? presence.user_id!); + this.presenceUpdate.delete(presence.user.id); }, 1.5e3), }); } - presenceEquals( - oldPresence: FixedGatewayPresenceUpdateDispatchData, - newPresence: FixedGatewayPresenceUpdateDispatchData, - ) { + presenceEquals(oldPresence: GatewayPresenceUpdateDispatchData, newPresence: GatewayPresenceUpdateDispatchData) { return ( newPresence && oldPresence.status === newPresence.status && diff --git a/src/websocket/discord/worker.ts b/src/websocket/discord/worker.ts index 9d6477c..d6c5fda 100644 --- a/src/websocket/discord/worker.ts +++ b/src/websocket/discord/worker.ts @@ -27,12 +27,16 @@ export type WorkerSendCacheRequest = CreateWorkerMessage< nonce: string; method: | 'scan' + | 'bulkGet' | 'get' + | 'bulkSet' | 'set' + | 'bulkPatch' | 'patch' | 'values' | 'keys' | 'count' + | 'bulkRemove' | 'remove' | 'flush' | 'contains' diff --git a/src/websocket/discord/workermanager.ts b/src/websocket/discord/workermanager.ts index e0f2d56..ca392c2 100644 --- a/src/websocket/discord/workermanager.ts +++ b/src/websocket/discord/workermanager.ts @@ -235,7 +235,7 @@ export class WorkerManager extends Map< } break; case 'PRESENCE_UPDATE': - if (!this.presenceUpdateHandler.check(message.payload.d as any)) { + if (!this.presenceUpdateHandler.check(message.payload.d)) { return; } break;