diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..9712c04 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +{ + "ignorePatterns": ["node_modules/*"], +} \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..be9cdc5 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,285 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:prettier/recommended" + ], + "plugins": [ + "import" + ], + "parserOptions": { + "ecmaVersion": 2021 + }, + "env": { + "es2021": true, + "node": true + }, + "rules": { + "import/order": [ + "error", + { + "groups": [ + "builtin", + "external", + "internal", + "index", + "sibling", + "parent" + ], + "alphabetize": { + "order": "asc" + } + } + ], + "prettier/prettier": [ + 2, + { + "printWidth": 120, + "singleQuote": true, + "quoteProps": "as-needed", + "trailingComma": "all", + "endOfLine": "lf", + "arrowParens": "avoid" + } + ], + "strict": [ + "error", + "global" + ], + "no-await-in-loop": "warn", + "no-compare-neg-zero": "error", + "no-template-curly-in-string": "error", + "no-unsafe-negation": "error", + "valid-jsdoc": [ + "error", + { + "requireReturn": false, + "requireReturnDescription": false, + "prefer": { + "return": "returns", + "arg": "param" + }, + "preferType": { + "String": "string", + "Number": "number", + "Boolean": "boolean", + "Symbol": "symbol", + "object": "Object", + "function": "Function", + "array": "Array", + "date": "Date", + "error": "Error", + "null": "void" + } + } + ], + "accessor-pairs": "warn", + "array-callback-return": "error", + "consistent-return": "error", + "curly": [ + "error", + "multi-line", + "consistent" + ], + "dot-location": [ + "error", + "property" + ], + "dot-notation": "error", + "eqeqeq": "error", + "no-empty-function": "error", + "no-floating-decimal": "error", + "no-implied-eval": "error", + "no-invalid-this": "error", + "no-lone-blocks": "error", + "no-multi-spaces": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-new": "error", + "no-octal-escape": "error", + "no-return-assign": "error", + "no-return-await": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-throw-literal": "error", + "no-unmodified-loop-condition": "error", + "no-unused-expressions": "error", + "no-useless-call": "error", + "no-useless-concat": "error", + "no-useless-escape": "error", + "no-useless-return": "error", + "no-void": "error", + "no-warning-comments": "warn", + "prefer-promise-reject-errors": "error", + "require-await": "warn", + "wrap-iife": "error", + "yoda": "error", + "no-label-var": "error", + "no-shadow": "error", + "no-undef-init": "error", + "callback-return": "error", + "getter-return": "off", + "handle-callback-err": "error", + "no-mixed-requires": "error", + "no-new-require": "error", + "no-path-concat": "error", + "array-bracket-spacing": "error", + "block-spacing": "error", + "brace-style": [ + "error", + "1tbs", + { + "allowSingleLine": true + } + ], + "capitalized-comments": [ + "error", + "always", + { + "ignoreConsecutiveComments": true + } + ], + "comma-dangle": [ + "error", + "always-multiline" + ], + "comma-spacing": "error", + "comma-style": "error", + "computed-property-spacing": "error", + "consistent-this": [ + "error", + "$this" + ], + "eol-last": "error", + "func-names": "error", + "func-name-matching": "error", + "func-style": [ + "error", + "declaration", + { + "allowArrowFunctions": true + } + ], + "key-spacing": "error", + "keyword-spacing": "error", + "max-depth": "error", + "max-len": [ + "error", + 120, + 2 + ], + "max-nested-callbacks": [ + "error", + { + "max": 4 + } + ], + "max-statements-per-line": [ + "error", + { + "max": 2 + } + ], + "new-cap": "off", + "newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 3 + } + ], + "no-array-constructor": "error", + "no-inline-comments": "off", + "no-lonely-if": "error", + "no-multiple-empty-lines": [ + "error", + { + "max": 2, + "maxEOF": 1, + "maxBOF": 0 + } + ], + "no-new-object": "error", + "no-spaced-func": "error", + "no-trailing-spaces": "error", + "no-unneeded-ternary": "error", + "no-whitespace-before-property": "error", + "nonblock-statement-body-position": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "operator-assignment": "error", + "padded-blocks": [ + "error", + "never" + ], + "quote-props": [ + "error", + "as-needed" + ], + "quotes": [ + "error", + "single", + { + "avoidEscape": true, + "allowTemplateLiterals": true + } + ], + "semi-spacing": "error", + "semi": "error", + "space-before-blocks": "error", + "space-before-function-paren": [ + "error", + { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + } + ], + "space-in-parens": "error", + "space-infix-ops": "error", + "space-unary-ops": "error", + "spaced-comment": "error", + "template-tag-spacing": "error", + "unicode-bom": "error", + "arrow-body-style": "error", + "arrow-parens": [ + "error", + "as-needed" + ], + "arrow-spacing": "error", + "no-duplicate-imports": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "prefer-arrow-callback": "error", + "prefer-numeric-literals": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "rest-spread-spacing": "error", + "template-curly-spacing": "error", + "yield-star-spacing": "error", + "no-restricted-globals": [ + "error", + { + "name": "Buffer", + "message": "Import Buffer from `node:buffer` instead" + }, + { + "name": "process", + "message": "Import process from `node:process` instead" + }, + { + "name": "setTimeout", + "message": "Import setTimeout from `node:timers` instead" + }, + { + "name": "setInterval", + "message": "Import setInterval from `node:timers` instead" + }, + { + "name": "setImmediate", + "message": "Import setImmediate from `node:timers` instead" + } + ] + } +} \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..084f44b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: npm + directory: "/" + schedule: + interval: daily + time: "12:00" + open-pull-requests-limit: 15 \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..819c467 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,24 @@ +name: Lint +on: + push: + branches: + - '*' + pull_request: + branches: + - '*' +jobs: + eslint: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npm run lint:all \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..25a6e27 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "printWidth": 120, + "trailingComma": "all", + "endOfLine": "lf", + "arrowParens": "avoid" +} \ No newline at end of file diff --git a/package.json b/package.json index 8823a6b..202002b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,12 @@ "main": "./src/index.js", "types": "./typings/index.d.ts", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "lint:typings": "tslint typings/index.d.ts", + "format": "prettier --write src/**/*.js typings/**/*.ts", + "lint:all": "npm run lint && npm run lint:typings" }, "files": [ "src", diff --git a/src/client/Client.js b/src/client/Client.js index 465187c..b2e9b45 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -126,7 +126,7 @@ class Client extends BaseClient { this.users = new UserManager(this); /** Patch - * + * */ this.relationships = new RelationshipsManager(this); this.setting = new ClientUserSettingManager(this); @@ -244,44 +244,41 @@ class Client extends BaseClient { * Update Cloudflare Cookie and Discord Fingerprint */ async updateCookie() { - /* Auto find fingerprint and add Cookie */ - let cookie = ''; - await require('axios')({ - method: 'get', - url: 'https://discord.com/api/v9/experiments', - headers: this.options.http.headers, - }) - .then((res) => { - if (!'set-cookie' in res.headers) return; - res.headers['set-cookie'].map((line) => { - line.split('; ').map((arr) => { - if ( - arr.startsWith('Expires') || - arr.startsWith('Path') || - arr.startsWith('Domain') || - arr.startsWith('HttpOnly') || - arr.startsWith('Secure') || - arr.startsWith('Max-Age') || - arr.startsWith('SameSite') - ) { - return; - } else { - cookie += `${arr}; `; - } - }); - }); - this.options.http.headers['Cookie'] = `${cookie}locale=en`; - this.options.http.headers['x-fingerprint'] = res.data.fingerprint; - this.emit(Events.DEBUG, `Added Cookie: ${cookie}`); - this.emit(Events.DEBUG, `Added Fingerprint: ${res.data.fingerprint}`); - }) - .catch((err) => { - this.emit( - Events.DEBUG, - `Finding Cookie and Fingerprint failed: ${err.message}`, - ); - }); - } + /* Auto find fingerprint and add Cookie */ + let cookie = ''; + await require('axios')({ + method: 'get', + url: 'https://discord.com/api/v9/experiments', + headers: this.options.http.headers, + }) + .then(res => { + if (!'set-cookie' in res.headers) return; + res.headers['set-cookie'].map(line => { + line.split('; ').map(arr => { + if ( + arr.startsWith('Expires') || + arr.startsWith('Path') || + arr.startsWith('Domain') || + arr.startsWith('HttpOnly') || + arr.startsWith('Secure') || + arr.startsWith('Max-Age') || + arr.startsWith('SameSite') + ) { + return; + } else { + cookie += `${arr}; `; + } + }); + }); + this.options.http.headers['Cookie'] = `${cookie}locale=en`; + this.options.http.headers['x-fingerprint'] = res.data.fingerprint; + this.emit(Events.DEBUG, `Added Cookie: ${cookie}`); + this.emit(Events.DEBUG, `Added Fingerprint: ${res.data.fingerprint}`); + }) + .catch(err => { + this.emit(Events.DEBUG, `Finding Cookie and Fingerprint failed: ${err.message}`); + }); + } /** * Logs the client in, establishing a WebSocket connection to Discord. diff --git a/src/client/actions/InteractionCreate.js b/src/client/actions/InteractionCreate.js index 94be343..1a0484f 100644 --- a/src/client/actions/InteractionCreate.js +++ b/src/client/actions/InteractionCreate.js @@ -60,7 +60,10 @@ class InteractionCreateAction extends Action { InteractionType = AutocompleteInteraction; break; default: - client.emit(Events.DEBUG, `[INTERACTION] Received [BOT] / Send (Selfbot) interactionID ${data.id} with unknown type: ${data.type}`); + client.emit( + Events.DEBUG, + `[INTERACTION] Received [BOT] / Send (Selfbot) interactionID ${data.id} with unknown type: ${data.type}`, + ); return; } diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 04bd8c5..6c4b32c 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -217,7 +217,7 @@ class WebSocketManager extends EventEmitter { this.shardQueue.add(shard); if (shard.sessionId) { - this.debug(`Session id is present, attempting an immediate reconnect...`, shard); + this.debug('Session id is present, attempting an immediate reconnect...', shard); this.reconnect(); } else { shard.destroy({ reset: true, emit: false, log: false }); @@ -279,7 +279,7 @@ class WebSocketManager extends EventEmitter { } catch (error) { this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`); if (error.httpStatus !== 401) { - this.debug(`Possible network error occurred. Retrying in 5s...`); + this.debug('Possible network error occurred. Retrying in 5s...'); await sleep(5_000); this.reconnecting = false; return this.reconnect(); diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index bab2ba6..83e82b0 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -13,7 +13,7 @@ let zlib; try { zlib = require('zlib-sync'); -} catch { } // eslint-disable-line no-empty +} catch {} // eslint-disable-line no-empty /** * Represents a Shard's WebSocket connection @@ -487,8 +487,9 @@ class WebSocketShard extends EventEmitter { () => { this.debug( `Shard ${hasGuildsIntent ? 'did' : 'will'} not receive any more guild packets` + - `${hasGuildsIntent ? ` in ${waitGuildTimeout} ms` : ''}.\nUnavailable guild count: ${this.expectedGuilds.size - }`, + `${hasGuildsIntent ? ` in ${waitGuildTimeout} ms` : ''}.\nUnavailable guild count: ${ + this.expectedGuilds.size + }`, ); this.readyTimeout = null; diff --git a/src/client/websocket/handlers/GUILD_APPLICATION_COMMANDS_UPDATE.js b/src/client/websocket/handlers/GUILD_APPLICATION_COMMANDS_UPDATE.js index e4d64ad..48510ac 100644 --- a/src/client/websocket/handlers/GUILD_APPLICATION_COMMANDS_UPDATE.js +++ b/src/client/websocket/handlers/GUILD_APPLICATION_COMMANDS_UPDATE.js @@ -1,10 +1,10 @@ 'use strict'; module.exports = (client, { d: data }) => { - if (!data.application_commands[0]) return; - for (const command of data.application_commands) { - const user = client.users.cache.get(command.application_id); - if (!user) continue; - user.applications._add(command, true); - }; -}; \ No newline at end of file + if (!data.application_commands[0]) return; + for (const command of data.application_commands) { + const user = client.users.cache.get(command.application_id); + if (!user) continue; + user.applications._add(command, true); + } +}; diff --git a/src/client/websocket/handlers/GUILD_MEMBER_LIST_UPDATE.js b/src/client/websocket/handlers/GUILD_MEMBER_LIST_UPDATE.js index 80687d2..5c25ecd 100644 --- a/src/client/websocket/handlers/GUILD_MEMBER_LIST_UPDATE.js +++ b/src/client/websocket/handlers/GUILD_MEMBER_LIST_UPDATE.js @@ -4,32 +4,34 @@ const { Collection } = require('@discordjs/collection'); const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { - // console.log(data); - // console.log(data.ops[0]) - const guild = client.guilds.cache.get(data.guild_id); - if (!guild) return; - const members = new Collection(); - // Get Member from side Discord Channel (online counting if large server) - for (const object of data.ops) { - if (object.op == 'SYNC') { - for (const member_ of object.items) { - const member = member_.member; - if (!member) continue; - members.set(member.user.id, guild.members._add(member)); - if (member.presence) - guild.presences._add(Object.assign(member.presence, { guild })); - } - } else if (object.op == 'INVALIDATE') { - console.warn(`Invalidate [${object.range[0]}, ${object.range[1]}]`); - } else if (object.op == 'UPDATE' || object.op == 'INSERT') { - const member = object.item.member; - if (!member) continue; - members.set(member.user.id, guild.members._add(member)); - if (member.presence) - guild.presences._add(Object.assign(member.presence, { guild })); - } else if (object.op == 'DELETE') { - // nothing; + // console.log(data); + // console.log(data.ops[0]) + const guild = client.guilds.cache.get(data.guild_id); + if (!guild) return; + const members = new Collection(); + // Get Member from side Discord Channel (online counting if large server) + for (const object of data.ops) { + if (object.op == 'SYNC') { + for (const member_ of object.items) { + const member = member_.member; + if (!member) continue; + members.set(member.user.id, guild.members._add(member)); + if (member.presence) { + guild.presences._add(Object.assign(member.presence, { guild })); } + } + } else if (object.op == 'INVALIDATE') { + console.warn(`Invalidate [${object.range[0]}, ${object.range[1]}]`); + } else if (object.op == 'UPDATE' || object.op == 'INSERT') { + const member = object.item.member; + if (!member) continue; + members.set(member.user.id, guild.members._add(member)); + if (member.presence) { + guild.presences._add(Object.assign(member.presence, { guild })); + } + } else if (object.op == 'DELETE') { + // nothing; } - client.emit(Events.GUILD_MEMBER_LIST_UPDATE, members, guild, data); + } + client.emit(Events.GUILD_MEMBER_LIST_UPDATE, members, guild, data); }; diff --git a/src/client/websocket/handlers/INTERACTION_CREATE.js b/src/client/websocket/handlers/INTERACTION_CREATE.js index 86913ac..f5e76ad 100644 --- a/src/client/websocket/handlers/INTERACTION_CREATE.js +++ b/src/client/websocket/handlers/INTERACTION_CREATE.js @@ -2,6 +2,6 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, packet) => { - if (client.user.bot) client.actions.InteractionCreate.handle(packet.d); + if (client.user.bot) client.actions.InteractionCreate.handle(packet.d); else client.emit(Events.INTERACTION_CREATE, packet.d); }; diff --git a/src/client/websocket/handlers/INTERACTION_FAILED.js b/src/client/websocket/handlers/INTERACTION_FAILED.js index 9a9f086..d8f3bca 100644 --- a/src/client/websocket/handlers/INTERACTION_FAILED.js +++ b/src/client/websocket/handlers/INTERACTION_FAILED.js @@ -2,5 +2,5 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { - client.emit(Events.INTERACTION_FAILED, data); + client.emit(Events.INTERACTION_FAILED, data); }; diff --git a/src/client/websocket/handlers/INTERACTION_SUCCESS.js b/src/client/websocket/handlers/INTERACTION_SUCCESS.js index 5acff20..478ef00 100644 --- a/src/client/websocket/handlers/INTERACTION_SUCCESS.js +++ b/src/client/websocket/handlers/INTERACTION_SUCCESS.js @@ -2,5 +2,5 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { - client.emit(Events.INTERACTION_SUCCESS, data); -}; \ No newline at end of file + client.emit(Events.INTERACTION_SUCCESS, data); +}; diff --git a/src/client/websocket/handlers/MESSAGE_ACK.js b/src/client/websocket/handlers/MESSAGE_ACK.js index 0c0fa5d..2d8d355 100644 --- a/src/client/websocket/handlers/MESSAGE_ACK.js +++ b/src/client/websocket/handlers/MESSAGE_ACK.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = (client, { d: data }) => { - // client.user.messageMentions.delete(data.channel_id); -}; \ No newline at end of file + // client.user.messageMentions.delete(data.channel_id); +}; diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index ba2a9d1..409242e 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -5,112 +5,99 @@ const chalk = require('chalk'); const axios = require('axios'); const Discord = require('../../../index'); const RichPresence = require('discord-rpc-contructor'); +const { ChannelTypes } = require('../../../util/Constants'); const checkUpdate = async () => { - const res_ = await axios.get( - `https://registry.npmjs.com/${encodeURIComponent( - 'discord.js-selfbot-v13', - )}`, - ); - const lastest_tag = res_.data['dist-tags'].latest; - // Checking if the package is outdated - // Stable version - if (lastest_tag !== Discord.version && Discord.version.includes('-') == false) { - return console.log(`${chalk.yellowBright( - '[WARNING]', - )} New Discord.js-selfbot-v13 version. -Old Version: ${chalk.redBright( - Discord.version, - )} => New Version: ${chalk.greenBright(lastest_tag)}`); - } - return console.log( - `${chalk.greenBright( - '[OK]', - )} Discord.js-selfbot-v13 is up to date. Version: ${chalk.blueBright( - Discord.version, - )}`, - ); + const res_ = await axios.get(`https://registry.npmjs.com/${encodeURIComponent('discord.js-selfbot-v13')}`); + const lastest_tag = res_.data['dist-tags'].latest; + // Checking if the package is outdated + // Stable version + if (lastest_tag !== Discord.version && Discord.version.includes('-') == false) { + return console.log(`${chalk.yellowBright('[WARNING]')} New Discord.js-selfbot-v13 version. +Old Version: ${chalk.redBright(Discord.version)} => New Version: ${chalk.greenBright(lastest_tag)}`); + } + return console.log( + `${chalk.greenBright('[OK]')} Discord.js-selfbot-v13 is up to date. Version: ${chalk.blueBright(Discord.version)}`, + ); }; -const customStatusAuto = async (client) => { - let custom_status; - if ( - client.setting.rawSetting.custom_status?.text || - res.rawSetting.custom_status?.emoji_name - ) { - custom_status = new RichPresence.CustomStatus(); - if (client.setting.rawSetting.custom_status.emoji_id) { - const emoji = await client.emojis.resolve( - client.setting.rawSetting.custom_status.emoji_id, - ); - if (emoji) custom_status.setDiscordEmoji(emoji); - } else { - custom_status.setUnicodeEmoji( - client.setting.rawSetting.custom_status.emoji_name, - ); - } - custom_status.setState(client.setting.rawSetting.custom_status?.text); - client.user.setPresence({ - activities: custom_status ? [custom_status.toDiscord()] : [], - status: client.setting.rawSetting.status, - }); - } -} +const customStatusAuto = async client => { + let custom_status; + if (client.setting.rawSetting.custom_status?.text || res.rawSetting.custom_status?.emoji_name) { + custom_status = new RichPresence.CustomStatus(); + if (client.setting.rawSetting.custom_status.emoji_id) { + const emoji = await client.emojis.resolve(client.setting.rawSetting.custom_status.emoji_id); + if (emoji) custom_status.setDiscordEmoji(emoji); + } else { + custom_status.setUnicodeEmoji(client.setting.rawSetting.custom_status.emoji_name); + } + custom_status.setState(client.setting.rawSetting.custom_status?.text); + client.user.setPresence({ + activities: custom_status ? [custom_status.toDiscord()] : [], + status: client.setting.rawSetting.status, + }); + } +}; module.exports = (client, { d: data }, shard) => { - if (client.options.checkUpdate) { - try { - checkUpdate(); - } catch (e) { - console.log(e); - } - } - client.session_id = data.session_id; - if (client.user) { - client.user._patch(data.user); - } else { - ClientUser ??= require('../../../structures/ClientUser'); - client.user = new ClientUser(client, data.user); - client.users.cache.set(client.user.id, client.user); - } + console.log(data.private_channels); + if (client.options.checkUpdate) { + try { + checkUpdate(); + } catch (e) { + console.log(e); + } + } + client.session_id = data.session_id; + if (client.user) { + client.user._patch(data.user); + } else { + ClientUser ??= require('../../../structures/ClientUser'); + client.user = new ClientUser(client, data.user); + client.users.cache.set(client.user.id, client.user); + } - client.user.setAFK(false); + client.user.setAFK(false); - client.setting._patch(data.user_settings); + client.setting._patch(data.user_settings); - client.user.connectedAccounts = data.connected_accounts ?? []; + client.user.connectedAccounts = data.connected_accounts ?? []; - for (const [userid, note] of Object.entries(data.notes)) { - client.user.notes.set(userid, note); - } + for (const [userid, note] of Object.entries(data.notes)) { + client.user.notes.set(userid, note); + } - if (client.options.readyStatus) { - customStatusAuto(client); - } + for (const private_channel of data.private_channels) { + client.channels._add(private_channel); + } - /** - * read_state: Return Array: - * { - * mention_count: 14, // ok it's ping count - * last_pin_timestamp: '1970-01-01T00:00:00+00:00', // why discord ? - * last_message_id: 0, // :) - * id: '840218426969817119' // channel id - * }, - */ + if (client.options.readyStatus) { + customStatusAuto(client); + } - /* + /** + * read_state: Return Array: + * { + * mention_count: 14, // ok it's ping count + * last_pin_timestamp: '1970-01-01T00:00:00+00:00', // why discord ? + * last_message_id: 0, // :) + * id: '840218426969817119' // channel id + * }, + */ + + /* for (const object of data.read_state) { if (object.mention_count == 0) continue; client.user.messageMentions.set(object.id, object); } */ - for (const guild of data.guilds) { - guild.shardId = shard.id; - client.guilds._add(guild); - } + for (const guild of data.guilds) { + guild.shardId = shard.id; + client.guilds._add(guild); + } - client.relationships._setup(data.relationships); + client.relationships._setup(data.relationships); - shard.checkReady(); + shard.checkReady(); }; diff --git a/src/client/websocket/handlers/RELATIONSHIP_ADD.js b/src/client/websocket/handlers/RELATIONSHIP_ADD.js index 00ea645..71f10bb 100644 --- a/src/client/websocket/handlers/RELATIONSHIP_ADD.js +++ b/src/client/websocket/handlers/RELATIONSHIP_ADD.js @@ -3,13 +3,13 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { - data.user ? client.users._add(data.user) : null; - client.relationships.cache.set(data.id, data.type); - /** - * Emitted whenever a relationship is updated. - * @event Client#relationshipUpdate - * @param {UserID} user The userID that was updated - * @param {Number} type The new relationship type - */ - client.emit(Events.RELATIONSHIP_ADD, data.id, data.type); + data.user ? client.users._add(data.user) : null; + client.relationships.cache.set(data.id, data.type); + /** + * Emitted whenever a relationship is updated. + * @event Client#relationshipUpdate + * @param {UserID} user The userID that was updated + * @param {Number} type The new relationship type + */ + client.emit(Events.RELATIONSHIP_ADD, data.id, data.type); }; diff --git a/src/client/websocket/handlers/RELATIONSHIP_REMOVE.js b/src/client/websocket/handlers/RELATIONSHIP_REMOVE.js index 437c26a..dafa6c5 100644 --- a/src/client/websocket/handlers/RELATIONSHIP_REMOVE.js +++ b/src/client/websocket/handlers/RELATIONSHIP_REMOVE.js @@ -3,11 +3,11 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { - client.relationships.cache.delete(data.id); - /** - * Emitted whenever a relationship is updated. - * @event Client#relationshipUpdate - * @param {UserID} user The userID that was updated - */ - client.emit(Events.RELATIONSHIP_REMOVE, data.id); + client.relationships.cache.delete(data.id); + /** + * Emitted whenever a relationship is updated. + * @event Client#relationshipUpdate + * @param {UserID} user The userID that was updated + */ + client.emit(Events.RELATIONSHIP_REMOVE, data.id); }; diff --git a/src/client/websocket/handlers/USER_NOTE_UPDATE.js b/src/client/websocket/handlers/USER_NOTE_UPDATE.js index 9f02ab9..5e36745 100644 --- a/src/client/websocket/handlers/USER_NOTE_UPDATE.js +++ b/src/client/websocket/handlers/USER_NOTE_UPDATE.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = (client, { d: data }) => { - client.user.notes.set(data.id, data.note); -}; \ No newline at end of file + client.user.notes.set(data.id, data.note); +}; diff --git a/src/client/websocket/handlers/USER_SETTINGS_UPDATE.js b/src/client/websocket/handlers/USER_SETTINGS_UPDATE.js index 2b45baa..fa44cf1 100644 --- a/src/client/websocket/handlers/USER_SETTINGS_UPDATE.js +++ b/src/client/websocket/handlers/USER_SETTINGS_UPDATE.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = (client, { d: data }) => { - client.setting._patch(data); -}; \ No newline at end of file + client.setting._patch(data); +}; diff --git a/src/client/websocket/handlers/index.js b/src/client/websocket/handlers/index.js index 648f28a..cfaa2da 100644 --- a/src/client/websocket/handlers/index.js +++ b/src/client/websocket/handlers/index.js @@ -1,80 +1,71 @@ 'use strict'; const handlers = Object.fromEntries([ - ['READY', require('./READY')], - ['RESUMED', require('./RESUMED')], - ['RELATIONSHIP_ADD', require('./RELATIONSHIP_ADD')], - ['RELATIONSHIP_REMOVE', require('./RELATIONSHIP_REMOVE')], - ['APPLICATION_COMMAND_CREATE', require('./APPLICATION_COMMAND_CREATE')], - ['APPLICATION_COMMAND_DELETE', require('./APPLICATION_COMMAND_DELETE')], - ['APPLICATION_COMMAND_UPDATE', require('./APPLICATION_COMMAND_UPDATE')], - ['GUILD_CREATE', require('./GUILD_CREATE')], - ['GUILD_DELETE', require('./GUILD_DELETE')], - ['GUILD_UPDATE', require('./GUILD_UPDATE')], - ['INVITE_CREATE', require('./INVITE_CREATE')], - ['INVITE_DELETE', require('./INVITE_DELETE')], - ['GUILD_MEMBER_ADD', require('./GUILD_MEMBER_ADD')], - ['GUILD_MEMBER_REMOVE', require('./GUILD_MEMBER_REMOVE')], - ['GUILD_MEMBER_UPDATE', require('./GUILD_MEMBER_UPDATE')], - ['GUILD_MEMBERS_CHUNK', require('./GUILD_MEMBERS_CHUNK')], - ['GUILD_MEMBER_LIST_UPDATE', require('./GUILD_MEMBER_LIST_UPDATE.js')], - [ - 'GUILD_APPLICATION_COMMANDS_UPDATE', - require('./GUILD_APPLICATION_COMMANDS_UPDATE.js'), - ], - ['GUILD_INTEGRATIONS_UPDATE', require('./GUILD_INTEGRATIONS_UPDATE')], - ['GUILD_ROLE_CREATE', require('./GUILD_ROLE_CREATE')], - ['GUILD_ROLE_DELETE', require('./GUILD_ROLE_DELETE')], - ['GUILD_ROLE_UPDATE', require('./GUILD_ROLE_UPDATE')], - ['GUILD_BAN_ADD', require('./GUILD_BAN_ADD')], - ['GUILD_BAN_REMOVE', require('./GUILD_BAN_REMOVE')], - ['GUILD_EMOJIS_UPDATE', require('./GUILD_EMOJIS_UPDATE')], - ['CHANNEL_CREATE', require('./CHANNEL_CREATE')], - ['CHANNEL_DELETE', require('./CHANNEL_DELETE')], - ['CHANNEL_UPDATE', require('./CHANNEL_UPDATE')], - ['CHANNEL_PINS_UPDATE', require('./CHANNEL_PINS_UPDATE')], - ['MESSAGE_CREATE', require('./MESSAGE_CREATE')], - ['MESSAGE_DELETE', require('./MESSAGE_DELETE')], - ['MESSAGE_UPDATE', require('./MESSAGE_UPDATE')], - ['MESSAGE_DELETE_BULK', require('./MESSAGE_DELETE_BULK')], - ['MESSAGE_REACTION_ADD', require('./MESSAGE_REACTION_ADD')], - ['MESSAGE_REACTION_REMOVE', require('./MESSAGE_REACTION_REMOVE')], - ['MESSAGE_REACTION_REMOVE_ALL', require('./MESSAGE_REACTION_REMOVE_ALL')], - ['MESSAGE_REACTION_REMOVE_EMOJI', require('./MESSAGE_REACTION_REMOVE_EMOJI')], - ['THREAD_CREATE', require('./THREAD_CREATE')], - ['THREAD_UPDATE', require('./THREAD_UPDATE')], - ['THREAD_DELETE', require('./THREAD_DELETE')], - ['THREAD_LIST_SYNC', require('./THREAD_LIST_SYNC')], - ['THREAD_MEMBER_UPDATE', require('./THREAD_MEMBER_UPDATE')], - ['THREAD_MEMBERS_UPDATE', require('./THREAD_MEMBERS_UPDATE')], - ['USER_SETTINGS_UPDATE', require('./USER_SETTINGS_UPDATE')], // opcode 0 - // USER_SETTINGS_PROTO_UPDATE // opcode 0 - ['MESSAGE_ACK', require('./MESSAGE_ACK')], - ['USER_NOTE_UPDATE', require('./USER_NOTE_UPDATE')], - ['USER_UPDATE', require('./USER_UPDATE')], - ['PRESENCE_UPDATE', require('./PRESENCE_UPDATE')], - ['TYPING_START', require('./TYPING_START')], - ['VOICE_STATE_UPDATE', require('./VOICE_STATE_UPDATE')], - ['VOICE_SERVER_UPDATE', require('./VOICE_SERVER_UPDATE')], - ['WEBHOOKS_UPDATE', require('./WEBHOOKS_UPDATE')], - ['INTERACTION_CREATE', require('./INTERACTION_CREATE')], - ['INTERACTION_SUCCESS', require('./INTERACTION_SUCCESS')], - ['INTERACTION_FAILED', require('./INTERACTION_FAILED')], - ['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE')], - ['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE')], - ['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE')], - ['GUILD_STICKERS_UPDATE', require('./GUILD_STICKERS_UPDATE')], - ['GUILD_SCHEDULED_EVENT_CREATE', require('./GUILD_SCHEDULED_EVENT_CREATE')], - ['GUILD_SCHEDULED_EVENT_UPDATE', require('./GUILD_SCHEDULED_EVENT_UPDATE')], - ['GUILD_SCHEDULED_EVENT_DELETE', require('./GUILD_SCHEDULED_EVENT_DELETE')], - [ - 'GUILD_SCHEDULED_EVENT_USER_ADD', - require('./GUILD_SCHEDULED_EVENT_USER_ADD'), - ], - [ - 'GUILD_SCHEDULED_EVENT_USER_REMOVE', - require('./GUILD_SCHEDULED_EVENT_USER_REMOVE'), - ], + ['READY', require('./READY')], + ['RESUMED', require('./RESUMED')], + ['RELATIONSHIP_ADD', require('./RELATIONSHIP_ADD')], + ['RELATIONSHIP_REMOVE', require('./RELATIONSHIP_REMOVE')], + ['APPLICATION_COMMAND_CREATE', require('./APPLICATION_COMMAND_CREATE')], + ['APPLICATION_COMMAND_DELETE', require('./APPLICATION_COMMAND_DELETE')], + ['APPLICATION_COMMAND_UPDATE', require('./APPLICATION_COMMAND_UPDATE')], + ['GUILD_CREATE', require('./GUILD_CREATE')], + ['GUILD_DELETE', require('./GUILD_DELETE')], + ['GUILD_UPDATE', require('./GUILD_UPDATE')], + ['INVITE_CREATE', require('./INVITE_CREATE')], + ['INVITE_DELETE', require('./INVITE_DELETE')], + ['GUILD_MEMBER_ADD', require('./GUILD_MEMBER_ADD')], + ['GUILD_MEMBER_REMOVE', require('./GUILD_MEMBER_REMOVE')], + ['GUILD_MEMBER_UPDATE', require('./GUILD_MEMBER_UPDATE')], + ['GUILD_MEMBERS_CHUNK', require('./GUILD_MEMBERS_CHUNK')], + ['GUILD_MEMBER_LIST_UPDATE', require('./GUILD_MEMBER_LIST_UPDATE.js')], + ['GUILD_APPLICATION_COMMANDS_UPDATE', require('./GUILD_APPLICATION_COMMANDS_UPDATE.js')], + ['GUILD_INTEGRATIONS_UPDATE', require('./GUILD_INTEGRATIONS_UPDATE')], + ['GUILD_ROLE_CREATE', require('./GUILD_ROLE_CREATE')], + ['GUILD_ROLE_DELETE', require('./GUILD_ROLE_DELETE')], + ['GUILD_ROLE_UPDATE', require('./GUILD_ROLE_UPDATE')], + ['GUILD_BAN_ADD', require('./GUILD_BAN_ADD')], + ['GUILD_BAN_REMOVE', require('./GUILD_BAN_REMOVE')], + ['GUILD_EMOJIS_UPDATE', require('./GUILD_EMOJIS_UPDATE')], + ['CHANNEL_CREATE', require('./CHANNEL_CREATE')], + ['CHANNEL_DELETE', require('./CHANNEL_DELETE')], + ['CHANNEL_UPDATE', require('./CHANNEL_UPDATE')], + ['CHANNEL_PINS_UPDATE', require('./CHANNEL_PINS_UPDATE')], + ['MESSAGE_CREATE', require('./MESSAGE_CREATE')], + ['MESSAGE_DELETE', require('./MESSAGE_DELETE')], + ['MESSAGE_UPDATE', require('./MESSAGE_UPDATE')], + ['MESSAGE_DELETE_BULK', require('./MESSAGE_DELETE_BULK')], + ['MESSAGE_REACTION_ADD', require('./MESSAGE_REACTION_ADD')], + ['MESSAGE_REACTION_REMOVE', require('./MESSAGE_REACTION_REMOVE')], + ['MESSAGE_REACTION_REMOVE_ALL', require('./MESSAGE_REACTION_REMOVE_ALL')], + ['MESSAGE_REACTION_REMOVE_EMOJI', require('./MESSAGE_REACTION_REMOVE_EMOJI')], + ['THREAD_CREATE', require('./THREAD_CREATE')], + ['THREAD_UPDATE', require('./THREAD_UPDATE')], + ['THREAD_DELETE', require('./THREAD_DELETE')], + ['THREAD_LIST_SYNC', require('./THREAD_LIST_SYNC')], + ['THREAD_MEMBER_UPDATE', require('./THREAD_MEMBER_UPDATE')], + ['THREAD_MEMBERS_UPDATE', require('./THREAD_MEMBERS_UPDATE')], + ['USER_SETTINGS_UPDATE', require('./USER_SETTINGS_UPDATE')], // Opcode 0 + // USER_SETTINGS_PROTO_UPDATE // opcode 0 + ['MESSAGE_ACK', require('./MESSAGE_ACK')], + ['USER_NOTE_UPDATE', require('./USER_NOTE_UPDATE')], + ['USER_UPDATE', require('./USER_UPDATE')], + ['PRESENCE_UPDATE', require('./PRESENCE_UPDATE')], + ['TYPING_START', require('./TYPING_START')], + ['VOICE_STATE_UPDATE', require('./VOICE_STATE_UPDATE')], + ['VOICE_SERVER_UPDATE', require('./VOICE_SERVER_UPDATE')], + ['WEBHOOKS_UPDATE', require('./WEBHOOKS_UPDATE')], + ['INTERACTION_CREATE', require('./INTERACTION_CREATE')], + ['INTERACTION_SUCCESS', require('./INTERACTION_SUCCESS')], + ['INTERACTION_FAILED', require('./INTERACTION_FAILED')], + ['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE')], + ['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE')], + ['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE')], + ['GUILD_STICKERS_UPDATE', require('./GUILD_STICKERS_UPDATE')], + ['GUILD_SCHEDULED_EVENT_CREATE', require('./GUILD_SCHEDULED_EVENT_CREATE')], + ['GUILD_SCHEDULED_EVENT_UPDATE', require('./GUILD_SCHEDULED_EVENT_UPDATE')], + ['GUILD_SCHEDULED_EVENT_DELETE', require('./GUILD_SCHEDULED_EVENT_DELETE')], + ['GUILD_SCHEDULED_EVENT_USER_ADD', require('./GUILD_SCHEDULED_EVENT_USER_ADD')], + ['GUILD_SCHEDULED_EVENT_USER_REMOVE', require('./GUILD_SCHEDULED_EVENT_USER_REMOVE')], ]); module.exports = handlers; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index d8dbe5e..47597f0 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -3,210 +3,173 @@ const { register } = require('./DJSError'); const Messages = { - CLIENT_INVALID_OPTION: (prop, must) => `The ${prop} option must be ${must}`, - CLIENT_INVALID_PROVIDED_SHARDS: 'None of the provided shards were valid.', - CLIENT_MISSING_INTENTS: 'Valid intents must be provided for the Client.', - CLIENT_NOT_READY: (action) => - `The client needs to be logged in to ${action}.`, + CLIENT_INVALID_OPTION: (prop, must) => `The ${prop} option must be ${must}`, + CLIENT_INVALID_PROVIDED_SHARDS: 'None of the provided shards were valid.', + CLIENT_MISSING_INTENTS: 'Valid intents must be provided for the Client.', + CLIENT_NOT_READY: action => `The client needs to be logged in to ${action}.`, - TOKEN_INVALID: 'An invalid token was provided.', - TOKEN_MISSING: - 'Request to use token, but token was unavailable to the client.', + TOKEN_INVALID: 'An invalid token was provided.', + TOKEN_MISSING: 'Request to use token, but token was unavailable to the client.', - WS_CLOSE_REQUESTED: 'WebSocket closed due to user request.', - WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.', - WS_NOT_OPEN: (data = 'data') => `WebSocket not open to send ${data}`, - MANAGER_DESTROYED: 'Manager was destroyed.', + WS_CLOSE_REQUESTED: 'WebSocket closed due to user request.', + WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.', + WS_NOT_OPEN: (data = 'data') => `WebSocket not open to send ${data}`, + MANAGER_DESTROYED: 'Manager was destroyed.', - BITFIELD_INVALID: (bit) => `Invalid bitfield flag or number: ${bit}.`, + BITFIELD_INVALID: bit => `Invalid bitfield flag or number: ${bit}.`, - SHARDING_INVALID: 'Invalid shard settings were provided.', - SHARDING_REQUIRED: - 'This session would have handled too many guilds - Sharding is required.', - INVALID_INTENTS: 'Invalid intent provided for WebSocket intents.', - DISALLOWED_INTENTS: - 'Privileged intent provided is not enabled or whitelisted.', - SHARDING_NO_SHARDS: 'No shards have been spawned.', - SHARDING_IN_PROCESS: 'Shards are still being spawned.', - SHARDING_INVALID_EVAL_BROADCAST: 'Script to evaluate must be a function', - SHARDING_SHARD_NOT_FOUND: (id) => `Shard ${id} could not be found.`, - SHARDING_ALREADY_SPAWNED: (count) => `Already spawned ${count} shards.`, - SHARDING_PROCESS_EXISTS: (id) => `Shard ${id} already has an active process.`, - SHARDING_WORKER_EXISTS: (id) => `Shard ${id} already has an active worker.`, - SHARDING_READY_TIMEOUT: (id) => - `Shard ${id}'s Client took too long to become ready.`, - SHARDING_READY_DISCONNECTED: (id) => - `Shard ${id}'s Client disconnected before becoming ready.`, - SHARDING_READY_DIED: (id) => - `Shard ${id}'s process exited before its Client became ready.`, - SHARDING_NO_CHILD_EXISTS: (id) => - `Shard ${id} has no active process or worker.`, - SHARDING_SHARD_MISCALCULATION: (shard, guild, count) => - `Calculated invalid shard ${shard} for guild ${guild} with ${count} shards.`, + SHARDING_INVALID: 'Invalid shard settings were provided.', + SHARDING_REQUIRED: 'This session would have handled too many guilds - Sharding is required.', + INVALID_INTENTS: 'Invalid intent provided for WebSocket intents.', + DISALLOWED_INTENTS: 'Privileged intent provided is not enabled or whitelisted.', + SHARDING_NO_SHARDS: 'No shards have been spawned.', + SHARDING_IN_PROCESS: 'Shards are still being spawned.', + SHARDING_INVALID_EVAL_BROADCAST: 'Script to evaluate must be a function', + SHARDING_SHARD_NOT_FOUND: id => `Shard ${id} could not be found.`, + SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards.`, + SHARDING_PROCESS_EXISTS: id => `Shard ${id} already has an active process.`, + SHARDING_WORKER_EXISTS: id => `Shard ${id} already has an active worker.`, + SHARDING_READY_TIMEOUT: id => `Shard ${id}'s Client took too long to become ready.`, + SHARDING_READY_DISCONNECTED: id => `Shard ${id}'s Client disconnected before becoming ready.`, + SHARDING_READY_DIED: id => `Shard ${id}'s process exited before its Client became ready.`, + SHARDING_NO_CHILD_EXISTS: id => `Shard ${id} has no active process or worker.`, + SHARDING_SHARD_MISCALCULATION: (shard, guild, count) => + `Calculated invalid shard ${shard} for guild ${guild} with ${count} shards.`, - COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).', - COLOR_CONVERT: 'Unable to convert color to a number.', + COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).', + COLOR_CONVERT: 'Unable to convert color to a number.', - INVITE_OPTIONS_MISSING_CHANNEL: - 'A valid guild channel must be provided when GuildScheduledEvent is EXTERNAL.', + INVITE_OPTIONS_MISSING_CHANNEL: 'A valid guild channel must be provided when GuildScheduledEvent is EXTERNAL.', - EMBED_TITLE: 'MessageEmbed title must be a string.', - EMBED_FIELD_NAME: 'MessageEmbed field names must be non-empty strings.', - EMBED_FIELD_VALUE: 'MessageEmbed field values must be non-empty strings.', - EMBED_FOOTER_TEXT: 'MessageEmbed footer text must be a string.', - EMBED_DESCRIPTION: 'MessageEmbed description must be a string.', - EMBED_AUTHOR_NAME: 'MessageEmbed author name must be a string.', - /* add */ - EMBED_PROVIDER_NAME: 'MessageEmbed provider name must be a string.', + EMBED_TITLE: 'MessageEmbed title must be a string.', + EMBED_FIELD_NAME: 'MessageEmbed field names must be non-empty strings.', + EMBED_FIELD_VALUE: 'MessageEmbed field values must be non-empty strings.', + EMBED_FOOTER_TEXT: 'MessageEmbed footer text must be a string.', + EMBED_DESCRIPTION: 'MessageEmbed description must be a string.', + EMBED_AUTHOR_NAME: 'MessageEmbed author name must be a string.', + /* add */ + EMBED_PROVIDER_NAME: 'MessageEmbed provider name must be a string.', - BUTTON_LABEL: 'MessageButton label must be a string', - BUTTON_URL: 'MessageButton URL must be a string', - BUTTON_CUSTOM_ID: 'MessageButton customId must be a string', + BUTTON_LABEL: 'MessageButton label must be a string', + BUTTON_URL: 'MessageButton URL must be a string', + BUTTON_CUSTOM_ID: 'MessageButton customId must be a string', - SELECT_MENU_CUSTOM_ID: 'MessageSelectMenu customId must be a string', - SELECT_MENU_PLACEHOLDER: 'MessageSelectMenu placeholder must be a string', - SELECT_OPTION_LABEL: 'MessageSelectOption label must be a string', - SELECT_OPTION_VALUE: 'MessageSelectOption value must be a string', - SELECT_OPTION_DESCRIPTION: 'MessageSelectOption description must be a string', + SELECT_MENU_CUSTOM_ID: 'MessageSelectMenu customId must be a string', + SELECT_MENU_PLACEHOLDER: 'MessageSelectMenu placeholder must be a string', + SELECT_OPTION_LABEL: 'MessageSelectOption label must be a string', + SELECT_OPTION_VALUE: 'MessageSelectOption value must be a string', + SELECT_OPTION_DESCRIPTION: 'MessageSelectOption description must be a string', - INTERACTION_COLLECTOR_ERROR: (reason) => - `Collector received no interactions before ending with reason: ${reason}`, + INTERACTION_COLLECTOR_ERROR: reason => `Collector received no interactions before ending with reason: ${reason}`, - FILE_NOT_FOUND: (file) => `File could not be found: ${file}`, + FILE_NOT_FOUND: file => `File could not be found: ${file}`, - USER_BANNER_NOT_FETCHED: - "You must fetch this user's banner before trying to generate its URL!", - USER_NO_DM_CHANNEL: 'No DM Channel exists!', + USER_BANNER_NOT_FETCHED: "You must fetch this user's banner before trying to generate its URL!", + USER_NO_DM_CHANNEL: 'No DM Channel exists!', - VOICE_NOT_STAGE_CHANNEL: 'You are only allowed to do this in stage channels.', + VOICE_NOT_STAGE_CHANNEL: 'You are only allowed to do this in stage channels.', - VOICE_STATE_NOT_OWN: - 'You cannot self-deafen/mute/request to speak on VoiceStates that do not belong to the ClientUser.', - VOICE_STATE_INVALID_TYPE: (name) => `${name} must be a boolean.`, + VOICE_STATE_NOT_OWN: + 'You cannot self-deafen/mute/request to speak on VoiceStates that do not belong to the ClientUser.', + VOICE_STATE_INVALID_TYPE: name => `${name} must be a boolean.`, - REQ_RESOURCE_TYPE: - 'The resource must be a string, Buffer or a valid file stream.', + REQ_RESOURCE_TYPE: 'The resource must be a string, Buffer or a valid file stream.', - IMAGE_FORMAT: (format) => `Invalid image format: ${format}`, - IMAGE_SIZE: (size) => `Invalid image size: ${size}`, + IMAGE_FORMAT: format => `Invalid image format: ${format}`, + IMAGE_SIZE: size => `Invalid image size: ${size}`, - MESSAGE_BULK_DELETE_TYPE: - 'The messages must be an Array, Collection, or number.', - MESSAGE_NONCE_TYPE: 'Message nonce must be an integer or a string.', - MESSAGE_CONTENT_TYPE: 'Message content must be a non-empty string.', + MESSAGE_BULK_DELETE_TYPE: 'The messages must be an Array, Collection, or number.', + MESSAGE_NONCE_TYPE: 'Message nonce must be an integer or a string.', + MESSAGE_CONTENT_TYPE: 'Message content must be a non-empty string.', - SPLIT_MAX_LEN: - 'Chunk exceeds the max length and contains no split characters.', + SPLIT_MAX_LEN: 'Chunk exceeds the max length and contains no split characters.', - BAN_RESOLVE_ID: (ban = false) => - `Couldn't resolve the user id to ${ban ? 'ban' : 'unban'}.`, - FETCH_BAN_RESOLVE_ID: "Couldn't resolve the user id to fetch the ban.", + BAN_RESOLVE_ID: (ban = false) => `Couldn't resolve the user id to ${ban ? 'ban' : 'unban'}.`, + FETCH_BAN_RESOLVE_ID: "Couldn't resolve the user id to fetch the ban.", - PRUNE_DAYS_TYPE: 'Days must be a number', + PRUNE_DAYS_TYPE: 'Days must be a number', - GUILD_CHANNEL_RESOLVE: 'Could not resolve channel to a guild channel.', - GUILD_VOICE_CHANNEL_RESOLVE: - 'Could not resolve channel to a guild voice channel.', - GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.', - GUILD_CHANNEL_UNOWNED: - "The fetched channel does not belong to this manager's guild.", - GUILD_OWNED: 'Guild is owned by the client.', - GUILD_MEMBERS_TIMEOUT: "Members didn't arrive in time.", - GUILD_UNCACHED_ME: 'The client user as a member of this guild is uncached.', - CHANNEL_NOT_CACHED: - 'Could not find the channel where this message came from in the cache!', - STAGE_CHANNEL_RESOLVE: 'Could not resolve channel to a stage channel.', - GUILD_SCHEDULED_EVENT_RESOLVE: 'Could not resolve the guild scheduled event.', + GUILD_CHANNEL_RESOLVE: 'Could not resolve channel to a guild channel.', + GUILD_VOICE_CHANNEL_RESOLVE: 'Could not resolve channel to a guild voice channel.', + GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.', + GUILD_CHANNEL_UNOWNED: "The fetched channel does not belong to this manager's guild.", + GUILD_OWNED: 'Guild is owned by the client.', + GUILD_MEMBERS_TIMEOUT: "Members didn't arrive in time.", + GUILD_UNCACHED_ME: 'The client user as a member of this guild is uncached.', + CHANNEL_NOT_CACHED: 'Could not find the channel where this message came from in the cache!', + STAGE_CHANNEL_RESOLVE: 'Could not resolve channel to a stage channel.', + GUILD_SCHEDULED_EVENT_RESOLVE: 'Could not resolve the guild scheduled event.', - INVALID_TYPE: (name, expected, an = false) => - `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`, - INVALID_ELEMENT: (type, name, elem) => - `Supplied ${type} ${name} includes an invalid element: ${elem}`, + INVALID_TYPE: (name, expected, an = false) => `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`, + INVALID_ELEMENT: (type, name, elem) => `Supplied ${type} ${name} includes an invalid element: ${elem}`, - MESSAGE_THREAD_PARENT: - 'The message was not sent in a guild text or news channel', - MESSAGE_EXISTING_THREAD: 'The message already has a thread', - THREAD_INVITABLE_TYPE: (type) => `Invitable cannot be edited on ${type}`, + MESSAGE_THREAD_PARENT: 'The message was not sent in a guild text or news channel', + MESSAGE_EXISTING_THREAD: 'The message already has a thread', + THREAD_INVITABLE_TYPE: type => `Invitable cannot be edited on ${type}`, - WEBHOOK_MESSAGE: 'The message was not sent by a webhook.', - WEBHOOK_TOKEN_UNAVAILABLE: - 'This action requires a webhook token, but none is available.', - WEBHOOK_URL_INVALID: 'The provided webhook URL is not valid.', - WEBHOOK_APPLICATION: - 'This message webhook belongs to an application and cannot be fetched.', - MESSAGE_REFERENCE_MISSING: 'The message does not reference another message', + WEBHOOK_MESSAGE: 'The message was not sent by a webhook.', + WEBHOOK_TOKEN_UNAVAILABLE: 'This action requires a webhook token, but none is available.', + WEBHOOK_URL_INVALID: 'The provided webhook URL is not valid.', + WEBHOOK_APPLICATION: 'This message webhook belongs to an application and cannot be fetched.', + MESSAGE_REFERENCE_MISSING: 'The message does not reference another message', - EMOJI_TYPE: 'Emoji must be a string or GuildEmoji/ReactionEmoji', - EMOJI_MANAGED: 'Emoji is managed and has no Author.', - MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION: (guild) => - `Client must have Manage Emojis and Stickers permission in guild ${guild} to see emoji authors.`, - NOT_GUILD_STICKER: - 'Sticker is a standard (non-guild) sticker and has no author.', + EMOJI_TYPE: 'Emoji must be a string or GuildEmoji/ReactionEmoji', + EMOJI_MANAGED: 'Emoji is managed and has no Author.', + MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION: guild => + `Client must have Manage Emojis and Stickers permission in guild ${guild} to see emoji authors.`, + NOT_GUILD_STICKER: 'Sticker is a standard (non-guild) sticker and has no author.', - REACTION_RESOLVE_USER: - "Couldn't resolve the user id to remove from the reaction.", + REACTION_RESOLVE_USER: "Couldn't resolve the user id to remove from the reaction.", - VANITY_URL: 'This guild does not have the VANITY_URL feature enabled.', + VANITY_URL: 'This guild does not have the VANITY_URL feature enabled.', - INVITE_RESOLVE_CODE: 'Could not resolve the code to fetch the invite.', + INVITE_RESOLVE_CODE: 'Could not resolve the code to fetch the invite.', - INVITE_NOT_FOUND: 'Could not find the requested invite.', + INVITE_NOT_FOUND: 'Could not find the requested invite.', - NOT_OWNER_GROUP_DM_CHANNEL: "You can't do this action [Missing Permission]", - USER_ALREADY_IN_GROUP_DM_CHANNEL: 'User is already in the channel.', - USER_NOT_IN_GROUP_DM_CHANNEL: 'User is not in the channel.', + NOT_OWNER_GROUP_DM_CHANNEL: "You can't do this action [Missing Permission]", + USER_ALREADY_IN_GROUP_DM_CHANNEL: 'User is already in the channel.', + USER_NOT_IN_GROUP_DM_CHANNEL: 'User is not in the channel.', - DELETE_GROUP_DM_CHANNEL: - "Bots don't have access to Group DM Channels and cannot delete them", - FETCH_GROUP_DM_CHANNEL: - "Bots don't have access to Group DM Channels and cannot fetch them", + DELETE_GROUP_DM_CHANNEL: "Bots don't have access to Group DM Channels and cannot delete them", + FETCH_GROUP_DM_CHANNEL: "Bots don't have access to Group DM Channels and cannot fetch them", - MEMBER_FETCH_NONCE_LENGTH: 'Nonce length must not exceed 32 characters.', + MEMBER_FETCH_NONCE_LENGTH: 'Nonce length must not exceed 32 characters.', - GLOBAL_COMMAND_PERMISSIONS: - 'Permissions for global commands may only be fetched or modified by providing a GuildResolvable ' + - "or from a guild's application command manager.", - GUILD_UNCACHED_ROLE_RESOLVE: - 'Cannot resolve roles from an arbitrary guild, provide an id instead', + GLOBAL_COMMAND_PERMISSIONS: + 'Permissions for global commands may only be fetched or modified by providing a GuildResolvable ' + + "or from a guild's application command manager.", + GUILD_UNCACHED_ROLE_RESOLVE: 'Cannot resolve roles from an arbitrary guild, provide an id instead', - INTERACTION_ALREADY_REPLIED: - 'The reply to this interaction has already been sent or deferred.', - INTERACTION_NOT_REPLIED: - 'The reply to this interaction has not been sent or deferred.', - INTERACTION_EPHEMERAL_REPLIED: 'Ephemeral responses cannot be deleted.', + INTERACTION_ALREADY_REPLIED: 'The reply to this interaction has already been sent or deferred.', + INTERACTION_NOT_REPLIED: 'The reply to this interaction has not been sent or deferred.', + INTERACTION_EPHEMERAL_REPLIED: 'Ephemeral responses cannot be deleted.', - COMMAND_INTERACTION_OPTION_NOT_FOUND: (name) => - `Required option "${name}" not found.`, - COMMAND_INTERACTION_OPTION_TYPE: (name, type, expected) => - `Option "${name}" is of type: ${type}; expected ${expected}.`, - COMMAND_INTERACTION_OPTION_EMPTY: (name, type) => - `Required option "${name}" is of type: ${type}; expected a non-empty value.`, - COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND: - 'No subcommand specified for interaction.', - COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP: - 'No subcommand group specified for interaction.', - AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION: - 'No focused option for autocomplete interaction.', + COMMAND_INTERACTION_OPTION_NOT_FOUND: name => `Required option "${name}" not found.`, + COMMAND_INTERACTION_OPTION_TYPE: (name, type, expected) => + `Option "${name}" is of type: ${type}; expected ${expected}.`, + COMMAND_INTERACTION_OPTION_EMPTY: (name, type) => + `Required option "${name}" is of type: ${type}; expected a non-empty value.`, + COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND: 'No subcommand specified for interaction.', + COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP: 'No subcommand group specified for interaction.', + AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION: 'No focused option for autocomplete interaction.', - INVITE_MISSING_SCOPES: - 'At least one valid scope must be provided for the invite', + INVITE_MISSING_SCOPES: 'At least one valid scope must be provided for the invite', - NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`, + NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`, - SWEEP_FILTER_RETURN: - 'The return value of the sweepFilter function was not false or a Function', + SWEEP_FILTER_RETURN: 'The return value of the sweepFilter function was not false or a Function', - INVALID_BOT_METHOD: `Bot accounts cannot use this method`, - INVALID_USER_METHOD: `User accounts cannot use this method`, - INVALID_LOCALE: 'Unable to select this location', - FOLDER_NOT_FOUND: 'Server directory not found', - FOLDER_POSITION_INVALID: 'The server index in the directory is invalid', - APPLICATION_ID_INVALID: "The application isn't BOT", - INVALID_NITRO: 'Invalid Nitro Code', - MESSAGE_ID_NOT_FOUND: 'Message ID not found', - MESSAGE_EMBED_LINK_LENGTH: - 'Message content with embed link length is too long', - GUILD_MEMBERS_FETCH: (msg) => `${msg}` + INVALID_BOT_METHOD: 'Bot accounts cannot use this method', + INVALID_USER_METHOD: 'User accounts cannot use this method', + INVALID_LOCALE: 'Unable to select this location', + FOLDER_NOT_FOUND: 'Server directory not found', + FOLDER_POSITION_INVALID: 'The server index in the directory is invalid', + APPLICATION_ID_INVALID: "The application isn't BOT", + INVALID_NITRO: 'Invalid Nitro Code', + MESSAGE_ID_NOT_FOUND: 'Message ID not found', + MESSAGE_EMBED_LINK_LENGTH: 'Message content with embed link length is too long', + GUILD_MEMBERS_FETCH: msg => `${msg}`, }; for (const [name, message] of Object.entries(Messages)) register(name, message); diff --git a/src/index.js b/src/index.js index 754cf72..18e1e34 100644 --- a/src/index.js +++ b/src/index.js @@ -155,4 +155,4 @@ exports.Widget = require('./structures/Widget'); exports.WidgetMember = require('./structures/WidgetMember'); exports.WelcomeChannel = require('./structures/WelcomeChannel'); exports.WelcomeScreen = require('./structures/WelcomeScreen'); -exports.WebSocket = require('./WebSocket'); \ No newline at end of file +exports.WebSocket = require('./WebSocket'); diff --git a/src/managers/ApplicationCommandManager.js b/src/managers/ApplicationCommandManager.js index df93e25..ab978af 100644 --- a/src/managers/ApplicationCommandManager.js +++ b/src/managers/ApplicationCommandManager.js @@ -1,224 +1,224 @@ -'use strict'; - -const { Collection } = require('@discordjs/collection'); -const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager'); -const CachedManager = require('./CachedManager'); -const { TypeError } = require('../errors'); -const ApplicationCommand = require('../structures/ApplicationCommand'); -const { ApplicationCommandTypes } = require('../util/Constants'); - -/** - * Manages API methods for application commands and stores their cache. - * @extends {CachedManager} - */ -class ApplicationCommandManager extends CachedManager { - constructor(client, iterable, user) { - super(client, ApplicationCommand, iterable); - - /** - * The manager for permissions of arbitrary commands on arbitrary guilds - * @type {ApplicationCommandPermissionsManager} - */ - this.permissions = new ApplicationCommandPermissionsManager(this, user); - this.user = user; - } - - /** - * The cache of this manager - * @type {Collection} - * @name ApplicationCommandManager#cache - */ - - _add(data, cache, guildId) { - return super._add(data, cache, { extras: [this.guild, guildId] }); - } - - /** - * The APIRouter path to the commands - * @param {Snowflake} [options.id] The application command's id - * @param {Snowflake} [options.guildId] The guild's id to use in the path, - * ignored when using a {@link GuildApplicationCommandManager} - * @returns {Object} - * @private - */ - commandPath({ id, guildId } = {}) { - let path = this.client.api.applications(this.user.id); - if (this.guild ?? guildId) path = path.guilds(this.guild?.id ?? guildId); - return id ? path.commands(id) : path.commands; - } - - /** - * Data that resolves to give an ApplicationCommand object. This can be: - * * An ApplicationCommand object - * * A Snowflake - * @typedef {ApplicationCommand|Snowflake} ApplicationCommandResolvable - */ - - /** - * Options used to fetch data from Discord - * @typedef {Object} BaseFetchOptions - * @property {boolean} [cache=true] Whether to cache the fetched data if it wasn't already - * @property {boolean} [force=false] Whether to skip the cache check and request the API - */ - - /** - * Options used to fetch Application Commands from Discord - * @typedef {BaseFetchOptions} FetchApplicationCommandOptions - * @property {Snowflake} [guildId] The guild's id to fetch commands for, for when the guild is not cached - */ - - /** - * Obtains one or multiple application commands from Discord, or the cache if it's already available. - * @param {Snowflake} [id] The application command's id - * @param {FetchApplicationCommandOptions} [options] Additional options for this fetch - * @returns {Promise>} - * @example - * // Fetch a single command - * client.application.commands.fetch('123456789012345678') - * .then(command => console.log(`Fetched command ${command.name}`)) - * .catch(console.error); - * @example - * // Fetch all commands - * guild.commands.fetch() - * .then(commands => console.log(`Fetched ${commands.size} commands`)) - * .catch(console.error); - */ - async fetch(id, { guildId, cache = true, force = false } = {}) { - // change from user.createDM to opcode (risky action) - if (typeof id === 'object') { - ({ guildId, cache = true } = id); - } else if (id) { - if (!force) { - const existing = this.cache.get(id); - if (existing) return existing; - } - await this.user.createDM().catch(() => {}); - const command = await this.commandPath({ id, guildId }).get(); - return this._add(command, cache); - } - await this.user.createDM().catch(() => {}); - const data = await this.commandPath({ guildId }).get(); - return data.reduce((coll, command) => coll.set(command.id, this._add(command, cache, guildId)), new Collection()); - } - - /** - * Creates an application command. - * @param {ApplicationCommandData|APIApplicationCommand} command The command - * @param {Snowflake} [guildId] The guild's id to create this command in, - * ignored when using a {@link GuildApplicationCommandManager} - * @returns {Promise} - * @example - * // Create a new command - * client.application.commands.create({ - * name: 'test', - * description: 'A test command', - * }) - * .then(console.log) - * .catch(console.error); - */ - async create(command, guildId) { - if(!this.client.user.bot) throw new Error("INVALID_USER_METHOD"); - const data = await this.commandPath({ guildId }).post({ - data: this.constructor.transformCommand(command), - }); - return this._add(data, true, guildId); - } - - /** - * Sets all the commands for this application or guild. - * @param {ApplicationCommandData[]|APIApplicationCommand[]} commands The commands - * @param {Snowflake} [guildId] The guild's id to create the commands in, - * ignored when using a {@link GuildApplicationCommandManager} - * @returns {Promise>} - * @example - * // Set all commands to just this one - * client.application.commands.set([ - * { - * name: 'test', - * description: 'A test command', - * }, - * ]) - * .then(console.log) - * .catch(console.error); - * @example - * // Remove all commands - * guild.commands.set([]) - * .then(console.log) - * .catch(console.error); - */ - async set(commands, guildId) { - if(!this.client.user.bot) throw new Error("INVALID_USER_METHOD"); - const data = await this.commandPath({ guildId }).put({ - data: commands.map(c => this.constructor.transformCommand(c)), - }); - return data.reduce((coll, command) => coll.set(command.id, this._add(command, true, guildId)), new Collection()); - } - - /** - * Edits an application command. - * @param {ApplicationCommandResolvable} command The command to edit - * @param {ApplicationCommandData|APIApplicationCommand} data The data to update the command with - * @param {Snowflake} [guildId] The guild's id where the command registered, - * ignored when using a {@link GuildApplicationCommandManager} - * @returns {Promise} - * @example - * // Edit an existing command - * client.application.commands.edit('123456789012345678', { - * description: 'New description', - * }) - * .then(console.log) - * .catch(console.error); - */ - async edit(command, data, guildId) { - if(!this.client.user.bot) throw new Error("INVALID_USER_METHOD"); - const id = this.resolveId(command); - if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); - - const patched = await this.commandPath({ id, guildId }).patch({ - data: this.constructor.transformCommand(data), - }); - return this._add(patched, true, guildId); - } - - /** - * Deletes an application command. - * @param {ApplicationCommandResolvable} command The command to delete - * @param {Snowflake} [guildId] The guild's id where the command is registered, - * ignored when using a {@link GuildApplicationCommandManager} - * @returns {Promise} - * @example - * // Delete a command - * guild.commands.delete('123456789012345678') - * .then(console.log) - * .catch(console.error); - */ - async delete(command, guildId) { - if(!this.client.user.bot) throw new Error("INVALID_USER_METHOD"); - const id = this.resolveId(command); - if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); - - await this.commandPath({ id, guildId }).delete(); - - const cached = this.cache.get(id); - this.cache.delete(id); - return cached ?? null; - } - - /** - * Transforms an {@link ApplicationCommandData} object into something that can be used with the API. - * @param {ApplicationCommandData|APIApplicationCommand} command The command to transform - * @returns {APIApplicationCommand} - * @private - */ - static transformCommand(command) { - return { - name: command.name, - description: command.description, - type: typeof command.type === 'number' ? command.type : ApplicationCommandTypes[command.type], - options: command.options?.map(o => ApplicationCommand.transformOption(o)), - default_permission: command.defaultPermission ?? command.default_permission, - }; - } -} - -module.exports = ApplicationCommandManager; +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager'); +const CachedManager = require('./CachedManager'); +const { TypeError } = require('../errors'); +const ApplicationCommand = require('../structures/ApplicationCommand'); +const { ApplicationCommandTypes } = require('../util/Constants'); + +/** + * Manages API methods for application commands and stores their cache. + * @extends {CachedManager} + */ +class ApplicationCommandManager extends CachedManager { + constructor(client, iterable, user) { + super(client, ApplicationCommand, iterable); + + /** + * The manager for permissions of arbitrary commands on arbitrary guilds + * @type {ApplicationCommandPermissionsManager} + */ + this.permissions = new ApplicationCommandPermissionsManager(this, user); + this.user = user; + } + + /** + * The cache of this manager + * @type {Collection} + * @name ApplicationCommandManager#cache + */ + + _add(data, cache, guildId) { + return super._add(data, cache, { extras: [this.guild, guildId] }); + } + + /** + * The APIRouter path to the commands + * @param {Snowflake} [options.id] The application command's id + * @param {Snowflake} [options.guildId] The guild's id to use in the path, + * ignored when using a {@link GuildApplicationCommandManager} + * @returns {Object} + * @private + */ + commandPath({ id, guildId } = {}) { + let path = this.client.api.applications(this.user.id); + if (this.guild ?? guildId) path = path.guilds(this.guild?.id ?? guildId); + return id ? path.commands(id) : path.commands; + } + + /** + * Data that resolves to give an ApplicationCommand object. This can be: + * * An ApplicationCommand object + * * A Snowflake + * @typedef {ApplicationCommand|Snowflake} ApplicationCommandResolvable + */ + + /** + * Options used to fetch data from Discord + * @typedef {Object} BaseFetchOptions + * @property {boolean} [cache=true] Whether to cache the fetched data if it wasn't already + * @property {boolean} [force=false] Whether to skip the cache check and request the API + */ + + /** + * Options used to fetch Application Commands from Discord + * @typedef {BaseFetchOptions} FetchApplicationCommandOptions + * @property {Snowflake} [guildId] The guild's id to fetch commands for, for when the guild is not cached + */ + + /** + * Obtains one or multiple application commands from Discord, or the cache if it's already available. + * @param {Snowflake} [id] The application command's id + * @param {FetchApplicationCommandOptions} [options] Additional options for this fetch + * @returns {Promise>} + * @example + * // Fetch a single command + * client.application.commands.fetch('123456789012345678') + * .then(command => console.log(`Fetched command ${command.name}`)) + * .catch(console.error); + * @example + * // Fetch all commands + * guild.commands.fetch() + * .then(commands => console.log(`Fetched ${commands.size} commands`)) + * .catch(console.error); + */ + async fetch(id, { guildId, cache = true, force = false } = {}) { + // change from user.createDM to opcode (risky action) + if (typeof id === 'object') { + ({ guildId, cache = true } = id); + } else if (id) { + if (!force) { + const existing = this.cache.get(id); + if (existing) return existing; + } + await this.user.createDM().catch(() => {}); + const command = await this.commandPath({ id, guildId }).get(); + return this._add(command, cache); + } + await this.user.createDM().catch(() => {}); + const data = await this.commandPath({ guildId }).get(); + return data.reduce((coll, command) => coll.set(command.id, this._add(command, cache, guildId)), new Collection()); + } + + /** + * Creates an application command. + * @param {ApplicationCommandData|APIApplicationCommand} command The command + * @param {Snowflake} [guildId] The guild's id to create this command in, + * ignored when using a {@link GuildApplicationCommandManager} + * @returns {Promise} + * @example + * // Create a new command + * client.application.commands.create({ + * name: 'test', + * description: 'A test command', + * }) + * .then(console.log) + * .catch(console.error); + */ + async create(command, guildId) { + if (!this.client.user.bot) throw new Error('INVALID_USER_METHOD'); + const data = await this.commandPath({ guildId }).post({ + data: this.constructor.transformCommand(command), + }); + return this._add(data, true, guildId); + } + + /** + * Sets all the commands for this application or guild. + * @param {ApplicationCommandData[]|APIApplicationCommand[]} commands The commands + * @param {Snowflake} [guildId] The guild's id to create the commands in, + * ignored when using a {@link GuildApplicationCommandManager} + * @returns {Promise>} + * @example + * // Set all commands to just this one + * client.application.commands.set([ + * { + * name: 'test', + * description: 'A test command', + * }, + * ]) + * .then(console.log) + * .catch(console.error); + * @example + * // Remove all commands + * guild.commands.set([]) + * .then(console.log) + * .catch(console.error); + */ + async set(commands, guildId) { + if (!this.client.user.bot) throw new Error('INVALID_USER_METHOD'); + const data = await this.commandPath({ guildId }).put({ + data: commands.map(c => this.constructor.transformCommand(c)), + }); + return data.reduce((coll, command) => coll.set(command.id, this._add(command, true, guildId)), new Collection()); + } + + /** + * Edits an application command. + * @param {ApplicationCommandResolvable} command The command to edit + * @param {ApplicationCommandData|APIApplicationCommand} data The data to update the command with + * @param {Snowflake} [guildId] The guild's id where the command registered, + * ignored when using a {@link GuildApplicationCommandManager} + * @returns {Promise} + * @example + * // Edit an existing command + * client.application.commands.edit('123456789012345678', { + * description: 'New description', + * }) + * .then(console.log) + * .catch(console.error); + */ + async edit(command, data, guildId) { + if (!this.client.user.bot) throw new Error('INVALID_USER_METHOD'); + const id = this.resolveId(command); + if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); + + const patched = await this.commandPath({ id, guildId }).patch({ + data: this.constructor.transformCommand(data), + }); + return this._add(patched, true, guildId); + } + + /** + * Deletes an application command. + * @param {ApplicationCommandResolvable} command The command to delete + * @param {Snowflake} [guildId] The guild's id where the command is registered, + * ignored when using a {@link GuildApplicationCommandManager} + * @returns {Promise} + * @example + * // Delete a command + * guild.commands.delete('123456789012345678') + * .then(console.log) + * .catch(console.error); + */ + async delete(command, guildId) { + if (!this.client.user.bot) throw new Error('INVALID_USER_METHOD'); + const id = this.resolveId(command); + if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); + + await this.commandPath({ id, guildId }).delete(); + + const cached = this.cache.get(id); + this.cache.delete(id); + return cached ?? null; + } + + /** + * Transforms an {@link ApplicationCommandData} object into something that can be used with the API. + * @param {ApplicationCommandData|APIApplicationCommand} command The command to transform + * @returns {APIApplicationCommand} + * @private + */ + static transformCommand(command) { + return { + name: command.name, + description: command.description, + type: typeof command.type === 'number' ? command.type : ApplicationCommandTypes[command.type], + options: command.options?.map(o => ApplicationCommand.transformOption(o)), + default_permission: command.defaultPermission ?? command.default_permission, + }; + } +} + +module.exports = ApplicationCommandManager; diff --git a/src/managers/ApplicationCommandPermissionsManager.js b/src/managers/ApplicationCommandPermissionsManager.js index cf2048a..4eeaea7 100644 --- a/src/managers/ApplicationCommandPermissionsManager.js +++ b/src/managers/ApplicationCommandPermissionsManager.js @@ -1,422 +1,425 @@ -'use strict'; - -const { Collection } = require('@discordjs/collection'); -const BaseManager = require('./BaseManager'); -const { Error, TypeError } = require('../errors'); -const { ApplicationCommandPermissionTypes, APIErrors } = require('../util/Constants'); - -/** - * Manages API methods for permissions of Application Commands. - * @extends {BaseManager} - */ -class ApplicationCommandPermissionsManager extends BaseManager { - constructor(manager, user) { - super(manager.client); - - /** - * The manager or command that this manager belongs to - * @type {ApplicationCommandManager|ApplicationCommand} - * @private - */ - this.manager = manager; - - /** - * The guild that this manager acts on - * @type {?Guild} - */ - this.guild = manager.guild ?? null; - - /** - * The id of the guild that this manager acts on - * @type {?Snowflake} - */ - this.guildId = manager.guildId ?? manager.guild?.id ?? null; - - /** - * The id of the command this manager acts on - * @type {?Snowflake} - */ - this.commandId = manager.id ?? null; - - this.user = user; - } - - /** - * The APIRouter path to the commands - * @param {Snowflake} guildId The guild's id to use in the path, - * @param {Snowflake} [commandId] The application command's id - * @returns {Object} - * @private - */ - permissionsPath(guildId, commandId) { - return this.client.api.applications(typeof this.user == 'string' ? this.user : this.user.id).guilds(guildId).commands(commandId).permissions; - } - - /** - * Data for setting the permissions of an application command. - * @typedef {Object} ApplicationCommandPermissionData - * @property {Snowflake} id The role or user's id - * @property {ApplicationCommandPermissionType|number} type Whether this permission is for a role or a user - * @property {boolean} permission Whether the role or user has the permission to use this command - */ - - /** - * The object returned when fetching permissions for an application command. - * @typedef {Object} ApplicationCommandPermissions - * @property {Snowflake} id The role or user's id - * @property {ApplicationCommandPermissionType} type Whether this permission is for a role or a user - * @property {boolean} permission Whether the role or user has the permission to use this command - */ - - /** - * Options for managing permissions for one or more Application Commands - * When passing these options to a manager where `guildId` is `null`, - * `guild` is a required parameter - * @typedef {Object} BaseApplicationCommandPermissionsOptions - * @property {GuildResolvable} [guild] The guild to modify / check permissions for - * Ignored when the manager has a non-null `guildId` property - * @property {ApplicationCommandResolvable} [command] The command to modify / check permissions for - * Ignored when the manager has a non-null `commandId` property - */ - - /** - * Fetches the permissions for one or multiple commands. - * @param {BaseApplicationCommandPermissionsOptions} [options] Options used to fetch permissions - * @returns {Promise>} - * @example - * // Fetch permissions for one command - * guild.commands.permissions.fetch({ command: '123456789012345678' }) - * .then(perms => console.log(`Fetched permissions for ${perms.length} users`)) - * .catch(console.error); - * @example - * // Fetch permissions for all commands in a guild - * client.application.commands.permissions.fetch({ guild: '123456789012345678' }) - * .then(perms => console.log(`Fetched permissions for ${perms.size} commands`)) - * .catch(console.error); - */ - async fetch({ guild, command } = {}) { - const { guildId, commandId } = this._validateOptions(guild, command); - if (commandId) { - const data = await this.permissionsPath(guildId, commandId).get(); - return data.permissions.map(perm => this.constructor.transformPermissions(perm, true)); - } - - const data = await this.permissionsPath(guildId).get(); - return data.reduce( - (coll, perm) => - coll.set( - perm.id, - perm.permissions.map(p => this.constructor.transformPermissions(p, true)), - ), - new Collection(), - ); - } - - /** - * Data used for overwriting the permissions for all application commands in a guild. - * @typedef {Object} GuildApplicationCommandPermissionData - * @property {Snowflake} id The command's id - * @property {ApplicationCommandPermissionData[]} permissions The permissions for this command - */ - - /** - * Options used to set permissions for one or more Application Commands in a guild - * One of `command` AND `permissions`, OR `fullPermissions` is required. - * `fullPermissions` is not a valid option when passing to a manager where `commandId` is non-null - * @typedef {BaseApplicationCommandPermissionsOptions} SetApplicationCommandPermissionsOptions - * @property {ApplicationCommandPermissionData[]} [permissions] The new permissions for the command - * @property {GuildApplicationCommandPermissionData[]} [fullPermissions] The new permissions for all commands - * in a guild When this parameter is set, `permissions` and `command` are ignored - */ - - /** - * Sets the permissions for one or more commands. - * @param {SetApplicationCommandPermissionsOptions} options Options used to set permissions - * @returns {Promise>} - * @example - * // Set the permissions for one command - * client.application.commands.permissions.set({ guild: '892455839386304532', command: '123456789012345678', - * permissions: [ - * { - * id: '876543210987654321', - * type: 'USER', - * permission: false, - * }, - * ]}) - * .then(console.log) - * .catch(console.error); - * @example - * // Set the permissions for all commands - * guild.commands.permissions.set({ fullPermissions: [ - * { - * id: '123456789012345678', - * permissions: [{ - * id: '876543210987654321', - * type: 'USER', - * permission: false, - * }], - * }, - * ]}) - * .then(console.log) - * .catch(console.error); - */ - async set({ guild, command, permissions, fullPermissions } = {}) { - if(!this.manager.client.user.bot) throw new Error("INVALID_USER_METHOD"); - const { guildId, commandId } = this._validateOptions(guild, command); - - if (commandId) { - if (!Array.isArray(permissions)) { - throw new TypeError('INVALID_TYPE', 'permissions', 'Array of ApplicationCommandPermissionData', true); - } - const data = await this.permissionsPath(guildId, commandId).put({ - data: { permissions: permissions.map(perm => this.constructor.transformPermissions(perm)) }, - }); - return data.permissions.map(perm => this.constructor.transformPermissions(perm, true)); - } - - if (!Array.isArray(fullPermissions)) { - throw new TypeError('INVALID_TYPE', 'fullPermissions', 'Array of GuildApplicationCommandPermissionData', true); - } - - const APIPermissions = []; - for (const perm of fullPermissions) { - if (!Array.isArray(perm.permissions)) throw new TypeError('INVALID_ELEMENT', 'Array', 'fullPermissions', perm); - APIPermissions.push({ - id: perm.id, - permissions: perm.permissions.map(p => this.constructor.transformPermissions(p)), - }); - } - const data = await this.permissionsPath(guildId).put({ - data: APIPermissions, - }); - return data.reduce( - (coll, perm) => - coll.set( - perm.id, - perm.permissions.map(p => this.constructor.transformPermissions(p, true)), - ), - new Collection(), - ); - } - - /** - * Options used to add permissions to a command - * The `command` parameter is not optional when the managers `commandId` is `null` - * @typedef {BaseApplicationCommandPermissionsOptions} AddApplicationCommandPermissionsOptions - * @property {ApplicationCommandPermissionData[]} permissions The permissions to add to the command - */ - - /** - * Add permissions to a command. - * @param {AddApplicationCommandPermissionsOptions} options Options used to add permissions - * @returns {Promise} - * @example - * // Block a role from the command permissions - * guild.commands.permissions.add({ command: '123456789012345678', permissions: [ - * { - * id: '876543211234567890', - * type: 'ROLE', - * permission: false - * }, - * ]}) - * .then(console.log) - * .catch(console.error); - */ - async add({ guild, command, permissions }) { - if(!this.manager.client.user.bot) throw new Error("INVALID_USER_METHOD"); - const { guildId, commandId } = this._validateOptions(guild, command); - if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); - if (!Array.isArray(permissions)) { - throw new TypeError('INVALID_TYPE', 'permissions', 'Array of ApplicationCommandPermissionData', true); - } - - let existing = []; - try { - existing = await this.fetch({ guild: guildId, command: commandId }); - } catch (error) { - if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; - } - - const newPermissions = permissions.slice(); - for (const perm of existing) { - if (!newPermissions.some(x => x.id === perm.id)) { - newPermissions.push(perm); - } - } - - return this.set({ guild: guildId, command: commandId, permissions: newPermissions }); - } - - /** - * Options used to remove permissions from a command - * The `command` parameter is not optional when the managers `commandId` is `null` - * @typedef {BaseApplicationCommandPermissionsOptions} RemoveApplicationCommandPermissionsOptions - * @property {UserResolvable|UserResolvable[]} [users] The user(s) to remove from the command permissions - * One of `users` or `roles` is required - * @property {RoleResolvable|RoleResolvable[]} [roles] The role(s) to remove from the command permissions - * One of `users` or `roles` is required - */ - - /** - * Remove permissions from a command. - * @param {RemoveApplicationCommandPermissionsOptions} options Options used to remove permissions - * @returns {Promise} - * @example - * // Remove a user permission from this command - * guild.commands.permissions.remove({ command: '123456789012345678', users: '876543210123456789' }) - * .then(console.log) - * .catch(console.error); - * @example - * // Remove multiple roles from this command - * guild.commands.permissions.remove({ - * command: '123456789012345678', roles: ['876543210123456789', '765432101234567890'] - * }) - * .then(console.log) - * .catch(console.error); - */ - async remove({ guild, command, users, roles }) { - if(!this.manager.client.user.bot) throw new Error("INVALID_USER_METHOD"); - const { guildId, commandId } = this._validateOptions(guild, command); - if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); - - if (!users && !roles) throw new TypeError('INVALID_TYPE', 'users OR roles', 'Array or Resolvable', true); - - let resolvedIds = []; - if (Array.isArray(users)) { - users.forEach(user => { - const userId = this.client.users.resolveId(user); - if (!userId) throw new TypeError('INVALID_ELEMENT', 'Array', 'users', user); - resolvedIds.push(userId); - }); - } else if (users) { - const userId = this.client.users.resolveId(users); - if (!userId) { - throw new TypeError('INVALID_TYPE', 'users', 'Array or UserResolvable'); - } - resolvedIds.push(userId); - } - - if (Array.isArray(roles)) { - roles.forEach(role => { - if (typeof role === 'string') { - resolvedIds.push(role); - return; - } - if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE'); - const roleId = this.guild.roles.resolveId(role); - if (!roleId) throw new TypeError('INVALID_ELEMENT', 'Array', 'users', role); - resolvedIds.push(roleId); - }); - } else if (roles) { - if (typeof roles === 'string') { - resolvedIds.push(roles); - } else { - if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE'); - const roleId = this.guild.roles.resolveId(roles); - if (!roleId) { - throw new TypeError('INVALID_TYPE', 'users', 'Array or RoleResolvable'); - } - resolvedIds.push(roleId); - } - } - - let existing = []; - try { - existing = await this.fetch({ guild: guildId, command: commandId }); - } catch (error) { - if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; - } - - const permissions = existing.filter(perm => !resolvedIds.includes(perm.id)); - - return this.set({ guild: guildId, command: commandId, permissions }); - } - - /** - * Options used to check the existence of permissions on a command - * The `command` parameter is not optional when the managers `commandId` is `null` - * @typedef {BaseApplicationCommandPermissionsOptions} HasApplicationCommandPermissionsOptions - * @property {UserResolvable|RoleResolvable} permissionId The user or role to check if a permission exists for - * on this command. - */ - - /** - * Check whether a permission exists for a user or role - * @param {AddApplicationCommandPermissionsOptions} options Options used to check permissions - * @returns {Promise} - * @example - * // Check whether a user has permission to use a command - * guild.commands.permissions.has({ command: '123456789012345678', permissionId: '876543210123456789' }) - * .then(console.log) - * .catch(console.error); - */ - async has({ guild, command, permissionId }) { - const { guildId, commandId } = this._validateOptions(guild, command); - if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); - - if (!permissionId) throw new TypeError('INVALID_TYPE', 'permissionId', 'UserResolvable or RoleResolvable'); - let resolvedId = permissionId; - if (typeof permissionId !== 'string') { - resolvedId = this.client.users.resolveId(permissionId); - if (!resolvedId) { - if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE'); - resolvedId = this.guild.roles.resolveId(permissionId); - } - if (!resolvedId) { - throw new TypeError('INVALID_TYPE', 'permissionId', 'UserResolvable or RoleResolvable'); - } - } - - let existing = []; - try { - existing = await this.fetch({ guild: guildId, command: commandId }); - } catch (error) { - if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; - } - - return existing.some(perm => perm.id === resolvedId); - } - - _validateOptions(guild, command) { - const guildId = this.guildId ?? this.client.guilds.resolveId(guild); - if (!guildId) throw new Error('GLOBAL_COMMAND_PERMISSIONS'); - let commandId = this.commandId; - if (command && !commandId) { - commandId = this.manager.resolveId?.(command); - if (!commandId && this.guild) { - commandId = this.guild.commands.resolveId(command); - } - commandId ??= this.client.application?.commands.resolveId(command); - if (!commandId) { - throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable', true); - } - } - return { guildId, commandId }; - } - - /** - * Transforms an {@link ApplicationCommandPermissionData} object into something that can be used with the API. - * @param {ApplicationCommandPermissionData} permissions The permissions to transform - * @param {boolean} [received] Whether these permissions have been received from Discord - * @returns {APIApplicationCommandPermissions} - * @private - */ - static transformPermissions(permissions, received) { - return { - id: permissions.id, - permission: permissions.permission, - type: - typeof permissions.type === 'number' && !received - ? permissions.type - : ApplicationCommandPermissionTypes[permissions.type], - }; - } -} - -module.exports = ApplicationCommandPermissionsManager; - -/* eslint-disable max-len */ -/** - * @external APIApplicationCommandPermissions - * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-structure} - */ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const BaseManager = require('./BaseManager'); +const { Error, TypeError } = require('../errors'); +const { ApplicationCommandPermissionTypes, APIErrors } = require('../util/Constants'); + +/** + * Manages API methods for permissions of Application Commands. + * @extends {BaseManager} + */ +class ApplicationCommandPermissionsManager extends BaseManager { + constructor(manager, user) { + super(manager.client); + + /** + * The manager or command that this manager belongs to + * @type {ApplicationCommandManager|ApplicationCommand} + * @private + */ + this.manager = manager; + + /** + * The guild that this manager acts on + * @type {?Guild} + */ + this.guild = manager.guild ?? null; + + /** + * The id of the guild that this manager acts on + * @type {?Snowflake} + */ + this.guildId = manager.guildId ?? manager.guild?.id ?? null; + + /** + * The id of the command this manager acts on + * @type {?Snowflake} + */ + this.commandId = manager.id ?? null; + + this.user = user; + } + + /** + * The APIRouter path to the commands + * @param {Snowflake} guildId The guild's id to use in the path, + * @param {Snowflake} [commandId] The application command's id + * @returns {Object} + * @private + */ + permissionsPath(guildId, commandId) { + return this.client.api + .applications(typeof this.user == 'string' ? this.user : this.user.id) + .guilds(guildId) + .commands(commandId).permissions; + } + + /** + * Data for setting the permissions of an application command. + * @typedef {Object} ApplicationCommandPermissionData + * @property {Snowflake} id The role or user's id + * @property {ApplicationCommandPermissionType|number} type Whether this permission is for a role or a user + * @property {boolean} permission Whether the role or user has the permission to use this command + */ + + /** + * The object returned when fetching permissions for an application command. + * @typedef {Object} ApplicationCommandPermissions + * @property {Snowflake} id The role or user's id + * @property {ApplicationCommandPermissionType} type Whether this permission is for a role or a user + * @property {boolean} permission Whether the role or user has the permission to use this command + */ + + /** + * Options for managing permissions for one or more Application Commands + * When passing these options to a manager where `guildId` is `null`, + * `guild` is a required parameter + * @typedef {Object} BaseApplicationCommandPermissionsOptions + * @property {GuildResolvable} [guild] The guild to modify / check permissions for + * Ignored when the manager has a non-null `guildId` property + * @property {ApplicationCommandResolvable} [command] The command to modify / check permissions for + * Ignored when the manager has a non-null `commandId` property + */ + + /** + * Fetches the permissions for one or multiple commands. + * @param {BaseApplicationCommandPermissionsOptions} [options] Options used to fetch permissions + * @returns {Promise>} + * @example + * // Fetch permissions for one command + * guild.commands.permissions.fetch({ command: '123456789012345678' }) + * .then(perms => console.log(`Fetched permissions for ${perms.length} users`)) + * .catch(console.error); + * @example + * // Fetch permissions for all commands in a guild + * client.application.commands.permissions.fetch({ guild: '123456789012345678' }) + * .then(perms => console.log(`Fetched permissions for ${perms.size} commands`)) + * .catch(console.error); + */ + async fetch({ guild, command } = {}) { + const { guildId, commandId } = this._validateOptions(guild, command); + if (commandId) { + const data = await this.permissionsPath(guildId, commandId).get(); + return data.permissions.map(perm => this.constructor.transformPermissions(perm, true)); + } + + const data = await this.permissionsPath(guildId).get(); + return data.reduce( + (coll, perm) => + coll.set( + perm.id, + perm.permissions.map(p => this.constructor.transformPermissions(p, true)), + ), + new Collection(), + ); + } + + /** + * Data used for overwriting the permissions for all application commands in a guild. + * @typedef {Object} GuildApplicationCommandPermissionData + * @property {Snowflake} id The command's id + * @property {ApplicationCommandPermissionData[]} permissions The permissions for this command + */ + + /** + * Options used to set permissions for one or more Application Commands in a guild + * One of `command` AND `permissions`, OR `fullPermissions` is required. + * `fullPermissions` is not a valid option when passing to a manager where `commandId` is non-null + * @typedef {BaseApplicationCommandPermissionsOptions} SetApplicationCommandPermissionsOptions + * @property {ApplicationCommandPermissionData[]} [permissions] The new permissions for the command + * @property {GuildApplicationCommandPermissionData[]} [fullPermissions] The new permissions for all commands + * in a guild When this parameter is set, `permissions` and `command` are ignored + */ + + /** + * Sets the permissions for one or more commands. + * @param {SetApplicationCommandPermissionsOptions} options Options used to set permissions + * @returns {Promise>} + * @example + * // Set the permissions for one command + * client.application.commands.permissions.set({ guild: '892455839386304532', command: '123456789012345678', + * permissions: [ + * { + * id: '876543210987654321', + * type: 'USER', + * permission: false, + * }, + * ]}) + * .then(console.log) + * .catch(console.error); + * @example + * // Set the permissions for all commands + * guild.commands.permissions.set({ fullPermissions: [ + * { + * id: '123456789012345678', + * permissions: [{ + * id: '876543210987654321', + * type: 'USER', + * permission: false, + * }], + * }, + * ]}) + * .then(console.log) + * .catch(console.error); + */ + async set({ guild, command, permissions, fullPermissions } = {}) { + if (!this.manager.client.user.bot) throw new Error('INVALID_USER_METHOD'); + const { guildId, commandId } = this._validateOptions(guild, command); + + if (commandId) { + if (!Array.isArray(permissions)) { + throw new TypeError('INVALID_TYPE', 'permissions', 'Array of ApplicationCommandPermissionData', true); + } + const data = await this.permissionsPath(guildId, commandId).put({ + data: { permissions: permissions.map(perm => this.constructor.transformPermissions(perm)) }, + }); + return data.permissions.map(perm => this.constructor.transformPermissions(perm, true)); + } + + if (!Array.isArray(fullPermissions)) { + throw new TypeError('INVALID_TYPE', 'fullPermissions', 'Array of GuildApplicationCommandPermissionData', true); + } + + const APIPermissions = []; + for (const perm of fullPermissions) { + if (!Array.isArray(perm.permissions)) throw new TypeError('INVALID_ELEMENT', 'Array', 'fullPermissions', perm); + APIPermissions.push({ + id: perm.id, + permissions: perm.permissions.map(p => this.constructor.transformPermissions(p)), + }); + } + const data = await this.permissionsPath(guildId).put({ + data: APIPermissions, + }); + return data.reduce( + (coll, perm) => + coll.set( + perm.id, + perm.permissions.map(p => this.constructor.transformPermissions(p, true)), + ), + new Collection(), + ); + } + + /** + * Options used to add permissions to a command + * The `command` parameter is not optional when the managers `commandId` is `null` + * @typedef {BaseApplicationCommandPermissionsOptions} AddApplicationCommandPermissionsOptions + * @property {ApplicationCommandPermissionData[]} permissions The permissions to add to the command + */ + + /** + * Add permissions to a command. + * @param {AddApplicationCommandPermissionsOptions} options Options used to add permissions + * @returns {Promise} + * @example + * // Block a role from the command permissions + * guild.commands.permissions.add({ command: '123456789012345678', permissions: [ + * { + * id: '876543211234567890', + * type: 'ROLE', + * permission: false + * }, + * ]}) + * .then(console.log) + * .catch(console.error); + */ + async add({ guild, command, permissions }) { + if (!this.manager.client.user.bot) throw new Error('INVALID_USER_METHOD'); + const { guildId, commandId } = this._validateOptions(guild, command); + if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); + if (!Array.isArray(permissions)) { + throw new TypeError('INVALID_TYPE', 'permissions', 'Array of ApplicationCommandPermissionData', true); + } + + let existing = []; + try { + existing = await this.fetch({ guild: guildId, command: commandId }); + } catch (error) { + if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; + } + + const newPermissions = permissions.slice(); + for (const perm of existing) { + if (!newPermissions.some(x => x.id === perm.id)) { + newPermissions.push(perm); + } + } + + return this.set({ guild: guildId, command: commandId, permissions: newPermissions }); + } + + /** + * Options used to remove permissions from a command + * The `command` parameter is not optional when the managers `commandId` is `null` + * @typedef {BaseApplicationCommandPermissionsOptions} RemoveApplicationCommandPermissionsOptions + * @property {UserResolvable|UserResolvable[]} [users] The user(s) to remove from the command permissions + * One of `users` or `roles` is required + * @property {RoleResolvable|RoleResolvable[]} [roles] The role(s) to remove from the command permissions + * One of `users` or `roles` is required + */ + + /** + * Remove permissions from a command. + * @param {RemoveApplicationCommandPermissionsOptions} options Options used to remove permissions + * @returns {Promise} + * @example + * // Remove a user permission from this command + * guild.commands.permissions.remove({ command: '123456789012345678', users: '876543210123456789' }) + * .then(console.log) + * .catch(console.error); + * @example + * // Remove multiple roles from this command + * guild.commands.permissions.remove({ + * command: '123456789012345678', roles: ['876543210123456789', '765432101234567890'] + * }) + * .then(console.log) + * .catch(console.error); + */ + async remove({ guild, command, users, roles }) { + if (!this.manager.client.user.bot) throw new Error('INVALID_USER_METHOD'); + const { guildId, commandId } = this._validateOptions(guild, command); + if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); + + if (!users && !roles) throw new TypeError('INVALID_TYPE', 'users OR roles', 'Array or Resolvable', true); + + const resolvedIds = []; + if (Array.isArray(users)) { + users.forEach(user => { + const userId = this.client.users.resolveId(user); + if (!userId) throw new TypeError('INVALID_ELEMENT', 'Array', 'users', user); + resolvedIds.push(userId); + }); + } else if (users) { + const userId = this.client.users.resolveId(users); + if (!userId) { + throw new TypeError('INVALID_TYPE', 'users', 'Array or UserResolvable'); + } + resolvedIds.push(userId); + } + + if (Array.isArray(roles)) { + roles.forEach(role => { + if (typeof role === 'string') { + resolvedIds.push(role); + return; + } + if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE'); + const roleId = this.guild.roles.resolveId(role); + if (!roleId) throw new TypeError('INVALID_ELEMENT', 'Array', 'users', role); + resolvedIds.push(roleId); + }); + } else if (roles) { + if (typeof roles === 'string') { + resolvedIds.push(roles); + } else { + if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE'); + const roleId = this.guild.roles.resolveId(roles); + if (!roleId) { + throw new TypeError('INVALID_TYPE', 'users', 'Array or RoleResolvable'); + } + resolvedIds.push(roleId); + } + } + + let existing = []; + try { + existing = await this.fetch({ guild: guildId, command: commandId }); + } catch (error) { + if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; + } + + const permissions = existing.filter(perm => !resolvedIds.includes(perm.id)); + + return this.set({ guild: guildId, command: commandId, permissions }); + } + + /** + * Options used to check the existence of permissions on a command + * The `command` parameter is not optional when the managers `commandId` is `null` + * @typedef {BaseApplicationCommandPermissionsOptions} HasApplicationCommandPermissionsOptions + * @property {UserResolvable|RoleResolvable} permissionId The user or role to check if a permission exists for + * on this command. + */ + + /** + * Check whether a permission exists for a user or role + * @param {AddApplicationCommandPermissionsOptions} options Options used to check permissions + * @returns {Promise} + * @example + * // Check whether a user has permission to use a command + * guild.commands.permissions.has({ command: '123456789012345678', permissionId: '876543210123456789' }) + * .then(console.log) + * .catch(console.error); + */ + async has({ guild, command, permissionId }) { + const { guildId, commandId } = this._validateOptions(guild, command); + if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); + + if (!permissionId) throw new TypeError('INVALID_TYPE', 'permissionId', 'UserResolvable or RoleResolvable'); + let resolvedId = permissionId; + if (typeof permissionId !== 'string') { + resolvedId = this.client.users.resolveId(permissionId); + if (!resolvedId) { + if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE'); + resolvedId = this.guild.roles.resolveId(permissionId); + } + if (!resolvedId) { + throw new TypeError('INVALID_TYPE', 'permissionId', 'UserResolvable or RoleResolvable'); + } + } + + let existing = []; + try { + existing = await this.fetch({ guild: guildId, command: commandId }); + } catch (error) { + if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; + } + + return existing.some(perm => perm.id === resolvedId); + } + + _validateOptions(guild, command) { + const guildId = this.guildId ?? this.client.guilds.resolveId(guild); + if (!guildId) throw new Error('GLOBAL_COMMAND_PERMISSIONS'); + let commandId = this.commandId; + if (command && !commandId) { + commandId = this.manager.resolveId?.(command); + if (!commandId && this.guild) { + commandId = this.guild.commands.resolveId(command); + } + commandId ??= this.client.application?.commands.resolveId(command); + if (!commandId) { + throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable', true); + } + } + return { guildId, commandId }; + } + + /** + * Transforms an {@link ApplicationCommandPermissionData} object into something that can be used with the API. + * @param {ApplicationCommandPermissionData} permissions The permissions to transform + * @param {boolean} [received] Whether these permissions have been received from Discord + * @returns {APIApplicationCommandPermissions} + * @private + */ + static transformPermissions(permissions, received) { + return { + id: permissions.id, + permission: permissions.permission, + type: + typeof permissions.type === 'number' && !received + ? permissions.type + : ApplicationCommandPermissionTypes[permissions.type], + }; + } +} + +module.exports = ApplicationCommandPermissionsManager; + +/* eslint-disable max-len */ +/** + * @external APIApplicationCommandPermissions + * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-structure} + */ diff --git a/src/managers/BlockedManager.js b/src/managers/BlockedManager.js index 32cadcf..fac1b7a 100644 --- a/src/managers/BlockedManager.js +++ b/src/managers/BlockedManager.js @@ -1,75 +1,75 @@ -'use strict'; - -const CachedManager = require('./CachedManager'); -const GuildMember = require('../structures/GuildMember'); -const Message = require('../structures/Message'); -const ThreadMember = require('../structures/ThreadMember'); -const User = require('../structures/User'); - -/** - * Manages API methods for users and stores their cache. - * @extends {CachedManager} - */ -class BlockedManager extends CachedManager { - constructor(client, iterable) { - super(client, User, iterable); - } - - /** - * The cache of this manager - * @type {Collection} - * @name BlockedManager#cache - */ - - /** - * Data that resolves to give a User object. This can be: - * * A User object - * * A Snowflake - * * A Message object (resolves to the message author) - * * A GuildMember object - * * A ThreadMember object - * @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable - */ - - /** - * Resolves a {@link UserResolvable} to a {@link User} object. - * @param {UserResolvable} user The UserResolvable to identify - * @returns {?User} - */ - resolve(user) { - if (user instanceof GuildMember || user instanceof ThreadMember) return user.user; - if (user instanceof Message) return user.author; - return super.resolve(user); - } - - /** - * Resolves a {@link UserResolvable} to a {@link User} id. - * @param {UserResolvable} user The UserResolvable to identify - * @returns {?Snowflake} - */ - resolveId(user) { - if (user instanceof ThreadMember) return user.id; - if (user instanceof GuildMember) return user.user.id; - if (user instanceof Message) return user.author.id; - return super.resolveId(user); - } - - /** - * Obtains a user from Discord, or the user cache if it's already available. - * @param {UserResolvable} user The user to fetch - * @param {BaseFetchOptions} [options] Additional options for this fetch - * @returns {Promise} - */ - async fetch(user, { cache = true, force = false } = {}) { - const id = this.resolveId(user); - if (!force) { - const existing = this.cache.get(id); - if (existing && !existing.partial) return existing; - } - - const data = await this.client.api.users(id).get(); - return this._add(data, cache); - } -} - -module.exports = BlockedManager; +'use strict'; + +const CachedManager = require('./CachedManager'); +const GuildMember = require('../structures/GuildMember'); +const Message = require('../structures/Message'); +const ThreadMember = require('../structures/ThreadMember'); +const User = require('../structures/User'); + +/** + * Manages API methods for users and stores their cache. + * @extends {CachedManager} + */ +class BlockedManager extends CachedManager { + constructor(client, iterable) { + super(client, User, iterable); + } + + /** + * The cache of this manager + * @type {Collection} + * @name BlockedManager#cache + */ + + /** + * Data that resolves to give a User object. This can be: + * * A User object + * * A Snowflake + * * A Message object (resolves to the message author) + * * A GuildMember object + * * A ThreadMember object + * @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable + */ + + /** + * Resolves a {@link UserResolvable} to a {@link User} object. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?User} + */ + resolve(user) { + if (user instanceof GuildMember || user instanceof ThreadMember) return user.user; + if (user instanceof Message) return user.author; + return super.resolve(user); + } + + /** + * Resolves a {@link UserResolvable} to a {@link User} id. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?Snowflake} + */ + resolveId(user) { + if (user instanceof ThreadMember) return user.id; + if (user instanceof GuildMember) return user.user.id; + if (user instanceof Message) return user.author.id; + return super.resolveId(user); + } + + /** + * Obtains a user from Discord, or the user cache if it's already available. + * @param {UserResolvable} user The user to fetch + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise} + */ + async fetch(user, { cache = true, force = false } = {}) { + const id = this.resolveId(user); + if (!force) { + const existing = this.cache.get(id); + if (existing && !existing.partial) return existing; + } + + const data = await this.client.api.users(id).get(); + return this._add(data, cache); + } +} + +module.exports = BlockedManager; diff --git a/src/managers/ChannelManager.js b/src/managers/ChannelManager.js index 456cc32..c5a889d 100644 --- a/src/managers/ChannelManager.js +++ b/src/managers/ChannelManager.js @@ -132,8 +132,8 @@ class ChannelManager extends CachedManager { console.log(recipients); if (recipients.length < 2 || recipients.length > 9) throw new Error('Invalid Users length (2 - 9)'); const data = await this.client.api.users['@me'].channels.post({ - data: { recipients: recipients.map((r) => r.id) }, - }); + data: { recipients: recipients.map(r => r.id) }, + }); return this._add(data, null, { cache: true, allowUnknownGuild: true }); } } diff --git a/src/managers/ClientUserSettingManager.js b/src/managers/ClientUserSettingManager.js index 35f389e..56d43fe 100644 --- a/src/managers/ClientUserSettingManager.js +++ b/src/managers/ClientUserSettingManager.js @@ -1,367 +1,339 @@ -'use strict'; - -const CachedManager = require('./CachedManager'); -const { default: Collection } = require('@discordjs/collection'); -const { Error, TypeError } = require('../errors/DJSError'); -const { remove } = require('lodash'); -const { localeObject, DMScanLevel, stickerAnimationMode } = require('../util/Constants') -/** - * Manages API methods for users and stores their cache. - * @extends {CachedManager} - */ -class ClientUserSettingManager extends CachedManager { - constructor(client, iterable) { - super(client); - // Raw data - this.rawSetting = {}; - // Language - this.locale = null; - // Setting => ACTIVITY SETTINGS => Activity Status => Display current activity as a status message - this.activityDisplay = null; - // - this.disableDMfromServer = new Collection(); - // Allow direct messages from server members - this.DMfromServerMode = null; - // - this.displayImage = null; - // - this.linkedImageDisplay = null; - // Setting => APP SETTINGS => Accessibility => Automatically play GIFs when Discord is focused. - this.autoplayGIF = null; - // Show embeds and preview website links pasted into chat - this.previewLink = null; - // Setting => APP SETTINGS => Accessibility => Play Animated Emojis - this.animatedEmojis = null; - // Setting => APP SETTINGS => Accessibility => Text-to-speech => Allow playback - this.allowTTS = null; - // Setting => APP SETTINGS => Appearance => Message Display => Compact Mode [OK] - this.compactMode = null; - // Setting => APP SETTINGS => Text & Images => Emoji => Convert Emoticons - this.convertEmoticons = null; - // SAFE DIRECT MESSAGING - this.DMScanLevel = null; - // Setting => APP SETTINGS => Appearance => Theme [OK] - this.theme = ''; - // - this.developerMode = null; - // - this.afkTimeout = null; - // - this.stickerAnimationMode = null; - // WHO CAN ADD YOU AS A FRIEND ? - this.addFriendFrom = { - all: null, - mutual_friends: null, - mutual_guilds: null, - }; - // Setting => APP SETTINGS => Text & Images => Emoji => Show emoji reactions - this.showEmojiReactions = null; - // Custom Stauts [It's not working now] - this.customStatus = null; - // Guild folder and position - this.guildMetadata = new Collection(); - // Todo: add new method from Discum - } - /** - * - * @param {Object} data Raw Data to patch - * @extends https://github.com/Merubokkusu/Discord-S.C.U.M/blob/master/discum/user/user.py - * @private - */ - _patch(data) { - this.rawSetting = Object.assign(this.rawSetting, data); - if ('locale' in data) { - this.locale = localeObject[data.locale]; - } - if ('show_current_game' in data) { - this.activityDisplay = data.show_current_game; - } - if ('default_guilds_restricted' in data) { - this.DMfromServerMode = data.default_guilds_restricted; - } - if ('inline_attachment_media' in data) { - this.displayImage = data.inline_attachment_media; - } - if ('inline_embed_media' in data) { - this.linkedImageDisplay = data.inline_embed_media; - } - if ('gif_auto_play' in data) { - this.autoplayGIF = data.gif_auto_play; - } - if ('render_embeds' in data) { - this.previewLink = data.render_embeds; - } - if ('animate_emoji' in data) { - this.animatedEmojis = data.animate_emoji; - } - if ('enable_tts_command' in data) { - this.allowTTS = data.enable_tts_command; - } - if ('message_display_compact' in data) { - this.compactMode = data.message_display_compact; - } - if ('convert_emoticons' in data) { - this.convertEmoticons = data.convert_emoticons; - } - if ('explicit_content_filter' in data) { - this.DMScanLevel = DMScanLevel[data.explicit_content_filter]; - } - if ('theme' in data) { - this.theme = data.theme; - } - if ('developer_mode' in data) { - this.developerMode = data.developer_mode; - } - if ('afk_timeout' in data) { - this.afkTimeout = data.afk_timeout * 1000; // second => milisecond - } - if ('animate_stickers' in data) { - this.stickerAnimationMode = stickerAnimationMode[data.animate_stickers]; - } - if ('render_reactions' in data) { - this.showEmojiReactions = data.render_reactions; - } - if ('custom_status' in data) { - this.customStatus = data.custom_status || {}; // Thanks PinkDuwc._#3443 reported this issue - this.customStatus.status = data.status; - } - if ('friend_source_flags' in data) { - this.addFriendFrom = { - all: data.friend_source_flags.all || false, - mutual_friends: - data.friend_source_flags.all ? true : data.friend_source_flags.mutual_friends, - mutual_guilds: - data.friend_source_flags.all ? true : data.friend_source_flags.mutual_guilds, - }; - } - if ('guild_folders' in data) { - const data_ = data.guild_positions.map((guildId, i) => { - // Find folder - const folderIndex = data.guild_folders.findIndex((obj) => - obj.guild_ids.includes(guildId), - ); - const metadata = { - guildId: guildId, - guildIndex: i, - folderId: data.guild_folders[folderIndex]?.id, - folderIndex, - folderName: data.guild_folders[folderIndex]?.name, - folderColor: data.guild_folders[folderIndex]?.color, - folderGuilds: data.guild_folders[folderIndex]?.guild_ids, - }; - return [guildId, metadata]; - }); - this.guildMetadata = new Collection(data_); - } - if ('restricted_guilds' in data) { - data.restricted_guilds.map((guildId) => { - const guild = this.client.guilds.cache.get(guildId); - if (!guild) return; - guild.disableDM = true; - this.disableDMfromServer.set(guildId, true); - }); - } - } - async fetch() { - if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); - try { - const data = await this.client.api.users('@me').settings.get(); - this._patch(data); - return this; - } catch (e) { - throw e; - } - } - /** - * Edit data - * @param {Object} data Data to edit - * @private - */ - async edit(data) { - if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); - try { - const res = await this.client.api.users('@me').settings.patch({ data }); - this._patch(res); - return this; - } catch (e) { - throw e; - } - } - /** - * Set compact mode - * @param {Boolean | null} value Compact mode enable or disable - * @returns {Boolean} - */ - async setDisplayCompactMode(value) { - if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); - if ( - typeof value !== 'boolean' && - typeof value !== 'null' && - typeof value !== 'undefined' - ) - throw new TypeError( - 'INVALID_TYPE', - 'value', - 'boolean | null | undefined', - true, - ); - if (!value) value = !this.compactMode; - if (value !== this.compactMode) { - await this.edit({ message_display_compact: value }); - } - return this.compactMode; - } - /** - * Discord Theme - * @param {null |dark |light} value Theme to set - * @returns {theme} - */ - async setTheme(value) { - if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); - const validValues = ['dark', 'light']; - if ( - typeof value !== 'string' && - typeof value !== 'null' && - typeof value !== 'undefined' - ) - throw new TypeError( - 'INVALID_TYPE', - 'value', - 'string | null | undefined', - true, - ); - if (!validValues.includes(value)) { - value == validValues[0] - ? (value = validValues[1]) - : (value = validValues[0]); - } - if (value !== this.theme) { - await this.edit({ theme: value }); - } - return this.theme; - } - /** - * * Locale Setting, must be one of: - * * `DANISH` - * * `GERMAN` - * * `ENGLISH_UK` - * * `ENGLISH_US` - * * `SPANISH` - * * `FRENCH` - * * `CROATIAN` - * * `ITALIAN` - * * `LITHUANIAN` - * * `HUNGARIAN` - * * `DUTCH` - * * `NORWEGIAN` - * * `POLISH` - * * `BRAZILIAN_PORTUGUESE` - * * `ROMANIA_ROMANIAN` - * * `FINNISH` - * * `SWEDISH` - * * `VIETNAMESE` - * * `TURKISH` - * * `CZECH` - * * `GREEK` - * * `BULGARIAN` - * * `RUSSIAN` - * * `UKRAINIAN` - * * `HINDI` - * * `THAI` - * * `CHINA_CHINESE` - * * `JAPANESE` - * * `TAIWAN_CHINESE` - * * `KOREAN` - * @param {string} value - * @returns {locale} - */ - async setLocale(value) { - if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); - if (typeof value !== 'string') - throw new TypeError('INVALID_TYPE', 'value', 'string', true); - if (!localeObject[value]) throw new Error('INVALID_LOCALE'); - if (localeObject[value] !== this.locale) { - await this.edit({ locale: localeObject[value] }); - } - return this.locale; - } - // TODO: Guild positions & folders - // Change Index in Array [Hidden] - /** - * - * @param {Array} array Array - * @param {Number} from Index1 - * @param {Number} to Index2 - * @returns {Array} - * @private - */ - _move(array, from, to) { - array.splice(to, 0, array.splice(from, 1)[0]); - return array; - } - // TODO: Move Guild - // folder to folder - // folder to home - // home to home - // home to folder - /** - * Change Guild Position (from * to Folder or Home) - * @param {GuildIDResolve} guildId guild.id - * @param {Number} newPosition Guild Position - * * **WARNING**: Type = `FOLDER`, newPosition is the guild's index in the Folder. - * @param {number} type Move to folder or home - * * `FOLDER`: 1 - * * `HOME`: 2 - * @param {FolderID} folderId If you want to move to folder - * @private - */ - async guildChangePosition(guildId, newPosition, type, folderId) { - // get Guild default position - // Escape - const oldGuildFolderPosition = this.rawSetting.guild_folders.findIndex( - (value) => value.guild_ids.includes(guildId), - ); - const newGuildFolderPosition = this.rawSetting.guild_folders.findIndex( - (value) => - value.guild_ids.includes(this.rawSetting.guild_positions[newPosition]), - ); - if (type == 2 || `${type}`.toUpperCase() == 'HOME') { - // Delete GuildID from Folder and create new Folder - // Check it is folder - const folder = this.rawSetting.guild_folders[oldGuildFolderPosition]; - if (folder.id) { - this.rawSetting.guild_folders[oldGuildFolderPosition].guild_ids = - this.rawSetting.guild_folders[ - oldGuildFolderPosition - ].guild_ids.filter((v) => v !== guildId); - } - this.rawSetting.guild_folders = this._move( - this.rawSetting.guild_folders, - oldGuildFolderPosition, - newGuildFolderPosition, - ); - this.rawSetting.guild_folders[newGuildFolderPosition].id = null; - } else if (type == 1 || `${type}`.toUpperCase() == 'FOLDER') { - // Delete GuildID from oldFolder - this.rawSetting.guild_folders[oldGuildFolderPosition].guild_ids = - this.rawSetting.guild_folders[oldGuildFolderPosition].guild_ids.filter( - (v) => v !== guildId, - ); - // Index new Folder - const folderIndex = this.rawSetting.guild_folders.findIndex( - (value) => value.id == folderId, - ); - const folder = this.rawSetting.guild_folders[folderIndex]; - folder.guild_ids.push(guildId); - folder.guild_ids = [...new Set(folder.guild_ids)]; - folder.guild_ids = this._move( - folder.guild_ids, - folder.guild_ids.findIndex((v) => v == guildId), - newPosition, - ); - } - this.edit({ guild_folders: this.rawSetting.guild_folders }); - } -} - -module.exports = ClientUserSettingManager; +'use strict'; + +const CachedManager = require('./CachedManager'); +const { default: Collection } = require('@discordjs/collection'); +const { Error, TypeError } = require('../errors/DJSError'); +const { remove } = require('lodash'); +const { localeObject, DMScanLevel, stickerAnimationMode } = require('../util/Constants'); +/** + * Manages API methods for users and stores their cache. + * @extends {CachedManager} + */ +class ClientUserSettingManager extends CachedManager { + constructor(client, iterable) { + super(client); + // Raw data + this.rawSetting = {}; + // Language + this.locale = null; + // Setting => ACTIVITY SETTINGS => Activity Status => Display current activity as a status message + this.activityDisplay = null; + // + this.disableDMfromServer = new Collection(); + // Allow direct messages from server members + this.DMfromServerMode = null; + // + this.displayImage = null; + // + this.linkedImageDisplay = null; + // Setting => APP SETTINGS => Accessibility => Automatically play GIFs when Discord is focused. + this.autoplayGIF = null; + // Show embeds and preview website links pasted into chat + this.previewLink = null; + // Setting => APP SETTINGS => Accessibility => Play Animated Emojis + this.animatedEmojis = null; + // Setting => APP SETTINGS => Accessibility => Text-to-speech => Allow playback + this.allowTTS = null; + // Setting => APP SETTINGS => Appearance => Message Display => Compact Mode [OK] + this.compactMode = null; + // Setting => APP SETTINGS => Text & Images => Emoji => Convert Emoticons + this.convertEmoticons = null; + // SAFE DIRECT MESSAGING + this.DMScanLevel = null; + // Setting => APP SETTINGS => Appearance => Theme [OK] + this.theme = ''; + // + this.developerMode = null; + // + this.afkTimeout = null; + // + this.stickerAnimationMode = null; + // WHO CAN ADD YOU AS A FRIEND ? + this.addFriendFrom = { + all: null, + mutual_friends: null, + mutual_guilds: null, + }; + // Setting => APP SETTINGS => Text & Images => Emoji => Show emoji reactions + this.showEmojiReactions = null; + // Custom Stauts [It's not working now] + this.customStatus = null; + // Guild folder and position + this.guildMetadata = new Collection(); + // Todo: add new method from Discum + } + /** + * + * @param {Object} data Raw Data to patch + * @extends https://github.com/Merubokkusu/Discord-S.C.U.M/blob/master/discum/user/user.py + * @private + */ + _patch(data) { + this.rawSetting = Object.assign(this.rawSetting, data); + if ('locale' in data) { + this.locale = localeObject[data.locale]; + } + if ('show_current_game' in data) { + this.activityDisplay = data.show_current_game; + } + if ('default_guilds_restricted' in data) { + this.DMfromServerMode = data.default_guilds_restricted; + } + if ('inline_attachment_media' in data) { + this.displayImage = data.inline_attachment_media; + } + if ('inline_embed_media' in data) { + this.linkedImageDisplay = data.inline_embed_media; + } + if ('gif_auto_play' in data) { + this.autoplayGIF = data.gif_auto_play; + } + if ('render_embeds' in data) { + this.previewLink = data.render_embeds; + } + if ('animate_emoji' in data) { + this.animatedEmojis = data.animate_emoji; + } + if ('enable_tts_command' in data) { + this.allowTTS = data.enable_tts_command; + } + if ('message_display_compact' in data) { + this.compactMode = data.message_display_compact; + } + if ('convert_emoticons' in data) { + this.convertEmoticons = data.convert_emoticons; + } + if ('explicit_content_filter' in data) { + this.DMScanLevel = DMScanLevel[data.explicit_content_filter]; + } + if ('theme' in data) { + this.theme = data.theme; + } + if ('developer_mode' in data) { + this.developerMode = data.developer_mode; + } + if ('afk_timeout' in data) { + this.afkTimeout = data.afk_timeout * 1000; // second => milisecond + } + if ('animate_stickers' in data) { + this.stickerAnimationMode = stickerAnimationMode[data.animate_stickers]; + } + if ('render_reactions' in data) { + this.showEmojiReactions = data.render_reactions; + } + if ('custom_status' in data) { + this.customStatus = data.custom_status || {}; // Thanks PinkDuwc._#3443 reported this issue + this.customStatus.status = data.status; + } + if ('friend_source_flags' in data) { + this.addFriendFrom = { + all: data.friend_source_flags.all || false, + mutual_friends: data.friend_source_flags.all ? true : data.friend_source_flags.mutual_friends, + mutual_guilds: data.friend_source_flags.all ? true : data.friend_source_flags.mutual_guilds, + }; + } + if ('guild_folders' in data) { + const data_ = data.guild_positions.map((guildId, i) => { + // Find folder + const folderIndex = data.guild_folders.findIndex(obj => obj.guild_ids.includes(guildId)); + const metadata = { + guildId: guildId, + guildIndex: i, + folderId: data.guild_folders[folderIndex]?.id, + folderIndex, + folderName: data.guild_folders[folderIndex]?.name, + folderColor: data.guild_folders[folderIndex]?.color, + folderGuilds: data.guild_folders[folderIndex]?.guild_ids, + }; + return [guildId, metadata]; + }); + this.guildMetadata = new Collection(data_); + } + if ('restricted_guilds' in data) { + data.restricted_guilds.map(guildId => { + const guild = this.client.guilds.cache.get(guildId); + if (!guild) return; + guild.disableDM = true; + this.disableDMfromServer.set(guildId, true); + }); + } + } + async fetch() { + if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); + try { + const data = await this.client.api.users('@me').settings.get(); + this._patch(data); + return this; + } catch (e) { + throw e; + } + } + /** + * Edit data + * @param {Object} data Data to edit + * @private + */ + async edit(data) { + if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); + try { + const res = await this.client.api.users('@me').settings.patch({ data }); + this._patch(res); + return this; + } catch (e) { + throw e; + } + } + /** + * Set compact mode + * @param {Boolean | null} value Compact mode enable or disable + * @returns {Boolean} + */ + async setDisplayCompactMode(value) { + if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); + if (typeof value !== 'boolean' && typeof value !== 'null' && typeof value !== 'undefined') { + throw new TypeError('INVALID_TYPE', 'value', 'boolean | null | undefined', true); + } + if (!value) value = !this.compactMode; + if (value !== this.compactMode) { + await this.edit({ message_display_compact: value }); + } + return this.compactMode; + } + /** + * Discord Theme + * @param {null |dark |light} value Theme to set + * @returns {theme} + */ + async setTheme(value) { + if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); + const validValues = ['dark', 'light']; + if (typeof value !== 'string' && typeof value !== 'null' && typeof value !== 'undefined') { + throw new TypeError('INVALID_TYPE', 'value', 'string | null | undefined', true); + } + if (!validValues.includes(value)) { + value == validValues[0] ? (value = validValues[1]) : (value = validValues[0]); + } + if (value !== this.theme) { + await this.edit({ theme: value }); + } + return this.theme; + } + /** + * * Locale Setting, must be one of: + * * `DANISH` + * * `GERMAN` + * * `ENGLISH_UK` + * * `ENGLISH_US` + * * `SPANISH` + * * `FRENCH` + * * `CROATIAN` + * * `ITALIAN` + * * `LITHUANIAN` + * * `HUNGARIAN` + * * `DUTCH` + * * `NORWEGIAN` + * * `POLISH` + * * `BRAZILIAN_PORTUGUESE` + * * `ROMANIA_ROMANIAN` + * * `FINNISH` + * * `SWEDISH` + * * `VIETNAMESE` + * * `TURKISH` + * * `CZECH` + * * `GREEK` + * * `BULGARIAN` + * * `RUSSIAN` + * * `UKRAINIAN` + * * `HINDI` + * * `THAI` + * * `CHINA_CHINESE` + * * `JAPANESE` + * * `TAIWAN_CHINESE` + * * `KOREAN` + * @param {string} value + * @returns {locale} + */ + async setLocale(value) { + if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); + if (typeof value !== 'string') { + throw new TypeError('INVALID_TYPE', 'value', 'string', true); + } + if (!localeObject[value]) throw new Error('INVALID_LOCALE'); + if (localeObject[value] !== this.locale) { + await this.edit({ locale: localeObject[value] }); + } + return this.locale; + } + // TODO: Guild positions & folders + // Change Index in Array [Hidden] + /** + * + * @param {Array} array Array + * @param {Number} from Index1 + * @param {Number} to Index2 + * @returns {Array} + * @private + */ + _move(array, from, to) { + array.splice(to, 0, array.splice(from, 1)[0]); + return array; + } + // TODO: Move Guild + // folder to folder + // folder to home + // home to home + // home to folder + /** + * Change Guild Position (from * to Folder or Home) + * @param {GuildIDResolve} guildId guild.id + * @param {Number} newPosition Guild Position + * * **WARNING**: Type = `FOLDER`, newPosition is the guild's index in the Folder. + * @param {number} type Move to folder or home + * * `FOLDER`: 1 + * * `HOME`: 2 + * @param {FolderID} folderId If you want to move to folder + * @private + */ + async guildChangePosition(guildId, newPosition, type, folderId) { + // get Guild default position + // Escape + const oldGuildFolderPosition = this.rawSetting.guild_folders.findIndex(value => value.guild_ids.includes(guildId)); + const newGuildFolderPosition = this.rawSetting.guild_folders.findIndex(value => + value.guild_ids.includes(this.rawSetting.guild_positions[newPosition]), + ); + if (type == 2 || `${type}`.toUpperCase() == 'HOME') { + // Delete GuildID from Folder and create new Folder + // Check it is folder + const folder = this.rawSetting.guild_folders[oldGuildFolderPosition]; + if (folder.id) { + this.rawSetting.guild_folders[oldGuildFolderPosition].guild_ids = this.rawSetting.guild_folders[ + oldGuildFolderPosition + ].guild_ids.filter(v => v !== guildId); + } + this.rawSetting.guild_folders = this._move( + this.rawSetting.guild_folders, + oldGuildFolderPosition, + newGuildFolderPosition, + ); + this.rawSetting.guild_folders[newGuildFolderPosition].id = null; + } else if (type == 1 || `${type}`.toUpperCase() == 'FOLDER') { + // Delete GuildID from oldFolder + this.rawSetting.guild_folders[oldGuildFolderPosition].guild_ids = this.rawSetting.guild_folders[ + oldGuildFolderPosition + ].guild_ids.filter(v => v !== guildId); + // Index new Folder + const folderIndex = this.rawSetting.guild_folders.findIndex(value => value.id == folderId); + const folder = this.rawSetting.guild_folders[folderIndex]; + folder.guild_ids.push(guildId); + folder.guild_ids = [...new Set(folder.guild_ids)]; + folder.guild_ids = this._move( + folder.guild_ids, + folder.guild_ids.findIndex(v => v == guildId), + newPosition, + ); + } + this.edit({ guild_folders: this.rawSetting.guild_folders }); + } +} + +module.exports = ClientUserSettingManager; diff --git a/src/managers/FriendsManager.js b/src/managers/FriendsManager.js index 612af61..1d45dcb 100644 --- a/src/managers/FriendsManager.js +++ b/src/managers/FriendsManager.js @@ -1,75 +1,75 @@ -'use strict'; - -const CachedManager = require('./CachedManager'); -const GuildMember = require('../structures/GuildMember'); -const Message = require('../structures/Message'); -const ThreadMember = require('../structures/ThreadMember'); -const User = require('../structures/User'); - -/** - * Manages API methods for users and stores their cache. - * @extends {CachedManager} - */ -class FriendsManager extends CachedManager { - constructor(client, iterable) { - super(client, User, iterable); - } - - /** - * The cache of this manager - * @type {Collection} - * @name FriendsManager#cache - */ - - /** - * Data that resolves to give a User object. This can be: - * * A User object - * * A Snowflake - * * A Message object (resolves to the message author) - * * A GuildMember object - * * A ThreadMember object - * @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable - */ - - /** - * Resolves a {@link UserResolvable} to a {@link User} object. - * @param {UserResolvable} user The UserResolvable to identify - * @returns {?User} - */ - resolve(user) { - if (user instanceof GuildMember || user instanceof ThreadMember) return user.user; - if (user instanceof Message) return user.author; - return super.resolve(user); - } - - /** - * Resolves a {@link UserResolvable} to a {@link User} id. - * @param {UserResolvable} user The UserResolvable to identify - * @returns {?Snowflake} - */ - resolveId(user) { - if (user instanceof ThreadMember) return user.id; - if (user instanceof GuildMember) return user.user.id; - if (user instanceof Message) return user.author.id; - return super.resolveId(user); - } - - /** - * Obtains a user from Discord, or the user cache if it's already available. - * @param {UserResolvable} user The user to fetch - * @param {BaseFetchOptions} [options] Additional options for this fetch - * @returns {Promise} - */ - async fetch(user, { cache = true, force = false } = {}) { - const id = this.resolveId(user); - if (!force) { - const existing = this.cache.get(id); - if (existing && !existing.partial) return existing; - } - - const data = await this.client.api.users(id).get(); - return this._add(data, cache); - } -} - -module.exports = FriendsManager; +'use strict'; + +const CachedManager = require('./CachedManager'); +const GuildMember = require('../structures/GuildMember'); +const Message = require('../structures/Message'); +const ThreadMember = require('../structures/ThreadMember'); +const User = require('../structures/User'); + +/** + * Manages API methods for users and stores their cache. + * @extends {CachedManager} + */ +class FriendsManager extends CachedManager { + constructor(client, iterable) { + super(client, User, iterable); + } + + /** + * The cache of this manager + * @type {Collection} + * @name FriendsManager#cache + */ + + /** + * Data that resolves to give a User object. This can be: + * * A User object + * * A Snowflake + * * A Message object (resolves to the message author) + * * A GuildMember object + * * A ThreadMember object + * @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable + */ + + /** + * Resolves a {@link UserResolvable} to a {@link User} object. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?User} + */ + resolve(user) { + if (user instanceof GuildMember || user instanceof ThreadMember) return user.user; + if (user instanceof Message) return user.author; + return super.resolve(user); + } + + /** + * Resolves a {@link UserResolvable} to a {@link User} id. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?Snowflake} + */ + resolveId(user) { + if (user instanceof ThreadMember) return user.id; + if (user instanceof GuildMember) return user.user.id; + if (user instanceof Message) return user.author.id; + return super.resolveId(user); + } + + /** + * Obtains a user from Discord, or the user cache if it's already available. + * @param {UserResolvable} user The user to fetch + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise} + */ + async fetch(user, { cache = true, force = false } = {}) { + const id = this.resolveId(user); + if (!force) { + const existing = this.cache.get(id); + if (existing && !existing.partial) return existing; + } + + const data = await this.client.api.users(id).get(); + return this._add(data, cache); + } +} + +module.exports = FriendsManager; diff --git a/src/managers/GuildMemberManager.js b/src/managers/GuildMemberManager.js index 8bcd4ec..22074d9 100644 --- a/src/managers/GuildMemberManager.js +++ b/src/managers/GuildMemberManager.js @@ -419,7 +419,12 @@ class GuildMemberManager extends CachedManager { return new Promise((resolve, reject) => { if (!query && !user_ids) query = ''; if (nonce.length > 32) throw new RangeError('MEMBER_FETCH_NONCE_LENGTH'); - if (this.guild.me.permissions.has('ADMINISTRATOR') || this.guild.me.permissions.has('KICK_MEMBERS') || this.guild.me.permissions.has('BAN_MEMBERS') || this.guild.me.permissions.has('MANAGE_ROLES')) { + if ( + this.guild.me.permissions.has('ADMINISTRATOR') || + this.guild.me.permissions.has('KICK_MEMBERS') || + this.guild.me.permissions.has('BAN_MEMBERS') || + this.guild.me.permissions.has('MANAGE_ROLES') + ) { this.guild.shard.send({ op: Opcodes.REQUEST_GUILD_MEMBERS, d: { @@ -435,20 +440,28 @@ class GuildMemberManager extends CachedManager { let channel; let channels = this.guild.channels.cache.filter(c => c.isText()); channels = channels.filter(c => c.permissionsFor(this.guild.me).has('VIEW_CHANNEL')); - if (!channels.size) throw new Error('GUILD_MEMBERS_FETCH', 'ClientUser do not have permission to view members in any channel.'); - const channels_allowed_everyone = channels.filter((c) => + if (!channels.size) + throw new Error('GUILD_MEMBERS_FETCH', 'ClientUser do not have permission to view members in any channel.'); + const channels_allowed_everyone = channels.filter(c => c.permissionsFor(this.guild.roles.everyone).has('VIEW_CHANNEL'), ); channel = channels_allowed_everyone.first() ?? channels.first(); // create array limit [0, 99] const list = []; - let allMember = this.guild.memberCount; + const allMember = this.guild.memberCount; if (allMember < 100) { list.push([[0, 99]]); } else if (allMember < 200) { - list.push([[0, 99], [100, 199]]); + list.push([ + [0, 99], + [100, 199], + ]); } else if (allMember < 300) { - list.push([[0, 99], [100, 199], [200, 299]]); + list.push([ + [0, 99], + [100, 199], + [200, 299], + ]); } else { let x = 100; for (let i = 0; i < allMember; i++) { @@ -464,20 +477,22 @@ class GuildMemberManager extends CachedManager { x = x + 200; } } - Promise.all(list.map(async (l) => { - this.guild.shard.send({ - op: Opcodes.LAZY_REQUEST, - d: { - guild_id: this.guild.id, - typing: true, - threads: false, - activities: true, - channels: { - [channel.id]: l, + Promise.all( + list.map(async l => { + this.guild.shard.send({ + op: Opcodes.LAZY_REQUEST, + d: { + guild_id: this.guild.id, + typing: true, + threads: false, + activities: true, + channels: { + [channel.id]: l, + }, }, - }, - }); - })) + }); + }), + ); } const fetchedMembers = new Collection(); let i = 0; diff --git a/src/managers/GuildScheduledEventManager.js b/src/managers/GuildScheduledEventManager.js index b35e45c..2440ca7 100644 --- a/src/managers/GuildScheduledEventManager.js +++ b/src/managers/GuildScheduledEventManager.js @@ -268,7 +268,7 @@ class GuildScheduledEventManager extends CachedManager { const guildScheduledEventId = this.resolveId(guildScheduledEvent); if (!guildScheduledEventId) throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE'); - let { limit, withMember, before, after } = options; + const { limit, withMember, before, after } = options; const data = await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).users.get({ query: { limit, with_member: withMember, before, after }, diff --git a/src/managers/MessageManager.js b/src/managers/MessageManager.js index d156d55..94b0fb5 100644 --- a/src/managers/MessageManager.js +++ b/src/managers/MessageManager.js @@ -1,247 +1,244 @@ -'use strict'; - -const { Collection } = require('@discordjs/collection'); -const CachedManager = require('./CachedManager'); -const { TypeError, Error } = require('../errors'); -const { Message } = require('../structures/Message'); -const MessagePayload = require('../structures/MessagePayload'); -const Util = require('../util/Util'); -const BigNumber = require('bignumber.js'); - -/** - * Manages API methods for Messages and holds their cache. - * @extends {CachedManager} - */ -class MessageManager extends CachedManager { - constructor(channel, iterable) { - super(channel.client, Message, iterable); - - /** - * The channel that the messages belong to - * @type {TextBasedChannels} - */ - this.channel = channel; - } - - /** - * The cache of Messages - * @type {Collection} - * @name MessageManager#cache - */ - - _add(data, cache) { - return super._add(data, cache); - } - - /** - * The parameters to pass in when requesting previous messages from a channel. `around`, `before` and - * `after` are mutually exclusive. All the parameters are optional. - * @typedef {Object} ChannelLogsQueryOptions - * @property {number} [limit=50] Number of messages to acquire - * @property {Snowflake} [before] The message's id to get the messages that were posted before it - * @property {Snowflake} [after] The message's id to get the messages that were posted after it - * @property {Snowflake} [around] The message's id to get the messages that were posted around it - */ - - /** - * Gets a message, or messages, from this channel. - * The returned Collection does not contain reaction users of the messages if they were not cached. - * Those need to be fetched separately in such a case. - * @param {Snowflake|ChannelLogsQueryOptions} [message] The id of the message to fetch, or query parameters. - * @param {BaseFetchOptions} [options] Additional options for this fetch - * @returns {Promise>} - * @example - * // Get message - * channel.messages.fetch('99539446449315840') - * .then(message => console.log(message.content)) - * .catch(console.error); - * @example - * // Get messages - * channel.messages.fetch({ limit: 10 }) - * .then(messages => console.log(`Received ${messages.size} messages`)) - * .catch(console.error); - * @example - * // Get messages and filter by user id - * channel.messages.fetch() - * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`)) - * .catch(console.error); - */ - fetch(message, { cache = true, force = false } = {}) { - return typeof message === 'string' ? this._fetchId(message, cache, force) : this._fetchMany(message, cache); - } - - /** - * Fetches the pinned messages of this channel and returns a collection of them. - * The returned Collection does not contain any reaction data of the messages. - * Those need to be fetched separately. - * @param {boolean} [cache=true] Whether to cache the message(s) - * @returns {Promise>} - * @example - * // Get pinned messages - * channel.messages.fetchPinned() - * .then(messages => console.log(`Received ${messages.size} messages`)) - * .catch(console.error); - */ - async fetchPinned(cache = true) { - const data = await this.client.api.channels[this.channel.id].pins.get(); - const messages = new Collection(); - for (const message of data) messages.set(message.id, this._add(message, cache)); - return messages; - } - - /** - * Data that can be resolved to a Message object. This can be: - * * A Message - * * A Snowflake - * @typedef {Message|Snowflake} MessageResolvable - */ - - /** - * Resolves a {@link MessageResolvable} to a {@link Message} object. - * @method resolve - * @memberof MessageManager - * @instance - * @param {MessageResolvable} message The message resolvable to resolve - * @returns {?Message} - */ - - /** - * Resolves a {@link MessageResolvable} to a {@link Message} id. - * @method resolveId - * @memberof MessageManager - * @instance - * @param {MessageResolvable} message The message resolvable to resolve - * @returns {?Snowflake} - */ - - /** - * Edits a message, even if it's not cached. - * @param {MessageResolvable} message The message to edit - * @param {string|MessageEditOptions|MessagePayload} options The options to edit the message - * @returns {Promise} - */ - async edit(message, options) { - const messageId = this.resolveId(message); - if (!messageId) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); - - let messagePayload; - if (options instanceof MessagePayload) { - messagePayload = await options.resolveData(); - } else { - messagePayload = await MessagePayload.create( - message instanceof Message ? message : this, - options, - ).resolveData(); - } - const { data, files } = await messagePayload.resolveFiles(); - const d = await this.client.api.channels[this.channel.id].messages[messageId].patch({ data, files }); - - const existing = this.cache.get(messageId); - if (existing) { - const clone = existing._clone(); - clone._patch(d); - return clone; - } - return this._add(d); - } - - /** - * Publishes a message in an announcement channel to all channels following it, even if it's not cached. - * @param {MessageResolvable} message The message to publish - * @returns {Promise} - */ - async crosspost(message) { - message = this.resolveId(message); - if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); - - const data = await this.client.api.channels(this.channel.id).messages(message).crosspost.post(); - return this.cache.get(data.id) ?? this._add(data); - } - - /** - * Pins a message to the channel's pinned messages, even if it's not cached. - * @param {MessageResolvable} message The message to pin - * @returns {Promise} - */ - async pin(message) { - message = this.resolveId(message); - if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); - - await this.client.api.channels(this.channel.id).pins(message).put(); - } - - /** - * Unpins a message from the channel's pinned messages, even if it's not cached. - * @param {MessageResolvable} message The message to unpin - * @returns {Promise} - */ - async unpin(message) { - message = this.resolveId(message); - if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); - - await this.client.api.channels(this.channel.id).pins(message).delete(); - } - - /** - * Adds a reaction to a message, even if it's not cached. - * @param {MessageResolvable} message The message to react to - * @param {EmojiIdentifierResolvable} emoji The emoji to react with - * @returns {Promise} - */ - async react(message, emoji) { - message = this.resolveId(message); - if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); - - emoji = Util.resolvePartialEmoji(emoji); - if (!emoji) throw new TypeError('EMOJI_TYPE', 'emoji', 'EmojiIdentifierResolvable'); - - const emojiId = emoji.id - ? `${emoji.animated ? 'a:' : ''}${emoji.name}:${emoji.id}` - : encodeURIComponent(emoji.name); - - // eslint-disable-next-line newline-per-chained-call - await this.client.api.channels(this.channel.id).messages(message).reactions(emojiId, '@me').put(); - } - - /** - * Deletes a message, even if it's not cached. - * @param {MessageResolvable} message The message to delete - * @returns {Promise} - */ - async delete(message) { - message = this.resolveId(message); - if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); - - await this.client.api.channels(this.channel.id).messages(message).delete(); - } - - async _fetchId(messageId, cache, force) { - if (!force) { - const existing = this.cache.get(messageId); - if (existing && !existing.partial) return existing; - } - - // const data = await this.client.api.channels[this.channel.id].messages[messageId].get(); // Discord Block - // https://canary.discord.com/api/v9/guilds/809133733591384155/messages/search?channel_id=840225732902518825&max_id=957254525360697375&min_id=957254525360697373 - const data = ( - await this.client.api.guilds[this.channel.guild.id].messages.search.get({ - query: { - channel_id: this.channel.id, - max_id: new BigNumber.BigNumber(messageId).plus(1).toString(), - min_id: new BigNumber.BigNumber(messageId).minus(1).toString(), - }, - }) - ).messages[0] - if (data) return this._add(data[0], cache); - else throw new Error('MESSAGE_ID_NOT_FOUND'); - } - - async _fetchMany(options = {}, cache) { - const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); - const messages = new Collection(); - for (const message of data) messages.set(message.id, this._add(message, cache)); - return messages; - } -} - -module.exports = MessageManager; +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const CachedManager = require('./CachedManager'); +const { TypeError, Error } = require('../errors'); +const { Message } = require('../structures/Message'); +const MessagePayload = require('../structures/MessagePayload'); +const Util = require('../util/Util'); +const BigNumber = require('bignumber.js'); + +/** + * Manages API methods for Messages and holds their cache. + * @extends {CachedManager} + */ +class MessageManager extends CachedManager { + constructor(channel, iterable) { + super(channel.client, Message, iterable); + + /** + * The channel that the messages belong to + * @type {TextBasedChannels} + */ + this.channel = channel; + } + + /** + * The cache of Messages + * @type {Collection} + * @name MessageManager#cache + */ + + _add(data, cache) { + return super._add(data, cache); + } + + /** + * The parameters to pass in when requesting previous messages from a channel. `around`, `before` and + * `after` are mutually exclusive. All the parameters are optional. + * @typedef {Object} ChannelLogsQueryOptions + * @property {number} [limit=50] Number of messages to acquire + * @property {Snowflake} [before] The message's id to get the messages that were posted before it + * @property {Snowflake} [after] The message's id to get the messages that were posted after it + * @property {Snowflake} [around] The message's id to get the messages that were posted around it + */ + + /** + * Gets a message, or messages, from this channel. + * The returned Collection does not contain reaction users of the messages if they were not cached. + * Those need to be fetched separately in such a case. + * @param {Snowflake|ChannelLogsQueryOptions} [message] The id of the message to fetch, or query parameters. + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise>} + * @example + * // Get message + * channel.messages.fetch('99539446449315840') + * .then(message => console.log(message.content)) + * .catch(console.error); + * @example + * // Get messages + * channel.messages.fetch({ limit: 10 }) + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + * @example + * // Get messages and filter by user id + * channel.messages.fetch() + * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`)) + * .catch(console.error); + */ + fetch(message, { cache = true, force = false } = {}) { + return typeof message === 'string' ? this._fetchId(message, cache, force) : this._fetchMany(message, cache); + } + + /** + * Fetches the pinned messages of this channel and returns a collection of them. + * The returned Collection does not contain any reaction data of the messages. + * Those need to be fetched separately. + * @param {boolean} [cache=true] Whether to cache the message(s) + * @returns {Promise>} + * @example + * // Get pinned messages + * channel.messages.fetchPinned() + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + */ + async fetchPinned(cache = true) { + const data = await this.client.api.channels[this.channel.id].pins.get(); + const messages = new Collection(); + for (const message of data) messages.set(message.id, this._add(message, cache)); + return messages; + } + + /** + * Data that can be resolved to a Message object. This can be: + * * A Message + * * A Snowflake + * @typedef {Message|Snowflake} MessageResolvable + */ + + /** + * Resolves a {@link MessageResolvable} to a {@link Message} object. + * @method resolve + * @memberof MessageManager + * @instance + * @param {MessageResolvable} message The message resolvable to resolve + * @returns {?Message} + */ + + /** + * Resolves a {@link MessageResolvable} to a {@link Message} id. + * @method resolveId + * @memberof MessageManager + * @instance + * @param {MessageResolvable} message The message resolvable to resolve + * @returns {?Snowflake} + */ + + /** + * Edits a message, even if it's not cached. + * @param {MessageResolvable} message The message to edit + * @param {string|MessageEditOptions|MessagePayload} options The options to edit the message + * @returns {Promise} + */ + async edit(message, options) { + const messageId = this.resolveId(message); + if (!messageId) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); + + let messagePayload; + if (options instanceof MessagePayload) { + messagePayload = await options.resolveData(); + } else { + messagePayload = await MessagePayload.create(message instanceof Message ? message : this, options).resolveData(); + } + const { data, files } = await messagePayload.resolveFiles(); + const d = await this.client.api.channels[this.channel.id].messages[messageId].patch({ data, files }); + + const existing = this.cache.get(messageId); + if (existing) { + const clone = existing._clone(); + clone._patch(d); + return clone; + } + return this._add(d); + } + + /** + * Publishes a message in an announcement channel to all channels following it, even if it's not cached. + * @param {MessageResolvable} message The message to publish + * @returns {Promise} + */ + async crosspost(message) { + message = this.resolveId(message); + if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); + + const data = await this.client.api.channels(this.channel.id).messages(message).crosspost.post(); + return this.cache.get(data.id) ?? this._add(data); + } + + /** + * Pins a message to the channel's pinned messages, even if it's not cached. + * @param {MessageResolvable} message The message to pin + * @returns {Promise} + */ + async pin(message) { + message = this.resolveId(message); + if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); + + await this.client.api.channels(this.channel.id).pins(message).put(); + } + + /** + * Unpins a message from the channel's pinned messages, even if it's not cached. + * @param {MessageResolvable} message The message to unpin + * @returns {Promise} + */ + async unpin(message) { + message = this.resolveId(message); + if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); + + await this.client.api.channels(this.channel.id).pins(message).delete(); + } + + /** + * Adds a reaction to a message, even if it's not cached. + * @param {MessageResolvable} message The message to react to + * @param {EmojiIdentifierResolvable} emoji The emoji to react with + * @returns {Promise} + */ + async react(message, emoji) { + message = this.resolveId(message); + if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); + + emoji = Util.resolvePartialEmoji(emoji); + if (!emoji) throw new TypeError('EMOJI_TYPE', 'emoji', 'EmojiIdentifierResolvable'); + + const emojiId = emoji.id + ? `${emoji.animated ? 'a:' : ''}${emoji.name}:${emoji.id}` + : encodeURIComponent(emoji.name); + + // eslint-disable-next-line newline-per-chained-call + await this.client.api.channels(this.channel.id).messages(message).reactions(emojiId, '@me').put(); + } + + /** + * Deletes a message, even if it's not cached. + * @param {MessageResolvable} message The message to delete + * @returns {Promise} + */ + async delete(message) { + message = this.resolveId(message); + if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); + + await this.client.api.channels(this.channel.id).messages(message).delete(); + } + + async _fetchId(messageId, cache, force) { + if (!force) { + const existing = this.cache.get(messageId); + if (existing && !existing.partial) return existing; + } + + // const data = await this.client.api.channels[this.channel.id].messages[messageId].get(); // Discord Block + // https://canary.discord.com/api/v9/guilds/809133733591384155/messages/search?channel_id=840225732902518825&max_id=957254525360697375&min_id=957254525360697373 + const data = ( + await this.client.api.guilds[this.channel.guild.id].messages.search.get({ + query: { + channel_id: this.channel.id, + max_id: new BigNumber.BigNumber(messageId).plus(1).toString(), + min_id: new BigNumber.BigNumber(messageId).minus(1).toString(), + }, + }) + ).messages[0]; + if (data) return this._add(data[0], cache); + else throw new Error('MESSAGE_ID_NOT_FOUND'); + } + + async _fetchMany(options = {}, cache) { + const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); + const messages = new Collection(); + for (const message of data) messages.set(message.id, this._add(message, cache)); + return messages; + } +} + +module.exports = MessageManager; diff --git a/src/managers/PermissionOverwriteManager.js b/src/managers/PermissionOverwriteManager.js index 8dbc881..066092b 100644 --- a/src/managers/PermissionOverwriteManager.js +++ b/src/managers/PermissionOverwriteManager.js @@ -89,7 +89,7 @@ class PermissionOverwriteManager extends CachedManager { * @private */ async upsert(userOrRole, options, overwriteOptions = {}, existing) { - let userOrRoleId = this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole); + const userOrRoleId = this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole); let { type, reason } = overwriteOptions; if (typeof type !== 'number') { userOrRole = this.channel.guild.roles.resolve(userOrRole) ?? this.client.users.resolve(userOrRole); diff --git a/src/managers/ReactionUserManager.js b/src/managers/ReactionUserManager.js index fc9a217..d1a492e 100644 --- a/src/managers/ReactionUserManager.js +++ b/src/managers/ReactionUserManager.js @@ -3,7 +3,7 @@ const { Collection } = require('@discordjs/collection'); const CachedManager = require('./CachedManager'); const { Error } = require('../errors'); -const Discord = require("discord.js-selfbot-v13") +const Discord = require('discord.js-selfbot-v13'); /** * Manages API methods for users who reacted to a reaction and stores their cache. * @extends {CachedManager} diff --git a/src/managers/RelationshipsManager.js b/src/managers/RelationshipsManager.js index c071edf..b216ff2 100644 --- a/src/managers/RelationshipsManager.js +++ b/src/managers/RelationshipsManager.js @@ -11,105 +11,103 @@ const { RelationshipTypes } = require('../util/Constants'); * Manages API methods for users and stores their cache. */ class RelationshipsManager { - constructor(client, users) { - this.client = client; - this.cache = new Collection(); - this._setup(users); + constructor(client, users) { + this.client = client; + this.cache = new Collection(); + this._setup(users); + } + + _setup(users) { + if (!Array.isArray(users)) return; + for (const relationShip of users) { + this.cache.set(relationShip.id, relationShip.type); + } + } + + /** + * Resolves a {@link UserResolvable} to a {@link User} id. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?Snowflake} + */ + resolveId(user) { + if (user instanceof ThreadMember) return user.id; + if (user instanceof GuildMember) return user.user.id; + if (user instanceof Message) return user.author.id; + if (user instanceof User) return user.id; + return user; + } + + /** + * Obtains a user from Discord, or the user cache if it's already available. + * @param {UserResolvable} user The user to fetch + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise} + */ + async fetch(user, { cache = true, force = false } = {}) { + const id = this.resolveId(user); + if (!force) { + const existing = this.cache.get(id); + if (existing && !existing.partial) return existing; } - _setup(users) { - if (!Array.isArray(users)) return; - for (const relationShip of users) { - this.cache.set(relationShip.id, relationShip.type); - } - } + const data = await this.client.api.users['@me'].relationships.get(); + await this._setup(data); + return this.cache.get(id); + } - /** - * Resolves a {@link UserResolvable} to a {@link User} id. - * @param {UserResolvable} user The UserResolvable to identify - * @returns {?Snowflake} - */ - resolveId(user) { - if (user instanceof ThreadMember) return user.id; - if (user instanceof GuildMember) return user.user.id; - if (user instanceof Message) return user.author.id; - if (user instanceof User) return user.id; - return user; - } + // some option .-. - /** - * Obtains a user from Discord, or the user cache if it's already available. - * @param {UserResolvable} user The user to fetch - * @param {BaseFetchOptions} [options] Additional options for this fetch - * @returns {Promise} - */ - async fetch(user, { cache = true, force = false } = {}) { - const id = this.resolveId(user); - if (!force) { - const existing = this.cache.get(id); - if (existing && !existing.partial) return existing; - } + async deleteFriend(user) { + const id = this.resolveId(user); + // check if already friends + if (this.cache.get(id) !== RelationshipTypes.FRIEND) return false; + await this.client.api.users['@me'].relationships[id].delete(); // 204 status and no data + return true; + } - const data = await this.client.api.users['@me'].relationships.get(); - await this._setup(data); - return this.cache.get(id); - } + async deleteBlocked(user) { + const id = this.resolveId(user); + // check if already blocked + if (this.cache.get(id) !== RelationshipTypes.BLOCKED) return false; + await this.client.api.users['@me'].relationships[id].delete(); // 204 status and no data + return true; + } - // some option .-. + async sendFriendRequest(username, discriminator) { + await this.client.api.users('@me').relationships.post({ + data: { + username, + discriminator: parseInt(discriminator), + }, + }); + return true; + } - async deleteFriend(user) { - const id = this.resolveId(user); - // check if already friends - if (this.cache.get(id) !== RelationshipTypes.FRIEND) return false; - await this.client.api.users['@me'].relationships[id].delete(); // 204 status and no data - return true; - } + async addFriend(user) { + const id = this.resolveId(user); + // check if already friends + if (this.cache.get(id) === RelationshipTypes.FRIEND) return false; + // check if outgoing request + if (this.cache.get(id) === RelationshipTypes.OUTGOING_REQUEST) return false; + await this.client.api.users['@me'].relationships[id].put({ + data: { + type: RelationshipTypes.FRIEND, + }, + }); + return true; + } - async deleteBlocked(user) { - const id = this.resolveId(user); - // check if already blocked - if (this.cache.get(id) !== RelationshipTypes.BLOCKED) return false; - await this.client.api.users['@me'].relationships[id].delete(); // 204 status and no data - return true; - } - - async sendFriendRequest(username, discriminator) { - await this.client.api.users('@me').relationships.post({ - data: { - username, - discriminator: parseInt(discriminator), - }, - }); - return true; - } - - async addFriend(user) { - const id = this.resolveId(user); - // check if already friends - if (this.cache.get(id) === RelationshipTypes.FRIEND) return false; - // check if outgoing request - if (this.cache.get(id) === RelationshipTypes.OUTGOING_REQUEST) return false; - await this.client.api - .users['@me'].relationships[id].put({ - data: { - type: RelationshipTypes.FRIEND, - }, - }); - return true; - } - - async addBlocked(user) { - const id = this.resolveId(user); - // check - if (this.cache.get(id) === RelationshipTypes.BLOCKED) return false; - await this.client.api - .users['@me'].relationships[id].put({ - data: { - type: RelationshipTypes.BLOCKED, - }, - }); - return true; - } + async addBlocked(user) { + const id = this.resolveId(user); + // check + if (this.cache.get(id) === RelationshipTypes.BLOCKED) return false; + await this.client.api.users['@me'].relationships[id].put({ + data: { + type: RelationshipTypes.BLOCKED, + }, + }); + return true; + } } module.exports = RelationshipsManager; diff --git a/src/managers/UserManager.js b/src/managers/UserManager.js index c84be8c..d8083bc 100644 --- a/src/managers/UserManager.js +++ b/src/managers/UserManager.js @@ -93,7 +93,10 @@ class UserManager extends CachedManager { const data = await this.client.api.users(id).get(); const userObject = this._add(data, cache); await userObject.getProfile().catch(() => {}); - const noteObject = await this.client.api.users['@me'].notes(id).get().catch(() => null); + const noteObject = await this.client.api.users['@me'] + .notes(id) + .get() + .catch(() => null); userObject.note = noteObject?.note ?? null; return userObject; } diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 8925dc4..f93ac6c 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -11,8 +11,8 @@ const { } = require('../util/Constants'); function parseResponse(res) { - if (res.headers.get('content-type').startsWith('application/json')) return res.json(); - return res.arrayBuffer(); // Cre: TheDevYellowy + if (res.headers.get('content-type').startsWith('application/json')) return res.json(); + return res.arrayBuffer(); // Cre: TheDevYellowy } function getAPIOffset(serverDate) { diff --git a/src/structures/ApplicationCommand.js b/src/structures/ApplicationCommand.js index 3a293d8..bb81038 100644 --- a/src/structures/ApplicationCommand.js +++ b/src/structures/ApplicationCommand.js @@ -1,628 +1,581 @@ -'use strict'; - -const Base = require('./Base'); -const ApplicationCommandPermissionsManager = require('../managers/ApplicationCommandPermissionsManager'); -const { - ApplicationCommandOptionTypes, - ApplicationCommandTypes, - ChannelTypes, -} = require('../util/Constants'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); -const { Message } = require('discord.js'); - -/** - * Represents an application command. - * @extends {Base} - */ -class ApplicationCommand extends Base { - constructor(client, data, guild, guildId) { - super(client); - - /** - * The command's id - * @type {Snowflake} - */ - this.id = data.id; - - /** - * The parent application's id - * @type {Snowflake} - */ - this.applicationId = data.application_id; - - /** - * The guild this command is part of - * @type {?Guild} - */ - this.guild = guild ?? null; - - /** - * The guild's id this command is part of, this may be non-null when `guild` is `null` if the command - * was fetched from the `ApplicationCommandManager` - * @type {?Snowflake} - */ - this.guildId = guild?.id ?? guildId ?? null; - - /** - * The manager for permissions of this command on its guild or arbitrary guilds when the command is global - * @type {ApplicationCommandPermissionsManager} - */ - this.permissions = new ApplicationCommandPermissionsManager( - this, - this.applicationId, - ); - - /** - * The type of this application command - * @type {ApplicationCommandType} - */ - this.type = ApplicationCommandTypes[data.type]; - - this.user = client.users.cache.get(this.applicationId); - - this._patch(data); - } - - _patch(data) { - if ('name' in data) { - /** - * The name of this command - * @type {string} - */ - this.name = data.name; - } - - if ('description' in data) { - /** - * The description of this command - * @type {string} - */ - this.description = data.description; - } - - if ('options' in data) { - /** - * The options of this command - * @type {ApplicationCommandOption[]} - */ - this.options = data.options.map((o) => - this.constructor.transformOption(o, true), - ); - } else { - this.options ??= []; - } - - if ('default_permission' in data) { - /** - * Whether the command is enabled by default when the app is added to a guild - * @type {boolean} - */ - this.defaultPermission = data.default_permission; - } - - if ('version' in data) { - /** - * Autoincrementing version identifier updated during substantial record changes - * @type {Snowflake} - */ - this.version = data.version; - } - } - - /** - * The timestamp the command was created at - * @type {number} - * @readonly - */ - get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); - } - - /** - * The time the command was created at - * @type {Date} - * @readonly - */ - get createdAt() { - return new Date(this.createdTimestamp); - } - - /** - * The manager that this command belongs to - * @type {ApplicationCommandManager} - * @readonly - */ - get manager() { - return (this.guild ?? this.client.application).commands; - } - - /** - * Data for creating or editing an application command. - * @typedef {Object} ApplicationCommandData - * @property {string} name The name of the command - * @property {string} description The description of the command - * @property {ApplicationCommandType} [type] The type of the command - * @property {ApplicationCommandOptionData[]} [options] Options for the command - * @property {boolean} [defaultPermission] Whether the command is enabled by default when the app is added to a guild - */ - - /** - * An option for an application command or subcommand. - * In addition to the listed properties, when used as a parameter, - * API style `snake_case` properties can be used for compatibility with generators like `@discordjs/builders`. - * Note that providing a value for the `camelCase` counterpart for any `snake_case` property - * will discard the provided `snake_case` property. - * @typedef {Object} ApplicationCommandOptionData - * @property {ApplicationCommandOptionType|number} type The type of the option - * @property {string} name The name of the option - * @property {string} description The description of the option - * @property {boolean} [autocomplete] Whether the option is an autocomplete option - * @property {boolean} [required] Whether the option is required - * @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from - * @property {ApplicationCommandOptionData[]} [options] Additional options if this option is a subcommand (group) - * @property {ChannelType[]|number[]} [channelTypes] When the option type is channel, - * the allowed types of channels that can be selected - * @property {number} [minValue] The minimum value for an `INTEGER` or `NUMBER` option - * @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option - */ - - /** - * Edits this application command. - * @param {ApplicationCommandData} data The data to update the command with - * @returns {Promise} - * @example - * // Edit the description of this command - * command.edit({ - * description: 'New description', - * }) - * .then(console.log) - * .catch(console.error); - */ - edit(data) { - return this.manager.edit(this, data, this.guildId); - } - - /** - * Edits the name of this ApplicationCommand - * @param {string} name The new name of the command - * @returns {Promise} - */ - setName(name) { - return this.edit({ name }); - } - - /** - * Edits the description of this ApplicationCommand - * @param {string} description The new description of the command - * @returns {Promise} - */ - setDescription(description) { - return this.edit({ description }); - } - - /** - * Edits the default permission of this ApplicationCommand - * @param {boolean} [defaultPermission=true] The default permission for this command - * @returns {Promise} - */ - setDefaultPermission(defaultPermission = true) { - return this.edit({ defaultPermission }); - } - - /** - * Edits the options of this ApplicationCommand - * @param {ApplicationCommandOptionData[]} options The options to set for this command - * @returns {Promise} - */ - setOptions(options) { - return this.edit({ options }); - } - - /** - * Deletes this command. - * @returns {Promise} - * @example - * // Delete this command - * command.delete() - * .then(console.log) - * .catch(console.error); - */ - delete() { - return this.manager.delete(this, this.guildId); - } - - /** - * Whether this command equals another command. It compares all properties, so for most operations - * it is advisable to just compare `command.id === command2.id` as it is much faster and is often - * what most users need. - * @param {ApplicationCommand|ApplicationCommandData|APIApplicationCommand} command The command to compare with - * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options and choices are in the same - * order in the array The client may not always respect this ordering! - * @returns {boolean} - */ - equals(command, enforceOptionOrder = false) { - // If given an id, check if the id matches - if (command.id && this.id !== command.id) return false; - - // Check top level parameters - const commandType = - typeof command.type === 'string' - ? command.type - : ApplicationCommandTypes[command.type]; - if ( - command.name !== this.name || - ('description' in command && command.description !== this.description) || - ('version' in command && command.version !== this.version) || - ('autocomplete' in command && - command.autocomplete !== this.autocomplete) || - (commandType && commandType !== this.type) || - // Future proof for options being nullable - // TODO: remove ?? 0 on each when nullable - (command.options?.length ?? 0) !== (this.options?.length ?? 0) || - (command.defaultPermission ?? command.default_permission ?? true) !== - this.defaultPermission - ) { - return false; - } - - if (command.options) { - return this.constructor.optionsEqual( - this.options, - command.options, - enforceOptionOrder, - ); - } - return true; - } - - /** - * Recursively checks that all options for an {@link ApplicationCommand} are equal to the provided options. - * In most cases it is better to compare using {@link ApplicationCommand#equals} - * @param {ApplicationCommandOptionData[]} existing The options on the existing command, - * should be {@link ApplicationCommand#options} - * @param {ApplicationCommandOptionData[]|APIApplicationCommandOption[]} options The options to compare against - * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options and choices are in the same - * order in the array The client may not always respect this ordering! - * @returns {boolean} - */ - static optionsEqual(existing, options, enforceOptionOrder = false) { - if (existing.length !== options.length) return false; - if (enforceOptionOrder) { - return existing.every((option, index) => - this._optionEquals(option, options[index], enforceOptionOrder), - ); - } - const newOptions = new Map(options.map((option) => [option.name, option])); - for (const option of existing) { - const foundOption = newOptions.get(option.name); - if (!foundOption || !this._optionEquals(option, foundOption)) - return false; - } - return true; - } - - /** - * Checks that an option for an {@link ApplicationCommand} is equal to the provided option - * In most cases it is better to compare using {@link ApplicationCommand#equals} - * @param {ApplicationCommandOptionData} existing The option on the existing command, - * should be from {@link ApplicationCommand#options} - * @param {ApplicationCommandOptionData|APIApplicationCommandOption} option The option to compare against - * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options or choices are in the same - * order in their array The client may not always respect this ordering! - * @returns {boolean} - * @private - */ - static _optionEquals(existing, option, enforceOptionOrder = false) { - const optionType = - typeof option.type === 'string' - ? option.type - : ApplicationCommandOptionTypes[option.type]; - if ( - option.name !== existing.name || - optionType !== existing.type || - option.description !== existing.description || - option.autocomplete !== existing.autocomplete || - (option.required ?? - (['SUB_COMMAND', 'SUB_COMMAND_GROUP'].includes(optionType) - ? undefined - : false)) !== existing.required || - option.choices?.length !== existing.choices?.length || - option.options?.length !== existing.options?.length || - (option.channelTypes ?? option.channel_types)?.length !== - existing.channelTypes?.length || - (option.minValue ?? option.min_value) !== existing.minValue || - (option.maxValue ?? option.max_value) !== existing.maxValue - ) { - return false; - } - - if (existing.choices) { - if ( - enforceOptionOrder && - !existing.choices.every( - (choice, index) => - choice.name === option.choices[index].name && - choice.value === option.choices[index].value, - ) - ) { - return false; - } - if (!enforceOptionOrder) { - const newChoices = new Map( - option.choices.map((choice) => [choice.name, choice]), - ); - for (const choice of existing.choices) { - const foundChoice = newChoices.get(choice.name); - if (!foundChoice || foundChoice.value !== choice.value) return false; - } - } - } - - if (existing.channelTypes) { - const newTypes = (option.channelTypes ?? option.channel_types).map( - (type) => (typeof type === 'number' ? ChannelTypes[type] : type), - ); - for (const type of existing.channelTypes) { - if (!newTypes.includes(type)) return false; - } - } - - if (existing.options) { - return this.optionsEqual( - existing.options, - option.options, - enforceOptionOrder, - ); - } - return true; - } - - /** - * An option for an application command or subcommand. - * @typedef {Object} ApplicationCommandOption - * @property {ApplicationCommandOptionType} type The type of the option - * @property {string} name The name of the option - * @property {string} description The description of the option - * @property {boolean} [required] Whether the option is required - * @property {boolean} [autocomplete] Whether the option is an autocomplete option - * @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from - * @property {ApplicationCommandOption[]} [options] Additional options if this option is a subcommand (group) - * @property {ChannelType[]} [channelTypes] When the option type is channel, - * the allowed types of channels that can be selected - * @property {number} [minValue] The minimum value for an `INTEGER` or `NUMBER` option - * @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option - */ - - /** - * A choice for an application command option. - * @typedef {Object} ApplicationCommandOptionChoice - * @property {string} name The name of the choice - * @property {string|number} value The value of the choice - */ - - /** - * Transforms an {@link ApplicationCommandOptionData} object into something that can be used with the API. - * @param {ApplicationCommandOptionData} option The option to transform - * @param {boolean} [received] Whether this option has been received from Discord - * @returns {APIApplicationCommandOption} - * @private - */ - static transformOption(option, received) { - const stringType = - typeof option.type === 'string' - ? option.type - : ApplicationCommandOptionTypes[option.type]; - const channelTypesKey = received ? 'channelTypes' : 'channel_types'; - const minValueKey = received ? 'minValue' : 'min_value'; - const maxValueKey = received ? 'maxValue' : 'max_value'; - return { - type: - typeof option.type === 'number' && !received - ? option.type - : ApplicationCommandOptionTypes[option.type], - name: option.name, - description: option.description, - required: - option.required ?? - (stringType === 'SUB_COMMAND' || stringType === 'SUB_COMMAND_GROUP' - ? undefined - : false), - autocomplete: option.autocomplete, - choices: option.choices, - options: option.options?.map((o) => this.transformOption(o, received)), - [channelTypesKey]: received - ? option.channel_types?.map((type) => ChannelTypes[type]) - : option.channelTypes?.map((type) => - typeof type === 'string' ? ChannelTypes[type] : type, - ) ?? - // When transforming to API data, accept API data - option.channel_types, - [minValueKey]: option.minValue ?? option.min_value, - [maxValueKey]: option.maxValue ?? option.max_value, - }; - } - /** - * Send Slash command to channel - * @param {Message} message Discord Message - * @param {Array} options The options to Slash Command - * @returns {Promise} - * @example - * const botID = '12345678987654321' - * const user = await client.users.fetch(botID); - * const application = await user.applications.fetch(); - * const command = application.commands.first(); - * await command.sendSlashCommand(messsage, ['option1', 'option2']); - */ - async sendSlashCommand(message, options = []) { - // Check Options - if (!message instanceof Message) - throw new TypeError('The message must be a Discord.Message'); - if (!Array.isArray(options)) - throw new TypeError('The options must be an array of strings'); - if (this.type !== 'CHAT_INPUT') return false; - const optionFormat = []; - let option_ = []; - let i = 0; - // Check Command type is Sub group ? - const subCommandCheck = this.options.some((option) => - ['SUB_COMMAND', 'SUB_COMMAND_GROUP'].includes(option.type), - ); - let subCommand; - if (subCommandCheck) { - subCommand = this.options.find((option) => option.name == options[0]); - options.shift(); - option_[0] = { - type: ApplicationCommandOptionTypes[subCommand.type], - name: subCommand.name, - options: optionFormat, - }; - } else { - option_ = optionFormat; - } - for (i; i < options.length; i++) { - const value = options[i]; - if (typeof value !== 'string') { - throw new TypeError( - `Expected option to be a String, got ${typeof value}. If you type Number, please convert to String.`, - ); - } - if (!subCommandCheck && !this?.options[i]) continue; - if (subCommandCheck && subCommand?.options && !subCommand?.options[i]) continue; - if (!subCommandCheck) { - // Check value is invalid - let choice; - if (this.options[i].choices && this.options[i].choices.length > 0) { - choice = - this.options[i].choices.find((c) => c.name == value) || - this.options[i].choices.find((c) => c.value == value); - if (!choice) { - throw new Error( - `Invalid option: ${value} is not a valid choice for this option\nList of choices: ${this.options[ - i - ].choices - .map((c, i) => `#${i + 1} Name: ${c.name} Value: ${c.value}\n`) - .join('')}`, - ); - } - } - const data = { - type: ApplicationCommandOptionTypes[this.options[i].type], - name: this.options[i].name, - value: choice?.value || (this.options[i].type == 'INTEGER' ? Number(value) : (this.options[i].type == 'BOOLEAN' ? Boolean(value) : value)), - }; - optionFormat.push(data); - } else { - // First element is sub command and removed - if (!value) continue; - // Check value is invalid - let choice; - if ( - subCommand?.options && - subCommand.options[i].choices && - subCommand.options[i].choices.length > 0 - ) { - choice = - subCommand.options[i].choices.find((c) => c.name == value) || - subCommand.options[i].choices.find((c) => c.value == value); - if (!choice) { - throw new Error( - `Invalid option: ${value} is not a valid choice for this option\nList of choices: \n${subCommand.options[ - i - ].choices.map( - (c, i) => `#${i + 1} Name: ${c.name} Value: ${c.value}\n`, - ).join('')}`, - ); - } - } - const data = { - type: ApplicationCommandOptionTypes[subCommand.options[i].type], - name: subCommand.options[i].name, - value: - choice?.value || - (subCommand.options[i].type == 'INTEGER' - ? Number(value) - : subCommand.options[i].type == 'BOOLEAN' - ? Boolean(value) - : value), - }; - optionFormat.push(data); - } - } - if (!subCommandCheck && this.options[i]?.required) - throw new Error('Value required missing'); - if ( - subCommandCheck && - subCommand?.options && - subCommand?.options[i]?.required - ) - throw new Error('Value required missing'); - await this.client.api.interactions.post({ - body: { - type: 2, // ??? - application_id: this.applicationId, - guild_id: message.guildId, - channel_id: message.channelId, - session_id: this.client.session_id, - data: { - // ApplicationCommandData - version: this.version, - id: this.id, - name: this.name, - type: ApplicationCommandTypes[this.type], - options: option_, - }, - }, - }); - return true; - } - /** - * Message Context Menu - * @param {Message} message Discord Message - * @returns {Promise} - * @example - * const botID = '12345678987654321' - * const user = await client.users.fetch(botID); - * const application = await user.applications.fetch(); - * const command = application.commands.first(); - * await command.sendContextMenu(messsage); - */ - async sendContextMenu(message, sendFromMessage = false) { - if (!message instanceof Message && !sendFromMessage) - throw new TypeError('The message must be a Discord.Message'); - if (this.type == 'CHAT_INPUT') return false; - await this.client.api.interactions.post({ - body: { - type: 2, // ??? - application_id: this.applicationId, - guild_id: message.guildId, - channel_id: message.channelId, - session_id: this.client.session_id, - data: { - // ApplicationCommandData - version: this.version, - id: this.id, - name: this.name, - type: ApplicationCommandTypes[this.type], - target_id: - ApplicationCommandTypes[this.type] == 1 - ? message.author.id - : message.id, - }, - }, - }); - return true; - } -} - -module.exports = ApplicationCommand; - -/* eslint-disable max-len */ -/** - * @external APIApplicationCommand - * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure} - */ - -/** - * @external APIApplicationCommandOption - * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure} - */ +'use strict'; + +const Base = require('./Base'); +const ApplicationCommandPermissionsManager = require('../managers/ApplicationCommandPermissionsManager'); +const { ApplicationCommandOptionTypes, ApplicationCommandTypes, ChannelTypes } = require('../util/Constants'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); +const { Message } = require('discord.js'); + +/** + * Represents an application command. + * @extends {Base} + */ +class ApplicationCommand extends Base { + constructor(client, data, guild, guildId) { + super(client); + + /** + * The command's id + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The parent application's id + * @type {Snowflake} + */ + this.applicationId = data.application_id; + + /** + * The guild this command is part of + * @type {?Guild} + */ + this.guild = guild ?? null; + + /** + * The guild's id this command is part of, this may be non-null when `guild` is `null` if the command + * was fetched from the `ApplicationCommandManager` + * @type {?Snowflake} + */ + this.guildId = guild?.id ?? guildId ?? null; + + /** + * The manager for permissions of this command on its guild or arbitrary guilds when the command is global + * @type {ApplicationCommandPermissionsManager} + */ + this.permissions = new ApplicationCommandPermissionsManager(this, this.applicationId); + + /** + * The type of this application command + * @type {ApplicationCommandType} + */ + this.type = ApplicationCommandTypes[data.type]; + + this.user = client.users.cache.get(this.applicationId); + + this._patch(data); + } + + _patch(data) { + if ('name' in data) { + /** + * The name of this command + * @type {string} + */ + this.name = data.name; + } + + if ('description' in data) { + /** + * The description of this command + * @type {string} + */ + this.description = data.description; + } + + if ('options' in data) { + /** + * The options of this command + * @type {ApplicationCommandOption[]} + */ + this.options = data.options.map(o => this.constructor.transformOption(o, true)); + } else { + this.options ??= []; + } + + if ('default_permission' in data) { + /** + * Whether the command is enabled by default when the app is added to a guild + * @type {boolean} + */ + this.defaultPermission = data.default_permission; + } + + if ('version' in data) { + /** + * Autoincrementing version identifier updated during substantial record changes + * @type {Snowflake} + */ + this.version = data.version; + } + } + + /** + * The timestamp the command was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return SnowflakeUtil.timestampFrom(this.id); + } + + /** + * The time the command was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The manager that this command belongs to + * @type {ApplicationCommandManager} + * @readonly + */ + get manager() { + return (this.guild ?? this.client.application).commands; + } + + /** + * Data for creating or editing an application command. + * @typedef {Object} ApplicationCommandData + * @property {string} name The name of the command + * @property {string} description The description of the command + * @property {ApplicationCommandType} [type] The type of the command + * @property {ApplicationCommandOptionData[]} [options] Options for the command + * @property {boolean} [defaultPermission] Whether the command is enabled by default when the app is added to a guild + */ + + /** + * An option for an application command or subcommand. + * In addition to the listed properties, when used as a parameter, + * API style `snake_case` properties can be used for compatibility with generators like `@discordjs/builders`. + * Note that providing a value for the `camelCase` counterpart for any `snake_case` property + * will discard the provided `snake_case` property. + * @typedef {Object} ApplicationCommandOptionData + * @property {ApplicationCommandOptionType|number} type The type of the option + * @property {string} name The name of the option + * @property {string} description The description of the option + * @property {boolean} [autocomplete] Whether the option is an autocomplete option + * @property {boolean} [required] Whether the option is required + * @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from + * @property {ApplicationCommandOptionData[]} [options] Additional options if this option is a subcommand (group) + * @property {ChannelType[]|number[]} [channelTypes] When the option type is channel, + * the allowed types of channels that can be selected + * @property {number} [minValue] The minimum value for an `INTEGER` or `NUMBER` option + * @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option + */ + + /** + * Edits this application command. + * @param {ApplicationCommandData} data The data to update the command with + * @returns {Promise} + * @example + * // Edit the description of this command + * command.edit({ + * description: 'New description', + * }) + * .then(console.log) + * .catch(console.error); + */ + edit(data) { + return this.manager.edit(this, data, this.guildId); + } + + /** + * Edits the name of this ApplicationCommand + * @param {string} name The new name of the command + * @returns {Promise} + */ + setName(name) { + return this.edit({ name }); + } + + /** + * Edits the description of this ApplicationCommand + * @param {string} description The new description of the command + * @returns {Promise} + */ + setDescription(description) { + return this.edit({ description }); + } + + /** + * Edits the default permission of this ApplicationCommand + * @param {boolean} [defaultPermission=true] The default permission for this command + * @returns {Promise} + */ + setDefaultPermission(defaultPermission = true) { + return this.edit({ defaultPermission }); + } + + /** + * Edits the options of this ApplicationCommand + * @param {ApplicationCommandOptionData[]} options The options to set for this command + * @returns {Promise} + */ + setOptions(options) { + return this.edit({ options }); + } + + /** + * Deletes this command. + * @returns {Promise} + * @example + * // Delete this command + * command.delete() + * .then(console.log) + * .catch(console.error); + */ + delete() { + return this.manager.delete(this, this.guildId); + } + + /** + * Whether this command equals another command. It compares all properties, so for most operations + * it is advisable to just compare `command.id === command2.id` as it is much faster and is often + * what most users need. + * @param {ApplicationCommand|ApplicationCommandData|APIApplicationCommand} command The command to compare with + * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options and choices are in the same + * order in the array The client may not always respect this ordering! + * @returns {boolean} + */ + equals(command, enforceOptionOrder = false) { + // If given an id, check if the id matches + if (command.id && this.id !== command.id) return false; + + // Check top level parameters + const commandType = typeof command.type === 'string' ? command.type : ApplicationCommandTypes[command.type]; + if ( + command.name !== this.name || + ('description' in command && command.description !== this.description) || + ('version' in command && command.version !== this.version) || + ('autocomplete' in command && command.autocomplete !== this.autocomplete) || + (commandType && commandType !== this.type) || + // Future proof for options being nullable + // TODO: remove ?? 0 on each when nullable + (command.options?.length ?? 0) !== (this.options?.length ?? 0) || + (command.defaultPermission ?? command.default_permission ?? true) !== this.defaultPermission + ) { + return false; + } + + if (command.options) { + return this.constructor.optionsEqual(this.options, command.options, enforceOptionOrder); + } + return true; + } + + /** + * Recursively checks that all options for an {@link ApplicationCommand} are equal to the provided options. + * In most cases it is better to compare using {@link ApplicationCommand#equals} + * @param {ApplicationCommandOptionData[]} existing The options on the existing command, + * should be {@link ApplicationCommand#options} + * @param {ApplicationCommandOptionData[]|APIApplicationCommandOption[]} options The options to compare against + * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options and choices are in the same + * order in the array The client may not always respect this ordering! + * @returns {boolean} + */ + static optionsEqual(existing, options, enforceOptionOrder = false) { + if (existing.length !== options.length) return false; + if (enforceOptionOrder) { + return existing.every((option, index) => this._optionEquals(option, options[index], enforceOptionOrder)); + } + const newOptions = new Map(options.map(option => [option.name, option])); + for (const option of existing) { + const foundOption = newOptions.get(option.name); + if (!foundOption || !this._optionEquals(option, foundOption)) { + return false; + } + } + return true; + } + + /** + * Checks that an option for an {@link ApplicationCommand} is equal to the provided option + * In most cases it is better to compare using {@link ApplicationCommand#equals} + * @param {ApplicationCommandOptionData} existing The option on the existing command, + * should be from {@link ApplicationCommand#options} + * @param {ApplicationCommandOptionData|APIApplicationCommandOption} option The option to compare against + * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options or choices are in the same + * order in their array The client may not always respect this ordering! + * @returns {boolean} + * @private + */ + static _optionEquals(existing, option, enforceOptionOrder = false) { + const optionType = typeof option.type === 'string' ? option.type : ApplicationCommandOptionTypes[option.type]; + if ( + option.name !== existing.name || + optionType !== existing.type || + option.description !== existing.description || + option.autocomplete !== existing.autocomplete || + (option.required ?? (['SUB_COMMAND', 'SUB_COMMAND_GROUP'].includes(optionType) ? undefined : false)) !== + existing.required || + option.choices?.length !== existing.choices?.length || + option.options?.length !== existing.options?.length || + (option.channelTypes ?? option.channel_types)?.length !== existing.channelTypes?.length || + (option.minValue ?? option.min_value) !== existing.minValue || + (option.maxValue ?? option.max_value) !== existing.maxValue + ) { + return false; + } + + if (existing.choices) { + if ( + enforceOptionOrder && + !existing.choices.every( + (choice, index) => choice.name === option.choices[index].name && choice.value === option.choices[index].value, + ) + ) { + return false; + } + if (!enforceOptionOrder) { + const newChoices = new Map(option.choices.map(choice => [choice.name, choice])); + for (const choice of existing.choices) { + const foundChoice = newChoices.get(choice.name); + if (!foundChoice || foundChoice.value !== choice.value) return false; + } + } + } + + if (existing.channelTypes) { + const newTypes = (option.channelTypes ?? option.channel_types).map(type => + typeof type === 'number' ? ChannelTypes[type] : type, + ); + for (const type of existing.channelTypes) { + if (!newTypes.includes(type)) return false; + } + } + + if (existing.options) { + return this.optionsEqual(existing.options, option.options, enforceOptionOrder); + } + return true; + } + + /** + * An option for an application command or subcommand. + * @typedef {Object} ApplicationCommandOption + * @property {ApplicationCommandOptionType} type The type of the option + * @property {string} name The name of the option + * @property {string} description The description of the option + * @property {boolean} [required] Whether the option is required + * @property {boolean} [autocomplete] Whether the option is an autocomplete option + * @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from + * @property {ApplicationCommandOption[]} [options] Additional options if this option is a subcommand (group) + * @property {ChannelType[]} [channelTypes] When the option type is channel, + * the allowed types of channels that can be selected + * @property {number} [minValue] The minimum value for an `INTEGER` or `NUMBER` option + * @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option + */ + + /** + * A choice for an application command option. + * @typedef {Object} ApplicationCommandOptionChoice + * @property {string} name The name of the choice + * @property {string|number} value The value of the choice + */ + + /** + * Transforms an {@link ApplicationCommandOptionData} object into something that can be used with the API. + * @param {ApplicationCommandOptionData} option The option to transform + * @param {boolean} [received] Whether this option has been received from Discord + * @returns {APIApplicationCommandOption} + * @private + */ + static transformOption(option, received) { + const stringType = typeof option.type === 'string' ? option.type : ApplicationCommandOptionTypes[option.type]; + const channelTypesKey = received ? 'channelTypes' : 'channel_types'; + const minValueKey = received ? 'minValue' : 'min_value'; + const maxValueKey = received ? 'maxValue' : 'max_value'; + return { + type: typeof option.type === 'number' && !received ? option.type : ApplicationCommandOptionTypes[option.type], + name: option.name, + description: option.description, + required: + option.required ?? (stringType === 'SUB_COMMAND' || stringType === 'SUB_COMMAND_GROUP' ? undefined : false), + autocomplete: option.autocomplete, + choices: option.choices, + options: option.options?.map(o => this.transformOption(o, received)), + [channelTypesKey]: received + ? option.channel_types?.map(type => ChannelTypes[type]) + : option.channelTypes?.map(type => (typeof type === 'string' ? ChannelTypes[type] : type)) ?? + // When transforming to API data, accept API data + option.channel_types, + [minValueKey]: option.minValue ?? option.min_value, + [maxValueKey]: option.maxValue ?? option.max_value, + }; + } + /** + * Send Slash command to channel + * @param {Message} message Discord Message + * @param {Array} options The options to Slash Command + * @returns {Promise} + * @example + * const botID = '12345678987654321' + * const user = await client.users.fetch(botID); + * const application = await user.applications.fetch(); + * const command = application.commands.first(); + * await command.sendSlashCommand(messsage, ['option1', 'option2']); + */ + async sendSlashCommand(message, options = []) { + // Check Options + if (!message instanceof Message) { + throw new TypeError('The message must be a Discord.Message'); + } + if (!Array.isArray(options)) { + throw new TypeError('The options must be an array of strings'); + } + if (this.type !== 'CHAT_INPUT') return false; + const optionFormat = []; + let option_ = []; + let i = 0; + // Check Command type is Sub group ? + const subCommandCheck = this.options.some(option => ['SUB_COMMAND', 'SUB_COMMAND_GROUP'].includes(option.type)); + let subCommand; + if (subCommandCheck) { + subCommand = this.options.find(option => option.name == options[0]); + options.shift(); + option_[0] = { + type: ApplicationCommandOptionTypes[subCommand.type], + name: subCommand.name, + options: optionFormat, + }; + } else { + option_ = optionFormat; + } + for (i; i < options.length; i++) { + const value = options[i]; + if (typeof value !== 'string') { + throw new TypeError( + `Expected option to be a String, got ${typeof value}. If you type Number, please convert to String.`, + ); + } + if (!subCommandCheck && !this?.options[i]) continue; + if (subCommandCheck && subCommand?.options && !subCommand?.options[i]) continue; + if (!subCommandCheck) { + // Check value is invalid + let choice; + if (this.options[i].choices && this.options[i].choices.length > 0) { + choice = + this.options[i].choices.find(c => c.name == value) || this.options[i].choices.find(c => c.value == value); + if (!choice) { + throw new Error( + `Invalid option: ${value} is not a valid choice for this option\nList of choices: ${this.options[ + i + ].choices + .map((c, i) => `#${i + 1} Name: ${c.name} Value: ${c.value}\n`) + .join('')}`, + ); + } + } + const data = { + type: ApplicationCommandOptionTypes[this.options[i].type], + name: this.options[i].name, + value: + choice?.value || + (this.options[i].type == 'INTEGER' + ? Number(value) + : this.options[i].type == 'BOOLEAN' + ? Boolean(value) + : value), + }; + optionFormat.push(data); + } else { + // First element is sub command and removed + if (!value) continue; + // Check value is invalid + let choice; + if (subCommand?.options && subCommand.options[i].choices && subCommand.options[i].choices.length > 0) { + choice = + subCommand.options[i].choices.find(c => c.name == value) || + subCommand.options[i].choices.find(c => c.value == value); + if (!choice) { + throw new Error( + `Invalid option: ${value} is not a valid choice for this option\nList of choices: \n${subCommand.options[ + i + ].choices + .map((c, i) => `#${i + 1} Name: ${c.name} Value: ${c.value}\n`) + .join('')}`, + ); + } + } + const data = { + type: ApplicationCommandOptionTypes[subCommand.options[i].type], + name: subCommand.options[i].name, + value: + choice?.value || + (subCommand.options[i].type == 'INTEGER' + ? Number(value) + : subCommand.options[i].type == 'BOOLEAN' + ? Boolean(value) + : value), + }; + optionFormat.push(data); + } + } + if (!subCommandCheck && this.options[i]?.required) { + throw new Error('Value required missing'); + } + if (subCommandCheck && subCommand?.options && subCommand?.options[i]?.required) { + throw new Error('Value required missing'); + } + await this.client.api.interactions.post({ + body: { + type: 2, // ??? + application_id: this.applicationId, + guild_id: message.guildId, + channel_id: message.channelId, + session_id: this.client.session_id, + data: { + // ApplicationCommandData + version: this.version, + id: this.id, + name: this.name, + type: ApplicationCommandTypes[this.type], + options: option_, + }, + }, + }); + return true; + } + /** + * Message Context Menu + * @param {Message} message Discord Message + * @returns {Promise} + * @example + * const botID = '12345678987654321' + * const user = await client.users.fetch(botID); + * const application = await user.applications.fetch(); + * const command = application.commands.first(); + * await command.sendContextMenu(messsage); + */ + async sendContextMenu(message, sendFromMessage = false) { + if (!message instanceof Message && !sendFromMessage) { + throw new TypeError('The message must be a Discord.Message'); + } + if (this.type == 'CHAT_INPUT') return false; + await this.client.api.interactions.post({ + body: { + type: 2, // ??? + application_id: this.applicationId, + guild_id: message.guildId, + channel_id: message.channelId, + session_id: this.client.session_id, + data: { + // ApplicationCommandData + version: this.version, + id: this.id, + name: this.name, + type: ApplicationCommandTypes[this.type], + target_id: ApplicationCommandTypes[this.type] == 1 ? message.author.id : message.id, + }, + }, + }); + return true; + } +} + +module.exports = ApplicationCommand; + +/* eslint-disable max-len */ +/** + * @external APIApplicationCommand + * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure} + */ + +/** + * @external APIApplicationCommandOption + * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure} + */ diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index 38c5ead..c2470e0 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -1,103 +1,103 @@ -'use strict'; - -const { TypeError } = require('../errors'); -const { MessageComponentTypes, Events } = require('../util/Constants'); - -/** - * Represents an interactive component of a Message. It should not be necessary to construct this directly. - * See {@link MessageComponent} - */ -class BaseMessageComponent { - /** - * Options for a BaseMessageComponent - * @typedef {Object} BaseMessageComponentOptions - * @property {MessageComponentTypeResolvable} type The type of this component - */ - - /** - * Data that can be resolved into options for a MessageComponent. This can be: - * * MessageActionRowOptions - * * MessageButtonOptions - * * MessageSelectMenuOptions - * @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions - */ - - /** - * Components that can be sent in a message. These can be: - * * MessageActionRow - * * MessageButton - * * MessageSelectMenu - * @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent - * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types} - */ - - /** - * Data that can be resolved to a MessageComponentType. This can be: - * * MessageComponentType - * * string - * * number - * @typedef {string|number|MessageComponentType} MessageComponentTypeResolvable - */ - - /** - * @param {BaseMessageComponent|BaseMessageComponentOptions} [data={}] The options for this component - */ - constructor(data) { - /** - * The type of this component - * @type {?MessageComponentType} - */ - this.type = 'type' in data ? BaseMessageComponent.resolveType(data.type) : null; - } - - /** - * Constructs a MessageComponent based on the type of the incoming data - * @param {MessageComponentOptions} data Data for a MessageComponent - * @param {Client|WebhookClient} [client] Client constructing this component - * @returns {?MessageComponent} - * @private - */ - static create(data, client) { - let component; - let type = data.type; - - if (typeof type === 'string') type = MessageComponentTypes[type]; - - switch (type) { - case MessageComponentTypes.ACTION_ROW: { - const MessageActionRow = require('./MessageActionRow'); - component = data instanceof MessageActionRow ? data : new MessageActionRow(data, client); - break; - } - case MessageComponentTypes.BUTTON: { - const MessageButton = require('./MessageButton'); - component = data instanceof MessageButton ? data : new MessageButton(data); - break; - } - case MessageComponentTypes.SELECT_MENU: { - const MessageSelectMenu = require('./MessageSelectMenu'); - component = data instanceof MessageSelectMenu ? data : new MessageSelectMenu(data); - break; - } - default: - if (client) { - client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`); - } else { - throw new TypeError('INVALID_TYPE', 'data.type', 'valid MessageComponentType'); - } - } - return component; - } - - /** - * Resolves the type of a MessageComponent - * @param {MessageComponentTypeResolvable} type The type to resolve - * @returns {MessageComponentType} - * @private - */ - static resolveType(type) { - return typeof type === 'string' ? type : MessageComponentTypes[type]; - } -} - -module.exports = BaseMessageComponent; +'use strict'; + +const { TypeError } = require('../errors'); +const { MessageComponentTypes, Events } = require('../util/Constants'); + +/** + * Represents an interactive component of a Message. It should not be necessary to construct this directly. + * See {@link MessageComponent} + */ +class BaseMessageComponent { + /** + * Options for a BaseMessageComponent + * @typedef {Object} BaseMessageComponentOptions + * @property {MessageComponentTypeResolvable} type The type of this component + */ + + /** + * Data that can be resolved into options for a MessageComponent. This can be: + * * MessageActionRowOptions + * * MessageButtonOptions + * * MessageSelectMenuOptions + * @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions + */ + + /** + * Components that can be sent in a message. These can be: + * * MessageActionRow + * * MessageButton + * * MessageSelectMenu + * @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent + * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types} + */ + + /** + * Data that can be resolved to a MessageComponentType. This can be: + * * MessageComponentType + * * string + * * number + * @typedef {string|number|MessageComponentType} MessageComponentTypeResolvable + */ + + /** + * @param {BaseMessageComponent|BaseMessageComponentOptions} [data={}] The options for this component + */ + constructor(data) { + /** + * The type of this component + * @type {?MessageComponentType} + */ + this.type = 'type' in data ? BaseMessageComponent.resolveType(data.type) : null; + } + + /** + * Constructs a MessageComponent based on the type of the incoming data + * @param {MessageComponentOptions} data Data for a MessageComponent + * @param {Client|WebhookClient} [client] Client constructing this component + * @returns {?MessageComponent} + * @private + */ + static create(data, client) { + let component; + let type = data.type; + + if (typeof type === 'string') type = MessageComponentTypes[type]; + + switch (type) { + case MessageComponentTypes.ACTION_ROW: { + const MessageActionRow = require('./MessageActionRow'); + component = data instanceof MessageActionRow ? data : new MessageActionRow(data, client); + break; + } + case MessageComponentTypes.BUTTON: { + const MessageButton = require('./MessageButton'); + component = data instanceof MessageButton ? data : new MessageButton(data); + break; + } + case MessageComponentTypes.SELECT_MENU: { + const MessageSelectMenu = require('./MessageSelectMenu'); + component = data instanceof MessageSelectMenu ? data : new MessageSelectMenu(data); + break; + } + default: + if (client) { + client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`); + } else { + throw new TypeError('INVALID_TYPE', 'data.type', 'valid MessageComponentType'); + } + } + return component; + } + + /** + * Resolves the type of a MessageComponent + * @param {MessageComponentTypeResolvable} type The type to resolve + * @returns {MessageComponentType} + * @private + */ + static resolveType(type) { + return typeof type === 'string' ? type : MessageComponentTypes[type]; + } +} + +module.exports = BaseMessageComponent; diff --git a/src/structures/Channel.js b/src/structures/Channel.js index f7e2ea1..a148f19 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -1,275 +1,284 @@ -'use strict'; - -const process = require('node:process'); -const Base = require('./Base'); -let CategoryChannel; -let DMChannel; -let NewsChannel; -let StageChannel; -let StoreChannel; -let TextChannel; -let ThreadChannel; -let VoiceChannel; -const { ChannelTypes, ThreadChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); -const { Message } = require('discord.js'); -//const { ApplicationCommand } = require('discord.js-selfbot-v13'); - Not being used in this file, not necessary. - -/** - * @type {WeakSet} - * @private - * @internal - */ -const deletedChannels = new WeakSet(); -let deprecationEmittedForDeleted = false; - -/** - * Represents any channel on Discord. - * @extends {Base} - * @abstract - */ -class Channel extends Base { - constructor(client, data, immediatePatch = true) { - super(client); - - const type = ChannelTypes[data?.type]; - /** - * The type of the channel - * @type {ChannelType} - */ - this.type = type ?? 'UNKNOWN'; - - if (data && immediatePatch) this._patch(data); - } - - _patch(data) { - /** - * The channel's id - * @type {Snowflake} - */ - this.id = data.id; - } - - /** - * The timestamp the channel was created at - * @type {number} - * @readonly - */ - get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); - } - - /** - * The time the channel was created at - * @type {Date} - * @readonly - */ - get createdAt() { - return new Date(this.createdTimestamp); - } - - /** - * Whether or not the structure has been deleted - * @type {boolean} - * @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091 - */ - get deleted() { - if (!deprecationEmittedForDeleted) { - deprecationEmittedForDeleted = true; - process.emitWarning( - 'Channel#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', - 'DeprecationWarning', - ); - } - - return deletedChannels.has(this); - } - - set deleted(value) { - if (!deprecationEmittedForDeleted) { - deprecationEmittedForDeleted = true; - process.emitWarning( - 'Channel#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', - 'DeprecationWarning', - ); - } - - if (value) deletedChannels.add(this); - else deletedChannels.delete(this); - } - - /** - * Whether this Channel is a partial - * This is always false outside of DM channels. - * @type {boolean} - * @readonly - */ - get partial() { - return false; - } - - /** - * When concatenated with a string, this automatically returns the channel's mention instead of the Channel object. - * @returns {string} - * @example - * // Logs: Hello from <#123456789012345678>! - * console.log(`Hello from ${channel}!`); - */ - toString() { - return `<#${this.id}>`; - } - - /** - * Deletes this channel. - * @returns {Promise} - * @example - * // Delete the channel - * channel.delete() - * .then(console.log) - * .catch(console.error); - */ - async delete() { - await this.client.api.channels(this.id).delete(); - return this; - } - - /** - * Fetches this channel. - * @param {boolean} [force=true] Whether to skip the cache check and request the API - * @returns {Promise} - */ - fetch(force = true) { - return this.client.channels.fetch(this.id, { force }); - } - - /** - * Indicates whether this channel is {@link TextBasedChannels text-based}. - * @returns {boolean} - */ - isText() { - return 'messages' in this; - } - - /** - * Indicates whether this channel is {@link BaseGuildVoiceChannel voice-based}. - * @returns {boolean} - */ - isVoice() { - return VoiceBasedChannelTypes.includes(this.type); - } - - /** - * Indicates whether this channel is a {@link ThreadChannel}. - * @returns {boolean} - */ - isThread() { - return ThreadChannelTypes.includes(this.type); - } - - static create(client, data, guild, { allowUnknownGuild, fromInteraction } = {}) { - CategoryChannel ??= require('./CategoryChannel'); - DMChannel ??= require('./DMChannel'); - NewsChannel ??= require('./NewsChannel'); - StageChannel ??= require('./StageChannel'); - StoreChannel ??= require('./StoreChannel'); - TextChannel ??= require('./TextChannel'); - ThreadChannel ??= require('./ThreadChannel'); - VoiceChannel ??= require('./VoiceChannel'); - - let channel; - if (!data.guild_id && !guild) { - if ((data.recipients && data.type !== ChannelTypes.GROUP_DM) || data.type === ChannelTypes.DM) { - channel = new DMChannel(client, data); - } else if (data.type === ChannelTypes.GROUP_DM) { - const PartialGroupDMChannel = require('./PartialGroupDMChannel'); - channel = new PartialGroupDMChannel(client, data); - } - } else { - guild ??= client.guilds.cache.get(data.guild_id); - - if (guild || allowUnknownGuild) { - switch (data.type) { - case ChannelTypes.GUILD_TEXT: { - channel = new TextChannel(guild, data, client); - break; - } - case ChannelTypes.GUILD_VOICE: { - channel = new VoiceChannel(guild, data, client); - break; - } - case ChannelTypes.GUILD_CATEGORY: { - channel = new CategoryChannel(guild, data, client); - break; - } - case ChannelTypes.GUILD_NEWS: { - channel = new NewsChannel(guild, data, client); - break; - } - case ChannelTypes.GUILD_STORE: { - channel = new StoreChannel(guild, data, client); - break; - } - case ChannelTypes.GUILD_STAGE_VOICE: { - channel = new StageChannel(guild, data, client); - break; - } - case ChannelTypes.GUILD_NEWS_THREAD: - case ChannelTypes.GUILD_PUBLIC_THREAD: - case ChannelTypes.GUILD_PRIVATE_THREAD: { - channel = new ThreadChannel(guild, data, client, fromInteraction); - if (!allowUnknownGuild) channel.parent?.threads.cache.set(channel.id, channel); - break; - } - } - if (channel && !allowUnknownGuild) guild.channels?.cache.set(channel.id, channel); - } - } - return channel; - } - - toJSON(...props) { - return super.toJSON({ createdTimestamp: true }, ...props); - } - - // Send Slash - /** - * Send Slash to this channel - * @param {DiscordBot} botID Bot ID - * @param {String} commandName Command name - * @param {Array} args Command arguments - * @returns {Promise} - */ - async sendSlash(botID, commandName, args = []) { - if (!this.isText()) throw new Error('This channel is not text-based.'); - if(!botID) throw new Error('Bot ID is required'); - const user = await this.client.users.fetch(botID).catch(() => {}); - if (!user || !user.bot || !user.applications) throw new Error('BotID is not a bot or does not have an application slash command'); - if (!commandName || typeof commandName !== 'string') throw new Error('Command name is required'); - const listApplication = user.applications.cache.size == 0 ? await user.applications.fetch() : user.applications.cache; - let slashCommand; - await Promise.all(listApplication.map(async application => { - if (commandName == application.name && application.type == 'CHAT_INPUT') slashCommand = application; - })); - if (!slashCommand) throw new Error( - `Command ${commandName} is not found\nList command avalible: ${listApplication.filter(a => a.type == 'CHAT_INPUT').map(a => a.name).join(', ')}`, - ); - return slashCommand.sendSlashCommand( - new Message(this.client, { - channel_id: this.id, - guild_id: this.guild?.id || null, - author: this.client.user, - content: '', - id: this.client.user.id - }), - args - ); - } -} - -exports.Channel = Channel; -exports.deletedChannels = deletedChannels; - -/** - * @external APIChannel - * @see {@link https://discord.com/developers/docs/resources/channel#channel-object} - */ +'use strict'; + +const process = require('node:process'); +const Base = require('./Base'); +let CategoryChannel; +let DMChannel; +let NewsChannel; +let StageChannel; +let StoreChannel; +let TextChannel; +let ThreadChannel; +let VoiceChannel; +const { ChannelTypes, ThreadChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); +const { Message } = require('discord.js'); +// const { ApplicationCommand } = require('discord.js-selfbot-v13'); - Not being used in this file, not necessary. + +/** + * @type {WeakSet} + * @private + * @internal + */ +const deletedChannels = new WeakSet(); +let deprecationEmittedForDeleted = false; + +/** + * Represents any channel on Discord. + * @extends {Base} + * @abstract + */ +class Channel extends Base { + constructor(client, data, immediatePatch = true) { + super(client); + + const type = ChannelTypes[data?.type]; + /** + * The type of the channel + * @type {ChannelType} + */ + this.type = type ?? 'UNKNOWN'; + + if (data && immediatePatch) this._patch(data); + } + + _patch(data) { + /** + * The channel's id + * @type {Snowflake} + */ + this.id = data.id; + } + + /** + * The timestamp the channel was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return SnowflakeUtil.timestampFrom(this.id); + } + + /** + * The time the channel was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Whether or not the structure has been deleted + * @type {boolean} + * @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091 + */ + get deleted() { + if (!deprecationEmittedForDeleted) { + deprecationEmittedForDeleted = true; + process.emitWarning( + 'Channel#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + return deletedChannels.has(this); + } + + set deleted(value) { + if (!deprecationEmittedForDeleted) { + deprecationEmittedForDeleted = true; + process.emitWarning( + 'Channel#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + if (value) deletedChannels.add(this); + else deletedChannels.delete(this); + } + + /** + * Whether this Channel is a partial + * This is always false outside of DM channels. + * @type {boolean} + * @readonly + */ + get partial() { + return false; + } + + /** + * When concatenated with a string, this automatically returns the channel's mention instead of the Channel object. + * @returns {string} + * @example + * // Logs: Hello from <#123456789012345678>! + * console.log(`Hello from ${channel}!`); + */ + toString() { + return `<#${this.id}>`; + } + + /** + * Deletes this channel. + * @returns {Promise} + * @example + * // Delete the channel + * channel.delete() + * .then(console.log) + * .catch(console.error); + */ + async delete() { + await this.client.api.channels(this.id).delete(); + return this; + } + + /** + * Fetches this channel. + * @param {boolean} [force=true] Whether to skip the cache check and request the API + * @returns {Promise} + */ + fetch(force = true) { + return this.client.channels.fetch(this.id, { force }); + } + + /** + * Indicates whether this channel is {@link TextBasedChannels text-based}. + * @returns {boolean} + */ + isText() { + return 'messages' in this; + } + + /** + * Indicates whether this channel is {@link BaseGuildVoiceChannel voice-based}. + * @returns {boolean} + */ + isVoice() { + return VoiceBasedChannelTypes.includes(this.type); + } + + /** + * Indicates whether this channel is a {@link ThreadChannel}. + * @returns {boolean} + */ + isThread() { + return ThreadChannelTypes.includes(this.type); + } + + static create(client, data, guild, { allowUnknownGuild, fromInteraction } = {}) { + CategoryChannel ??= require('./CategoryChannel'); + DMChannel ??= require('./DMChannel'); + NewsChannel ??= require('./NewsChannel'); + StageChannel ??= require('./StageChannel'); + StoreChannel ??= require('./StoreChannel'); + TextChannel ??= require('./TextChannel'); + ThreadChannel ??= require('./ThreadChannel'); + VoiceChannel ??= require('./VoiceChannel'); + + let channel; + if (!data.guild_id && !guild) { + if ((data.recipients && data.type !== ChannelTypes.GROUP_DM) || data.type === ChannelTypes.DM) { + channel = new DMChannel(client, data); + } else if (data.type === ChannelTypes.GROUP_DM) { + const PartialGroupDMChannel = require('./PartialGroupDMChannel'); + channel = new PartialGroupDMChannel(client, data); + } + } else { + guild ??= client.guilds.cache.get(data.guild_id); + + if (guild || allowUnknownGuild) { + switch (data.type) { + case ChannelTypes.GUILD_TEXT: { + channel = new TextChannel(guild, data, client); + break; + } + case ChannelTypes.GUILD_VOICE: { + channel = new VoiceChannel(guild, data, client); + break; + } + case ChannelTypes.GUILD_CATEGORY: { + channel = new CategoryChannel(guild, data, client); + break; + } + case ChannelTypes.GUILD_NEWS: { + channel = new NewsChannel(guild, data, client); + break; + } + case ChannelTypes.GUILD_STORE: { + channel = new StoreChannel(guild, data, client); + break; + } + case ChannelTypes.GUILD_STAGE_VOICE: { + channel = new StageChannel(guild, data, client); + break; + } + case ChannelTypes.GUILD_NEWS_THREAD: + case ChannelTypes.GUILD_PUBLIC_THREAD: + case ChannelTypes.GUILD_PRIVATE_THREAD: { + channel = new ThreadChannel(guild, data, client, fromInteraction); + if (!allowUnknownGuild) channel.parent?.threads.cache.set(channel.id, channel); + break; + } + } + if (channel && !allowUnknownGuild) guild.channels?.cache.set(channel.id, channel); + } + } + return channel; + } + + toJSON(...props) { + return super.toJSON({ createdTimestamp: true }, ...props); + } + + // Send Slash + /** + * Send Slash to this channel + * @param {DiscordBot} botID Bot ID + * @param {String} commandName Command name + * @param {Array} args Command arguments + * @returns {Promise} + */ + async sendSlash(botID, commandName, args = []) { + if (!this.isText()) throw new Error('This channel is not text-based.'); + if (!botID) throw new Error('Bot ID is required'); + const user = await this.client.users.fetch(botID).catch(() => {}); + if (!user || !user.bot || !user.applications) + throw new Error('BotID is not a bot or does not have an application slash command'); + if (!commandName || typeof commandName !== 'string') throw new Error('Command name is required'); + const listApplication = + user.applications.cache.size == 0 ? await user.applications.fetch() : user.applications.cache; + let slashCommand; + await Promise.all( + listApplication.map(async application => { + if (commandName == application.name && application.type == 'CHAT_INPUT') slashCommand = application; + }), + ); + if (!slashCommand) { + throw new Error( + `Command ${commandName} is not found\nList command avalible: ${listApplication + .filter(a => a.type == 'CHAT_INPUT') + .map(a => a.name) + .join(', ')}`, + ); + } + return slashCommand.sendSlashCommand( + new Message(this.client, { + channel_id: this.id, + guild_id: this.guild?.id || null, + author: this.client.user, + content: '', + id: this.client.user.id, + }), + args, + ); + } +} + +exports.Channel = Channel; +exports.deletedChannels = deletedChannels; + +/** + * @external APIChannel + * @see {@link https://discord.com/developers/docs/resources/channel#channel-object} + */ diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index 88a4745..a1b6f76 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -18,7 +18,7 @@ class ClientApplication extends Application { * The application command manager for this application * @type {ApplicationCommandManager} */ - this.commands = null // Selfbot + this.commands = null; // Selfbot } _patch(data) { @@ -97,7 +97,7 @@ class ClientApplication extends Application { * @returns {Promise} */ async fetch() { - if(!this.client.user.bot) throw new Error("INVALID_USER_METHOD"); + if (!this.client.user.bot) throw new Error('INVALID_USER_METHOD'); const app = await this.client.api.oauth2.applications('@me').get(); this._patch(app); return this; diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index 897fd8e..68f93ac 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -54,14 +54,14 @@ class ClientPresence extends Presence { } } else if (!activities && (status || afk || since) && this.activities.length) { data.activities.push( - ...this.activities.map((a) => - Object.assign(a, { - name: a.name, - type: a.type, - url: a.url ?? undefined, - }), - ), - ); + ...this.activities.map(a => + Object.assign(a, { + name: a.name, + type: a.type, + url: a.url ?? undefined, + }), + ), + ); } return data; diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 7adad2d..73be7f0 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -1,364 +1,368 @@ -'use strict'; - -const User = require('./User'); -const DataResolver = require('../util/DataResolver'); -const { HypeSquadOptions } = require('../util/Constants'); -const { Util } = require('..'); -const { Collection } = require('@discordjs/collection'); - -/** - * Represents the logged in client's Discord user. - * @extends {User} - */ -class ClientUser extends User { - _patch(data) { - super._patch(data); - - /* - Add: notes - */ - this.notes = new Collection(); - // this.messageMentions = new Collection(); - - if ('verified' in data) { - /** - * Whether or not this account has been verified - * @type {boolean} - */ - this.verified = data.verified; - } - - if ('mfa_enabled' in data) { - /** - * If the bot's {@link ClientApplication#owner Owner} has MFA enabled on their account - * @type {?boolean} - */ - this.mfaEnabled = - typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; - } else { - this.mfaEnabled ??= null; - } - - if ('token' in data) this.client.token = data.token; - - // Add (Selfbot) - if ('premium' in data) this.nitro = data.premium; - /** - * Nitro Status - * `0`: None - * `1`: Classic - * `2`: Boost - * @external - * https://discord.com/developers/docs/resources/user#user-object-premium-types - * @type {Number} - */ - if ('purchased_flags' in data) this.nitroType = data.purchased_flags; - if ('phone' in data) this.phoneNumber = data.phone; - if ('nsfw_allowed' in data) this.nsfwAllowed = data.nsfw_allowed; - if ('email' in data) this.emailAddress = data.email; - } - - /** - * Represents the client user's presence - * @type {ClientPresence} - * @readonly - */ - get presence() { - return this.client.presence; - } - - /** - * Data used to edit the logged in client - * @typedef {Object} ClientUserEditData - * @property {string} [username] The new username - * @property {?(BufferResolvable|Base64Resolvable)} [avatar] The new avatar - */ - - /** - * Edits the logged in client. - * @param {ClientUserEditData} data The new data - * @returns {Promise} - */ - async edit(data) { - if (typeof data.avatar !== 'undefined') - data.avatar = await DataResolver.resolveImage(data.avatar); - if (typeof data.banner !== 'undefined') - data.banner = await DataResolver.resolveImage(data.banner); - const newData = await this.client.api.users('@me').patch({ data }); - this.client.token = newData.token; - this.client.password = data?.password - ? data?.password - : this.client.password; - const { updated } = this.client.actions.UserUpdate.handle(newData); - return updated ?? this; - } - - /** - * Sets the username of the logged in client. - * Changing usernames in Discord is heavily rate limited, with only 2 requests - * every hour. Use this sparingly! - * @param {string} username The new username - * @param {string} password The password of the account - * @returns {Promise} - * @example - * // Set username - * client.user.setUsername('discordjs') - * .then(user => console.log(`My new username is ${user.username}`)) - * .catch(console.error); - */ - setUsername(username, password) { - if (!password && !this.client.password) - throw new Error('A password is required to change a username.'); - return this.edit({ - username, - password: this.client.password ? this.client.password : password, - }); - } - - /** - * Sets the avatar of the logged in client. - * @param {?(BufferResolvable|Base64Resolvable)} avatar The new avatar - * @returns {Promise} - * @example - * // Set avatar - * client.user.setAvatar('./avatar.png') - * .then(user => console.log(`New avatar set!`)) - * .catch(console.error); - */ - setAvatar(avatar) { - return this.edit({ avatar }); - } - /** - * Sets the banner of the logged in client. - * @param {?(BufferResolvable|Base64Resolvable)} banner The new banner - * @returns {Promise} - * @example - * // Set banner - * client.user.setBanner('./banner.png') - * .then(user => console.log(`New banner set!`)) - * .catch(console.error); - */ - setBanner(banner) { - if (this.nitroType !== 2) - throw new Error( - 'You must be a Nitro Boosted User to change your banner.', - ); - return this.edit({ banner }); - } - - /** - * Set HyperSquad House - * @param {HypeSquadOptions} type - * `LEAVE`: 0 - * `HOUSE_BRAVERY`: 1 - * `HOUSE_BRILLIANCE`: 2 - * `HOUSE_BALANCE`: 3 - * @returns {Promise} - * @example - * // Set HyperSquad HOUSE_BRAVERY - * client.user.setHypeSquad(1); || client.user.setHypeSquad('HOUSE_BRAVERY'); - * // Leave - * client.user.setHypeSquad(0); - */ - async setHypeSquad(type) { - const id = typeof type === 'string' ? HypeSquadOptions[type] : type; - if (!id && id !== 0) throw new Error('Invalid HypeSquad type.'); - if (id !== 0) return await this.client.api.hypesquad.online.post({ - data: { house_id: id }, - }); - else return await this.client.api.hypesquad.online.delete(); - } - - /** - * Set Accent color - * @param {ColorResolvable} color Color to set - * @returns {Promise} - */ - setAccentColor(color = null) { - return this.edit({ accent_color: color ? Util.resolveColor(color) : null }); - } - - /** - * Set discriminator - * @param {User.discriminator} discriminator It is #1234 - * @param {string} password The password of the account - * @returns {Promise} - */ - setDiscriminator(discriminator, password) { - if (!this.nitro) throw new Error('You must be a Nitro User to change your discriminator.'); - if (!password && !this.client.password) - throw new Error('A password is required to change a discriminator.'); - return this.edit({ - discriminator, - password: this.client.password ? this.client.password : password, - }); - } - - /** - * Set About me - * @param {String} bio Bio to set - * @returns {Promise} - */ - setAboutMe(bio = null) { - return this.edit({ - bio, - }); - } - - /** - * Change the email - * @param {Email} email Email to change - * @param {string} password Password of the account - * @returns {Promise} - */ - setEmail(email, password) { - if (!password && !this.client.password) - throw new Error('A password is required to change a email.'); - return this.edit({ - email, - password: this.client.password ? this.client.password : password, - }); - } - - /** - * Set new password - * @param {string} oldPassword Old password - * @param {string} newPassword New password to set - * @returns {Promise} - */ - setPassword(oldPassword, newPassword) { - if (!oldPassword && !this.client.password) - throw new Error('A password is required to change a password.'); - if (!newPassword) throw new Error('New password is required.'); - return this.edit({ - password: this.client.password ? this.client.password : oldPassword, - new_password: newPassword, - }); - } - - /** - * Disable account - * @param {string} password Password of the account - * @returns {Promise} - */ - async disableAccount(password) { - if (!password && !this.client.password) - throw new Error('A password is required to disable an account.'); - return await this.client.api.users['@me'].disable.post({ - data: { - password: this.client.password ? this.client.password : password, - }, - }); - } - - /** - * Delete account. Warning: Cannot be changed once used! - * @param {string} password Password of the account - * @returns {Promise} - */ - async deleteAccount(password) { - if (!password && !this.client.password) - throw new Error('A password is required to delete an account.'); - return await this.client.api.users['@me'].delete.post({ - data: { - password: this.client.password ? this.client.password : password, - }, - }); - } - - /** - * Options for setting activities - * @typedef {Object} ActivitiesOptions - * @property {string} [name] Name of the activity - * @property {ActivityType|number} [type] Type of the activity - * @property {string} [url] Twitch / YouTube stream URL - */ - - /** - * Data resembling a raw Discord presence. - * @typedef {Object} PresenceData - * @property {PresenceStatusData} [status] Status of the user - * @property {boolean} [afk] Whether the user is AFK - * @property {ActivitiesOptions[]} [activities] Activity the user is playing - * @property {number|number[]} [shardId] Shard id(s) to have the activity set on - */ - - /** - * Sets the full presence of the client user. - * @param {PresenceData} data Data for the presence - * @returns {ClientPresence} - * @example - * // Set the client user's presence - * client.user.setPresence({ activities: [{ name: 'with discord.js' }], status: 'idle' }); - */ - setPresence(data) { - return this.client.presence.set(data); - } - - /** - * A user's status. Must be one of: - * * `online` - * * `idle` - * * `invisible` - * * `dnd` (do not disturb) - * @typedef {string} PresenceStatusData - */ - - /** - * Sets the status of the client user. - * @param {PresenceStatusData} status Status to change to - * @param {number|number[]} [shardId] Shard id(s) to have the activity set on - * @returns {ClientPresence} - * @example - * // Set the client user's status - * client.user.setStatus('idle'); - */ - setStatus(status, shardId) { - return this.setPresence({ status, shardId }); - } - - /** - * Options for setting an activity. - * @typedef {Object} ActivityOptions - * @property {string} [name] Name of the activity - * @property {string} [url] Twitch / YouTube stream URL - * @property {ActivityType|number} [type] Type of the activity - * @property {number|number[]} [shardId] Shard Id(s) to have the activity set on - */ - - /** - * Sets the activity the client user is playing. - * @param {string|ActivityOptions} [name] Activity being played, or options for setting the activity - * @param {ActivityOptions} [options] Options for setting the activity - * @returns {ClientPresence} - * @example - * // Set the client user's activity - * client.user.setActivity('discord.js', { type: 'WATCHING' }); - */ - setActivity(name, options = {}) { - if (!name) - return this.setPresence({ activities: [], shardId: options.shardId }); - - const activity = Object.assign( - {}, - options, - typeof name === 'object' ? name : { name }, - ); - return this.setPresence({ - activities: [activity], - shardId: activity.shardId, - }); - } - - /** - * Sets/removes the AFK flag for the client user. - * @param {boolean} [afk=true] Whether or not the user is AFK - * @param {number|number[]} [shardId] Shard Id(s) to have the AFK flag set on - * @returns {ClientPresence} - */ - setAFK(afk = true, shardId) { - return this.setPresence({ afk, shardId }); - } -} - -module.exports = ClientUser; +'use strict'; + +const User = require('./User'); +const DataResolver = require('../util/DataResolver'); +const { HypeSquadOptions } = require('../util/Constants'); +const { Util } = require('..'); +const { Collection } = require('@discordjs/collection'); + +/** + * Represents the logged in client's Discord user. + * @extends {User} + */ +class ClientUser extends User { + _patch(data) { + super._patch(data); + + /* + Add: notes + */ + this.notes = new Collection(); + // this.messageMentions = new Collection(); + + if ('verified' in data) { + /** + * Whether or not this account has been verified + * @type {boolean} + */ + this.verified = data.verified; + } + + if ('mfa_enabled' in data) { + /** + * If the bot's {@link ClientApplication#owner Owner} has MFA enabled on their account + * @type {?boolean} + */ + this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; + } else { + this.mfaEnabled ??= null; + } + + if ('token' in data) this.client.token = data.token; + + // Add (Selfbot) + if ('premium' in data) this.nitro = data.premium; + /** + * Nitro Status + * `0`: None + * `1`: Classic + * `2`: Boost + * @external + * https://discord.com/developers/docs/resources/user#user-object-premium-types + * @type {Number} + */ + if ('purchased_flags' in data) this.nitroType = data.purchased_flags; + if ('phone' in data) this.phoneNumber = data.phone; + if ('nsfw_allowed' in data) this.nsfwAllowed = data.nsfw_allowed; + if ('email' in data) this.emailAddress = data.email; + } + + /** + * Represents the client user's presence + * @type {ClientPresence} + * @readonly + */ + get presence() { + return this.client.presence; + } + + /** + * Data used to edit the logged in client + * @typedef {Object} ClientUserEditData + * @property {string} [username] The new username + * @property {?(BufferResolvable|Base64Resolvable)} [avatar] The new avatar + */ + + /** + * Edits the logged in client. + * @param {ClientUserEditData} data The new data + * @returns {Promise} + */ + async edit(data) { + if (typeof data.avatar !== 'undefined') { + data.avatar = await DataResolver.resolveImage(data.avatar); + } + if (typeof data.banner !== 'undefined') { + data.banner = await DataResolver.resolveImage(data.banner); + } + const newData = await this.client.api.users('@me').patch({ data }); + this.client.token = newData.token; + this.client.password = data?.password ? data?.password : this.client.password; + const { updated } = this.client.actions.UserUpdate.handle(newData); + return updated ?? this; + } + + /** + * Sets the username of the logged in client. + * Changing usernames in Discord is heavily rate limited, with only 2 requests + * every hour. Use this sparingly! + * @param {string} username The new username + * @param {string} password The password of the account + * @returns {Promise} + * @example + * // Set username + * client.user.setUsername('discordjs') + * .then(user => console.log(`My new username is ${user.username}`)) + * .catch(console.error); + */ + setUsername(username, password) { + if (!password && !this.client.password) { + throw new Error('A password is required to change a username.'); + } + return this.edit({ + username, + password: this.client.password ? this.client.password : password, + }); + } + + /** + * Sets the avatar of the logged in client. + * @param {?(BufferResolvable|Base64Resolvable)} avatar The new avatar + * @returns {Promise} + * @example + * // Set avatar + * client.user.setAvatar('./avatar.png') + * .then(user => console.log(`New avatar set!`)) + * .catch(console.error); + */ + setAvatar(avatar) { + return this.edit({ avatar }); + } + /** + * Sets the banner of the logged in client. + * @param {?(BufferResolvable|Base64Resolvable)} banner The new banner + * @returns {Promise} + * @example + * // Set banner + * client.user.setBanner('./banner.png') + * .then(user => console.log(`New banner set!`)) + * .catch(console.error); + */ + setBanner(banner) { + if (this.nitroType !== 2) { + throw new Error('You must be a Nitro Boosted User to change your banner.'); + } + return this.edit({ banner }); + } + + /** + * Set HyperSquad House + * @param {HypeSquadOptions} type + * `LEAVE`: 0 + * `HOUSE_BRAVERY`: 1 + * `HOUSE_BRILLIANCE`: 2 + * `HOUSE_BALANCE`: 3 + * @returns {Promise} + * @example + * // Set HyperSquad HOUSE_BRAVERY + * client.user.setHypeSquad(1); || client.user.setHypeSquad('HOUSE_BRAVERY'); + * // Leave + * client.user.setHypeSquad(0); + */ + async setHypeSquad(type) { + const id = typeof type === 'string' ? HypeSquadOptions[type] : type; + if (!id && id !== 0) throw new Error('Invalid HypeSquad type.'); + if (id !== 0) { + return await this.client.api.hypesquad.online.post({ + data: { house_id: id }, + }); + } else { + return await this.client.api.hypesquad.online.delete(); + } + } + + /** + * Set Accent color + * @param {ColorResolvable} color Color to set + * @returns {Promise} + */ + setAccentColor(color = null) { + return this.edit({ accent_color: color ? Util.resolveColor(color) : null }); + } + + /** + * Set discriminator + * @param {User.discriminator} discriminator It is #1234 + * @param {string} password The password of the account + * @returns {Promise} + */ + setDiscriminator(discriminator, password) { + if (!this.nitro) throw new Error('You must be a Nitro User to change your discriminator.'); + if (!password && !this.client.password) { + throw new Error('A password is required to change a discriminator.'); + } + return this.edit({ + discriminator, + password: this.client.password ? this.client.password : password, + }); + } + + /** + * Set About me + * @param {String} bio Bio to set + * @returns {Promise} + */ + setAboutMe(bio = null) { + return this.edit({ + bio, + }); + } + + /** + * Change the email + * @param {Email} email Email to change + * @param {string} password Password of the account + * @returns {Promise} + */ + setEmail(email, password) { + if (!password && !this.client.password) { + throw new Error('A password is required to change a email.'); + } + return this.edit({ + email, + password: this.client.password ? this.client.password : password, + }); + } + + /** + * Set new password + * @param {string} oldPassword Old password + * @param {string} newPassword New password to set + * @returns {Promise} + */ + setPassword(oldPassword, newPassword) { + if (!oldPassword && !this.client.password) { + throw new Error('A password is required to change a password.'); + } + if (!newPassword) throw new Error('New password is required.'); + return this.edit({ + password: this.client.password ? this.client.password : oldPassword, + new_password: newPassword, + }); + } + + /** + * Disable account + * @param {string} password Password of the account + * @returns {Promise} + */ + async disableAccount(password) { + if (!password && !this.client.password) { + throw new Error('A password is required to disable an account.'); + } + return await this.client.api.users['@me'].disable.post({ + data: { + password: this.client.password ? this.client.password : password, + }, + }); + } + + /** + * Delete account. Warning: Cannot be changed once used! + * @param {string} password Password of the account + * @returns {Promise} + */ + async deleteAccount(password) { + if (!password && !this.client.password) { + throw new Error('A password is required to delete an account.'); + } + return await this.client.api.users['@me'].delete.post({ + data: { + password: this.client.password ? this.client.password : password, + }, + }); + } + + /** + * Options for setting activities + * @typedef {Object} ActivitiesOptions + * @property {string} [name] Name of the activity + * @property {ActivityType|number} [type] Type of the activity + * @property {string} [url] Twitch / YouTube stream URL + */ + + /** + * Data resembling a raw Discord presence. + * @typedef {Object} PresenceData + * @property {PresenceStatusData} [status] Status of the user + * @property {boolean} [afk] Whether the user is AFK + * @property {ActivitiesOptions[]} [activities] Activity the user is playing + * @property {number|number[]} [shardId] Shard id(s) to have the activity set on + */ + + /** + * Sets the full presence of the client user. + * @param {PresenceData} data Data for the presence + * @returns {ClientPresence} + * @example + * // Set the client user's presence + * client.user.setPresence({ activities: [{ name: 'with discord.js' }], status: 'idle' }); + */ + setPresence(data) { + return this.client.presence.set(data); + } + + /** + * A user's status. Must be one of: + * * `online` + * * `idle` + * * `invisible` + * * `dnd` (do not disturb) + * @typedef {string} PresenceStatusData + */ + + /** + * Sets the status of the client user. + * @param {PresenceStatusData} status Status to change to + * @param {number|number[]} [shardId] Shard id(s) to have the activity set on + * @returns {ClientPresence} + * @example + * // Set the client user's status + * client.user.setStatus('idle'); + */ + setStatus(status, shardId) { + return this.setPresence({ status, shardId }); + } + + /** + * Options for setting an activity. + * @typedef {Object} ActivityOptions + * @property {string} [name] Name of the activity + * @property {string} [url] Twitch / YouTube stream URL + * @property {ActivityType|number} [type] Type of the activity + * @property {number|number[]} [shardId] Shard Id(s) to have the activity set on + */ + + /** + * Sets the activity the client user is playing. + * @param {string|ActivityOptions} [name] Activity being played, or options for setting the activity + * @param {ActivityOptions} [options] Options for setting the activity + * @returns {ClientPresence} + * @example + * // Set the client user's activity + * client.user.setActivity('discord.js', { type: 'WATCHING' }); + */ + setActivity(name, options = {}) { + if (!name) { + return this.setPresence({ activities: [], shardId: options.shardId }); + } + + const activity = Object.assign({}, options, typeof name === 'object' ? name : { name }); + return this.setPresence({ + activities: [activity], + shardId: activity.shardId, + }); + } + + /** + * Sets/removes the AFK flag for the client user. + * @param {boolean} [afk=true] Whether or not the user is AFK + * @param {number|number[]} [shardId] Shard Id(s) to have the AFK flag set on + * @returns {ClientPresence} + */ + setAFK(afk = true, shardId) { + return this.setPresence({ afk, shardId }); + } +} + +module.exports = ClientUser; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 5768496..555de4f 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -1,101 +1,101 @@ -'use strict'; - -const { Channel } = require('./Channel'); -const TextBasedChannel = require('./interfaces/TextBasedChannel'); -const MessageManager = require('../managers/MessageManager'); - -/** - * Represents a direct message channel between two users. - * @extends {Channel} - * @implements {TextBasedChannel} - */ -class DMChannel extends Channel { - constructor(client, data) { - super(client, data); - - // Override the channel type so partials have a known type - this.type = 'DM'; - - /** - * A manager of the messages belonging to this channel - * @type {MessageManager} - */ - this.messages = new MessageManager(this); - } - - _patch(data) { - super._patch(data); - - if (data.recipients) { - /** - * The recipient on the other end of the DM - * @type {User} - */ - this.recipient = this.client.users._add(data.recipients[0]); - } - - if ('last_message_id' in data) { - /** - * The channel's last message id, if one was sent - * @type {?Snowflake} - */ - this.lastMessageId = data.last_message_id; - } - - if ('last_pin_timestamp' in data) { - /** - * The timestamp when the last pinned message was pinned, if there was one - * @type {?number} - */ - this.lastPinTimestamp = new Date(data.last_pin_timestamp).getTime(); - } else { - this.lastPinTimestamp ??= null; - } - } - - /** - * Whether this DMChannel is a partial - * @type {boolean} - * @readonly - */ - get partial() { - return typeof this.lastMessageId === 'undefined'; - } - - /** - * Fetch this DMChannel. - * @param {boolean} [force=true] Whether to skip the cache check and request the API - * @returns {Promise} - */ - fetch(force = true) { - return this.recipient.createDM(force); - } - - /** - * When concatenated with a string, this automatically returns the recipient's mention instead of the - * DMChannel object. - * @returns {string} - * @example - * // Logs: Hello from <@123456789012345678>! - * console.log(`Hello from ${channel}!`); - */ - toString() { - return this.recipient.toString(); - } - - // These are here only for documentation purposes - they are implemented by TextBasedChannel - /* eslint-disable no-empty-function */ - get lastMessage() {} - get lastPinAt() {} - send() {} - sendTyping() {} - createMessageCollector() {} - awaitMessages() {} - createMessageComponentCollector() {} - awaitMessageComponent() {} - // Doesn't work on DM channels; bulkDelete() {} -} - -TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']); - -module.exports = DMChannel; +'use strict'; + +const { Channel } = require('./Channel'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const MessageManager = require('../managers/MessageManager'); + +/** + * Represents a direct message channel between two users. + * @extends {Channel} + * @implements {TextBasedChannel} + */ +class DMChannel extends Channel { + constructor(client, data) { + super(client, data); + + // Override the channel type so partials have a known type + this.type = 'DM'; + + /** + * A manager of the messages belonging to this channel + * @type {MessageManager} + */ + this.messages = new MessageManager(this); + } + + _patch(data) { + super._patch(data); + + if (data.recipients) { + /** + * The recipient on the other end of the DM + * @type {User} + */ + this.recipient = this.client.users._add(data.recipients[0]); + } + + if ('last_message_id' in data) { + /** + * The channel's last message id, if one was sent + * @type {?Snowflake} + */ + this.lastMessageId = data.last_message_id; + } + + if ('last_pin_timestamp' in data) { + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = new Date(data.last_pin_timestamp).getTime(); + } else { + this.lastPinTimestamp ??= null; + } + } + + /** + * Whether this DMChannel is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return typeof this.lastMessageId === 'undefined'; + } + + /** + * Fetch this DMChannel. + * @param {boolean} [force=true] Whether to skip the cache check and request the API + * @returns {Promise} + */ + fetch(force = true) { + return this.recipient.createDM(force); + } + + /** + * When concatenated with a string, this automatically returns the recipient's mention instead of the + * DMChannel object. + * @returns {string} + * @example + * // Logs: Hello from <@123456789012345678>! + * console.log(`Hello from ${channel}!`); + */ + toString() { + return this.recipient.toString(); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + get lastMessage() {} + get lastPinAt() {} + send() {} + sendTyping() {} + createMessageCollector() {} + awaitMessages() {} + createMessageComponentCollector() {} + awaitMessageComponent() {} + // Doesn't work on DM channels; bulkDelete() {} +} + +TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']); + +module.exports = DMChannel; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 52718b1..57e985d 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -55,1562 +55,1518 @@ const deletedGuilds = new WeakSet(); * @extends {AnonymousGuild} */ class Guild extends AnonymousGuild { - constructor(client, data) { - super(client, data, false); - - /** - * A manager of the members belonging to this guild - * @type {GuildMemberManager} - */ - this.members = new GuildMemberManager(this); - - /** - * A manager of the channels belonging to this guild - * @type {GuildChannelManager} - */ - this.channels = new GuildChannelManager(this); - - /** - * A manager of the bans belonging to this guild - * @type {GuildBanManager} - */ - this.bans = new GuildBanManager(this); - - /** - * A manager of the roles belonging to this guild - * @type {RoleManager} - */ - this.roles = new RoleManager(this); - - /** - * A manager of the presences belonging to this guild - * @type {PresenceManager} - */ - this.presences = new PresenceManager(this.client); - - /** - * A manager of the voice states of this guild - * @type {VoiceStateManager} - */ - this.voiceStates = new VoiceStateManager(this); - - /** - * A manager of the stage instances of this guild - * @type {StageInstanceManager} - */ - this.stageInstances = new StageInstanceManager(this); - - /** - * A manager of the invites of this guild - * @type {GuildInviteManager} - */ - this.invites = new GuildInviteManager(this); - - /** - * A manager of the scheduled events of this guild - * @type {GuildScheduledEventManager} - */ - this.scheduledEvents = new GuildScheduledEventManager(this); - - if (!data) return; - if (data.unavailable) { - /** - * Whether the guild is available to access. If it is not available, it indicates a server outage - * @type {boolean} - */ - this.available = false; - } else { - this._patch(data); - if (!data.channels) this.available = false; - } - - /** - * The id of the shard this Guild belongs to. - * @type {number} - */ - this.shardId = data.shardId; - - this.disableDM = false; - } - - /** - * Whether or not the structure has been deleted - * @type {boolean} - * @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091 - */ - get deleted() { - if (!deprecationEmittedForDeleted) { - deprecationEmittedForDeleted = true; - process.emitWarning( - 'Guild#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', - 'DeprecationWarning', - ); - } - - return deletedGuilds.has(this); - } - - set deleted(value) { - if (!deprecationEmittedForDeleted) { - deprecationEmittedForDeleted = true; - process.emitWarning( - 'Guild#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', - 'DeprecationWarning', - ); - } - - if (value) deletedGuilds.add(this); - else deletedGuilds.delete(this); - } - - /** - * The Shard this Guild belongs to. - * @type {WebSocketShard} - * @readonly - */ - get shard() { - return this.client.ws.shards.get(this.shardId); - } - - _patch(data) { - super._patch(data); - this.id = data.id; - if ('name' in data) this.name = data.name; - if ('icon' in data) this.icon = data.icon; - if ('unavailable' in data) { - this.available = !data.unavailable; - } else { - this.available ??= true; - } - - if ('discovery_splash' in data) { - /** - * The hash of the guild discovery splash image - * @type {?string} - */ - this.discoverySplash = data.discovery_splash; - } - - if ('member_count' in data) { - /** - * The full amount of members in this guild - * @type {number} - */ - this.memberCount = data.member_count; - } - - if ('large' in data) { - /** - * Whether the guild is "large" (has more than {@link WebsocketOptions large_threshold} members, 50 by default) - * @type {boolean} - */ - this.large = Boolean(data.large); - } - - if ('premium_progress_bar_enabled' in data) { - /** - * Whether this guild has its premium (boost) progress bar enabled - * @type {boolean} - */ - this.premiumProgressBarEnabled = data.premium_progress_bar_enabled; - } - - /** - * An array of enabled guild features, here are the possible values: - * * ANIMATED_ICON - * * BANNER - * * COMMERCE - * * COMMUNITY - * * DISCOVERABLE - * * FEATURABLE - * * INVITE_SPLASH - * * MEMBER_VERIFICATION_GATE_ENABLED - * * NEWS - * * PARTNERED - * * PREVIEW_ENABLED - * * VANITY_URL - * * VERIFIED - * * VIP_REGIONS - * * WELCOME_SCREEN_ENABLED - * * TICKETED_EVENTS_ENABLED - * * MONETIZATION_ENABLED - * * MORE_STICKERS - * * THREE_DAY_THREAD_ARCHIVE - * * SEVEN_DAY_THREAD_ARCHIVE - * * PRIVATE_THREADS - * * ROLE_ICONS - * @typedef {string} Features - * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-guild-features} - */ - - if ('application_id' in data) { - /** - * The id of the application that created this guild (if applicable) - * @type {?Snowflake} - */ - this.applicationId = data.application_id; - } - - if ('afk_timeout' in data) { - /** - * The time in seconds before a user is counted as "away from keyboard" - * @type {?number} - */ - this.afkTimeout = data.afk_timeout; - } - - if ('afk_channel_id' in data) { - /** - * The id of the voice channel where AFK members are moved - * @type {?Snowflake} - */ - this.afkChannelId = data.afk_channel_id; - } - - if ('system_channel_id' in data) { - /** - * The system channel's id - * @type {?Snowflake} - */ - this.systemChannelId = data.system_channel_id; - } - - if ('premium_tier' in data) { - /** - * The premium tier of this guild - * @type {PremiumTier} - */ - this.premiumTier = PremiumTiers[data.premium_tier]; - } - - if ('premium_subscription_count' in data) { - /** - * The total number of boosts for this server - * @type {?number} - */ - this.premiumSubscriptionCount = data.premium_subscription_count; - } - - if ('widget_enabled' in data) { - /** - * Whether widget images are enabled on this guild - * @type {?boolean} - */ - this.widgetEnabled = data.widget_enabled; - } - - if ('widget_channel_id' in data) { - /** - * The widget channel's id, if enabled - * @type {?string} - */ - this.widgetChannelId = data.widget_channel_id; - } - - if ('explicit_content_filter' in data) { - /** - * The explicit content filter level of the guild - * @type {ExplicitContentFilterLevel} - */ - this.explicitContentFilter = - ExplicitContentFilterLevels[data.explicit_content_filter]; - } - - if ('mfa_level' in data) { - /** - * The required MFA level for this guild - * @type {MFALevel} - */ - this.mfaLevel = MFALevels[data.mfa_level]; - } - - if ('joined_at' in data) { - /** - * The timestamp the client user joined the guild at - * @type {number} - */ - this.joinedTimestamp = new Date(data.joined_at).getTime(); - } - - if ('default_message_notifications' in data) { - /** - * The default message notification level of the guild - * @type {DefaultMessageNotificationLevel} - */ - this.defaultMessageNotifications = - DefaultMessageNotificationLevels[data.default_message_notifications]; - } - - if ('system_channel_flags' in data) { - /** - * The value set for the guild's system channel flags - * @type {Readonly} - */ - this.systemChannelFlags = new SystemChannelFlags( - data.system_channel_flags, - ).freeze(); - } - - if ('max_members' in data) { - /** - * The maximum amount of members the guild can have - * @type {?number} - */ - this.maximumMembers = data.max_members; - } else { - this.maximumMembers ??= null; - } - - if ('max_presences' in data) { - /** - * The maximum amount of presences the guild can have - * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter - * @type {?number} - */ - this.maximumPresences = data.max_presences ?? 25_000; - } else { - this.maximumPresences ??= null; - } - - if ('approximate_member_count' in data) { - /** - * The approximate amount of members the guild has - * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter - * @type {?number} - */ - this.approximateMemberCount = data.approximate_member_count; - } else { - this.approximateMemberCount ??= null; - } - - if ('approximate_presence_count' in data) { - /** - * The approximate amount of presences the guild has - * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter - * @type {?number} - */ - this.approximatePresenceCount = data.approximate_presence_count; - } else { - this.approximatePresenceCount ??= null; - } - - /** - * The use count of the vanity URL code of the guild, if any - * You will need to fetch this parameter using {@link Guild#fetchVanityData} if you want to receive it - * @type {?number} - */ - this.vanityURLUses ??= null; - - if ('rules_channel_id' in data) { - /** - * The rules channel's id for the guild - * @type {?Snowflake} - */ - this.rulesChannelId = data.rules_channel_id; - } - - if ('public_updates_channel_id' in data) { - /** - * The community updates channel's id for the guild - * @type {?Snowflake} - */ - this.publicUpdatesChannelId = data.public_updates_channel_id; - } - - if ('preferred_locale' in data) { - /** - * The preferred locale of the guild, defaults to `en-US` - * @type {string} - * @see {@link https://discord.com/developers/docs/dispatch/field-values#predefined-field-values-accepted-locales} - */ - this.preferredLocale = data.preferred_locale; - } - - if (data.channels) { - this.channels.cache.clear(); - for (const rawChannel of data.channels) { - this.client.channels._add(rawChannel, this); - } - } - - if (data.threads) { - for (const rawThread of data.threads) { - this.client.channels._add(rawThread, this); - } - } - - if (data.roles) { - this.roles.cache.clear(); - for (const role of data.roles) this.roles._add(role); - } - - if (data.members) { - this.members.cache.clear(); - for (const guildUser of data.members) this.members._add(guildUser); - } - - if ('owner_id' in data) { - /** - * The user id of this guild's owner - * @type {Snowflake} - */ - this.ownerId = data.owner_id; - } - - if (data.presences) { - for (const presence of data.presences) { - this.presences._add(Object.assign(presence, { guild: this })); - } - } - - if (data.stage_instances) { - this.stageInstances.cache.clear(); - for (const stageInstance of data.stage_instances) { - this.stageInstances._add(stageInstance); - } - } - - if (data.guild_scheduled_events) { - this.scheduledEvents.cache.clear(); - for (const scheduledEvent of data.guild_scheduled_events) { - this.scheduledEvents._add(scheduledEvent); - } - } - - if (data.voice_states) { - this.voiceStates.cache.clear(); - for (const voiceState of data.voice_states) { - this.voiceStates._add(voiceState); - } - } - - if (!this.emojis) { - /** - * A manager of the emojis belonging to this guild - * @type {GuildEmojiManager} - */ - this.emojis = new GuildEmojiManager(this); - if (data.emojis) for (const emoji of data.emojis) this.emojis._add(emoji); - } else if (data.emojis) { - this.client.actions.GuildEmojisUpdate.handle({ - guild_id: this.id, - emojis: data.emojis, - }); - } - - if (!this.stickers) { - /** - * A manager of the stickers belonging to this guild - * @type {GuildStickerManager} - */ - this.stickers = new GuildStickerManager(this); - if (data.stickers) - for (const sticker of data.stickers) this.stickers._add(sticker); - } else if (data.stickers) { - this.client.actions.GuildStickersUpdate.handle({ - guild_id: this.id, - stickers: data.stickers, - }); - } - } - - /** - * The time the client user joined the guild - * @type {Date} - * @readonly - */ - get joinedAt() { - return new Date(this.joinedTimestamp); - } - - /** - * The URL to this guild's discovery splash image. - * @param {StaticImageURLOptions} [options={}] Options for the Image URL - * @returns {?string} - */ - discoverySplashURL({ format, size } = {}) { - return ( - this.discoverySplash && - this.client.rest.cdn.DiscoverySplash( - this.id, - this.discoverySplash, - format, - size, - ) - ); - } - - /** - * Fetches the owner of the guild. - * If the member object isn't needed, use {@link Guild#ownerId} instead. - * @param {BaseFetchOptions} [options] The options for fetching the member - * @returns {Promise} - */ - fetchOwner(options) { - return this.members.fetch({ ...options, user: this.ownerId }); - } - - /** - * AFK voice channel for this guild - * @type {?VoiceChannel} - * @readonly - */ - get afkChannel() { - return this.client.channels.resolve(this.afkChannelId); - } - - /** - * System channel for this guild - * @type {?TextChannel} - * @readonly - */ - get systemChannel() { - return this.client.channels.resolve(this.systemChannelId); - } - - /** - * Widget channel for this guild - * @type {?TextChannel} - * @readonly - */ - get widgetChannel() { - return this.client.channels.resolve(this.widgetChannelId); - } - - /** - * Rules channel for this guild - * @type {?TextChannel} - * @readonly - */ - get rulesChannel() { - return this.client.channels.resolve(this.rulesChannelId); - } - - /** - * Public updates channel for this guild - * @type {?TextChannel} - * @readonly - */ - get publicUpdatesChannel() { - return this.client.channels.resolve(this.publicUpdatesChannelId); - } - - /** - * The client user as a GuildMember of this guild - * @type {?GuildMember} - * @readonly - */ - get me() { - return ( - this.members.resolve(this.client.user.id) ?? - (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) - ? this.members._add({ user: { id: this.client.user.id } }, true) - : null) - ); - } - - /** - * The maximum bitrate available for this guild - * @type {number} - * @readonly - */ - get maximumBitrate() { - if (this.features.includes('VIP_REGIONS')) { - return 384_000; - } - - switch (PremiumTiers[this.premiumTier]) { - case PremiumTiers.TIER_1: - return 128_000; - case PremiumTiers.TIER_2: - return 256_000; - case PremiumTiers.TIER_3: - return 384_000; - default: - return 96_000; - } - } - - /** - * Search slash command / message context - * @param {guildSearchInteraction} options - * { - * - * type: 1 | 2 | 3, [CHAT_INPUT | USER | MESSAGE] - * - * query: string | undefined, - * - * limit: number | 1, - * - * offset: number | 0, - * - * botID: [Snowflake] | undefined, - * - * } - */ - async searchInteraction(options = {}) { - let { query, type, limit, offset, botID } = Object.assign( - { query: undefined, type: 1, limit: 1, offset: 0, botID: [] }, - options, - ); - if (typeof type == 'string') { - if (type == 'CHAT_INPUT') type = 1; - else if (type == 'USER') type = 2; - else if (type == 'MESSAGE') type = 3; - } - if (type < 1 || type > 3) - throw new RangeError('Type must be 1, 2, 3'); - if (typeof type !== 'number') - throw new TypeError('Type must be a number | string'); - this.shard.send({ - op: Opcodes.REQUEST_APPLICATION_COMMANDS, - d: { - guild_id: this.id, - applications: false, - limit, - offset, - type, - query: query, - command_ids: Array.isArray(botID) ? botID : undefined, - }, - }); - } - - /** - * Fetches a collection of integrations to this guild. - * Resolves with a collection mapping integrations by their ids. - * @returns {Promise>} - * @example - * // Fetch integrations - * guild.fetchIntegrations() - * .then(integrations => console.log(`Fetched ${integrations.size} integrations`)) - * .catch(console.error); - */ - async fetchIntegrations() { - const data = await this.client.api.guilds(this.id).integrations.get(); - return data.reduce( - (collection, integration) => - collection.set( - integration.id, - new Integration(this.client, integration, this), - ), - new Collection(), - ); - } - - /** - * Fetches a collection of templates from this guild. - * Resolves with a collection mapping templates by their codes. - * @returns {Promise>} - */ - async fetchTemplates() { - const templates = await this.client.api.guilds(this.id).templates.get(); - return templates.reduce( - (col, data) => col.set(data.code, new GuildTemplate(this.client, data)), - new Collection(), - ); - } - - /** - * Fetches the welcome screen for this guild. - * @returns {Promise} - */ - async fetchWelcomeScreen() { - const data = await this.client.api.guilds(this.id, 'welcome-screen').get(); - return new WelcomeScreen(this, data); - } - - /** - * Creates a template for the guild. - * @param {string} name The name for the template - * @param {string} [description] The description for the template - * @returns {Promise} - */ - async createTemplate(name, description) { - const data = await this.client.api - .guilds(this.id) - .templates.post({ data: { name, description } }); - return new GuildTemplate(this.client, data); - } - - /** - * Obtains a guild preview for this guild from Discord. - * @returns {Promise} - */ - async fetchPreview() { - const data = await this.client.api.guilds(this.id).preview.get(); - return new GuildPreview(this.client, data); - } - - /** - * An object containing information about a guild's vanity invite. - * @typedef {Object} Vanity - * @property {?string} code Vanity invite code - * @property {number} uses How many times this invite has been used - */ - - /** - * Fetches the vanity URL invite object to this guild. - * Resolves with an object containing the vanity URL invite code and the use count - * @returns {Promise} - * @example - * // Fetch invite data - * guild.fetchVanityData() - * .then(res => { - * console.log(`Vanity URL: https://discord.gg/${res.code} with ${res.uses} uses`); - * }) - * .catch(console.error); - */ - async fetchVanityData() { - if (!this.features.includes('VANITY_URL')) { - throw new Error('VANITY_URL'); - } - const data = await this.client.api.guilds(this.id, 'vanity-url').get(); - this.vanityURLCode = data.code; - this.vanityURLUses = data.uses; - - return data; - } - - /** - * Fetches all webhooks for the guild. - * @returns {Promise>} - * @example - * // Fetch webhooks - * guild.fetchWebhooks() - * .then(webhooks => console.log(`Fetched ${webhooks.size} webhooks`)) - * .catch(console.error); - */ - async fetchWebhooks() { - const apiHooks = await this.client.api.guilds(this.id).webhooks.get(); - const hooks = new Collection(); - for (const hook of apiHooks) - hooks.set(hook.id, new Webhook(this.client, hook)); - return hooks; - } - - /** - * Fetches the guild widget data, requires the widget to be enabled. - * @returns {Promise} - * @example - * // Fetches the guild widget data - * guild.fetchWidget() - * .then(widget => console.log(`The widget shows ${widget.channels.size} channels`)) - * .catch(console.error); - */ - fetchWidget() { - return this.client.fetchGuildWidget(this.id); - } - - /** - * Data for the Guild Widget Settings object - * @typedef {Object} GuildWidgetSettings - * @property {boolean} enabled Whether the widget is enabled - * @property {?GuildChannel} channel The widget invite channel - */ - - /** - * The Guild Widget Settings object - * @typedef {Object} GuildWidgetSettingsData - * @property {boolean} enabled Whether the widget is enabled - * @property {?GuildChannelResolvable} channel The widget invite channel - */ - - /** - * Fetches the guild widget settings. - * @returns {Promise} - * @example - * // Fetches the guild widget settings - * guild.fetchWidgetSettings() - * .then(widget => console.log(`The widget is ${widget.enabled ? 'enabled' : 'disabled'}`)) - * .catch(console.error); - */ - async fetchWidgetSettings() { - const data = await this.client.api.guilds(this.id).widget.get(); - this.widgetEnabled = data.enabled; - this.widgetChannelId = data.channel_id; - return { - enabled: data.enabled, - channel: data.channel_id - ? this.channels.cache.get(data.channel_id) - : null, - }; - } - - /** - * Options used to fetch audit logs. - * @typedef {Object} GuildAuditLogsFetchOptions - * @property {Snowflake|GuildAuditLogsEntry} [before] Only return entries before this entry - * @property {number} [limit] The number of entries to return - * @property {UserResolvable} [user] Only return entries for actions made by this user - * @property {AuditLogAction|number} [type] Only return entries for this action type - */ - - /** - * Fetches audit logs for this guild. - * @param {GuildAuditLogsFetchOptions} [options={}] Options for fetching audit logs - * @returns {Promise} - * @example - * // Output audit log entries - * guild.fetchAuditLogs() - * .then(audit => console.log(audit.entries.first())) - * .catch(console.error); - */ - async fetchAuditLogs(options = {}) { - if (options.before && options.before instanceof GuildAuditLogs.Entry) - options.before = options.before.id; - if (typeof options.type === 'string') - options.type = GuildAuditLogs.Actions[options.type]; - - const data = await this.client.api.guilds(this.id)['audit-logs'].get({ - query: { - before: options.before, - limit: options.limit, - user_id: this.client.users.resolveId(options.user), - action_type: options.type, - }, - }); - return GuildAuditLogs.build(this, data); - } - - /** - * The data for editing a guild. - * @typedef {Object} GuildEditData - * @property {string} [name] The name of the guild - * @property {VerificationLevel|number} [verificationLevel] The verification level of the guild - * @property {ExplicitContentFilterLevel|number} [explicitContentFilter] The level of the explicit content filter - * @property {VoiceChannelResolvable} [afkChannel] The AFK channel of the guild - * @property {TextChannelResolvable} [systemChannel] The system channel of the guild - * @property {number} [afkTimeout] The AFK timeout of the guild - * @property {?(BufferResolvable|Base64Resolvable)} [icon] The icon of the guild - * @property {GuildMemberResolvable} [owner] The owner of the guild - * @property {?(BufferResolvable|Base64Resolvable)} [splash] The invite splash image of the guild - * @property {?(BufferResolvable|Base64Resolvable)} [discoverySplash] The discovery splash image of the guild - * @property {?(BufferResolvable|Base64Resolvable)} [banner] The banner of the guild - * @property {DefaultMessageNotificationLevel|number} [defaultMessageNotifications] The default message notification - * level of the guild - * @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild - * @property {TextChannelResolvable} [rulesChannel] The rules channel of the guild - * @property {TextChannelResolvable} [publicUpdatesChannel] The community updates channel of the guild - * @property {string} [preferredLocale] The preferred locale of the guild - * @property {boolean} [premiumProgressBarEnabled] Whether the guild's premium progress bar is enabled - * @property {string} [description] The discovery description of the guild - * @property {Features[]} [features] The features of the guild - */ - - /** - * Data that can be resolved to a Text Channel object. This can be: - * * A TextChannel - * * A Snowflake - * @typedef {TextChannel|Snowflake} TextChannelResolvable - */ - - /** - * Data that can be resolved to a Voice Channel object. This can be: - * * A VoiceChannel - * * A Snowflake - * @typedef {VoiceChannel|Snowflake} VoiceChannelResolvable - */ - - /** - * Updates the guild with new information - e.g. a new name. - * @param {GuildEditData} data The data to update the guild with - * @param {string} [reason] Reason for editing this guild - * @returns {Promise} - * @example - * // Set the guild name - * guild.edit({ - * name: 'Discord Guild', - * }) - * .then(updated => console.log(`New guild name ${updated}`)) - * .catch(console.error); - */ - async edit(data, reason) { - const _data = {}; - if (data.name) _data.name = data.name; - if (typeof data.verificationLevel !== 'undefined') { - _data.verification_level = - typeof data.verificationLevel === 'number' - ? data.verificationLevel - : VerificationLevels[data.verificationLevel]; - } - if (typeof data.afkChannel !== 'undefined') { - _data.afk_channel_id = this.client.channels.resolveId(data.afkChannel); - } - if (typeof data.systemChannel !== 'undefined') { - _data.system_channel_id = this.client.channels.resolveId( - data.systemChannel, - ); - } - if (data.afkTimeout) _data.afk_timeout = Number(data.afkTimeout); - if (typeof data.icon !== 'undefined') - _data.icon = await DataResolver.resolveImage(data.icon); - if (data.owner) _data.owner_id = this.client.users.resolveId(data.owner); - if (typeof data.splash !== 'undefined') - _data.splash = await DataResolver.resolveImage(data.splash); - if (typeof data.discoverySplash !== 'undefined') { - _data.discovery_splash = await DataResolver.resolveImage( - data.discoverySplash, - ); - } - if (typeof data.banner !== 'undefined') - _data.banner = await DataResolver.resolveImage(data.banner); - if (typeof data.explicitContentFilter !== 'undefined') { - _data.explicit_content_filter = - typeof data.explicitContentFilter === 'number' - ? data.explicitContentFilter - : ExplicitContentFilterLevels[data.explicitContentFilter]; - } - if (typeof data.defaultMessageNotifications !== 'undefined') { - _data.default_message_notifications = - typeof data.defaultMessageNotifications === 'number' - ? data.defaultMessageNotifications - : DefaultMessageNotificationLevels[data.defaultMessageNotifications]; - } - if (typeof data.systemChannelFlags !== 'undefined') { - _data.system_channel_flags = SystemChannelFlags.resolve( - data.systemChannelFlags, - ); - } - if (typeof data.rulesChannel !== 'undefined') { - _data.rules_channel_id = this.client.channels.resolveId( - data.rulesChannel, - ); - } - if (typeof data.publicUpdatesChannel !== 'undefined') { - _data.public_updates_channel_id = this.client.channels.resolveId( - data.publicUpdatesChannel, - ); - } - if (typeof data.features !== 'undefined') { - _data.features = data.features; - } - if (typeof data.description !== 'undefined') { - _data.description = data.description; - } - if (data.preferredLocale) _data.preferred_locale = data.preferredLocale; - if ('premiumProgressBarEnabled' in data) - _data.premium_progress_bar_enabled = data.premiumProgressBarEnabled; - const newData = await this.client.api - .guilds(this.id) - .patch({ data: _data, reason }); - return this.client.actions.GuildUpdate.handle(newData).updated; - } - - /** - * Welcome channel data - * @typedef {Object} WelcomeChannelData - * @property {string} description The description to show for this welcome channel - * @property {TextChannel|NewsChannel|StoreChannel|Snowflake} channel The channel to link for this welcome channel - * @property {EmojiIdentifierResolvable} [emoji] The emoji to display for this welcome channel - */ - - /** - * Welcome screen edit data - * @typedef {Object} WelcomeScreenEditData - * @property {boolean} [enabled] Whether the welcome screen is enabled - * @property {string} [description] The description for the welcome screen - * @property {WelcomeChannelData[]} [welcomeChannels] The welcome channel data for the welcome screen - */ - - /** - * Data that can be resolved to a GuildTextChannel object. This can be: - * * A TextChannel - * * A NewsChannel - * * A Snowflake - * @typedef {TextChannel|NewsChannel|Snowflake} GuildTextChannelResolvable - */ - - /** - * Data that can be resolved to a GuildVoiceChannel object. This can be: - * * A VoiceChannel - * * A StageChannel - * * A Snowflake - * @typedef {VoiceChannel|StageChannel|Snowflake} GuildVoiceChannelResolvable - */ - - /** - * Updates the guild's welcome screen - * @param {WelcomeScreenEditData} data Data to edit the welcome screen with - * @returns {Promise} - * @example - * guild.editWelcomeScreen({ - * description: 'Hello World', - * enabled: true, - * welcomeChannels: [ - * { - * description: 'foobar', - * channel: '222197033908436994', - * } - * ], - * }) - */ - async editWelcomeScreen(data) { - const { enabled, description, welcomeChannels } = data; - const welcome_channels = welcomeChannels?.map((welcomeChannelData) => { - const emoji = this.emojis.resolve(welcomeChannelData.emoji); - return { - emoji_id: emoji?.id, - emoji_name: emoji?.name ?? welcomeChannelData.emoji, - channel_id: this.channels.resolveId(welcomeChannelData.channel), - description: welcomeChannelData.description, - }; - }); - - const patchData = await this.client.api - .guilds(this.id, 'welcome-screen') - .patch({ - data: { - welcome_channels, - description, - enabled, - }, - }); - return new WelcomeScreen(this, patchData); - } - - /** - * Edits the level of the explicit content filter. - * @param {ExplicitContentFilterLevel|number} explicitContentFilter The new level of the explicit content filter - * @param {string} [reason] Reason for changing the level of the guild's explicit content filter - * @returns {Promise} - */ - setExplicitContentFilter(explicitContentFilter, reason) { - return this.edit({ explicitContentFilter }, reason); - } - - /* eslint-disable max-len */ - /** - * Edits the setting of the default message notifications of the guild. - * @param {DefaultMessageNotificationLevel|number} defaultMessageNotifications The new default message notification level of the guild - * @param {string} [reason] Reason for changing the setting of the default message notifications - * @returns {Promise} - */ - setDefaultMessageNotifications(defaultMessageNotifications, reason) { - return this.edit({ defaultMessageNotifications }, reason); - } - /* eslint-enable max-len */ - - /** - * Edits the flags of the default message notifications of the guild. - * @param {SystemChannelFlagsResolvable} systemChannelFlags The new flags for the default message notifications - * @param {string} [reason] Reason for changing the flags of the default message notifications - * @returns {Promise} - */ - setSystemChannelFlags(systemChannelFlags, reason) { - return this.edit({ systemChannelFlags }, reason); - } - - /** - * Edits the name of the guild. - * @param {string} name The new name of the guild - * @param {string} [reason] Reason for changing the guild's name - * @returns {Promise} - * @example - * // Edit the guild name - * guild.setName('Discord Guild') - * .then(updated => console.log(`Updated guild name to ${updated.name}`)) - * .catch(console.error); - */ - setName(name, reason) { - return this.edit({ name }, reason); - } - - /** - * Edits the verification level of the guild. - * @param {VerificationLevel|number} verificationLevel The new verification level of the guild - * @param {string} [reason] Reason for changing the guild's verification level - * @returns {Promise} - * @example - * // Edit the guild verification level - * guild.setVerificationLevel(1) - * .then(updated => console.log(`Updated guild verification level to ${guild.verificationLevel}`)) - * .catch(console.error); - */ - setVerificationLevel(verificationLevel, reason) { - return this.edit({ verificationLevel }, reason); - } - - /** - * Edits the AFK channel of the guild. - * @param {VoiceChannelResolvable} afkChannel The new AFK channel - * @param {string} [reason] Reason for changing the guild's AFK channel - * @returns {Promise} - * @example - * // Edit the guild AFK channel - * guild.setAFKChannel(channel) - * .then(updated => console.log(`Updated guild AFK channel to ${guild.afkChannel.name}`)) - * .catch(console.error); - */ - setAFKChannel(afkChannel, reason) { - return this.edit({ afkChannel }, reason); - } - - /** - * Edits the system channel of the guild. - * @param {TextChannelResolvable} systemChannel The new system channel - * @param {string} [reason] Reason for changing the guild's system channel - * @returns {Promise} - * @example - * // Edit the guild system channel - * guild.setSystemChannel(channel) - * .then(updated => console.log(`Updated guild system channel to ${guild.systemChannel.name}`)) - * .catch(console.error); - */ - setSystemChannel(systemChannel, reason) { - return this.edit({ systemChannel }, reason); - } - - /** - * Edits the AFK timeout of the guild. - * @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK - * @param {string} [reason] Reason for changing the guild's AFK timeout - * @returns {Promise} - * @example - * // Edit the guild AFK channel - * guild.setAFKTimeout(60) - * .then(updated => console.log(`Updated guild AFK timeout to ${guild.afkTimeout}`)) - * .catch(console.error); - */ - setAFKTimeout(afkTimeout, reason) { - return this.edit({ afkTimeout }, reason); - } - - /** - * Sets a new guild icon. - * @param {?(Base64Resolvable|BufferResolvable)} icon The new icon of the guild - * @param {string} [reason] Reason for changing the guild's icon - * @returns {Promise} - * @example - * // Edit the guild icon - * guild.setIcon('./icon.png') - * .then(updated => console.log('Updated the guild icon')) - * .catch(console.error); - */ - setIcon(icon, reason) { - return this.edit({ icon }, reason); - } - - /** - * Sets a new owner of the guild. - * @param {GuildMemberResolvable} owner The new owner of the guild - * @param {string} [reason] Reason for setting the new owner - * @returns {Promise} - * @example - * // Edit the guild owner - * guild.setOwner(guild.members.cache.first()) - * .then(guild => guild.fetchOwner()) - * .then(owner => console.log(`Updated the guild owner to ${owner.displayName}`)) - * .catch(console.error); - */ - setOwner(owner, reason) { - return this.edit({ owner }, reason); - } - - /** - * Sets a new guild invite splash image. - * @param {?(Base64Resolvable|BufferResolvable)} splash The new invite splash image of the guild - * @param {string} [reason] Reason for changing the guild's invite splash image - * @returns {Promise} - * @example - * // Edit the guild splash - * guild.setSplash('./splash.png') - * .then(updated => console.log('Updated the guild splash')) - * .catch(console.error); - */ - setSplash(splash, reason) { - return this.edit({ splash }, reason); - } - - /** - * Sets a new guild discovery splash image. - * @param {?(Base64Resolvable|BufferResolvable)} discoverySplash The new discovery splash image of the guild - * @param {string} [reason] Reason for changing the guild's discovery splash image - * @returns {Promise} - * @example - * // Edit the guild discovery splash - * guild.setDiscoverySplash('./discoverysplash.png') - * .then(updated => console.log('Updated the guild discovery splash')) - * .catch(console.error); - */ - setDiscoverySplash(discoverySplash, reason) { - return this.edit({ discoverySplash }, reason); - } - - /** - * Sets a new guild banner. - * @param {?(Base64Resolvable|BufferResolvable)} banner The new banner of the guild - * @param {string} [reason] Reason for changing the guild's banner - * @returns {Promise} - * @example - * guild.setBanner('./banner.png') - * .then(updated => console.log('Updated the guild banner')) - * .catch(console.error); - */ - setBanner(banner, reason) { - return this.edit({ banner }, reason); - } - - /** - * Edits the rules channel of the guild. - * @param {TextChannelResolvable} rulesChannel The new rules channel - * @param {string} [reason] Reason for changing the guild's rules channel - * @returns {Promise} - * @example - * // Edit the guild rules channel - * guild.setRulesChannel(channel) - * .then(updated => console.log(`Updated guild rules channel to ${guild.rulesChannel.name}`)) - * .catch(console.error); - */ - setRulesChannel(rulesChannel, reason) { - return this.edit({ rulesChannel }, reason); - } - /** - * Change Guild Position (from * to Folder or Home) - * @param {number} position Guild Position - * * **WARNING**: Type = `FOLDER`, newPosition is the guild's index in the Folder. - * @param {String|Number} type Move to folder or home - * * `FOLDER`: 1 - * * `HOME`: 2 - * @param {String|Number|void|null} folderID If you want to move to folder - * @returns {Promise} - * @example - * // Move guild to folderID 123456, index 1 - * guild.setPosition(1, 'FOLDER', 123456) - * .then(guild => console.log(`Guild moved to folderID ${guild.folder.folderId}`)); - */ - async setPosition(position, type, folderID) { - if (type == 1 || `${type}`.toUpperCase() === 'FOLDER') { - folderID = folderID || this.folder.folderId; - if (!['number', 'string'].includes(typeof folderID)) - throw new TypeError('INVALID_TYPE', 'folderID', 'String | Number'); - // Get Data from Folder ID - const folder = await this.client.setting.rawSetting.guild_folders.find( - (obj) => obj.id == folderID, - ); - if (!folder) throw new Error('FOLDER_NOT_FOUND'); - if (folder.guild_ids.length - 1 < position || position < 0) - throw new Error('FOLDER_POSITION_INVALID'); - if (position !== folder.guild_ids.indexOf(this.id)) { - await this.client.setting.guildChangePosition( - this.id, - position, - 1, - folderID, - ); - } - } else if (type == 2 || `${type}`.toUpperCase() === 'HOME') { - if (this.client.setting.guild_positions - 1 < position || position < 0) - throw new Error('FOLDER_POSITION_INVALID'); - if (position !== this.position) { - await this.client.setting.guildChangePosition( - this.id, - position, - 2, - null, - ); - } - } else { - throw new TypeError('INVALID_TYPE', 'type', '`Folder`| `Home`'); - } - return this; - } - - /** - * Edits the community updates channel of the guild. - * @param {TextChannelResolvable} publicUpdatesChannel The new community updates channel - * @param {string} [reason] Reason for changing the guild's community updates channel - * @returns {Promise} - * @example - * // Edit the guild community updates channel - * guild.setPublicUpdatesChannel(channel) - * .then(updated => console.log(`Updated guild community updates channel to ${guild.publicUpdatesChannel.name}`)) - * .catch(console.error); - */ - setPublicUpdatesChannel(publicUpdatesChannel, reason) { - return this.edit({ publicUpdatesChannel }, reason); - } - - /** - * Edits the preferred locale of the guild. - * @param {string} preferredLocale The new preferred locale of the guild - * @param {string} [reason] Reason for changing the guild's preferred locale - * @returns {Promise} - * @example - * // Edit the guild preferred locale - * guild.setPreferredLocale('en-US') - * .then(updated => console.log(`Updated guild preferred locale to ${guild.preferredLocale}`)) - * .catch(console.error); - */ - setPreferredLocale(preferredLocale, reason) { - return this.edit({ preferredLocale }, reason); - } - - /** - * Edits the enabled state of the guild's premium progress bar - * @param {boolean} [enabled=true] The new enabled state of the guild's premium progress bar - * @param {string} [reason] Reason for changing the state of the guild's premium progress bar - * @returns {Promise} - */ - setPremiumProgressBarEnabled(enabled = true, reason) { - return this.edit({ premiumProgressBarEnabled: enabled }, reason); - } - - /** - * Data that can be resolved to give a Category Channel object. This can be: - * * A CategoryChannel object - * * A Snowflake - * @typedef {CategoryChannel|Snowflake} CategoryChannelResolvable - */ - - /** - * The data needed for updating a channel's position. - * @typedef {Object} ChannelPosition - * @property {GuildChannel|Snowflake} channel Channel to update - * @property {number} [position] New position for the channel - * @property {CategoryChannelResolvable} [parent] Parent channel for this channel - * @property {boolean} [lockPermissions] If the overwrites should be locked to the parents overwrites - */ - - /** - * Batch-updates the guild's channels' positions. - * Only one channel's parent can be changed at a time - * @param {ChannelPosition[]} channelPositions Channel positions to update - * @returns {Promise} - * @deprecated Use {@link GuildChannelManager#setPositions} instead - * @example - * guild.setChannelPositions([{ channel: channelId, position: newChannelIndex }]) - * .then(guild => console.log(`Updated channel positions for ${guild}`)) - * .catch(console.error); - */ - setChannelPositions(channelPositions) { - if (!deprecationEmittedForSetChannelPositions) { - process.emitWarning( - 'The Guild#setChannelPositions method is deprecated. Use GuildChannelManager#setPositions instead.', - 'DeprecationWarning', - ); - - deprecationEmittedForSetChannelPositions = true; - } - - return this.channels.setPositions(channelPositions); - } - - /** - * The data needed for updating a guild role's position - * @typedef {Object} GuildRolePosition - * @property {RoleResolvable} role The role's id - * @property {number} position The position to update - */ - - /** - * Batch-updates the guild's role positions - * @param {GuildRolePosition[]} rolePositions Role positions to update - * @returns {Promise} - * @deprecated Use {@link RoleManager#setPositions} instead - * @example - * guild.setRolePositions([{ role: roleId, position: updatedRoleIndex }]) - * .then(guild => console.log(`Role positions updated for ${guild}`)) - * .catch(console.error); - */ - setRolePositions(rolePositions) { - if (!deprecationEmittedForSetRolePositions) { - process.emitWarning( - 'The Guild#setRolePositions method is deprecated. Use RoleManager#setPositions instead.', - 'DeprecationWarning', - ); - - deprecationEmittedForSetRolePositions = true; - } - - return this.roles.setPositions(rolePositions); - } - - /** - * Edits the guild's widget settings. - * @param {GuildWidgetSettingsData} settings The widget settings for the guild - * @param {string} [reason] Reason for changing the guild's widget settings - * @returns {Promise} - */ - async setWidgetSettings(settings, reason) { - await this.client.api.guilds(this.id).widget.patch({ - data: { - enabled: settings.enabled, - channel_id: this.channels.resolveId(settings.channel), - }, - reason, - }); - return this; - } - - /** - * Leaves the guild. - * @returns {Promise} - * @example - * // Leave a guild - * guild.leave() - * .then(g => console.log(`Left the guild ${g}`)) - * .catch(console.error); - */ - async leave() { - if (this.ownerId === this.client.user.id) throw new Error('GUILD_OWNED'); - await this.client.api.users('@me').guilds(this.id).delete(); - return this.client.actions.GuildDelete.handle({ id: this.id }).guild; - } - - /** - * Deletes the guild. - * @returns {Promise} - * @example - * // Delete a guild - * guild.delete() - * .then(g => console.log(`Deleted the guild ${g}`)) - * .catch(console.error); - */ - async delete() { - await this.client.api.guilds(this.id).delete(); - return this.client.actions.GuildDelete.handle({ id: this.id }).guild; - } - - /** - * Whether this guild equals another guild. It compares all properties, so for most operations - * it is advisable to just compare `guild.id === guild2.id` as it is much faster and is often - * what most users need. - * @param {Guild} guild The guild to compare with - * @returns {boolean} - */ - equals(guild) { - return ( - guild && - guild instanceof this.constructor && - this.id === guild.id && - this.available === guild.available && - this.splash === guild.splash && - this.discoverySplash === guild.discoverySplash && - this.name === guild.name && - this.memberCount === guild.memberCount && - this.large === guild.large && - this.icon === guild.icon && - this.ownerId === guild.ownerId && - this.verificationLevel === guild.verificationLevel && - (this.features === guild.features || - (this.features.length === guild.features.length && - this.features.every((feat, i) => feat === guild.features[i]))) - ); - } - - /** - * Set Community Feature - * @param {Boolean} stats True / False to enable / disable Community Feature - * @param {TextChannelResolvable} publicUpdatesChannel - * @param {TextChannelResolvable} rulesChannel - * @param {String} reason - */ - async setCommunity( - stats = true, - publicUpdatesChannel = '1', - rulesChannel = '1', - reason, - ) { - if (stats) { - // Check everyone role - const everyoneRole = this.roles.everyone; - if (everyoneRole.mentionable) { - await everyoneRole.setMentionable(false, reason); - } - // Setting - this.edit( - { - defaultMessageNotifications: 'ONLY_MENTIONS', - explicitContentFilter: 'ALL_MEMBERS', - features: [...this.features, 'COMMUNITY'], - publicUpdatesChannel, - rulesChannel, - verificationLevel: - VerificationLevels[this.verificationLevel] < 1 - ? 'LOW' - : this.verificationLevel, // Email - }, - reason, - ); - } else { - this.edit( - { - publicUpdatesChannel: null, - rulesChannel: null, - features: this.features.filter((f) => f !== 'COMMUNITY'), - preferredLocale: this.preferredLocale, - description: this.description, - }, - reason, - ); - } - } - - toJSON() { - const json = super.toJSON({ - available: false, - createdTimestamp: true, - nameAcronym: true, - presences: false, - voiceStates: false, - }); - json.iconURL = this.iconURL(); - json.splashURL = this.splashURL(); - json.discoverySplashURL = this.discoverySplashURL(); - json.bannerURL = this.bannerURL(); - return json; - } - - /** - * The voice state adapter for this guild that can be used with @discordjs/voice to play audio in voice - * and stage channels. - * @type {Function} - * @readonly - */ - get voiceAdapterCreator() { - return (methods) => { - this.client.voice.adapters.set(this.id, methods); - return { - sendPayload: (data) => { - if (this.shard.status !== Status.READY) return false; - this.shard.send(data); - return true; - }, - destroy: () => { - this.client.voice.adapters.delete(this.id); - }, - }; - }; - } - - /** - * Creates a collection of this guild's roles, sorted by their position and ids. - * @returns {Collection} - * @private - */ - _sortedRoles() { - return Util.discordSort(this.roles.cache); - } - - /** - * Creates a collection of this guild's or a specific category's channels, sorted by their position and ids. - * @param {GuildChannel} [channel] Category to get the channels of - * @returns {Collection} - * @private - */ - _sortedChannels(channel) { - const category = channel.type === ChannelTypes.GUILD_CATEGORY; - return Util.discordSort( - this.channels.cache.filter( - (c) => - (['GUILD_TEXT', 'GUILD_NEWS', 'GUILD_STORE'].includes(channel.type) - ? ['GUILD_TEXT', 'GUILD_NEWS', 'GUILD_STORE'].includes(c.type) - : c.type === channel.type) && - (category || c.parent === channel.parent), - ), - ); - } + constructor(client, data) { + super(client, data, false); + + /** + * A manager of the members belonging to this guild + * @type {GuildMemberManager} + */ + this.members = new GuildMemberManager(this); + + /** + * A manager of the channels belonging to this guild + * @type {GuildChannelManager} + */ + this.channels = new GuildChannelManager(this); + + /** + * A manager of the bans belonging to this guild + * @type {GuildBanManager} + */ + this.bans = new GuildBanManager(this); + + /** + * A manager of the roles belonging to this guild + * @type {RoleManager} + */ + this.roles = new RoleManager(this); + + /** + * A manager of the presences belonging to this guild + * @type {PresenceManager} + */ + this.presences = new PresenceManager(this.client); + + /** + * A manager of the voice states of this guild + * @type {VoiceStateManager} + */ + this.voiceStates = new VoiceStateManager(this); + + /** + * A manager of the stage instances of this guild + * @type {StageInstanceManager} + */ + this.stageInstances = new StageInstanceManager(this); + + /** + * A manager of the invites of this guild + * @type {GuildInviteManager} + */ + this.invites = new GuildInviteManager(this); + + /** + * A manager of the scheduled events of this guild + * @type {GuildScheduledEventManager} + */ + this.scheduledEvents = new GuildScheduledEventManager(this); + + if (!data) return; + if (data.unavailable) { + /** + * Whether the guild is available to access. If it is not available, it indicates a server outage + * @type {boolean} + */ + this.available = false; + } else { + this._patch(data); + if (!data.channels) this.available = false; + } + + /** + * The id of the shard this Guild belongs to. + * @type {number} + */ + this.shardId = data.shardId; + + this.disableDM = false; + } + + /** + * Whether or not the structure has been deleted + * @type {boolean} + * @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091 + */ + get deleted() { + if (!deprecationEmittedForDeleted) { + deprecationEmittedForDeleted = true; + process.emitWarning( + 'Guild#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + return deletedGuilds.has(this); + } + + set deleted(value) { + if (!deprecationEmittedForDeleted) { + deprecationEmittedForDeleted = true; + process.emitWarning( + 'Guild#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + if (value) deletedGuilds.add(this); + else deletedGuilds.delete(this); + } + + /** + * The Shard this Guild belongs to. + * @type {WebSocketShard} + * @readonly + */ + get shard() { + return this.client.ws.shards.get(this.shardId); + } + + _patch(data) { + super._patch(data); + this.id = data.id; + if ('name' in data) this.name = data.name; + if ('icon' in data) this.icon = data.icon; + if ('unavailable' in data) { + this.available = !data.unavailable; + } else { + this.available ??= true; + } + + if ('discovery_splash' in data) { + /** + * The hash of the guild discovery splash image + * @type {?string} + */ + this.discoverySplash = data.discovery_splash; + } + + if ('member_count' in data) { + /** + * The full amount of members in this guild + * @type {number} + */ + this.memberCount = data.member_count; + } + + if ('large' in data) { + /** + * Whether the guild is "large" (has more than {@link WebsocketOptions large_threshold} members, 50 by default) + * @type {boolean} + */ + this.large = Boolean(data.large); + } + + if ('premium_progress_bar_enabled' in data) { + /** + * Whether this guild has its premium (boost) progress bar enabled + * @type {boolean} + */ + this.premiumProgressBarEnabled = data.premium_progress_bar_enabled; + } + + /** + * An array of enabled guild features, here are the possible values: + * * ANIMATED_ICON + * * BANNER + * * COMMERCE + * * COMMUNITY + * * DISCOVERABLE + * * FEATURABLE + * * INVITE_SPLASH + * * MEMBER_VERIFICATION_GATE_ENABLED + * * NEWS + * * PARTNERED + * * PREVIEW_ENABLED + * * VANITY_URL + * * VERIFIED + * * VIP_REGIONS + * * WELCOME_SCREEN_ENABLED + * * TICKETED_EVENTS_ENABLED + * * MONETIZATION_ENABLED + * * MORE_STICKERS + * * THREE_DAY_THREAD_ARCHIVE + * * SEVEN_DAY_THREAD_ARCHIVE + * * PRIVATE_THREADS + * * ROLE_ICONS + * @typedef {string} Features + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-guild-features} + */ + + if ('application_id' in data) { + /** + * The id of the application that created this guild (if applicable) + * @type {?Snowflake} + */ + this.applicationId = data.application_id; + } + + if ('afk_timeout' in data) { + /** + * The time in seconds before a user is counted as "away from keyboard" + * @type {?number} + */ + this.afkTimeout = data.afk_timeout; + } + + if ('afk_channel_id' in data) { + /** + * The id of the voice channel where AFK members are moved + * @type {?Snowflake} + */ + this.afkChannelId = data.afk_channel_id; + } + + if ('system_channel_id' in data) { + /** + * The system channel's id + * @type {?Snowflake} + */ + this.systemChannelId = data.system_channel_id; + } + + if ('premium_tier' in data) { + /** + * The premium tier of this guild + * @type {PremiumTier} + */ + this.premiumTier = PremiumTiers[data.premium_tier]; + } + + if ('premium_subscription_count' in data) { + /** + * The total number of boosts for this server + * @type {?number} + */ + this.premiumSubscriptionCount = data.premium_subscription_count; + } + + if ('widget_enabled' in data) { + /** + * Whether widget images are enabled on this guild + * @type {?boolean} + */ + this.widgetEnabled = data.widget_enabled; + } + + if ('widget_channel_id' in data) { + /** + * The widget channel's id, if enabled + * @type {?string} + */ + this.widgetChannelId = data.widget_channel_id; + } + + if ('explicit_content_filter' in data) { + /** + * The explicit content filter level of the guild + * @type {ExplicitContentFilterLevel} + */ + this.explicitContentFilter = ExplicitContentFilterLevels[data.explicit_content_filter]; + } + + if ('mfa_level' in data) { + /** + * The required MFA level for this guild + * @type {MFALevel} + */ + this.mfaLevel = MFALevels[data.mfa_level]; + } + + if ('joined_at' in data) { + /** + * The timestamp the client user joined the guild at + * @type {number} + */ + this.joinedTimestamp = new Date(data.joined_at).getTime(); + } + + if ('default_message_notifications' in data) { + /** + * The default message notification level of the guild + * @type {DefaultMessageNotificationLevel} + */ + this.defaultMessageNotifications = DefaultMessageNotificationLevels[data.default_message_notifications]; + } + + if ('system_channel_flags' in data) { + /** + * The value set for the guild's system channel flags + * @type {Readonly} + */ + this.systemChannelFlags = new SystemChannelFlags(data.system_channel_flags).freeze(); + } + + if ('max_members' in data) { + /** + * The maximum amount of members the guild can have + * @type {?number} + */ + this.maximumMembers = data.max_members; + } else { + this.maximumMembers ??= null; + } + + if ('max_presences' in data) { + /** + * The maximum amount of presences the guild can have + * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter + * @type {?number} + */ + this.maximumPresences = data.max_presences ?? 25_000; + } else { + this.maximumPresences ??= null; + } + + if ('approximate_member_count' in data) { + /** + * The approximate amount of members the guild has + * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter + * @type {?number} + */ + this.approximateMemberCount = data.approximate_member_count; + } else { + this.approximateMemberCount ??= null; + } + + if ('approximate_presence_count' in data) { + /** + * The approximate amount of presences the guild has + * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter + * @type {?number} + */ + this.approximatePresenceCount = data.approximate_presence_count; + } else { + this.approximatePresenceCount ??= null; + } + + /** + * The use count of the vanity URL code of the guild, if any + * You will need to fetch this parameter using {@link Guild#fetchVanityData} if you want to receive it + * @type {?number} + */ + this.vanityURLUses ??= null; + + if ('rules_channel_id' in data) { + /** + * The rules channel's id for the guild + * @type {?Snowflake} + */ + this.rulesChannelId = data.rules_channel_id; + } + + if ('public_updates_channel_id' in data) { + /** + * The community updates channel's id for the guild + * @type {?Snowflake} + */ + this.publicUpdatesChannelId = data.public_updates_channel_id; + } + + if ('preferred_locale' in data) { + /** + * The preferred locale of the guild, defaults to `en-US` + * @type {string} + * @see {@link https://discord.com/developers/docs/dispatch/field-values#predefined-field-values-accepted-locales} + */ + this.preferredLocale = data.preferred_locale; + } + + if (data.channels) { + this.channels.cache.clear(); + for (const rawChannel of data.channels) { + this.client.channels._add(rawChannel, this); + } + } + + if (data.threads) { + for (const rawThread of data.threads) { + this.client.channels._add(rawThread, this); + } + } + + if (data.roles) { + this.roles.cache.clear(); + for (const role of data.roles) this.roles._add(role); + } + + if (data.members) { + this.members.cache.clear(); + for (const guildUser of data.members) this.members._add(guildUser); + } + + if ('owner_id' in data) { + /** + * The user id of this guild's owner + * @type {Snowflake} + */ + this.ownerId = data.owner_id; + } + + if (data.presences) { + for (const presence of data.presences) { + this.presences._add(Object.assign(presence, { guild: this })); + } + } + + if (data.stage_instances) { + this.stageInstances.cache.clear(); + for (const stageInstance of data.stage_instances) { + this.stageInstances._add(stageInstance); + } + } + + if (data.guild_scheduled_events) { + this.scheduledEvents.cache.clear(); + for (const scheduledEvent of data.guild_scheduled_events) { + this.scheduledEvents._add(scheduledEvent); + } + } + + if (data.voice_states) { + this.voiceStates.cache.clear(); + for (const voiceState of data.voice_states) { + this.voiceStates._add(voiceState); + } + } + + if (!this.emojis) { + /** + * A manager of the emojis belonging to this guild + * @type {GuildEmojiManager} + */ + this.emojis = new GuildEmojiManager(this); + if (data.emojis) for (const emoji of data.emojis) this.emojis._add(emoji); + } else if (data.emojis) { + this.client.actions.GuildEmojisUpdate.handle({ + guild_id: this.id, + emojis: data.emojis, + }); + } + + if (!this.stickers) { + /** + * A manager of the stickers belonging to this guild + * @type {GuildStickerManager} + */ + this.stickers = new GuildStickerManager(this); + if (data.stickers) { + for (const sticker of data.stickers) this.stickers._add(sticker); + } + } else if (data.stickers) { + this.client.actions.GuildStickersUpdate.handle({ + guild_id: this.id, + stickers: data.stickers, + }); + } + } + + /** + * The time the client user joined the guild + * @type {Date} + * @readonly + */ + get joinedAt() { + return new Date(this.joinedTimestamp); + } + + /** + * The URL to this guild's discovery splash image. + * @param {StaticImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + discoverySplashURL({ format, size } = {}) { + return this.discoverySplash && this.client.rest.cdn.DiscoverySplash(this.id, this.discoverySplash, format, size); + } + + /** + * Fetches the owner of the guild. + * If the member object isn't needed, use {@link Guild#ownerId} instead. + * @param {BaseFetchOptions} [options] The options for fetching the member + * @returns {Promise} + */ + fetchOwner(options) { + return this.members.fetch({ ...options, user: this.ownerId }); + } + + /** + * AFK voice channel for this guild + * @type {?VoiceChannel} + * @readonly + */ + get afkChannel() { + return this.client.channels.resolve(this.afkChannelId); + } + + /** + * System channel for this guild + * @type {?TextChannel} + * @readonly + */ + get systemChannel() { + return this.client.channels.resolve(this.systemChannelId); + } + + /** + * Widget channel for this guild + * @type {?TextChannel} + * @readonly + */ + get widgetChannel() { + return this.client.channels.resolve(this.widgetChannelId); + } + + /** + * Rules channel for this guild + * @type {?TextChannel} + * @readonly + */ + get rulesChannel() { + return this.client.channels.resolve(this.rulesChannelId); + } + + /** + * Public updates channel for this guild + * @type {?TextChannel} + * @readonly + */ + get publicUpdatesChannel() { + return this.client.channels.resolve(this.publicUpdatesChannelId); + } + + /** + * The client user as a GuildMember of this guild + * @type {?GuildMember} + * @readonly + */ + get me() { + return ( + this.members.resolve(this.client.user.id) ?? + (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) + ? this.members._add({ user: { id: this.client.user.id } }, true) + : null) + ); + } + + /** + * The maximum bitrate available for this guild + * @type {number} + * @readonly + */ + get maximumBitrate() { + if (this.features.includes('VIP_REGIONS')) { + return 384_000; + } + + switch (PremiumTiers[this.premiumTier]) { + case PremiumTiers.TIER_1: + return 128_000; + case PremiumTiers.TIER_2: + return 256_000; + case PremiumTiers.TIER_3: + return 384_000; + default: + return 96_000; + } + } + + /** + * Search slash command / message context + * @param {guildSearchInteraction} options + * { + * + * type: 1 | 2 | 3, [CHAT_INPUT | USER | MESSAGE] + * + * query: string | undefined, + * + * limit: number | 1, + * + * offset: number | 0, + * + * botID: [Snowflake] | undefined, + * + * } + */ + async searchInteraction(options = {}) { + let { query, type, limit, offset, botID } = Object.assign( + { query: undefined, type: 1, limit: 1, offset: 0, botID: [] }, + options, + ); + if (typeof type == 'string') { + if (type == 'CHAT_INPUT') type = 1; + else if (type == 'USER') type = 2; + else if (type == 'MESSAGE') type = 3; + } + if (type < 1 || type > 3) { + throw new RangeError('Type must be 1, 2, 3'); + } + if (typeof type !== 'number') { + throw new TypeError('Type must be a number | string'); + } + this.shard.send({ + op: Opcodes.REQUEST_APPLICATION_COMMANDS, + d: { + guild_id: this.id, + applications: false, + limit, + offset, + type, + query: query, + command_ids: Array.isArray(botID) ? botID : undefined, + }, + }); + } + + /** + * Fetches a collection of integrations to this guild. + * Resolves with a collection mapping integrations by their ids. + * @returns {Promise>} + * @example + * // Fetch integrations + * guild.fetchIntegrations() + * .then(integrations => console.log(`Fetched ${integrations.size} integrations`)) + * .catch(console.error); + */ + async fetchIntegrations() { + const data = await this.client.api.guilds(this.id).integrations.get(); + return data.reduce( + (collection, integration) => collection.set(integration.id, new Integration(this.client, integration, this)), + new Collection(), + ); + } + + /** + * Fetches a collection of templates from this guild. + * Resolves with a collection mapping templates by their codes. + * @returns {Promise>} + */ + async fetchTemplates() { + const templates = await this.client.api.guilds(this.id).templates.get(); + return templates.reduce((col, data) => col.set(data.code, new GuildTemplate(this.client, data)), new Collection()); + } + + /** + * Fetches the welcome screen for this guild. + * @returns {Promise} + */ + async fetchWelcomeScreen() { + const data = await this.client.api.guilds(this.id, 'welcome-screen').get(); + return new WelcomeScreen(this, data); + } + + /** + * Creates a template for the guild. + * @param {string} name The name for the template + * @param {string} [description] The description for the template + * @returns {Promise} + */ + async createTemplate(name, description) { + const data = await this.client.api.guilds(this.id).templates.post({ data: { name, description } }); + return new GuildTemplate(this.client, data); + } + + /** + * Obtains a guild preview for this guild from Discord. + * @returns {Promise} + */ + async fetchPreview() { + const data = await this.client.api.guilds(this.id).preview.get(); + return new GuildPreview(this.client, data); + } + + /** + * An object containing information about a guild's vanity invite. + * @typedef {Object} Vanity + * @property {?string} code Vanity invite code + * @property {number} uses How many times this invite has been used + */ + + /** + * Fetches the vanity URL invite object to this guild. + * Resolves with an object containing the vanity URL invite code and the use count + * @returns {Promise} + * @example + * // Fetch invite data + * guild.fetchVanityData() + * .then(res => { + * console.log(`Vanity URL: https://discord.gg/${res.code} with ${res.uses} uses`); + * }) + * .catch(console.error); + */ + async fetchVanityData() { + if (!this.features.includes('VANITY_URL')) { + throw new Error('VANITY_URL'); + } + const data = await this.client.api.guilds(this.id, 'vanity-url').get(); + this.vanityURLCode = data.code; + this.vanityURLUses = data.uses; + + return data; + } + + /** + * Fetches all webhooks for the guild. + * @returns {Promise>} + * @example + * // Fetch webhooks + * guild.fetchWebhooks() + * .then(webhooks => console.log(`Fetched ${webhooks.size} webhooks`)) + * .catch(console.error); + */ + async fetchWebhooks() { + const apiHooks = await this.client.api.guilds(this.id).webhooks.get(); + const hooks = new Collection(); + for (const hook of apiHooks) { + hooks.set(hook.id, new Webhook(this.client, hook)); + } + return hooks; + } + + /** + * Fetches the guild widget data, requires the widget to be enabled. + * @returns {Promise} + * @example + * // Fetches the guild widget data + * guild.fetchWidget() + * .then(widget => console.log(`The widget shows ${widget.channels.size} channels`)) + * .catch(console.error); + */ + fetchWidget() { + return this.client.fetchGuildWidget(this.id); + } + + /** + * Data for the Guild Widget Settings object + * @typedef {Object} GuildWidgetSettings + * @property {boolean} enabled Whether the widget is enabled + * @property {?GuildChannel} channel The widget invite channel + */ + + /** + * The Guild Widget Settings object + * @typedef {Object} GuildWidgetSettingsData + * @property {boolean} enabled Whether the widget is enabled + * @property {?GuildChannelResolvable} channel The widget invite channel + */ + + /** + * Fetches the guild widget settings. + * @returns {Promise} + * @example + * // Fetches the guild widget settings + * guild.fetchWidgetSettings() + * .then(widget => console.log(`The widget is ${widget.enabled ? 'enabled' : 'disabled'}`)) + * .catch(console.error); + */ + async fetchWidgetSettings() { + const data = await this.client.api.guilds(this.id).widget.get(); + this.widgetEnabled = data.enabled; + this.widgetChannelId = data.channel_id; + return { + enabled: data.enabled, + channel: data.channel_id ? this.channels.cache.get(data.channel_id) : null, + }; + } + + /** + * Options used to fetch audit logs. + * @typedef {Object} GuildAuditLogsFetchOptions + * @property {Snowflake|GuildAuditLogsEntry} [before] Only return entries before this entry + * @property {number} [limit] The number of entries to return + * @property {UserResolvable} [user] Only return entries for actions made by this user + * @property {AuditLogAction|number} [type] Only return entries for this action type + */ + + /** + * Fetches audit logs for this guild. + * @param {GuildAuditLogsFetchOptions} [options={}] Options for fetching audit logs + * @returns {Promise} + * @example + * // Output audit log entries + * guild.fetchAuditLogs() + * .then(audit => console.log(audit.entries.first())) + * .catch(console.error); + */ + async fetchAuditLogs(options = {}) { + if (options.before && options.before instanceof GuildAuditLogs.Entry) { + options.before = options.before.id; + } + if (typeof options.type === 'string') { + options.type = GuildAuditLogs.Actions[options.type]; + } + + const data = await this.client.api.guilds(this.id)['audit-logs'].get({ + query: { + before: options.before, + limit: options.limit, + user_id: this.client.users.resolveId(options.user), + action_type: options.type, + }, + }); + return GuildAuditLogs.build(this, data); + } + + /** + * The data for editing a guild. + * @typedef {Object} GuildEditData + * @property {string} [name] The name of the guild + * @property {VerificationLevel|number} [verificationLevel] The verification level of the guild + * @property {ExplicitContentFilterLevel|number} [explicitContentFilter] The level of the explicit content filter + * @property {VoiceChannelResolvable} [afkChannel] The AFK channel of the guild + * @property {TextChannelResolvable} [systemChannel] The system channel of the guild + * @property {number} [afkTimeout] The AFK timeout of the guild + * @property {?(BufferResolvable|Base64Resolvable)} [icon] The icon of the guild + * @property {GuildMemberResolvable} [owner] The owner of the guild + * @property {?(BufferResolvable|Base64Resolvable)} [splash] The invite splash image of the guild + * @property {?(BufferResolvable|Base64Resolvable)} [discoverySplash] The discovery splash image of the guild + * @property {?(BufferResolvable|Base64Resolvable)} [banner] The banner of the guild + * @property {DefaultMessageNotificationLevel|number} [defaultMessageNotifications] The default message notification + * level of the guild + * @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild + * @property {TextChannelResolvable} [rulesChannel] The rules channel of the guild + * @property {TextChannelResolvable} [publicUpdatesChannel] The community updates channel of the guild + * @property {string} [preferredLocale] The preferred locale of the guild + * @property {boolean} [premiumProgressBarEnabled] Whether the guild's premium progress bar is enabled + * @property {string} [description] The discovery description of the guild + * @property {Features[]} [features] The features of the guild + */ + + /** + * Data that can be resolved to a Text Channel object. This can be: + * * A TextChannel + * * A Snowflake + * @typedef {TextChannel|Snowflake} TextChannelResolvable + */ + + /** + * Data that can be resolved to a Voice Channel object. This can be: + * * A VoiceChannel + * * A Snowflake + * @typedef {VoiceChannel|Snowflake} VoiceChannelResolvable + */ + + /** + * Updates the guild with new information - e.g. a new name. + * @param {GuildEditData} data The data to update the guild with + * @param {string} [reason] Reason for editing this guild + * @returns {Promise} + * @example + * // Set the guild name + * guild.edit({ + * name: 'Discord Guild', + * }) + * .then(updated => console.log(`New guild name ${updated}`)) + * .catch(console.error); + */ + async edit(data, reason) { + const _data = {}; + if (data.name) _data.name = data.name; + if (typeof data.verificationLevel !== 'undefined') { + _data.verification_level = + typeof data.verificationLevel === 'number' + ? data.verificationLevel + : VerificationLevels[data.verificationLevel]; + } + if (typeof data.afkChannel !== 'undefined') { + _data.afk_channel_id = this.client.channels.resolveId(data.afkChannel); + } + if (typeof data.systemChannel !== 'undefined') { + _data.system_channel_id = this.client.channels.resolveId(data.systemChannel); + } + if (data.afkTimeout) _data.afk_timeout = Number(data.afkTimeout); + if (typeof data.icon !== 'undefined') { + _data.icon = await DataResolver.resolveImage(data.icon); + } + if (data.owner) _data.owner_id = this.client.users.resolveId(data.owner); + if (typeof data.splash !== 'undefined') { + _data.splash = await DataResolver.resolveImage(data.splash); + } + if (typeof data.discoverySplash !== 'undefined') { + _data.discovery_splash = await DataResolver.resolveImage(data.discoverySplash); + } + if (typeof data.banner !== 'undefined') { + _data.banner = await DataResolver.resolveImage(data.banner); + } + if (typeof data.explicitContentFilter !== 'undefined') { + _data.explicit_content_filter = + typeof data.explicitContentFilter === 'number' + ? data.explicitContentFilter + : ExplicitContentFilterLevels[data.explicitContentFilter]; + } + if (typeof data.defaultMessageNotifications !== 'undefined') { + _data.default_message_notifications = + typeof data.defaultMessageNotifications === 'number' + ? data.defaultMessageNotifications + : DefaultMessageNotificationLevels[data.defaultMessageNotifications]; + } + if (typeof data.systemChannelFlags !== 'undefined') { + _data.system_channel_flags = SystemChannelFlags.resolve(data.systemChannelFlags); + } + if (typeof data.rulesChannel !== 'undefined') { + _data.rules_channel_id = this.client.channels.resolveId(data.rulesChannel); + } + if (typeof data.publicUpdatesChannel !== 'undefined') { + _data.public_updates_channel_id = this.client.channels.resolveId(data.publicUpdatesChannel); + } + if (typeof data.features !== 'undefined') { + _data.features = data.features; + } + if (typeof data.description !== 'undefined') { + _data.description = data.description; + } + if (data.preferredLocale) _data.preferred_locale = data.preferredLocale; + if ('premiumProgressBarEnabled' in data) { + _data.premium_progress_bar_enabled = data.premiumProgressBarEnabled; + } + const newData = await this.client.api.guilds(this.id).patch({ data: _data, reason }); + return this.client.actions.GuildUpdate.handle(newData).updated; + } + + /** + * Welcome channel data + * @typedef {Object} WelcomeChannelData + * @property {string} description The description to show for this welcome channel + * @property {TextChannel|NewsChannel|StoreChannel|Snowflake} channel The channel to link for this welcome channel + * @property {EmojiIdentifierResolvable} [emoji] The emoji to display for this welcome channel + */ + + /** + * Welcome screen edit data + * @typedef {Object} WelcomeScreenEditData + * @property {boolean} [enabled] Whether the welcome screen is enabled + * @property {string} [description] The description for the welcome screen + * @property {WelcomeChannelData[]} [welcomeChannels] The welcome channel data for the welcome screen + */ + + /** + * Data that can be resolved to a GuildTextChannel object. This can be: + * * A TextChannel + * * A NewsChannel + * * A Snowflake + * @typedef {TextChannel|NewsChannel|Snowflake} GuildTextChannelResolvable + */ + + /** + * Data that can be resolved to a GuildVoiceChannel object. This can be: + * * A VoiceChannel + * * A StageChannel + * * A Snowflake + * @typedef {VoiceChannel|StageChannel|Snowflake} GuildVoiceChannelResolvable + */ + + /** + * Updates the guild's welcome screen + * @param {WelcomeScreenEditData} data Data to edit the welcome screen with + * @returns {Promise} + * @example + * guild.editWelcomeScreen({ + * description: 'Hello World', + * enabled: true, + * welcomeChannels: [ + * { + * description: 'foobar', + * channel: '222197033908436994', + * } + * ], + * }) + */ + async editWelcomeScreen(data) { + const { enabled, description, welcomeChannels } = data; + const welcome_channels = welcomeChannels?.map(welcomeChannelData => { + const emoji = this.emojis.resolve(welcomeChannelData.emoji); + return { + emoji_id: emoji?.id, + emoji_name: emoji?.name ?? welcomeChannelData.emoji, + channel_id: this.channels.resolveId(welcomeChannelData.channel), + description: welcomeChannelData.description, + }; + }); + + const patchData = await this.client.api.guilds(this.id, 'welcome-screen').patch({ + data: { + welcome_channels, + description, + enabled, + }, + }); + return new WelcomeScreen(this, patchData); + } + + /** + * Edits the level of the explicit content filter. + * @param {ExplicitContentFilterLevel|number} explicitContentFilter The new level of the explicit content filter + * @param {string} [reason] Reason for changing the level of the guild's explicit content filter + * @returns {Promise} + */ + setExplicitContentFilter(explicitContentFilter, reason) { + return this.edit({ explicitContentFilter }, reason); + } + + /* eslint-disable max-len */ + /** + * Edits the setting of the default message notifications of the guild. + * @param {DefaultMessageNotificationLevel|number} defaultMessageNotifications The new default message notification level of the guild + * @param {string} [reason] Reason for changing the setting of the default message notifications + * @returns {Promise} + */ + setDefaultMessageNotifications(defaultMessageNotifications, reason) { + return this.edit({ defaultMessageNotifications }, reason); + } + /* eslint-enable max-len */ + + /** + * Edits the flags of the default message notifications of the guild. + * @param {SystemChannelFlagsResolvable} systemChannelFlags The new flags for the default message notifications + * @param {string} [reason] Reason for changing the flags of the default message notifications + * @returns {Promise} + */ + setSystemChannelFlags(systemChannelFlags, reason) { + return this.edit({ systemChannelFlags }, reason); + } + + /** + * Edits the name of the guild. + * @param {string} name The new name of the guild + * @param {string} [reason] Reason for changing the guild's name + * @returns {Promise} + * @example + * // Edit the guild name + * guild.setName('Discord Guild') + * .then(updated => console.log(`Updated guild name to ${updated.name}`)) + * .catch(console.error); + */ + setName(name, reason) { + return this.edit({ name }, reason); + } + + /** + * Edits the verification level of the guild. + * @param {VerificationLevel|number} verificationLevel The new verification level of the guild + * @param {string} [reason] Reason for changing the guild's verification level + * @returns {Promise} + * @example + * // Edit the guild verification level + * guild.setVerificationLevel(1) + * .then(updated => console.log(`Updated guild verification level to ${guild.verificationLevel}`)) + * .catch(console.error); + */ + setVerificationLevel(verificationLevel, reason) { + return this.edit({ verificationLevel }, reason); + } + + /** + * Edits the AFK channel of the guild. + * @param {VoiceChannelResolvable} afkChannel The new AFK channel + * @param {string} [reason] Reason for changing the guild's AFK channel + * @returns {Promise} + * @example + * // Edit the guild AFK channel + * guild.setAFKChannel(channel) + * .then(updated => console.log(`Updated guild AFK channel to ${guild.afkChannel.name}`)) + * .catch(console.error); + */ + setAFKChannel(afkChannel, reason) { + return this.edit({ afkChannel }, reason); + } + + /** + * Edits the system channel of the guild. + * @param {TextChannelResolvable} systemChannel The new system channel + * @param {string} [reason] Reason for changing the guild's system channel + * @returns {Promise} + * @example + * // Edit the guild system channel + * guild.setSystemChannel(channel) + * .then(updated => console.log(`Updated guild system channel to ${guild.systemChannel.name}`)) + * .catch(console.error); + */ + setSystemChannel(systemChannel, reason) { + return this.edit({ systemChannel }, reason); + } + + /** + * Edits the AFK timeout of the guild. + * @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK + * @param {string} [reason] Reason for changing the guild's AFK timeout + * @returns {Promise} + * @example + * // Edit the guild AFK channel + * guild.setAFKTimeout(60) + * .then(updated => console.log(`Updated guild AFK timeout to ${guild.afkTimeout}`)) + * .catch(console.error); + */ + setAFKTimeout(afkTimeout, reason) { + return this.edit({ afkTimeout }, reason); + } + + /** + * Sets a new guild icon. + * @param {?(Base64Resolvable|BufferResolvable)} icon The new icon of the guild + * @param {string} [reason] Reason for changing the guild's icon + * @returns {Promise} + * @example + * // Edit the guild icon + * guild.setIcon('./icon.png') + * .then(updated => console.log('Updated the guild icon')) + * .catch(console.error); + */ + setIcon(icon, reason) { + return this.edit({ icon }, reason); + } + + /** + * Sets a new owner of the guild. + * @param {GuildMemberResolvable} owner The new owner of the guild + * @param {string} [reason] Reason for setting the new owner + * @returns {Promise} + * @example + * // Edit the guild owner + * guild.setOwner(guild.members.cache.first()) + * .then(guild => guild.fetchOwner()) + * .then(owner => console.log(`Updated the guild owner to ${owner.displayName}`)) + * .catch(console.error); + */ + setOwner(owner, reason) { + return this.edit({ owner }, reason); + } + + /** + * Sets a new guild invite splash image. + * @param {?(Base64Resolvable|BufferResolvable)} splash The new invite splash image of the guild + * @param {string} [reason] Reason for changing the guild's invite splash image + * @returns {Promise} + * @example + * // Edit the guild splash + * guild.setSplash('./splash.png') + * .then(updated => console.log('Updated the guild splash')) + * .catch(console.error); + */ + setSplash(splash, reason) { + return this.edit({ splash }, reason); + } + + /** + * Sets a new guild discovery splash image. + * @param {?(Base64Resolvable|BufferResolvable)} discoverySplash The new discovery splash image of the guild + * @param {string} [reason] Reason for changing the guild's discovery splash image + * @returns {Promise} + * @example + * // Edit the guild discovery splash + * guild.setDiscoverySplash('./discoverysplash.png') + * .then(updated => console.log('Updated the guild discovery splash')) + * .catch(console.error); + */ + setDiscoverySplash(discoverySplash, reason) { + return this.edit({ discoverySplash }, reason); + } + + /** + * Sets a new guild banner. + * @param {?(Base64Resolvable|BufferResolvable)} banner The new banner of the guild + * @param {string} [reason] Reason for changing the guild's banner + * @returns {Promise} + * @example + * guild.setBanner('./banner.png') + * .then(updated => console.log('Updated the guild banner')) + * .catch(console.error); + */ + setBanner(banner, reason) { + return this.edit({ banner }, reason); + } + + /** + * Edits the rules channel of the guild. + * @param {TextChannelResolvable} rulesChannel The new rules channel + * @param {string} [reason] Reason for changing the guild's rules channel + * @returns {Promise} + * @example + * // Edit the guild rules channel + * guild.setRulesChannel(channel) + * .then(updated => console.log(`Updated guild rules channel to ${guild.rulesChannel.name}`)) + * .catch(console.error); + */ + setRulesChannel(rulesChannel, reason) { + return this.edit({ rulesChannel }, reason); + } + /** + * Change Guild Position (from * to Folder or Home) + * @param {number} position Guild Position + * * **WARNING**: Type = `FOLDER`, newPosition is the guild's index in the Folder. + * @param {String|Number} type Move to folder or home + * * `FOLDER`: 1 + * * `HOME`: 2 + * @param {String|Number|void|null} folderID If you want to move to folder + * @returns {Promise} + * @example + * // Move guild to folderID 123456, index 1 + * guild.setPosition(1, 'FOLDER', 123456) + * .then(guild => console.log(`Guild moved to folderID ${guild.folder.folderId}`)); + */ + async setPosition(position, type, folderID) { + if (type == 1 || `${type}`.toUpperCase() === 'FOLDER') { + folderID = folderID || this.folder.folderId; + if (!['number', 'string'].includes(typeof folderID)) { + throw new TypeError('INVALID_TYPE', 'folderID', 'String | Number'); + } + // Get Data from Folder ID + const folder = await this.client.setting.rawSetting.guild_folders.find(obj => obj.id == folderID); + if (!folder) throw new Error('FOLDER_NOT_FOUND'); + if (folder.guild_ids.length - 1 < position || position < 0) { + throw new Error('FOLDER_POSITION_INVALID'); + } + if (position !== folder.guild_ids.indexOf(this.id)) { + await this.client.setting.guildChangePosition(this.id, position, 1, folderID); + } + } else if (type == 2 || `${type}`.toUpperCase() === 'HOME') { + if (this.client.setting.guild_positions - 1 < position || position < 0) { + throw new Error('FOLDER_POSITION_INVALID'); + } + if (position !== this.position) { + await this.client.setting.guildChangePosition(this.id, position, 2, null); + } + } else { + throw new TypeError('INVALID_TYPE', 'type', '`Folder`| `Home`'); + } + return this; + } + + /** + * Edits the community updates channel of the guild. + * @param {TextChannelResolvable} publicUpdatesChannel The new community updates channel + * @param {string} [reason] Reason for changing the guild's community updates channel + * @returns {Promise} + * @example + * // Edit the guild community updates channel + * guild.setPublicUpdatesChannel(channel) + * .then(updated => console.log(`Updated guild community updates channel to ${guild.publicUpdatesChannel.name}`)) + * .catch(console.error); + */ + setPublicUpdatesChannel(publicUpdatesChannel, reason) { + return this.edit({ publicUpdatesChannel }, reason); + } + + /** + * Edits the preferred locale of the guild. + * @param {string} preferredLocale The new preferred locale of the guild + * @param {string} [reason] Reason for changing the guild's preferred locale + * @returns {Promise} + * @example + * // Edit the guild preferred locale + * guild.setPreferredLocale('en-US') + * .then(updated => console.log(`Updated guild preferred locale to ${guild.preferredLocale}`)) + * .catch(console.error); + */ + setPreferredLocale(preferredLocale, reason) { + return this.edit({ preferredLocale }, reason); + } + + /** + * Edits the enabled state of the guild's premium progress bar + * @param {boolean} [enabled=true] The new enabled state of the guild's premium progress bar + * @param {string} [reason] Reason for changing the state of the guild's premium progress bar + * @returns {Promise} + */ + setPremiumProgressBarEnabled(enabled = true, reason) { + return this.edit({ premiumProgressBarEnabled: enabled }, reason); + } + + /** + * Data that can be resolved to give a Category Channel object. This can be: + * * A CategoryChannel object + * * A Snowflake + * @typedef {CategoryChannel|Snowflake} CategoryChannelResolvable + */ + + /** + * The data needed for updating a channel's position. + * @typedef {Object} ChannelPosition + * @property {GuildChannel|Snowflake} channel Channel to update + * @property {number} [position] New position for the channel + * @property {CategoryChannelResolvable} [parent] Parent channel for this channel + * @property {boolean} [lockPermissions] If the overwrites should be locked to the parents overwrites + */ + + /** + * Batch-updates the guild's channels' positions. + * Only one channel's parent can be changed at a time + * @param {ChannelPosition[]} channelPositions Channel positions to update + * @returns {Promise} + * @deprecated Use {@link GuildChannelManager#setPositions} instead + * @example + * guild.setChannelPositions([{ channel: channelId, position: newChannelIndex }]) + * .then(guild => console.log(`Updated channel positions for ${guild}`)) + * .catch(console.error); + */ + setChannelPositions(channelPositions) { + if (!deprecationEmittedForSetChannelPositions) { + process.emitWarning( + 'The Guild#setChannelPositions method is deprecated. Use GuildChannelManager#setPositions instead.', + 'DeprecationWarning', + ); + + deprecationEmittedForSetChannelPositions = true; + } + + return this.channels.setPositions(channelPositions); + } + + /** + * The data needed for updating a guild role's position + * @typedef {Object} GuildRolePosition + * @property {RoleResolvable} role The role's id + * @property {number} position The position to update + */ + + /** + * Batch-updates the guild's role positions + * @param {GuildRolePosition[]} rolePositions Role positions to update + * @returns {Promise} + * @deprecated Use {@link RoleManager#setPositions} instead + * @example + * guild.setRolePositions([{ role: roleId, position: updatedRoleIndex }]) + * .then(guild => console.log(`Role positions updated for ${guild}`)) + * .catch(console.error); + */ + setRolePositions(rolePositions) { + if (!deprecationEmittedForSetRolePositions) { + process.emitWarning( + 'The Guild#setRolePositions method is deprecated. Use RoleManager#setPositions instead.', + 'DeprecationWarning', + ); + + deprecationEmittedForSetRolePositions = true; + } + + return this.roles.setPositions(rolePositions); + } + + /** + * Edits the guild's widget settings. + * @param {GuildWidgetSettingsData} settings The widget settings for the guild + * @param {string} [reason] Reason for changing the guild's widget settings + * @returns {Promise} + */ + async setWidgetSettings(settings, reason) { + await this.client.api.guilds(this.id).widget.patch({ + data: { + enabled: settings.enabled, + channel_id: this.channels.resolveId(settings.channel), + }, + reason, + }); + return this; + } + + /** + * Leaves the guild. + * @returns {Promise} + * @example + * // Leave a guild + * guild.leave() + * .then(g => console.log(`Left the guild ${g}`)) + * .catch(console.error); + */ + async leave() { + if (this.ownerId === this.client.user.id) throw new Error('GUILD_OWNED'); + await this.client.api.users('@me').guilds(this.id).delete(); + return this.client.actions.GuildDelete.handle({ id: this.id }).guild; + } + + /** + * Deletes the guild. + * @returns {Promise} + * @example + * // Delete a guild + * guild.delete() + * .then(g => console.log(`Deleted the guild ${g}`)) + * .catch(console.error); + */ + async delete() { + await this.client.api.guilds(this.id).delete(); + return this.client.actions.GuildDelete.handle({ id: this.id }).guild; + } + + /** + * Whether this guild equals another guild. It compares all properties, so for most operations + * it is advisable to just compare `guild.id === guild2.id` as it is much faster and is often + * what most users need. + * @param {Guild} guild The guild to compare with + * @returns {boolean} + */ + equals(guild) { + return ( + guild && + guild instanceof this.constructor && + this.id === guild.id && + this.available === guild.available && + this.splash === guild.splash && + this.discoverySplash === guild.discoverySplash && + this.name === guild.name && + this.memberCount === guild.memberCount && + this.large === guild.large && + this.icon === guild.icon && + this.ownerId === guild.ownerId && + this.verificationLevel === guild.verificationLevel && + (this.features === guild.features || + (this.features.length === guild.features.length && + this.features.every((feat, i) => feat === guild.features[i]))) + ); + } + + /** + * Set Community Feature + * @param {Boolean} stats True / False to enable / disable Community Feature + * @param {TextChannelResolvable} publicUpdatesChannel + * @param {TextChannelResolvable} rulesChannel + * @param {String} reason + */ + async setCommunity(stats = true, publicUpdatesChannel = '1', rulesChannel = '1', reason) { + if (stats) { + // Check everyone role + const everyoneRole = this.roles.everyone; + if (everyoneRole.mentionable) { + await everyoneRole.setMentionable(false, reason); + } + // Setting + this.edit( + { + defaultMessageNotifications: 'ONLY_MENTIONS', + explicitContentFilter: 'ALL_MEMBERS', + features: [...this.features, 'COMMUNITY'], + publicUpdatesChannel, + rulesChannel, + verificationLevel: VerificationLevels[this.verificationLevel] < 1 ? 'LOW' : this.verificationLevel, // Email + }, + reason, + ); + } else { + this.edit( + { + publicUpdatesChannel: null, + rulesChannel: null, + features: this.features.filter(f => f !== 'COMMUNITY'), + preferredLocale: this.preferredLocale, + description: this.description, + }, + reason, + ); + } + } + + toJSON() { + const json = super.toJSON({ + available: false, + createdTimestamp: true, + nameAcronym: true, + presences: false, + voiceStates: false, + }); + json.iconURL = this.iconURL(); + json.splashURL = this.splashURL(); + json.discoverySplashURL = this.discoverySplashURL(); + json.bannerURL = this.bannerURL(); + return json; + } + + /** + * The voice state adapter for this guild that can be used with @discordjs/voice to play audio in voice + * and stage channels. + * @type {Function} + * @readonly + */ + get voiceAdapterCreator() { + return methods => { + this.client.voice.adapters.set(this.id, methods); + return { + sendPayload: data => { + if (this.shard.status !== Status.READY) return false; + this.shard.send(data); + return true; + }, + destroy: () => { + this.client.voice.adapters.delete(this.id); + }, + }; + }; + } + + /** + * Creates a collection of this guild's roles, sorted by their position and ids. + * @returns {Collection} + * @private + */ + _sortedRoles() { + return Util.discordSort(this.roles.cache); + } + + /** + * Creates a collection of this guild's or a specific category's channels, sorted by their position and ids. + * @param {GuildChannel} [channel] Category to get the channels of + * @returns {Collection} + * @private + */ + _sortedChannels(channel) { + const category = channel.type === ChannelTypes.GUILD_CATEGORY; + return Util.discordSort( + this.channels.cache.filter( + c => + (['GUILD_TEXT', 'GUILD_NEWS', 'GUILD_STORE'].includes(channel.type) + ? ['GUILD_TEXT', 'GUILD_NEWS', 'GUILD_STORE'].includes(c.type) + : c.type === channel.type) && + (category || c.parent === channel.parent), + ), + ); + } } exports.Guild = Guild; diff --git a/src/structures/Invite.js b/src/structures/Invite.js index 8ca3ee9..b6773b4 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -327,8 +327,8 @@ class Invite extends Base { await this.client.api.invites(this.code).post({}); if (autoVerify) { const getForm = await this.client.api - .guilds(this.guild.id)['member-verification'] - .get({ query: { with_guild: false, invite_code: this.code } }) + .guilds(this.guild.id) + ['member-verification'].get({ query: { with_guild: false, invite_code: this.code } }) .catch(() => {}); if (!getForm) return void 0; const form = Object.assign(getForm.form_fields[0], { response: true }); @@ -338,7 +338,6 @@ class Invite extends Base { } return void 0; } - } /** diff --git a/src/structures/Message.js b/src/structures/Message.js index 8192c1a..111e2c3 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -1,1124 +1,1090 @@ -'use strict'; - -const process = require('node:process'); -const { Collection } = require('@discordjs/collection'); -const Base = require('./Base'); -const BaseMessageComponent = require('./BaseMessageComponent'); -const ClientApplication = require('./ClientApplication'); -const InteractionCollector = require('./InteractionCollector'); -const MessageAttachment = require('./MessageAttachment'); -const Embed = require('./MessageEmbed'); -const Mentions = require('./MessageMentions'); -const MessagePayload = require('./MessagePayload'); -const ReactionCollector = require('./ReactionCollector'); -const { Sticker } = require('./Sticker'); -const { Error } = require('../errors'); -const ReactionManager = require('../managers/ReactionManager'); -const { InteractionTypes, MessageTypes, SystemMessageTypes } = require('../util/Constants'); -const MessageFlags = require('../util/MessageFlags'); -const Permissions = require('../util/Permissions'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); -const Util = require('../util/Util'); -const { findBestMatch } = require('string-similarity'); // not check similarity -//const { ApplicationCommand } = require('discord.js-selfbot-v13'); - Not being used in this file, not necessary. - -/** - * @type {WeakSet} - * @private - * @internal - */ -const deletedMessages = new WeakSet(); -let deprecationEmittedForDeleted = false; - -/** - * Represents a message on Discord. - * @extends {Base} - */ -class Message extends Base { - constructor(client, data) { - super(client); - - /** - * The id of the channel the message was sent in - * @type {Snowflake} - */ - this.channelId = data.channel_id; - - /** - * The id of the guild the message was sent in, if any - * @type {?Snowflake} - */ - this.guildId = data.guild_id ?? this.channel?.guild?.id ?? null; - - this._patch(data); - } - - _patch(data) { - /** - * The message's id - * @type {Snowflake} - */ - this.id = data.id; - - /** - * The timestamp the message was sent at - * @type {number} - */ - this.createdTimestamp = SnowflakeUtil.timestampFrom(this.id); - - if ('type' in data) { - /** - * The type of the message - * @type {?MessageType} - */ - this.type = MessageTypes[data.type]; - - /** - * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications) - * @type {?boolean} - */ - this.system = SystemMessageTypes.includes(this.type); - } else { - this.system ??= null; - this.type ??= null; - } - - if ('content' in data) { - /** - * The content of the message - * @type {?string} - */ - this.content = data.content; - } else { - this.content ??= null; - } - - if ('author' in data) { - /** - * The author of the message - * @type {?User} - */ - this.author = this.client.users._add(data.author, !data.webhook_id); - } else { - this.author ??= null; - } - - if ('pinned' in data) { - /** - * Whether or not this message is pinned - * @type {?boolean} - */ - this.pinned = Boolean(data.pinned); - } else { - this.pinned ??= null; - } - - if ('tts' in data) { - /** - * Whether or not the message was Text-To-Speech - * @type {?boolean} - */ - this.tts = data.tts; - } else { - this.tts ??= null; - } - - if ('nonce' in data) { - /** - * A random number or string used for checking message delivery - * This is only received after the message was sent successfully, and - * lost if re-fetched - * @type {?string} - */ - this.nonce = data.nonce; - } else { - this.nonce ??= null; - } - - if ('embeds' in data) { - /** - * A list of embeds in the message - e.g. YouTube Player - * @type {MessageEmbed[]} - */ - this.embeds = data.embeds.map((e) => new Embed(e, true)); - } else { - this.embeds = this.embeds?.slice() ?? []; - } - - if ('components' in data) { - /** - * A list of MessageActionRows in the message - * @type {MessageActionRow[]} - */ - this.components = data.components.map((c) => - BaseMessageComponent.create(c, this.client), - ); - } else { - this.components = this.components?.slice() ?? []; - } - - if ('attachments' in data) { - /** - * A collection of attachments in the message - e.g. Pictures - mapped by their ids - * @type {Collection} - */ - this.attachments = new Collection(); - if (data.attachments) { - for (const attachment of data.attachments) { - this.attachments.set( - attachment.id, - new MessageAttachment( - attachment.url, - attachment.filename, - attachment, - ), - ); - } - } - } else { - this.attachments = new Collection(this.attachments); - } - - if ('sticker_items' in data || 'stickers' in data) { - /** - * A collection of stickers in the message - * @type {Collection} - */ - this.stickers = new Collection( - (data.sticker_items ?? data.stickers)?.map((s) => [ - s.id, - new Sticker(this.client, s), - ]), - ); - } else { - this.stickers = new Collection(this.stickers); - } - - // Discord sends null if the message has not been edited - if (data.edited_timestamp) { - /** - * The timestamp the message was last edited at (if applicable) - * @type {?number} - */ - this.editedTimestamp = new Date(data.edited_timestamp).getTime(); - } else { - this.editedTimestamp ??= null; - } - - if ('reactions' in data) { - /** - * A manager of the reactions belonging to this message - * @type {ReactionManager} - */ - this.reactions = new ReactionManager(this); - if (data.reactions?.length > 0) { - for (const reaction of data.reactions) { - this.reactions._add(reaction); - } - } - } else { - this.reactions ??= new ReactionManager(this); - } - - if (!this.mentions) { - /** - * All valid mentions that the message contains - * @type {MessageMentions} - */ - this.mentions = new Mentions( - this, - data.mentions, - data.mention_roles, - data.mention_everyone, - data.mention_channels, - data.referenced_message?.author, - ); - } else { - this.mentions = new Mentions( - this, - data.mentions ?? this.mentions.users, - data.mention_roles ?? this.mentions.roles, - data.mention_everyone ?? this.mentions.everyone, - data.mention_channels ?? this.mentions.crosspostedChannels, - data.referenced_message?.author ?? this.mentions.repliedUser, - ); - } - - if ('webhook_id' in data) { - /** - * The id of the webhook that sent the message, if applicable - * @type {?Snowflake} - */ - this.webhookId = data.webhook_id; - } else { - this.webhookId ??= null; - } - - if ('application' in data) { - /** - * Supplemental application information for group activities - * @type {?ClientApplication} - */ - this.groupActivityApplication = new ClientApplication( - this.client, - data.application, - ); - } else { - this.groupActivityApplication ??= null; - } - - if ('application_id' in data) { - /** - * The id of the application of the interaction that sent this message, if any - * @type {?Snowflake} - */ - this.applicationId = data.application_id; - } else { - this.applicationId ??= null; - } - - if ('activity' in data) { - /** - * Group activity - * @type {?MessageActivity} - */ - this.activity = { - partyId: data.activity.party_id, - type: data.activity.type, - }; - } else { - this.activity ??= null; - } - - if ('thread' in data) { - this.client.channels._add(data.thread, this.guild); - } - - if (this.member && data.member) { - this.member._patch(data.member); - } else if (data.member && this.guild && this.author) { - this.guild.members._add( - Object.assign(data.member, { user: this.author }), - ); - } - - if ('flags' in data) { - /** - * Flags that are applied to the message - * @type {Readonly} - */ - this.flags = new MessageFlags(data.flags).freeze(); - } else { - this.flags = new MessageFlags(this.flags).freeze(); - } - - /** - * Reference data sent in a message that contains ids identifying the referenced message. - * This can be present in the following types of message: - * * Crossposted messages (IS_CROSSPOST {@link MessageFlags.FLAGS message flag}) - * * CHANNEL_FOLLOW_ADD - * * CHANNEL_PINNED_MESSAGE - * * REPLY - * * THREAD_STARTER_MESSAGE - * @see {@link https://discord.com/developers/docs/resources/channel#message-types} - * @typedef {Object} MessageReference - * @property {Snowflake} channelId The channel's id the message was referenced - * @property {?Snowflake} guildId The guild's id the message was referenced - * @property {?Snowflake} messageId The message's id that was referenced - */ - - if ('message_reference' in data) { - /** - * Message reference data - * @type {?MessageReference} - */ - this.reference = { - channelId: data.message_reference.channel_id, - guildId: data.message_reference.guild_id, - messageId: data.message_reference.message_id, - }; - } else { - this.reference ??= null; - } - - if (data.referenced_message) { - this.channel?.messages._add({ - guild_id: data.message_reference?.guild_id, - ...data.referenced_message, - }); - } - - /** - * Partial data of the interaction that a message is a reply to - * @typedef {Object} MessageInteraction - * @property {Snowflake} id The interaction's id - * @property {InteractionType} type The type of the interaction - * @property {string} commandName The name of the interaction's application command - * @property {User} user The user that invoked the interaction - */ - - if (data.interaction) { - /** - * Partial data of the interaction that this message is a reply to - * @type {?MessageInteraction} - */ - this.interaction = { - id: data.interaction.id, - type: InteractionTypes[data.interaction.type], - commandName: data.interaction.name, - user: this.client.users._add(data.interaction.user), - }; - } else { - this.interaction ??= null; - } - } - - /** - * Whether or not the structure has been deleted - * @type {boolean} - * @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091 - */ - get deleted() { - if (!deprecationEmittedForDeleted) { - deprecationEmittedForDeleted = true; - process.emitWarning( - 'Message#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', - 'DeprecationWarning', - ); - } - - return deletedMessages.has(this); - } - - set deleted(value) { - if (!deprecationEmittedForDeleted) { - deprecationEmittedForDeleted = true; - process.emitWarning( - 'Message#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', - 'DeprecationWarning', - ); - } - - if (value) deletedMessages.add(this); - else deletedMessages.delete(this); - } - - /** - * The channel that the message was sent in - * @type {TextChannel|DMChannel|NewsChannel|ThreadChannel} - * @readonly - */ - get channel() { - return this.client.channels.resolve(this.channelId); - } - - /** - * Whether or not this message is a partial - * @type {boolean} - * @readonly - */ - get partial() { - return typeof this.content !== 'string' || !this.author; - } - - /** - * Represents the author of the message as a guild member. - * Only available if the message comes from a guild where the author is still a member - * @type {?GuildMember} - * @readonly - */ - get member() { - return this.guild?.members.resolve(this.author) ?? null; - } - - /** - * The time the message was sent at - * @type {Date} - * @readonly - */ - get createdAt() { - return new Date(this.createdTimestamp); - } - - /** - * The time the message was last edited at (if applicable) - * @type {?Date} - * @readonly - */ - get editedAt() { - return this.editedTimestamp ? new Date(this.editedTimestamp) : null; - } - - /** - * The guild the message was sent in (if in a guild channel) - * @type {?Guild} - * @readonly - */ - get guild() { - return ( - this.client.guilds.resolve(this.guildId) ?? this.channel?.guild ?? null - ); - } - - /** - * Whether this message has a thread associated with it - * @type {boolean} - * @readonly - */ - get hasThread() { - return this.flags.has(MessageFlags.FLAGS.HAS_THREAD); - } - - /** - * The thread started by this message - * This property is not suitable for checking whether a message has a thread, - * use {@link Message#hasThread} instead. - * @type {?ThreadChannel} - * @readonly - */ - get thread() { - return this.channel?.threads?.resolve(this.id) ?? null; - } - - /** - * The URL to jump to this message - * @type {string} - * @readonly - */ - get url() { - return `https://discord.com/channels/${this.guildId ?? '@me'}/${this.channelId - }/${this.id}`; - } - - /** - * The message contents with all mentions replaced by the equivalent text. - * If mentions cannot be resolved to a name, the relevant mention in the message content will not be converted. - * @type {?string} - * @readonly - */ - get cleanContent() { - // eslint-disable-next-line eqeqeq - return this.content != null - ? Util.cleanContent(this.content, this.channel) - : null; - } - - /** - * Creates a reaction collector. - * @param {ReactionCollectorOptions} [options={}] Options to send to the collector - * @returns {ReactionCollector} - * @example - * // Create a reaction collector - * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId'; - * const collector = message.createReactionCollector({ filter, time: 15_000 }); - * collector.on('collect', r => console.log(`Collected ${r.emoji.name}`)); - * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); - */ - createReactionCollector(options = {}) { - return new ReactionCollector(this, options); - } - - /** - * An object containing the same properties as CollectorOptions, but a few more: - * @typedef {ReactionCollectorOptions} AwaitReactionsOptions - * @property {string[]} [errors] Stop/end reasons that cause the promise to reject - */ - - /** - * Similar to createReactionCollector but in promise form. - * Resolves with a collection of reactions that pass the specified filter. - * @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector - * @returns {Promise>} - * @example - * // Create a reaction collector - * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId' - * message.awaitReactions({ filter, time: 15_000 }) - * .then(collected => console.log(`Collected ${collected.size} reactions`)) - * .catch(console.error); - */ - awaitReactions(options = {}) { - return new Promise((resolve, reject) => { - const collector = this.createReactionCollector(options); - collector.once('end', (reactions, reason) => { - if (options.errors?.includes(reason)) reject(reactions); - else resolve(reactions); - }); - }); - } - - /** - * @typedef {CollectorOptions} MessageComponentCollectorOptions - * @property {MessageComponentType} [componentType] The type of component to listen for - * @property {number} [max] The maximum total amount of interactions to collect - * @property {number} [maxComponents] The maximum number of components to collect - * @property {number} [maxUsers] The maximum number of users to interact - */ - - /** - * Creates a message component interaction collector. - * @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector - * @returns {InteractionCollector} - * @example - * // Create a message component interaction collector - * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId'; - * const collector = message.createMessageComponentCollector({ filter, time: 15_000 }); - * collector.on('collect', i => console.log(`Collected ${i.customId}`)); - * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); - */ - createMessageComponentCollector(options = {}) { - return new InteractionCollector(this.client, { - ...options, - interactionType: InteractionTypes.MESSAGE_COMPONENT, - message: this, - }); - } - - /** - * An object containing the same properties as CollectorOptions, but a few more: - * @typedef {Object} AwaitMessageComponentOptions - * @property {CollectorFilter} [filter] The filter applied to this collector - * @property {number} [time] Time to wait for an interaction before rejecting - * @property {MessageComponentType} [componentType] The type of component interaction to collect - */ - - /** - * Collects a single component interaction that passes the filter. - * The Promise will reject if the time expires. - * @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector - * @returns {Promise} - * @example - * // Collect a message component interaction - * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId'; - * message.awaitMessageComponent({ filter, time: 15_000 }) - * .then(interaction => console.log(`${interaction.customId} was clicked!`)) - * .catch(console.error); - */ - awaitMessageComponent(options = {}) { - const _options = { ...options, max: 1 }; - return new Promise((resolve, reject) => { - const collector = this.createMessageComponentCollector(_options); - collector.once('end', (interactions, reason) => { - const interaction = interactions.first(); - if (interaction) resolve(interaction); - else reject(new Error('INTERACTION_COLLECTOR_ERROR', reason)); - }); - }); - } - - /** - * Whether the message is editable by the client user - * @type {boolean} - * @readonly - */ - get editable() { - const precheck = Boolean( - this.author.id === this.client.user.id && - !deletedMessages.has(this) && - (!this.guild || this.channel?.viewable), - ); - // Regardless of permissions thread messages cannot be edited if - // the thread is locked. - if (this.channel?.isThread()) { - return precheck && !this.channel.locked; - } - return precheck; - } - - /** - * Whether the message is deletable by the client user - * @type {boolean} - * @readonly - */ - get deletable() { - if (deletedMessages.has(this)) { - return false; - } - if (!this.guild) { - return this.author.id === this.client.user.id; - } - // DMChannel does not have viewable property, so check viewable after proved that message is on a guild. - if (!this.channel?.viewable) { - return false; - } - - const permissions = this.channel?.permissionsFor(this.client.user); - if (!permissions) return false; - // This flag allows deleting even if timed out - if (permissions.has(Permissions.FLAGS.ADMINISTRATOR, false)) return true; - - return Boolean( - this.author.id === this.client.user.id || - (permissions.has(Permissions.FLAGS.MANAGE_MESSAGES, false) && - this.guild.me.communicationDisabledUntilTimestamp < Date.now()), - ); - } - - /** - * Whether the message is pinnable by the client user - * @type {boolean} - * @readonly - */ - get pinnable() { - const { channel } = this; - return Boolean( - !this.system && - !deletedMessages.has(this) && - (!this.guild || - (channel?.viewable && - channel - ?.permissionsFor(this.client.user) - ?.has(Permissions.FLAGS.MANAGE_MESSAGES, false))), - ); - } - - /** - * Fetches the Message this crosspost/reply/pin-add references, if available to the client - * @returns {Promise} - */ - async fetchReference() { - if (!this.reference) throw new Error('MESSAGE_REFERENCE_MISSING'); - const { channelId, messageId } = this.reference; - const channel = this.client.channels.resolve(channelId); - if (!channel) throw new Error('GUILD_CHANNEL_RESOLVE'); - const message = await channel.messages.fetch(messageId); - return message; - } - - /** - * Whether the message is crosspostable by the client user - * @type {boolean} - * @readonly - */ - get crosspostable() { - const bitfield = - Permissions.FLAGS.SEND_MESSAGES | - (this.author.id === this.client.user.id - ? Permissions.defaultBit - : Permissions.FLAGS.MANAGE_MESSAGES); - const { channel } = this; - return Boolean( - channel?.type === 'GUILD_NEWS' && - !this.flags.has(MessageFlags.FLAGS.CROSSPOSTED) && - this.type === 'DEFAULT' && - channel.viewable && - channel.permissionsFor(this.client.user)?.has(bitfield, false) && - !deletedMessages.has(this), - ); - } - - /** - * Options that can be passed into {@link Message#edit}. - * @typedef {Object} MessageEditOptions - * @property {?string} [content] Content to be edited - * @property {MessageEmbed[]|APIEmbed[]} [embeds] Embeds to be added/edited - * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content - * @property {MessageFlags} [flags] Which flags to set for the message. Only `SUPPRESS_EMBEDS` can be edited. - * @property {MessageAttachment[]} [attachments] An array of attachments to keep, - * all attachments will be kept if omitted - * @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to add to the message - * @property {MessageActionRow[]|MessageActionRowOptions[]} [components] - * Action rows containing interactive components for the message (buttons, select menus) - */ - - /** - * Edits the content of the message. - * @param {string|MessagePayload|MessageEditOptions} options The options to provide - * @returns {Promise} - * @example - * // Update the content of a message - * message.edit('This is my new content!') - * .then(msg => console.log(`Updated the content of a message to ${msg.content}`)) - * .catch(console.error); - */ - edit(options) { - if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED')); - return this.channel.messages.edit(this, options); - } - - /** - * Publishes a message in an announcement channel to all channels following it. - * @returns {Promise} - * @example - * // Crosspost a message - * if (message.channel.type === 'GUILD_NEWS') { - * message.crosspost() - * .then(() => console.log('Crossposted message')) - * .catch(console.error); - * } - */ - crosspost() { - if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED')); - return this.channel.messages.crosspost(this.id); - } - - /** - * Pins this message to the channel's pinned messages. - * @returns {Promise} - * @example - * // Pin a message - * message.pin() - * .then(console.log) - * .catch(console.error) - */ - async pin() { - if (!this.channel) throw new Error('CHANNEL_NOT_CACHED'); - await this.channel.messages.pin(this.id); - return this; - } - - /** - * Unpins this message from the channel's pinned messages. - * @returns {Promise} - * @example - * // Unpin a message - * message.unpin() - * .then(console.log) - * .catch(console.error) - */ - async unpin() { - if (!this.channel) throw new Error('CHANNEL_NOT_CACHED'); - await this.channel.messages.unpin(this.id); - return this; - } - - /** - * Adds a reaction to the message. - * @param {EmojiIdentifierResolvable} emoji The emoji to react with - * @returns {Promise} - * @example - * // React to a message with a unicode emoji - * message.react('🤔') - * .then(console.log) - * .catch(console.error); - * @example - * // React to a message with a custom emoji - * message.react(message.guild.emojis.cache.get('123456789012345678')) - * .then(console.log) - * .catch(console.error); - */ - async react(emoji) { - if (!this.channel) throw new Error('CHANNEL_NOT_CACHED'); - await this.channel.messages.react(this.id, emoji); - - return this.client.actions.MessageReactionAdd.handle( - { - user: this.client.user, - channel: this.channel, - message: this, - emoji: Util.resolvePartialEmoji(emoji), - }, - true, - ).reaction; - } - - /** - * Deletes the message. - * @returns {Promise} - * @example - * // Delete a message - * message.delete() - * .then(msg => console.log(`Deleted message from ${msg.author.username}`)) - * .catch(console.error); - */ - async delete() { - if (!this.channel) throw new Error('CHANNEL_NOT_CACHED'); - await this.channel.messages.delete(this.id); - return this; - } - - /** - * Options provided when sending a message as an inline reply. - * @typedef {BaseMessageOptions} ReplyMessageOptions - * @property {boolean} [failIfNotExists=true] Whether to error if the referenced message - * does not exist (creates a standard message in this case when false) - * @property {StickerResolvable[]} [stickers=[]] Stickers to send in the message - */ - - /** - * Send an inline reply to this message. - * @param {string|MessagePayload|ReplyMessageOptions} options The options to provide - * @returns {Promise} - * @example - * // Reply to a message - * message.reply('This is a reply!') - * .then(() => console.log(`Replied to message "${message.content}"`)) - * .catch(console.error); - */ - reply(options) { - if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED')); - let data; - - if (options instanceof MessagePayload) { - data = options; - } else { - data = MessagePayload.create(this, options, { - reply: { - messageReference: this, - failIfNotExists: - options?.failIfNotExists ?? this.client.options.failIfNotExists, - }, - }); - } - return this.channel.send(data); - } - - /** - * A number that is allowed to be the duration (in minutes) of inactivity after which a thread is automatically - * archived. This can be: - * * `60` (1 hour) - * * `1440` (1 day) - * * `4320` (3 days) This is only available when the guild has the `THREE_DAY_THREAD_ARCHIVE` feature. - * * `10080` (7 days) This is only available when the guild has the `SEVEN_DAY_THREAD_ARCHIVE` feature. - * * `'MAX'` Based on the guild's features - * @typedef {number|string} ThreadAutoArchiveDuration - */ - - /** - * Options for starting a thread on a message. - * @typedef {Object} StartThreadOptions - * @property {string} name The name of the new thread - * @property {ThreadAutoArchiveDuration} [autoArchiveDuration=this.channel.defaultAutoArchiveDuration] The amount of - * time (in minutes) after which the thread should automatically archive in case of no recent activity - * @property {string} [reason] Reason for creating the thread - * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the thread in seconds - */ - - /** - * Create a new public thread from this message - * @see ThreadManager#create - * @param {StartThreadOptions} [options] Options for starting a thread on this message - * @returns {Promise} - */ - startThread(options = {}) { - if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED')); - if (!['GUILD_TEXT', 'GUILD_NEWS'].includes(this.channel.type)) { - return Promise.reject(new Error('MESSAGE_THREAD_PARENT')); - } - if (this.hasThread) - return Promise.reject(new Error('MESSAGE_EXISTING_THREAD')); - return this.channel.threads.create({ ...options, startMessage: this }); - } - - /** - * Fetch this message. - * @param {boolean} [force=true] Whether to skip the cache check and request the API - * @returns {Promise} - */ - fetch(force = true) { - if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED')); - return this.channel.messages.fetch(this.id, { force }); - } - - /** - * Fetches the webhook used to create this message. - * @returns {Promise} - */ - fetchWebhook() { - if (!this.webhookId) return Promise.reject(new Error('WEBHOOK_MESSAGE')); - if (this.webhookId === this.applicationId) - return Promise.reject(new Error('WEBHOOK_APPLICATION')); - return this.client.fetchWebhook(this.webhookId); - } - - /** - * Suppresses or unsuppresses embeds on a message. - * @param {boolean} [suppress=true] If the embeds should be suppressed or not - * @returns {Promise} - */ - suppressEmbeds(suppress = true) { - const flags = new MessageFlags(this.flags.bitfield); - - if (suppress) { - flags.add(MessageFlags.FLAGS.SUPPRESS_EMBEDS); - } else { - flags.remove(MessageFlags.FLAGS.SUPPRESS_EMBEDS); - } - - return this.edit({ flags }); - } - - /** - * Removes the attachments from this message. - * @returns {Promise} - */ - removeAttachments() { - return this.edit({ attachments: [] }); - } - - /** - * Resolves a component by a custom id. - * @param {string} customId The custom id to resolve against - * @returns {?MessageActionRowComponent} - */ - resolveComponent(customId) { - return ( - this.components - .flatMap((row) => row.components) - .find((component) => component.customId === customId) ?? null - ); - } - - /** - * Used mainly internally. Whether two messages are identical in properties. If you want to compare messages - * without checking all the properties, use `message.id === message2.id`, which is much more efficient. This - * method allows you to see if there are differences in content, embeds, attachments, nonce and tts properties. - * @param {Message} message The message to compare it to - * @param {APIMessage} rawData Raw data passed through the WebSocket about this message - * @returns {boolean} - */ - equals(message, rawData) { - if (!message) return false; - const embedUpdate = !message.author && !message.attachments; - if (embedUpdate) - return ( - this.id === message.id && this.embeds.length === message.embeds.length - ); - - let equal = - this.id === message.id && - this.author.id === message.author.id && - this.content === message.content && - this.tts === message.tts && - this.nonce === message.nonce && - this.embeds.length === message.embeds.length && - this.attachments.length === message.attachments.length; - - if (equal && rawData) { - equal = - this.mentions.everyone === message.mentions.everyone && - this.createdTimestamp === new Date(rawData.timestamp).getTime() && - this.editedTimestamp === new Date(rawData.edited_timestamp).getTime(); - } - - return equal; - } - - /** - * Whether this message is from a guild. - * @returns {boolean} - */ - inGuild() { - return Boolean(this.guildId); - } - - /** - * When concatenated with a string, this automatically concatenates the message's content instead of the object. - * @returns {string} - * @example - * // Logs: Message: This is a message! - * console.log(`Message: ${message}`); - */ - toString() { - return this.content; - } - - toJSON() { - return super.toJSON({ - channel: 'channelId', - author: 'authorId', - groupActivityApplication: 'groupActivityApplicationId', - guild: 'guildId', - cleanContent: true, - member: false, - reactions: false, - }); - } - // Added - /** - * Click specific button [Suggestion: Dux#2925] - * @param {String} buttonID Button ID - * @returns {Promise} - */ - async clickButton(buttonID) { - if (typeof buttonID !== 'string') - throw new TypeError('BUTTON_ID_NOT_STRING'); - if (!this.components[0]) throw new TypeError('MESSAGE_NO_COMPONENTS'); - let button; - await Promise.all( - this.components.map(async (row) => { - await Promise.all( - row.components.map(async (interactionComponent) => { - if ( - interactionComponent.type == 'BUTTON' && - interactionComponent.customId == buttonID - ) { - button = interactionComponent; - } - }), - ); - }), - ); - if (!button) throw new TypeError('BUTTON_NOT_FOUND'); - else button.click(this); - } - /** - * Select specific menu or First Menu - * @param {String|Array} menuID Select Menu specific id or auto select first Menu - * @param {Array} options Menu Options - */ - async selectMenu(menuID, options = []) { - if (!this.components[0]) throw new TypeError('MESSAGE_NO_COMPONENTS'); - let menuFirst; - let menuCorrect; - let menuCount = 0; - await Promise.all( - this.components.map(async (row) => { - const firstElement = row.components[0]; // Because 1 row has only 1 menu; - if (firstElement.type == 'SELECT_MENU') { - menuCount++; - if (firstElement.customId == menuID) { - menuCorrect = firstElement; - } else if (!menuFirst) { - menuFirst = firstElement; - } - } - }), - ); - if (menuCount == 0) throw new TypeError('MENU_NOT_FOUND'); - if (!menuCorrect) { - if (menuCount == 1) menuCorrect = menuFirst; - else if (typeof menuID !== 'string') throw new TypeError('MENU_ID_NOT_STRING'); - else throw new TypeError('MENU_ID_NOT_FOUND'); - } - menuCorrect.select(this, Array.isArray(menuID) ? menuID : options); - } - // - /** - * Send context Menu v2 - * @param {DiscordBotID} botID Bot id - * @param {String} commandName Command name in Context Menu - * @returns {Promise} - */ - async contextMenu(botID, commandName) { - if (!botID) throw new Error('Bot ID is required'); - const user = await this.client.users.fetch(botID).catch(() => { }); - if (!user || !user.bot || !user.applications) - throw new Error( - 'BotID is not a bot or does not have an application slash command', - ); - if (!commandName || typeof commandName !== 'string') - throw new Error('Command name is required'); - const listApplication = - user.applications.cache.size == 0 - ? await user.applications.fetch() - : user.applications.cache; - let contextCMD; - await Promise.all( - listApplication.map(async (application) => { - if (commandName == application.name && application.type !== 'CHAT_INPUT') - contextCMD = application; - }), - ); - if (!contextCMD) - throw new Error( - `Command ${commandName} is not found\nList command avalible: ${listApplication - .filter((a) => a.type !== 'CHAT_INPUT') - .map((a) => a.name) - .join(', ')}`, - ); - return contextCMD.sendContextMenu(this, true); - } -} - -exports.Message = Message; -exports.deletedMessages = deletedMessages; +'use strict'; + +const process = require('node:process'); +const { Collection } = require('@discordjs/collection'); +const Base = require('./Base'); +const BaseMessageComponent = require('./BaseMessageComponent'); +const ClientApplication = require('./ClientApplication'); +const InteractionCollector = require('./InteractionCollector'); +const MessageAttachment = require('./MessageAttachment'); +const Embed = require('./MessageEmbed'); +const Mentions = require('./MessageMentions'); +const MessagePayload = require('./MessagePayload'); +const ReactionCollector = require('./ReactionCollector'); +const { Sticker } = require('./Sticker'); +const { Error } = require('../errors'); +const ReactionManager = require('../managers/ReactionManager'); +const { InteractionTypes, MessageTypes, SystemMessageTypes } = require('../util/Constants'); +const MessageFlags = require('../util/MessageFlags'); +const Permissions = require('../util/Permissions'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); +const Util = require('../util/Util'); +const { findBestMatch } = require('string-similarity'); // not check similarity +// const { ApplicationCommand } = require('discord.js-selfbot-v13'); - Not being used in this file, not necessary. + +/** + * @type {WeakSet} + * @private + * @internal + */ +const deletedMessages = new WeakSet(); +let deprecationEmittedForDeleted = false; + +/** + * Represents a message on Discord. + * @extends {Base} + */ +class Message extends Base { + constructor(client, data) { + super(client); + + /** + * The id of the channel the message was sent in + * @type {Snowflake} + */ + this.channelId = data.channel_id; + + /** + * The id of the guild the message was sent in, if any + * @type {?Snowflake} + */ + this.guildId = data.guild_id ?? this.channel?.guild?.id ?? null; + + this._patch(data); + } + + _patch(data) { + /** + * The message's id + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The timestamp the message was sent at + * @type {number} + */ + this.createdTimestamp = SnowflakeUtil.timestampFrom(this.id); + + if ('type' in data) { + /** + * The type of the message + * @type {?MessageType} + */ + this.type = MessageTypes[data.type]; + + /** + * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications) + * @type {?boolean} + */ + this.system = SystemMessageTypes.includes(this.type); + } else { + this.system ??= null; + this.type ??= null; + } + + if ('content' in data) { + /** + * The content of the message + * @type {?string} + */ + this.content = data.content; + } else { + this.content ??= null; + } + + if ('author' in data) { + /** + * The author of the message + * @type {?User} + */ + this.author = this.client.users._add(data.author, !data.webhook_id); + } else { + this.author ??= null; + } + + if ('pinned' in data) { + /** + * Whether or not this message is pinned + * @type {?boolean} + */ + this.pinned = Boolean(data.pinned); + } else { + this.pinned ??= null; + } + + if ('tts' in data) { + /** + * Whether or not the message was Text-To-Speech + * @type {?boolean} + */ + this.tts = data.tts; + } else { + this.tts ??= null; + } + + if ('nonce' in data) { + /** + * A random number or string used for checking message delivery + * This is only received after the message was sent successfully, and + * lost if re-fetched + * @type {?string} + */ + this.nonce = data.nonce; + } else { + this.nonce ??= null; + } + + if ('embeds' in data) { + /** + * A list of embeds in the message - e.g. YouTube Player + * @type {MessageEmbed[]} + */ + this.embeds = data.embeds.map(e => new Embed(e, true)); + } else { + this.embeds = this.embeds?.slice() ?? []; + } + + if ('components' in data) { + /** + * A list of MessageActionRows in the message + * @type {MessageActionRow[]} + */ + this.components = data.components.map(c => BaseMessageComponent.create(c, this.client)); + } else { + this.components = this.components?.slice() ?? []; + } + + if ('attachments' in data) { + /** + * A collection of attachments in the message - e.g. Pictures - mapped by their ids + * @type {Collection} + */ + this.attachments = new Collection(); + if (data.attachments) { + for (const attachment of data.attachments) { + this.attachments.set(attachment.id, new MessageAttachment(attachment.url, attachment.filename, attachment)); + } + } + } else { + this.attachments = new Collection(this.attachments); + } + + if ('sticker_items' in data || 'stickers' in data) { + /** + * A collection of stickers in the message + * @type {Collection} + */ + this.stickers = new Collection( + (data.sticker_items ?? data.stickers)?.map(s => [s.id, new Sticker(this.client, s)]), + ); + } else { + this.stickers = new Collection(this.stickers); + } + + // Discord sends null if the message has not been edited + if (data.edited_timestamp) { + /** + * The timestamp the message was last edited at (if applicable) + * @type {?number} + */ + this.editedTimestamp = new Date(data.edited_timestamp).getTime(); + } else { + this.editedTimestamp ??= null; + } + + if ('reactions' in data) { + /** + * A manager of the reactions belonging to this message + * @type {ReactionManager} + */ + this.reactions = new ReactionManager(this); + if (data.reactions?.length > 0) { + for (const reaction of data.reactions) { + this.reactions._add(reaction); + } + } + } else { + this.reactions ??= new ReactionManager(this); + } + + if (!this.mentions) { + /** + * All valid mentions that the message contains + * @type {MessageMentions} + */ + this.mentions = new Mentions( + this, + data.mentions, + data.mention_roles, + data.mention_everyone, + data.mention_channels, + data.referenced_message?.author, + ); + } else { + this.mentions = new Mentions( + this, + data.mentions ?? this.mentions.users, + data.mention_roles ?? this.mentions.roles, + data.mention_everyone ?? this.mentions.everyone, + data.mention_channels ?? this.mentions.crosspostedChannels, + data.referenced_message?.author ?? this.mentions.repliedUser, + ); + } + + if ('webhook_id' in data) { + /** + * The id of the webhook that sent the message, if applicable + * @type {?Snowflake} + */ + this.webhookId = data.webhook_id; + } else { + this.webhookId ??= null; + } + + if ('application' in data) { + /** + * Supplemental application information for group activities + * @type {?ClientApplication} + */ + this.groupActivityApplication = new ClientApplication(this.client, data.application); + } else { + this.groupActivityApplication ??= null; + } + + if ('application_id' in data) { + /** + * The id of the application of the interaction that sent this message, if any + * @type {?Snowflake} + */ + this.applicationId = data.application_id; + } else { + this.applicationId ??= null; + } + + if ('activity' in data) { + /** + * Group activity + * @type {?MessageActivity} + */ + this.activity = { + partyId: data.activity.party_id, + type: data.activity.type, + }; + } else { + this.activity ??= null; + } + + if ('thread' in data) { + this.client.channels._add(data.thread, this.guild); + } + + if (this.member && data.member) { + this.member._patch(data.member); + } else if (data.member && this.guild && this.author) { + this.guild.members._add(Object.assign(data.member, { user: this.author })); + } + + if ('flags' in data) { + /** + * Flags that are applied to the message + * @type {Readonly} + */ + this.flags = new MessageFlags(data.flags).freeze(); + } else { + this.flags = new MessageFlags(this.flags).freeze(); + } + + /** + * Reference data sent in a message that contains ids identifying the referenced message. + * This can be present in the following types of message: + * * Crossposted messages (IS_CROSSPOST {@link MessageFlags.FLAGS message flag}) + * * CHANNEL_FOLLOW_ADD + * * CHANNEL_PINNED_MESSAGE + * * REPLY + * * THREAD_STARTER_MESSAGE + * @see {@link https://discord.com/developers/docs/resources/channel#message-types} + * @typedef {Object} MessageReference + * @property {Snowflake} channelId The channel's id the message was referenced + * @property {?Snowflake} guildId The guild's id the message was referenced + * @property {?Snowflake} messageId The message's id that was referenced + */ + + if ('message_reference' in data) { + /** + * Message reference data + * @type {?MessageReference} + */ + this.reference = { + channelId: data.message_reference.channel_id, + guildId: data.message_reference.guild_id, + messageId: data.message_reference.message_id, + }; + } else { + this.reference ??= null; + } + + if (data.referenced_message) { + this.channel?.messages._add({ + guild_id: data.message_reference?.guild_id, + ...data.referenced_message, + }); + } + + /** + * Partial data of the interaction that a message is a reply to + * @typedef {Object} MessageInteraction + * @property {Snowflake} id The interaction's id + * @property {InteractionType} type The type of the interaction + * @property {string} commandName The name of the interaction's application command + * @property {User} user The user that invoked the interaction + */ + + if (data.interaction) { + /** + * Partial data of the interaction that this message is a reply to + * @type {?MessageInteraction} + */ + this.interaction = { + id: data.interaction.id, + type: InteractionTypes[data.interaction.type], + commandName: data.interaction.name, + user: this.client.users._add(data.interaction.user), + }; + } else { + this.interaction ??= null; + } + } + + /** + * Whether or not the structure has been deleted + * @type {boolean} + * @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091 + */ + get deleted() { + if (!deprecationEmittedForDeleted) { + deprecationEmittedForDeleted = true; + process.emitWarning( + 'Message#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + return deletedMessages.has(this); + } + + set deleted(value) { + if (!deprecationEmittedForDeleted) { + deprecationEmittedForDeleted = true; + process.emitWarning( + 'Message#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + if (value) deletedMessages.add(this); + else deletedMessages.delete(this); + } + + /** + * The channel that the message was sent in + * @type {TextChannel|DMChannel|NewsChannel|ThreadChannel} + * @readonly + */ + get channel() { + return this.client.channels.resolve(this.channelId); + } + + /** + * Whether or not this message is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return typeof this.content !== 'string' || !this.author; + } + + /** + * Represents the author of the message as a guild member. + * Only available if the message comes from a guild where the author is still a member + * @type {?GuildMember} + * @readonly + */ + get member() { + return this.guild?.members.resolve(this.author) ?? null; + } + + /** + * The time the message was sent at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The time the message was last edited at (if applicable) + * @type {?Date} + * @readonly + */ + get editedAt() { + return this.editedTimestamp ? new Date(this.editedTimestamp) : null; + } + + /** + * The guild the message was sent in (if in a guild channel) + * @type {?Guild} + * @readonly + */ + get guild() { + return this.client.guilds.resolve(this.guildId) ?? this.channel?.guild ?? null; + } + + /** + * Whether this message has a thread associated with it + * @type {boolean} + * @readonly + */ + get hasThread() { + return this.flags.has(MessageFlags.FLAGS.HAS_THREAD); + } + + /** + * The thread started by this message + * This property is not suitable for checking whether a message has a thread, + * use {@link Message#hasThread} instead. + * @type {?ThreadChannel} + * @readonly + */ + get thread() { + return this.channel?.threads?.resolve(this.id) ?? null; + } + + /** + * The URL to jump to this message + * @type {string} + * @readonly + */ + get url() { + return `https://discord.com/channels/${this.guildId ?? '@me'}/${this.channelId}/${this.id}`; + } + + /** + * The message contents with all mentions replaced by the equivalent text. + * If mentions cannot be resolved to a name, the relevant mention in the message content will not be converted. + * @type {?string} + * @readonly + */ + get cleanContent() { + // eslint-disable-next-line eqeqeq + return this.content != null ? Util.cleanContent(this.content, this.channel) : null; + } + + /** + * Creates a reaction collector. + * @param {ReactionCollectorOptions} [options={}] Options to send to the collector + * @returns {ReactionCollector} + * @example + * // Create a reaction collector + * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId'; + * const collector = message.createReactionCollector({ filter, time: 15_000 }); + * collector.on('collect', r => console.log(`Collected ${r.emoji.name}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createReactionCollector(options = {}) { + return new ReactionCollector(this, options); + } + + /** + * An object containing the same properties as CollectorOptions, but a few more: + * @typedef {ReactionCollectorOptions} AwaitReactionsOptions + * @property {string[]} [errors] Stop/end reasons that cause the promise to reject + */ + + /** + * Similar to createReactionCollector but in promise form. + * Resolves with a collection of reactions that pass the specified filter. + * @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise>} + * @example + * // Create a reaction collector + * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId' + * message.awaitReactions({ filter, time: 15_000 }) + * .then(collected => console.log(`Collected ${collected.size} reactions`)) + * .catch(console.error); + */ + awaitReactions(options = {}) { + return new Promise((resolve, reject) => { + const collector = this.createReactionCollector(options); + collector.once('end', (reactions, reason) => { + if (options.errors?.includes(reason)) reject(reactions); + else resolve(reactions); + }); + }); + } + + /** + * @typedef {CollectorOptions} MessageComponentCollectorOptions + * @property {MessageComponentType} [componentType] The type of component to listen for + * @property {number} [max] The maximum total amount of interactions to collect + * @property {number} [maxComponents] The maximum number of components to collect + * @property {number} [maxUsers] The maximum number of users to interact + */ + + /** + * Creates a message component interaction collector. + * @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector + * @returns {InteractionCollector} + * @example + * // Create a message component interaction collector + * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId'; + * const collector = message.createMessageComponentCollector({ filter, time: 15_000 }); + * collector.on('collect', i => console.log(`Collected ${i.customId}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createMessageComponentCollector(options = {}) { + return new InteractionCollector(this.client, { + ...options, + interactionType: InteractionTypes.MESSAGE_COMPONENT, + message: this, + }); + } + + /** + * An object containing the same properties as CollectorOptions, but a few more: + * @typedef {Object} AwaitMessageComponentOptions + * @property {CollectorFilter} [filter] The filter applied to this collector + * @property {number} [time] Time to wait for an interaction before rejecting + * @property {MessageComponentType} [componentType] The type of component interaction to collect + */ + + /** + * Collects a single component interaction that passes the filter. + * The Promise will reject if the time expires. + * @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector + * @returns {Promise} + * @example + * // Collect a message component interaction + * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId'; + * message.awaitMessageComponent({ filter, time: 15_000 }) + * .then(interaction => console.log(`${interaction.customId} was clicked!`)) + * .catch(console.error); + */ + awaitMessageComponent(options = {}) { + const _options = { ...options, max: 1 }; + return new Promise((resolve, reject) => { + const collector = this.createMessageComponentCollector(_options); + collector.once('end', (interactions, reason) => { + const interaction = interactions.first(); + if (interaction) resolve(interaction); + else reject(new Error('INTERACTION_COLLECTOR_ERROR', reason)); + }); + }); + } + + /** + * Whether the message is editable by the client user + * @type {boolean} + * @readonly + */ + get editable() { + const precheck = Boolean( + this.author.id === this.client.user.id && !deletedMessages.has(this) && (!this.guild || this.channel?.viewable), + ); + // Regardless of permissions thread messages cannot be edited if + // the thread is locked. + if (this.channel?.isThread()) { + return precheck && !this.channel.locked; + } + return precheck; + } + + /** + * Whether the message is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + if (deletedMessages.has(this)) { + return false; + } + if (!this.guild) { + return this.author.id === this.client.user.id; + } + // DMChannel does not have viewable property, so check viewable after proved that message is on a guild. + if (!this.channel?.viewable) { + return false; + } + + const permissions = this.channel?.permissionsFor(this.client.user); + if (!permissions) return false; + // This flag allows deleting even if timed out + if (permissions.has(Permissions.FLAGS.ADMINISTRATOR, false)) return true; + + return Boolean( + this.author.id === this.client.user.id || + (permissions.has(Permissions.FLAGS.MANAGE_MESSAGES, false) && + this.guild.me.communicationDisabledUntilTimestamp < Date.now()), + ); + } + + /** + * Whether the message is pinnable by the client user + * @type {boolean} + * @readonly + */ + get pinnable() { + const { channel } = this; + return Boolean( + !this.system && + !deletedMessages.has(this) && + (!this.guild || + (channel?.viewable && + channel?.permissionsFor(this.client.user)?.has(Permissions.FLAGS.MANAGE_MESSAGES, false))), + ); + } + + /** + * Fetches the Message this crosspost/reply/pin-add references, if available to the client + * @returns {Promise} + */ + async fetchReference() { + if (!this.reference) throw new Error('MESSAGE_REFERENCE_MISSING'); + const { channelId, messageId } = this.reference; + const channel = this.client.channels.resolve(channelId); + if (!channel) throw new Error('GUILD_CHANNEL_RESOLVE'); + const message = await channel.messages.fetch(messageId); + return message; + } + + /** + * Whether the message is crosspostable by the client user + * @type {boolean} + * @readonly + */ + get crosspostable() { + const bitfield = + Permissions.FLAGS.SEND_MESSAGES | + (this.author.id === this.client.user.id ? Permissions.defaultBit : Permissions.FLAGS.MANAGE_MESSAGES); + const { channel } = this; + return Boolean( + channel?.type === 'GUILD_NEWS' && + !this.flags.has(MessageFlags.FLAGS.CROSSPOSTED) && + this.type === 'DEFAULT' && + channel.viewable && + channel.permissionsFor(this.client.user)?.has(bitfield, false) && + !deletedMessages.has(this), + ); + } + + /** + * Options that can be passed into {@link Message#edit}. + * @typedef {Object} MessageEditOptions + * @property {?string} [content] Content to be edited + * @property {MessageEmbed[]|APIEmbed[]} [embeds] Embeds to be added/edited + * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content + * @property {MessageFlags} [flags] Which flags to set for the message. Only `SUPPRESS_EMBEDS` can be edited. + * @property {MessageAttachment[]} [attachments] An array of attachments to keep, + * all attachments will be kept if omitted + * @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to add to the message + * @property {MessageActionRow[]|MessageActionRowOptions[]} [components] + * Action rows containing interactive components for the message (buttons, select menus) + */ + + /** + * Edits the content of the message. + * @param {string|MessagePayload|MessageEditOptions} options The options to provide + * @returns {Promise} + * @example + * // Update the content of a message + * message.edit('This is my new content!') + * .then(msg => console.log(`Updated the content of a message to ${msg.content}`)) + * .catch(console.error); + */ + edit(options) { + if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED')); + return this.channel.messages.edit(this, options); + } + + /** + * Publishes a message in an announcement channel to all channels following it. + * @returns {Promise} + * @example + * // Crosspost a message + * if (message.channel.type === 'GUILD_NEWS') { + * message.crosspost() + * .then(() => console.log('Crossposted message')) + * .catch(console.error); + * } + */ + crosspost() { + if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED')); + return this.channel.messages.crosspost(this.id); + } + + /** + * Pins this message to the channel's pinned messages. + * @returns {Promise} + * @example + * // Pin a message + * message.pin() + * .then(console.log) + * .catch(console.error) + */ + async pin() { + if (!this.channel) throw new Error('CHANNEL_NOT_CACHED'); + await this.channel.messages.pin(this.id); + return this; + } + + /** + * Unpins this message from the channel's pinned messages. + * @returns {Promise} + * @example + * // Unpin a message + * message.unpin() + * .then(console.log) + * .catch(console.error) + */ + async unpin() { + if (!this.channel) throw new Error('CHANNEL_NOT_CACHED'); + await this.channel.messages.unpin(this.id); + return this; + } + + /** + * Adds a reaction to the message. + * @param {EmojiIdentifierResolvable} emoji The emoji to react with + * @returns {Promise} + * @example + * // React to a message with a unicode emoji + * message.react('🤔') + * .then(console.log) + * .catch(console.error); + * @example + * // React to a message with a custom emoji + * message.react(message.guild.emojis.cache.get('123456789012345678')) + * .then(console.log) + * .catch(console.error); + */ + async react(emoji) { + if (!this.channel) throw new Error('CHANNEL_NOT_CACHED'); + await this.channel.messages.react(this.id, emoji); + + return this.client.actions.MessageReactionAdd.handle( + { + user: this.client.user, + channel: this.channel, + message: this, + emoji: Util.resolvePartialEmoji(emoji), + }, + true, + ).reaction; + } + + /** + * Deletes the message. + * @returns {Promise} + * @example + * // Delete a message + * message.delete() + * .then(msg => console.log(`Deleted message from ${msg.author.username}`)) + * .catch(console.error); + */ + async delete() { + if (!this.channel) throw new Error('CHANNEL_NOT_CACHED'); + await this.channel.messages.delete(this.id); + return this; + } + + /** + * Options provided when sending a message as an inline reply. + * @typedef {BaseMessageOptions} ReplyMessageOptions + * @property {boolean} [failIfNotExists=true] Whether to error if the referenced message + * does not exist (creates a standard message in this case when false) + * @property {StickerResolvable[]} [stickers=[]] Stickers to send in the message + */ + + /** + * Send an inline reply to this message. + * @param {string|MessagePayload|ReplyMessageOptions} options The options to provide + * @returns {Promise} + * @example + * // Reply to a message + * message.reply('This is a reply!') + * .then(() => console.log(`Replied to message "${message.content}"`)) + * .catch(console.error); + */ + reply(options) { + if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED')); + let data; + + if (options instanceof MessagePayload) { + data = options; + } else { + data = MessagePayload.create(this, options, { + reply: { + messageReference: this, + failIfNotExists: options?.failIfNotExists ?? this.client.options.failIfNotExists, + }, + }); + } + return this.channel.send(data); + } + + /** + * A number that is allowed to be the duration (in minutes) of inactivity after which a thread is automatically + * archived. This can be: + * * `60` (1 hour) + * * `1440` (1 day) + * * `4320` (3 days) This is only available when the guild has the `THREE_DAY_THREAD_ARCHIVE` feature. + * * `10080` (7 days) This is only available when the guild has the `SEVEN_DAY_THREAD_ARCHIVE` feature. + * * `'MAX'` Based on the guild's features + * @typedef {number|string} ThreadAutoArchiveDuration + */ + + /** + * Options for starting a thread on a message. + * @typedef {Object} StartThreadOptions + * @property {string} name The name of the new thread + * @property {ThreadAutoArchiveDuration} [autoArchiveDuration=this.channel.defaultAutoArchiveDuration] The amount of + * time (in minutes) after which the thread should automatically archive in case of no recent activity + * @property {string} [reason] Reason for creating the thread + * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the thread in seconds + */ + + /** + * Create a new public thread from this message + * @see ThreadManager#create + * @param {StartThreadOptions} [options] Options for starting a thread on this message + * @returns {Promise} + */ + startThread(options = {}) { + if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED')); + if (!['GUILD_TEXT', 'GUILD_NEWS'].includes(this.channel.type)) { + return Promise.reject(new Error('MESSAGE_THREAD_PARENT')); + } + if (this.hasThread) { + return Promise.reject(new Error('MESSAGE_EXISTING_THREAD')); + } + return this.channel.threads.create({ ...options, startMessage: this }); + } + + /** + * Fetch this message. + * @param {boolean} [force=true] Whether to skip the cache check and request the API + * @returns {Promise} + */ + fetch(force = true) { + if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED')); + return this.channel.messages.fetch(this.id, { force }); + } + + /** + * Fetches the webhook used to create this message. + * @returns {Promise} + */ + fetchWebhook() { + if (!this.webhookId) return Promise.reject(new Error('WEBHOOK_MESSAGE')); + if (this.webhookId === this.applicationId) { + return Promise.reject(new Error('WEBHOOK_APPLICATION')); + } + return this.client.fetchWebhook(this.webhookId); + } + + /** + * Suppresses or unsuppresses embeds on a message. + * @param {boolean} [suppress=true] If the embeds should be suppressed or not + * @returns {Promise} + */ + suppressEmbeds(suppress = true) { + const flags = new MessageFlags(this.flags.bitfield); + + if (suppress) { + flags.add(MessageFlags.FLAGS.SUPPRESS_EMBEDS); + } else { + flags.remove(MessageFlags.FLAGS.SUPPRESS_EMBEDS); + } + + return this.edit({ flags }); + } + + /** + * Removes the attachments from this message. + * @returns {Promise} + */ + removeAttachments() { + return this.edit({ attachments: [] }); + } + + /** + * Resolves a component by a custom id. + * @param {string} customId The custom id to resolve against + * @returns {?MessageActionRowComponent} + */ + resolveComponent(customId) { + return this.components.flatMap(row => row.components).find(component => component.customId === customId) ?? null; + } + + /** + * Used mainly internally. Whether two messages are identical in properties. If you want to compare messages + * without checking all the properties, use `message.id === message2.id`, which is much more efficient. This + * method allows you to see if there are differences in content, embeds, attachments, nonce and tts properties. + * @param {Message} message The message to compare it to + * @param {APIMessage} rawData Raw data passed through the WebSocket about this message + * @returns {boolean} + */ + equals(message, rawData) { + if (!message) return false; + const embedUpdate = !message.author && !message.attachments; + if (embedUpdate) { + return this.id === message.id && this.embeds.length === message.embeds.length; + } + + let equal = + this.id === message.id && + this.author.id === message.author.id && + this.content === message.content && + this.tts === message.tts && + this.nonce === message.nonce && + this.embeds.length === message.embeds.length && + this.attachments.length === message.attachments.length; + + if (equal && rawData) { + equal = + this.mentions.everyone === message.mentions.everyone && + this.createdTimestamp === new Date(rawData.timestamp).getTime() && + this.editedTimestamp === new Date(rawData.edited_timestamp).getTime(); + } + + return equal; + } + + /** + * Whether this message is from a guild. + * @returns {boolean} + */ + inGuild() { + return Boolean(this.guildId); + } + + /** + * When concatenated with a string, this automatically concatenates the message's content instead of the object. + * @returns {string} + * @example + * // Logs: Message: This is a message! + * console.log(`Message: ${message}`); + */ + toString() { + return this.content; + } + + toJSON() { + return super.toJSON({ + channel: 'channelId', + author: 'authorId', + groupActivityApplication: 'groupActivityApplicationId', + guild: 'guildId', + cleanContent: true, + member: false, + reactions: false, + }); + } + // Added + /** + * Click specific button [Suggestion: Dux#2925] + * @param {String} buttonID Button ID + * @returns {Promise} + */ + async clickButton(buttonID) { + if (typeof buttonID !== 'string') { + throw new TypeError('BUTTON_ID_NOT_STRING'); + } + if (!this.components[0]) throw new TypeError('MESSAGE_NO_COMPONENTS'); + let button; + await Promise.all( + this.components.map(async row => { + await Promise.all( + row.components.map(async interactionComponent => { + if (interactionComponent.type == 'BUTTON' && interactionComponent.customId == buttonID) { + button = interactionComponent; + } + }), + ); + }), + ); + if (!button) throw new TypeError('BUTTON_NOT_FOUND'); + else button.click(this); + } + /** + * Select specific menu or First Menu + * @param {String|Array} menuID Select Menu specific id or auto select first Menu + * @param {Array} options Menu Options + */ + async selectMenu(menuID, options = []) { + if (!this.components[0]) throw new TypeError('MESSAGE_NO_COMPONENTS'); + let menuFirst; + let menuCorrect; + let menuCount = 0; + await Promise.all( + this.components.map(async row => { + const firstElement = row.components[0]; // Because 1 row has only 1 menu; + if (firstElement.type == 'SELECT_MENU') { + menuCount++; + if (firstElement.customId == menuID) { + menuCorrect = firstElement; + } else if (!menuFirst) { + menuFirst = firstElement; + } + } + }), + ); + if (menuCount == 0) throw new TypeError('MENU_NOT_FOUND'); + if (!menuCorrect) { + if (menuCount == 1) menuCorrect = menuFirst; + else if (typeof menuID !== 'string') throw new TypeError('MENU_ID_NOT_STRING'); + else throw new TypeError('MENU_ID_NOT_FOUND'); + } + menuCorrect.select(this, Array.isArray(menuID) ? menuID : options); + } + // + /** + * Send context Menu v2 + * @param {DiscordBotID} botID Bot id + * @param {String} commandName Command name in Context Menu + * @returns {Promise} + */ + async contextMenu(botID, commandName) { + if (!botID) throw new Error('Bot ID is required'); + const user = await this.client.users.fetch(botID).catch(() => {}); + if (!user || !user.bot || !user.applications) { + throw new Error('BotID is not a bot or does not have an application slash command'); + } + if (!commandName || typeof commandName !== 'string') { + throw new Error('Command name is required'); + } + const listApplication = + user.applications.cache.size == 0 ? await user.applications.fetch() : user.applications.cache; + let contextCMD; + await Promise.all( + listApplication.map(async application => { + if (commandName == application.name && application.type !== 'CHAT_INPUT') { + contextCMD = application; + } + }), + ); + if (!contextCMD) { + throw new Error( + `Command ${commandName} is not found\nList command avalible: ${listApplication + .filter(a => a.type !== 'CHAT_INPUT') + .map(a => a.name) + .join(', ')}`, + ); + } + return contextCMD.sendContextMenu(this, true); + } +} + +exports.Message = Message; +exports.deletedMessages = deletedMessages; diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index 6a95357..c44b32a 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -1,101 +1,101 @@ -'use strict'; - -const BaseMessageComponent = require('./BaseMessageComponent'); -const { MessageComponentTypes } = require('../util/Constants'); - -/** - * Represents an action row containing message components. - * @extends {BaseMessageComponent} - */ -class MessageActionRow extends BaseMessageComponent { - /** - * Components that can be placed in an action row - * * MessageButton - * * MessageSelectMenu - * @typedef {MessageButton|MessageSelectMenu} MessageActionRowComponent - */ - - /** - * Options for components that can be placed in an action row - * * MessageButtonOptions - * * MessageSelectMenuOptions - * @typedef {MessageButtonOptions|MessageSelectMenuOptions} MessageActionRowComponentOptions - */ - - /** - * Data that can be resolved into components that can be placed in an action row - * * MessageActionRowComponent - * * MessageActionRowComponentOptions - * @typedef {MessageActionRowComponent|MessageActionRowComponentOptions} MessageActionRowComponentResolvable - */ - - /** - * @typedef {BaseMessageComponentOptions} MessageActionRowOptions - * @property {MessageActionRowComponentResolvable[]} [components] - * The components to place in this action row - */ - - /** - * @param {MessageActionRow|MessageActionRowOptions} [data={}] MessageActionRow to clone or raw data - * @param {Client} [client] The client constructing this MessageActionRow, if provided - */ - constructor(data = {}, client = null) { - super({ type: 'ACTION_ROW' }); - - /** - * The components in this action row - * @type {MessageActionRowComponent[]} - */ - this.components = data.components?.map(c => BaseMessageComponent.create(c, client)) ?? []; - } - - /** - * Adds components to the action row. - * @param {...MessageActionRowComponentResolvable[]} components The components to add - * @returns {MessageActionRow} - */ - addComponents(...components) { - this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c))); - return this; - } - - /** - * Sets the components of the action row. - * @param {...MessageActionRowComponentResolvable[]} components The components to set - * @returns {MessageActionRow} - */ - setComponents(...components) { - this.spliceComponents(0, this.components.length, components); - return this; - } - - /** - * Removes, replaces, and inserts components in the action row. - * @param {number} index The index to start at - * @param {number} deleteCount The number of components to remove - * @param {...MessageActionRowComponentResolvable[]} [components] The replacing components - * @returns {MessageActionRow} - */ - spliceComponents(index, deleteCount, ...components) { - this.components.splice(index, deleteCount, ...components.flat(Infinity).map(c => BaseMessageComponent.create(c))); - return this; - } - - /** - * Transforms the action row to a plain object. - * @returns {APIMessageComponent} The raw data of this action row - */ - toJSON() { - return { - components: this.components.map(c => c.toJSON()), - type: MessageComponentTypes[this.type], - }; - } -} - -module.exports = MessageActionRow; - -/** - * @external APIMessageComponent - * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object} - */ +'use strict'; + +const BaseMessageComponent = require('./BaseMessageComponent'); +const { MessageComponentTypes } = require('../util/Constants'); + +/** + * Represents an action row containing message components. + * @extends {BaseMessageComponent} + */ +class MessageActionRow extends BaseMessageComponent { + /** + * Components that can be placed in an action row + * * MessageButton + * * MessageSelectMenu + * @typedef {MessageButton|MessageSelectMenu} MessageActionRowComponent + */ + + /** + * Options for components that can be placed in an action row + * * MessageButtonOptions + * * MessageSelectMenuOptions + * @typedef {MessageButtonOptions|MessageSelectMenuOptions} MessageActionRowComponentOptions + */ + + /** + * Data that can be resolved into components that can be placed in an action row + * * MessageActionRowComponent + * * MessageActionRowComponentOptions + * @typedef {MessageActionRowComponent|MessageActionRowComponentOptions} MessageActionRowComponentResolvable + */ + + /** + * @typedef {BaseMessageComponentOptions} MessageActionRowOptions + * @property {MessageActionRowComponentResolvable[]} [components] + * The components to place in this action row + */ + + /** + * @param {MessageActionRow|MessageActionRowOptions} [data={}] MessageActionRow to clone or raw data + * @param {Client} [client] The client constructing this MessageActionRow, if provided + */ + constructor(data = {}, client = null) { + super({ type: 'ACTION_ROW' }); + + /** + * The components in this action row + * @type {MessageActionRowComponent[]} + */ + this.components = data.components?.map(c => BaseMessageComponent.create(c, client)) ?? []; + } + + /** + * Adds components to the action row. + * @param {...MessageActionRowComponentResolvable[]} components The components to add + * @returns {MessageActionRow} + */ + addComponents(...components) { + this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c))); + return this; + } + + /** + * Sets the components of the action row. + * @param {...MessageActionRowComponentResolvable[]} components The components to set + * @returns {MessageActionRow} + */ + setComponents(...components) { + this.spliceComponents(0, this.components.length, components); + return this; + } + + /** + * Removes, replaces, and inserts components in the action row. + * @param {number} index The index to start at + * @param {number} deleteCount The number of components to remove + * @param {...MessageActionRowComponentResolvable[]} [components] The replacing components + * @returns {MessageActionRow} + */ + spliceComponents(index, deleteCount, ...components) { + this.components.splice(index, deleteCount, ...components.flat(Infinity).map(c => BaseMessageComponent.create(c))); + return this; + } + + /** + * Transforms the action row to a plain object. + * @returns {APIMessageComponent} The raw data of this action row + */ + toJSON() { + return { + components: this.components.map(c => c.toJSON()), + type: MessageComponentTypes[this.type], + }; + } +} + +module.exports = MessageActionRow; + +/** + * @external APIMessageComponent + * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object} + */ diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index c21d98f..cd088a8 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -1,193 +1,191 @@ -'use strict'; - -const BaseMessageComponent = require('./BaseMessageComponent'); -const { Message } = require('./Message'); -const { RangeError } = require('../errors'); -const { MessageButtonStyles, MessageComponentTypes } = require('../util/Constants'); -const Util = require('../util/Util'); - -/** - * Represents a button message component. - * @extends {BaseMessageComponent} - */ -class MessageButton extends BaseMessageComponent { - /** - * @typedef {BaseMessageComponentOptions} MessageButtonOptions - * @property {string} [label] The text to be displayed on this button - * @property {string} [customId] A unique string to be sent in the interaction when clicked - * @property {MessageButtonStyleResolvable} [style] The style of this button - * @property {EmojiIdentifierResolvable} [emoji] The emoji to be displayed to the left of the text - * @property {string} [url] Optional URL for link-style buttons - * @property {boolean} [disabled=false] Disables the button to prevent interactions - */ - - /** - * @param {MessageButton|MessageButtonOptions} [data={}] MessageButton to clone or raw data - */ - constructor(data = {}) { - super({ type: 'BUTTON' }); - - this.setup(data); - } - - setup(data) { - /** - * The text to be displayed on this button - * @type {?string} - */ - this.label = data.label ?? null; - - /** - * A unique string to be sent in the interaction when clicked - * @type {?string} - */ - this.customId = data.custom_id ?? data.customId ?? null; - - /** - * The style of this button - * @type {?MessageButtonStyle} - */ - this.style = data.style ? MessageButton.resolveStyle(data.style) : null; - - /** - * Emoji for this button - * @type {?RawEmoji} - */ - this.emoji = data.emoji ? Util.resolvePartialEmoji(data.emoji) : null; - - /** - * The URL this button links to, if it is a Link style button - * @type {?string} - */ - this.url = data.url ?? null; - - /** - * Whether this button is currently disabled - * @type {boolean} - */ - this.disabled = data.disabled ?? false; - } - - /** - * Sets the custom id for this button - * @param {string} customId A unique string to be sent in the interaction when clicked - * @returns {MessageButton} - */ - setCustomId(customId) { - this.customId = Util.verifyString(customId, RangeError, 'BUTTON_CUSTOM_ID'); - return this; - } - - /** - * Sets the interactive status of the button - * @param {boolean} [disabled=true] Whether this button should be disabled - * @returns {MessageButton} - */ - setDisabled(disabled = true) { - this.disabled = disabled; - return this; - } - - /** - * Set the emoji of this button - * @param {EmojiIdentifierResolvable} emoji The emoji to be displayed on this button - * @returns {MessageButton} - */ - setEmoji(emoji) { - this.emoji = Util.resolvePartialEmoji(emoji); - return this; - } - - /** - * Sets the label of this button - * @param {string} label The text to be displayed on this button - * @returns {MessageButton} - */ - setLabel(label) { - this.label = Util.verifyString(label, RangeError, 'BUTTON_LABEL'); - return this; - } - - /** - * Sets the style of this button - * @param {MessageButtonStyleResolvable} style The style of this button - * @returns {MessageButton} - */ - setStyle(style) { - this.style = MessageButton.resolveStyle(style); - return this; - } - - /** - * Sets the URL of this button. - * MessageButton#style must be LINK when setting a URL - * @param {string} url The URL of this button - * @returns {MessageButton} - */ - setURL(url) { - this.url = Util.verifyString(url, RangeError, 'BUTTON_URL'); - return this; - } - - /** - * Transforms the button to a plain object. - * @returns {APIMessageButton} The raw data of this button - */ - toJSON() { - return { - custom_id: this.customId, - disabled: this.disabled, - emoji: this.emoji, - label: this.label, - style: MessageButtonStyles[this.style], - type: MessageComponentTypes[this.type], - url: this.url, - }; - } - - /** - * Data that can be resolved to a MessageButtonStyle. This can be - * * MessageButtonStyle - * * number - * @typedef {number|MessageButtonStyle} MessageButtonStyleResolvable - */ - - /** - * Resolves the style of a button - * @param {MessageButtonStyleResolvable} style The style to resolve - * @returns {MessageButtonStyle} - * @private - */ - static resolveStyle(style) { - return typeof style === 'string' ? style : MessageButtonStyles[style]; - } - // Patch Click - /** - * Click the button - * @param {Message} message Discord Message - * @returns {boolean} - */ - async click(message) { - if (!message instanceof Message) throw new Error("[UNKNOWN_MESSAGE] Please pass a valid Message"); - if (!this.customId || this.style == 5 || this.disabled) return false; // Button URL, Disabled - await message.client.api.interactions.post( - { - data: { - type: 3, // ? - guild_id: message.guild?.id ?? null, // In DMs - channel_id: message.channel.id, - message_id: message.id, - application_id: message.author.id, - session_id: message.client.session_id, - data: { - component_type: 2, // Button - custom_id: this.customId - }, - } - } - ) - return true; - } -} - -module.exports = MessageButton; +'use strict'; + +const BaseMessageComponent = require('./BaseMessageComponent'); +const { Message } = require('./Message'); +const { RangeError } = require('../errors'); +const { MessageButtonStyles, MessageComponentTypes } = require('../util/Constants'); +const Util = require('../util/Util'); + +/** + * Represents a button message component. + * @extends {BaseMessageComponent} + */ +class MessageButton extends BaseMessageComponent { + /** + * @typedef {BaseMessageComponentOptions} MessageButtonOptions + * @property {string} [label] The text to be displayed on this button + * @property {string} [customId] A unique string to be sent in the interaction when clicked + * @property {MessageButtonStyleResolvable} [style] The style of this button + * @property {EmojiIdentifierResolvable} [emoji] The emoji to be displayed to the left of the text + * @property {string} [url] Optional URL for link-style buttons + * @property {boolean} [disabled=false] Disables the button to prevent interactions + */ + + /** + * @param {MessageButton|MessageButtonOptions} [data={}] MessageButton to clone or raw data + */ + constructor(data = {}) { + super({ type: 'BUTTON' }); + + this.setup(data); + } + + setup(data) { + /** + * The text to be displayed on this button + * @type {?string} + */ + this.label = data.label ?? null; + + /** + * A unique string to be sent in the interaction when clicked + * @type {?string} + */ + this.customId = data.custom_id ?? data.customId ?? null; + + /** + * The style of this button + * @type {?MessageButtonStyle} + */ + this.style = data.style ? MessageButton.resolveStyle(data.style) : null; + + /** + * Emoji for this button + * @type {?RawEmoji} + */ + this.emoji = data.emoji ? Util.resolvePartialEmoji(data.emoji) : null; + + /** + * The URL this button links to, if it is a Link style button + * @type {?string} + */ + this.url = data.url ?? null; + + /** + * Whether this button is currently disabled + * @type {boolean} + */ + this.disabled = data.disabled ?? false; + } + + /** + * Sets the custom id for this button + * @param {string} customId A unique string to be sent in the interaction when clicked + * @returns {MessageButton} + */ + setCustomId(customId) { + this.customId = Util.verifyString(customId, RangeError, 'BUTTON_CUSTOM_ID'); + return this; + } + + /** + * Sets the interactive status of the button + * @param {boolean} [disabled=true] Whether this button should be disabled + * @returns {MessageButton} + */ + setDisabled(disabled = true) { + this.disabled = disabled; + return this; + } + + /** + * Set the emoji of this button + * @param {EmojiIdentifierResolvable} emoji The emoji to be displayed on this button + * @returns {MessageButton} + */ + setEmoji(emoji) { + this.emoji = Util.resolvePartialEmoji(emoji); + return this; + } + + /** + * Sets the label of this button + * @param {string} label The text to be displayed on this button + * @returns {MessageButton} + */ + setLabel(label) { + this.label = Util.verifyString(label, RangeError, 'BUTTON_LABEL'); + return this; + } + + /** + * Sets the style of this button + * @param {MessageButtonStyleResolvable} style The style of this button + * @returns {MessageButton} + */ + setStyle(style) { + this.style = MessageButton.resolveStyle(style); + return this; + } + + /** + * Sets the URL of this button. + * MessageButton#style must be LINK when setting a URL + * @param {string} url The URL of this button + * @returns {MessageButton} + */ + setURL(url) { + this.url = Util.verifyString(url, RangeError, 'BUTTON_URL'); + return this; + } + + /** + * Transforms the button to a plain object. + * @returns {APIMessageButton} The raw data of this button + */ + toJSON() { + return { + custom_id: this.customId, + disabled: this.disabled, + emoji: this.emoji, + label: this.label, + style: MessageButtonStyles[this.style], + type: MessageComponentTypes[this.type], + url: this.url, + }; + } + + /** + * Data that can be resolved to a MessageButtonStyle. This can be + * * MessageButtonStyle + * * number + * @typedef {number|MessageButtonStyle} MessageButtonStyleResolvable + */ + + /** + * Resolves the style of a button + * @param {MessageButtonStyleResolvable} style The style to resolve + * @returns {MessageButtonStyle} + * @private + */ + static resolveStyle(style) { + return typeof style === 'string' ? style : MessageButtonStyles[style]; + } + // Patch Click + /** + * Click the button + * @param {Message} message Discord Message + * @returns {boolean} + */ + async click(message) { + if (!message instanceof Message) throw new Error('[UNKNOWN_MESSAGE] Please pass a valid Message'); + if (!this.customId || this.style == 5 || this.disabled) return false; // Button URL, Disabled + await message.client.api.interactions.post({ + data: { + type: 3, // ? + guild_id: message.guild?.id ?? null, // In DMs + channel_id: message.channel.id, + message_id: message.id, + application_id: message.author.id, + session_id: message.client.session_id, + data: { + component_type: 2, // Button + custom_id: this.customId, + }, + }, + }); + return true; + } +} + +module.exports = MessageButton; diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 4b75efb..9b935f9 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -1,235 +1,235 @@ -'use strict'; - -const { Collection } = require('@discordjs/collection'); -const { ChannelTypes } = require('../util/Constants'); -const Util = require('../util/Util'); - -/** - * Keeps track of mentions in a {@link Message}. - */ -class MessageMentions { - constructor(message, users, roles, everyone, crosspostedChannels, repliedUser) { - /** - * The client the message is from - * @type {Client} - * @readonly - */ - Object.defineProperty(this, 'client', { value: message.client }); - - /** - * The guild the message is in - * @type {?Guild} - * @readonly - */ - Object.defineProperty(this, 'guild', { value: message.guild }); - - /** - * The initial message content - * @type {string} - * @readonly - * @private - */ - Object.defineProperty(this, '_content', { value: message.content }); - - /** - * Whether `@everyone` or `@here` were mentioned - * @type {boolean} - */ - this.everyone = Boolean(everyone); - - if (users) { - if (users instanceof Collection) { - /** - * Any users that were mentioned - * Order as received from the API, not as they appear in the message content - * @type {Collection} - */ - this.users = new Collection(users); - } else { - this.users = new Collection(); - for (const mention of users) { - if (mention.member && message.guild) { - message.guild.members._add(Object.assign(mention.member, { user: mention })); - } - const user = message.client.users._add(mention); - this.users.set(user.id, user); - } - } - } else { - this.users = new Collection(); - } - - if (roles instanceof Collection) { - /** - * Any roles that were mentioned - * Order as received from the API, not as they appear in the message content - * @type {Collection} - */ - this.roles = new Collection(roles); - } else if (roles) { - this.roles = new Collection(); - const guild = message.guild; - if (guild) { - for (const mention of roles) { - const role = guild.roles.cache.get(mention); - if (role) this.roles.set(role.id, role); - } - } - } else { - this.roles = new Collection(); - } - - /** - * Cached members for {@link MessageMentions#members} - * @type {?Collection} - * @private - */ - this._members = null; - - /** - * Cached channels for {@link MessageMentions#channels} - * @type {?Collection} - * @private - */ - this._channels = null; - - /** - * Crossposted channel data. - * @typedef {Object} CrosspostedChannel - * @property {Snowflake} channelId The mentioned channel's id - * @property {Snowflake} guildId The id of the guild that has the channel - * @property {ChannelType} type The channel's type - * @property {string} name The channel's name - */ - - if (crosspostedChannels) { - if (crosspostedChannels instanceof Collection) { - /** - * A collection of crossposted channels - * Order as received from the API, not as they appear in the message content - * @type {Collection} - */ - this.crosspostedChannels = new Collection(crosspostedChannels); - } else { - this.crosspostedChannels = new Collection(); - const channelTypes = Object.keys(ChannelTypes); - for (const d of crosspostedChannels) { - const type = channelTypes[d.type]; - this.crosspostedChannels.set(d.id, { - channelId: d.id, - guildId: d.guild_id, - type: type ?? 'UNKNOWN', - name: d.name, - }); - } - } - } else { - this.crosspostedChannels = new Collection(); - } - - /** - * The author of the message that this message is a reply to - * @type {?User} - */ - this.repliedUser = repliedUser ? this.client.users._add(repliedUser) : null; - } - - /** - * Any members that were mentioned (only in {@link Guild}s) - * Order as received from the API, not as they appear in the message content - * @type {?Collection} - * @readonly - */ - get members() { - if (this._members) return this._members; - if (!this.guild) return null; - this._members = new Collection(); - this.users.forEach(user => { - const member = this.guild.members.resolve(user); - if (member) this._members.set(member.user.id, member); - }); - return this._members; - } - - /** - * Any channels that were mentioned - * Order as they appear first in the message content - * @type {Collection} - * @readonly - */ - get channels() { - if (this._channels) return this._channels; - this._channels = new Collection(); - let matches; - while ((matches = this.constructor.CHANNELS_PATTERN.exec(this._content)) !== null) { - const chan = this.client.channels.cache.get(matches[1]); - if (chan) this._channels.set(chan.id, chan); - } - return this._channels; - } - - /** - * Options used to check for a mention. - * @typedef {Object} MessageMentionsHasOptions - * @property {boolean} [ignoreDirect=false] Whether to ignore direct mentions to the item - * @property {boolean} [ignoreRoles=false] Whether to ignore role mentions to a guild member - * @property {boolean} [ignoreEveryone=false] Whether to ignore everyone/here mentions - */ - - /** - * Checks if a user, guild member, role, or channel is mentioned. - * Takes into account user mentions, role mentions, and `@everyone`/`@here` mentions. - * @param {UserResolvable|RoleResolvable|ChannelResolvable} data The User/Role/Channel to check for - * @param {MessageMentionsHasOptions} [options] The options for the check - * @returns {boolean} - */ - has(data, { ignoreDirect = false, ignoreRoles = false, ignoreEveryone = false } = {}) { - if (!ignoreEveryone && this.everyone) return true; - const { GuildMember } = require('./GuildMember'); - if (!ignoreRoles && data instanceof GuildMember) { - for (const role of this.roles.values()) if (data.roles.cache.has(role.id)) return true; - } - - if (!ignoreDirect) { - const id = - this.guild?.roles.resolveId(data) ?? this.client.channels.resolveId(data) ?? this.client.users.resolveId(data); - - return typeof id === 'string' && (this.users.has(id) || this.channels.has(id) || this.roles.has(id)); - } - - return false; - } - - toJSON() { - return Util.flatten(this, { - members: true, - channels: true, - }); - } -} - -/** - * Regular expression that globally matches `@everyone` and `@here` - * @type {RegExp} - */ -MessageMentions.EVERYONE_PATTERN = /@(everyone|here)/g; - -/** - * Regular expression that globally matches user mentions like `<@81440962496172032>` - * @type {RegExp} - */ -MessageMentions.USERS_PATTERN = /<@!?(\d{17,19})>/g; - -/** - * Regular expression that globally matches role mentions like `<@&297577916114403338>` - * @type {RegExp} - */ -MessageMentions.ROLES_PATTERN = /<@&(\d{17,19})>/g; - -/** - * Regular expression that globally matches channel mentions like `<#222079895583457280>` - * @type {RegExp} - */ -MessageMentions.CHANNELS_PATTERN = /<#(\d{17,19})>/g; - -module.exports = MessageMentions; +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { ChannelTypes } = require('../util/Constants'); +const Util = require('../util/Util'); + +/** + * Keeps track of mentions in a {@link Message}. + */ +class MessageMentions { + constructor(message, users, roles, everyone, crosspostedChannels, repliedUser) { + /** + * The client the message is from + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: message.client }); + + /** + * The guild the message is in + * @type {?Guild} + * @readonly + */ + Object.defineProperty(this, 'guild', { value: message.guild }); + + /** + * The initial message content + * @type {string} + * @readonly + * @private + */ + Object.defineProperty(this, '_content', { value: message.content }); + + /** + * Whether `@everyone` or `@here` were mentioned + * @type {boolean} + */ + this.everyone = Boolean(everyone); + + if (users) { + if (users instanceof Collection) { + /** + * Any users that were mentioned + * Order as received from the API, not as they appear in the message content + * @type {Collection} + */ + this.users = new Collection(users); + } else { + this.users = new Collection(); + for (const mention of users) { + if (mention.member && message.guild) { + message.guild.members._add(Object.assign(mention.member, { user: mention })); + } + const user = message.client.users._add(mention); + this.users.set(user.id, user); + } + } + } else { + this.users = new Collection(); + } + + if (roles instanceof Collection) { + /** + * Any roles that were mentioned + * Order as received from the API, not as they appear in the message content + * @type {Collection} + */ + this.roles = new Collection(roles); + } else if (roles) { + this.roles = new Collection(); + const guild = message.guild; + if (guild) { + for (const mention of roles) { + const role = guild.roles.cache.get(mention); + if (role) this.roles.set(role.id, role); + } + } + } else { + this.roles = new Collection(); + } + + /** + * Cached members for {@link MessageMentions#members} + * @type {?Collection} + * @private + */ + this._members = null; + + /** + * Cached channels for {@link MessageMentions#channels} + * @type {?Collection} + * @private + */ + this._channels = null; + + /** + * Crossposted channel data. + * @typedef {Object} CrosspostedChannel + * @property {Snowflake} channelId The mentioned channel's id + * @property {Snowflake} guildId The id of the guild that has the channel + * @property {ChannelType} type The channel's type + * @property {string} name The channel's name + */ + + if (crosspostedChannels) { + if (crosspostedChannels instanceof Collection) { + /** + * A collection of crossposted channels + * Order as received from the API, not as they appear in the message content + * @type {Collection} + */ + this.crosspostedChannels = new Collection(crosspostedChannels); + } else { + this.crosspostedChannels = new Collection(); + const channelTypes = Object.keys(ChannelTypes); + for (const d of crosspostedChannels) { + const type = channelTypes[d.type]; + this.crosspostedChannels.set(d.id, { + channelId: d.id, + guildId: d.guild_id, + type: type ?? 'UNKNOWN', + name: d.name, + }); + } + } + } else { + this.crosspostedChannels = new Collection(); + } + + /** + * The author of the message that this message is a reply to + * @type {?User} + */ + this.repliedUser = repliedUser ? this.client.users._add(repliedUser) : null; + } + + /** + * Any members that were mentioned (only in {@link Guild}s) + * Order as received from the API, not as they appear in the message content + * @type {?Collection} + * @readonly + */ + get members() { + if (this._members) return this._members; + if (!this.guild) return null; + this._members = new Collection(); + this.users.forEach(user => { + const member = this.guild.members.resolve(user); + if (member) this._members.set(member.user.id, member); + }); + return this._members; + } + + /** + * Any channels that were mentioned + * Order as they appear first in the message content + * @type {Collection} + * @readonly + */ + get channels() { + if (this._channels) return this._channels; + this._channels = new Collection(); + let matches; + while ((matches = this.constructor.CHANNELS_PATTERN.exec(this._content)) !== null) { + const chan = this.client.channels.cache.get(matches[1]); + if (chan) this._channels.set(chan.id, chan); + } + return this._channels; + } + + /** + * Options used to check for a mention. + * @typedef {Object} MessageMentionsHasOptions + * @property {boolean} [ignoreDirect=false] Whether to ignore direct mentions to the item + * @property {boolean} [ignoreRoles=false] Whether to ignore role mentions to a guild member + * @property {boolean} [ignoreEveryone=false] Whether to ignore everyone/here mentions + */ + + /** + * Checks if a user, guild member, role, or channel is mentioned. + * Takes into account user mentions, role mentions, and `@everyone`/`@here` mentions. + * @param {UserResolvable|RoleResolvable|ChannelResolvable} data The User/Role/Channel to check for + * @param {MessageMentionsHasOptions} [options] The options for the check + * @returns {boolean} + */ + has(data, { ignoreDirect = false, ignoreRoles = false, ignoreEveryone = false } = {}) { + if (!ignoreEveryone && this.everyone) return true; + const { GuildMember } = require('./GuildMember'); + if (!ignoreRoles && data instanceof GuildMember) { + for (const role of this.roles.values()) if (data.roles.cache.has(role.id)) return true; + } + + if (!ignoreDirect) { + const id = + this.guild?.roles.resolveId(data) ?? this.client.channels.resolveId(data) ?? this.client.users.resolveId(data); + + return typeof id === 'string' && (this.users.has(id) || this.channels.has(id) || this.roles.has(id)); + } + + return false; + } + + toJSON() { + return Util.flatten(this, { + members: true, + channels: true, + }); + } +} + +/** + * Regular expression that globally matches `@everyone` and `@here` + * @type {RegExp} + */ +MessageMentions.EVERYONE_PATTERN = /@(everyone|here)/g; + +/** + * Regular expression that globally matches user mentions like `<@81440962496172032>` + * @type {RegExp} + */ +MessageMentions.USERS_PATTERN = /<@!?(\d{17,19})>/g; + +/** + * Regular expression that globally matches role mentions like `<@&297577916114403338>` + * @type {RegExp} + */ +MessageMentions.ROLES_PATTERN = /<@&(\d{17,19})>/g; + +/** + * Regular expression that globally matches channel mentions like `<#222079895583457280>` + * @type {RegExp} + */ +MessageMentions.CHANNELS_PATTERN = /<#(\d{17,19})>/g; + +module.exports = MessageMentions; diff --git a/src/structures/MessagePayload.js b/src/structures/MessagePayload.js index a541b65..c2be94d 100644 --- a/src/structures/MessagePayload.js +++ b/src/structures/MessagePayload.js @@ -1,314 +1,313 @@ -'use strict'; - -const { Buffer } = require('node:buffer'); -const BaseMessageComponent = require('./BaseMessageComponent'); -const MessageEmbed = require('./MessageEmbed'); -const WebEmbed = require('./WebEmbed'); -const { RangeError } = require('../errors'); -const DataResolver = require('../util/DataResolver'); -const MessageFlags = require('../util/MessageFlags'); -const Util = require('../util/Util'); - -/** - * Represents a message to be sent to the API. - */ -class MessagePayload { - /** - * @param {MessageTarget} target The target for this message to be sent to - * @param {MessageOptions|WebhookMessageOptions} options Options passed in from send - */ - constructor(target, options) { - /** - * The target for this message to be sent to - * @type {MessageTarget} - */ - this.target = target; - - /** - * Options passed in from send - * @type {MessageOptions|WebhookMessageOptions} - */ - this.options = options; - - /** - * Data sendable to the API - * @type {?APIMessage} - */ - this.data = null; - - /** - * @typedef {Object} MessageFile - * @property {Buffer|string|Stream} attachment The original attachment that generated this file - * @property {string} name The name of this file - * @property {Buffer|Stream} file The file to be sent to the API - */ - - /** - * Files sendable to the API - * @type {?MessageFile[]} - */ - this.files = null; - } - - /** - * Whether or not the target is a {@link Webhook} or a {@link WebhookClient} - * @type {boolean} - * @readonly - */ - get isWebhook() { - const Webhook = require('./Webhook'); - const WebhookClient = require('../client/WebhookClient'); - return this.target instanceof Webhook || this.target instanceof WebhookClient; - } - - /** - * Whether or not the target is a {@link User} - * @type {boolean} - * @readonly - */ - get isUser() { - const User = require('./User'); - const { GuildMember } = require('./GuildMember'); - return this.target instanceof User || this.target instanceof GuildMember; - } - - /** - * Whether or not the target is a {@link Message} - * @type {boolean} - * @readonly - */ - get isMessage() { - const { Message } = require('./Message'); - return this.target instanceof Message; - } - - /** - * Whether or not the target is a {@link MessageManager} - * @type {boolean} - * @readonly - */ - get isMessageManager() { - const MessageManager = require('../managers/MessageManager'); - return this.target instanceof MessageManager; - } - - /** - * Whether or not the target is an {@link Interaction} or an {@link InteractionWebhook} - * @type {boolean} - * @readonly - */ - get isInteraction() { - const Interaction = require('./Interaction'); - const InteractionWebhook = require('./InteractionWebhook'); - return this.target instanceof Interaction || this.target instanceof InteractionWebhook; - } - - /** - * Makes the content of this message. - * @returns {?string} - */ - makeContent() { - let content; - if (this.options.content === null) { - content = ''; - } else if (typeof this.options.content !== 'undefined') { - content = Util.verifyString(this.options.content, RangeError, 'MESSAGE_CONTENT_TYPE', false); - } - - return content; - } - - /** - * Resolves data. - * @returns {MessagePayload} - */ - async resolveData() { - if (this.data) return this; - const isInteraction = this.isInteraction; - const isWebhook = this.isWebhook; - - let content = this.makeContent(); - const tts = Boolean(this.options.tts); - - let nonce; - if (typeof this.options.nonce !== 'undefined') { - nonce = this.options.nonce; - // eslint-disable-next-line max-len - if (typeof nonce === 'number' ? !Number.isInteger(nonce) : typeof nonce !== 'string') { - throw new RangeError('MESSAGE_NONCE_TYPE'); - } - } - - const components = this.options.components?.map(c => BaseMessageComponent.create(c).toJSON()); - - let username; - let avatarURL; - if (isWebhook) { - username = this.options.username ?? this.target.name; - if (this.options.avatarURL) avatarURL = this.options.avatarURL; - } - - let flags; - if (this.isMessage || this.isMessageManager) { - // eslint-disable-next-line eqeqeq - flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags?.bitfield; - } else if (isInteraction && this.options.ephemeral) { - flags = MessageFlags.FLAGS.EPHEMERAL; - } - - let allowedMentions = - typeof this.options.allowedMentions === 'undefined' - ? this.target.client.options.allowedMentions - : this.options.allowedMentions; - - if (allowedMentions) { - allowedMentions = Util.cloneObject(allowedMentions); - allowedMentions.replied_user = allowedMentions.repliedUser; - delete allowedMentions.repliedUser; - } - - let message_reference; - if (typeof this.options.reply === 'object') { - const reference = this.options.reply.messageReference; - const message_id = this.isMessage ? reference.id ?? reference : this.target.messages.resolveId(reference); - if (message_id) { - message_reference = { - message_id, - fail_if_not_exists: this.options.reply.failIfNotExists ?? this.target.client.options.failIfNotExists, - }; - } - } - - const attachments = this.options.files?.map((file, index) => ({ - id: index.toString(), - description: file.description, - })); - if (Array.isArray(this.options.attachments)) { - this.options.attachments.push(...(attachments ?? [])); - } else { - this.options.attachments = attachments; - } - - if (this.options.embeds) { - if (!Array.isArray(this.options.embeds)) { - this.options.embeds = [this.options.embeds]; - } - - const webembeds = this.options.embeds.filter( - (e) => e instanceof WebEmbed, - ); - this.options.embeds = this.options.embeds.filter(e => e instanceof MessageEmbed); - - if (webembeds.length > 0) { - if (!content) content = ''; - // add hidden embed link - content += `\n${WebEmbed.hiddenEmbed} \n`; - if (webembeds.length > 1) { - console.warn('Multiple webembeds are not supported, this will be ignored.'); - } - // const embed = webembeds[0]; - for (const webE of webembeds) { - const data = await webE.toMessage(); - content += `\n${data}`; - } - } - // Check content - if (content.length > 2000) { - console.warn(`[WARN] Content is longer than 2000 characters.`); - } - if (content.length > 4000) { // Max length if user has nitro boost - throw new RangeError('MESSAGE_EMBED_LINK_LENGTH'); - } - } - - this.data = { - content, - tts, - nonce, - embeds: this.options.embeds?.map(embed => new MessageEmbed(embed).toJSON()), - components, - username, - avatar_url: avatarURL, - allowed_mentions: - typeof content === 'undefined' && typeof message_reference === 'undefined' ? undefined : allowedMentions, - flags, - message_reference, - attachments: this.options.attachments, - sticker_ids: this.options.stickers?.map(sticker => sticker.id ?? sticker), - }; - return this; - } - - /** - * Resolves files. - * @returns {Promise} - */ - async resolveFiles() { - if (this.files) return this; - - this.files = await Promise.all(this.options.files?.map(file => this.constructor.resolveFile(file)) ?? []); - return this; - } - - /** - * Resolves a single file into an object sendable to the API. - * @param {BufferResolvable|Stream|FileOptions|MessageAttachment} fileLike Something that could be resolved to a file - * @returns {Promise} - */ - static async resolveFile(fileLike) { - let attachment; - let name; - - const findName = thing => { - if (typeof thing === 'string') { - return Util.basename(thing); - } - - if (thing.path) { - return Util.basename(thing.path); - } - - return 'file.jpg'; - }; - - const ownAttachment = - typeof fileLike === 'string' || fileLike instanceof Buffer || typeof fileLike.pipe === 'function'; - if (ownAttachment) { - attachment = fileLike; - name = findName(attachment); - } else { - attachment = fileLike.attachment; - name = fileLike.name ?? findName(attachment); - } - - const resource = await DataResolver.resolveFile(attachment); - return { attachment, name, file: resource }; - } - - /** - * Creates a {@link MessagePayload} from user-level arguments. - * @param {MessageTarget} target Target to send to - * @param {string|MessageOptions|WebhookMessageOptions} options Options or content to use - * @param {MessageOptions|WebhookMessageOptions} [extra={}] Extra options to add onto specified options - * @returns {MessagePayload} - */ - static create(target, options, extra = {}) { - return new this( - target, - typeof options !== 'object' || options === null ? { content: options, ...extra } : { ...options, ...extra }, - ); - } -} - -module.exports = MessagePayload; - -/** - * A target for a message. - * @typedef {TextChannel|DMChannel|User|GuildMember|Webhook|WebhookClient|Interaction|InteractionWebhook| - * Message|MessageManager} MessageTarget - */ - -/** - * @external APIMessage - * @see {@link https://discord.com/developers/docs/resources/channel#message-object} - */ +'use strict'; + +const { Buffer } = require('node:buffer'); +const BaseMessageComponent = require('./BaseMessageComponent'); +const MessageEmbed = require('./MessageEmbed'); +const WebEmbed = require('./WebEmbed'); +const { RangeError } = require('../errors'); +const DataResolver = require('../util/DataResolver'); +const MessageFlags = require('../util/MessageFlags'); +const Util = require('../util/Util'); + +/** + * Represents a message to be sent to the API. + */ +class MessagePayload { + /** + * @param {MessageTarget} target The target for this message to be sent to + * @param {MessageOptions|WebhookMessageOptions} options Options passed in from send + */ + constructor(target, options) { + /** + * The target for this message to be sent to + * @type {MessageTarget} + */ + this.target = target; + + /** + * Options passed in from send + * @type {MessageOptions|WebhookMessageOptions} + */ + this.options = options; + + /** + * Data sendable to the API + * @type {?APIMessage} + */ + this.data = null; + + /** + * @typedef {Object} MessageFile + * @property {Buffer|string|Stream} attachment The original attachment that generated this file + * @property {string} name The name of this file + * @property {Buffer|Stream} file The file to be sent to the API + */ + + /** + * Files sendable to the API + * @type {?MessageFile[]} + */ + this.files = null; + } + + /** + * Whether or not the target is a {@link Webhook} or a {@link WebhookClient} + * @type {boolean} + * @readonly + */ + get isWebhook() { + const Webhook = require('./Webhook'); + const WebhookClient = require('../client/WebhookClient'); + return this.target instanceof Webhook || this.target instanceof WebhookClient; + } + + /** + * Whether or not the target is a {@link User} + * @type {boolean} + * @readonly + */ + get isUser() { + const User = require('./User'); + const { GuildMember } = require('./GuildMember'); + return this.target instanceof User || this.target instanceof GuildMember; + } + + /** + * Whether or not the target is a {@link Message} + * @type {boolean} + * @readonly + */ + get isMessage() { + const { Message } = require('./Message'); + return this.target instanceof Message; + } + + /** + * Whether or not the target is a {@link MessageManager} + * @type {boolean} + * @readonly + */ + get isMessageManager() { + const MessageManager = require('../managers/MessageManager'); + return this.target instanceof MessageManager; + } + + /** + * Whether or not the target is an {@link Interaction} or an {@link InteractionWebhook} + * @type {boolean} + * @readonly + */ + get isInteraction() { + const Interaction = require('./Interaction'); + const InteractionWebhook = require('./InteractionWebhook'); + return this.target instanceof Interaction || this.target instanceof InteractionWebhook; + } + + /** + * Makes the content of this message. + * @returns {?string} + */ + makeContent() { + let content; + if (this.options.content === null) { + content = ''; + } else if (typeof this.options.content !== 'undefined') { + content = Util.verifyString(this.options.content, RangeError, 'MESSAGE_CONTENT_TYPE', false); + } + + return content; + } + + /** + * Resolves data. + * @returns {MessagePayload} + */ + async resolveData() { + if (this.data) return this; + const isInteraction = this.isInteraction; + const isWebhook = this.isWebhook; + + let content = this.makeContent(); + const tts = Boolean(this.options.tts); + + let nonce; + if (typeof this.options.nonce !== 'undefined') { + nonce = this.options.nonce; + // eslint-disable-next-line max-len + if (typeof nonce === 'number' ? !Number.isInteger(nonce) : typeof nonce !== 'string') { + throw new RangeError('MESSAGE_NONCE_TYPE'); + } + } + + const components = this.options.components?.map(c => BaseMessageComponent.create(c).toJSON()); + + let username; + let avatarURL; + if (isWebhook) { + username = this.options.username ?? this.target.name; + if (this.options.avatarURL) avatarURL = this.options.avatarURL; + } + + let flags; + if (this.isMessage || this.isMessageManager) { + // eslint-disable-next-line eqeqeq + flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags?.bitfield; + } else if (isInteraction && this.options.ephemeral) { + flags = MessageFlags.FLAGS.EPHEMERAL; + } + + let allowedMentions = + typeof this.options.allowedMentions === 'undefined' + ? this.target.client.options.allowedMentions + : this.options.allowedMentions; + + if (allowedMentions) { + allowedMentions = Util.cloneObject(allowedMentions); + allowedMentions.replied_user = allowedMentions.repliedUser; + delete allowedMentions.repliedUser; + } + + let message_reference; + if (typeof this.options.reply === 'object') { + const reference = this.options.reply.messageReference; + const message_id = this.isMessage ? reference.id ?? reference : this.target.messages.resolveId(reference); + if (message_id) { + message_reference = { + message_id, + fail_if_not_exists: this.options.reply.failIfNotExists ?? this.target.client.options.failIfNotExists, + }; + } + } + + const attachments = this.options.files?.map((file, index) => ({ + id: index.toString(), + description: file.description, + })); + if (Array.isArray(this.options.attachments)) { + this.options.attachments.push(...(attachments ?? [])); + } else { + this.options.attachments = attachments; + } + + if (this.options.embeds) { + if (!Array.isArray(this.options.embeds)) { + this.options.embeds = [this.options.embeds]; + } + + const webembeds = this.options.embeds.filter(e => e instanceof WebEmbed); + this.options.embeds = this.options.embeds.filter(e => e instanceof MessageEmbed); + + if (webembeds.length > 0) { + if (!content) content = ''; + // add hidden embed link + content += `\n${WebEmbed.hiddenEmbed} \n`; + if (webembeds.length > 1) { + console.warn('Multiple webembeds are not supported, this will be ignored.'); + } + // const embed = webembeds[0]; + for (const webE of webembeds) { + const data = await webE.toMessage(); + content += `\n${data}`; + } + } + // Check content + if (content.length > 2000) { + console.warn('[WARN] Content is longer than 2000 characters.'); + } + if (content.length > 4000) { + // Max length if user has nitro boost + throw new RangeError('MESSAGE_EMBED_LINK_LENGTH'); + } + } + + this.data = { + content, + tts, + nonce, + embeds: this.options.embeds?.map(embed => new MessageEmbed(embed).toJSON()), + components, + username, + avatar_url: avatarURL, + allowed_mentions: + typeof content === 'undefined' && typeof message_reference === 'undefined' ? undefined : allowedMentions, + flags, + message_reference, + attachments: this.options.attachments, + sticker_ids: this.options.stickers?.map(sticker => sticker.id ?? sticker), + }; + return this; + } + + /** + * Resolves files. + * @returns {Promise} + */ + async resolveFiles() { + if (this.files) return this; + + this.files = await Promise.all(this.options.files?.map(file => this.constructor.resolveFile(file)) ?? []); + return this; + } + + /** + * Resolves a single file into an object sendable to the API. + * @param {BufferResolvable|Stream|FileOptions|MessageAttachment} fileLike Something that could be resolved to a file + * @returns {Promise} + */ + static async resolveFile(fileLike) { + let attachment; + let name; + + const findName = thing => { + if (typeof thing === 'string') { + return Util.basename(thing); + } + + if (thing.path) { + return Util.basename(thing.path); + } + + return 'file.jpg'; + }; + + const ownAttachment = + typeof fileLike === 'string' || fileLike instanceof Buffer || typeof fileLike.pipe === 'function'; + if (ownAttachment) { + attachment = fileLike; + name = findName(attachment); + } else { + attachment = fileLike.attachment; + name = fileLike.name ?? findName(attachment); + } + + const resource = await DataResolver.resolveFile(attachment); + return { attachment, name, file: resource }; + } + + /** + * Creates a {@link MessagePayload} from user-level arguments. + * @param {MessageTarget} target Target to send to + * @param {string|MessageOptions|WebhookMessageOptions} options Options or content to use + * @param {MessageOptions|WebhookMessageOptions} [extra={}] Extra options to add onto specified options + * @returns {MessagePayload} + */ + static create(target, options, extra = {}) { + return new this( + target, + typeof options !== 'object' || options === null ? { content: options, ...extra } : { ...options, ...extra }, + ); + } +} + +module.exports = MessagePayload; + +/** + * A target for a message. + * @typedef {TextChannel|DMChannel|User|GuildMember|Webhook|WebhookClient|Interaction|InteractionWebhook| + * Message|MessageManager} MessageTarget + */ + +/** + * @external APIMessage + * @see {@link https://discord.com/developers/docs/resources/channel#message-object} + */ diff --git a/src/structures/MessageSelectMenu.js b/src/structures/MessageSelectMenu.js index f84e1c4..f5702fc 100644 --- a/src/structures/MessageSelectMenu.js +++ b/src/structures/MessageSelectMenu.js @@ -1,256 +1,262 @@ -'use strict'; - -const BaseMessageComponent = require('./BaseMessageComponent'); -const { MessageComponentTypes } = require('../util/Constants'); -const Util = require('../util/Util'); -const { Message } = require('./Message'); - -/** - * Represents a select menu message component - * @extends {BaseMessageComponent} - */ -class MessageSelectMenu extends BaseMessageComponent { - /** - * @typedef {BaseMessageComponentOptions} MessageSelectMenuOptions - * @property {string} [customId] A unique string to be sent in the interaction when clicked - * @property {string} [placeholder] Custom placeholder text to display when nothing is selected - * @property {number} [minValues] The minimum number of selections required - * @property {number} [maxValues] The maximum number of selections allowed - * @property {MessageSelectOption[]} [options] Options for the select menu - * @property {boolean} [disabled=false] Disables the select menu to prevent interactions - */ - - /** - * @typedef {Object} MessageSelectOption - * @property {string} label The text to be displayed on this option - * @property {string} value The value to be sent for this option - * @property {?string} description Optional description to show for this option - * @property {?RawEmoji} emoji Emoji to display for this option - * @property {boolean} default Render this option as the default selection - */ - - /** - * @typedef {Object} MessageSelectOptionData - * @property {string} label The text to be displayed on this option - * @property {string} value The value to be sent for this option - * @property {string} [description] Optional description to show for this option - * @property {EmojiIdentifierResolvable} [emoji] Emoji to display for this option - * @property {boolean} [default] Render this option as the default selection - */ - - /** - * @param {MessageSelectMenu|MessageSelectMenuOptions} [data={}] MessageSelectMenu to clone or raw data - */ - constructor(data = {}) { - super({ type: 'SELECT_MENU' }); - - this.setup(data); - } - - setup(data) { - /** - * A unique string to be sent in the interaction when clicked - * @type {?string} - */ - this.customId = data.custom_id ?? data.customId ?? null; - - /** - * Custom placeholder text to display when nothing is selected - * @type {?string} - */ - this.placeholder = data.placeholder ?? null; - - /** - * The minimum number of selections required - * @type {?number} - */ - this.minValues = data.min_values ?? data.minValues ?? null; - - /** - * The maximum number of selections allowed - * @type {?number} - */ - this.maxValues = data.max_values ?? data.maxValues ?? null; - - /** - * Options for the select menu - * @type {MessageSelectOption[]} - */ - this.options = this.constructor.normalizeOptions(data.options ?? []); - - /** - * Whether this select menu is currently disabled - * @type {boolean} - */ - this.disabled = data.disabled ?? false; - } - - /** - * Sets the custom id of this select menu - * @param {string} customId A unique string to be sent in the interaction when clicked - * @returns {MessageSelectMenu} - */ - setCustomId(customId) { - this.customId = Util.verifyString(customId, RangeError, 'SELECT_MENU_CUSTOM_ID'); - return this; - } - - /** - * Sets the interactive status of the select menu - * @param {boolean} [disabled=true] Whether this select menu should be disabled - * @returns {MessageSelectMenu} - */ - setDisabled(disabled = true) { - this.disabled = disabled; - return this; - } - - /** - * Sets the maximum number of selections allowed for this select menu - * @param {number} maxValues Number of selections to be allowed - * @returns {MessageSelectMenu} - */ - setMaxValues(maxValues) { - this.maxValues = maxValues; - return this; - } - - /** - * Sets the minimum number of selections required for this select menu - * This will default the maxValues to the number of options, unless manually set - * @param {number} minValues Number of selections to be required - * @returns {MessageSelectMenu} - */ - setMinValues(minValues) { - this.minValues = minValues; - return this; - } - - /** - * Sets the placeholder of this select menu - * @param {string} placeholder Custom placeholder text to display when nothing is selected - * @returns {MessageSelectMenu} - */ - setPlaceholder(placeholder) { - this.placeholder = Util.verifyString(placeholder, RangeError, 'SELECT_MENU_PLACEHOLDER'); - return this; - } - - /** - * Adds options to the select menu. - * @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The options to add - * @returns {MessageSelectMenu} - */ - addOptions(...options) { - this.options.push(...this.constructor.normalizeOptions(options)); - return this; - } - - /** - * Sets the options of the select menu. - * @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The options to set - * @returns {MessageSelectMenu} - */ - setOptions(...options) { - this.spliceOptions(0, this.options.length, options); - return this; - } - - /** - * Removes, replaces, and inserts options in the select menu. - * @param {number} index The index to start at - * @param {number} deleteCount The number of options to remove - * @param {...MessageSelectOptionData|MessageSelectOptionData[]} [options] The replacing option objects - * @returns {MessageSelectMenu} - */ - spliceOptions(index, deleteCount, ...options) { - this.options.splice(index, deleteCount, ...this.constructor.normalizeOptions(...options)); - return this; - } - - /** - * Transforms the select menu into a plain object - * @returns {APIMessageSelectMenu} The raw data of this select menu - */ - toJSON() { - return { - custom_id: this.customId, - disabled: this.disabled, - placeholder: this.placeholder, - min_values: this.minValues, - max_values: this.maxValues ?? (this.minValues ? this.options.length : undefined), - options: this.options, - type: typeof this.type === 'string' ? MessageComponentTypes[this.type] : this.type, - }; - } - - /** - * Normalizes option input and resolves strings and emojis. - * @param {MessageSelectOptionData} option The select menu option to normalize - * @returns {MessageSelectOption} - */ - static normalizeOption(option) { - let { label, value, description, emoji } = option; - - label = Util.verifyString(label, RangeError, 'SELECT_OPTION_LABEL'); - value = Util.verifyString(value, RangeError, 'SELECT_OPTION_VALUE'); - emoji = emoji ? Util.resolvePartialEmoji(emoji) : null; - description = description ? Util.verifyString(description, RangeError, 'SELECT_OPTION_DESCRIPTION', true) : null; - - return { label, value, description, emoji, default: option.default ?? false }; - } - - /** - * Normalizes option input and resolves strings and emojis. - * @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The select menu options to normalize - * @returns {MessageSelectOption[]} - */ - static normalizeOptions(...options) { - return options.flat(Infinity).map(option => this.normalizeOption(option)); - } - // Add - /** - * Select in menu - * @param {Message} message Discord Message - * @param {Array} values Option values - * @returns {Promise For loop - if (values.length < this.minValues) throw new RangeError("[SELECT_MENU_MIN_VALUES] The minimum number of values is " + this.minValues); - if (values.length > this.maxValues) throw new RangeError("[SELECT_MENU_MAX_VALUES] The maximum number of values is " + this.maxValues); - const validValue = this.options.map(obj => obj.value); - const check_ = await values.find(element => { - if (typeof element !== 'string') return true; - if (!validValue.includes(element)) return true; - return false; - }) - if (check_) throw new RangeError("[SELECT_MENU_INVALID_VALUE] The value " + check_ + " is invalid. Please use a valid value " + validValue.join(', ')); - await message.client.api.interactions.post( - { - data: { - type: 3, // ? - guild_id: message.guild?.id ?? null, // In DMs - channel_id: message.channel.id, - message_id: message.id, - application_id: message.author.id, - session_id: message.client.session_id, - data: { - component_type: 3, // Select Menu - custom_id: this.customId, - type: 3, // Select Menu - values, - }, - } - } - ) - return true; - } -} - -module.exports = MessageSelectMenu; +'use strict'; + +const BaseMessageComponent = require('./BaseMessageComponent'); +const { MessageComponentTypes } = require('../util/Constants'); +const Util = require('../util/Util'); +const { Message } = require('./Message'); + +/** + * Represents a select menu message component + * @extends {BaseMessageComponent} + */ +class MessageSelectMenu extends BaseMessageComponent { + /** + * @typedef {BaseMessageComponentOptions} MessageSelectMenuOptions + * @property {string} [customId] A unique string to be sent in the interaction when clicked + * @property {string} [placeholder] Custom placeholder text to display when nothing is selected + * @property {number} [minValues] The minimum number of selections required + * @property {number} [maxValues] The maximum number of selections allowed + * @property {MessageSelectOption[]} [options] Options for the select menu + * @property {boolean} [disabled=false] Disables the select menu to prevent interactions + */ + + /** + * @typedef {Object} MessageSelectOption + * @property {string} label The text to be displayed on this option + * @property {string} value The value to be sent for this option + * @property {?string} description Optional description to show for this option + * @property {?RawEmoji} emoji Emoji to display for this option + * @property {boolean} default Render this option as the default selection + */ + + /** + * @typedef {Object} MessageSelectOptionData + * @property {string} label The text to be displayed on this option + * @property {string} value The value to be sent for this option + * @property {string} [description] Optional description to show for this option + * @property {EmojiIdentifierResolvable} [emoji] Emoji to display for this option + * @property {boolean} [default] Render this option as the default selection + */ + + /** + * @param {MessageSelectMenu|MessageSelectMenuOptions} [data={}] MessageSelectMenu to clone or raw data + */ + constructor(data = {}) { + super({ type: 'SELECT_MENU' }); + + this.setup(data); + } + + setup(data) { + /** + * A unique string to be sent in the interaction when clicked + * @type {?string} + */ + this.customId = data.custom_id ?? data.customId ?? null; + + /** + * Custom placeholder text to display when nothing is selected + * @type {?string} + */ + this.placeholder = data.placeholder ?? null; + + /** + * The minimum number of selections required + * @type {?number} + */ + this.minValues = data.min_values ?? data.minValues ?? null; + + /** + * The maximum number of selections allowed + * @type {?number} + */ + this.maxValues = data.max_values ?? data.maxValues ?? null; + + /** + * Options for the select menu + * @type {MessageSelectOption[]} + */ + this.options = this.constructor.normalizeOptions(data.options ?? []); + + /** + * Whether this select menu is currently disabled + * @type {boolean} + */ + this.disabled = data.disabled ?? false; + } + + /** + * Sets the custom id of this select menu + * @param {string} customId A unique string to be sent in the interaction when clicked + * @returns {MessageSelectMenu} + */ + setCustomId(customId) { + this.customId = Util.verifyString(customId, RangeError, 'SELECT_MENU_CUSTOM_ID'); + return this; + } + + /** + * Sets the interactive status of the select menu + * @param {boolean} [disabled=true] Whether this select menu should be disabled + * @returns {MessageSelectMenu} + */ + setDisabled(disabled = true) { + this.disabled = disabled; + return this; + } + + /** + * Sets the maximum number of selections allowed for this select menu + * @param {number} maxValues Number of selections to be allowed + * @returns {MessageSelectMenu} + */ + setMaxValues(maxValues) { + this.maxValues = maxValues; + return this; + } + + /** + * Sets the minimum number of selections required for this select menu + * This will default the maxValues to the number of options, unless manually set + * @param {number} minValues Number of selections to be required + * @returns {MessageSelectMenu} + */ + setMinValues(minValues) { + this.minValues = minValues; + return this; + } + + /** + * Sets the placeholder of this select menu + * @param {string} placeholder Custom placeholder text to display when nothing is selected + * @returns {MessageSelectMenu} + */ + setPlaceholder(placeholder) { + this.placeholder = Util.verifyString(placeholder, RangeError, 'SELECT_MENU_PLACEHOLDER'); + return this; + } + + /** + * Adds options to the select menu. + * @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The options to add + * @returns {MessageSelectMenu} + */ + addOptions(...options) { + this.options.push(...this.constructor.normalizeOptions(options)); + return this; + } + + /** + * Sets the options of the select menu. + * @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The options to set + * @returns {MessageSelectMenu} + */ + setOptions(...options) { + this.spliceOptions(0, this.options.length, options); + return this; + } + + /** + * Removes, replaces, and inserts options in the select menu. + * @param {number} index The index to start at + * @param {number} deleteCount The number of options to remove + * @param {...MessageSelectOptionData|MessageSelectOptionData[]} [options] The replacing option objects + * @returns {MessageSelectMenu} + */ + spliceOptions(index, deleteCount, ...options) { + this.options.splice(index, deleteCount, ...this.constructor.normalizeOptions(...options)); + return this; + } + + /** + * Transforms the select menu into a plain object + * @returns {APIMessageSelectMenu} The raw data of this select menu + */ + toJSON() { + return { + custom_id: this.customId, + disabled: this.disabled, + placeholder: this.placeholder, + min_values: this.minValues, + max_values: this.maxValues ?? (this.minValues ? this.options.length : undefined), + options: this.options, + type: typeof this.type === 'string' ? MessageComponentTypes[this.type] : this.type, + }; + } + + /** + * Normalizes option input and resolves strings and emojis. + * @param {MessageSelectOptionData} option The select menu option to normalize + * @returns {MessageSelectOption} + */ + static normalizeOption(option) { + let { label, value, description, emoji } = option; + + label = Util.verifyString(label, RangeError, 'SELECT_OPTION_LABEL'); + value = Util.verifyString(value, RangeError, 'SELECT_OPTION_VALUE'); + emoji = emoji ? Util.resolvePartialEmoji(emoji) : null; + description = description ? Util.verifyString(description, RangeError, 'SELECT_OPTION_DESCRIPTION', true) : null; + + return { label, value, description, emoji, default: option.default ?? false }; + } + + /** + * Normalizes option input and resolves strings and emojis. + * @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The select menu options to normalize + * @returns {MessageSelectOption[]} + */ + static normalizeOptions(...options) { + return options.flat(Infinity).map(option => this.normalizeOption(option)); + } + // Add + /** + * Select in menu + * @param {Message} message Discord Message + * @param {Array} values Option values + * @returns {Promise For loop + if (values.length < this.minValues) + throw new RangeError('[SELECT_MENU_MIN_VALUES] The minimum number of values is ' + this.minValues); + if (values.length > this.maxValues) + throw new RangeError('[SELECT_MENU_MAX_VALUES] The maximum number of values is ' + this.maxValues); + const validValue = this.options.map(obj => obj.value); + const check_ = await values.find(element => { + if (typeof element !== 'string') return true; + if (!validValue.includes(element)) return true; + return false; + }); + if (check_) + throw new RangeError( + '[SELECT_MENU_INVALID_VALUE] The value ' + + check_ + + ' is invalid. Please use a valid value ' + + validValue.join(', '), + ); + await message.client.api.interactions.post({ + data: { + type: 3, // ? + guild_id: message.guild?.id ?? null, // In DMs + channel_id: message.channel.id, + message_id: message.id, + application_id: message.author.id, + session_id: message.client.session_id, + data: { + component_type: 3, // Select Menu + custom_id: this.customId, + type: 3, // Select Menu + values, + }, + }, + }); + return true; + } +} + +module.exports = MessageSelectMenu; diff --git a/src/structures/PartialGroupDMChannel.js b/src/structures/PartialGroupDMChannel.js index b774cee..87ef4ce 100644 --- a/src/structures/PartialGroupDMChannel.js +++ b/src/structures/PartialGroupDMChannel.js @@ -1,15 +1,13 @@ 'use strict'; +const { Collection } = require('@discordjs/collection'); const { Channel } = require('./Channel'); -const { Error } = require('../errors'); -const { Collection } = require('discord.js'); -const { Message } = require('./Message'); -const MessageManager = require('../managers/MessageManager'); -const User = require('./User'); -const DataResolver = require('../util/DataResolver'); -const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Invite = require('./Invite'); - +const User = require('./User'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const { Error } = require('../errors'); +const MessageManager = require('../managers/MessageManager'); +const DataResolver = require('../util/DataResolver'); /** * Represents a Partial Group DM Channel on Discord. @@ -77,19 +75,16 @@ class PartialGroupDMChannel extends Channel { } /** - * - * @param {Discord.Client} client - * @param {object} data + * + * @param {Discord.Client} client + * @param {Object} data * @private */ _setup(client, data) { if ('recipients' in data) { Promise.all( - data.recipients.map((recipient) => { - this.recipients.set( - recipient.id, - client.users.cache.get(data.owner_id) || recipient, - ); + data.recipients.map(recipient => { + this.recipients.set(recipient.id, client.users.cache.get(data.owner_id) || recipient); }), ); } @@ -103,16 +98,17 @@ class PartialGroupDMChannel extends Channel { } /** - * + * * @param {Object} data name, icon - * @returns + * @returns * @private */ async edit(data) { const _data = {}; if ('name' in data) _data.name = data.name?.trim() ?? null; - if (typeof data.icon !== 'undefined') + if (typeof data.icon !== 'undefined') { _data.icon = await DataResolver.resolveImage(data.icon); + } const newData = await this.client.api.channels(this.id).patch({ data: _data, }); @@ -126,19 +122,16 @@ class PartialGroupDMChannel extends Channel { * @returns {?string} */ iconURL({ format, size } = {}) { - return ( - this.icon && - this.client.rest.cdn.GDMIcon(this.id, this.icon, format, size) - ); + return this.icon && this.client.rest.cdn.GDMIcon(this.id, this.icon, format, size); } async addMember(user) { - if (this.ownerId !== this.client.user.id) + if (this.ownerId !== this.client.user.id) { return Promise.reject(new Error('NOT_OWNER_GROUP_DM_CHANNEL')); - if (!user instanceof User) - return Promise.reject( - new TypeError('User is not an instance of Discord.User'), - ); + } + if (!user instanceof User) { + return Promise.reject(new TypeError('User is not an instance of Discord.User')); + } if (this.recipients.get(user.id)) return Promise.reject(new Error('USER_ALREADY_IN_GROUP_DM_CHANNEL')); // await this.client.api.channels[this.id].recipients[user.id].put(); @@ -147,12 +140,12 @@ class PartialGroupDMChannel extends Channel { } async removeMember(user) { - if (this.ownerId !== this.client.user.id) + if (this.ownerId !== this.client.user.id) { return Promise.reject(new Error('NOT_OWNER_GROUP_DM_CHANNEL')); - if (!user instanceof User) - return Promise.reject( - new TypeError('User is not an instance of Discord.User'), - ); + } + if (!user instanceof User) { + return Promise.reject(new TypeError('User is not an instance of Discord.User')); + } if (!this.recipients.get(user.id)) return Promise.reject(new Error('USER_NOT_IN_GROUP_DM_CHANNEL')); await this.client.api.channels[this.id].recipients[user.id].delete(); this.recipients.delete(user.id); @@ -169,18 +162,19 @@ class PartialGroupDMChannel extends Channel { async getInvite() { const inviteCode = await this.client.api.channels(this.id).invites.post({ - data: { - max_age: 86400, - }, - }); + data: { + max_age: 86400, + }, + }); const invite = new Invite(this.client, inviteCode); this.invites.set(invite.code, invite); return invite; } async fetchInvite(force = false) { - if (this.ownerId !== this.client.user.id) + if (this.ownerId !== this.client.user.id) { return Promise.reject(new Error('NOT_OWNER_GROUP_DM_CHANNEL')); + } if (!force && this.invites.size) return this.invites; const invites = await this.client.api.channels(this.id).invites.get(); await Promise.all(invites.map(invite => this.invites.set(invite.code, new Invite(this.client, invite)))); @@ -188,10 +182,12 @@ class PartialGroupDMChannel extends Channel { } async removeInvite(invite) { - if (this.ownerId !== this.client.user.id) + if (this.ownerId !== this.client.user.id) { return Promise.reject(new Error('NOT_OWNER_GROUP_DM_CHANNEL')); - if (!invite instanceof Invite) + } + if (!invite instanceof Invite) { return Promise.reject(new TypeError('Invite is not an instance of Discord.Invite')); + } await this.client.api.channels(this.id).invites[invite.code].delete(); this.invites.delete(invite.code); return this; @@ -199,10 +195,10 @@ class PartialGroupDMChannel extends Channel { // These are here only for documentation purposes - they are implemented by TextBasedChannel /* eslint-disable no-empty-function */ - get lastMessage() { } - get lastPinAt() { } - send() { } - sendTyping() { } + get lastMessage() {} + get lastPinAt() {} + send() {} + sendTyping() {} } TextBasedChannel.applyToClass(PartialGroupDMChannel, false); diff --git a/src/structures/User.js b/src/structures/User.js index 826d4fe..ce3f812 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -15,442 +15,421 @@ const { Relationship } = require('../util/Constants'); * @extends {Base} */ class User extends Base { - constructor(client, data) { - super(client); + constructor(client, data) { + super(client); - /** - * The user's id - * @type {Snowflake} - */ - this.id = data.id; + /** + * The user's id + * @type {Snowflake} + */ + this.id = data.id; - this.bot = null; + this.bot = null; - this.system = null; + this.system = null; - this.flags = null; + this.flags = null; - // Code written by https://github.com/aiko-chan-ai - this.connectedAccounts = []; - this.premiumSince = null; - this.premiumGuildSince = null; - this.mutualGuilds = new Collection(); - this.applications = null; - this._patch(data); - } + // Code written by https://github.com/aiko-chan-ai + this.connectedAccounts = []; + this.premiumSince = null; + this.premiumGuildSince = null; + this.mutualGuilds = new Collection(); + this.applications = null; + this._patch(data); + } - _patch(data) { - if ('username' in data) { - /** - * The username of the user - * @type {?string} - */ - this.username = data.username; - } else { - this.username ??= null; - } + _patch(data) { + if ('username' in data) { + /** + * The username of the user + * @type {?string} + */ + this.username = data.username; + } else { + this.username ??= null; + } - if ('bot' in data) { - /** - * Whether or not the user is a bot - * @type {?boolean} - */ - this.bot = Boolean(data.bot); - if (this.bot == true) { - this.applications = new ApplicationCommandManager( - this.client, - undefined, - this, - ); - } - } else if (!this.partial && typeof this.bot !== 'boolean') { - this.bot = false; - } + if ('bot' in data) { + /** + * Whether or not the user is a bot + * @type {?boolean} + */ + this.bot = Boolean(data.bot); + if (this.bot == true) { + this.applications = new ApplicationCommandManager(this.client, undefined, this); + } + } else if (!this.partial && typeof this.bot !== 'boolean') { + this.bot = false; + } - if ('discriminator' in data) { - /** - * A discriminator based on username for the user - * @type {?string} - */ - this.discriminator = data.discriminator; - } else { - this.discriminator ??= null; - } + if ('discriminator' in data) { + /** + * A discriminator based on username for the user + * @type {?string} + */ + this.discriminator = data.discriminator; + } else { + this.discriminator ??= null; + } - if ('avatar' in data) { - /** - * The user avatar's hash - * @type {?string} - */ - this.avatar = data.avatar; - } else { - this.avatar ??= null; - } + if ('avatar' in data) { + /** + * The user avatar's hash + * @type {?string} + */ + this.avatar = data.avatar; + } else { + this.avatar ??= null; + } - if ('banner' in data) { - /** - * The user banner's hash - * The user must be force fetched for this property to be present or be updated - * @type {?string} - */ - this.banner = data.banner; - } else if (this.banner !== null) { - this.banner ??= undefined; - } + if ('banner' in data) { + /** + * The user banner's hash + * The user must be force fetched for this property to be present or be updated + * @type {?string} + */ + this.banner = data.banner; + } else if (this.banner !== null) { + this.banner ??= undefined; + } - if ('accent_color' in data) { - /** - * The base 10 accent color of the user's banner - * The user must be force fetched for this property to be present or be updated - * @type {?number} - */ - this.accentColor = data.accent_color; - } else if (this.accentColor !== null) { - this.accentColor ??= undefined; - } + if ('accent_color' in data) { + /** + * The base 10 accent color of the user's banner + * The user must be force fetched for this property to be present or be updated + * @type {?number} + */ + this.accentColor = data.accent_color; + } else if (this.accentColor !== null) { + this.accentColor ??= undefined; + } - if ('system' in data) { - /** - * Whether the user is an Official Discord System user (part of the urgent message system) - * @type {?boolean} - */ - this.system = Boolean(data.system); - } else if (!this.partial && typeof this.system !== 'boolean') { - this.system = false; - } + if ('system' in data) { + /** + * Whether the user is an Official Discord System user (part of the urgent message system) + * @type {?boolean} + */ + this.system = Boolean(data.system); + } else if (!this.partial && typeof this.system !== 'boolean') { + this.system = false; + } - if ('public_flags' in data) { - /** - * The flags for this user - * @type {?UserFlags} - */ - this.flags = new UserFlags(data.public_flags); - } - } + if ('public_flags' in data) { + /** + * The flags for this user + * @type {?UserFlags} + */ + this.flags = new UserFlags(data.public_flags); + } + } - /** - * Check relationship status - * @readonly - */ - get relationships() { - const i = this.client.relationships.cache.get(this.id) ?? 0; - return Relationship[parseInt(i)]; - } + /** + * Check relationship status + * @readonly + */ + get relationships() { + const i = this.client.relationships.cache.get(this.id) ?? 0; + return Relationship[parseInt(i)]; + } - /** - * Check note - * @readonly - */ - get note() { - return this.client.user.notes.get(this.id); - } + /** + * Check note + * @readonly + */ + get note() { + return this.client.user.notes.get(this.id); + } - // Code written by https://github.com/aiko-chan-ai - _ProfilePatch(data) { - if (!data) return; + // Code written by https://github.com/aiko-chan-ai + _ProfilePatch(data) { + if (!data) return; - if (data.connected_accounts.length > 0) - this.connectedAccounts = data.connected_accounts; + if (data.connected_accounts.length > 0) { + this.connectedAccounts = data.connected_accounts; + } - if ('premium_since' in data) { - const date = new Date(data.premium_since); - this.premiumSince = date.getTime(); - } + if ('premium_since' in data) { + const date = new Date(data.premium_since); + this.premiumSince = date.getTime(); + } - if ('premium_guild_since' in data) { - const date = new Date(data.premium_guild_since); - this.premiumGuildSince = date.getTime(); - } + if ('premium_guild_since' in data) { + const date = new Date(data.premium_guild_since); + this.premiumGuildSince = date.getTime(); + } - this.mutualGuilds = new Collection( - data.mutual_guilds.map((obj) => [obj.id, obj]), - ); - } + this.mutualGuilds = new Collection(data.mutual_guilds.map(obj => [obj.id, obj])); + } - /** - * Get profile from Discord, if client is in a server with the target. - *
Code written by https://github.com/aiko-chan-ai - */ - async getProfile() { - if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); - try { - const data = await this.client.api.users(this.id).profile.get(); - this._ProfilePatch(data); - return this; - } catch (e) { - throw e; - } - } + /** + * Get profile from Discord, if client is in a server with the target. + *
Code written by https://github.com/aiko-chan-ai + */ + async getProfile() { + if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); + try { + const data = await this.client.api.users(this.id).profile.get(); + this._ProfilePatch(data); + return this; + } catch (e) { + throw e; + } + } - /** - * Friends the user and send Request [If no request] - * @returns {Promise} the user object - */ - async setFriend() { - return this.client.relationships.addFriend(this); - } + /** + * Friends the user and send Request [If no request] + * @returns {Promise} the user object + */ + async setFriend() { + return this.client.relationships.addFriend(this); + } - /** - * Send Friend Request to the user - * @returns {Promise} the user object - */ - async sendFriendRequest() { - return this.client.relationships.sendFriendRequest( - this.username, - this.discriminator, - ); - } - /** - * Blocks the user - * @returns {Promise} the user object - */ - async setBlock() { - return this.client.relationships.addBlocked(this); - } + /** + * Send Friend Request to the user + * @returns {Promise} the user object + */ + async sendFriendRequest() { + return this.client.relationships.sendFriendRequest(this.username, this.discriminator); + } + /** + * Blocks the user + * @returns {Promise} the user object + */ + async setBlock() { + return this.client.relationships.addBlocked(this); + } - /** - * Removes the user from your blocks list - * @returns {Promise} the user object - */ - async unBlock() { - return this.client.relationships.deleteBlocked(this); - } + /** + * Removes the user from your blocks list + * @returns {Promise} the user object + */ + async unBlock() { + return this.client.relationships.deleteBlocked(this); + } - /** - * Removes the user from your friends list - * @returns {Promise} the user object - */ - unFriend() { - return this.client.relationships.deleteFriend(this); - } + /** + * Removes the user from your friends list + * @returns {Promise} the user object + */ + unFriend() { + return this.client.relationships.deleteFriend(this); + } - /** - * Whether this User is a partial - * @type {boolean} - * @readonly - */ - get partial() { - return typeof this.username !== 'string'; - } + /** + * Whether this User is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return typeof this.username !== 'string'; + } - /** - * The timestamp the user was created at - * @type {number} - * @readonly - */ - get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); - } + /** + * The timestamp the user was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return SnowflakeUtil.timestampFrom(this.id); + } - /** - * The time the user was created at - * @type {Date} - * @readonly - */ - get createdAt() { - return new Date(this.createdTimestamp); - } + /** + * The time the user was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } - /** - * A link to the user's avatar. - * @param {ImageURLOptions} [options={}] Options for the Image URL - * @returns {?string} - */ - avatarURL({ format, size, dynamic } = {}) { - if (!this.avatar) return null; - return this.client.rest.cdn.Avatar( - this.id, - this.avatar, - format, - size, - dynamic, - ); - } + /** + * A link to the user's avatar. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + avatarURL({ format, size, dynamic } = {}) { + if (!this.avatar) return null; + return this.client.rest.cdn.Avatar(this.id, this.avatar, format, size, dynamic); + } - /** - * A link to the user's default avatar - * @type {string} - * @readonly - */ - get defaultAvatarURL() { - return this.client.rest.cdn.DefaultAvatar(this.discriminator % 5); - } + /** + * A link to the user's default avatar + * @type {string} + * @readonly + */ + get defaultAvatarURL() { + return this.client.rest.cdn.DefaultAvatar(this.discriminator % 5); + } - /** - * A link to the user's avatar if they have one. - * Otherwise a link to their default avatar will be returned. - * @param {ImageURLOptions} [options={}] Options for the Image URL - * @returns {string} - */ - displayAvatarURL(options) { - return this.avatarURL(options) ?? this.defaultAvatarURL; - } + /** + * A link to the user's avatar if they have one. + * Otherwise a link to their default avatar will be returned. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {string} + */ + displayAvatarURL(options) { + return this.avatarURL(options) ?? this.defaultAvatarURL; + } - /** - * The hexadecimal version of the user accent color, with a leading hash - * The user must be force fetched for this property to be present - * @type {?string} - * @readonly - */ - get hexAccentColor() { - if (typeof this.accentColor !== 'number') return this.accentColor; - return `#${this.accentColor.toString(16).padStart(6, '0')}`; - } + /** + * The hexadecimal version of the user accent color, with a leading hash + * The user must be force fetched for this property to be present + * @type {?string} + * @readonly + */ + get hexAccentColor() { + if (typeof this.accentColor !== 'number') return this.accentColor; + return `#${this.accentColor.toString(16).padStart(6, '0')}`; + } - /** - * A link to the user's banner. - * This method will throw an error if called before the user is force fetched. - * See {@link User#banner} for more info - * @param {ImageURLOptions} [options={}] Options for the Image URL - * @returns {?string} - */ - bannerURL({ format, size, dynamic } = {}) { - if (typeof this.banner === 'undefined') - throw new Error('USER_BANNER_NOT_FETCHED'); - if (!this.banner) return null; - return this.client.rest.cdn.Banner( - this.id, - this.banner, - format, - size, - dynamic, - ); - } + /** + * A link to the user's banner. + * This method will throw an error if called before the user is force fetched. + * See {@link User#banner} for more info + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + bannerURL({ format, size, dynamic } = {}) { + if (typeof this.banner === 'undefined') { + throw new Error('USER_BANNER_NOT_FETCHED'); + } + if (!this.banner) return null; + return this.client.rest.cdn.Banner(this.id, this.banner, format, size, dynamic); + } - /** - * The Discord "tag" (e.g. `hydrabolt#0001`) for this user - * @type {?string} - * @readonly - */ - get tag() { - return typeof this.username === 'string' - ? `${this.username}#${this.discriminator}` - : null; - } + /** + * The Discord "tag" (e.g. `hydrabolt#0001`) for this user + * @type {?string} + * @readonly + */ + get tag() { + return typeof this.username === 'string' ? `${this.username}#${this.discriminator}` : null; + } - /** - * The DM between the client's user and this user - * @type {?DMChannel} - * @readonly - */ - get dmChannel() { - return this.client.users.dmChannel(this.id); - } + /** + * The DM between the client's user and this user + * @type {?DMChannel} + * @readonly + */ + get dmChannel() { + return this.client.users.dmChannel(this.id); + } - /** - * Creates a DM channel between the client and the user. - * @param {boolean} [force=false] Whether to skip the cache check and request the API - * @returns {Promise} - */ - createDM(force = false) { - return this.client.users.createDM(this.id, force); - } + /** + * Creates a DM channel between the client and the user. + * @param {boolean} [force=false] Whether to skip the cache check and request the API + * @returns {Promise} + */ + createDM(force = false) { + return this.client.users.createDM(this.id, force); + } - /** - * Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful. - * @returns {Promise} - */ - deleteDM() { - return this.client.users.deleteDM(this.id); - } + /** + * Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful. + * @returns {Promise} + */ + deleteDM() { + return this.client.users.deleteDM(this.id); + } - /** - * Checks if the user is equal to another. - * It compares id, username, discriminator, avatar, banner, accent color, and bot flags. - * It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties. - * @param {User} user User to compare with - * @returns {boolean} - */ - equals(user) { - return ( - user && - this.id === user.id && - this.username === user.username && - this.discriminator === user.discriminator && - this.avatar === user.avatar && - this.flags?.bitfield === user.flags?.bitfield && - this.banner === user.banner && - this.accentColor === user.accentColor - ); - } + /** + * Checks if the user is equal to another. + * It compares id, username, discriminator, avatar, banner, accent color, and bot flags. + * It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties. + * @param {User} user User to compare with + * @returns {boolean} + */ + equals(user) { + return ( + user && + this.id === user.id && + this.username === user.username && + this.discriminator === user.discriminator && + this.avatar === user.avatar && + this.flags?.bitfield === user.flags?.bitfield && + this.banner === user.banner && + this.accentColor === user.accentColor + ); + } - /** - * Compares the user with an API user object - * @param {APIUser} user The API user object to compare - * @returns {boolean} - * @private - */ - _equals(user) { - return ( - user && - this.id === user.id && - this.username === user.username && - this.discriminator === user.discriminator && - this.avatar === user.avatar && - this.flags?.bitfield === user.public_flags && - ('banner' in user ? this.banner === user.banner : true) && - ('accent_color' in user ? this.accentColor === user.accent_color : true) - ); - } + /** + * Compares the user with an API user object + * @param {APIUser} user The API user object to compare + * @returns {boolean} + * @private + */ + _equals(user) { + return ( + user && + this.id === user.id && + this.username === user.username && + this.discriminator === user.discriminator && + this.avatar === user.avatar && + this.flags?.bitfield === user.public_flags && + ('banner' in user ? this.banner === user.banner : true) && + ('accent_color' in user ? this.accentColor === user.accent_color : true) + ); + } - /** - * Fetches this user's flags. - * @param {boolean} [force=false] Whether to skip the cache check and request the API - * @returns {Promise} - */ - fetchFlags(force = false) { - return this.client.users.fetchFlags(this.id, { force }); - } + /** + * Fetches this user's flags. + * @param {boolean} [force=false] Whether to skip the cache check and request the API + * @returns {Promise} + */ + fetchFlags(force = false) { + return this.client.users.fetchFlags(this.id, { force }); + } - /** - * Fetches this user. - * @param {boolean} [force=true] Whether to skip the cache check and request the API - * @returns {Promise} - */ - fetch(force = true) { - return this.client.users.fetch(this.id, { force }); - } + /** + * Fetches this user. + * @param {boolean} [force=true] Whether to skip the cache check and request the API + * @returns {Promise} + */ + fetch(force = true) { + return this.client.users.fetch(this.id, { force }); + } - /** - * When concatenated with a string, this automatically returns the user's mention instead of the User object. - * @returns {string} - * @example - * // Logs: Hello from <@123456789012345678>! - * console.log(`Hello from ${user}!`); - */ - toString() { - return `<@${this.id}>`; - } + /** + * When concatenated with a string, this automatically returns the user's mention instead of the User object. + * @returns {string} + * @example + * // Logs: Hello from <@123456789012345678>! + * console.log(`Hello from ${user}!`); + */ + toString() { + return `<@${this.id}>`; + } - toJSON(...props) { - const json = super.toJSON( - { - createdTimestamp: true, - defaultAvatarURL: true, - hexAccentColor: true, - tag: true, - }, - ...props, - ); - json.avatarURL = this.avatarURL(); - json.displayAvatarURL = this.displayAvatarURL(); - json.bannerURL = this.banner ? this.bannerURL() : this.banner; - return json; - } + toJSON(...props) { + const json = super.toJSON( + { + createdTimestamp: true, + defaultAvatarURL: true, + hexAccentColor: true, + tag: true, + }, + ...props, + ); + json.avatarURL = this.avatarURL(); + json.displayAvatarURL = this.displayAvatarURL(); + json.bannerURL = this.banner ? this.bannerURL() : this.banner; + return json; + } - /** - * Set note to user - * @param {String} note Note to set - * @returns {Promise} - */ - async setNote(note = null) { - await this.client.api.users['@me'].notes(id).put({ data: { note } }); - return (this.note = note); - } + /** + * Set note to user + * @param {String} note Note to set + * @returns {Promise} + */ + async setNote(note = null) { + await this.client.api.users['@me'].notes(id).put({ data: { note } }); + return (this.note = note); + } - // These are here only for documentation purposes - they are implemented by TextBasedChannel - /* eslint-disable no-empty-function */ - send() {} + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + send() {} } TextBasedChannel.applyToClass(User); diff --git a/src/structures/WebEmbed.js b/src/structures/WebEmbed.js index bcca214..3ae5ac8 100644 --- a/src/structures/WebEmbed.js +++ b/src/structures/WebEmbed.js @@ -1,370 +1,358 @@ -'use strict'; -const axios = require('axios'); -const baseURL = 'https://sagiri-fansub.tk/embed?'; -const hiddenCharter = - '||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||'; -const { RangeError } = require('../errors'); -const Util = require('../util/Util'); - -class WebEmbed { - constructor(data = {}) { - this._setup(data); - /** - * Shorten the link - * @type {?boolean} - */ - this.shorten = data.shorten ?? true; - - /** - * Hidden Embed link - * @type {?boolean} - */ - this.hidden = data.hidden ?? false; - } - /** - * @private - * @param {object} data - */ - _setup(data) { - /** - * Type image of this embed - * @type {?thumbnail | image} - */ - this.imageType = 'thumbnail'; - /** - * The title of this embed - * @type {?string} - */ - this.title = data.title ?? null; - - /** - * The description of this embed - * @type {?string} - */ - this.description = data.description ?? null; - - /** - * The URL of this embed - * @type {?string} - */ - this.url = data.url ?? null; - - /** - * The color of this embed - * @type {?number} - */ - this.color = 'color' in data ? Util.resolveColor(data.color) : null; - - /** - * Represents the image of a MessageEmbed - * @typedef {Object} MessageEmbedImage - * @property {string} url URL for this image - * @property {string} proxyURL ProxyURL for this image - * @property {number} height Height of this image - * @property {number} width Width of this image - */ - - /** - * The image of this embed, if there is one - * @type {?MessageEmbedImage} - */ - this.image = data.image - ? { - url: data.image.url, - proxyURL: data.image.proxyURL ?? data.image.proxy_url, - height: data.image.height, - width: data.image.width, - } - : null; - - /** - * The thumbnail of this embed (if there is one) - * @type {?MessageEmbedThumbnail} - */ - this.thumbnail = data.thumbnail - ? { - url: data.thumbnail.url, - proxyURL: data.thumbnail.proxyURL ?? data.thumbnail.proxy_url, - height: data.thumbnail.height, - width: data.thumbnail.width, - } - : null; - - /** - * Represents the video of a MessageEmbed - * @typedef {Object} MessageEmbedVideo - * @property {string} url URL of this video - * @property {string} proxyURL ProxyURL for this video - * @property {number} height Height of this video - * @property {number} width Width of this video - */ - - /** - * The video of this embed (if there is one) - * @type {?MessageEmbedVideo} - * @readonly - */ - this.video = data.video - ? { - url: data.video.url, - proxyURL: data.video.proxyURL ?? data.video.proxy_url, - height: data.video.height, - width: data.video.width, - } - : null; - - /** - * Represents the author field of a MessageEmbed - * @typedef {Object} MessageEmbedAuthor - * @property {string} name The name of this author - * @property {string} url URL of this author - * @property {string} iconURL URL of the icon for this author - * @property {string} proxyIconURL Proxied URL of the icon for this author - */ - - /** - * The author of this embed (if there is one) - * @type {?MessageEmbedAuthor} - */ - this.author = data.author - ? { - name: data.author.name, - url: data.author.url, - } - : null; - - /** - * Represents the provider of a MessageEmbed - * @typedef {Object} MessageEmbedProvider - * @property {string} name The name of this provider - * @property {string} url URL of this provider - */ - - /** - * The provider of this embed (if there is one) - * @type {?MessageEmbedProvider} - */ - this.provider = data.provider - ? { - name: data.provider.name, - url: data.provider.name, - } - : null; - } - /** - * The options to provide for setting an author for a {@link MessageEmbed}. - * @typedef {Object} EmbedAuthorData - * @property {string} name The name of this author. - */ - - /** - * Sets the author of this embed. - * @param {string|EmbedAuthorData|null} options The options to provide for the author. - * Provide `null` to remove the author data. - * @returns {MessageEmbed} - */ - setAuthor(options) { - if (options === null) { - this.author = {}; - return this; - } - const { name, url } = options; - this.author = { - name: Util.verifyString(name, RangeError, 'EMBED_AUTHOR_NAME'), - url, - }; - return this; - } - - /** - * The options to provide for setting an provider for a {@link MessageEmbed}. - * @typedef {Object} EmbedProviderData - * @property {string} name The name of this provider. - */ - - /** - * Sets the provider of this embed. - * @param {string|EmbedProviderData|null} options The options to provide for the provider. - * Provide `null` to remove the provider data. - * @returns {MessageEmbed} - */ - setProvider(options) { - if (options === null) { - this.provider = {}; - return this; - } - const { name, url } = options; - this.provider = { - name: Util.verifyString(name, RangeError, 'EMBED_PROVIDER_NAME'), - url, - }; - return this; - } - - /** - * Sets the color of this embed. - * @param {ColorResolvable} color The color of the embed - * @returns {MessageEmbed} - */ - setColor(color) { - this.color = Util.resolveColor(color); - return this; - } - - /** - * Sets the description of this embed. - * @param {string} description The description (Limit 350 characters) - * @returns {MessageEmbed} - */ - setDescription(description) { - this.description = Util.verifyString( - description, - RangeError, - 'EMBED_DESCRIPTION', - ); - return this; - } - - /** - * Sets the image of this embed. - * @param {string} url The URL of the image - * @returns {MessageEmbed} - */ - setImage(url) { - if (this.thumbnail && this.thumbnail.url) { - console.warn('You can only set image or thumbnail per embed.'); - this.thumbnail.url = null; - } - this.imageType = 'image'; - this.image = { url }; - return this; - } - - /** - * Sets the thumbnail of this embed. - * @param {string} url The URL of the image - * @returns {MessageEmbed} - */ - setThumbnail(url) { - if (this.image && this.image.url) { - console.warn('You can only set image or thumbnail per embed.'); - this.image.url = null; - } - this.imageType = 'thumbnail'; - this.thumbnail = { url }; - return this; - } - - /** - * Sets the video of this embed. - * @param {string} url The URL of the video - * @returns {MessageEmbed} - */ - setVideo(url) { - this.video = { url }; - return this; - } - - /** - * Sets the title of this embed. - * @param {string} title The title - * @returns {MessageEmbed} - */ - setTitle(title) { - this.title = Util.verifyString(title, RangeError, 'EMBED_TITLE'); - return this; - } - - /** - * Sets the URL of this embed. - * @param {string} url The URL - * @returns {MessageEmbed} - */ - setURL(url) { - this.url = url; - return this; - } - - /** - * Return Message Content + Embed (if hidden, pls check content length because it has 1000+ length) - * @returns {string} Message Content - */ - async toMessage() { - const arrayQuery = [`image_type=${this.imageType}`]; - if (this.title) { - arrayQuery.push(`title=${encodeURIComponent(this.title)}`); - } - if (this.description) { - arrayQuery.push(`description=${encodeURIComponent(this.description)}`); - } - if (this.url) { - arrayQuery.push(`url=${encodeURIComponent(this.url)}`); - } - if (this.color) { - arrayQuery.push( - `color=${encodeURIComponent('#' + this.color.toString(16))}`, - ); - } - if (this.image?.url) { - arrayQuery.push(`image=${encodeURIComponent(this.image.url)}`); - } - if (this.video?.url) { - arrayQuery.push(`video=${encodeURIComponent(this.video.url)}`); - } - if (this.author) { - if (this.author.name) - arrayQuery.push(`author_name=${encodeURIComponent(this.author.name)}`); - if (this.author.url) - arrayQuery.push(`author_url=${encodeURIComponent(this.author.url)}`); - } - if (this.provider) { - if (this.provider.name) - arrayQuery.push( - `provider_name=${encodeURIComponent(this.provider.name)}`, - ); - if (this.provider.url) - arrayQuery.push( - `provider_url=${encodeURIComponent(this.provider.url)}`, - ); - } - if (this.thumbnail?.url) { - arrayQuery.push(`image=${encodeURIComponent(this.thumbnail.url)}`); - } - const fullURL = `${baseURL}${arrayQuery.join('&')}`; - if (this.shorten) { - const url = await getShorten(fullURL); - if (!url) console.log('Cannot shorten URL in WebEmbed'); - return this.hidden - ? `${hiddenCharter} ${url || fullURL}` - : url || fullURL; - } else { - return this.hidden ? `${hiddenCharter} ${fullURL}` : fullURL; - } - } -} - -// Credit: https://www.npmjs.com/package/node-url-shortener + google :)) -const getShorten = async (url) => { - const APIurl = [ - // 'https://is.gd/create.php?format=simple&url=', :( - 'https://tinyurl.com/api-create.php?url=', - 'https://sagiri-fansub.tk/api/v1/short?url=', // my api, pls don't ddos :( - 'https://lazuee.ga/api/v1/shorten?url=' - // 'https://cdpt.in/shorten?url=', Redirects 5s :( - ]; - try { - const res = await axios.get( - `${ - APIurl[Math.floor(Math.random() * APIurl.length)] - }${encodeURIComponent(url)}`, - ); - if (typeof res.data == 'string') return res.data; - else if (typeof res.data == 'object') return res.data.shorten; - else throw null; - } catch { - return void 0; - } -} - -module.exports = WebEmbed; -module.exports.hiddenEmbed = hiddenCharter; +'use strict'; +const axios = require('axios'); +const baseURL = 'https://sagiri-fansub.tk/embed?'; +const hiddenCharter = + '||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||'; +const { RangeError } = require('../errors'); +const Util = require('../util/Util'); + +class WebEmbed { + constructor(data = {}) { + this._setup(data); + /** + * Shorten the link + * @type {?boolean} + */ + this.shorten = data.shorten ?? true; + + /** + * Hidden Embed link + * @type {?boolean} + */ + this.hidden = data.hidden ?? false; + } + /** + * @private + * @param {object} data + */ + _setup(data) { + /** + * Type image of this embed + * @type {?thumbnail | image} + */ + this.imageType = 'thumbnail'; + /** + * The title of this embed + * @type {?string} + */ + this.title = data.title ?? null; + + /** + * The description of this embed + * @type {?string} + */ + this.description = data.description ?? null; + + /** + * The URL of this embed + * @type {?string} + */ + this.url = data.url ?? null; + + /** + * The color of this embed + * @type {?number} + */ + this.color = 'color' in data ? Util.resolveColor(data.color) : null; + + /** + * Represents the image of a MessageEmbed + * @typedef {Object} MessageEmbedImage + * @property {string} url URL for this image + * @property {string} proxyURL ProxyURL for this image + * @property {number} height Height of this image + * @property {number} width Width of this image + */ + + /** + * The image of this embed, if there is one + * @type {?MessageEmbedImage} + */ + this.image = data.image + ? { + url: data.image.url, + proxyURL: data.image.proxyURL ?? data.image.proxy_url, + height: data.image.height, + width: data.image.width, + } + : null; + + /** + * The thumbnail of this embed (if there is one) + * @type {?MessageEmbedThumbnail} + */ + this.thumbnail = data.thumbnail + ? { + url: data.thumbnail.url, + proxyURL: data.thumbnail.proxyURL ?? data.thumbnail.proxy_url, + height: data.thumbnail.height, + width: data.thumbnail.width, + } + : null; + + /** + * Represents the video of a MessageEmbed + * @typedef {Object} MessageEmbedVideo + * @property {string} url URL of this video + * @property {string} proxyURL ProxyURL for this video + * @property {number} height Height of this video + * @property {number} width Width of this video + */ + + /** + * The video of this embed (if there is one) + * @type {?MessageEmbedVideo} + * @readonly + */ + this.video = data.video + ? { + url: data.video.url, + proxyURL: data.video.proxyURL ?? data.video.proxy_url, + height: data.video.height, + width: data.video.width, + } + : null; + + /** + * Represents the author field of a MessageEmbed + * @typedef {Object} MessageEmbedAuthor + * @property {string} name The name of this author + * @property {string} url URL of this author + * @property {string} iconURL URL of the icon for this author + * @property {string} proxyIconURL Proxied URL of the icon for this author + */ + + /** + * The author of this embed (if there is one) + * @type {?MessageEmbedAuthor} + */ + this.author = data.author + ? { + name: data.author.name, + url: data.author.url, + } + : null; + + /** + * Represents the provider of a MessageEmbed + * @typedef {Object} MessageEmbedProvider + * @property {string} name The name of this provider + * @property {string} url URL of this provider + */ + + /** + * The provider of this embed (if there is one) + * @type {?MessageEmbedProvider} + */ + this.provider = data.provider + ? { + name: data.provider.name, + url: data.provider.name, + } + : null; + } + /** + * The options to provide for setting an author for a {@link MessageEmbed}. + * @typedef {Object} EmbedAuthorData + * @property {string} name The name of this author. + */ + + /** + * Sets the author of this embed. + * @param {string|EmbedAuthorData|null} options The options to provide for the author. + * Provide `null` to remove the author data. + * @returns {MessageEmbed} + */ + setAuthor(options) { + if (options === null) { + this.author = {}; + return this; + } + const { name, url } = options; + this.author = { + name: Util.verifyString(name, RangeError, 'EMBED_AUTHOR_NAME'), + url, + }; + return this; + } + + /** + * The options to provide for setting an provider for a {@link MessageEmbed}. + * @typedef {Object} EmbedProviderData + * @property {string} name The name of this provider. + */ + + /** + * Sets the provider of this embed. + * @param {string|EmbedProviderData|null} options The options to provide for the provider. + * Provide `null` to remove the provider data. + * @returns {MessageEmbed} + */ + setProvider(options) { + if (options === null) { + this.provider = {}; + return this; + } + const { name, url } = options; + this.provider = { + name: Util.verifyString(name, RangeError, 'EMBED_PROVIDER_NAME'), + url, + }; + return this; + } + + /** + * Sets the color of this embed. + * @param {ColorResolvable} color The color of the embed + * @returns {MessageEmbed} + */ + setColor(color) { + this.color = Util.resolveColor(color); + return this; + } + + /** + * Sets the description of this embed. + * @param {string} description The description (Limit 350 characters) + * @returns {MessageEmbed} + */ + setDescription(description) { + this.description = Util.verifyString(description, RangeError, 'EMBED_DESCRIPTION'); + return this; + } + + /** + * Sets the image of this embed. + * @param {string} url The URL of the image + * @returns {MessageEmbed} + */ + setImage(url) { + if (this.thumbnail && this.thumbnail.url) { + console.warn('You can only set image or thumbnail per embed.'); + this.thumbnail.url = null; + } + this.imageType = 'image'; + this.image = { url }; + return this; + } + + /** + * Sets the thumbnail of this embed. + * @param {string} url The URL of the image + * @returns {MessageEmbed} + */ + setThumbnail(url) { + if (this.image && this.image.url) { + console.warn('You can only set image or thumbnail per embed.'); + this.image.url = null; + } + this.imageType = 'thumbnail'; + this.thumbnail = { url }; + return this; + } + + /** + * Sets the video of this embed. + * @param {string} url The URL of the video + * @returns {MessageEmbed} + */ + setVideo(url) { + this.video = { url }; + return this; + } + + /** + * Sets the title of this embed. + * @param {string} title The title + * @returns {MessageEmbed} + */ + setTitle(title) { + this.title = Util.verifyString(title, RangeError, 'EMBED_TITLE'); + return this; + } + + /** + * Sets the URL of this embed. + * @param {string} url The URL + * @returns {MessageEmbed} + */ + setURL(url) { + this.url = url; + return this; + } + + /** + * Return Message Content + Embed (if hidden, pls check content length because it has 1000+ length) + * @returns {string} Message Content + */ + async toMessage() { + const arrayQuery = [`image_type=${this.imageType}`]; + if (this.title) { + arrayQuery.push(`title=${encodeURIComponent(this.title)}`); + } + if (this.description) { + arrayQuery.push(`description=${encodeURIComponent(this.description)}`); + } + if (this.url) { + arrayQuery.push(`url=${encodeURIComponent(this.url)}`); + } + if (this.color) { + arrayQuery.push(`color=${encodeURIComponent('#' + this.color.toString(16))}`); + } + if (this.image?.url) { + arrayQuery.push(`image=${encodeURIComponent(this.image.url)}`); + } + if (this.video?.url) { + arrayQuery.push(`video=${encodeURIComponent(this.video.url)}`); + } + if (this.author) { + if (this.author.name) { + arrayQuery.push(`author_name=${encodeURIComponent(this.author.name)}`); + } + if (this.author.url) { + arrayQuery.push(`author_url=${encodeURIComponent(this.author.url)}`); + } + } + if (this.provider) { + if (this.provider.name) { + arrayQuery.push(`provider_name=${encodeURIComponent(this.provider.name)}`); + } + if (this.provider.url) { + arrayQuery.push(`provider_url=${encodeURIComponent(this.provider.url)}`); + } + } + if (this.thumbnail?.url) { + arrayQuery.push(`image=${encodeURIComponent(this.thumbnail.url)}`); + } + const fullURL = `${baseURL}${arrayQuery.join('&')}`; + if (this.shorten) { + const url = await getShorten(fullURL); + if (!url) console.log('Cannot shorten URL in WebEmbed'); + return this.hidden ? `${hiddenCharter} ${url || fullURL}` : url || fullURL; + } else { + return this.hidden ? `${hiddenCharter} ${fullURL}` : fullURL; + } + } +} + +// Credit: https://www.npmjs.com/package/node-url-shortener + google :)) +const getShorten = async url => { + const APIurl = [ + // 'https://is.gd/create.php?format=simple&url=', :( + 'https://tinyurl.com/api-create.php?url=', + 'https://sagiri-fansub.tk/api/v1/short?url=', // my api, pls don't ddos :( + 'https://lazuee.ga/api/v1/shorten?url=', + // 'https://cdpt.in/shorten?url=', Redirects 5s :( + ]; + try { + const res = await axios.get(`${APIurl[Math.floor(Math.random() * APIurl.length)]}${encodeURIComponent(url)}`); + if (typeof res.data == 'string') return res.data; + else if (typeof res.data == 'object') return res.data.shorten; + else throw null; + } catch { + return void 0; + } +}; + +module.exports = WebEmbed; +module.exports.hiddenEmbed = hiddenCharter; diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index a25badf..72a1f4c 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -316,15 +316,12 @@ class Webhook { if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); let messagePayload; - if (options instanceof MessagePayload) { - messagePayload = await options.resolveData(); - } else { - messagePayload = await MessagePayload.create( - message instanceof Message ? message : this, - options, - ).resolveData(); - } - const { data, files } = await messagePayload.resolveFiles(); + if (options instanceof MessagePayload) { + messagePayload = await options.resolveData(); + } else { + messagePayload = await MessagePayload.create(message instanceof Message ? message : this, options).resolveData(); + } + const { data, files } = await messagePayload.resolveFiles(); const d = await this.client.api .webhooks(this.id, this.token) diff --git a/src/structures/interfaces/Application.js b/src/structures/interfaces/Application.js index 035a016..738faee 100644 --- a/src/structures/interfaces/Application.js +++ b/src/structures/interfaces/Application.js @@ -1,136 +1,136 @@ -'use strict'; - -const { ClientApplicationAssetTypes, Endpoints } = require('../../util/Constants'); -const SnowflakeUtil = require('../../util/SnowflakeUtil'); -const Base = require('../Base'); - -const AssetTypes = Object.keys(ClientApplicationAssetTypes); - -/** - * Represents an OAuth2 Application. - * @abstract - */ -class Application extends Base { - constructor(client, data) { - super(client); - - if (data) { - this._patch(data); - } - } - - _patch(data) { - /** - * The application's id - * @type {Snowflake} - */ - this.id = data.id; - - if ('name' in data) { - /** - * The name of the application - * @type {?string} - */ - this.name = data.name; - } else { - this.name ??= null; - } - - if ('description' in data) { - /** - * The application's description - * @type {?string} - */ - this.description = data.description; - } else { - this.description ??= null; - } - - if ('icon' in data) { - /** - * The application's icon hash - * @type {?string} - */ - this.icon = data.icon; - } else { - this.icon ??= null; - } - } - - /** - * The timestamp the application was created at - * @type {number} - * @readonly - */ - get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); - } - - /** - * The time the application was created at - * @type {Date} - * @readonly - */ - get createdAt() { - return new Date(this.createdTimestamp); - } - - /** - * A link to the application's icon. - * @param {StaticImageURLOptions} [options={}] Options for the Image URL - * @returns {?string} - */ - iconURL({ format, size } = {}) { - if (!this.icon) return null; - return this.client.rest.cdn.AppIcon(this.id, this.icon, { format, size }); - } - - /** - * A link to this application's cover image. - * @param {StaticImageURLOptions} [options={}] Options for the Image URL - * @returns {?string} - */ - coverURL({ format, size } = {}) { - if (!this.cover) return null; - return Endpoints.CDN(this.client.options.http.cdn).AppIcon(this.id, this.cover, { format, size }); - } - - /** - * Asset data. - * @typedef {Object} ApplicationAsset - * @property {Snowflake} id The asset's id - * @property {string} name The asset's name - * @property {string} type The asset's type - */ - - /** - * Gets the application's rich presence assets. - * @returns {Promise>} - */ - async fetchAssets() { - const assets = await this.client.api.oauth2.applications(this.id).assets.get(); - return assets.map(a => ({ - id: a.id, - name: a.name, - type: AssetTypes[a.type - 1], - })); - } - - /** - * When concatenated with a string, this automatically returns the application's name instead of the - * Application object. - * @returns {?string} - * @example - * // Logs: Application name: My App - * console.log(`Application name: ${application}`); - */ - toString() { - return this.name; - } - - toJSON() { - return super.toJSON({ createdTimestamp: true }); - } -} - -module.exports = Application; +'use strict'; + +const { ClientApplicationAssetTypes, Endpoints } = require('../../util/Constants'); +const SnowflakeUtil = require('../../util/SnowflakeUtil'); +const Base = require('../Base'); + +const AssetTypes = Object.keys(ClientApplicationAssetTypes); + +/** + * Represents an OAuth2 Application. + * @abstract + */ +class Application extends Base { + constructor(client, data) { + super(client); + + if (data) { + this._patch(data); + } + } + + _patch(data) { + /** + * The application's id + * @type {Snowflake} + */ + this.id = data.id; + + if ('name' in data) { + /** + * The name of the application + * @type {?string} + */ + this.name = data.name; + } else { + this.name ??= null; + } + + if ('description' in data) { + /** + * The application's description + * @type {?string} + */ + this.description = data.description; + } else { + this.description ??= null; + } + + if ('icon' in data) { + /** + * The application's icon hash + * @type {?string} + */ + this.icon = data.icon; + } else { + this.icon ??= null; + } + } + + /** + * The timestamp the application was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return SnowflakeUtil.timestampFrom(this.id); + } + + /** + * The time the application was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * A link to the application's icon. + * @param {StaticImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + iconURL({ format, size } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.AppIcon(this.id, this.icon, { format, size }); + } + + /** + * A link to this application's cover image. + * @param {StaticImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + coverURL({ format, size } = {}) { + if (!this.cover) return null; + return Endpoints.CDN(this.client.options.http.cdn).AppIcon(this.id, this.cover, { format, size }); + } + + /** + * Asset data. + * @typedef {Object} ApplicationAsset + * @property {Snowflake} id The asset's id + * @property {string} name The asset's name + * @property {string} type The asset's type + */ + + /** + * Gets the application's rich presence assets. + * @returns {Promise>} + */ + async fetchAssets() { + const assets = await this.client.api.oauth2.applications(this.id).assets.get(); + return assets.map(a => ({ + id: a.id, + name: a.name, + type: AssetTypes[a.type - 1], + })); + } + + /** + * When concatenated with a string, this automatically returns the application's name instead of the + * Application object. + * @returns {?string} + * @example + * // Logs: Application name: My App + * console.log(`Application name: ${application}`); + */ + toString() { + return this.name; + } + + toJSON() { + return super.toJSON({ createdTimestamp: true }); + } +} + +module.exports = Application; diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index f2fec0a..e160826 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -1,360 +1,360 @@ -'use strict'; - -/* eslint-disable import/order */ -const MessageCollector = require('../MessageCollector'); -const MessagePayload = require('../MessagePayload'); -const SnowflakeUtil = require('../../util/SnowflakeUtil'); -const { Collection } = require('@discordjs/collection'); -const { InteractionTypes } = require('../../util/Constants'); -const { TypeError, Error } = require('../../errors'); -const InteractionCollector = require('../InteractionCollector'); - -/** - * Interface for classes that have text-channel-like features. - * @interface - */ -class TextBasedChannel { - constructor() { - /** - * A manager of the messages sent to this channel - * @type {MessageManager} - */ - this.messages = new MessageManager(this); - - /** - * The channel's last message id, if one was sent - * @type {?Snowflake} - */ - this.lastMessageId = null; - - /** - * The timestamp when the last pinned message was pinned, if there was one - * @type {?number} - */ - this.lastPinTimestamp = null; - } - - /** - * The Message object of the last message in the channel, if one was sent - * @type {?Message} - * @readonly - */ - get lastMessage() { - return this.messages.resolve(this.lastMessageId); - } - - /** - * The date when the last pinned message was pinned, if there was one - * @type {?Date} - * @readonly - */ - get lastPinAt() { - return this.lastPinTimestamp ? new Date(this.lastPinTimestamp) : null; - } - - /** - * Base options provided when sending. - * @typedef {Object} BaseMessageOptions - * @property {boolean} [tts=false] Whether or not the message should be spoken aloud - * @property {string} [nonce=''] The nonce for the message - * @property {string} [content=''] The content for the message - * @property {WebEmbed[]|MessageEmbed[]|APIEmbed[]} [embeds] The embeds for the message - * (see [here](https://discord.com/developers/docs/resources/channel#embed-object) for more details) - * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content - * (see [here](https://discord.com/developers/docs/resources/channel#allowed-mentions-object) for more details) - * @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to send with the message - * @property {MessageActionRow[]|MessageActionRowOptions[]} [components] - * Action rows containing interactive components for the message (buttons, select menus) - * @property {MessageAttachment[]} [attachments] Attachments to send in the message - */ - - /** - * Options provided when sending or editing a message. - * @typedef {BaseMessageOptions} MessageOptions - * @property {ReplyOptions} [reply] The options for replying to a message - * @property {StickerResolvable[]} [stickers=[]] Stickers to send in the message - */ - - /** - * Options provided to control parsing of mentions by Discord - * @typedef {Object} MessageMentionOptions - * @property {MessageMentionTypes[]} [parse] Types of mentions to be parsed - * @property {Snowflake[]} [users] Snowflakes of Users to be parsed as mentions - * @property {Snowflake[]} [roles] Snowflakes of Roles to be parsed as mentions - * @property {boolean} [repliedUser=true] Whether the author of the Message being replied to should be pinged - */ - - /** - * Types of mentions to enable in MessageMentionOptions. - * - `roles` - * - `users` - * - `everyone` - * @typedef {string} MessageMentionTypes - */ - - /** - * @typedef {Object} FileOptions - * @property {BufferResolvable} attachment File to attach - * @property {string} [name='file.jpg'] Filename of the attachment - * @property {string} description The description of the file - */ - - /** - * Options for sending a message with a reply. - * @typedef {Object} ReplyOptions - * @property {MessageResolvable} messageReference The message to reply to (must be in the same channel and not system) - * @property {boolean} [failIfNotExists=true] Whether to error if the referenced message - * does not exist (creates a standard message in this case when false) - */ - - /** - * Sends a message to this channel. - * @param {string|MessagePayload|MessageOptions} options The options to provide - * @returns {Promise} - * @example - * // Send a basic message - * channel.send('hello!') - * .then(message => console.log(`Sent message: ${message.content}`)) - * .catch(console.error); - * @example - * // Send a remote file - * channel.send({ - * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] - * }) - * .then(console.log) - * .catch(console.error); - * @example - * // Send a local file - * channel.send({ - * files: [{ - * attachment: 'entire/path/to/file.jpg', - * name: 'file.jpg' - * description: 'A description of the file' - * }] - * }) - * .then(console.log) - * .catch(console.error); - * @example - * // Send an embed with a local image inside - * channel.send({ - * content: 'This is an embed', - * embeds: [ - * { - * thumbnail: { - * url: 'attachment://file.jpg' - * } - * } - * ], - * files: [{ - * attachment: 'entire/path/to/file.jpg', - * name: 'file.jpg' - * description: 'A description of the file' - * }] - * }) - * .then(console.log) - * .catch(console.error); - */ - async send(options) { - const User = require('../User'); - const { GuildMember } = require('../GuildMember'); - - if (this instanceof User || this instanceof GuildMember) { - const dm = await this.createDM(); - return dm.send(options); - } - - let messagePayload; - - if (options instanceof MessagePayload) { - messagePayload = await options.resolveData(); - } else { - messagePayload = await MessagePayload.create(this, options).resolveData(); - } - - const { data, files } = await messagePayload.resolveFiles(); - const d = await this.client.api.channels[this.id].messages.post({ data, files }); - - return this.messages.cache.get(d.id) ?? this.messages._add(d); - } - - /** - * Sends a typing indicator in the channel. - * @returns {Promise} Resolves upon the typing status being sent - * @example - * // Start typing in a channel - * channel.sendTyping(); - */ - async sendTyping() { - await this.client.api.channels(this.id).typing.post(); - } - - /** - * Creates a Message Collector. - * @param {MessageCollectorOptions} [options={}] The options to pass to the collector - * @returns {MessageCollector} - * @example - * // Create a message collector - * const filter = m => m.content.includes('discord'); - * const collector = channel.createMessageCollector({ filter, time: 15_000 }); - * collector.on('collect', m => console.log(`Collected ${m.content}`)); - * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); - */ - createMessageCollector(options = {}) { - return new MessageCollector(this, options); - } - - /** - * An object containing the same properties as CollectorOptions, but a few more: - * @typedef {MessageCollectorOptions} AwaitMessagesOptions - * @property {string[]} [errors] Stop/end reasons that cause the promise to reject - */ - - /** - * Similar to createMessageCollector but in promise form. - * Resolves with a collection of messages that pass the specified filter. - * @param {AwaitMessagesOptions} [options={}] Optional options to pass to the internal collector - * @returns {Promise>} - * @example - * // Await !vote messages - * const filter = m => m.content.startsWith('!vote'); - * // Errors: ['time'] treats ending because of the time limit as an error - * channel.awaitMessages({ filter, max: 4, time: 60_000, errors: ['time'] }) - * .then(collected => console.log(collected.size)) - * .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`)); - */ - awaitMessages(options = {}) { - return new Promise((resolve, reject) => { - const collector = this.createMessageCollector(options); - collector.once('end', (collection, reason) => { - if (options.errors?.includes(reason)) { - reject(collection); - } else { - resolve(collection); - } - }); - }); - } - - /** - * Creates a button interaction collector. - * @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector - * @returns {InteractionCollector} - * @example - * // Create a button interaction collector - * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId'; - * const collector = channel.createMessageComponentCollector({ filter, time: 15_000 }); - * collector.on('collect', i => console.log(`Collected ${i.customId}`)); - * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); - */ - createMessageComponentCollector(options = {}) { - return new InteractionCollector(this.client, { - ...options, - interactionType: InteractionTypes.MESSAGE_COMPONENT, - channel: this, - }); - } - - /** - * Collects a single component interaction that passes the filter. - * The Promise will reject if the time expires. - * @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector - * @returns {Promise} - * @example - * // Collect a message component interaction - * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId'; - * channel.awaitMessageComponent({ filter, time: 15_000 }) - * .then(interaction => console.log(`${interaction.customId} was clicked!`)) - * .catch(console.error); - */ - awaitMessageComponent(options = {}) { - const _options = { ...options, max: 1 }; - return new Promise((resolve, reject) => { - const collector = this.createMessageComponentCollector(_options); - collector.once('end', (interactions, reason) => { - const interaction = interactions.first(); - if (interaction) resolve(interaction); - else reject(new Error('INTERACTION_COLLECTOR_ERROR', reason)); - }); - }); - } - - /** - * Bulk deletes given messages that are newer than two weeks. - * @param {Collection|MessageResolvable[]|number} messages - * Messages or number of messages to delete - * @param {boolean} [filterOld=false] Filter messages to remove those which are older than two weeks automatically - * @returns {Promise>} Returns the deleted messages - * @example - * // Bulk delete messages - * channel.bulkDelete(5) - * .then(messages => console.log(`Bulk deleted ${messages.size} messages`)) - * .catch(console.error); - */ - async bulkDelete(messages, filterOld = false) { - if (Array.isArray(messages) || messages instanceof Collection) { - let messageIds = messages instanceof Collection ? [...messages.keys()] : messages.map(m => m.id ?? m); - if (filterOld) { - messageIds = messageIds.filter(id => Date.now() - SnowflakeUtil.timestampFrom(id) < 1_209_600_000); - } - if (messageIds.length === 0) return new Collection(); - if (messageIds.length === 1) { - await this.client.api.channels(this.id).messages(messageIds[0]).delete(); - const message = this.client.actions.MessageDelete.getMessage( - { - message_id: messageIds[0], - }, - this, - ); - return message ? new Collection([[message.id, message]]) : new Collection(); - } - await this.client.api.channels[this.id].messages['bulk-delete'].post({ data: { messages: messageIds } }); - return messageIds.reduce( - (col, id) => - col.set( - id, - this.client.actions.MessageDeleteBulk.getMessage( - { - message_id: id, - }, - this, - ), - ), - new Collection(), - ); - } - if (!isNaN(messages)) { - const msgs = await this.messages.fetch({ limit: messages }); - return this.bulkDelete(msgs, filterOld); - } - throw new TypeError('MESSAGE_BULK_DELETE_TYPE'); - } - - static applyToClass(structure, full = false, ignore = []) { - const props = ['send']; - if (full) { - props.push( - 'lastMessage', - 'lastPinAt', - 'bulkDelete', - 'sendTyping', - 'createMessageCollector', - 'awaitMessages', - 'createMessageComponentCollector', - 'awaitMessageComponent', - ); - } - for (const prop of props) { - if (ignore.includes(prop)) continue; - Object.defineProperty( - structure.prototype, - prop, - Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop), - ); - } - } -} - -module.exports = TextBasedChannel; - -// Fixes Circular -const MessageManager = require('../../managers/MessageManager'); +'use strict'; + +/* eslint-disable import/order */ +const MessageCollector = require('../MessageCollector'); +const MessagePayload = require('../MessagePayload'); +const SnowflakeUtil = require('../../util/SnowflakeUtil'); +const { Collection } = require('@discordjs/collection'); +const { InteractionTypes } = require('../../util/Constants'); +const { TypeError, Error } = require('../../errors'); +const InteractionCollector = require('../InteractionCollector'); + +/** + * Interface for classes that have text-channel-like features. + * @interface + */ +class TextBasedChannel { + constructor() { + /** + * A manager of the messages sent to this channel + * @type {MessageManager} + */ + this.messages = new MessageManager(this); + + /** + * The channel's last message id, if one was sent + * @type {?Snowflake} + */ + this.lastMessageId = null; + + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = null; + } + + /** + * The Message object of the last message in the channel, if one was sent + * @type {?Message} + * @readonly + */ + get lastMessage() { + return this.messages.resolve(this.lastMessageId); + } + + /** + * The date when the last pinned message was pinned, if there was one + * @type {?Date} + * @readonly + */ + get lastPinAt() { + return this.lastPinTimestamp ? new Date(this.lastPinTimestamp) : null; + } + + /** + * Base options provided when sending. + * @typedef {Object} BaseMessageOptions + * @property {boolean} [tts=false] Whether or not the message should be spoken aloud + * @property {string} [nonce=''] The nonce for the message + * @property {string} [content=''] The content for the message + * @property {WebEmbed[]|MessageEmbed[]|APIEmbed[]} [embeds] The embeds for the message + * (see [here](https://discord.com/developers/docs/resources/channel#embed-object) for more details) + * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content + * (see [here](https://discord.com/developers/docs/resources/channel#allowed-mentions-object) for more details) + * @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to send with the message + * @property {MessageActionRow[]|MessageActionRowOptions[]} [components] + * Action rows containing interactive components for the message (buttons, select menus) + * @property {MessageAttachment[]} [attachments] Attachments to send in the message + */ + + /** + * Options provided when sending or editing a message. + * @typedef {BaseMessageOptions} MessageOptions + * @property {ReplyOptions} [reply] The options for replying to a message + * @property {StickerResolvable[]} [stickers=[]] Stickers to send in the message + */ + + /** + * Options provided to control parsing of mentions by Discord + * @typedef {Object} MessageMentionOptions + * @property {MessageMentionTypes[]} [parse] Types of mentions to be parsed + * @property {Snowflake[]} [users] Snowflakes of Users to be parsed as mentions + * @property {Snowflake[]} [roles] Snowflakes of Roles to be parsed as mentions + * @property {boolean} [repliedUser=true] Whether the author of the Message being replied to should be pinged + */ + + /** + * Types of mentions to enable in MessageMentionOptions. + * - `roles` + * - `users` + * - `everyone` + * @typedef {string} MessageMentionTypes + */ + + /** + * @typedef {Object} FileOptions + * @property {BufferResolvable} attachment File to attach + * @property {string} [name='file.jpg'] Filename of the attachment + * @property {string} description The description of the file + */ + + /** + * Options for sending a message with a reply. + * @typedef {Object} ReplyOptions + * @property {MessageResolvable} messageReference The message to reply to (must be in the same channel and not system) + * @property {boolean} [failIfNotExists=true] Whether to error if the referenced message + * does not exist (creates a standard message in this case when false) + */ + + /** + * Sends a message to this channel. + * @param {string|MessagePayload|MessageOptions} options The options to provide + * @returns {Promise} + * @example + * // Send a basic message + * channel.send('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + * @example + * // Send a remote file + * channel.send({ + * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send a local file + * channel.send({ + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * description: 'A description of the file' + * }] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send an embed with a local image inside + * channel.send({ + * content: 'This is an embed', + * embeds: [ + * { + * thumbnail: { + * url: 'attachment://file.jpg' + * } + * } + * ], + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * description: 'A description of the file' + * }] + * }) + * .then(console.log) + * .catch(console.error); + */ + async send(options) { + const User = require('../User'); + const { GuildMember } = require('../GuildMember'); + + if (this instanceof User || this instanceof GuildMember) { + const dm = await this.createDM(); + return dm.send(options); + } + + let messagePayload; + + if (options instanceof MessagePayload) { + messagePayload = await options.resolveData(); + } else { + messagePayload = await MessagePayload.create(this, options).resolveData(); + } + + const { data, files } = await messagePayload.resolveFiles(); + const d = await this.client.api.channels[this.id].messages.post({ data, files }); + + return this.messages.cache.get(d.id) ?? this.messages._add(d); + } + + /** + * Sends a typing indicator in the channel. + * @returns {Promise} Resolves upon the typing status being sent + * @example + * // Start typing in a channel + * channel.sendTyping(); + */ + async sendTyping() { + await this.client.api.channels(this.id).typing.post(); + } + + /** + * Creates a Message Collector. + * @param {MessageCollectorOptions} [options={}] The options to pass to the collector + * @returns {MessageCollector} + * @example + * // Create a message collector + * const filter = m => m.content.includes('discord'); + * const collector = channel.createMessageCollector({ filter, time: 15_000 }); + * collector.on('collect', m => console.log(`Collected ${m.content}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createMessageCollector(options = {}) { + return new MessageCollector(this, options); + } + + /** + * An object containing the same properties as CollectorOptions, but a few more: + * @typedef {MessageCollectorOptions} AwaitMessagesOptions + * @property {string[]} [errors] Stop/end reasons that cause the promise to reject + */ + + /** + * Similar to createMessageCollector but in promise form. + * Resolves with a collection of messages that pass the specified filter. + * @param {AwaitMessagesOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise>} + * @example + * // Await !vote messages + * const filter = m => m.content.startsWith('!vote'); + * // Errors: ['time'] treats ending because of the time limit as an error + * channel.awaitMessages({ filter, max: 4, time: 60_000, errors: ['time'] }) + * .then(collected => console.log(collected.size)) + * .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`)); + */ + awaitMessages(options = {}) { + return new Promise((resolve, reject) => { + const collector = this.createMessageCollector(options); + collector.once('end', (collection, reason) => { + if (options.errors?.includes(reason)) { + reject(collection); + } else { + resolve(collection); + } + }); + }); + } + + /** + * Creates a button interaction collector. + * @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector + * @returns {InteractionCollector} + * @example + * // Create a button interaction collector + * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId'; + * const collector = channel.createMessageComponentCollector({ filter, time: 15_000 }); + * collector.on('collect', i => console.log(`Collected ${i.customId}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createMessageComponentCollector(options = {}) { + return new InteractionCollector(this.client, { + ...options, + interactionType: InteractionTypes.MESSAGE_COMPONENT, + channel: this, + }); + } + + /** + * Collects a single component interaction that passes the filter. + * The Promise will reject if the time expires. + * @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector + * @returns {Promise} + * @example + * // Collect a message component interaction + * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId'; + * channel.awaitMessageComponent({ filter, time: 15_000 }) + * .then(interaction => console.log(`${interaction.customId} was clicked!`)) + * .catch(console.error); + */ + awaitMessageComponent(options = {}) { + const _options = { ...options, max: 1 }; + return new Promise((resolve, reject) => { + const collector = this.createMessageComponentCollector(_options); + collector.once('end', (interactions, reason) => { + const interaction = interactions.first(); + if (interaction) resolve(interaction); + else reject(new Error('INTERACTION_COLLECTOR_ERROR', reason)); + }); + }); + } + + /** + * Bulk deletes given messages that are newer than two weeks. + * @param {Collection|MessageResolvable[]|number} messages + * Messages or number of messages to delete + * @param {boolean} [filterOld=false] Filter messages to remove those which are older than two weeks automatically + * @returns {Promise>} Returns the deleted messages + * @example + * // Bulk delete messages + * channel.bulkDelete(5) + * .then(messages => console.log(`Bulk deleted ${messages.size} messages`)) + * .catch(console.error); + */ + async bulkDelete(messages, filterOld = false) { + if (Array.isArray(messages) || messages instanceof Collection) { + let messageIds = messages instanceof Collection ? [...messages.keys()] : messages.map(m => m.id ?? m); + if (filterOld) { + messageIds = messageIds.filter(id => Date.now() - SnowflakeUtil.timestampFrom(id) < 1_209_600_000); + } + if (messageIds.length === 0) return new Collection(); + if (messageIds.length === 1) { + await this.client.api.channels(this.id).messages(messageIds[0]).delete(); + const message = this.client.actions.MessageDelete.getMessage( + { + message_id: messageIds[0], + }, + this, + ); + return message ? new Collection([[message.id, message]]) : new Collection(); + } + await this.client.api.channels[this.id].messages['bulk-delete'].post({ data: { messages: messageIds } }); + return messageIds.reduce( + (col, id) => + col.set( + id, + this.client.actions.MessageDeleteBulk.getMessage( + { + message_id: id, + }, + this, + ), + ), + new Collection(), + ); + } + if (!isNaN(messages)) { + const msgs = await this.messages.fetch({ limit: messages }); + return this.bulkDelete(msgs, filterOld); + } + throw new TypeError('MESSAGE_BULK_DELETE_TYPE'); + } + + static applyToClass(structure, full = false, ignore = []) { + const props = ['send']; + if (full) { + props.push( + 'lastMessage', + 'lastPinAt', + 'bulkDelete', + 'sendTyping', + 'createMessageCollector', + 'awaitMessages', + 'createMessageComponentCollector', + 'awaitMessageComponent', + ); + } + for (const prop of props) { + if (ignore.includes(prop)) continue; + Object.defineProperty( + structure.prototype, + prop, + Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop), + ); + } + } +} + +module.exports = TextBasedChannel; + +// Fixes Circular +const MessageManager = require('../../managers/MessageManager'); diff --git a/src/util/Constants.js b/src/util/Constants.js index 14f13d7..ba2b91a 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -4,104 +4,104 @@ const process = require('node:process'); const Package = (exports.Package = require('../../package.json')); const { Error, RangeError, TypeError } = require('../errors'); const listUserAgent = [ - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.2 Safari/605.1.15", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362", - "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:69.0) Gecko/20100101 Firefox/69.0", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; rv:68.0) Gecko/20100101 Firefox/68.0", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Safari/605.1.15", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36 OPR/63.0.3368.107", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:69.0) Gecko/20100101 Firefox/69.0", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.2 Safari/605.1.15", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 YaBrowser/19.9.3.314 Yowser/2.5 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko", - "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Safari/605.1.15", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Safari/605.1.15", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64; rv:69.0) Gecko/20100101 Firefox/69.0", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Safari/605.1.15", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.2 Safari/605.1.15", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; rv:69.0) Gecko/20100101 Firefox/69.0", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Safari/605.1.15", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:69.0) Gecko/20100101 Firefox/69.0", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0", - "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:70.0) Gecko/20100101 Firefox/70.0", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/77.0.3865.90 Chrome/77.0.3865.90 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36", - "Mozilla/5.0 (Windows NT 5.1; rv:52.0) Gecko/20100101 Firefox/52.0", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Safari/605.1.15", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Safari/605.1.15", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36 OPR/63.0.3368.107", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:69.0) Gecko/20100101 Firefox/69.0", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0 Waterfox/56.2.14", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3835.0 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:53.0) Gecko/20100101 Firefox/53.0", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393", - "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)", - "Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko", - "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4", - "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", - "Mozilla/5.0 (Linux; Android 6.0.1; SAMSUNG SM-G570Y Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/4.0 Chrome/44.0.2403.133 Mobile Safari/537.36", - "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; FSL 7.0.5.01003)", - "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0", - "Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2.8) Gecko/20100723 Ubuntu/10.04 (lucid) Firefox/3.6.8", - "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", - "Mozilla/4.0 (compatible; MSIE 6.0; MSIE 5.5; Windows NT 5.0) Opera 7.02 Bork-edition [en]", - "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.112 Safari/535.1", - "Mozilla/5.0 (Windows NT 5.1; rv:13.0) Gecko/20100101 Firefox/13.0.1" -] + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.2 Safari/605.1.15', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362', + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:69.0) Gecko/20100101 Firefox/69.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; rv:68.0) Gecko/20100101 Firefox/68.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Safari/605.1.15', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36 OPR/63.0.3368.107', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:69.0) Gecko/20100101 Firefox/69.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.2 Safari/605.1.15', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763', + 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 YaBrowser/19.9.3.314 Yowser/2.5 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko', + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Safari/605.1.15', + 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Safari/605.1.15', + 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64; rv:69.0) Gecko/20100101 Firefox/69.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Safari/605.1.15', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0', + 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.2 Safari/605.1.15', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.1; rv:69.0) Gecko/20100101 Firefox/69.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Safari/605.1.15', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:69.0) Gecko/20100101 Firefox/69.0', + 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0', + 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0', + 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:70.0) Gecko/20100101 Firefox/70.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/77.0.3865.90 Chrome/77.0.3865.90 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + 'Mozilla/5.0 (Windows NT 5.1; rv:52.0) Gecko/20100101 Firefox/52.0', + 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Safari/605.1.15', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Safari/605.1.15', + 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36 OPR/63.0.3368.107', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:69.0) Gecko/20100101 Firefox/69.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0 Waterfox/56.2.14', + 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3835.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:53.0) Gecko/20100101 Firefox/53.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393', + 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)', + 'Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko', + 'Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4', + 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', + 'Mozilla/5.0 (Linux; Android 6.0.1; SAMSUNG SM-G570Y Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/4.0 Chrome/44.0.2403.133 Mobile Safari/537.36', + 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; FSL 7.0.5.01003)', + 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0', + 'Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2.8) Gecko/20100723 Ubuntu/10.04 (lucid) Firefox/3.6.8', + 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)', + 'Mozilla/4.0 (compatible; MSIE 6.0; MSIE 5.5; Windows NT 5.0) Opera 7.02 Bork-edition [en]', + 'Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.112 Safari/535.1', + 'Mozilla/5.0 (Windows NT 5.1; rv:13.0) Gecko/20100101 Firefox/13.0.1', +]; exports.DMScanLevel = { 0: 'NOT_SCAN', @@ -116,36 +116,36 @@ exports.stickerAnimationMode = { }; exports.localeObject = { - da: 'DANISH', - de: 'GERMAN', - 'en-GB': 'ENGLISH_UK', - 'en-US': 'ENGLISH_US', - 'es-ES': 'SPANISH', - fr: 'FRENCH', - hr: 'CROATIAN', - it: 'ITALIAN', - lt: 'LITHUANIAN', - hu: 'HUNGARIAN', - nl: 'DUTCH', - no: 'NORWEGIAN', - pl: 'POLISH', - 'pt-BR': 'BRAZILIAN_PORTUGUESE', - ro: 'ROMANIA_ROMANIAN', - fi: 'FINNISH', - 'sv-SE': 'SWEDISH', - vi: 'VIETNAMESE', - tr: 'TURKISH', - cs: 'CZECH', - el: 'GREEK', - bg: 'BULGARIAN', - ru: 'RUSSIAN', - uk: 'UKRAINIAN', - hi: 'HINDI', - th: 'THAI', - 'zh-CN': 'CHINA_CHINESE', - ja: 'JAPANESE', - 'zh-TW': 'TAIWAN_CHINESE', - ko: 'KOREAN', + da: 'DANISH', + de: 'GERMAN', + 'en-GB': 'ENGLISH_UK', + 'en-US': 'ENGLISH_US', + 'es-ES': 'SPANISH', + fr: 'FRENCH', + hr: 'CROATIAN', + it: 'ITALIAN', + lt: 'LITHUANIAN', + hu: 'HUNGARIAN', + nl: 'DUTCH', + no: 'NORWEGIAN', + pl: 'POLISH', + 'pt-BR': 'BRAZILIAN_PORTUGUESE', + ro: 'ROMANIA_ROMANIAN', + fi: 'FINNISH', + 'sv-SE': 'SWEDISH', + vi: 'VIETNAMESE', + tr: 'TURKISH', + cs: 'CZECH', + el: 'GREEK', + bg: 'BULGARIAN', + ru: 'RUSSIAN', + uk: 'UKRAINIAN', + hi: 'HINDI', + th: 'THAI', + 'zh-CN': 'CHINA_CHINESE', + ja: 'JAPANESE', + 'zh-TW': 'TAIWAN_CHINESE', + ko: 'KOREAN', }; exports.UserAgent = listUserAgent[Math.floor(Math.random() * listUserAgent.length)]; @@ -254,127 +254,127 @@ exports.Status = { }; exports.Opcodes = { - DISPATCH: 0, // # Receive dispatches an event - HEARTBEAT: 1, // # Send/Receive used for ping checking - IDENTIFY: 2, // # Send used for client handshake - STATUS_UPDATE: 3, // # Send used to update the client status - VOICE_STATE_UPDATE: 4, // # Send used to join/move/leave voice channels - VOICE_GUILD_PING: 5, // # Send used for voice ping checking - RESUME: 6, // # Send used to resume a closed connection - RECONNECT: 7, // # Receive used to tell when to reconnect (sometimes...) - REQUEST_GUILD_MEMBERS: 8, // # Send used to request guild members (when searching for members in the search bar of a guild) - INVALID_SESSION: 9, // # Receive used to notify client they have an invalid session id - HELLO: 10, // # Receive sent immediately after connecting, contains heartbeat and server debug information - HEARTBEAT_ACK: 11, // # Sent immediately following a client heartbeat that was received - // GUILD_SYNC: 12, // # Receive guild_sync but not used anymore - /** Add some opcode from Discum + DISPATCH: 0, // # Receive dispatches an event + HEARTBEAT: 1, // # Send/Receive used for ping checking + IDENTIFY: 2, // # Send used for client handshake + STATUS_UPDATE: 3, // # Send used to update the client status + VOICE_STATE_UPDATE: 4, // # Send used to join/move/leave voice channels + VOICE_GUILD_PING: 5, // # Send used for voice ping checking + RESUME: 6, // # Send used to resume a closed connection + RECONNECT: 7, // # Receive used to tell when to reconnect (sometimes...) + REQUEST_GUILD_MEMBERS: 8, // # Send used to request guild members (when searching for members in the search bar of a guild) + INVALID_SESSION: 9, // # Receive used to notify client they have an invalid session id + HELLO: 10, // # Receive sent immediately after connecting, contains heartbeat and server debug information + HEARTBEAT_ACK: 11, // # Sent immediately following a client heartbeat that was received + // GUILD_SYNC: 12, // # Receive guild_sync but not used anymore + /** Add some opcode from Discum /* @extends https://github.com/Merubokkusu/Discord-S.C.U.M/blob/master/discum/gateway/gateway.py#L56 */ - DM_UPDATE: 13, // # Send used to get dm features - LAZY_REQUEST: 14, // # Send discord responds back with GUILD_MEMBER_LIST_UPDATE type SYNC... - LOBBY_CONNECT: 15, - LOBBY_DISCONNECT: 16, - LOBBY_VOICE_STATE_UPDATE: 17, // # Receive - STREAM_CREATE: 18, - STREAM_DELETE: 19, - STREAM_WATCH: 20, - STREAM_PING: 21, // # Send - STREAM_SET_PAUSED: 22, - REQUEST_APPLICATION_COMMANDS: 24, // # Send request application/bot cmds (user, message, and slash cmds) + DM_UPDATE: 13, // # Send used to get dm features + LAZY_REQUEST: 14, // # Send discord responds back with GUILD_MEMBER_LIST_UPDATE type SYNC... + LOBBY_CONNECT: 15, + LOBBY_DISCONNECT: 16, + LOBBY_VOICE_STATE_UPDATE: 17, // # Receive + STREAM_CREATE: 18, + STREAM_DELETE: 19, + STREAM_WATCH: 20, + STREAM_PING: 21, // # Send + STREAM_SET_PAUSED: 22, + REQUEST_APPLICATION_COMMANDS: 24, // # Send request application/bot cmds (user, message, and slash cmds) }; exports.Events = { - RATE_LIMIT: 'rateLimit', - INVALID_REQUEST_WARNING: 'invalidRequestWarning', - API_RESPONSE: 'apiResponse', - API_REQUEST: 'apiRequest', - CLIENT_READY: 'ready', - /** - * @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information. - */ - APPLICATION_COMMAND_CREATE: 'applicationCommandCreate', - /** - * @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information. - */ - APPLICATION_COMMAND_DELETE: 'applicationCommandDelete', - /** - * @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information. - */ - APPLICATION_COMMAND_UPDATE: 'applicationCommandUpdate', - GUILD_CREATE: 'guildCreate', - GUILD_DELETE: 'guildDelete', - GUILD_UPDATE: 'guildUpdate', - GUILD_UNAVAILABLE: 'guildUnavailable', - GUILD_MEMBER_ADD: 'guildMemberAdd', - GUILD_MEMBER_REMOVE: 'guildMemberRemove', - GUILD_MEMBER_UPDATE: 'guildMemberUpdate', - GUILD_MEMBER_AVAILABLE: 'guildMemberAvailable', - GUILD_MEMBERS_CHUNK: 'guildMembersChunk', - GUILD_MEMBER_LIST_UPDATE: 'guildMemberListUpdate', - GUILD_INTEGRATIONS_UPDATE: 'guildIntegrationsUpdate', - GUILD_ROLE_CREATE: 'roleCreate', - GUILD_ROLE_DELETE: 'roleDelete', - INVITE_CREATE: 'inviteCreate', - INVITE_DELETE: 'inviteDelete', - GUILD_ROLE_UPDATE: 'roleUpdate', - GUILD_EMOJI_CREATE: 'emojiCreate', - GUILD_EMOJI_DELETE: 'emojiDelete', - GUILD_EMOJI_UPDATE: 'emojiUpdate', - GUILD_BAN_ADD: 'guildBanAdd', - GUILD_BAN_REMOVE: 'guildBanRemove', - CHANNEL_CREATE: 'channelCreate', - CHANNEL_DELETE: 'channelDelete', - CHANNEL_UPDATE: 'channelUpdate', - CHANNEL_PINS_UPDATE: 'channelPinsUpdate', - MESSAGE_CREATE: 'messageCreate', - MESSAGE_DELETE: 'messageDelete', - MESSAGE_UPDATE: 'messageUpdate', - MESSAGE_BULK_DELETE: 'messageDeleteBulk', - MESSAGE_REACTION_ADD: 'messageReactionAdd', - MESSAGE_REACTION_REMOVE: 'messageReactionRemove', - MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll', - MESSAGE_REACTION_REMOVE_EMOJI: 'messageReactionRemoveEmoji', - THREAD_CREATE: 'threadCreate', - THREAD_DELETE: 'threadDelete', - THREAD_UPDATE: 'threadUpdate', - THREAD_LIST_SYNC: 'threadListSync', - THREAD_MEMBER_UPDATE: 'threadMemberUpdate', - THREAD_MEMBERS_UPDATE: 'threadMembersUpdate', - USER_UPDATE: 'userUpdate', - PRESENCE_UPDATE: 'presenceUpdate', - VOICE_SERVER_UPDATE: 'voiceServerUpdate', - VOICE_STATE_UPDATE: 'voiceStateUpdate', - TYPING_START: 'typingStart', - WEBHOOKS_UPDATE: 'webhookUpdate', - INTERACTION_CREATE: 'interactionCreate', - INTERACTION_SUCCESS: 'interactionSuccess', - INTERACTION_FAILED: 'interactionFailed', - ERROR: 'error', - WARN: 'warn', - DEBUG: 'debug', - CACHE_SWEEP: 'cacheSweep', - SHARD_DISCONNECT: 'shardDisconnect', - SHARD_ERROR: 'shardError', - SHARD_RECONNECTING: 'shardReconnecting', - SHARD_READY: 'shardReady', - SHARD_RESUME: 'shardResume', - INVALIDATED: 'invalidated', - RAW: 'raw', - STAGE_INSTANCE_CREATE: 'stageInstanceCreate', - STAGE_INSTANCE_UPDATE: 'stageInstanceUpdate', - STAGE_INSTANCE_DELETE: 'stageInstanceDelete', - GUILD_STICKER_CREATE: 'stickerCreate', - GUILD_STICKER_DELETE: 'stickerDelete', - GUILD_STICKER_UPDATE: 'stickerUpdate', - GUILD_SCHEDULED_EVENT_CREATE: 'guildScheduledEventCreate', - GUILD_SCHEDULED_EVENT_UPDATE: 'guildScheduledEventUpdate', - GUILD_SCHEDULED_EVENT_DELETE: 'guildScheduledEventDelete', - GUILD_SCHEDULED_EVENT_USER_ADD: 'guildScheduledEventUserAdd', - GUILD_SCHEDULED_EVENT_USER_REMOVE: 'guildScheduledEventUserRemove', - RELATIONSHIP_ADD: 'relationshipAdd', - RELATIONSHIP_REMOVE: 'relationshipRemove', - /* Add */ - UNHANDLED_PACKET: 'unhandledPacket', + RATE_LIMIT: 'rateLimit', + INVALID_REQUEST_WARNING: 'invalidRequestWarning', + API_RESPONSE: 'apiResponse', + API_REQUEST: 'apiRequest', + CLIENT_READY: 'ready', + /** + * @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information. + */ + APPLICATION_COMMAND_CREATE: 'applicationCommandCreate', + /** + * @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information. + */ + APPLICATION_COMMAND_DELETE: 'applicationCommandDelete', + /** + * @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information. + */ + APPLICATION_COMMAND_UPDATE: 'applicationCommandUpdate', + GUILD_CREATE: 'guildCreate', + GUILD_DELETE: 'guildDelete', + GUILD_UPDATE: 'guildUpdate', + GUILD_UNAVAILABLE: 'guildUnavailable', + GUILD_MEMBER_ADD: 'guildMemberAdd', + GUILD_MEMBER_REMOVE: 'guildMemberRemove', + GUILD_MEMBER_UPDATE: 'guildMemberUpdate', + GUILD_MEMBER_AVAILABLE: 'guildMemberAvailable', + GUILD_MEMBERS_CHUNK: 'guildMembersChunk', + GUILD_MEMBER_LIST_UPDATE: 'guildMemberListUpdate', + GUILD_INTEGRATIONS_UPDATE: 'guildIntegrationsUpdate', + GUILD_ROLE_CREATE: 'roleCreate', + GUILD_ROLE_DELETE: 'roleDelete', + INVITE_CREATE: 'inviteCreate', + INVITE_DELETE: 'inviteDelete', + GUILD_ROLE_UPDATE: 'roleUpdate', + GUILD_EMOJI_CREATE: 'emojiCreate', + GUILD_EMOJI_DELETE: 'emojiDelete', + GUILD_EMOJI_UPDATE: 'emojiUpdate', + GUILD_BAN_ADD: 'guildBanAdd', + GUILD_BAN_REMOVE: 'guildBanRemove', + CHANNEL_CREATE: 'channelCreate', + CHANNEL_DELETE: 'channelDelete', + CHANNEL_UPDATE: 'channelUpdate', + CHANNEL_PINS_UPDATE: 'channelPinsUpdate', + MESSAGE_CREATE: 'messageCreate', + MESSAGE_DELETE: 'messageDelete', + MESSAGE_UPDATE: 'messageUpdate', + MESSAGE_BULK_DELETE: 'messageDeleteBulk', + MESSAGE_REACTION_ADD: 'messageReactionAdd', + MESSAGE_REACTION_REMOVE: 'messageReactionRemove', + MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll', + MESSAGE_REACTION_REMOVE_EMOJI: 'messageReactionRemoveEmoji', + THREAD_CREATE: 'threadCreate', + THREAD_DELETE: 'threadDelete', + THREAD_UPDATE: 'threadUpdate', + THREAD_LIST_SYNC: 'threadListSync', + THREAD_MEMBER_UPDATE: 'threadMemberUpdate', + THREAD_MEMBERS_UPDATE: 'threadMembersUpdate', + USER_UPDATE: 'userUpdate', + PRESENCE_UPDATE: 'presenceUpdate', + VOICE_SERVER_UPDATE: 'voiceServerUpdate', + VOICE_STATE_UPDATE: 'voiceStateUpdate', + TYPING_START: 'typingStart', + WEBHOOKS_UPDATE: 'webhookUpdate', + INTERACTION_CREATE: 'interactionCreate', + INTERACTION_SUCCESS: 'interactionSuccess', + INTERACTION_FAILED: 'interactionFailed', + ERROR: 'error', + WARN: 'warn', + DEBUG: 'debug', + CACHE_SWEEP: 'cacheSweep', + SHARD_DISCONNECT: 'shardDisconnect', + SHARD_ERROR: 'shardError', + SHARD_RECONNECTING: 'shardReconnecting', + SHARD_READY: 'shardReady', + SHARD_RESUME: 'shardResume', + INVALIDATED: 'invalidated', + RAW: 'raw', + STAGE_INSTANCE_CREATE: 'stageInstanceCreate', + STAGE_INSTANCE_UPDATE: 'stageInstanceUpdate', + STAGE_INSTANCE_DELETE: 'stageInstanceDelete', + GUILD_STICKER_CREATE: 'stickerCreate', + GUILD_STICKER_DELETE: 'stickerDelete', + GUILD_STICKER_UPDATE: 'stickerUpdate', + GUILD_SCHEDULED_EVENT_CREATE: 'guildScheduledEventCreate', + GUILD_SCHEDULED_EVENT_UPDATE: 'guildScheduledEventUpdate', + GUILD_SCHEDULED_EVENT_DELETE: 'guildScheduledEventDelete', + GUILD_SCHEDULED_EVENT_USER_ADD: 'guildScheduledEventUserAdd', + GUILD_SCHEDULED_EVENT_USER_REMOVE: 'guildScheduledEventUserRemove', + RELATIONSHIP_ADD: 'relationshipAdd', + RELATIONSHIP_REMOVE: 'relationshipRemove', + /* Add */ + UNHANDLED_PACKET: 'unhandledPacket', }; exports.ShardEvents = { @@ -1330,8 +1330,8 @@ exports.RelationshipTypes = createEnum([null, 'FRIEND', 'BLOCKED', 'INCOMING_REQ exports.Relationship = { 0: 'NONE', - 1: 'FRIEND', - 2: 'BLOCKED', + 1: 'FRIEND', + 2: 'BLOCKED', 3: 'INCOMING_REQUEST', 4: 'OUTGOING_REQUEST', }; @@ -1374,7 +1374,7 @@ exports.HypeSquadOptions = createEnum(['LEAVE', 'HOUSE_BRAVERY', 'HOUSE_BRILLIAN exports._cleanupSymbol = Symbol('djsCleanup'); function keyMirror(arr) { - let tmp = Object.create(null); + const tmp = Object.create(null); for (const value of arr) tmp[value] = value; return tmp; } diff --git a/src/util/Options.js b/src/util/Options.js index 19554e4..a742a75 100644 --- a/src/util/Options.js +++ b/src/util/Options.js @@ -132,7 +132,7 @@ class Options extends null { */ static createDefault() { return { - jsonTransformer: (object) => JSONBig.stringify(object), + jsonTransformer: object => JSONBig.stringify(object), checkUpdate: true, readyStatus: false, autoCookie: true, @@ -143,14 +143,7 @@ class Options extends null { messageSweepInterval: 0, invalidRequestWarningInterval: 0, intents: 65535, - partials: [ - 'USER', - 'CHANNEL', - 'GUILD_MEMBER', - 'MESSAGE', - 'REACTION', - 'GUILD_SCHEDULED_EVENT', - ], // Enable the partials + partials: ['USER', 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE', 'REACTION', 'GUILD_SCHEDULED_EVENT'], // Enable the partials restWsBridgeTimeout: 5_000, restRequestTimeout: 15_000, restGlobalRateLimit: 0, @@ -165,9 +158,9 @@ class Options extends null { large_threshold: 50, compress: false, properties: { - //$os: 'iPhone14,5', - //$browser: 'Discord iOS', - //$device: 'iPhone14,5 OS 15.2', + // $os: 'iPhone14,5', + // $browser: 'Discord iOS', + // $device: 'iPhone14,5 OS 15.2', $os: 'Windows', $browser: 'Discord Client', $device: 'ASUS ROG Phone 5', @@ -182,8 +175,7 @@ class Options extends null { 'Cache-Control': 'no-cache', Pragma: 'no-cache', Referer: 'https://discord.com/channels/@me', - 'Sec-Ch-Ua': - '"Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100', + 'Sec-Ch-Ua': '"Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100', 'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Platform': '"Windows"', 'Sec-Fetch-Dest': 'empty', diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..3592420 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,47 @@ +{ + // Mapped from https://www.typescriptlang.org/tsconfig + "compilerOptions": { + // Type Checking + "allowUnreachableCode": false, + "allowUnusedLabels": false, + // if true: conflicts with discord-api-types + "exactOptionalPropertyTypes": false, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "strict": true, + "useUnknownInCatchVariables": true, + // Modules + "module": "CommonJS", + "moduleResolution": "node", + "resolveJsonModule": true, + // Emit + "declaration": false, + "importHelpers": true, + "importsNotUsedAsValues": "error", + "inlineSources": false, + "newLine": "lf", + "noEmitHelpers": true, + "outDir": "dist", + "preserveConstEnums": true, + "removeComments": false, + "sourceMap": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + // Language and Environment + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "ESNext" + ], + "target": "ES2020", + "useDefineForClassFields": true, + // Output Formatting + "pretty": false, + // Completeness + "skipLibCheck": false, + "skipDefaultLibCheck": true + } +} \ No newline at end of file diff --git a/typings/enums.d.ts b/typings/enums.d.ts index 0f19a56..463d7b2 100644 --- a/typings/enums.d.ts +++ b/typings/enums.d.ts @@ -30,36 +30,36 @@ export const enum relationshipsType { } export const enum localeSetting { - DANISH = 'da', - GERMAN = 'de', - ENGLISH_UK = 'en-GB', - ENGLISH_US = 'en-US', - SPANISH = 'es-ES', - FRENCH = 'fr', - CROATIAN = 'hr', - ITALIAN = 'it', - LITHUANIAN = 'lt', - HUNGARIAN = 'hu', - DUTCH = 'nl', - NORWEGIAN = 'no', - POLISH = 'pl', - BRAZILIAN_PORTUGUESE = 'pt-BR', - ROMANIA_ROMANIAN = 'ro', - FINNISH = 'fi', - SWEDISH = 'sv-SE', - VIETNAMESE = 'vi', - TURKISH = 'tr', - CZECH = 'cs', - GREEK = 'el', - BULGARIAN = 'bg', - RUSSIAN = 'ru', - UKRAINIAN = 'uk', - HINDI = 'hi', - THAI = 'th', - CHINA_CHINESE = 'zh-CN', - JAPANESE = 'ja', - TAIWAN_CHINESE = 'zh-TW', - KOREAN = 'ko', + DANISH = 'da', + GERMAN = 'de', + ENGLISH_UK = 'en-GB', + ENGLISH_US = 'en-US', + SPANISH = 'es-ES', + FRENCH = 'fr', + CROATIAN = 'hr', + ITALIAN = 'it', + LITHUANIAN = 'lt', + HUNGARIAN = 'hu', + DUTCH = 'nl', + NORWEGIAN = 'no', + POLISH = 'pl', + BRAZILIAN_PORTUGUESE = 'pt-BR', + ROMANIA_ROMANIAN = 'ro', + FINNISH = 'fi', + SWEDISH = 'sv-SE', + VIETNAMESE = 'vi', + TURKISH = 'tr', + CZECH = 'cs', + GREEK = 'el', + BULGARIAN = 'bg', + RUSSIAN = 'ru', + UKRAINIAN = 'uk', + HINDI = 'hi', + THAI = 'th', + CHINA_CHINESE = 'zh-CN', + JAPANESE = 'ja', + TAIWAN_CHINESE = 'zh-TW', + KOREAN = 'ko', } export const enum ApplicationCommandTypes { diff --git a/typings/index.d.ts b/typings/index.d.ts index e249320..4fbdbf5 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -147,7 +147,7 @@ import { RawWidgetData, RawWidgetMemberData, } from './rawDataTypes'; -import { RelationshipType } from '../src/util/Constants'; +import { RelationshipTypes } from '../src/util/Constants'; //#region Classes @@ -1008,7 +1008,7 @@ export class Guild extends AnonymousGuild { public setIcon(icon: BufferResolvable | Base64Resolvable | null, reason?: string): Promise; public setName(name: string, reason?: string): Promise; public setOwner(owner: GuildMemberResolvable, reason?: string): Promise; - public setPosition(position: number, type: FOLDER | HOME, folderID?: FolderID): Promise; + public setPosition(position: number, type: 'FOLDER' | 'HOME', folderID?: FolderID): Promise; public setPreferredLocale(preferredLocale: string, reason?: string): Promise; public setPublicUpdatesChannel(publicUpdatesChannel: TextChannelResolvable | null, reason?: string): Promise; /** @deprecated Use {@link RoleManager.setPositions} instead */ @@ -1019,7 +1019,12 @@ export class Guild extends AnonymousGuild { public setSystemChannelFlags(systemChannelFlags: SystemChannelFlagsResolvable, reason?: string): Promise; public setVerificationLevel(verificationLevel: VerificationLevel | number, reason?: string): Promise; public setPremiumProgressBarEnabled(enabled?: boolean, reason?: string): Promise; - public setCommunity(stats: boolean, publicUpdatesChannel: TextChannelResolvable, rulesChannel: TextChannelResolvable, reason?: string): Promise; + public setCommunity( + stats: boolean, + publicUpdatesChannel: TextChannelResolvable, + rulesChannel: TextChannelResolvable, + reason?: string, + ): Promise; public setWidgetSettings(settings: GuildWidgetSettingsData, reason?: string): Promise; public toJSON(): unknown; } @@ -1043,17 +1048,17 @@ export class GuildAuditLogs { export class GuildAuditLogsEntry< TActionRaw extends GuildAuditLogsResolvable = 'ALL', TAction = TActionRaw extends keyof GuildAuditLogsIds - ? GuildAuditLogsIds[TActionRaw] - : TActionRaw extends null - ? 'ALL' - : TActionRaw, + ? GuildAuditLogsIds[TActionRaw] + : TActionRaw extends null + ? 'ALL' + : TActionRaw, TActionType extends GuildAuditLogsActionType = TAction extends keyof GuildAuditLogsTypes - ? GuildAuditLogsTypes[TAction][1] - : 'ALL', + ? GuildAuditLogsTypes[TAction][1] + : 'ALL', TTargetType extends GuildAuditLogsTarget = TAction extends keyof GuildAuditLogsTypes - ? GuildAuditLogsTypes[TAction][0] - : 'UNKNOWN', - > { + ? GuildAuditLogsTypes[TAction][0] + : 'UNKNOWN', +> { private constructor(logs: GuildAuditLogs, guild: Guild, data: RawGuildAuditLogEntryData); public action: TAction; public actionType: TActionType; @@ -1291,7 +1296,7 @@ export class HTTPError extends Error { } // tslint:disable-next-line:no-empty-interface - Merge RateLimitData into RateLimitError to not have to type it again -export interface RateLimitError extends RateLimitData { } +export interface RateLimitError extends RateLimitData {} export class RateLimitError extends Error { private constructor(data: RateLimitData); public name: 'RateLimitError'; @@ -1344,7 +1349,7 @@ export type CacheTypeReducer< RawType = CachedType, PresentType = CachedType | RawType, Fallback = PresentType | null, - > = [State] extends ['cached'] +> = [State] extends ['cached'] ? CachedType : [State] extends ['raw'] ? RawType @@ -1490,25 +1495,25 @@ export class LimitedCollection extends Collection { export type MessageCollectorOptionsParams = | { - componentType?: T; - } & MessageComponentCollectorOptions[T]>; + componentType?: T; + } & MessageComponentCollectorOptions[T]>; export type MessageChannelCollectorOptionsParams< T extends MessageComponentTypeResolvable, Cached extends boolean = boolean, - > = +> = | { - componentType?: T; - } & MessageChannelComponentCollectorOptions[T]>; + componentType?: T; + } & MessageChannelComponentCollectorOptions[T]>; export type AwaitMessageCollectorOptionsParams< T extends MessageComponentTypeResolvable, Cached extends boolean = boolean, - > = +> = | { componentType?: T } & Pick< - InteractionCollectorOptions[T]>, - keyof AwaitMessageComponentOptions - >; + InteractionCollectorOptions[T]>, + keyof AwaitMessageComponentOptions + >; export interface StringMappedInteractionTypes { BUTTON: ButtonInteraction; @@ -1600,8 +1605,11 @@ export class Message extends Base { public unpin(): Promise; public inGuild(): this is Message & this; // Added - public clickButton(buttonID: String): Promise - public selectMenu(menuID: String | Array, options: Array): Promise + public clickButton(buttonID: String): Promise; + public selectMenu( + menuID: String | Array, + options: Array, + ): Promise; public contextMenu(botID: DiscordBotID, commandName: String): Promise; } @@ -1717,7 +1725,7 @@ export class MessageComponentInteraction e export class MessageContextMenuInteraction< Cached extends CacheType = CacheType, - > extends ContextMenuInteraction { +> extends ContextMenuInteraction { public readonly targetMessage: NonNullable['message']>; public inGuild(): this is MessageContextMenuInteraction<'present'>; public inCachedGuild(): this is MessageContextMenuInteraction<'cached'>; @@ -2973,7 +2981,7 @@ export class ApplicationCommandManager< ApplicationCommandScope = ApplicationCommand<{ guild: GuildResolvable }>, PermissionsOptionsExtras = { guild: GuildResolvable }, PermissionsGuildType = null, - > extends CachedManager { +> extends CachedManager { protected constructor(client: Client, iterable?: Iterable, user: User); public permissions: ApplicationCommandPermissionsManager< { command?: ApplicationCommandResolvable } & PermissionsOptionsExtras, @@ -3020,8 +3028,11 @@ export class ApplicationCommandPermissionsManager< FullPermissionsOptions, GuildType, CommandIdType, - > extends BaseManager { - private constructor(manager: ApplicationCommandManager | GuildApplicationCommandManager | ApplicationCommand, user: User); +> extends BaseManager { + private constructor( + manager: ApplicationCommandManager | GuildApplicationCommandManager | ApplicationCommand, + user: User, + ); private manager: ApplicationCommandManager | GuildApplicationCommandManager | ApplicationCommand; public client: Client; @@ -3037,13 +3048,13 @@ export class ApplicationCommandPermissionsManager< public remove( options: | (FetchSingleOptions & { - users: UserResolvable | UserResolvable[]; - roles?: RoleResolvable | RoleResolvable[]; - }) + users: UserResolvable | UserResolvable[]; + roles?: RoleResolvable | RoleResolvable[]; + }) | (FetchSingleOptions & { - users?: UserResolvable | UserResolvable[]; - roles: RoleResolvable | RoleResolvable[]; - }), + users?: UserResolvable | UserResolvable[]; + roles: RoleResolvable | RoleResolvable[]; + }), ): Promise; public set( options: FetchSingleOptions & { permissions: ApplicationCommandPermissionData[] }, @@ -3092,8 +3103,16 @@ export class ClientUserSettingManager { public afkTimeout: number | null; // second public stickerAnimationMode: stickerAnimationMode; public showEmojiReactions: boolean | null; - public customStatus: { text?: string, expires_at?: string | null, emoji_name?: string, emoji_id?: Snowflake | null, status?: PresenceStatusData } | object; - public addFriendFrom: { all?: boolean, mutual_friends?: boolean, mututal_guilds?: boolean } | object; + public customStatus: + | { + text?: string; + expires_at?: string | null; + emoji_name?: string; + emoji_id?: Snowflake | null; + status?: PresenceStatusData; + } + | object; + public addFriendFrom: { all?: boolean; mutual_friends?: boolean; mututal_guilds?: boolean } | object; public guildMetadata: Collection; public disableDMfromServer: Collection; public fetch(): Promise; @@ -3227,7 +3246,7 @@ export class GuildScheduledEventManager extends CachedManager< public fetch(): Promise>; public fetch< T extends GuildScheduledEventResolvable | FetchGuildScheduledEventOptions | FetchGuildScheduledEventsOptions, - >(options?: T): Promise>; + >(options?: T): Promise>; public edit>( guildScheduledEvent: GuildScheduledEventResolvable, options: GuildScheduledEventEditOptions, @@ -3757,12 +3776,12 @@ export interface ApplicationCommandChannelOption extends BaseApplicationCommandO export interface ApplicationCommandAutocompleteOption extends Omit { type: - | 'STRING' - | 'NUMBER' - | 'INTEGER' - | ApplicationCommandOptionTypes.STRING - | ApplicationCommandOptionTypes.NUMBER - | ApplicationCommandOptionTypes.INTEGER; + | 'STRING' + | 'NUMBER' + | 'INTEGER' + | ApplicationCommandOptionTypes.STRING + | ApplicationCommandOptionTypes.NUMBER + | ApplicationCommandOptionTypes.INTEGER; autocomplete: true; } @@ -3912,11 +3931,11 @@ export interface BaseFetchOptions { } export interface guildSearchInteraction { - type?: ApplicationCommandTypes, - query?: String | void, - limit?: Number, - offset?: Number, - botID?: Array, + type?: ApplicationCommandTypes; + query?: String | void; + limit?: Number; + offset?: Number; + botID?: Array; } export interface BaseMessageComponentOptions { @@ -3970,8 +3989,8 @@ export type CacheFactory = ( export type CacheWithLimitsOptions = { [K in keyof Caches]?: Caches[K][0]['prototype'] extends DataManager - ? LimitedCollectionOptions | number - : never; + ? LimitedCollectionOptions | number + : never; }; export interface CategoryCreateChannelOptions { @@ -4089,7 +4108,7 @@ export interface ClientEvents extends BaseClientEvents { members: Collection, guild: Guild, data: {}, // see: https://luna.gitlab.io/discord-unofficial-docs/lazy_guilds.html - ] + ]; guildUpdate: [oldGuild: Guild, newGuild: Guild]; inviteCreate: [invite: Invite]; inviteDelete: [invite: Invite]; @@ -4127,9 +4146,9 @@ export interface ClientEvents extends BaseClientEvents { webhookUpdate: [channel: TextChannel | NewsChannel]; /** @deprecated Use interactionCreate instead */ interaction: [interaction: Interaction]; - interactionCreate: [interaction: Interaction | { nonce: Snowflake, id: Snowflake }]; - interactionSuccess: [interaction: { nonce: Snowflake, id: Snowflake }]; - interactionFailed: [interaction: { nonce: Snowflake, id: Snowflake }]; + interactionCreate: [interaction: Interaction | { nonce: Snowflake; id: Snowflake }]; + interactionSuccess: [interaction: { nonce: Snowflake; id: Snowflake }]; + interactionFailed: [interaction: { nonce: Snowflake; id: Snowflake }]; shardDisconnect: [closeEvent: CloseEvent, shardId: number]; shardError: [error: Error, shardId: number]; shardReady: [shardId: number, unavailableGuilds: Set | undefined]; @@ -4146,20 +4165,9 @@ export interface ClientEvents extends BaseClientEvents { guildScheduledEventDelete: [guildScheduledEvent: GuildScheduledEvent]; guildScheduledEventUserAdd: [guildScheduledEvent: GuildScheduledEvent, user: User]; guildScheduledEventUserRemove: [guildScheduledEvent: GuildScheduledEvent, user: User]; - relationshipAdd: [ - id: Snowflake, - type: RelationshipType, - user: User, - ]; - relationshipRemove: [ - id: Snowflake, - type: RelationshipType, - user: User, - ]; - unhandledPacket: [ - packet: { op: GatewayOpcodes | number, d?: any, s?: number, t?: string }, - shard: WebSocketShard, - ]; + relationshipAdd: [id: Snowflake, type: RelationshipTypes, user: User]; + relationshipRemove: [id: Snowflake, type: RelationshipTypes, user: User]; + unhandledPacket: [packet: { op: GatewayOpcodes | number; d?: any; s?: number; t?: string }, shard: WebSocketShard]; } export interface ClientFetchInviteOptions { @@ -4339,80 +4347,80 @@ export interface ConstantsEvents { /** @deprecated See [this issue](https://github.com/discord/discord-api-docs/issues/3690) for more information. */ APPLICATION_COMMAND_DELETE: 'applicationCommandDelete'; /** @deprecated See [this issue](https://github.com/discord/discord-api-docs/issues/3690) for more information. */ - APPLICATION_COMMAND_UPDATE: 'applicationCommandUpdate', - GUILD_CREATE: 'guildCreate', - GUILD_DELETE: 'guildDelete', - GUILD_UPDATE: 'guildUpdate', - GUILD_UNAVAILABLE: 'guildUnavailable', - GUILD_MEMBER_ADD: 'guildMemberAdd', - GUILD_MEMBER_REMOVE: 'guildMemberRemove', - GUILD_MEMBER_UPDATE: 'guildMemberUpdate', - GUILD_MEMBER_AVAILABLE: 'guildMemberAvailable', - GUILD_MEMBERS_CHUNK: 'guildMembersChunk', - GUILD_MEMBER_LIST_UPDATE: 'guildMemberListUpdate', - GUILD_INTEGRATIONS_UPDATE: 'guildIntegrationsUpdate', - GUILD_ROLE_CREATE: 'roleCreate', - GUILD_ROLE_DELETE: 'roleDelete', - INVITE_CREATE: 'inviteCreate', - INVITE_DELETE: 'inviteDelete', - GUILD_ROLE_UPDATE: 'roleUpdate', - GUILD_EMOJI_CREATE: 'emojiCreate', - GUILD_EMOJI_DELETE: 'emojiDelete', - GUILD_EMOJI_UPDATE: 'emojiUpdate', - GUILD_BAN_ADD: 'guildBanAdd', - GUILD_BAN_REMOVE: 'guildBanRemove', - CHANNEL_CREATE: 'channelCreate', - CHANNEL_DELETE: 'channelDelete', - CHANNEL_UPDATE: 'channelUpdate', - CHANNEL_PINS_UPDATE: 'channelPinsUpdate', - MESSAGE_CREATE: 'messageCreate', - MESSAGE_DELETE: 'messageDelete', - MESSAGE_UPDATE: 'messageUpdate', - MESSAGE_BULK_DELETE: 'messageDeleteBulk', - MESSAGE_REACTION_ADD: 'messageReactionAdd', - MESSAGE_REACTION_REMOVE: 'messageReactionRemove', - MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll', - MESSAGE_REACTION_REMOVE_EMOJI: 'messageReactionRemoveEmoji', - THREAD_CREATE: 'threadCreate', - THREAD_DELETE: 'threadDelete', - THREAD_UPDATE: 'threadUpdate', - THREAD_LIST_SYNC: 'threadListSync', - THREAD_MEMBER_UPDATE: 'threadMemberUpdate', - THREAD_MEMBERS_UPDATE: 'threadMembersUpdate', - USER_UPDATE: 'userUpdate', - PRESENCE_UPDATE: 'presenceUpdate', - VOICE_SERVER_UPDATE: 'voiceServerUpdate', - VOICE_STATE_UPDATE: 'voiceStateUpdate', - TYPING_START: 'typingStart', - WEBHOOKS_UPDATE: 'webhookUpdate', - INTERACTION_CREATE: 'interactionCreate', - INTERACTION_SUCCESS: 'interactionSuccess', - INTERACTION_FAILED: 'interactionFailed', - ERROR: 'error', - WARN: 'warn', - DEBUG: 'debug', - CACHE_SWEEP: 'cacheSweep', - SHARD_DISCONNECT: 'shardDisconnect', - SHARD_ERROR: 'shardError', - SHARD_RECONNECTING: 'shardReconnecting', - SHARD_READY: 'shardReady', - SHARD_RESUME: 'shardResume', - INVALIDATED: 'invalidated', - RAW: 'raw', - STAGE_INSTANCE_CREATE: 'stageInstanceCreate', - STAGE_INSTANCE_UPDATE: 'stageInstanceUpdate', - STAGE_INSTANCE_DELETE: 'stageInstanceDelete', - GUILD_STICKER_CREATE: 'stickerCreate', - GUILD_STICKER_DELETE: 'stickerDelete', - GUILD_STICKER_UPDATE: 'stickerUpdate', - GUILD_SCHEDULED_EVENT_CREATE: 'guildScheduledEventCreate', - GUILD_SCHEDULED_EVENT_UPDATE: 'guildScheduledEventUpdate', - GUILD_SCHEDULED_EVENT_DELETE: 'guildScheduledEventDelete', - GUILD_SCHEDULED_EVENT_USER_ADD: 'guildScheduledEventUserAdd', - GUILD_SCHEDULED_EVENT_USER_REMOVE: 'guildScheduledEventUserRemove', - RELATIONSHIP_ADD: 'relationshipAdd', - RELATIONSHIP_REMOVE: 'relationshipRemove', - UNHANDLED_PACKET: 'unhandledPacket', + APPLICATION_COMMAND_UPDATE: 'applicationCommandUpdate'; + GUILD_CREATE: 'guildCreate'; + GUILD_DELETE: 'guildDelete'; + GUILD_UPDATE: 'guildUpdate'; + GUILD_UNAVAILABLE: 'guildUnavailable'; + GUILD_MEMBER_ADD: 'guildMemberAdd'; + GUILD_MEMBER_REMOVE: 'guildMemberRemove'; + GUILD_MEMBER_UPDATE: 'guildMemberUpdate'; + GUILD_MEMBER_AVAILABLE: 'guildMemberAvailable'; + GUILD_MEMBERS_CHUNK: 'guildMembersChunk'; + GUILD_MEMBER_LIST_UPDATE: 'guildMemberListUpdate'; + GUILD_INTEGRATIONS_UPDATE: 'guildIntegrationsUpdate'; + GUILD_ROLE_CREATE: 'roleCreate'; + GUILD_ROLE_DELETE: 'roleDelete'; + INVITE_CREATE: 'inviteCreate'; + INVITE_DELETE: 'inviteDelete'; + GUILD_ROLE_UPDATE: 'roleUpdate'; + GUILD_EMOJI_CREATE: 'emojiCreate'; + GUILD_EMOJI_DELETE: 'emojiDelete'; + GUILD_EMOJI_UPDATE: 'emojiUpdate'; + GUILD_BAN_ADD: 'guildBanAdd'; + GUILD_BAN_REMOVE: 'guildBanRemove'; + CHANNEL_CREATE: 'channelCreate'; + CHANNEL_DELETE: 'channelDelete'; + CHANNEL_UPDATE: 'channelUpdate'; + CHANNEL_PINS_UPDATE: 'channelPinsUpdate'; + MESSAGE_CREATE: 'messageCreate'; + MESSAGE_DELETE: 'messageDelete'; + MESSAGE_UPDATE: 'messageUpdate'; + MESSAGE_BULK_DELETE: 'messageDeleteBulk'; + MESSAGE_REACTION_ADD: 'messageReactionAdd'; + MESSAGE_REACTION_REMOVE: 'messageReactionRemove'; + MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll'; + MESSAGE_REACTION_REMOVE_EMOJI: 'messageReactionRemoveEmoji'; + THREAD_CREATE: 'threadCreate'; + THREAD_DELETE: 'threadDelete'; + THREAD_UPDATE: 'threadUpdate'; + THREAD_LIST_SYNC: 'threadListSync'; + THREAD_MEMBER_UPDATE: 'threadMemberUpdate'; + THREAD_MEMBERS_UPDATE: 'threadMembersUpdate'; + USER_UPDATE: 'userUpdate'; + PRESENCE_UPDATE: 'presenceUpdate'; + VOICE_SERVER_UPDATE: 'voiceServerUpdate'; + VOICE_STATE_UPDATE: 'voiceStateUpdate'; + TYPING_START: 'typingStart'; + WEBHOOKS_UPDATE: 'webhookUpdate'; + INTERACTION_CREATE: 'interactionCreate'; + INTERACTION_SUCCESS: 'interactionSuccess'; + INTERACTION_FAILED: 'interactionFailed'; + ERROR: 'error'; + WARN: 'warn'; + DEBUG: 'debug'; + CACHE_SWEEP: 'cacheSweep'; + SHARD_DISCONNECT: 'shardDisconnect'; + SHARD_ERROR: 'shardError'; + SHARD_RECONNECTING: 'shardReconnecting'; + SHARD_READY: 'shardReady'; + SHARD_RESUME: 'shardResume'; + INVALIDATED: 'invalidated'; + RAW: 'raw'; + STAGE_INSTANCE_CREATE: 'stageInstanceCreate'; + STAGE_INSTANCE_UPDATE: 'stageInstanceUpdate'; + STAGE_INSTANCE_DELETE: 'stageInstanceDelete'; + GUILD_STICKER_CREATE: 'stickerCreate'; + GUILD_STICKER_DELETE: 'stickerDelete'; + GUILD_STICKER_UPDATE: 'stickerUpdate'; + GUILD_SCHEDULED_EVENT_CREATE: 'guildScheduledEventCreate'; + GUILD_SCHEDULED_EVENT_UPDATE: 'guildScheduledEventUpdate'; + GUILD_SCHEDULED_EVENT_DELETE: 'guildScheduledEventDelete'; + GUILD_SCHEDULED_EVENT_USER_ADD: 'guildScheduledEventUserAdd'; + GUILD_SCHEDULED_EVENT_USER_REMOVE: 'guildScheduledEventUserRemove'; + RELATIONSHIP_ADD: 'relationshipAdd'; + RELATIONSHIP_REMOVE: 'relationshipRemove'; + UNHANDLED_PACKET: 'unhandledPacket'; } export interface ConstantsOpcodes { @@ -4750,20 +4758,20 @@ export interface GuildAuditLogsEntryExtraField { MESSAGE_UNPIN: { channel: GuildTextBasedChannel | { id: Snowflake }; messageId: Snowflake }; MEMBER_DISCONNECT: { count: number }; CHANNEL_OVERWRITE_CREATE: - | Role - | GuildMember - | { id: Snowflake; name: string; type: OverwriteTypes.role } - | { id: Snowflake; type: OverwriteTypes.member }; + | Role + | GuildMember + | { id: Snowflake; name: string; type: OverwriteTypes.role } + | { id: Snowflake; type: OverwriteTypes.member }; CHANNEL_OVERWRITE_UPDATE: - | Role - | GuildMember - | { id: Snowflake; name: string; type: OverwriteTypes.role } - | { id: Snowflake; type: OverwriteTypes.member }; + | Role + | GuildMember + | { id: Snowflake; name: string; type: OverwriteTypes.role } + | { id: Snowflake; type: OverwriteTypes.member }; CHANNEL_OVERWRITE_DELETE: - | Role - | GuildMember - | { id: Snowflake; name: string; type: OverwriteTypes.role } - | { id: Snowflake; type: OverwriteTypes.member }; + | Role + | GuildMember + | { id: Snowflake; name: string; type: OverwriteTypes.role } + | { id: Snowflake; type: OverwriteTypes.member }; STAGE_INSTANCE_CREATE: StageChannel | { id: Snowflake }; STAGE_INSTANCE_DELETE: StageChannel | { id: Snowflake }; STAGE_INSTANCE_UPDATE: StageChannel | { id: Snowflake }; @@ -4776,8 +4784,8 @@ export interface GuildAuditLogsEntryTargetField, - > extends Omit, 'channel'> { +> extends Omit, 'channel'> { channel?: GuildVoiceChannelResolvable | null; status?: T | number; } @@ -4980,14 +4988,14 @@ export type GuildScheduledEventEntityType = keyof typeof GuildScheduledEventEnti export type GuildScheduledEventManagerFetchResult< T extends GuildScheduledEventResolvable | FetchGuildScheduledEventOptions | FetchGuildScheduledEventsOptions, - > = T extends GuildScheduledEventResolvable | FetchGuildScheduledEventOptions +> = T extends GuildScheduledEventResolvable | FetchGuildScheduledEventOptions ? GuildScheduledEvent : Collection; export type GuildScheduledEventManagerFetchSubscribersResult = T extends { withMember: true } - ? Collection> - : Collection>; + ? Collection> + : Collection>; export type GuildScheduledEventPrivacyLevel = keyof typeof GuildScheduledEventPrivacyLevels; @@ -5538,24 +5546,24 @@ export type Partialize< N extends keyof T | null = null, M extends keyof T | null = null, E extends keyof T | '' = '', - > = { - readonly client: Client; - id: Snowflake; - partial: true; - } & { - [K in keyof Omit]: K extends N ? null : K extends M ? T[K] | null : T[K]; - }; +> = { + readonly client: Client; + id: Snowflake; + partial: true; +} & { + [K in keyof Omit]: K extends N ? null : K extends M ? T[K] | null : T[K]; +}; export interface PartialDMChannel extends Partialize { lastMessageId: undefined; } -export interface PartialGuildMember extends Partialize { } +export interface PartialGuildMember extends Partialize {} export interface PartialMessage - extends Partialize { } + extends Partialize {} -export interface PartialMessageReaction extends Partialize { } +export interface PartialMessageReaction extends Partialize {} export interface PartialOverwriteData { id: Snowflake | number; @@ -5570,7 +5578,7 @@ export interface PartialRoleData extends RoleData { export type PartialTypes = 'USER' | 'CHANNEL' | 'GUILD_MEMBER' | 'MESSAGE' | 'REACTION' | 'GUILD_SCHEDULED_EVENT'; -export interface PartialUser extends Partialize { } +export interface PartialUser extends Partialize {} export type PresenceStatusData = ClientPresenceStatus | 'invisible'; @@ -5595,9 +5603,9 @@ export interface RawUserSettingsData { allow_accessibility_detection?: boolean; animate_emoji?: boolean; animate_stickers?: number; - contact_sync_enabled:? boolean; + contact_sync_enabled: ?boolean; convert_emoticons?: boolean; - custom_status?: { text?: string, expires_at?: string | null, emoji_name?: string, emoji_id?: Snowflake | null }; + custom_status?: { text?: string; expires_at?: string | null; emoji_name?: string; emoji_id?: Snowflake | null }; default_guilds_restricted?: boolean; detect_platform_accounts?: boolean; developer_mode?: boolean; @@ -5605,10 +5613,10 @@ export interface RawUserSettingsData { enable_tts_command?: boolean; explicit_content_filter?: DMScanLevel; friend_discovery_flags?: number; - friend_source_flags?: { all?: boolean, mutual_friends?: boolean, mututal_guilds?: boolean }; + friend_source_flags?: { all?: boolean; mutual_friends?: boolean; mututal_guilds?: boolean }; gif_auto_play?: boolean; - guild_folders?: Array<{ id?: Snowflake, guild_ids?: Array, name?: string }>; - guild_positions?: Array; + guild_folders?: Array<{ id?: Snowflake; guild_ids?: Array; name?: string }>; + guild_positions?: Array; inline_attachment_media?: boolean; inline_embed_media?: boolean; locale?: string; @@ -5792,8 +5800,8 @@ export interface SweeperDefinitions { export type SweeperOptions = { [K in keyof SweeperDefinitions]?: SweeperDefinitions[K][2] extends true - ? SweepOptions | LifetimeSweepOptions - : SweepOptions; + ? SweepOptions | LifetimeSweepOptions + : SweepOptions; }; export interface LimitedCollectionOptions {