diff --git a/DOCUMENT.md b/DOCUMENT.md index 2d87b3e..b66c221 100644 --- a/DOCUMENT.md +++ b/DOCUMENT.md @@ -164,13 +164,13 @@ Custom Status ```js const RichPresence = require('discord-rpc-contructor'); // My module :)) const custom = new RichPresence.CustomStatus() - .setUnicodeEmoji('🎮') // Set Unicode Emoji [Using one] + .setUnicodeEmoji('🎮') // Set Unicode Emoji [Using one] .setDiscordEmoji({ // Set Custom Emoji (Nitro) [Using one] name: 'nom', id: '737373737373737373', animated: false, }) - .setState('Testing') // Name of presence + .setState('Testing') // Name of presence .toDiscord(); client.user.setActivity(custom); ``` @@ -238,4 +238,4 @@ And you can change the status 5 times every 20 seconds! ## Warning - This is a beta version, so there are some bugs. - If you use the `Client.destroy()` function, for an account that uses 2FA, it will cause a logout and the Token will no longer be usable. -- With bot account you can login using `Client.login('Bot Token', true)`, but there will be some missing constructor like MessageEmbed, MessageActionRow, MessageButton, ... \ No newline at end of file +- Downgrade to Discord.js v13 (Old version is Discord.js v14@dev) \ No newline at end of file diff --git a/README.md b/README.md index b561c15..78f95cd 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,7 @@ ## About -- discord.js-selfbot-v13 is a [Node.js](https://nodejs.org) module that allows user accounts (and bot .-.) to interact with the Discord API v10. (and discord.js@v14-dev) =)) -- Fork from [this](https://github.com/TheDevYellowy/dsb.js) module. +- discord.js-selfbot-v13 is a [Node.js](https://nodejs.org) module that allows user accounts (and bot .-.) to interact with the Discord API v10. ### I don't take any responsibility for blocked Discord accounts that used this module. ### Using this on a user account is prohibited by the [Discord TOS](https://discord.com/terms) and can lead to the account block. @@ -69,10 +68,8 @@ client.login('token'); - Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the [documentation](https://discord.js.org/#/docs). - See [the contribution guide](https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md) if you'd like to submit a PR. -- Thanks to . [TheDevYellowy](https://github.com/TheDevYellowy/) for patching this module! ## Need help? -Contact me in Discord: [Shiraori#1782](https://discord.com/users/721746046543331449) (UserID: 721746046543331449) - +Contact me in Discord: [Shiraori#1782](https://discord.com/users/721746046543331449) ## . Vietnamese -- Tóm lại là module này dùng Discord.js v13 (thực ra là dev của v14), API v10 nên chưa chết sớm đâu, cứ dùng đi =)) \ No newline at end of file +- Tóm lại là module này dùng Discord.js v13 , API v10 nên chưa chết sớm đâu, cứ dùng đi =)) \ No newline at end of file diff --git a/package.json b/package.json index 3988aef..e16df60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "discord.js-selfbot-v13", - "version": "0.3.0", + "version": "1.0.2", "description": "A unofficial discord.js fork for creating selfbots [Based on discord.js v13]", "main": "./src/index.js", "types": "./typings/index.d.ts", @@ -34,15 +34,13 @@ "bugs": { "url": "https://github.com/aiko-chan-ai/discord.js-selfbot-v13/issues" }, - "contributors": [ - "TheDevYellowy " - ], "homepage": "https://github.com/aiko-chan-ai/discord.js-selfbot-v13#readme", "dependencies": { "@discordjs/builders": "^0.12.0", "@discordjs/collection": "^0.5.0", "@sapphire/async-queue": "^1.3.0", "@sapphire/snowflake": "^3.2.0", + "@types/node-fetch": "^2.5.12", "@types/ws": "^8.5.2", "axios": "^0.26.1", "chalk": "^4.1.2", @@ -53,7 +51,7 @@ "json-bigint": "^1.0.0", "lodash": "^4.17.21", "lodash.snakecase": "^4.1.1", - "node-fetch": "^3.2.2", + "node-fetch": "^2.6.1", "undici": "^4.15.0", "ws": "^8.5.0" }, diff --git a/src/RPC/index.d.ts b/src/RPC/index.d.ts deleted file mode 100644 index 589831c..00000000 --- a/src/RPC/index.d.ts +++ /dev/null @@ -1,209 +0,0 @@ -declare class RpcError extends Error { - name: string; -} -declare class Rpc { - game: discordPresence | null; - constructor(rpcObj?: discordPresence | null, readonly?: boolean); - lock(): void; - toDiscord(): { - game: discordPresence | null; - }; - toObject(): discordPresence; - toString(): string; - setName(name: string | null): this; - setApplicationId(id: string | null): this; - setType(type: PresenceType | number): this; - setUrl(url: string | null): this; - setDetails(details: string | null): this; - setState(state: string | null): this; - setSyncId(sync_id: string | null): this; - setId(id: string | null): this; - setSessionId(session_id: string | null): this; - setParty(party: discordPresence["party"]): this; - setFlags(flags: number | null): this; - setCreatedAt(created_at: number | null): this; - setAssets(assetsFunc: (AssetsObj: setAssetsObj) => void): Rpc; - /** - * - * @param large_image *ID image* - */ - setAssetsLargeImage(large_image: string | null): this; - /** - * - * @param large_image *ID image* - */ - setAssetsSmallImage(small_image: string | null): this; - setAssetsLargeText(large_text: string | null): this; - setAssetsSmallText(small_text: string | null): this; - setStartTimestamp(start: number | null): this; - setEndTimestamp(end: number | null): this; - setPartySize(size: [number, number] | null): this; - setPartyId(id: string | null): this; - setJoinSecret(secret: string | null): this; - setSpectateSecret(secret: string | null): this; - setMatch(secret: string | null): this; - setSecrets(secrets: discordPresence["secrets"] | null): this; - /** - * Twitch - */ - setTwitchAssets(assetsFunc: (AssetsObj: setAssetsObj) => void): Rpc; - /** - * - * @param large_image *ID Image* - */ - setTwitchAssetsLargeImage(large_image: string | null): this; - /** - * - * @param large_image *ID Image* - */ - setTwitchAssetsSmallImage(small_image: string | null): this; - /** Spotify */ - setSpotifyAssets(assetsFunc: (AssetsObj: setAssetsObj) => void): Rpc; - /** - * - * @param large_image *ID Image* - */ - setSpotifyAssetsLargeImage(large_image: string | null): this; - /** - * - * @param large_image *ID Image* - */ - setSpotifyAssetsSmallImage(small_image: string | null): this; - private verifyNull; - private verifyNullAssets; - private verifyNullTimestamps; - private verifyNullParty; - private verifyNullSecrets; -} -declare class CustomStatus { - game: CustomStatusGame; - constructor(CustomStatusGame?: CustomStatusGame); - /** - * Name - * @param state Name of the status - */ - setState(state: string): CustomStatus; - /** - * Custom Status with Emoji Custom - * @param emoji Object - * emoji.name: string - * emoji.id: string - * emoji.animated: boolean - */ - setDiscordEmoji(emoji: emojiLike): CustomStatus; - /** - * Unicode Emoji - * @param emoji String - */ - setUnicodeEmoji(emoji: string): CustomStatus; - /** Convert to JSON Activity */ - toDiscord(): CustomStatusGame; - toObject(): CustomStatusGame; - toString(): string; -} -interface setEmojiObj { - setName(name: string): setEmojiObj; - setId(id: string): setEmojiObj; - setAnimated(animated: boolean): setEmojiObj; -} -interface setAssetsObj { - setLargeImage(img: string | null): setAssetsObj; - setSmallImage(img: string | null): setAssetsObj; - setLargeText(text: string | null): setAssetsObj; - setSmallText(text: string | null): setAssetsObj; - setNull(): setAssetsObj; -} -interface CustomStatusGame { - name: string; - emoji: { - name: string; - id: string | null; - animated: boolean; - } | null; - state: string; -} -interface rpcManager { - default?: rpcManager; - Rpc: { - new (rpcobj?: discordPresence): Rpc; - }; - PresenceTypes: PresenceType[]; - PresenceTypesString: PresenceTypeString[]; - PresenceTypesNumber: PresenceTypeNumber[]; - RpcError: { - new (message: string): RpcError; - }; - getRpcImages(application_id: string): Promise; - getRpcImage(application_id: string, name: string): Promise; - __esModule: true; - createSpotifyRpc(client: clientLike, rpcobj?: discordPresence): Rpc; - version: string; - CustomStatus: { - new (CustomStatusGame?: CustomStatusGame): CustomStatus; - }; - uuid(): string; -} -interface emojiLike { - id: string; - animated: boolean; - name: string; - [k: string]: any; -} -interface clientLike { - ws: { - connection: { - sessionID: string; - [k: string]: any; - }; - [k: string]: any; - }; - user: { - id: string; - [k: string]: any; - }; - [k: string]: any; -} -interface discordPresence { - "name": string; - "platform"?: string; - "application_id"?: string; - "type": PresenceTypeNumber; - "url"?: string; - "details"?: string; - "state"?: string; - "sync_id"?: string; - "id"?: string; - "session_id"?: string; - "party"?: { - "size"?: [number, number]; - "id": string; - }; - "flags"?: number; - "created_at"?: number; - "assets"?: { - "large_image"?: string; - "small_image"?: string; - "small_text"?: string; - "large_text"?: string; - }; - "timestamps"?: { - "start"?: number; - "end"?: number; - }; - "secrets"?: { - "join"?: string; - "spectate"?: string; - "match"?: string; - }; -} -/** getRPC {@link getRpcImage} */ -declare type Image = { - name: string; - id: string; - type: number; -}; -declare type PresenceTypeString = "PLAYING" | "STREAMING" | "LISTENING" | "WATCHING" | "CUSTOM" | "COMPETING"; -declare type PresenceTypeNumber = 0 | 1 | 2 | 3 | 4 | 5; -declare type PresenceType = PresenceTypeNumber | PresenceTypeString; -declare var rpcManager: rpcManager; -export = rpcManager; diff --git a/src/RPC/index.js b/src/RPC/index.js deleted file mode 100644 index d551ac2..00000000 --- a/src/RPC/index.js +++ /dev/null @@ -1,1876 +0,0 @@ -module.exports = (function (e) { - var t = {}; - function s(r) { - if (t[r]) return t[r].exports; - var i = (t[r] = { i: r, l: !1, exports: {} }); - return e[r].call(i.exports, i, i.exports, s), (i.l = !0), i.exports; - } - return ( - (s.m = e), - (s.c = t), - (s.d = function (e, t, r) { - s.o(e, t) || Object.defineProperty(e, t, { enumerable: !0, get: r }); - }), - (s.r = function (e) { - "undefined" != typeof Symbol && - Symbol.toStringTag && - Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }), - Object.defineProperty(e, "__esModule", { value: !0 }); - }), - (s.t = function (e, t) { - if ((1 & t && (e = s(e)), 8 & t)) return e; - if (4 & t && "object" == typeof e && e && e.__esModule) return e; - var r = Object.create(null); - if ( - (s.r(r), - Object.defineProperty(r, "default", { enumerable: !0, value: e }), - 2 & t && "string" != typeof e) - ) - for (var i in e) - s.d( - r, - i, - function (t) { - return e[t]; - }.bind(null, i) - ); - return r; - }), - (s.n = function (e) { - var t = - e && e.__esModule - ? function () { - return e.default; - } - : function () { - return e; - }; - return s.d(t, "a", t), t; - }), - (s.o = function (e, t) { - return Object.prototype.hasOwnProperty.call(e, t); - }), - (s.p = "/assets/"), - s((s.s = 0)) - ); - })([ - function (e, t, s) { - "use strict"; - const r = s(1), - i = s(9), - n = s(10); - var o = [ - 'PLAYING', - 'STREAMING', - 'LISTENING', - 'WATCHING', - 'CUSTOM', - 'COMPETING', - ], - a = [0, 1, 2, 3, 4, 5], - l = [].concat(a).concat(o); - async function u(e) { - if (!e || "string" != typeof e) - throw new c( - `'${e}' không phải là ID Application Discord. Typeof: ` - ); - let t = await r(i.default.discord().application(e).assets(), { - headers: { - "User-Agent": i.default.ua, - "accept-language": i.default.acceptedLangs, - }, - }); - if (200 !== t.status) { - let e, - s = await t.text(); - if (!s) throw new c("Lỗi không xác định: " + t.status + " " + t.statusText); - try { - e = JSON.parse(s); - } catch (e) { - throw new c(s); - } - if (e.message) throw new c(e.message); - if (e.application_id) throw new c(e.application_id[0]); - throw new c(JSON.stringify(e)); - } - return await t.json(); - } - class c extends Error { - constructor() { - super(...arguments), (this.name = "RpcError"); - } - } - class h { - constructor(e, t = !1) { - (this.game = null), (this.game = e || null), t && this.lock(); - } - lock() { - Object.freeze(this.game); - } - toDiscord() { - return { game: this.game }; - } - toObject() { - return this.game; - } - toString() { - return this.game - ? `${this.game.name}${ - this.game.application_id - ? ' (' + this.game.application_id + ')' - : '' - } ` - : 'No game'; - } - setButton(array) { - // Check value - if (!Array.isArray(array)) throw new c(`'${array}' not an array`); - if (array.length > 2) throw new c(`'${array}' length > 2`); - if (array.find((o) => !o?.label)) throw new c(`'${array}' has button with noname`); - return ( - this.verifyNull(), - this.verifyNullButton(), - null === array - ? (delete this.game.buttons, this) - : ((this.game.buttons = array), this) - ); - } - setName(e) { - return this.verifyNull(), (this.game.name = e), this; - } - setApplicationId(e) { - return ( - this.verifyNull(), - null === e - ? (delete this.game.application_id, this) - : ((this.game.application_id = e), this) - ); - } - setType(e) { - let t = 0; - if ('string' == typeof e) { - if (!o.includes(e)) - throw new c( - `'${e}' không phải là Presence hợp lệ: ${l.join(', ')}`, - ); - t = o.indexOf(e); - } else t = e; - return this.verifyNull(), (this.game.type = t), this; - } - setUrl(e) { - return ( - this.verifyNull(), - null === e - ? (delete this.game.url, this) - : ((this.game.url = e), this) - ); - } - setDetails(e) { - return ( - this.verifyNull(), - null === e - ? (delete this.game.details, this) - : ((this.game.details = e), this) - ); - } - setState(e) { - return ( - this.verifyNull(), - null === e - ? (delete this.game.state, this) - : ((this.game.state = e), this) - ); - } - setSyncId(e) { - return ( - this.verifyNull(), - null === e - ? (delete this.game.sync_id, this) - : ((this.game.sync_id = e), this) - ); - } - setId(e) { - return ( - this.verifyNull(), - null === e - ? (delete this.game.id, this) - : ((this.game.id = e), this) - ); - } - setSessionId(e) { - return ( - this.verifyNull(), - null === e - ? (delete this.game.session_id, this) - : ((this.game.session_id = e), this) - ); - } - setParty(e) { - return ( - this.verifyNull(), - null === e - ? (delete this.game.party, this) - : ((this.game.party = e), this) - ); - } - setFlags(e) { - return ( - this.verifyNull(), - null === e - ? (delete this.game.flags, this) - : ((this.game.flags = e), this) - ); - } - setCreatedAt(e) { - return ( - this.verifyNull(), - null === e - ? (delete this.game.created_at, this) - : ((this.game.created_at = e), this) - ); - } - setAssets(e) { - this.verifyNull(); - let t = { - setLargeImage: function (e) { - return ( - this.verifyNullAssets(), (this.game.assets.large_image = e), t - ); - }.bind(this), - setSmallImage: function (e) { - return ( - this.verifyNullAssets(), (this.game.assets.small_image = e), t - ); - }.bind(this), - setLargeText: function (e) { - return ( - this.verifyNullAssets(), (this.game.assets.large_text = e), t - ); - }.bind(this), - setSmallText: function (e) { - return ( - this.verifyNullAssets(), (this.game.assets.small_text = e), t - ); - }.bind(this), - setNull: function () { - return (this.game.assets = null), t; - }.bind(this), - }; - return e(t), this; - } - setAssetsLargeImage(e) { - return ( - this.verifyNull(), - this.verifyNullAssets(), - null === e - ? (delete this.game.assets.large_image, this) - : ((this.game.assets.large_image = e), this) - ); - } - setAssetsSmallImage(e) { - return ( - this.verifyNull(), - this.verifyNullAssets(), - null === e - ? (delete this.game.assets.small_image, this) - : ((this.game.assets.small_image = e), this) - ); - } - setAssetsLargeText(e) { - return ( - this.verifyNull(), - this.verifyNullAssets(), - null === e - ? (delete this.game.assets.large_text, this) - : ((this.game.assets.large_text = e), this) - ); - } - setAssetsSmallText(e) { - return ( - this.verifyNull(), - this.verifyNullAssets(), - null === e - ? (delete this.game.assets.small_text, this) - : ((this.game.assets.small_text = e), this) - ); - } - setStartTimestamp(e) { - return ( - this.verifyNull(), - this.verifyNullTimestamps(), - null === e - ? (delete this.game.timestamps.start, this) - : ((this.game.timestamps.start = e), this) - ); - } - setEndTimestamp(e) { - return ( - this.verifyNull(), - this.verifyNullTimestamps(), - null === e - ? (delete this.game.timestamps.end, this) - : ((this.game.timestamps.end = e), this) - ); - } - setPartySize(e) { - return ( - this.verifyNull(), - this.verifyNullParty(), - null === e - ? (delete this.game.party.size, this) - : ((this.game.party.size = e), this) - ); - } - setPartyId(e) { - return ( - this.verifyNull(), - this.verifyNullParty(), - null === e - ? (delete this.game.party.id, this) - : ((this.game.party.id = e), this) - ); - } - setJoinSecret(e) { - return ( - this.verifyNull(), - this.verifyNullSecrets(), - null === e - ? (delete this.game.secrets.join, this) - : ((this.game.secrets.join = e), this) - ); - } - setSpectateSecret(e) { - return ( - this.verifyNull(), - this.verifyNullSecrets(), - null === e - ? (delete this.game.secrets.spectate, this) - : ((this.game.secrets.spectate = e), this) - ); - } - setMatch(e) { - return ( - this.verifyNull(), - this.verifyNullSecrets(), - null === e - ? (delete this.game.secrets.match, this) - : ((this.game.secrets.match = e), this) - ); - } - setSecrets(e) { - return ( - this.verifyNull(), - this.verifyNullSecrets(), - null === e - ? (delete this.game.secrets, this) - : ((this.game.secrets = e), this) - ); - } - setTwitchAssets(e) { - this.verifyNull(); - let t = { - setLargeImage: function (e) { - return ( - this.verifyNullAssets(), - (this.game.assets.large_image = - 'twitch:' + e.replace(/twitch\:/g, '')), - t - ); - }.bind(this), - setSmallImage: function (e) { - return ( - this.verifyNullAssets(), - (this.game.assets.small_image = - 'twitch:' + e.replace(/twitch\:/g, '')), - t - ); - }.bind(this), - setLargeText: function (e) { - return ( - this.verifyNullAssets(), (this.game.assets.large_text = e), t - ); - }.bind(this), - setSmallText: function (e) { - return ( - this.verifyNullAssets(), (this.game.assets.small_text = e), t - ); - }.bind(this), - setNull: function () { - return (this.game.assets = null), t; - }.bind(this), - }; - return e(t), this; - } - setTwitchAssetsLargeImage(e) { - return ( - this.verifyNull(), - this.verifyNullAssets(), - null === e - ? (delete this.game.assets.large_image, this) - : ((this.game.assets.large_image = - 'twitch:' + e.replace(/twitch\:/g, '')), - this) - ); - } - setTwitchAssetsSmallImage(e) { - return ( - this.verifyNull(), - this.verifyNullAssets(), - null === e - ? (delete this.game.assets.small_image, this) - : ((this.game.assets.small_image = - 'twitch:' + e.replace(/twitch\:/g, '')), - this) - ); - } - setSpotifyAssets(e) { - this.verifyNull(); - let t = { - setLargeImage: function (e) { - return ( - this.verifyNullAssets(), - (this.game.assets.large_image = - 'spotify:' + e.replace(/spotify\:/g, '')), - t - ); - }.bind(this), - setSmallImage: function (e) { - return ( - this.verifyNullAssets(), - (this.game.assets.small_image = - 'spotify:' + e.replace(/spotify\:/g, '')), - t - ); - }.bind(this), - setLargeText: function (e) { - return ( - this.verifyNullAssets(), (this.game.assets.large_text = e), t - ); - }.bind(this), - setSmallText: function (e) { - return ( - this.verifyNullAssets(), (this.game.assets.small_text = e), t - ); - }.bind(this), - setNull: function () { - return (this.game.assets = null), t; - }.bind(this), - }; - return e(t), this; - } - setSpotifyAssetsLargeImage(e) { - return ( - this.verifyNull(), - this.verifyNullAssets(), - null === e - ? (delete this.game.assets.large_image, this) - : ((this.game.assets.large_image = - 'spotify:' + e.replace(/spotify\:/g, '')), - this) - ); - } - setSpotifyAssetsSmallImage(e) { - return ( - this.verifyNull(), - this.verifyNullAssets(), - null === e - ? (delete this.game.assets.small_image, this) - : ((this.game.assets.small_image = - 'spotify:' + e.replace(/spotify\:/g, '')), - this) - ); - } - verifyNull() { - this.game || (this.game = { name: '', type: 0 }); - } - verifyNullAssets() { - this.game.assets || (this.game.assets = {}); - } - verifyNullTimestamps() { - this.game.timestamps || (this.game.timestamps = {}); - } - verifyNullParty() { - this.game.party || (this.game.party = { id: '' }); - } - verifyNullSecrets() { - this.game.secrets || (this.game.secrets = {}); - } - verifyNullButton() { - this.game.buttons || (this.game.buttons = []); - } - } - var f = { - Rpc: h, - PresenceTypes: l, - PresenceTypesString: o, - PresenceTypesNumber: a, - RpcError: c, - getRpcImages: u, - getRpcImage: async function (e, t) { - if ("string" != typeof t || !t) - throw new c(`'${t}' không phải là String`); - let s = await u(e), - r = s.find((e) => e.name === t); - if (!r) - throw new c( - `Image '${t}' không có trong ApplicationID ${e}. Các hình ảnh sẵn có là: ${s - .map((e) => e.name) - .join(", ")}.` - ); - return r; - }, - __esModule: !0, - createSpotifyRpc: (e, t) => - new h(t) - .setType(2) - .setEndTimestamp(Date.now() + 864e5) - .setSyncId("6l7PqWKsgm4NLomOE7Veou") - .setSessionId(e.ws.connection.sessionID) - .setPartyId("spotify:" + e.user.id) - .setName("Spotify") - .setId("spotify:1") - .setFlags(48) - .setCreatedAt(1561389854174) - .setSecrets({ - join: "025ed05c71f639de8bfaa0d679d7c94b2fdce12f", - spectate: "e7eb30d2ee025ed05c71ea495f770b76454ee4e0", - match: "4b2fdce12f639de8bfa7e3591b71a0d679d7c93f", - }), - version: n.version, - CustomStatus: class { - constructor(e) { - (this.game = { - name: 'Custom Status', - emoji: null, - type: 4, - state: null, - }), - e && (this.game = e); - } - setState(e) { - return (this.game.state = e), this; - } - setEmoji(e) { - let t = { - setName: function (e) { - return ( - this.game.emoji || (this.game.emoji = {}), - (this.game.emoji.name = e), - t - ); - }.bind(this), - setId: function (e) { - return ( - this.game.emoji || (this.game.emoji = {}), - (this.game.emoji.id = e), - t - ); - }.bind(this), - setAnimated: function (e) { - return ( - this.game.emoji || (this.game.emoji = {}), - (this.game.emoji.animated = e), - t - ); - }.bind(this), - }; - return e(t), this; - } - setDiscordEmoji(e) { - return ( - (this.game.emoji = { - name: e.name, - id: e.id, - animated: e.animated, - }), - this - ); - } - setUnicodeEmoji(e) { - return (this.game.emoji = { name: e, id: null, animated: !1 }), this; - } - toDiscord() { - return this.game; - } - toObject() { - return this.game; - } - toString() { - return `${this.game.name}: ${ - this.game.emoji - ? ((e = this.game.emoji), - (null === e.id - ? e.name - : `<${e.animated ? "a" : ""}:${e.name}:${e.id}>`) + " ") - : "" - }${this.game.state}`; - var e; - } - }, - }; - (f.default = f), (e.exports = f); - }, - function (e, t, s) { - "use strict"; - function r(e) { - return e && "object" == typeof e && "default" in e ? e.default : e; - } - Object.defineProperty(t, "__esModule", { value: !0 }); - var i = s(2), - n = r(s(3)), - o = r(s(4)), - a = r(s(5)), - l = r(s(6)); - const u = i.Readable, - c = Symbol("buffer"), - h = Symbol("type"); - class f { - constructor() { - this[h] = ""; - const e = arguments[0], - t = arguments[1], - s = []; - let r = 0; - if (e) { - const t = e, - i = Number(t.length); - for (let e = 0; e < i; e++) { - const i = t[e]; - let n; - (n = - i instanceof Buffer - ? i - : ArrayBuffer.isView(i) - ? Buffer.from(i.buffer, i.byteOffset, i.byteLength) - : i instanceof ArrayBuffer - ? Buffer.from(i) - : i instanceof f - ? i[c] - : Buffer.from("string" == typeof i ? i : String(i))), - (r += n.length), - s.push(n); - } - } - this[c] = Buffer.concat(s); - let i = t && void 0 !== t.type && String(t.type).toLowerCase(); - i && !/[^\u0020-\u007E]/.test(i) && (this[h] = i); - } - get size() { - return this[c].length; - } - get type() { - return this[h]; - } - text() { - return Promise.resolve(this[c].toString()); - } - arrayBuffer() { - const e = this[c], - t = e.buffer.slice(e.byteOffset, e.byteOffset + e.byteLength); - return Promise.resolve(t); - } - stream() { - const e = new u(); - return (e._read = function () {}), e.push(this[c]), e.push(null), e; - } - toString() { - return "[object Blob]"; - } - slice() { - const e = this.size, - t = arguments[0], - s = arguments[1]; - let r, i; - (r = void 0 === t ? 0 : t < 0 ? Math.max(e + t, 0) : Math.min(t, e)), - (i = void 0 === s ? e : s < 0 ? Math.max(e + s, 0) : Math.min(s, e)); - const n = Math.max(i - r, 0), - o = this[c].slice(r, r + n), - a = new f([], { type: arguments[2] }); - return (a[c] = o), a; - } - } - function m(e, t, s) { - Error.call(this, e), - (this.message = e), - (this.type = t), - s && (this.code = this.errno = s.code), - Error.captureStackTrace(this, this.constructor); - } - Object.defineProperties(f.prototype, { - size: { enumerable: !0 }, - type: { enumerable: !0 }, - slice: { enumerable: !0 }, - }), - Object.defineProperty(f.prototype, Symbol.toStringTag, { - value: "Blob", - writable: !1, - enumerable: !1, - configurable: !0, - }), - (m.prototype = Object.create(Error.prototype)), - (m.prototype.constructor = m), - (m.prototype.name = "FetchError"); - const d = Symbol("Body internals"), - p = i.PassThrough; - function g(e) { - var t = this, - s = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, - r = s.size; - let n = void 0 === r ? 0 : r; - var o = s.timeout; - let a = void 0 === o ? 0 : o; - null == e - ? (e = null) - : b(e) - ? (e = Buffer.from(e.toString())) - : v(e) || - Buffer.isBuffer(e) || - ("[object ArrayBuffer]" === Object.prototype.toString.call(e) - ? (e = Buffer.from(e)) - : ArrayBuffer.isView(e) - ? (e = Buffer.from(e.buffer, e.byteOffset, e.byteLength)) - : e instanceof i || (e = Buffer.from(String(e)))), - (this[d] = { body: e, disturbed: !1, error: null }), - (this.size = n), - (this.timeout = a), - e instanceof i && - e.on("error", function (e) { - const s = - "AbortError" === e.name - ? e - : new m( - `Invalid response body while trying to fetch ${t.url}: ${e.message}`, - "system", - e - ); - t[d].error = s; - }); - } - function y() { - var e = this; - if (this[d].disturbed) - return g.Promise.reject( - new TypeError(`body used already for: ${this.url}`) - ); - if (((this[d].disturbed = !0), this[d].error)) - return g.Promise.reject(this[d].error); - let t = this.body; - if (null === t) return g.Promise.resolve(Buffer.alloc(0)); - if ((v(t) && (t = t.stream()), Buffer.isBuffer(t))) - return g.Promise.resolve(t); - if (!(t instanceof i)) return g.Promise.resolve(Buffer.alloc(0)); - let s = [], - r = 0, - n = !1; - return new g.Promise(function (i, o) { - let a; - e.timeout && - (a = setTimeout(function () { - (n = !0), - o( - new m( - `Response timeout while trying to fetch ${e.url} (over ${e.timeout}ms)`, - "body-timeout" - ) - ); - }, e.timeout)), - t.on("error", function (t) { - "AbortError" === t.name - ? ((n = !0), o(t)) - : o( - new m( - `Invalid response body while trying to fetch ${e.url}: ${t.message}`, - "system", - t - ) - ); - }), - t.on("data", function (t) { - if (!n && null !== t) { - if (e.size && r + t.length > e.size) - return ( - (n = !0), - void o( - new m( - `content size at ${e.url} over limit: ${e.size}`, - "max-size" - ) - ) - ); - (r += t.length), s.push(t); - } - }), - t.on("end", function () { - if (!n) { - clearTimeout(a); - try { - i(Buffer.concat(s, r)); - } catch (t) { - o( - new m( - `Could not create Buffer from response body for ${e.url}: ${t.message}`, - "system", - t - ) - ); - } - } - }); - }); - } - function b(e) { - return ( - "object" == typeof e && - "function" == typeof e.append && - "function" == typeof e.delete && - "function" == typeof e.get && - "function" == typeof e.getAll && - "function" == typeof e.has && - "function" == typeof e.set && - ("URLSearchParams" === e.constructor.name || - "[object URLSearchParams]" === Object.prototype.toString.call(e) || - "function" == typeof e.sort) - ); - } - function v(e) { - return ( - "object" == typeof e && - "function" == typeof e.arrayBuffer && - "string" == typeof e.type && - "function" == typeof e.stream && - "function" == typeof e.constructor && - "string" == typeof e.constructor.name && - /^(Blob|File)$/.test(e.constructor.name) && - /^(Blob|File)$/.test(e[Symbol.toStringTag]) - ); - } - function w(e) { - let t, - s, - r = e.body; - if (e.bodyUsed) throw new Error("cannot clone body after it is used"); - return ( - r instanceof i && - "function" != typeof r.getBoundary && - ((t = new p()), - (s = new p()), - r.pipe(t), - r.pipe(s), - (e[d].body = t), - (r = s)), - r - ); - } - function S(e) { - return null === e - ? null - : "string" == typeof e - ? "text/plain;charset=UTF-8" - : b(e) - ? "application/x-www-form-urlencoded;charset=UTF-8" - : v(e) - ? e.type || null - : Buffer.isBuffer(e) - ? null - : "[object ArrayBuffer]" === Object.prototype.toString.call(e) - ? null - : ArrayBuffer.isView(e) - ? null - : "function" == typeof e.getBoundary - ? `multipart/form-data;boundary=${e.getBoundary()}` - : e instanceof i - ? null - : "text/plain;charset=UTF-8"; - } - function x(e) { - const t = e.body; - return null === t - ? 0 - : v(t) - ? t.size - : Buffer.isBuffer(t) - ? t.length - : t && - "function" == typeof t.getLengthSync && - ((t._lengthRetrievers && 0 == t._lengthRetrievers.length) || - (t.hasKnownLength && t.hasKnownLength())) - ? t.getLengthSync() - : null; - } - (g.prototype = { - get body() { - return this[d].body; - }, - get bodyUsed() { - return this[d].disturbed; - }, - arrayBuffer() { - return y.call(this).then(function (e) { - return e.buffer.slice(e.byteOffset, e.byteOffset + e.byteLength); - }); - }, - blob() { - let e = (this.headers && this.headers.get("content-type")) || ""; - return y.call(this).then(function (t) { - return Object.assign(new f([], { type: e.toLowerCase() }), { - [c]: t, - }); - }); - }, - json() { - var e = this; - return y.call(this).then(function (t) { - try { - return JSON.parse(t.toString()); - } catch (t) { - return g.Promise.reject( - new m( - `invalid json response body at ${e.url} reason: ${t.message}`, - "invalid-json" - ) - ); - } - }); - }, - text() { - return y.call(this).then(function (e) { - return e.toString(); - }); - }, - buffer() { - return y.call(this); - }, - textConverted() { - var e = this; - return y.call(this).then(function (t) { - return (function (e, t) { - throw new Error( - "The package `encoding` must be installed to use the textConverted() function" - ); - const s = t.get("content-type"); - let r, - i, - n = "utf-8"; - s && (r = /charset=([^;]*)/i.exec(s)); - (i = e.slice(0, 1024).toString()), - !r && i && (r = / 0 && void 0 !== arguments[0] - ? arguments[0] - : void 0; - if (((this[_] = Object.create(null)), e instanceof P)) { - const t = e.raw(), - s = Object.keys(t); - for (const e of s) for (const s of t[e]) this.append(e, s); - } else if (null == e); - else { - if ("object" != typeof e) - throw new TypeError("Provided initializer must be an object"); - { - const t = e[Symbol.iterator]; - if (null != t) { - if ("function" != typeof t) - throw new TypeError("Header pairs must be iterable"); - const s = []; - for (const t of e) { - if ( - "object" != typeof t || - "function" != typeof t[Symbol.iterator] - ) - throw new TypeError("Each header pair must be iterable"); - s.push(Array.from(t)); - } - for (const e of s) { - if (2 !== e.length) - throw new TypeError( - "Each header pair must be a name/value tuple" - ); - this.append(e[0], e[1]); - } - } else - for (const t of Object.keys(e)) { - const s = e[t]; - this.append(t, s); - } - } - } - } - get(e) { - N((e = `${e}`)); - const t = O(this[_], e); - return void 0 === t ? null : this[_][t].join(", "); - } - forEach(e) { - let t = - arguments.length > 1 && void 0 !== arguments[1] - ? arguments[1] - : void 0, - s = E(this), - r = 0; - for (; r < s.length; ) { - var i = s[r]; - const n = i[0], - o = i[1]; - e.call(t, o, n, this), (s = E(this)), r++; - } - } - set(e, t) { - (t = `${t}`), N((e = `${e}`)), A(t); - const s = O(this[_], e); - this[_][void 0 !== s ? s : e] = [t]; - } - append(e, t) { - (t = `${t}`), N((e = `${e}`)), A(t); - const s = O(this[_], e); - void 0 !== s ? this[_][s].push(t) : (this[_][e] = [t]); - } - has(e) { - return N((e = `${e}`)), void 0 !== O(this[_], e); - } - delete(e) { - N((e = `${e}`)); - const t = O(this[_], e); - void 0 !== t && delete this[_][t]; - } - raw() { - return this[_]; - } - keys() { - return $(this, "key"); - } - values() { - return $(this, "value"); - } - [Symbol.iterator]() { - return $(this, "key+value"); - } - } - function E(e) { - let t = - arguments.length > 1 && void 0 !== arguments[1] - ? arguments[1] - : "key+value"; - const s = Object.keys(e[_]).sort(); - return s.map( - "key" === t - ? function (e) { - return e.toLowerCase(); - } - : "value" === t - ? function (t) { - return e[_][t].join(", "); - } - : function (t) { - return [t.toLowerCase(), e[_][t].join(", ")]; - } - ); - } - (P.prototype.entries = P.prototype[Symbol.iterator]), - Object.defineProperty(P.prototype, Symbol.toStringTag, { - value: "Headers", - writable: !1, - enumerable: !1, - configurable: !0, - }), - Object.defineProperties(P.prototype, { - get: { enumerable: !0 }, - forEach: { enumerable: !0 }, - set: { enumerable: !0 }, - append: { enumerable: !0 }, - has: { enumerable: !0 }, - delete: { enumerable: !0 }, - keys: { enumerable: !0 }, - values: { enumerable: !0 }, - entries: { enumerable: !0 }, - }); - const L = Symbol("internal"); - function $(e, t) { - const s = Object.create(k); - return (s[L] = { target: e, kind: t, index: 0 }), s; - } - const k = Object.setPrototypeOf( - { - next() { - if (!this || Object.getPrototypeOf(this) !== k) - throw new TypeError("Value of `this` is not a HeadersIterator"); - var e = this[L]; - const t = e.target, - s = e.kind, - r = e.index, - i = E(t, s); - return r >= i.length - ? { value: void 0, done: !0 } - : ((this[L].index = r + 1), { value: i[r], done: !1 }); - }, - }, - Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())) - ); - function I(e) { - const t = Object.assign({ __proto__: null }, e[_]), - s = O(e[_], "Host"); - return void 0 !== s && (t[s] = t[s][0]), t; - } - Object.defineProperty(k, Symbol.toStringTag, { - value: "HeadersIterator", - writable: !1, - enumerable: !1, - configurable: !0, - }); - const B = Symbol("Response internals"), - C = n.STATUS_CODES; - class R { - constructor() { - let e = - arguments.length > 0 && void 0 !== arguments[0] - ? arguments[0] - : null, - t = - arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}; - g.call(this, e, t); - const s = t.status || 200, - r = new P(t.headers); - if (null != e && !r.has("Content-Type")) { - const t = S(e); - t && r.append("Content-Type", t); - } - this[B] = { - url: t.url, - status: s, - statusText: t.statusText || C[s], - headers: r, - counter: t.counter, - }; - } - get url() { - return this[B].url || ""; - } - get status() { - return this[B].status; - } - get ok() { - return this[B].status >= 200 && this[B].status < 300; - } - get redirected() { - return this[B].counter > 0; - } - get statusText() { - return this[B].statusText; - } - get headers() { - return this[B].headers; - } - clone() { - return new R(w(this), { - url: this.url, - status: this.status, - statusText: this.statusText, - headers: this.headers, - ok: this.ok, - redirected: this.redirected, - }); - } - } - g.mixIn(R.prototype), - Object.defineProperties(R.prototype, { - url: { enumerable: !0 }, - status: { enumerable: !0 }, - ok: { enumerable: !0 }, - redirected: { enumerable: !0 }, - statusText: { enumerable: !0 }, - headers: { enumerable: !0 }, - clone: { enumerable: !0 }, - }), - Object.defineProperty(R.prototype, Symbol.toStringTag, { - value: "Response", - writable: !1, - enumerable: !1, - configurable: !0, - }); - const z = Symbol("Request internals"), - U = o.parse, - q = o.format, - F = "destroy" in i.Readable.prototype; - function D(e) { - return "object" == typeof e && "object" == typeof e[z]; - } - class M { - constructor(e) { - let t, - s = - arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}; - D(e) - ? (t = U(e.url)) - : ((t = e && e.href ? U(e.href) : U(`${e}`)), (e = {})); - let r = s.method || e.method || "GET"; - if ( - ((r = r.toUpperCase()), - (null != s.body || (D(e) && null !== e.body)) && - ("GET" === r || "HEAD" === r)) - ) - throw new TypeError("Request with GET/HEAD method cannot have body"); - let i = null != s.body ? s.body : D(e) && null !== e.body ? w(e) : null; - g.call(this, i, { - timeout: s.timeout || e.timeout || 0, - size: s.size || e.size || 0, - }); - const n = new P(s.headers || e.headers || {}); - if (null != i && !n.has("Content-Type")) { - const e = S(i); - e && n.append("Content-Type", e); - } - let o = D(e) ? e.signal : null; - if ( - ("signal" in s && (o = s.signal), - null != o && - !(function (e) { - const t = e && "object" == typeof e && Object.getPrototypeOf(e); - return !(!t || "AbortSignal" !== t.constructor.name); - })(o)) - ) - throw new TypeError( - "Expected signal to be an instanceof AbortSignal" - ); - (this[z] = { - method: r, - redirect: s.redirect || e.redirect || "follow", - headers: n, - parsedURL: t, - signal: o, - }), - (this.follow = - void 0 !== s.follow - ? s.follow - : void 0 !== e.follow - ? e.follow - : 20), - (this.compress = - void 0 !== s.compress - ? s.compress - : void 0 === e.compress || e.compress), - (this.counter = s.counter || e.counter || 0), - (this.agent = s.agent || e.agent); - } - get method() { - return this[z].method; - } - get url() { - return q(this[z].parsedURL); - } - get headers() { - return this[z].headers; - } - get redirect() { - return this[z].redirect; - } - get signal() { - return this[z].signal; - } - clone() { - return new M(this); - } - } - function H(e) { - Error.call(this, e), - (this.type = "aborted"), - (this.message = e), - Error.captureStackTrace(this, this.constructor); - } - g.mixIn(M.prototype), - Object.defineProperty(M.prototype, Symbol.toStringTag, { - value: "Request", - writable: !1, - enumerable: !1, - configurable: !0, - }), - Object.defineProperties(M.prototype, { - method: { enumerable: !0 }, - url: { enumerable: !0 }, - headers: { enumerable: !0 }, - redirect: { enumerable: !0 }, - clone: { enumerable: !0 }, - signal: { enumerable: !0 }, - }), - (H.prototype = Object.create(Error.prototype)), - (H.prototype.constructor = H), - (H.prototype.name = "AbortError"); - const G = i.PassThrough, - V = o.resolve; - function W(e, t) { - if (!W.Promise) - throw new Error( - "native promise missing, set fetch.Promise to your favorite alternative" - ); - return ( - (g.Promise = W.Promise), - new W.Promise(function (r, o) { - const c = new M(e, t); - if ("data:" == c[z].parsedURL.protocol) { - if ("get" !== c.method.toLowerCase()) - return o( - new m("Cannot access " + e + " with another method than get.") - ); - let t = q(c[z].parsedURL); - if (((t = t.slice(5)), !t)) return o(new m("Invalid data url")); - let s, - i, - [n, a] = t.split(","); - if (n) { - let [e, t] = n.split(";"); - (i = e || "text/plain"), (s = t || "ascii"); - } else (s = "ascii"), (i = "text/plain"); - let l = new u({}), - h = new P(); - h.set("Content-Type", i + ";charset=ascii"); - const f = { - url: c.url, - status: 200, - statusText: "", - headers: h, - size: 0, - timeout: c.timeout, - counter: c.counter, - }; - let d = new R(l, f); - return l.push(a, s), l.push(null), r(d); - } - if ("file:" == c[z].parsedURL.protocol) { - let t = ["get", "delete", "post"]; - if (!t.includes(c.method.toLowerCase())) - return o( - new m( - "Cannot access " + - e + - " with another method than " + - t.join(", ") + - "." - ) - ); - let i = q(c[z].parsedURL); - if ( - ((i = i.slice(5)), - !( - "win32" == process.platform - ? /file:\/\/\/[A-Z]:\/(([\w ]+\/?(\.[\w ]+)?)+)?/ - : /file:\/\/\/((\w+\/?(\.\w+)?)+)?/ - ).test(q(c[z].parsedURL))) - ) - return o(new m("INVALID URL")); - i = i.slice(3); - let n = s(7); - return n.exists( - i, - (e) => ( - console.log(e), - e - ? n.stat(i, (e, t) => { - if (e) return o(e); - if (t.isDirectory()) - n.readdir( - i, - { encoding: "utf-8", withFileTypes: !0 }, - (e, t) => { - if (e) return o(new m(e.message || e)); - let s = new P(), - i = new u({}), - n = JSON.stringify( - t.map((e) => ({ - name: e.name, - isDirectory: e.isDirectory(), - isFile: e.isFile(), - })) - ); - s.set("type", "folder"); - const a = { - url: c.url, - status: 200, - statusText: "FOLDER", - headers: s, - timeout: c.timeout, - counter: c.counter, - }; - let l = new R(i, a); - return i.push(n), i.push(null), r(l); - } - ); - else if (t.isFile()) { - let e = n.createReadStream(i, { encoding: "utf-8" }); - e.on("error", function (e) { - o( - new m( - `request to ${c.url} failed, reason: ${e.message}`, - "system", - e - ) - ); - }); - let t = new P(); - t.set("type", "file"); - let l = s(8).basename(i).split("."), - u = ""; - function a(e) { - u = e; - } - if (l[1]) { - let e = l[l.length - 1].toLowerCase(); - if (["gif", "jpeg", "jpg", "png", "tiff"].includes(e)) - a("image/" + e); - else - switch (e) { - case "tif": - a("image/tiff"); - break; - case "apng": - a("image/png"); - break; - case "ico": - a("image/x-icon"); - break; - case "svg": - case "svgz": - a("image/svg+xml"); - break; - case "css": - a("text/css"); - case "csv": - a("text/csv"); - break; - case "html": - a("text/html"); - break; - case "js": - a("application/javascript"); - break; - case "xml": - a("text/xml"); - break; - case "mpg": - case "mpeg": - case "mp1": - case "mp2": - case "mp3": - case "m1v": - case "mpv": - case "m1a": - case "m2a": - case "mpa": - a( - (["mpe", "mpeg", "mpg"].includes(e) - ? "video" - : "audio") + "/mpeg" - ); - break; - case "mp4": - case "m4a": - case "m4p": - case "m4b": - case "m4r": - case "m4v": - let t = "audio"; - "mp4" == e && (t = "video"), a(t + "/mp4"); - break; - case "mov": - a("video/quicktime"); - break; - case "wmv": - case "wm": - a("video/x-ms-wmv"); - break; - case "avi": - a("video/x-msvideo"); - break; - case "flv": - a("video/x-flv"); - break; - case "json": - a("application/json"); - break; - case "zip": - a("application/zip"); - break; - case "jsonld": - a("application/ld+json"); - break; - case "ogg": - case "ogv": - case "oga": - case "ogx": - case "spx": - case "opus": - case "ogm": - a("application/ogg"); - break; - case "pdf": - a("application/pdf"); - break; - case "xhtml": - case "xht": - case "htm": - a("application/xhtml+xml"); - break; - case "wav": - a("audio/wav"); - break; - default: - a("text/plain"); - } - } else u = "text/plain"; - (u += ";charset=utf-8"), t.set("content-type", u); - let h = e.pipe(G()); - const f = { - url: c.url, - status: 200, - statusText: "FILE", - headers: t, - timeout: c.timeout, - counter: c.counter, - }; - let d = new R(h, f); - return r(d); - } - }) - : (function () { - let e = new P(), - t = new u({}); - const s = { - url: c.url, - status: 404, - statusText: "ERR_FILE_NOT_FOUND", - headers: e, - size: 0, - timeout: c.timeout, - counter: c.counter, - }; - let i = new R(t, s); - return t.push(null), r(i); - })() - ) - ); - } - const h = (function (e) { - const t = e[z].parsedURL, - s = new P(e[z].headers); - if ( - (s.has("Accept") || s.set("Accept", "*/*"), - !t.protocol || !t.hostname) - ) - throw new TypeError("Only absolute URLs are supported"); - if (!/^https?:$/.test(t.protocol)) - throw new TypeError("Only HTTP(S) protocols are supported"); - if (e.signal && e.body instanceof i.Readable && !F) - throw new Error( - "Cancellation of streamed requests with AbortSignal is not supported in node < 8" - ); - let r = null; - if ( - (null == e.body && /^(POST|PUT)$/i.test(e.method) && (r = "0"), - null != e.body) - ) { - const t = x(e); - "number" == typeof t && (r = String(t)); - } - r && s.set("Content-Length", r), - s.has("User-Agent") || - s.set( - "User-Agent", - "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" - ), - e.compress && - !s.has("Accept-Encoding") && - s.set("Accept-Encoding", "gzip,deflate"); - let n = e.agent; - return ( - "function" == typeof n && (n = n(t)), - s.has("Connection") || n || s.set("Connection", "close"), - Object.assign({}, t, { - method: e.method, - headers: I(s), - agent: n, - }) - ); - })(c), - f = ("https:" === h.protocol ? a : n).request, - d = c.signal; - let p = null; - const g = function () { - let e = new H("The user aborted a request."); - o(e), - c.body && c.body instanceof i.Readable && c.body.destroy(e), - p && p.body && p.body.emit("error", e); - }; - if (d && d.aborted) return void g(); - const y = function () { - g(), S(); - }, - b = f(h); - let w; - function S() { - b.abort(), d && d.removeEventListener("abort", y), clearTimeout(w); - } - d && d.addEventListener("abort", y), - c.timeout && - b.once("socket", function (e) { - w = setTimeout(function () { - o(new m(`network timeout at: ${c.url}`, "request-timeout")), - S(); - }, c.timeout); - }), - b.on("error", function (e) { - o( - new m( - `request to ${c.url} failed, reason: ${e.message}`, - "system", - e - ) - ), - S(); - }), - b.on("response", function (e) { - clearTimeout(w); - const t = (function (e) { - const t = new P(); - for (const s of Object.keys(e)) - if (!j.test(s)) - if (Array.isArray(e[s])) - for (const r of e[s]) - T.test(r) || - (void 0 === t[_][s] - ? (t[_][s] = [r]) - : t[_][s].push(r)); - else T.test(e[s]) || (t[_][s] = [e[s]]); - return t; - })(e.headers); - if (W.isRedirect(e.statusCode)) { - const s = t.get("Location"), - i = null === s ? null : V(c.url, s); - switch (c.redirect) { - case "error": - return ( - o( - new m( - `redirect mode is set to error: ${c.url}`, - "no-redirect" - ) - ), - void S() - ); - case "manual": - if (null !== i) - try { - t.set("Location", i); - } catch (e) { - o(e); - } - break; - case "follow": - if (null === i) break; - if (c.counter >= c.follow) - return ( - o( - new m( - `maximum redirect reached at: ${c.url}`, - "max-redirect" - ) - ), - void S() - ); - const s = { - headers: new P(c.headers), - follow: c.follow, - counter: c.counter + 1, - agent: c.agent, - compress: c.compress, - method: c.method, - body: c.body, - signal: c.signal, - timeout: c.timeout, - }; - return 303 !== e.statusCode && c.body && null === x(c) - ? (o( - new m( - "Cannot follow redirect with body being a readable stream", - "unsupported-redirect" - ) - ), - void S()) - : ((303 !== e.statusCode && - ((301 !== e.statusCode && 302 !== e.statusCode) || - "POST" !== c.method)) || - ((s.method = "GET"), - (s.body = void 0), - s.headers.delete("content-length")), - r(W(new M(i, s))), - void S()); - } - } - e.once("end", function () { - d && d.removeEventListener("abort", y); - }); - let s = e.pipe(new G()); - const i = { - url: c.url, - status: e.statusCode, - statusText: e.statusMessage, - headers: t, - size: c.size, - timeout: c.timeout, - counter: c.counter, - }, - n = t.get("Content-Encoding"); - if ( - !c.compress || - "HEAD" === c.method || - null === n || - 204 === e.statusCode || - 304 === e.statusCode - ) - return (p = new R(s, i)), void r(p); - const a = { flush: l.Z_SYNC_FLUSH, finishFlush: l.Z_SYNC_FLUSH }; - if ("gzip" == n || "x-gzip" == n) - return ( - (s = s.pipe(l.createGunzip(a))), (p = new R(s, i)), void r(p) - ); - if ("deflate" != n && "x-deflate" != n) { - if ("br" == n && "function" == typeof l.createBrotliDecompress) - return ( - (s = s.pipe(l.createBrotliDecompress())), - (p = new R(s, i)), - void r(p) - ); - (p = new R(s, i)), r(p); - } else { - e.pipe(new G()).once("data", function (e) { - (s = - 8 == (15 & e[0]) - ? s.pipe(l.createInflate()) - : s.pipe(l.createInflateRaw())), - (p = new R(s, i)), - r(p); - }); - } - }), - (function (e, t) { - const s = t.body; - null === s - ? e.end() - : v(s) - ? s.stream().pipe(e) - : Buffer.isBuffer(s) - ? (e.write(s), e.end()) - : s.pipe(e); - })(b, c); - }) - ); - } - (W.isRedirect = function (e) { - return 301 === e || 302 === e || 303 === e || 307 === e || 308 === e; - }), - (W.Promise = global.Promise), - (e.exports = t = W), - Object.defineProperty(t, "__esModule", { value: !0 }), - (t.default = t), - (t.Headers = P), - (t.Request = M), - (t.Response = R), - (t.FetchError = m); - }, - function (e, t) { - e.exports = require("stream"); - }, - function (e, t) { - e.exports = require("http"); - }, - function (e, t) { - e.exports = require("url"); - }, - function (e, t) { - e.exports = require("https"); - }, - function (e, t) { - e.exports = require("zlib"); - }, - function (e, t) { - e.exports = require("fs"); - }, - function (e, t) { - e.exports = require("path"); - }, - function (e, t, s) { - "use strict"; - Object.defineProperty(t, "__esModule", { value: !0 }), - (t.default = { - discord() { - let e = "https://discord.com/api/v9"; - return { - toString: () => e, - users(t) { - let s = e + "/users/" + t; - return { toString: () => s, guilds: () => s + "/guilds" }; - }, - application(t) { - let s = e + "/oauth2/applications/" + t; - return { toString: () => s, assets: () => s + "/assets" }; - }, - }; - }, - ua: "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) discord/0.0.305 Chrome/69.0.3497.128 Electron/4.0.8 Safari/537.36", - acceptedLangs: "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7", - }); - }, - function (e, t) { - e.exports = require("../../package.json"); - }, - ]); - module.exports.uuid = () => { - return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (a) => - (a ^ ((Math.random() * 16) >> (a / 4))).toString(16), - ); - }; \ No newline at end of file diff --git a/src/client/BaseClient.js b/src/client/BaseClient.js index cdedff8..3bf037f 100644 --- a/src/client/BaseClient.js +++ b/src/client/BaseClient.js @@ -2,7 +2,6 @@ const EventEmitter = require('node:events'); const RESTManager = require('../rest/RESTManager'); -const { TypeError } = require('../errors'); const Options = require('../util/Options'); const Util = require('../util/Util'); @@ -12,11 +11,7 @@ const Util = require('../util/Util'); */ class BaseClient extends EventEmitter { constructor(options = {}) { - super({ captureRejections: true }); - - if (typeof options !== 'object' || options === null) { - throw new TypeError('INVALID_TYPE', 'options', 'object', true); - } + super(); /** * The options the client was instantiated with @@ -26,11 +21,18 @@ class BaseClient extends EventEmitter { /** * The REST manager of the client - * @type {REST} + * @type {RESTManager} + * @private */ this.rest = new RESTManager(this); } + /** + * API shortcut + * @type {Object} + * @readonly + * @private + */ get api() { return this.rest.api; } @@ -40,7 +42,7 @@ class BaseClient extends EventEmitter { * @returns {void} */ destroy() { - if(this.rest.sweepInterval) clearInterval(this.rest.sweepInterval); + if (this.rest.sweepInterval) clearInterval(this.rest.sweepInterval); } /** @@ -73,6 +75,7 @@ class BaseClient extends EventEmitter { module.exports = BaseClient; /** - * @external REST - * @see {@link https://discord.js.org/#/docs/rest/main/class/REST} + * Emitted for general debugging information. + * @event BaseClient#debug + * @param {string} info The debug information */ diff --git a/src/client/Client.js b/src/client/Client.js index 56979a5..d198965 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -1,8 +1,8 @@ 'use strict'; const process = require('node:process'); +const { setInterval } = require('node:timers'); const { Collection } = require('@discordjs/collection'); -const { OAuth2Scopes, Routes } = require('discord-api-types/v9'); const BaseClient = require('./BaseClient'); const ActionsManager = require('./actions/ActionsManager'); const ClientVoiceManager = require('./voice/ClientVoiceManager'); @@ -12,9 +12,6 @@ const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager'); const ChannelManager = require('../managers/ChannelManager'); const GuildManager = require('../managers/GuildManager'); const UserManager = require('../managers/UserManager'); -const FriendsManager = require('../managers/FriendsManager'); -const BlockedManager = require('../managers/BlockedManager'); -const ClientUserSettingManager = require('../managers/ClientUserSettingManager'); const ShardClientUtil = require('../sharding/ShardClientUtil'); const ClientPresence = require('../structures/ClientPresence'); const GuildPreview = require('../structures/GuildPreview'); @@ -25,13 +22,16 @@ const StickerPack = require('../structures/StickerPack'); const VoiceRegion = require('../structures/VoiceRegion'); const Webhook = require('../structures/Webhook'); const Widget = require('../structures/Widget'); +const { Events, InviteScopes, Status } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); -const Events = require('../util/Events'); -const IntentsBitField = require('../util/IntentsBitField'); +const Intents = require('../util/Intents'); const Options = require('../util/Options'); -const PermissionsBitField = require('../util/PermissionsBitField'); -const Status = require('../util/Status'); +const Permissions = require('../util/Permissions'); const Sweepers = require('../util/Sweepers'); +// Patch +const FriendsManager = require('../managers/FriendsManager'); +const BlockedManager = require('../managers/BlockedManager'); +const ClientUserSettingManager = require('../managers/ClientUserSettingManager'); /** * The main hub for interacting with the Discord API, and the starting point for any bot. @@ -79,6 +79,20 @@ class Client extends BaseClient { this._validateOptions(); + /** + * Functions called when a cache is garbage collected or the Client is destroyed + * @type {Set} + * @private + */ + this._cleanups = new Set(); + + /** + * The finalizers used to cleanup items. + * @type {FinalizationRegistry} + * @private + */ + this._finalizers = new FinalizationRegistry(this._finalize.bind(this)); + /** * The WebSocket manager of the client * @type {WebSocketManager} @@ -111,6 +125,10 @@ class Client extends BaseClient { * @type {UserManager} */ this.users = new UserManager(this); + + /** Patch + * + */ this.friends = new FriendsManager(this); this.blocked = new BlockedManager(this); this.setting = new ClientUserSettingManager(this); @@ -156,12 +174,6 @@ class Client extends BaseClient { this.token = null; } - /** - * used for interacitons - * @type {?String} - */ - this.session_id = null; - /** * User that the client is logged in as * @type {?ClientUser} @@ -173,13 +185,24 @@ class Client extends BaseClient { * @type {?ClientApplication} */ this.application = null; - this.bot = null; /** - * Timestamp of the time the client was last `READY` at - * @type {?number} + * Time at which the client was last regarded as being in the `READY` state + * (each time the client disconnects and successfully reconnects, this will be overwritten) + * @type {?Date} */ - this.readyTimestamp = null; + this.readyAt = null; + + if (this.options.messageSweepInterval > 0) { + process.emitWarning( + 'The message sweeping client options are deprecated, use the global sweepers instead.', + 'DeprecationWarning', + ); + this.sweepMessageInterval = setInterval( + this.sweepMessages.bind(this), + this.options.messageSweepInterval * 1_000, + ).unref(); + } } /** @@ -196,13 +219,12 @@ class Client extends BaseClient { } /** - * Time at which the client was last regarded as being in the `READY` state - * (each time the client disconnects and successfully reconnects, this will be overwritten) - * @type {?Date} + * Timestamp of the time the client was last `READY` at + * @type {?number} * @readonly */ - get readyAt() { - return this.readyTimestamp && new Date(this.readyTimestamp); + get readyTimestamp() { + return this.readyAt?.getTime() ?? null; } /** @@ -211,23 +233,21 @@ class Client extends BaseClient { * @readonly */ get uptime() { - return this.readyTimestamp && Date.now() - this.readyTimestamp; + return this.readyAt ? Date.now() - this.readyAt : null; } /** * Logs the client in, establishing a WebSocket connection to Discord. * @param {string} [token=this.token] Token of the account to log in with - * @param {Boolean} [bot=false] Wether the token used is a bot account or not * @returns {Promise} Token of the account used * @example * client.login('my token'); */ - async login(token = this.token, bot = false) { + async login(token = this.token) { if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID'); this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); - this.bot = bot; this.emit( - Events.Debug, + Events.DEBUG, `Provided token: ${token .split('.') .map((val, i) => (i > 1 ? val.replace(/./g, '*') : val)) @@ -238,7 +258,7 @@ class Client extends BaseClient { this.options.ws.presence = this.presence._parse(this.options.presence); } - this.emit(Events.Debug, 'Preparing to connect to the gateway...'); + this.emit(Events.DEBUG, 'Preparing to connect to the gateway...'); try { await this.ws.connect(); @@ -255,7 +275,7 @@ class Client extends BaseClient { * @returns {boolean} */ isReady() { - return this.ws.status === Status.Ready; + return this.ws.status === Status.READY; } /** @@ -265,10 +285,14 @@ class Client extends BaseClient { destroy() { super.destroy(); + for (const fn of this._cleanups) fn(); + this._cleanups.clear(); + + if (this.sweepMessageInterval) clearInterval(this.sweepMessageInterval); + this.sweepers.destroy(); this.ws.destroy(); this.token = null; - //this.rest.setToken(null); } /** @@ -290,14 +314,9 @@ class Client extends BaseClient { */ async fetchInvite(invite, options) { const code = DataResolver.resolveInviteCode(invite); - const query = new URLSearchParams({ - with_counts: true, - with_expiration: true, + const data = await this.api.invites(code).get({ + query: { with_counts: true, with_expiration: true, guild_scheduled_event_id: options?.guildScheduledEventId }, }); - if (options?.guildScheduledEventId) { - query.set('guild_scheduled_event_id', options.guildScheduledEventId); - } - const data = await this.api.invites(code).get({ query }); return new Invite(this, data); } @@ -327,7 +346,7 @@ class Client extends BaseClient { * .catch(console.error); */ async fetchWebhook(id, token) { - const data = await this.api.webhook(id, token).get(); + const data = await this.api.webhooks(id, token).get(); return new Webhook(this, { token, ...data }); } @@ -372,6 +391,50 @@ class Client extends BaseClient { const data = await this.api('sticker-packs').get(); return new Collection(data.sticker_packs.map(p => [p.id, new StickerPack(this, p)])); } + /** + * A last ditch cleanup function for garbage collection. + * @param {Function} options.cleanup The function called to GC + * @param {string} [options.message] The message to send after a successful GC + * @param {string} [options.name] The name of the item being GCed + * @private + */ + _finalize({ cleanup, message, name }) { + try { + cleanup(); + this._cleanups.delete(cleanup); + if (message) { + this.emit(Events.DEBUG, message); + } + } catch { + this.emit(Events.DEBUG, `Garbage collection failed on ${name ?? 'an unknown item'}.`); + } + } + + /** + * Sweeps all text-based channels' messages and removes the ones older than the max message lifetime. + * If the message has been edited, the time of the edit is used rather than the time of the original message. + * @param {number} [lifetime=this.options.messageCacheLifetime] Messages that are older than this (in seconds) + * will be removed from the caches. The default is based on {@link ClientOptions#messageCacheLifetime} + * @returns {number} Amount of messages that were removed from the caches, + * or -1 if the message cache lifetime is unlimited + * @example + * // Remove all messages older than 1800 seconds from the messages cache + * const amount = client.sweepMessages(1800); + * console.log(`Successfully removed ${amount} messages from the cache.`); + */ + sweepMessages(lifetime = this.options.messageCacheLifetime) { + if (typeof lifetime !== 'number' || isNaN(lifetime)) { + throw new TypeError('INVALID_TYPE', 'lifetime', 'number'); + } + if (lifetime <= 0) { + this.emit(Events.DEBUG, "Didn't sweep messages - lifetime is unlimited"); + return -1; + } + + const messages = this.sweepers.sweepMessages(Sweepers.outdatedMessageSweepFilter(lifetime)()); + this.emit(Events.DEBUG, `Swept ${messages} messages older than ${lifetime} seconds`); + return messages; + } /** * Obtains a guild preview from Discord, available for all guilds the bot is in and all Discoverable guilds. @@ -400,7 +463,7 @@ class Client extends BaseClient { /** * Options for {@link Client#generateInvite}. * @typedef {Object} InviteGenerationOptions - * @property {OAuth2Scopes[]} scopes Scopes that should be requested + * @property {InviteScope[]} scopes Scopes that should be requested * @property {PermissionResolvable} [permissions] Permissions to request * @property {GuildResolvable} [guild] Guild to preselect * @property {boolean} [disableGuildSelect] Whether to disable the guild selection @@ -412,17 +475,17 @@ class Client extends BaseClient { * @returns {string} * @example * const link = client.generateInvite({ - * scopes: [OAuth2Scopes.ApplicationsCommands], + * scopes: ['applications.commands'], * }); * console.log(`Generated application invite link: ${link}`); * @example * const link = client.generateInvite({ * permissions: [ - * PermissionFlagsBits.SendMessages, - * PermissionFlagsBits.ManageGuild, - * PermissionFlagsBits.MentionEveryone, + * Permissions.FLAGS.SEND_MESSAGES, + * Permissions.FLAGS.MANAGE_GUILD, + * Permissions.FLAGS.MENTION_EVERYONE, * ], - * scopes: [OAuth2Scopes.Bot], + * scopes: ['bot'], * }); * console.log(`Generated bot invite link: ${link}`); */ @@ -441,18 +504,17 @@ class Client extends BaseClient { if (!Array.isArray(scopes)) { throw new TypeError('INVALID_TYPE', 'scopes', 'Array of Invite Scopes', true); } - if (!scopes.some(scope => [OAuth2Scopes.Bot, OAuth2Scopes.ApplicationsCommands].includes(scope))) { + if (!scopes.some(scope => ['bot', 'applications.commands'].includes(scope))) { throw new TypeError('INVITE_MISSING_SCOPES'); } - const validScopes = Object.values(OAuth2Scopes); - const invalidScope = scopes.find(scope => !validScopes.includes(scope)); + const invalidScope = scopes.find(scope => !InviteScopes.includes(scope)); if (invalidScope) { throw new TypeError('INVALID_ELEMENT', 'Array', 'scopes', invalidScope); } query.set('scope', scopes.join(' ')); if (options.permissions) { - const permissions = PermissionsBitField.resolve(options.permissions); + const permissions = Permissions.resolve(options.permissions); if (permissions) query.set('permissions', permissions); } @@ -466,7 +528,7 @@ class Client extends BaseClient { query.set('guild_id', guildId); } - return `${this.options.rest.api}${Routes.oauth2Authorization()}?${query}`; + return `${this.options.http.api}${this.api.oauth2.authorize}?${query}`; } toJSON() { @@ -495,7 +557,7 @@ class Client extends BaseClient { if (typeof options.intents === 'undefined') { throw new TypeError('CLIENT_MISSING_INTENTS'); } else { - options.intents = IntentsBitField.resolve(options.intents); + options.intents = Intents.resolve(options.intents); } if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) { throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number greater than or equal to 1'); @@ -507,42 +569,56 @@ class Client extends BaseClient { if (typeof options.makeCache !== 'function') { throw new TypeError('CLIENT_INVALID_OPTION', 'makeCache', 'a function'); } + if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'The messageCacheLifetime', 'a number'); + } + if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'messageSweepInterval', 'a number'); + } if (typeof options.sweepers !== 'object' || options.sweepers === null) { throw new TypeError('CLIENT_INVALID_OPTION', 'sweepers', 'an object'); } + if (typeof options.invalidRequestWarningInterval !== 'number' || isNaN(options.invalidRequestWarningInterval)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'invalidRequestWarningInterval', 'a number'); + } if (!Array.isArray(options.partials)) { throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array'); } if (typeof options.waitGuildTimeout !== 'number' || isNaN(options.waitGuildTimeout)) { throw new TypeError('CLIENT_INVALID_OPTION', 'waitGuildTimeout', 'a number'); } + if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'restWsBridgeTimeout', 'a number'); + } + if (typeof options.restRequestTimeout !== 'number' || isNaN(options.restRequestTimeout)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'restRequestTimeout', 'a number'); + } + if (typeof options.restGlobalRateLimit !== 'number' || isNaN(options.restGlobalRateLimit)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'restGlobalRateLimit', 'a number'); + } + if (typeof options.restSweepInterval !== 'number' || isNaN(options.restSweepInterval)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'restSweepInterval', 'a number'); + } + if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'retryLimit', 'a number'); + } if (typeof options.failIfNotExists !== 'boolean') { throw new TypeError('CLIENT_INVALID_OPTION', 'failIfNotExists', 'a boolean'); } + if (!Array.isArray(options.userAgentSuffix)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'userAgentSuffix', 'an array of strings'); + } + if ( + typeof options.rejectOnRateLimit !== 'undefined' && + !(typeof options.rejectOnRateLimit === 'function' || Array.isArray(options.rejectOnRateLimit)) + ) { + throw new TypeError('CLIENT_INVALID_OPTION', 'rejectOnRateLimit', 'an array or a function'); + } } } module.exports = Client; -/** - * A {@link https://developer.twitter.com/en/docs/twitter-ids Twitter snowflake}, - * except the epoch is 2015-01-01T00:00:00.000Z. - * - * If we have a snowflake '266241948824764416' we can represent it as binary: - * ``` - * 64 22 17 12 0 - * 000000111011000111100001101001000101000000 00001 00000 000000000000 - * number of milliseconds since Discord epoch worker pid increment - * ``` - * @typedef {string} Snowflake - */ - -/** - * Emitted for general debugging information. - * @event Client#debug - * @param {string} info The debug information - */ - /** * Emitted for general warnings. * @event Client#warn @@ -553,13 +629,3 @@ module.exports = Client; * @external Collection * @see {@link https://discord.js.org/#/docs/collection/main/class/Collection} */ - -/** - * @external ImageURLOptions - * @see {@link https://discord.js.org/#/docs/rest/main/typedef/ImageURLOptions} - */ - -/** - * @external BaseImageURLOptions - * @see {@link https://discord.js.org/#/docs/rest/main/typedef/BaseImageURLOptions} - */ diff --git a/src/client/WebhookClient.js b/src/client/WebhookClient.js index 1c66194..ce458a7 100644 --- a/src/client/WebhookClient.js +++ b/src/client/WebhookClient.js @@ -30,7 +30,7 @@ class WebhookClient extends BaseClient { if ('url' in data) { const url = data.url.match( // eslint-disable-next-line no-useless-escape - /https?:\/\/(?:ptb\.|canary\.)?discord\.com\/api(?:\/v\d{1,2})?\/webhooks\/(\d{17,19})\/([\w-]{68})/i, + /^https?:\/\/(?:canary|ptb)?\.?discord\.com\/api\/webhooks(?:\/v[0-9]\d*)?\/([^\/]+)\/([^\/]+)/i, ); if (!url || url.length <= 1) throw new Error('WEBHOOK_URL_INVALID'); diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index f70d3ca..8d0b6df 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -1,6 +1,6 @@ 'use strict'; -const Partials = require('../../util/Partials'); +const { PartialTypes } = require('../../util/Constants'); /* @@ -43,7 +43,7 @@ class GenericAction { }, this.client.channels, id, - Partials.Channel, + PartialTypes.CHANNEL, ) ); } @@ -60,7 +60,7 @@ class GenericAction { }, channel.messages, id, - Partials.Message, + PartialTypes.MESSAGE, cache, ) ); @@ -76,17 +76,17 @@ class GenericAction { }, message.reactions, id, - Partials.Reaction, + PartialTypes.REACTION, ); } getMember(data, guild) { - return this.getPayload(data, guild.members, data.user.id, Partials.GuildMember); + return this.getPayload(data, guild.members, data.user.id, PartialTypes.GUILD_MEMBER); } getUser(data) { const id = data.user_id; - return data.user ?? this.getPayload({ id }, this.client.users, id, Partials.User); + return data.user ?? this.getPayload({ id }, this.client.users, id, PartialTypes.USER); } getUserFromMember(data) { @@ -107,7 +107,7 @@ class GenericAction { { id, guild_id: data.guild_id ?? guild.id }, guild.scheduledEvents, id, - Partials.GuildScheduledEvent, + PartialTypes.GUILD_SCHEDULED_EVENT, ); } } diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js index fdf8ddd..68eb2dc 100644 --- a/src/client/actions/ChannelCreate.js +++ b/src/client/actions/ChannelCreate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class ChannelCreateAction extends Action { handle(data) { @@ -14,7 +14,7 @@ class ChannelCreateAction extends Action { * @event Client#channelCreate * @param {GuildChannel} channel The channel that was created */ - client.emit(Events.ChannelCreate, channel); + client.emit(Events.CHANNEL_CREATE, channel); } return { channel }; } diff --git a/src/client/actions/ChannelDelete.js b/src/client/actions/ChannelDelete.js index acf03d9..43fdb32 100644 --- a/src/client/actions/ChannelDelete.js +++ b/src/client/actions/ChannelDelete.js @@ -1,22 +1,38 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { deletedChannels } = require('../../structures/Channel'); +const DMChannel = require('../../structures/DMChannel'); +const { deletedMessages } = require('../../structures/Message'); +const { Events } = require('../../util/Constants'); class ChannelDeleteAction extends Action { + constructor(client) { + super(client); + this.deleted = new Map(); + } + handle(data) { const client = this.client; const channel = client.channels.cache.get(data.id); if (channel) { client.channels._remove(channel.id); + deletedChannels.add(channel); + if (channel.messages && !(channel instanceof DMChannel)) { + for (const message of channel.messages.cache.values()) { + deletedMessages.add(message); + } + } /** * Emitted whenever a channel is deleted. * @event Client#channelDelete * @param {DMChannel|GuildChannel} channel The channel that was deleted */ - client.emit(Events.ChannelDelete, channel); + client.emit(Events.CHANNEL_DELETE, channel); } + + return { channel }; } } diff --git a/src/client/actions/ChannelUpdate.js b/src/client/actions/ChannelUpdate.js index 88ee7f1..34d1a86 100644 --- a/src/client/actions/ChannelUpdate.js +++ b/src/client/actions/ChannelUpdate.js @@ -2,6 +2,7 @@ const Action = require('./Action'); const { Channel } = require('../../structures/Channel'); +const { ChannelTypes } = require('../../util/Constants'); class ChannelUpdateAction extends Action { handle(data) { @@ -11,7 +12,7 @@ class ChannelUpdateAction extends Action { if (channel) { const old = channel._update(data); - if (channel.type !== data.type) { + if (ChannelTypes[channel.type] !== data.type) { const newChannel = Channel.create(this.client, data, channel.guild); for (const [id, message] of channel.messages.cache) newChannel.messages.cache.set(id, message); channel = newChannel; diff --git a/src/client/actions/GuildBanAdd.js b/src/client/actions/GuildBanAdd.js index 2ef4b11..e97e789 100644 --- a/src/client/actions/GuildBanAdd.js +++ b/src/client/actions/GuildBanAdd.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildBanAdd extends Action { handle(data) { @@ -13,7 +13,7 @@ class GuildBanAdd extends Action { * @event Client#guildBanAdd * @param {GuildBan} ban The ban that occurred */ - if (guild) client.emit(Events.GuildBanAdd, guild.bans._add(data)); + if (guild) client.emit(Events.GUILD_BAN_ADD, guild.bans._add(data)); } } diff --git a/src/client/actions/GuildBanRemove.js b/src/client/actions/GuildBanRemove.js index 8048efd..5154735 100644 --- a/src/client/actions/GuildBanRemove.js +++ b/src/client/actions/GuildBanRemove.js @@ -2,7 +2,7 @@ const Action = require('./Action'); const GuildBan = require('../../structures/GuildBan'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildBanRemove extends Action { handle(data) { @@ -17,7 +17,7 @@ class GuildBanRemove extends Action { if (guild) { const ban = guild.bans.cache.get(data.user.id) ?? new GuildBan(client, data, guild); guild.bans.cache.delete(ban.user.id); - client.emit(Events.GuildBanRemove, ban); + client.emit(Events.GUILD_BAN_REMOVE, ban); } } } diff --git a/src/client/actions/GuildDelete.js b/src/client/actions/GuildDelete.js index eb0a44d..8ab860b 100644 --- a/src/client/actions/GuildDelete.js +++ b/src/client/actions/GuildDelete.js @@ -1,9 +1,16 @@ 'use strict'; +const { setTimeout } = require('node:timers'); const Action = require('./Action'); -const Events = require('../../util/Events'); +const { deletedGuilds } = require('../../structures/Guild'); +const { Events } = require('../../util/Constants'); class GuildDeleteAction extends Action { + constructor(client) { + super(client); + this.deleted = new Map(); + } + handle(data) { const client = this.client; @@ -18,11 +25,13 @@ class GuildDeleteAction extends Action { * @event Client#guildUnavailable * @param {Guild} guild The guild that has become unavailable */ - client.emit(Events.GuildUnavailable, guild); + client.emit(Events.GUILD_UNAVAILABLE, guild); // Stops the GuildDelete packet thinking a guild was actually deleted, // handles emitting of event itself - return; + return { + guild: null, + }; } for (const channel of guild.channels.cache.values()) this.client.channels._remove(channel.id); @@ -30,14 +39,26 @@ class GuildDeleteAction extends Action { // Delete guild client.guilds.cache.delete(guild.id); + deletedGuilds.add(guild); /** * Emitted whenever a guild kicks the client or the guild is deleted/left. * @event Client#guildDelete * @param {Guild} guild The guild that was deleted */ - client.emit(Events.GuildDelete, guild); + client.emit(Events.GUILD_DELETE, guild); + + this.deleted.set(guild.id, guild); + this.scheduleForDeletion(guild.id); + } else { + guild = this.deleted.get(data.id) ?? null; } + + return { guild }; + } + + scheduleForDeletion(id) { + setTimeout(() => this.deleted.delete(id), this.client.options.restWsBridgeTimeout).unref(); } } diff --git a/src/client/actions/GuildEmojiCreate.js b/src/client/actions/GuildEmojiCreate.js index 61858cf..ccc634c 100644 --- a/src/client/actions/GuildEmojiCreate.js +++ b/src/client/actions/GuildEmojiCreate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildEmojiCreateAction extends Action { handle(guild, createdEmoji) { @@ -12,7 +12,7 @@ class GuildEmojiCreateAction extends Action { * @event Client#emojiCreate * @param {GuildEmoji} emoji The emoji that was created */ - if (!already) this.client.emit(Events.GuildEmojiCreate, emoji); + if (!already) this.client.emit(Events.GUILD_EMOJI_CREATE, emoji); return { emoji }; } } diff --git a/src/client/actions/GuildEmojiDelete.js b/src/client/actions/GuildEmojiDelete.js index e3373c2..0c20287 100644 --- a/src/client/actions/GuildEmojiDelete.js +++ b/src/client/actions/GuildEmojiDelete.js @@ -1,17 +1,19 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { deletedEmojis } = require('../../structures/Emoji'); +const { Events } = require('../../util/Constants'); class GuildEmojiDeleteAction extends Action { handle(emoji) { emoji.guild.emojis.cache.delete(emoji.id); + deletedEmojis.add(emoji); /** * Emitted whenever a custom emoji is deleted in a guild. * @event Client#emojiDelete * @param {GuildEmoji} emoji The emoji that was deleted */ - this.client.emit(Events.GuildEmojiDelete, emoji); + this.client.emit(Events.GUILD_EMOJI_DELETE, emoji); return { emoji }; } } diff --git a/src/client/actions/GuildEmojiUpdate.js b/src/client/actions/GuildEmojiUpdate.js index 6bf9657..9fa59c9 100644 --- a/src/client/actions/GuildEmojiUpdate.js +++ b/src/client/actions/GuildEmojiUpdate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildEmojiUpdateAction extends Action { handle(current, data) { @@ -12,7 +12,7 @@ class GuildEmojiUpdateAction extends Action { * @param {GuildEmoji} oldEmoji The old emoji * @param {GuildEmoji} newEmoji The new emoji */ - this.client.emit(Events.GuildEmojiUpdate, old, current); + this.client.emit(Events.GUILD_EMOJI_UPDATE, old, current); return { emoji: current }; } } diff --git a/src/client/actions/GuildIntegrationsUpdate.js b/src/client/actions/GuildIntegrationsUpdate.js index 28b9bbb..bbce076 100644 --- a/src/client/actions/GuildIntegrationsUpdate.js +++ b/src/client/actions/GuildIntegrationsUpdate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildIntegrationsUpdate extends Action { handle(data) { @@ -12,7 +12,7 @@ class GuildIntegrationsUpdate extends Action { * @event Client#guildIntegrationsUpdate * @param {Guild} guild The guild whose integrations were updated */ - if (guild) client.emit(Events.GuildIntegrationsUpdate, guild); + if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild); } } diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index 646f4ec..5776696 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -1,8 +1,8 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); -const Status = require('../../util/Status'); +const { deletedGuildMembers } = require('../../structures/GuildMember'); +const { Events, Status } = require('../../util/Constants'); class GuildMemberRemoveAction extends Action { handle(data, shard) { @@ -13,13 +13,14 @@ class GuildMemberRemoveAction extends Action { member = this.getMember({ user: data.user }, guild); guild.memberCount--; if (member) { + deletedGuildMembers.add(member); guild.members.cache.delete(member.id); /** * Emitted whenever a member leaves a guild, or is kicked. * @event Client#guildMemberRemove * @param {GuildMember} member The member that has left/been kicked from the guild */ - if (shard.status === Status.Ready) client.emit(Events.GuildMemberRemove, member); + if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); } guild.voiceStates.cache.delete(data.user.id); } diff --git a/src/client/actions/GuildMemberUpdate.js b/src/client/actions/GuildMemberUpdate.js index dc41a79..2839685 100644 --- a/src/client/actions/GuildMemberUpdate.js +++ b/src/client/actions/GuildMemberUpdate.js @@ -1,8 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); -const Status = require('../../util/Status'); +const { Status, Events } = require('../../util/Constants'); class GuildMemberUpdateAction extends Action { handle(data, shard) { @@ -27,7 +26,7 @@ class GuildMemberUpdateAction extends Action { * @param {GuildMember} oldMember The member before the update * @param {GuildMember} newMember The member after the update */ - if (shard.status === Status.Ready && !member.equals(old)) client.emit(Events.GuildMemberUpdate, old, member); + if (shard.status === Status.READY && !member.equals(old)) client.emit(Events.GUILD_MEMBER_UPDATE, old, member); } else { const newMember = guild.members._add(data); /** @@ -35,7 +34,7 @@ class GuildMemberUpdateAction extends Action { * @event Client#guildMemberAvailable * @param {GuildMember} member The member that became available */ - this.client.emit(Events.GuildMemberAvailable, newMember); + this.client.emit(Events.GUILD_MEMBER_AVAILABLE, newMember); } } } diff --git a/src/client/actions/GuildRoleCreate.js b/src/client/actions/GuildRoleCreate.js index 461443b..102e0f6 100644 --- a/src/client/actions/GuildRoleCreate.js +++ b/src/client/actions/GuildRoleCreate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildRoleCreate extends Action { handle(data) { @@ -16,7 +16,7 @@ class GuildRoleCreate extends Action { * @event Client#roleCreate * @param {Role} role The role that was created */ - if (!already) client.emit(Events.GuildRoleCreate, role); + if (!already) client.emit(Events.GUILD_ROLE_CREATE, role); } return { role }; } diff --git a/src/client/actions/GuildRoleDelete.js b/src/client/actions/GuildRoleDelete.js index e043a1a..9d156db 100644 --- a/src/client/actions/GuildRoleDelete.js +++ b/src/client/actions/GuildRoleDelete.js @@ -1,7 +1,8 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { deletedRoles } = require('../../structures/Role'); +const { Events } = require('../../util/Constants'); class GuildRoleDeleteAction extends Action { handle(data) { @@ -13,12 +14,13 @@ class GuildRoleDeleteAction extends Action { role = guild.roles.cache.get(data.role_id); if (role) { guild.roles.cache.delete(data.role_id); + deletedRoles.add(role); /** * Emitted whenever a guild role is deleted. * @event Client#roleDelete * @param {Role} role The role that was deleted */ - client.emit(Events.GuildRoleDelete, role); + client.emit(Events.GUILD_ROLE_DELETE, role); } } diff --git a/src/client/actions/GuildRoleUpdate.js b/src/client/actions/GuildRoleUpdate.js index b0632c5..faea120 100644 --- a/src/client/actions/GuildRoleUpdate.js +++ b/src/client/actions/GuildRoleUpdate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildRoleUpdateAction extends Action { handle(data) { @@ -20,7 +20,7 @@ class GuildRoleUpdateAction extends Action { * @param {Role} oldRole The role before the update * @param {Role} newRole The role after the update */ - client.emit(Events.GuildRoleUpdate, old, role); + client.emit(Events.GUILD_ROLE_UPDATE, old, role); } return { diff --git a/src/client/actions/GuildScheduledEventCreate.js b/src/client/actions/GuildScheduledEventCreate.js index 0a2fb9b..b1a2d92 100644 --- a/src/client/actions/GuildScheduledEventCreate.js +++ b/src/client/actions/GuildScheduledEventCreate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildScheduledEventCreateAction extends Action { handle(data) { @@ -15,7 +15,7 @@ class GuildScheduledEventCreateAction extends Action { * @event Client#guildScheduledEventCreate * @param {GuildScheduledEvent} guildScheduledEvent The created guild scheduled event */ - client.emit(Events.GuildScheduledEventCreate, guildScheduledEvent); + client.emit(Events.GUILD_SCHEDULED_EVENT_CREATE, guildScheduledEvent); return { guildScheduledEvent }; } diff --git a/src/client/actions/GuildScheduledEventDelete.js b/src/client/actions/GuildScheduledEventDelete.js index 636bfc5..0e7baa1 100644 --- a/src/client/actions/GuildScheduledEventDelete.js +++ b/src/client/actions/GuildScheduledEventDelete.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildScheduledEventDeleteAction extends Action { handle(data) { @@ -18,7 +18,7 @@ class GuildScheduledEventDeleteAction extends Action { * @event Client#guildScheduledEventDelete * @param {GuildScheduledEvent} guildScheduledEvent The deleted guild scheduled event */ - client.emit(Events.GuildScheduledEventDelete, guildScheduledEvent); + client.emit(Events.GUILD_SCHEDULED_EVENT_DELETE, guildScheduledEvent); return { guildScheduledEvent }; } diff --git a/src/client/actions/GuildScheduledEventUpdate.js b/src/client/actions/GuildScheduledEventUpdate.js index 7cabd85..dfb86db 100644 --- a/src/client/actions/GuildScheduledEventUpdate.js +++ b/src/client/actions/GuildScheduledEventUpdate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildScheduledEventUpdateAction extends Action { handle(data) { @@ -18,7 +18,7 @@ class GuildScheduledEventUpdateAction extends Action { * @param {?GuildScheduledEvent} oldGuildScheduledEvent The guild scheduled event object before the update * @param {GuildScheduledEvent} newGuildScheduledEvent The guild scheduled event object after the update */ - client.emit(Events.GuildScheduledEventUpdate, oldGuildScheduledEvent, newGuildScheduledEvent); + client.emit(Events.GUILD_SCHEDULED_EVENT_UPDATE, oldGuildScheduledEvent, newGuildScheduledEvent); return { oldGuildScheduledEvent, newGuildScheduledEvent }; } diff --git a/src/client/actions/GuildScheduledEventUserAdd.js b/src/client/actions/GuildScheduledEventUserAdd.js index 03520db..ea6e4c4 100644 --- a/src/client/actions/GuildScheduledEventUserAdd.js +++ b/src/client/actions/GuildScheduledEventUserAdd.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildScheduledEventUserAddAction extends Action { handle(data) { @@ -19,7 +19,7 @@ class GuildScheduledEventUserAddAction extends Action { * @param {GuildScheduledEvent} guildScheduledEvent The guild scheduled event * @param {User} user The user who subscribed */ - client.emit(Events.GuildScheduledEventUserAdd, guildScheduledEvent, user); + client.emit(Events.GUILD_SCHEDULED_EVENT_USER_ADD, guildScheduledEvent, user); return { guildScheduledEvent, user }; } diff --git a/src/client/actions/GuildScheduledEventUserRemove.js b/src/client/actions/GuildScheduledEventUserRemove.js index 2a04849..b32e3ce 100644 --- a/src/client/actions/GuildScheduledEventUserRemove.js +++ b/src/client/actions/GuildScheduledEventUserRemove.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildScheduledEventUserRemoveAction extends Action { handle(data) { @@ -19,7 +19,7 @@ class GuildScheduledEventUserRemoveAction extends Action { * @param {GuildScheduledEvent} guildScheduledEvent The guild scheduled event * @param {User} user The user who unsubscribed */ - client.emit(Events.GuildScheduledEventUserRemove, guildScheduledEvent, user); + client.emit(Events.GUILD_SCHEDULED_EVENT_USER_REMOVE, guildScheduledEvent, user); return { guildScheduledEvent, user }; } diff --git a/src/client/actions/GuildStickerCreate.js b/src/client/actions/GuildStickerCreate.js index 7d81de9..c02cafa 100644 --- a/src/client/actions/GuildStickerCreate.js +++ b/src/client/actions/GuildStickerCreate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildStickerCreateAction extends Action { handle(guild, createdSticker) { @@ -12,7 +12,7 @@ class GuildStickerCreateAction extends Action { * @event Client#stickerCreate * @param {Sticker} sticker The sticker that was created */ - if (!already) this.client.emit(Events.GuildStickerCreate, sticker); + if (!already) this.client.emit(Events.GUILD_STICKER_CREATE, sticker); return { sticker }; } } diff --git a/src/client/actions/GuildStickerDelete.js b/src/client/actions/GuildStickerDelete.js index 7fd6b57..4adfe52 100644 --- a/src/client/actions/GuildStickerDelete.js +++ b/src/client/actions/GuildStickerDelete.js @@ -1,17 +1,19 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { deletedStickers } = require('../../structures/Sticker'); +const { Events } = require('../../util/Constants'); class GuildStickerDeleteAction extends Action { handle(sticker) { sticker.guild.stickers.cache.delete(sticker.id); + deletedStickers.add(sticker); /** * Emitted whenever a custom sticker is deleted in a guild. * @event Client#stickerDelete * @param {Sticker} sticker The sticker that was deleted */ - this.client.emit(Events.GuildStickerDelete, sticker); + this.client.emit(Events.GUILD_STICKER_DELETE, sticker); return { sticker }; } } diff --git a/src/client/actions/GuildStickerUpdate.js b/src/client/actions/GuildStickerUpdate.js index 5561c7e..0c3edc7 100644 --- a/src/client/actions/GuildStickerUpdate.js +++ b/src/client/actions/GuildStickerUpdate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildStickerUpdateAction extends Action { handle(current, data) { @@ -12,7 +12,7 @@ class GuildStickerUpdateAction extends Action { * @param {Sticker} oldSticker The old sticker * @param {Sticker} newSticker The new sticker */ - this.client.emit(Events.GuildStickerUpdate, old, current); + this.client.emit(Events.GUILD_STICKER_UPDATE, old, current); return { sticker: current }; } } diff --git a/src/client/actions/GuildUpdate.js b/src/client/actions/GuildUpdate.js index ef1f51b..78a8afb 100644 --- a/src/client/actions/GuildUpdate.js +++ b/src/client/actions/GuildUpdate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class GuildUpdateAction extends Action { handle(data) { @@ -16,7 +16,7 @@ class GuildUpdateAction extends Action { * @param {Guild} oldGuild The guild before the update * @param {Guild} newGuild The guild after the update */ - client.emit(Events.GuildUpdate, old, guild); + client.emit(Events.GUILD_UPDATE, old, guild); return { old, updated: guild, diff --git a/src/client/actions/InteractionCreate.js b/src/client/actions/InteractionCreate.js index 8c36ec9..04774a6 100644 --- a/src/client/actions/InteractionCreate.js +++ b/src/client/actions/InteractionCreate.js @@ -1,14 +1,16 @@ 'use strict'; -const { InteractionType, ComponentType, ApplicationCommandType } = require('discord-api-types/v9'); +const process = require('node:process'); const Action = require('./Action'); const AutocompleteInteraction = require('../../structures/AutocompleteInteraction'); const ButtonInteraction = require('../../structures/ButtonInteraction'); -const ChatInputCommandInteraction = require('../../structures/ChatInputCommandInteraction'); -const MessageContextMenuCommandInteraction = require('../../structures/MessageContextMenuCommandInteraction'); +const CommandInteraction = require('../../structures/CommandInteraction'); +const MessageContextMenuInteraction = require('../../structures/MessageContextMenuInteraction'); const SelectMenuInteraction = require('../../structures/SelectMenuInteraction'); -const UserContextMenuCommandInteraction = require('../../structures/UserContextMenuCommandInteraction'); -const Events = require('../../util/Events'); +const UserContextMenuInteraction = require('../../structures/UserContextMenuInteraction'); +const { Events, InteractionTypes, MessageComponentTypes, ApplicationCommandTypes } = require('../../util/Constants'); + +let deprecationEmitted = false; class InteractionCreateAction extends Action { handle(data) { @@ -17,59 +19,70 @@ class InteractionCreateAction extends Action { // Resolve and cache partial channels for Interaction#channel getter this.getChannel(data); - let InteractionClass; + let InteractionType; switch (data.type) { - case InteractionType.ApplicationCommand: + case InteractionTypes.APPLICATION_COMMAND: switch (data.data.type) { - case ApplicationCommandType.ChatInput: - InteractionClass = ChatInputCommandInteraction; + case ApplicationCommandTypes.CHAT_INPUT: + InteractionType = CommandInteraction; break; - case ApplicationCommandType.User: - InteractionClass = UserContextMenuCommandInteraction; + case ApplicationCommandTypes.USER: + InteractionType = UserContextMenuInteraction; break; - case ApplicationCommandType.Message: - InteractionClass = MessageContextMenuCommandInteraction; + case ApplicationCommandTypes.MESSAGE: + InteractionType = MessageContextMenuInteraction; break; default: client.emit( - Events.Debug, + Events.DEBUG, `[INTERACTION] Received application command interaction with unknown type: ${data.data.type}`, ); return; } break; - case InteractionType.MessageComponent: + case InteractionTypes.MESSAGE_COMPONENT: switch (data.data.component_type) { - case ComponentType.Button: - InteractionClass = ButtonInteraction; + case MessageComponentTypes.BUTTON: + InteractionType = ButtonInteraction; break; - case ComponentType.SelectMenu: - InteractionClass = SelectMenuInteraction; + case MessageComponentTypes.SELECT_MENU: + InteractionType = SelectMenuInteraction; break; default: client.emit( - Events.Debug, + Events.DEBUG, `[INTERACTION] Received component interaction with unknown type: ${data.data.component_type}`, ); return; } break; - case InteractionType.ApplicationCommandAutocomplete: - InteractionClass = AutocompleteInteraction; + case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE: + InteractionType = AutocompleteInteraction; break; default: - client.emit(Events.Debug, `[INTERACTION] Received interaction with unknown type: ${data.type}`); + client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`); return; } - const interaction = new InteractionClass(client, data); + const interaction = new InteractionType(client, data); /** * Emitted when an interaction is created. * @event Client#interactionCreate * @param {Interaction} interaction The interaction which was created */ - client.emit(Events.InteractionCreate, interaction); + client.emit(Events.INTERACTION_CREATE, interaction); + + /** + * Emitted when an interaction is created. + * @event Client#interaction + * @param {Interaction} interaction The interaction which was created + * @deprecated Use {@link Client#event:interactionCreate} instead + */ + if (client.emit('interaction', interaction) && !deprecationEmitted) { + deprecationEmitted = true; + process.emitWarning('The interaction event is deprecated. Use interactionCreate instead', 'DeprecationWarning'); + } } } diff --git a/src/client/actions/InviteCreate.js b/src/client/actions/InviteCreate.js index 2dc3019..2f7c216 100644 --- a/src/client/actions/InviteCreate.js +++ b/src/client/actions/InviteCreate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class InviteCreateAction extends Action { handle(data) { @@ -20,7 +20,7 @@ class InviteCreateAction extends Action { * @event Client#inviteCreate * @param {Invite} invite The invite that was created */ - client.emit(Events.InviteCreate, invite); + client.emit(Events.INVITE_CREATE, invite); return { invite }; } } diff --git a/src/client/actions/InviteDelete.js b/src/client/actions/InviteDelete.js index 37b8143..47c77fa 100644 --- a/src/client/actions/InviteDelete.js +++ b/src/client/actions/InviteDelete.js @@ -2,7 +2,7 @@ const Action = require('./Action'); const Invite = require('../../structures/Invite'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class InviteDeleteAction extends Action { handle(data) { @@ -22,7 +22,7 @@ class InviteDeleteAction extends Action { * @event Client#inviteDelete * @param {Invite} invite The invite that was deleted */ - client.emit(Events.InviteDelete, invite); + client.emit(Events.INVITE_DELETE, invite); return { invite }; } } diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js index 9a099e2..894886e 100644 --- a/src/client/actions/MessageCreate.js +++ b/src/client/actions/MessageCreate.js @@ -1,14 +1,17 @@ 'use strict'; +const process = require('node:process'); const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); + +let deprecationEmitted = false; class MessageCreateAction extends Action { handle(data) { const client = this.client; const channel = this.getChannel(data); if (channel) { - if (!channel.isTextBased()) return {}; + if (!channel.isText()) return {}; const existing = channel.messages.cache.get(data.id); if (existing) return { message: existing }; @@ -20,7 +23,18 @@ class MessageCreateAction extends Action { * @event Client#messageCreate * @param {Message} message The created message */ - client.emit(Events.MessageCreate, message); + client.emit(Events.MESSAGE_CREATE, message); + + /** + * Emitted whenever a message is created. + * @event Client#message + * @param {Message} message The created message + * @deprecated Use {@link Client#event:messageCreate} instead + */ + if (client.emit('message', message) && !deprecationEmitted) { + deprecationEmitted = true; + process.emitWarning('The message event is deprecated. Use messageCreate instead', 'DeprecationWarning'); + } return { message }; } diff --git a/src/client/actions/MessageDelete.js b/src/client/actions/MessageDelete.js index cb55c67..22921d3 100644 --- a/src/client/actions/MessageDelete.js +++ b/src/client/actions/MessageDelete.js @@ -1,7 +1,8 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { deletedMessages } = require('../../structures/Message'); +const { Events } = require('../../util/Constants'); class MessageDeleteAction extends Action { handle(data) { @@ -9,17 +10,18 @@ class MessageDeleteAction extends Action { const channel = this.getChannel(data); let message; if (channel) { - if (!channel.isTextBased()) return {}; + if (!channel.isText()) return {}; message = this.getMessage(data, channel); if (message) { channel.messages.cache.delete(message.id); + deletedMessages.add(message); /** * Emitted whenever a message is deleted. * @event Client#messageDelete * @param {Message} message The deleted message */ - client.emit(Events.MessageDelete, message); + client.emit(Events.MESSAGE_DELETE, message); } } diff --git a/src/client/actions/MessageDeleteBulk.js b/src/client/actions/MessageDeleteBulk.js index 148e665..31a3717 100644 --- a/src/client/actions/MessageDeleteBulk.js +++ b/src/client/actions/MessageDeleteBulk.js @@ -2,7 +2,8 @@ const { Collection } = require('@discordjs/collection'); const Action = require('./Action'); -const Events = require('../../util/Events'); +const { deletedMessages } = require('../../structures/Message'); +const { Events } = require('../../util/Constants'); class MessageDeleteBulkAction extends Action { handle(data) { @@ -10,7 +11,7 @@ class MessageDeleteBulkAction extends Action { const channel = client.channels.cache.get(data.channel_id); if (channel) { - if (!channel.isTextBased()) return {}; + if (!channel.isText()) return {}; const ids = data.ids; const messages = new Collection(); @@ -24,6 +25,7 @@ class MessageDeleteBulkAction extends Action { false, ); if (message) { + deletedMessages.add(message); messages.set(message.id, message); channel.messages.cache.delete(id); } @@ -34,7 +36,7 @@ class MessageDeleteBulkAction extends Action { * @event Client#messageDeleteBulk * @param {Collection} messages The deleted messages, mapped by their id */ - if (messages.size > 0) client.emit(Events.MessageBulkDelete, messages); + if (messages.size > 0) client.emit(Events.MESSAGE_BULK_DELETE, messages); return { messages }; } return {}; diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index ea97bd6..736b1e4 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -1,8 +1,8 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); -const Partials = require('../../util/Partials'); +const { Events } = require('../../util/Constants'); +const { PartialTypes } = require('../../util/Constants'); /* { user_id: 'id', @@ -23,14 +23,14 @@ class MessageReactionAdd extends Action { // Verify channel const channel = this.getChannel(data); - if (!channel?.isTextBased()) return false; + if (!channel || !channel.isText()) return false; // Verify message const message = this.getMessage(data, channel); if (!message) return false; // Verify reaction - const includePartial = this.client.options.partials.includes(Partials.Reaction); + const includePartial = this.client.options.partials.includes(PartialTypes.REACTION); if (message.partial && !includePartial) return false; const reaction = message.reactions._add({ emoji: data.emoji, @@ -46,7 +46,7 @@ class MessageReactionAdd extends Action { * @param {MessageReaction} messageReaction The reaction object * @param {User} user The user that applied the guild or reaction emoji */ - this.client.emit(Events.MessageReactionAdd, reaction, user); + this.client.emit(Events.MESSAGE_REACTION_ADD, reaction, user); return { message, reaction, user }; } diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js index 9ca3a8e..7a05949 100644 --- a/src/client/actions/MessageReactionRemove.js +++ b/src/client/actions/MessageReactionRemove.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); /* { user_id: 'id', @@ -20,7 +20,7 @@ class MessageReactionRemove extends Action { // Verify channel const channel = this.getChannel(data); - if (!channel?.isTextBased()) return false; + if (!channel || !channel.isText()) return false; // Verify message const message = this.getMessage(data, channel); @@ -36,7 +36,7 @@ class MessageReactionRemove extends Action { * @param {MessageReaction} messageReaction The reaction object * @param {User} user The user whose emoji or reaction emoji was removed */ - this.client.emit(Events.MessageReactionRemove, reaction, user); + this.client.emit(Events.MESSAGE_REACTION_REMOVE, reaction, user); return { message, reaction, user }; } diff --git a/src/client/actions/MessageReactionRemoveAll.js b/src/client/actions/MessageReactionRemoveAll.js index b1c023f..e2a3924 100644 --- a/src/client/actions/MessageReactionRemoveAll.js +++ b/src/client/actions/MessageReactionRemoveAll.js @@ -1,13 +1,13 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class MessageReactionRemoveAll extends Action { handle(data) { // Verify channel const channel = this.getChannel(data); - if (!channel?.isTextBased()) return false; + if (!channel || !channel.isText()) return false; // Verify message const message = this.getMessage(data, channel); @@ -17,7 +17,7 @@ class MessageReactionRemoveAll extends Action { const removed = message.reactions.cache.clone(); message.reactions.cache.clear(); - this.client.emit(Events.MessageReactionRemoveAll, message, removed); + this.client.emit(Events.MESSAGE_REACTION_REMOVE_ALL, message, removed); return { message }; } diff --git a/src/client/actions/MessageReactionRemoveEmoji.js b/src/client/actions/MessageReactionRemoveEmoji.js index 3290214..f8f28e8 100644 --- a/src/client/actions/MessageReactionRemoveEmoji.js +++ b/src/client/actions/MessageReactionRemoveEmoji.js @@ -1,12 +1,12 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class MessageReactionRemoveEmoji extends Action { handle(data) { const channel = this.getChannel(data); - if (!channel?.isTextBased()) return false; + if (!channel || !channel.isText()) return false; const message = this.getMessage(data, channel); if (!message) return false; @@ -20,7 +20,7 @@ class MessageReactionRemoveEmoji extends Action { * @event Client#messageReactionRemoveEmoji * @param {MessageReaction} reaction The reaction that was removed */ - this.client.emit(Events.MessageReactionRemoveEmoji, reaction); + this.client.emit(Events.MESSAGE_REACTION_REMOVE_EMOJI, reaction); return { reaction }; } } diff --git a/src/client/actions/MessageUpdate.js b/src/client/actions/MessageUpdate.js index fe757c0..9448ed6 100644 --- a/src/client/actions/MessageUpdate.js +++ b/src/client/actions/MessageUpdate.js @@ -6,7 +6,7 @@ class MessageUpdateAction extends Action { handle(data) { const channel = this.getChannel(data); if (channel) { - if (!channel.isTextBased()) return {}; + if (!channel.isText()) return {}; const { id, channel_id, guild_id, author, timestamp, type } = data; const message = this.getMessage({ id, channel_id, guild_id, author, timestamp, type }, channel); diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js index 0b4aaab..a09688b 100644 --- a/src/client/actions/PresenceUpdate.js +++ b/src/client/actions/PresenceUpdate.js @@ -1,15 +1,15 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class PresenceUpdateAction extends Action { handle(data) { let user = this.client.users.cache.get(data.user.id); - if (!user && data.user.username) user = this.client.users._add(data.user); + if (!user && data.user?.username) user = this.client.users._add(data.user); if (!user) return; - if (data.user.username) { + if (data.user?.username) { if (!user._equals(data.user)) this.client.actions.UserUpdate.handle(data.user); } @@ -24,17 +24,17 @@ class PresenceUpdateAction extends Action { deaf: false, mute: false, }); - this.client.emit(Events.GuildMemberAvailable, member); + this.client.emit(Events.GUILD_MEMBER_AVAILABLE, member); } const newPresence = guild.presences._add(Object.assign(data, { guild })); - if (this.client.listenerCount(Events.PresenceUpdate) && !newPresence.equals(oldPresence)) { + if (this.client.listenerCount(Events.PRESENCE_UPDATE) && !newPresence.equals(oldPresence)) { /** * Emitted whenever a guild member's presence (e.g. status, activity) is changed. * @event Client#presenceUpdate * @param {?Presence} oldPresence The presence before the update, if one at all * @param {Presence} newPresence The presence after the update */ - this.client.emit(Events.PresenceUpdate, oldPresence, newPresence); + this.client.emit(Events.PRESENCE_UPDATE, oldPresence, newPresence); } } } diff --git a/src/client/actions/StageInstanceCreate.js b/src/client/actions/StageInstanceCreate.js index 4edd530..bff4591 100644 --- a/src/client/actions/StageInstanceCreate.js +++ b/src/client/actions/StageInstanceCreate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class StageInstanceCreateAction extends Action { handle(data) { @@ -16,7 +16,7 @@ class StageInstanceCreateAction extends Action { * @event Client#stageInstanceCreate * @param {StageInstance} stageInstance The created stage instance */ - client.emit(Events.StageInstanceCreate, stageInstance); + client.emit(Events.STAGE_INSTANCE_CREATE, stageInstance); return { stageInstance }; } diff --git a/src/client/actions/StageInstanceDelete.js b/src/client/actions/StageInstanceDelete.js index 0d5da38..a227a9c 100644 --- a/src/client/actions/StageInstanceDelete.js +++ b/src/client/actions/StageInstanceDelete.js @@ -1,7 +1,8 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { deletedStageInstances } = require('../../structures/StageInstance'); +const { Events } = require('../../util/Constants'); class StageInstanceDeleteAction extends Action { handle(data) { @@ -12,13 +13,14 @@ class StageInstanceDeleteAction extends Action { const stageInstance = channel.guild.stageInstances._add(data); if (stageInstance) { channel.guild.stageInstances.cache.delete(stageInstance.id); + deletedStageInstances.add(stageInstance); /** * Emitted whenever a stage instance is deleted. * @event Client#stageInstanceDelete * @param {StageInstance} stageInstance The deleted stage instance */ - client.emit(Events.StageInstanceDelete, stageInstance); + client.emit(Events.STAGE_INSTANCE_DELETE, stageInstance); return { stageInstance }; } diff --git a/src/client/actions/StageInstanceUpdate.js b/src/client/actions/StageInstanceUpdate.js index 008a53c..17d74f9 100644 --- a/src/client/actions/StageInstanceUpdate.js +++ b/src/client/actions/StageInstanceUpdate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class StageInstanceUpdateAction extends Action { handle(data) { @@ -18,7 +18,7 @@ class StageInstanceUpdateAction extends Action { * @param {?StageInstance} oldStageInstance The stage instance before the update * @param {StageInstance} newStageInstance The stage instance after the update */ - client.emit(Events.StageInstanceUpdate, oldStageInstance, newStageInstance); + client.emit(Events.STAGE_INSTANCE_UPDATE, oldStageInstance, newStageInstance); return { oldStageInstance, newStageInstance }; } diff --git a/src/client/actions/ThreadCreate.js b/src/client/actions/ThreadCreate.js index a8ff6c6..f7c36e6 100644 --- a/src/client/actions/ThreadCreate.js +++ b/src/client/actions/ThreadCreate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class ThreadCreateAction extends Action { handle(data) { @@ -13,9 +13,8 @@ class ThreadCreateAction extends Action { * Emitted whenever a thread is created or when the client user is added to a thread. * @event Client#threadCreate * @param {ThreadChannel} thread The thread that was created - * @param {boolean} newlyCreated Whether the thread was newly created */ - client.emit(Events.ThreadCreate, thread, data.newly_created ?? false); + client.emit(Events.THREAD_CREATE, thread); } return { thread }; } diff --git a/src/client/actions/ThreadDelete.js b/src/client/actions/ThreadDelete.js index 3ec81a4..9307dd2 100644 --- a/src/client/actions/ThreadDelete.js +++ b/src/client/actions/ThreadDelete.js @@ -1,7 +1,9 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { deletedChannels } = require('../../structures/Channel'); +const { deletedMessages } = require('../../structures/Message'); +const { Events } = require('../../util/Constants'); class ThreadDeleteAction extends Action { handle(data) { @@ -10,13 +12,17 @@ class ThreadDeleteAction extends Action { if (thread) { client.channels._remove(thread.id); + deletedChannels.add(thread); + for (const message of thread.messages.cache.values()) { + deletedMessages.add(message); + } /** * Emitted whenever a thread is deleted. * @event Client#threadDelete * @param {ThreadChannel} thread The thread that was deleted */ - client.emit(Events.ThreadDelete, thread); + client.emit(Events.THREAD_DELETE, thread); } return { thread }; diff --git a/src/client/actions/ThreadListSync.js b/src/client/actions/ThreadListSync.js index bad1a47..1ffa7d5 100644 --- a/src/client/actions/ThreadListSync.js +++ b/src/client/actions/ThreadListSync.js @@ -2,7 +2,7 @@ const { Collection } = require('@discordjs/collection'); const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class ThreadListSyncAction extends Action { handle(data) { @@ -40,7 +40,7 @@ class ThreadListSyncAction extends Action { * @event Client#threadListSync * @param {Collection} threads The threads that were synced */ - client.emit(Events.ThreadListSync, syncedThreads); + client.emit(Events.THREAD_LIST_SYNC, syncedThreads); return { syncedThreads, diff --git a/src/client/actions/ThreadMemberUpdate.js b/src/client/actions/ThreadMemberUpdate.js index 0b17f70..b84bcac 100644 --- a/src/client/actions/ThreadMemberUpdate.js +++ b/src/client/actions/ThreadMemberUpdate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class ThreadMemberUpdateAction extends Action { handle(data) { @@ -21,7 +21,7 @@ class ThreadMemberUpdateAction extends Action { * @param {ThreadMember} oldMember The member before the update * @param {ThreadMember} newMember The member after the update */ - client.emit(Events.ThreadMemberUpdate, old, member); + client.emit(Events.THREAD_MEMBER_UPDATE, old, member); } return {}; } diff --git a/src/client/actions/ThreadMembersUpdate.js b/src/client/actions/ThreadMembersUpdate.js index 26ab70e..c9833d6 100644 --- a/src/client/actions/ThreadMembersUpdate.js +++ b/src/client/actions/ThreadMembersUpdate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class ThreadMembersUpdateAction extends Action { handle(data) { @@ -25,7 +25,7 @@ class ThreadMembersUpdateAction extends Action { * @param {Collection} oldMembers The members before the update * @param {Collection} newMembers The members after the update */ - client.emit(Events.ThreadMembersUpdate, old, thread.members.cache); + client.emit(Events.THREAD_MEMBERS_UPDATE, old, thread.members.cache); } return {}; } diff --git a/src/client/actions/TypingStart.js b/src/client/actions/TypingStart.js index 4e79920..9d0ae37 100644 --- a/src/client/actions/TypingStart.js +++ b/src/client/actions/TypingStart.js @@ -2,15 +2,15 @@ const Action = require('./Action'); const Typing = require('../../structures/Typing'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class TypingStart extends Action { handle(data) { const channel = this.getChannel(data); if (!channel) return; - if (!channel.isTextBased()) { - this.client.emit(Events.Warn, `Discord sent a typing packet to a ${channel.type} channel ${channel.id}`); + if (!channel.isText()) { + this.client.emit(Events.WARN, `Discord sent a typing packet to a ${channel.type} channel ${channel.id}`); return; } @@ -21,7 +21,7 @@ class TypingStart extends Action { * @event Client#typingStart * @param {Typing} typing The typing state */ - this.client.emit(Events.TypingStart, new Typing(channel, user, data)); + this.client.emit(Events.TYPING_START, new Typing(channel, user, data)); } } } diff --git a/src/client/actions/UserUpdate.js b/src/client/actions/UserUpdate.js index 1bf236a..df992f7 100644 --- a/src/client/actions/UserUpdate.js +++ b/src/client/actions/UserUpdate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class UserUpdateAction extends Action { handle(data) { @@ -18,7 +18,7 @@ class UserUpdateAction extends Action { * @param {User} oldUser The user before the update * @param {User} newUser The user after the update */ - client.emit(Events.UserUpdate, oldUser, newUser); + client.emit(Events.USER_UPDATE, oldUser, newUser); return { old: oldUser, updated: newUser, diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index fc7400f..f750e04 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -2,7 +2,7 @@ const Action = require('./Action'); const VoiceState = require('../../structures/VoiceState'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class VoiceStateUpdate extends Action { handle(data) { @@ -35,7 +35,7 @@ class VoiceStateUpdate extends Action { * @param {VoiceState} oldState The voice state before the update * @param {VoiceState} newState The voice state after the update */ - client.emit(Events.VoiceStateUpdate, oldState, newState); + client.emit(Events.VOICE_STATE_UPDATE, oldState, newState); } } } diff --git a/src/client/actions/WebhooksUpdate.js b/src/client/actions/WebhooksUpdate.js index b362b3b..7efccd7 100644 --- a/src/client/actions/WebhooksUpdate.js +++ b/src/client/actions/WebhooksUpdate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); class WebhooksUpdate extends Action { handle(data) { @@ -12,7 +12,7 @@ class WebhooksUpdate extends Action { * @event Client#webhookUpdate * @param {TextChannel|NewsChannel} channel The channel that had a webhook update */ - if (channel) client.emit(Events.WebhooksUpdate, channel); + if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel); } } diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 192e700..e6ea6ea 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -1,6 +1,6 @@ 'use strict'; -const Events = require('../../util/Events'); +const { Events } = require('../../util/Constants'); /** * Manages voice connections for the client @@ -21,7 +21,7 @@ class ClientVoiceManager { */ this.adapters = new Map(); - client.on(Events.ShardDisconnect, (_, shardId) => { + client.on(Events.SHARD_DISCONNECT, (_, shardId) => { for (const [guildId, adapter] of this.adapters.entries()) { if (client.guilds.cache.get(guildId)?.shardId === shardId) { adapter.destroy(); diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index a08b88a..65a23f9 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -4,32 +4,28 @@ const EventEmitter = require('node:events'); const { setImmediate } = require('node:timers'); const { setTimeout: sleep } = require('node:timers/promises'); const { Collection } = require('@discordjs/collection'); -const { GatewayCloseCodes, GatewayDispatchEvents, Routes } = require('discord-api-types/v9'); +const { RPCErrorCodes } = require('discord-api-types/v9'); const WebSocketShard = require('./WebSocketShard'); const PacketHandlers = require('./handlers'); const { Error } = require('../../errors'); -const Events = require('../../util/Events'); -const ShardEvents = require('../../util/ShardEvents'); -const Status = require('../../util/Status'); +const { Events, ShardEvents, Status, WSCodes, WSEvents } = require('../../util/Constants'); const BeforeReadyWhitelist = [ - GatewayDispatchEvents.Ready, - GatewayDispatchEvents.Resumed, - GatewayDispatchEvents.GuildCreate, - GatewayDispatchEvents.GuildDelete, - GatewayDispatchEvents.GuildMembersChunk, - GatewayDispatchEvents.GuildMemberAdd, - GatewayDispatchEvents.GuildMemberRemove, + WSEvents.READY, + WSEvents.RESUMED, + WSEvents.GUILD_CREATE, + WSEvents.GUILD_DELETE, + WSEvents.GUILD_MEMBERS_CHUNK, + WSEvents.GUILD_MEMBER_ADD, + WSEvents.GUILD_MEMBER_REMOVE, ]; -const UNRECOVERABLE_CLOSE_CODES = [ - GatewayCloseCodes.AuthenticationFailed, - GatewayCloseCodes.InvalidShard, - GatewayCloseCodes.ShardingRequired, - GatewayCloseCodes.InvalidIntents, - GatewayCloseCodes.DisallowedIntents, +const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes).slice(1).map(Number); +const UNRESUMABLE_CLOSE_CODES = [ + RPCErrorCodes.UnknownError, + RPCErrorCodes.InvalidPermissions, + RPCErrorCodes.InvalidClientId, ]; -const UNRESUMABLE_CLOSE_CODES = [1000, GatewayCloseCodes.AlreadyAuthenticated, GatewayCloseCodes.InvalidSeq]; /** * The WebSocket manager for this client. @@ -88,7 +84,7 @@ class WebSocketManager extends EventEmitter { * The current status of this WebSocketManager * @type {Status} */ - this.status = Status.Idle; + this.status = Status.IDLE; /** * If this manager was destroyed. It will prevent shards from reconnecting @@ -122,7 +118,7 @@ class WebSocketManager extends EventEmitter { * @private */ debug(message, shard) { - this.client.emit(Events.Debug, `[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`); + this.client.emit(Events.DEBUG, `[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`); } /** @@ -130,7 +126,7 @@ class WebSocketManager extends EventEmitter { * @private */ async connect() { - const invalidToken = new Error(GatewayCloseCodes[GatewayCloseCodes.AuthenticationFailed]); + const invalidToken = new Error(WSCodes[4004]); const { url: gatewayURL, shards: recommendedShards, @@ -180,20 +176,20 @@ class WebSocketManager extends EventEmitter { this.shardQueue.delete(shard); if (!shard.eventsAttached) { - shard.on(ShardEvents.AllReady, unavailableGuilds => { + shard.on(ShardEvents.ALL_READY, unavailableGuilds => { /** * Emitted when a shard turns ready. * @event Client#shardReady * @param {number} id The shard id that turned ready * @param {?Set} unavailableGuilds Set of unavailable guild ids, if any */ - this.client.emit(Events.ShardReady, shard.id, unavailableGuilds); + this.client.emit(Events.SHARD_READY, shard.id, unavailableGuilds); if (!this.shardQueue.size) this.reconnecting = false; this.checkShardsReady(); }); - shard.on(ShardEvents.Close, event => { + shard.on(ShardEvents.CLOSE, event => { if (event.code === 1_000 ? this.destroyed : UNRECOVERABLE_CLOSE_CODES.includes(event.code)) { /** * Emitted when a shard's WebSocket disconnects and will no longer reconnect. @@ -201,8 +197,8 @@ class WebSocketManager extends EventEmitter { * @param {CloseEvent} event The WebSocket close event * @param {number} id The shard id that disconnected */ - this.client.emit(Events.ShardDisconnect, event, shard.id); - this.debug(GatewayCloseCodes[event.code], shard); + this.client.emit(Events.SHARD_DISCONNECT, event, shard.id); + this.debug(WSCodes[event.code], shard); return; } @@ -216,7 +212,7 @@ class WebSocketManager extends EventEmitter { * @event Client#shardReconnecting * @param {number} id The shard id that is attempting to reconnect */ - this.client.emit(Events.ShardReconnecting, shard.id); + this.client.emit(Events.SHARD_RECONNECTING, shard.id); this.shardQueue.add(shard); @@ -229,14 +225,14 @@ class WebSocketManager extends EventEmitter { } }); - shard.on(ShardEvents.InvalidSession, () => { - this.client.emit(Events.ShardReconnecting, shard.id); + shard.on(ShardEvents.INVALID_SESSION, () => { + this.client.emit(Events.SHARD_RECONNECTING, shard.id); }); - shard.on(ShardEvents.Destroyed, () => { + shard.on(ShardEvents.DESTROYED, () => { this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard); - this.client.emit(Events.ShardReconnecting, shard.id); + this.client.emit(Events.SHARD_RECONNECTING, shard.id); this.shardQueue.add(shard); this.reconnect(); @@ -251,7 +247,7 @@ class WebSocketManager extends EventEmitter { await shard.connect(); } catch (error) { if (error?.code && UNRECOVERABLE_CLOSE_CODES.includes(error.code)) { - throw new Error(GatewayCloseCodes[error.code]); + throw new Error(WSCodes[error.code]); // Undefined if session is invalid, error event for regular closes } else if (!error || error.code) { this.debug('Failed to connect to the gateway, requeueing...', shard); @@ -276,7 +272,7 @@ class WebSocketManager extends EventEmitter { * @returns {Promise} */ async reconnect() { - if (this.reconnecting || this.status !== Status.Ready) return false; + if (this.reconnecting || this.status !== Status.READY) return false; this.reconnecting = true; try { await this.createShards(); @@ -289,14 +285,14 @@ class WebSocketManager extends EventEmitter { return this.reconnect(); } // If we get an error at this point, it means we cannot reconnect anymore - if (this.client.listenerCount(Events.Invalidated)) { + if (this.client.listenerCount(Events.INVALIDATED)) { /** * Emitted when the client's session becomes invalidated. * You are expected to handle closing the process gracefully and preventing a boot loop * if you are listening to this event. * @event Client#invalidated */ - this.client.emit(Events.Invalidated); + this.client.emit(Events.INVALIDATED); // Destroy just the shards. This means you have to handle the cleanup yourself this.destroy(); } else { @@ -337,7 +333,7 @@ class WebSocketManager extends EventEmitter { * @private */ handlePacket(packet, shard) { - if (packet && this.status !== Status.Ready) { + if (packet && this.status !== Status.READY) { if (!BeforeReadyWhitelist.includes(packet.t)) { this.packetQueue.push({ packet, shard }); return false; @@ -363,8 +359,8 @@ class WebSocketManager extends EventEmitter { * @private */ checkShardsReady() { - if (this.status === Status.Ready) return; - if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.Ready)) { + if (this.status === Status.READY) return; + if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.READY)) { return; } @@ -376,16 +372,16 @@ class WebSocketManager extends EventEmitter { * @private */ triggerClientReady() { - this.status = Status.Ready; + this.status = Status.READY; - this.client.readyTimestamp = Date.now(); + this.client.readyAt = new Date(); /** * Emitted when the client becomes ready to start working. * @event Client#ready * @param {Client} client The client */ - this.client.emit(Events.ClientReady, this.client); + this.client.emit(Events.CLIENT_READY, this.client); this.handlePacket(); } diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 34f34f7..35174f6 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -1,13 +1,10 @@ 'use strict'; const EventEmitter = require('node:events'); -const { setTimeout, setInterval, clearTimeout, clearInterval } = require('node:timers'); -const { GatewayDispatchEvents, GatewayIntentBits, GatewayOpcodes } = require('discord-api-types/v9'); +const { setTimeout, setInterval } = require('node:timers'); const WebSocket = require('../../WebSocket'); -const Events = require('../../util/Events'); -const IntentsBitField = require('../../util/IntentsBitField'); -const ShardEvents = require('../../util/ShardEvents'); -const Status = require('../../util/Status'); +const { Status, Events, ShardEvents, Opcodes, WSEvents } = require('../../util/Constants'); +const Intents = require('../../util/Intents'); const STATUS_KEYS = Object.keys(Status); const CONNECTION_STATE = Object.keys(WebSocket.WebSocket); @@ -41,7 +38,7 @@ class WebSocketShard extends EventEmitter { * The current status of the shard * @type {Status} */ - this.status = Status.Idle; + this.status = Status.IDLE; /** * The current sequence of the shard @@ -180,17 +177,17 @@ class WebSocketShard extends EventEmitter { connect() { const { gateway, client } = this.manager; - if (this.connection?.readyState === WebSocket.OPEN && this.status === Status.Ready) { + if (this.connection?.readyState === WebSocket.OPEN && this.status === Status.READY) { return Promise.resolve(); } return new Promise((resolve, reject) => { const cleanup = () => { - this.removeListener(ShardEvents.Close, onClose); - this.removeListener(ShardEvents.Ready, onReady); - this.removeListener(ShardEvents.Resumed, onResumed); - this.removeListener(ShardEvents.InvalidSession, onInvalidOrDestroyed); - this.removeListener(ShardEvents.Destroyed, onInvalidOrDestroyed); + this.removeListener(ShardEvents.CLOSE, onClose); + this.removeListener(ShardEvents.READY, onReady); + this.removeListener(ShardEvents.RESUMED, onResumed); + this.removeListener(ShardEvents.INVALID_SESSION, onInvalidOrDestroyed); + this.removeListener(ShardEvents.DESTROYED, onInvalidOrDestroyed); }; const onReady = () => { @@ -214,11 +211,11 @@ class WebSocketShard extends EventEmitter { reject(); }; - this.once(ShardEvents.Ready, onReady); - this.once(ShardEvents.Resumed, onResumed); - this.once(ShardEvents.Close, onClose); - this.once(ShardEvents.InvalidSession, onInvalidOrDestroyed); - this.once(ShardEvents.Destroyed, onInvalidOrDestroyed); + this.once(ShardEvents.READY, onReady); + this.once(ShardEvents.RESUMED, onResumed); + this.once(ShardEvents.CLOSE, onClose); + this.once(ShardEvents.INVALID_SESSION, onInvalidOrDestroyed); + this.once(ShardEvents.DESTROYED, onInvalidOrDestroyed); if (this.connection?.readyState === WebSocket.OPEN) { this.debug('An open connection was found, attempting an immediate identify.'); @@ -251,7 +248,7 @@ class WebSocketShard extends EventEmitter { Compression: ${zlib ? 'zlib-stream' : 'none'}`, ); - this.status = this.status === Status.Disconnected ? Status.Reconnecting : Status.Connecting; + this.status = this.status === Status.DISCONNECTED ? Status.RECONNECTING : Status.CONNECTING; this.setHelloTimeout(); this.connectedAt = Date.now(); @@ -270,7 +267,7 @@ class WebSocketShard extends EventEmitter { */ onOpen() { this.debug(`[CONNECTED] Took ${Date.now() - this.connectedAt}ms`); - this.status = Status.Nearly; + this.status = Status.NEARLY; } /** @@ -296,11 +293,11 @@ class WebSocketShard extends EventEmitter { try { packet = WebSocket.unpack(raw); } catch (err) { - this.manager.client.emit(Events.ShardError, err, this.id); + this.manager.client.emit(Events.SHARD_ERROR, err, this.id); return; } - this.manager.client.emit(Events.Raw, packet, this.id); - if (packet.op === GatewayOpcodes.Dispatch) this.manager.emit(packet.t, packet.d, this.id); + this.manager.client.emit(Events.RAW, packet, this.id); + if (packet.op === Opcodes.DISPATCH) this.manager.emit(packet.t, packet.d, this.id); this.onPacket(packet); } @@ -319,7 +316,7 @@ class WebSocketShard extends EventEmitter { * @param {Error} error The encountered error * @param {number} shardId The shard that encountered this error */ - this.manager.client.emit(Events.ShardError, error, this.id); + this.manager.client.emit(Events.SHARD_ERROR, error, this.id); } /** @@ -356,7 +353,7 @@ class WebSocketShard extends EventEmitter { // If we still have a connection object, clean up its listeners if (this.connection) this._cleanupConnection(); - this.status = Status.Disconnected; + this.status = Status.DISCONNECTED; /** * Emitted when a shard's WebSocket closes. @@ -364,7 +361,7 @@ class WebSocketShard extends EventEmitter { * @event WebSocketShard#close * @param {CloseEvent} event The received event */ - this.emit(ShardEvents.Close, event); + this.emit(ShardEvents.CLOSE, event); } /** @@ -379,28 +376,28 @@ class WebSocketShard extends EventEmitter { } switch (packet.t) { - case GatewayDispatchEvents.Ready: + case WSEvents.READY: /** * Emitted when the shard receives the READY payload and is now waiting for guilds * @event WebSocketShard#ready */ - this.emit(ShardEvents.Ready); + this.emit(ShardEvents.READY); this.sessionId = packet.d.session_id; this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id)); - this.status = Status.WaitingForGuilds; + this.status = Status.WAITING_FOR_GUILDS; this.debug(`[READY] Session ${this.sessionId}.`); this.lastHeartbeatAcked = true; this.sendHeartbeat('ReadyHeartbeat'); break; - case GatewayDispatchEvents.Resumed: { + case WSEvents.RESUMED: { /** * Emitted when the shard resumes successfully * @event WebSocketShard#resumed */ - this.emit(ShardEvents.Resumed); + this.emit(ShardEvents.RESUMED); - this.status = Status.Ready; + this.status = Status.READY; const replayed = packet.s - this.closeSequence; this.debug(`[RESUMED] Session ${this.sessionId} | Replayed ${replayed} events.`); this.lastHeartbeatAcked = true; @@ -412,16 +409,16 @@ class WebSocketShard extends EventEmitter { if (packet.s > this.sequence) this.sequence = packet.s; switch (packet.op) { - case GatewayOpcodes.Hello: + case Opcodes.HELLO: this.setHelloTimeout(-1); this.setHeartbeatTimer(packet.d.heartbeat_interval); this.identify(); break; - case GatewayOpcodes.Reconnect: + case Opcodes.RECONNECT: this.debug('[RECONNECT] Discord asked us to reconnect'); this.destroy({ closeCode: 4_000 }); break; - case GatewayOpcodes.InvalidSession: + case Opcodes.INVALID_SESSION: this.debug(`[INVALID SESSION] Resumable: ${packet.d}.`); // If we can resume the session, do so immediately if (packet.d) { @@ -433,19 +430,19 @@ class WebSocketShard extends EventEmitter { // Reset the session id as it's invalid this.sessionId = null; // Set the status to reconnecting - this.status = Status.Reconnecting; + this.status = Status.RECONNECTING; // Finally, emit the INVALID_SESSION event - this.emit(ShardEvents.InvalidSession); + this.emit(ShardEvents.INVALID_SESSION); break; - case GatewayOpcodes.HeartbeatAck: + case Opcodes.HEARTBEAT_ACK: this.ackHeartbeat(); break; - case GatewayOpcodes.Heartbeat: + case Opcodes.HEARTBEAT: this.sendHeartbeat('HeartbeatRequest', true); break; default: this.manager.handlePacket(packet, this); - if (this.status === Status.WaitingForGuilds && packet.t === GatewayDispatchEvents.GuildCreate) { + if (this.status === Status.WAITING_FOR_GUILDS && packet.t === WSEvents.GUILD_CREATE) { this.expectedGuilds.delete(packet.d.id); this.checkReady(); } @@ -465,7 +462,7 @@ class WebSocketShard extends EventEmitter { // Step 1. If we don't have any other guilds pending, we are ready if (!this.expectedGuilds.size) { this.debug('Shard received all its guilds. Marking as fully ready.'); - this.status = Status.Ready; + this.status = Status.READY; /** * Emitted when the shard is fully ready. @@ -475,10 +472,10 @@ class WebSocketShard extends EventEmitter { * @event WebSocketShard#allReady * @param {?Set} unavailableGuilds Set of unavailable guilds, if any */ - this.emit(ShardEvents.AllReady); + this.emit(ShardEvents.ALL_READY); return; } - const hasGuildsIntent = new IntentsBitField(this.manager.client.options.intents).has(GatewayIntentBits.Guilds); + const hasGuildsIntent = new Intents(this.manager.client.options.intents).has(Intents.FLAGS.GUILDS); // Step 2. Create a timeout that will mark the shard as ready if there are still unavailable guilds // * The timeout is 15 seconds by default // * This can be optionally changed in the client options via the `waitGuildTimeout` option @@ -497,11 +494,11 @@ class WebSocketShard extends EventEmitter { this.readyTimeout = null; - this.status = Status.Ready; + this.status = Status.READY; - this.emit(ShardEvents.AllReady, this.expectedGuilds); + this.emit(ShardEvents.ALL_READY, this.expectedGuilds); }, - 0, + hasGuildsIntent ? waitGuildTimeout : 0, ).unref(); } @@ -555,7 +552,7 @@ class WebSocketShard extends EventEmitter { */ sendHeartbeat( tag = 'HeartbeatTimer', - ignoreHeartbeatAck = [Status.WaitingForGuilds, Status.Identifying, Status.Resuming].includes(this.status), + ignoreHeartbeatAck = [Status.WAITING_FOR_GUILDS, Status.IDENTIFYING, Status.RESUMING].includes(this.status), ) { if (ignoreHeartbeatAck && !this.lastHeartbeatAcked) { this.debug(`[${tag}] Didn't process heartbeat ack yet but we are still connected. Sending one now.`); @@ -574,7 +571,7 @@ class WebSocketShard extends EventEmitter { this.debug(`[${tag}] Sending a heartbeat.`); this.lastHeartbeatAcked = false; this.lastPingTimestamp = Date.now(); - this.send({ op: GatewayOpcodes.Heartbeat, d: this.sequence }, true); + this.send({ op: Opcodes.HEARTBEAT, d: this.sequence }, true); } /** @@ -608,16 +605,18 @@ class WebSocketShard extends EventEmitter { return; } - this.status = Status.Identifying; + this.status = Status.IDENTIFYING; // Clone the identify payload and assign the token and shard info const d = { ...client.options.ws, + intents: Intents.resolve(client.options.intents), token: client.token, + shard: [this.id, Number(client.options.shardCount)], }; - this.debug(`[IDENTIFY] Shard ${this.id}/${client.options.shardCount} with intents: 32767`); - this.send({ op: GatewayOpcodes.Identify, d }, true); + this.debug(`[IDENTIFY] Shard ${this.id}/${client.options.shardCount} with intents: ${d.intents}`); + this.send({ op: Opcodes.IDENTIFY, d }, true); } /** @@ -631,7 +630,7 @@ class WebSocketShard extends EventEmitter { return; } - this.status = Status.Resuming; + this.status = Status.RESUMING; this.debug(`[RESUME] Session ${this.sessionId}, sequence ${this.closeSequence}`); @@ -641,7 +640,7 @@ class WebSocketShard extends EventEmitter { seq: this.closeSequence, }; - this.send({ op: GatewayOpcodes.Resume, d }, true); + this.send({ op: Opcodes.RESUME, d }, true); } /** @@ -671,7 +670,7 @@ class WebSocketShard extends EventEmitter { } this.connection.send(WebSocket.pack(data), err => { - if (err) this.manager.client.emit(Events.ShardError, err, this.id); + if (err) this.manager.client.emit(Events.SHARD_ERROR, err, this.id); }); } @@ -741,8 +740,8 @@ class WebSocketShard extends EventEmitter { // Step 2: Null the connection object this.connection = null; - // Step 3: Set the shard status to Disconnected - this.status = Status.Disconnected; + // Step 3: Set the shard status to DISCONNECTED + this.status = Status.DISCONNECTED; // Step 4: Cache the old sequence (use to attempt a resume) if (this.sequence !== -1) this.closeSequence = this.sequence; @@ -780,7 +779,7 @@ class WebSocketShard extends EventEmitter { * @private * @event WebSocketShard#destroyed */ - this.emit(ShardEvents.Destroyed); + this.emit(ShardEvents.DESTROYED); } } diff --git a/src/client/websocket/handlers/APPLICATION_COMMAND_CREATE.js b/src/client/websocket/handlers/APPLICATION_COMMAND_CREATE.js new file mode 100644 index 00000000..17af7d5 --- /dev/null +++ b/src/client/websocket/handlers/APPLICATION_COMMAND_CREATE.js @@ -0,0 +1,18 @@ +'use strict'; + +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const commandManager = data.guild_id ? client.guilds.cache.get(data.guild_id)?.commands : client.application.commands; + if (!commandManager) return; + + const command = commandManager._add(data, data.application_id === client.application.id); + + /** + * Emitted when a guild application command is created. + * @event Client#applicationCommandCreate + * @param {ApplicationCommand} command The command which was created + * @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information. + */ + client.emit(Events.APPLICATION_COMMAND_CREATE, command); +}; diff --git a/src/client/websocket/handlers/APPLICATION_COMMAND_DELETE.js b/src/client/websocket/handlers/APPLICATION_COMMAND_DELETE.js new file mode 100644 index 00000000..4b09274 --- /dev/null +++ b/src/client/websocket/handlers/APPLICATION_COMMAND_DELETE.js @@ -0,0 +1,20 @@ +'use strict'; + +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const commandManager = data.guild_id ? client.guilds.cache.get(data.guild_id)?.commands : client.application.commands; + if (!commandManager) return; + + const isOwn = data.application_id === client.application.id; + const command = commandManager._add(data, isOwn); + if (isOwn) commandManager.cache.delete(data.id); + + /** + * Emitted when a guild application command is deleted. + * @event Client#applicationCommandDelete + * @param {ApplicationCommand} command The command which was deleted + * @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information. + */ + client.emit(Events.APPLICATION_COMMAND_DELETE, command); +}; diff --git a/src/client/websocket/handlers/APPLICATION_COMMAND_UPDATE.js b/src/client/websocket/handlers/APPLICATION_COMMAND_UPDATE.js new file mode 100644 index 00000000..730d7f1e --- /dev/null +++ b/src/client/websocket/handlers/APPLICATION_COMMAND_UPDATE.js @@ -0,0 +1,20 @@ +'use strict'; + +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const commandManager = data.guild_id ? client.guilds.cache.get(data.guild_id)?.commands : client.application.commands; + if (!commandManager) return; + + const oldCommand = commandManager.cache.get(data.id)?._clone() ?? null; + const newCommand = commandManager._add(data, data.application_id === client.application.id); + + /** + * Emitted when a guild application command is updated. + * @event Client#applicationCommandUpdate + * @param {?ApplicationCommand} oldCommand The command before the update + * @param {ApplicationCommand} newCommand The command after the update + * @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information. + */ + client.emit(Events.APPLICATION_COMMAND_UPDATE, oldCommand, newCommand); +}; diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js index c46e527..b49e311 100644 --- a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -1,10 +1,10 @@ 'use strict'; -const Events = require('../../../util/Events'); +const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { const channel = client.channels.cache.get(data.channel_id); - const time = data.last_pin_timestamp ? Date.parse(data.last_pin_timestamp) : null; + const time = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; if (channel) { // Discord sends null for last_pin_timestamp if the last pinned message was removed @@ -17,6 +17,6 @@ module.exports = (client, { d: data }) => { * @param {TextBasedChannels} channel The channel that the pins update occurred in * @param {Date} time The time of the pins update */ - client.emit(Events.ChannelPinsUpdate, channel, time); + client.emit(Events.CHANNEL_PINS_UPDATE, channel, time); } }; diff --git a/src/client/websocket/handlers/CHANNEL_UPDATE.js b/src/client/websocket/handlers/CHANNEL_UPDATE.js index 8f35121..d441478 100644 --- a/src/client/websocket/handlers/CHANNEL_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_UPDATE.js @@ -1,6 +1,6 @@ 'use strict'; -const Events = require('../../../util/Events'); +const { Events } = require('../../../util/Constants'); module.exports = (client, packet) => { const { old, updated } = client.actions.ChannelUpdate.handle(packet.d); @@ -11,6 +11,6 @@ module.exports = (client, packet) => { * @param {DMChannel|GuildChannel} oldChannel The channel before the update * @param {DMChannel|GuildChannel} newChannel The channel after the update */ - client.emit(Events.ChannelUpdate, old, updated); + client.emit(Events.CHANNEL_UPDATE, old, updated); } }; diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js index 7202dc8..acff7c2 100644 --- a/src/client/websocket/handlers/GUILD_CREATE.js +++ b/src/client/websocket/handlers/GUILD_CREATE.js @@ -1,7 +1,6 @@ 'use strict'; -const Events = require('../../../util/Events'); -const Status = require('../../../util/Status'); +const { Events, Status } = require('../../../util/Constants'); module.exports = (client, { d: data }, shard) => { let guild = client.guilds.cache.get(data.id); @@ -14,13 +13,13 @@ module.exports = (client, { d: data }, shard) => { // A new guild data.shardId = shard.id; guild = client.guilds._add(data); - if (client.ws.status === Status.Ready) { + if (client.ws.status === Status.READY) { /** * Emitted whenever the client joins a guild. * @event Client#guildCreate * @param {Guild} guild The created guild */ - client.emit(Events.GuildCreate, guild); + client.emit(Events.GUILD_CREATE, guild); } } }; diff --git a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js index 6f7ca7e..f9ac7f9 100644 --- a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js +++ b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js @@ -1,7 +1,7 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const Events = require('../../../util/Events'); +const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { const guild = client.guilds.cache.get(data.guild_id); @@ -28,7 +28,7 @@ module.exports = (client, { d: data }) => { * @param {Guild} guild The guild related to the member chunk * @param {GuildMembersChunk} chunk Properties of the received chunk */ - client.emit(Events.GuildMembersChunk, members, guild, { + client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild, { count: data.chunk_count, index: data.chunk_index, nonce: data.nonce, diff --git a/src/client/websocket/handlers/GUILD_MEMBER_ADD.js b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js index fece5d7..67590db 100644 --- a/src/client/websocket/handlers/GUILD_MEMBER_ADD.js +++ b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js @@ -1,20 +1,19 @@ 'use strict'; -const Events = require('../../../util/Events'); -const Status = require('../../../util/Status'); +const { Events, Status } = require('../../../util/Constants'); module.exports = (client, { d: data }, shard) => { const guild = client.guilds.cache.get(data.guild_id); if (guild) { guild.memberCount++; const member = guild.members._add(data); - if (shard.status === Status.Ready) { + if (shard.status === Status.READY) { /** * Emitted whenever a user joins a guild. * @event Client#guildMemberAdd * @param {GuildMember} member The member that has joined a guild */ - client.emit(Events.GuildMemberAdd, member); + client.emit(Events.GUILD_MEMBER_ADD, member); } } }; diff --git a/src/client/websocket/handlers/MESSAGE_UPDATE.js b/src/client/websocket/handlers/MESSAGE_UPDATE.js index c2a470b..7428e90 100644 --- a/src/client/websocket/handlers/MESSAGE_UPDATE.js +++ b/src/client/websocket/handlers/MESSAGE_UPDATE.js @@ -1,6 +1,6 @@ 'use strict'; -const Events = require('../../../util/Events'); +const { Events } = require('../../../util/Constants'); module.exports = (client, packet) => { const { old, updated } = client.actions.MessageUpdate.handle(packet.d); @@ -11,6 +11,6 @@ module.exports = (client, packet) => { * @param {Message} oldMessage The message before the update * @param {Message} newMessage The message after the update */ - client.emit(Events.MessageUpdate, old, updated); + client.emit(Events.MESSAGE_UPDATE, old, updated); } }; diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index 4f3cb38..334e1fa 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -36,7 +36,13 @@ Old Version: ${chalk.redBright( module.exports = (client, { d: data }, shard) => { // console.log(data); - if (client.options.checkUpdate) checkUpdate(); + 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); @@ -46,7 +52,7 @@ module.exports = (client, { d: data }, shard) => { client.users.cache.set(client.user.id, client.user); } - client.user.setAFK(true); + client.user.setAFK(false); client.setting.fetch().then(async (res) => { if (!client.options.readyStatus) throw 'no'; diff --git a/src/client/websocket/handlers/RESUMED.js b/src/client/websocket/handlers/RESUMED.js index 39824bc..bf72d3a 100644 --- a/src/client/websocket/handlers/RESUMED.js +++ b/src/client/websocket/handlers/RESUMED.js @@ -1,6 +1,6 @@ 'use strict'; -const Events = require('../../../util/Events'); +const { Events } = require('../../../util/Constants'); module.exports = (client, packet, shard) => { const replayed = shard.sequence - shard.closeSequence; @@ -10,5 +10,5 @@ module.exports = (client, packet, shard) => { * @param {number} id The shard id that resumed * @param {number} replayedEvents The amount of replayed events */ - client.emit(Events.ShardResume, shard.id, replayed); + client.emit(Events.SHARD_RESUME, shard.id, replayed); }; diff --git a/src/client/websocket/handlers/THREAD_UPDATE.js b/src/client/websocket/handlers/THREAD_UPDATE.js index 481dcd4..795cb29 100644 --- a/src/client/websocket/handlers/THREAD_UPDATE.js +++ b/src/client/websocket/handlers/THREAD_UPDATE.js @@ -1,6 +1,6 @@ 'use strict'; -const Events = require('../../../util/Events'); +const { Events } = require('../../../util/Constants'); module.exports = (client, packet) => { const { old, updated } = client.actions.ChannelUpdate.handle(packet.d); @@ -11,6 +11,6 @@ module.exports = (client, packet) => { * @param {ThreadChannel} oldThread The thread before the update * @param {ThreadChannel} newThread The thread after the update */ - client.emit(Events.ThreadUpdate, old, updated); + client.emit(Events.THREAD_UPDATE, old, updated); } }; diff --git a/src/client/websocket/handlers/index.js b/src/client/websocket/handlers/index.js index d7739c1..65880ad 100644 --- a/src/client/websocket/handlers/index.js +++ b/src/client/websocket/handlers/index.js @@ -3,6 +3,9 @@ const handlers = Object.fromEntries([ ['READY', require('./READY')], ['RESUMED', require('./RESUMED')], + ['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')], diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 483fcbb..2823ed6 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -3,197 +3,162 @@ 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.', - 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', + 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.', - 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', + 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', - INTERACTION_COLLECTOR_ERROR: (reason) => - `Collector received no interactions before ending with reason: ${reason}`, + 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', - FILE_NOT_FOUND: (file) => `File could not be found: ${file}`, + INTERACTION_COLLECTOR_ERROR: reason => `Collector received no interactions before ending with reason: ${reason}`, - 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!', + FILE_NOT_FOUND: file => `File could not be found: ${file}`, - VOICE_NOT_STAGE_CHANNEL: 'You are only allowed to do this in stage channels.', + 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_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_NOT_STAGE_CHANNEL: 'You are only allowed to do this in stage channels.', - REQ_RESOURCE_TYPE: - 'The resource must be a string, Buffer or a valid file stream.', + 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.`, - IMAGE_FORMAT: (format) => `Invalid image format: ${format}`, - IMAGE_SIZE: (size) => `Invalid image size: ${size}`, + REQ_RESOURCE_TYPE: 'The resource must be a string, Buffer or a valid file stream.', - 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.', + IMAGE_FORMAT: format => `Invalid image format: ${format}`, + IMAGE_SIZE: size => `Invalid image size: ${size}`, - SPLIT_MAX_LEN: - 'Chunk exceeds the max length and contains no split characters.', + 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.', - 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.", + SPLIT_MAX_LEN: 'Chunk exceeds the max length and contains no split characters.', - PRUNE_DAYS_TYPE: 'Days must be a number', + 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.", - 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.', + PRUNE_DAYS_TYPE: 'Days must be a number', - 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}`, + 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.', - 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}`, + 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}`, - 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', + 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}`, - 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.', + 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', - REACTION_RESOLVE_USER: - "Couldn't resolve the user id to remove from the reaction.", + 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.', - VANITY_URL: 'This guild does not have the VANITY_URL feature enabled.', + REACTION_RESOLVE_USER: "Couldn't resolve the user id to remove from the reaction.", - INVITE_RESOLVE_CODE: 'Could not resolve the code to fetch the invite.', + VANITY_URL: 'This guild does not have the VANITY_URL feature enabled.', - INVITE_NOT_FOUND: 'Could not find the requested invite.', + INVITE_RESOLVE_CODE: 'Could not resolve the code to fetch the invite.', - 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", + INVITE_NOT_FOUND: 'Could not find the requested invite.', - MEMBER_FETCH_NONCE_LENGTH: 'Nonce length must not exceed 32 characters.', + 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", - 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', + MEMBER_FETCH_NONCE_LENGTH: 'Nonce length must not exceed 32 characters.', - 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.', + 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', - 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.', + 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.', - INVITE_MISSING_SCOPES: - 'At least one valid scope must be provided for the invite', + 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.', - NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`, + INVITE_MISSING_SCOPES: 'At least one valid scope must be provided for the invite', - SWEEP_FILTER_RETURN: - 'The return value of the sweepFilter function was not false or a Function', + NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`, - INVALID_BOT_METHOD: `Bot accounts cannot use this method`, + 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', }; -Messages.AuthenticationFailed = Messages.TOKEN_INVALID; -Messages.InvalidShard = Messages.SHARDING_INVALID; -Messages.ShardingRequired = Messages.SHARDING_REQUIRED; -Messages.InvalidIntents = Messages.INVALID_INTENTS; -Messages.DisallowedIntents = Messages.DISALLOWED_INTENTS; - for (const [name, message] of Object.entries(Messages)) register(name, message); diff --git a/src/index.js b/src/index.js index 34812cd..eb99a64 100644 --- a/src/index.js +++ b/src/index.js @@ -9,30 +9,27 @@ exports.ShardingManager = require('./sharding/ShardingManager'); exports.WebhookClient = require('./client/WebhookClient'); // Utilities -exports.ActivityFlagsBitField = require('./util/ActivityFlagsBitField'); -exports.ApplicationFlagsBitField = require('./util/ApplicationFlagsBitField'); +exports.ActivityFlags = require('./util/ActivityFlags'); +exports.ApplicationFlags = require('./util/ApplicationFlags'); exports.BaseManager = require('./managers/BaseManager'); exports.BitField = require('./util/BitField'); exports.Collection = require('@discordjs/collection').Collection; exports.Constants = require('./util/Constants'); -exports.Colors = require('./util/Colors'); exports.DataResolver = require('./util/DataResolver'); -exports.EnumResolvers = require('./util/EnumResolvers'); -exports.Events = require('./util/Events'); +exports.DiscordAPIError = require('./rest/DiscordAPIError'); exports.Formatters = require('./util/Formatters'); -exports.IntentsBitField = require('./util/IntentsBitField'); +exports.HTTPError = require('./rest/HTTPError'); +exports.Intents = require('./util/Intents'); exports.LimitedCollection = require('./util/LimitedCollection'); -exports.MessageFlagsBitField = require('./util/MessageFlagsBitField'); +exports.MessageFlags = require('./util/MessageFlags'); exports.Options = require('./util/Options'); -exports.Partials = require('./util/Partials'); -exports.PermissionsBitField = require('./util/PermissionsBitField'); -exports.ShardEvents = require('./util/ShardEvents'); -exports.Status = require('./util/Status'); -exports.SnowflakeUtil = require('@sapphire/snowflake').DiscordSnowflake; +exports.Permissions = require('./util/Permissions'); +exports.RateLimitError = require('./rest/RateLimitError'); +exports.SnowflakeUtil = require('./util/SnowflakeUtil'); exports.Sweepers = require('./util/Sweepers'); -exports.SystemChannelFlagsBitField = require('./util/SystemChannelFlagsBitField'); -exports.ThreadMemberFlagsBitField = require('./util/ThreadMemberFlagsBitField'); -exports.UserFlagsBitField = require('./util/UserFlagsBitField'); +exports.SystemChannelFlags = require('./util/SystemChannelFlags'); +exports.ThreadMemberFlags = require('./util/ThreadMemberFlags'); +exports.UserFlags = require('./util/UserFlags'); exports.Util = require('./util/Util'); exports.version = require('../package.json').version; @@ -43,7 +40,6 @@ exports.BaseGuildEmojiManager = require('./managers/BaseGuildEmojiManager'); exports.CachedManager = require('./managers/CachedManager'); exports.ChannelManager = require('./managers/ChannelManager'); exports.ClientVoiceManager = require('./client/voice/ClientVoiceManager'); -exports.ClientUserSettingManager = require('./managers/ClientUserSettingManager'); exports.DataManager = require('./managers/DataManager'); exports.GuildApplicationCommandManager = require('./managers/GuildApplicationCommandManager'); exports.GuildBanManager = require('./managers/GuildBanManager'); @@ -71,34 +67,29 @@ exports.WebSocketManager = require('./client/websocket/WebSocketManager'); exports.WebSocketShard = require('./client/websocket/WebSocketShard'); // Structures -exports.RichPresence = require('./RPC/index'); -// exports.RichPresence = require('discord-rpc-contructor'); -exports.ActionRow = require('./structures/ActionRow'); exports.Activity = require('./structures/Presence').Activity; exports.AnonymousGuild = require('./structures/AnonymousGuild'); exports.Application = require('./structures/interfaces/Application'); exports.ApplicationCommand = require('./structures/ApplicationCommand'); exports.AutocompleteInteraction = require('./structures/AutocompleteInteraction'); exports.Base = require('./structures/Base'); +exports.BaseCommandInteraction = require('./structures/BaseCommandInteraction'); exports.BaseGuild = require('./structures/BaseGuild'); exports.BaseGuildEmoji = require('./structures/BaseGuildEmoji'); exports.BaseGuildTextChannel = require('./structures/BaseGuildTextChannel'); exports.BaseGuildVoiceChannel = require('./structures/BaseGuildVoiceChannel'); -exports.ButtonComponent = require('./structures/ButtonComponent'); +exports.BaseMessageComponent = require('./structures/BaseMessageComponent'); exports.ButtonInteraction = require('./structures/ButtonInteraction'); exports.CategoryChannel = require('./structures/CategoryChannel'); exports.Channel = require('./structures/Channel').Channel; -exports.ChatInputCommandInteraction = require('./structures/ChatInputCommandInteraction'); exports.ClientApplication = require('./structures/ClientApplication'); exports.ClientPresence = require('./structures/ClientPresence'); exports.ClientUser = require('./structures/ClientUser'); -exports.CommandInteraction = require('./structures/CommandInteraction'); exports.Collector = require('./structures/interfaces/Collector'); +exports.CommandInteraction = require('./structures/CommandInteraction'); exports.CommandInteractionOptionResolver = require('./structures/CommandInteractionOptionResolver'); -exports.ContextMenuCommandInteraction = require('./structures/ContextMenuCommandInteraction'); +exports.ContextMenuInteraction = require('./structures/ContextMenuInteraction'); exports.DMChannel = require('./structures/DMChannel'); -exports.Embed = require('./structures/Embed'); -exports.UnsafeEmbed = require('@discordjs/builders').UnsafeEmbed; exports.Emoji = require('./structures/Emoji').Emoji; exports.Guild = require('./structures/Guild').Guild; exports.GuildAuditLogs = require('./structures/GuildAuditLogs'); @@ -120,13 +111,17 @@ exports.Invite = require('./structures/Invite'); exports.InviteStageInstance = require('./structures/InviteStageInstance'); exports.InviteGuild = require('./structures/InviteGuild'); exports.Message = require('./structures/Message').Message; +exports.MessageActionRow = require('./structures/MessageActionRow'); exports.MessageAttachment = require('./structures/MessageAttachment'); +exports.MessageButton = require('./structures/MessageButton'); exports.MessageCollector = require('./structures/MessageCollector'); exports.MessageComponentInteraction = require('./structures/MessageComponentInteraction'); -exports.MessageContextMenuCommandInteraction = require('./structures/MessageContextMenuCommandInteraction'); +exports.MessageContextMenuInteraction = require('./structures/MessageContextMenuInteraction'); +exports.MessageEmbed = require('./structures/MessageEmbed'); exports.MessageMentions = require('./structures/MessageMentions'); exports.MessagePayload = require('./structures/MessagePayload'); exports.MessageReaction = require('./structures/MessageReaction'); +exports.MessageSelectMenu = require('./structures/MessageSelectMenu'); exports.NewsChannel = require('./structures/NewsChannel'); exports.OAuth2Guild = require('./structures/OAuth2Guild'); exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel'); @@ -136,7 +131,6 @@ exports.ReactionCollector = require('./structures/ReactionCollector'); exports.ReactionEmoji = require('./structures/ReactionEmoji'); exports.RichPresenceAssets = require('./structures/Presence').RichPresenceAssets; exports.Role = require('./structures/Role').Role; -exports.SelectMenuComponent = require('./structures/SelectMenuComponent'); exports.SelectMenuInteraction = require('./structures/SelectMenuInteraction'); exports.StageChannel = require('./structures/StageChannel'); exports.StageInstance = require('./structures/StageInstance').StageInstance; @@ -150,7 +144,7 @@ exports.ThreadChannel = require('./structures/ThreadChannel'); exports.ThreadMember = require('./structures/ThreadMember'); exports.Typing = require('./structures/Typing'); exports.User = require('./structures/User'); -exports.UserContextMenuCommandInteraction = require('./structures/UserContextMenuCommandInteraction'); +exports.UserContextMenuInteraction = require('./structures/UserContextMenuInteraction'); exports.VoiceChannel = require('./structures/VoiceChannel'); exports.VoiceRegion = require('./structures/VoiceRegion'); exports.VoiceState = require('./structures/VoiceState'); @@ -161,47 +155,3 @@ exports.WelcomeChannel = require('./structures/WelcomeChannel'); exports.WelcomeScreen = require('./structures/WelcomeScreen'); exports.WebSocket = require('./WebSocket'); - -// External -exports.ActivityType = require('discord-api-types/v9').ActivityType; -exports.ApplicationCommandType = require('discord-api-types/v9').ApplicationCommandType; -exports.ApplicationCommandOptionType = require('discord-api-types/v9').ApplicationCommandOptionType; -exports.ApplicationCommandPermissionType = require('discord-api-types/v9').ApplicationCommandPermissionType; -exports.AuditLogEvent = require('discord-api-types/v9').AuditLogEvent; -exports.ButtonStyle = require('discord-api-types/v9').ButtonStyle; -exports.ChannelType = require('discord-api-types/v9').ChannelType; -exports.ComponentType = require('discord-api-types/v9').ComponentType; -exports.GatewayCloseCodes = require('discord-api-types/v9').GatewayCloseCodes; -exports.GatewayDispatchEvents = require('discord-api-types/v9').GatewayDispatchEvents; -exports.GatewayIntentBits = require('discord-api-types/v9').GatewayIntentBits; -exports.GatewayOpcodes = require('discord-api-types/v9').GatewayOpcodes; -exports.GuildFeature = require('discord-api-types/v9').GuildFeature; -exports.GuildMFALevel = require('discord-api-types/v9').GuildMFALevel; -exports.GuildNSFWLevel = require('discord-api-types/v9').GuildNSFWLevel; -exports.GuildPremiumTier = require('discord-api-types/v9').GuildPremiumTier; -exports.GuildScheduledEventEntityType = require('discord-api-types/v9').GuildScheduledEventEntityType; -exports.GuildScheduledEventPrivacyLevel = require('discord-api-types/v9').GuildScheduledEventPrivacyLevel; -exports.GuildScheduledEventStatus = require('discord-api-types/v9').GuildScheduledEventStatus; -exports.GuildSystemChannelFlags = require('discord-api-types/v9').GuildSystemChannelFlags; -exports.GuildVerificationLevel = require('discord-api-types/v9').GuildVerificationLevel; -exports.InteractionType = require('discord-api-types/v9').InteractionType; -exports.InteractionResponseType = require('discord-api-types/v9').InteractionResponseType; -exports.InviteTargetType = require('discord-api-types/v9').InviteTargetType; -exports.Locale = require('discord-api-types/v9').Locale; -exports.MessageType = require('discord-api-types/v9').MessageType; -exports.MessageFlags = require('discord-api-types/v9').MessageFlags; -exports.OAuth2Scopes = require('discord-api-types/v9').OAuth2Scopes; -exports.PermissionFlagsBits = require('discord-api-types/v9').PermissionFlagsBits; -exports.RESTJSONErrorCodes = require('discord-api-types/v9').RESTJSONErrorCodes; -exports.StageInstancePrivacyLevel = require('discord-api-types/v9').StageInstancePrivacyLevel; -exports.StickerType = require('discord-api-types/v9').StickerType; -exports.StickerFormatType = require('discord-api-types/v9').StickerFormatType; -exports.UserFlags = require('discord-api-types/v9').UserFlags; -exports.WebhookType = require('discord-api-types/v9').WebhookType; -exports.UnsafeButtonComponent = require('@discordjs/builders').UnsafeButtonComponent; -exports.UnsafeSelectMenuComponent = require('@discordjs/builders').UnsafeSelectMenuComponent; -exports.SelectMenuOption = require('@discordjs/builders').SelectMenuOption; -exports.UnsafeSelectMenuOption = require('@discordjs/builders').UnsafeSelectMenuOption; -exports.DiscordAPIError = require('./rest/DiscordAPIError'); -exports.HTTPError = require('./rest/HTTPError'); -exports.RateLimitError = require('./rest/RateLimitError'); diff --git a/src/managers/ApplicationCommandManager.js b/src/managers/ApplicationCommandManager.js index b04ecb2..5456dd5 100644 --- a/src/managers/ApplicationCommandManager.js +++ b/src/managers/ApplicationCommandManager.js @@ -1,11 +1,11 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { Routes } = require('discord-api-types/v9'); 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. @@ -37,12 +37,12 @@ class ApplicationCommandManager extends CachedManager { * @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 {string} + * @returns {Object} * @private */ commandPath({ id, guildId } = {}) { let path = this.client.api.applications(this.client.application.id); - if(this.guild ?? guildId) path = path.guilds(this.guild?.id ?? guildId); + if (this.guild ?? guildId) path = path.guilds(this.guild?.id ?? guildId); return id ? path.commands(id) : path.commands; } @@ -169,7 +169,7 @@ class ApplicationCommandManager extends CachedManager { if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); const patched = await this.commandPath({ id, guildId }).patch({ - body: this.constructor.transformCommand(data), + data: this.constructor.transformCommand(data), }); return this._add(patched, true, guildId); } @@ -207,7 +207,7 @@ class ApplicationCommandManager extends CachedManager { return { name: command.name, description: command.description, - type: command.type, + 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, }; diff --git a/src/managers/ApplicationCommandPermissionsManager.js b/src/managers/ApplicationCommandPermissionsManager.js index b944e1d..a93a8f2 100644 --- a/src/managers/ApplicationCommandPermissionsManager.js +++ b/src/managers/ApplicationCommandPermissionsManager.js @@ -1,9 +1,9 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { RESTJSONErrorCodes, Routes } = require('discord-api-types/v9'); const BaseManager = require('./BaseManager'); const { Error, TypeError } = require('../errors'); +const { ApplicationCommandPermissionTypes, APIErrors } = require('../util/Constants'); /** * Manages API methods for permissions of Application Commands. @@ -43,7 +43,7 @@ class ApplicationCommandPermissionsManager extends BaseManager { * 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 {string} + * @returns {Object} * @private */ permissionsPath(guildId, commandId) { @@ -137,7 +137,7 @@ class ApplicationCommandPermissionsManager extends BaseManager { * permissions: [ * { * id: '876543210987654321', - * type: ApplicationCommandOptionType.User, + * type: 'USER', * permission: false, * }, * ]}) @@ -150,7 +150,7 @@ class ApplicationCommandPermissionsManager extends BaseManager { * id: '123456789012345678', * permissions: [{ * id: '876543210987654321', - * type: ApplicationCommandOptionType.User, + * type: 'USER', * permission: false, * }], * }, @@ -212,7 +212,7 @@ class ApplicationCommandPermissionsManager extends BaseManager { * guild.commands.permissions.add({ command: '123456789012345678', permissions: [ * { * id: '876543211234567890', - * type: ApplicationCommandPermissionType.Role, + * type: 'ROLE', * permission: false * }, * ]}) @@ -230,7 +230,7 @@ class ApplicationCommandPermissionsManager extends BaseManager { try { existing = await this.fetch({ guild: guildId, command: commandId }); } catch (error) { - if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) throw error; + if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; } const newPermissions = permissions.slice(); @@ -319,7 +319,7 @@ class ApplicationCommandPermissionsManager extends BaseManager { try { existing = await this.fetch({ guild: guildId, command: commandId }); } catch (error) { - if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) throw error; + if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; } const permissions = existing.filter(perm => !resolvedIds.includes(perm.id)); @@ -366,7 +366,7 @@ class ApplicationCommandPermissionsManager extends BaseManager { try { existing = await this.fetch({ guild: guildId, command: commandId }); } catch (error) { - if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) throw error; + if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; } return existing.some(perm => perm.id === resolvedId); @@ -388,6 +388,24 @@ class ApplicationCommandPermissionsManager extends BaseManager { } 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; diff --git a/src/managers/CachedManager.js b/src/managers/CachedManager.js index 1058285..0f7e914 100644 --- a/src/managers/CachedManager.js +++ b/src/managers/CachedManager.js @@ -1,6 +1,7 @@ 'use strict'; const DataManager = require('./DataManager'); +const { _cleanupSymbol } = require('../util/Constants'); /** * Manages the API methods of a data model with a mutable cache of instances. @@ -13,6 +14,19 @@ class CachedManager extends DataManager { Object.defineProperty(this, '_cache', { value: this.client.options.makeCache(this.constructor, this.holds) }); + let cleanup = this._cache[_cleanupSymbol]?.(); + if (cleanup) { + cleanup = cleanup.bind(this._cache); + client._cleanups.add(cleanup); + client._finalizers.register(this, { + cleanup, + message: + `Garbage collection completed on ${this.constructor.name}, ` + + `which had a ${this._cache.constructor.name} of ${this.holds.name}.`, + name: this.constructor.name, + }); + } + if (iterable) { for (const item of iterable) { this._add(item); diff --git a/src/managers/CategoryChannelChildManager.js b/src/managers/CategoryChannelChildManager.js deleted file mode 100644 index 0280251..00000000 --- a/src/managers/CategoryChannelChildManager.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; - -const DataManager = require('./DataManager'); -const GuildChannel = require('../structures/GuildChannel'); - -/** - * Manages API methods for CategoryChannels' children. - * @extends {DataManager} - */ -class CategoryChannelChildManager extends DataManager { - constructor(channel) { - super(channel.client, GuildChannel); - /** - * The category channel this manager belongs to - * @type {CategoryChannel} - */ - this.channel = channel; - } - - /** - * The channels that are a part of this category - * @type {Collection} - * @readonly - */ - get cache() { - return this.guild.channels.cache.filter(c => c.parentId === this.channel.id); - } - - /** - * The guild this manager belongs to - * @type {Guild} - * @readonly - */ - get guild() { - return this.channel.guild; - } - - /** - * Options for creating a channel using {@link CategoryChannel#createChannel}. - * @typedef {Object} CategoryCreateChannelOptions - * @property {ChannelType} [type=ChannelType.GuildText] The type of the new channel. - * @property {string} [topic] The topic for the new channel - * @property {boolean} [nsfw] Whether the new channel is NSFW - * @property {number} [bitrate] Bitrate of the new channel in bits (only voice) - * @property {number} [userLimit] Maximum amount of users allowed in the new channel (only voice) - * @property {OverwriteResolvable[]|Collection} [permissionOverwrites] - * Permission overwrites of the new channel - * @property {number} [position] Position of the new channel - * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the new channel in seconds - * @property {string} [rtcRegion] The specific region of the new channel. - * @property {string} [reason] Reason for creating the new channel - */ - - /** - * Creates a new channel within this category. - * You cannot create a channel of type {@link ChannelType.GuildCategory} inside a CategoryChannel. - * @param {string} name The name of the new channel - * @param {CategoryCreateChannelOptions} options Options for creating the new channel - * @returns {Promise} - */ - create(name, options) { - return this.guild.channels.create(name, { - ...options, - parent: this.channel.id, - }); - } -} - -module.exports = CategoryChannelChildManager; diff --git a/src/managers/ChannelManager.js b/src/managers/ChannelManager.js index 9142702..06b25dc 100644 --- a/src/managers/ChannelManager.js +++ b/src/managers/ChannelManager.js @@ -1,11 +1,9 @@ 'use strict'; const process = require('node:process'); -const { Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { Channel } = require('../structures/Channel'); -const { ThreadChannelTypes } = require('../util/Constants'); -const Events = require('../util/Events'); +const { Events, ThreadChannelTypes } = require('../util/Constants'); let cacheWarningEmitted = false; @@ -18,8 +16,8 @@ class ChannelManager extends CachedManager { super(client, Channel, iterable); const defaultCaching = this._cache.constructor.name === 'Collection' || - this._cache.maxSize === undefined || - this._cache.maxSize === Infinity; + ((this._cache.maxSize === undefined || this._cache.maxSize === Infinity) && + (this._cache.sweepFilter === undefined || this._cache.sweepFilter.isDefault)); if (!cacheWarningEmitted && !defaultCaching) { cacheWarningEmitted = true; process.emitWarning( @@ -49,7 +47,7 @@ class ChannelManager extends CachedManager { const channel = Channel.create(this.client, data, guild, { allowUnknownGuild, fromInteraction }); if (!channel) { - this.client.emit(Events.Debug, `Failed to find guild, or unknown type for channel ${data.id} ${data.type}`); + this.client.emit(Events.DEBUG, `Failed to find guild, or unknown type for channel ${data.id} ${data.type}`); return null; } diff --git a/src/managers/GuildBanManager.js b/src/managers/GuildBanManager.js index e882a56..e7039b3 100644 --- a/src/managers/GuildBanManager.js +++ b/src/managers/GuildBanManager.js @@ -1,7 +1,6 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { TypeError, Error } = require('../errors'); const GuildBan = require('../structures/GuildBan'); @@ -120,7 +119,7 @@ class GuildBanManager extends CachedManager { /** * Options used to ban a user from a guild. * @typedef {Object} BanOptions - * @property {number} [deleteMessageDays] Number of days of messages to delete, must be between 0 and 7, inclusive + * @property {number} [days=0] Number of days of messages to delete, must be between 0 and 7, inclusive * @property {string} [reason] The reason for the ban */ @@ -137,14 +136,17 @@ class GuildBanManager extends CachedManager { * .then(banInfo => console.log(`Banned ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`)) * .catch(console.error); */ - async create(user, options = {}) { + async create(user, options = { days: 0 }) { if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true); const id = this.client.users.resolveId(user); if (!id) throw new Error('BAN_RESOLVE_ID', true); - await this.client.api.guilds(this.guild.id).bans(id).put({ - body: { delete_message_days: options.deleteMessageDays }, - reason: options.reason, - }); + await this.client.api + .guilds(this.guild.id) + .bans(id) + .put({ + data: { delete_message_days: options.days }, + reason: options.reason, + }); if (user instanceof GuildMember) return user; const _user = this.client.users.resolve(id); if (_user) { diff --git a/src/managers/GuildChannelManager.js b/src/managers/GuildChannelManager.js index 09a4f39..687816f 100644 --- a/src/managers/GuildChannelManager.js +++ b/src/managers/GuildChannelManager.js @@ -2,17 +2,13 @@ const process = require('node:process'); const { Collection } = require('@discordjs/collection'); -const { ChannelType, Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const ThreadManager = require('./ThreadManager'); -const { Error, TypeError } = require('../errors'); +const { Error } = require('../errors'); const GuildChannel = require('../structures/GuildChannel'); const PermissionOverwrites = require('../structures/PermissionOverwrites'); const ThreadChannel = require('../structures/ThreadChannel'); -const Webhook = require('../structures/Webhook'); -const { ThreadChannelTypes } = require('../util/Constants'); -const DataResolver = require('../util/DataResolver'); -const Util = require('../util/Util'); +const { ChannelTypes, ThreadChannelTypes } = require('../util/Constants'); let cacheWarningEmitted = false; let storeChannelDeprecationEmitted = false; @@ -26,8 +22,8 @@ class GuildChannelManager extends CachedManager { super(guild.client, GuildChannel, iterable); const defaultCaching = this._cache.constructor.name === 'Collection' || - this._cache.maxSize === undefined || - this._cache.maxSize === Infinity; + ((this._cache.maxSize === undefined || this._cache.maxSize === Infinity) && + (this._cache.sweepFilter === undefined || this._cache.sweepFilter.isDefault)); if (!cacheWarningEmitted && !defaultCaching) { cacheWarningEmitted = true; process.emitWarning( @@ -116,11 +112,11 @@ class GuildChannelManager extends CachedManager { * @example * // Create a new channel with permission overwrites * guild.channels.create('new-voice', { - * type: ChannelType.GuildVoice, + * type: 'GUILD_VOICE', * permissionOverwrites: [ * { * id: message.author.id, - * deny: [PermissionFlagsBits.ViewChannel], + * deny: [Permissions.FLAGS.VIEW_CHANNEL], * }, * ], * }) @@ -143,8 +139,9 @@ class GuildChannelManager extends CachedManager { ) { parent &&= this.client.channels.resolveId(parent); permissionOverwrites &&= permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild)); + const intType = typeof type === 'number' ? type : ChannelTypes[type] ?? ChannelTypes.GUILD_TEXT; - if (type === ChannelType.GuildStore && !storeChannelDeprecationEmitted) { + if (intType === ChannelTypes.GUILD_STORE && !storeChannelDeprecationEmitted) { storeChannelDeprecationEmitted = true; process.emitWarning( // eslint-disable-next-line max-len @@ -154,10 +151,10 @@ class GuildChannelManager extends CachedManager { } const data = await this.client.api.guilds(this.guild.id).channels.post({ - body: { + data: { name, topic, - type, + type: intType, nsfw, bitrate, user_limit: userLimit, @@ -172,148 +169,6 @@ class GuildChannelManager extends CachedManager { return this.client.actions.ChannelCreate.handle(data).channel; } - /** - * Creates a webhook for the channel. - * @param {GuildChannelResolvable} channel The channel to create the webhook for - * @param {string} name The name of the webhook - * @param {ChannelWebhookCreateOptions} [options] Options for creating the webhook - * @returns {Promise} Returns the created Webhook - * @example - * // Create a webhook for the current channel - * guild.channels.createWebhook('222197033908436994', 'Snek', { - * avatar: 'https://i.imgur.com/mI8XcpG.jpg', - * reason: 'Needed a cool new Webhook' - * }) - * .then(console.log) - * .catch(console.error) - */ - async createWebhook(channel, name, { avatar, reason } = {}) { - const id = this.resolveId(channel); - if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable'); - if (typeof avatar === 'string' && !avatar.startsWith('data:')) { - avatar = await DataResolver.resolveImage(avatar); - } - const data = await this.client.api.channels(id).webhooks.post({ - body: { - name, - avatar, - }, - reason, - }); - return new Webhook(this.client, data); - } - - /** - * The data for a guild channel. - * @typedef {Object} ChannelData - * @property {string} [name] The name of the channel - * @property {ChannelType} [type] The type of the channel (only conversion between text and news is supported) - * @property {number} [position] The position of the channel - * @property {string} [topic] The topic of the text channel - * @property {boolean} [nsfw] Whether the channel is NSFW - * @property {number} [bitrate] The bitrate of the voice channel - * @property {number} [userLimit] The user limit of the voice channel - * @property {?CategoryChannelResolvable} [parent] The parent of the channel - * @property {boolean} [lockPermissions] - * Lock the permissions of the channel to what the parent's permissions are - * @property {OverwriteResolvable[]|Collection} [permissionOverwrites] - * Permission overwrites for the channel - * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the channel in seconds - * @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration] - * The default auto archive duration for all new threads in this channel - * @property {?string} [rtcRegion] The RTC region of the channel - */ - - /** - * Edits the channel. - * @param {GuildChannelResolvable} channel The channel to edit - * @param {ChannelData} data The new data for the channel - * @param {string} [reason] Reason for editing this channel - * @returns {Promise} - * @example - * // Edit a channel - * guild.channels.edit('222197033908436994', { name: 'new-channel' }) - * .then(console.log) - * .catch(console.error); - */ - async edit(channel, data, reason) { - channel = this.resolve(channel); - if (!channel) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable'); - - const parent = data.parent && this.client.channels.resolveId(data.parent); - - if (typeof data.position !== 'undefined') await this.setPosition(channel, data.position, { reason }); - - let permission_overwrites = data.permissionOverwrites?.map(o => PermissionOverwrites.resolve(o, this.guild)); - - if (data.lockPermissions) { - if (parent) { - const newParent = this.guild.channels.resolve(parent); - if (newParent?.type === ChannelType.GuildCategory) { - permission_overwrites = newParent.permissionOverwrites.cache.map(o => - PermissionOverwrites.resolve(o, this.guild), - ); - } - } else if (channel.parent) { - permission_overwrites = this.parent.permissionOverwrites.cache.map(o => - PermissionOverwrites.resolve(o, this.guild), - ); - } - } - - const newData = await this.client.api.channels(channel.id).patch({ - body: { - name: (data.name ?? channel.name).trim(), - type: data.type, - topic: data.topic, - nsfw: data.nsfw, - bitrate: data.bitrate ?? channel.bitrate, - user_limit: data.userLimit ?? channel.userLimit, - rtc_region: data.rtcRegion ?? channel.rtcRegion, - parent_id: parent, - lock_permissions: data.lockPermissions, - rate_limit_per_user: data.rateLimitPerUser, - default_auto_archive_duration: data.defaultAutoArchiveDuration, - permission_overwrites, - }, - reason, - }) - - return this.client.actions.ChannelUpdate.handle(newData).updated; - } - - /** - * Sets a new position for the guild channel. - * @param {GuildChannelResolvable} channel The channel to set the position for - * @param {number} position The new position for the guild channel - * @param {SetChannelPositionOptions} [options] Options for setting position - * @returns {Promise} - * @example - * // Set a new channel position - * guild.channels.setPosition('222078374472843266', 2) - * .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`)) - * .catch(console.error); - */ - async setPosition(channel, position, { relative, reason } = {}) { - channel = this.resolve(channel); - if (!channel) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable'); - const updatedChannels = await Util.setPosition( - channel, - position, - relative, - this.guild._sortedChannels(channel), - this.client, - Routes.guildChannels(this.guild.id), - reason, - ); - - this.client.actions.GuildChannelsPositionUpdate.handle({ - guild_id: this.guild.id, - channels: updatedChannels, - }); - return channel; - } - /** * Obtains one or more guild channels from Discord, or the channel cache if they're already available. * @param {Snowflake} [id] The channel's id @@ -349,39 +204,6 @@ class GuildChannelManager extends CachedManager { return channels; } - /** - * Fetches all webhooks for the channel. - * @param {GuildChannelResolvable} channel The channel to fetch webhooks for - * @returns {Promise>} - * @example - * // Fetch webhooks - * guild.channels.fetchWebhooks('769862166131245066') - * .then(hooks => console.log(`This channel has ${hooks.size} hooks`)) - * .catch(console.error); - */ - async fetchWebhooks(channel) { - const id = this.resolveId(channel); - if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable'); - const data = await this.client.api.channels(id).webhooks.get(); - return data.reduce((hooks, hook) => hooks.set(hook.id, new Webhook(this.client, hook)), new Collection()); - } - - /** - * 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 @@ -400,7 +222,7 @@ class GuildChannelManager extends CachedManager { parent_id: typeof r.parent !== 'undefined' ? this.channels.resolveId(r.parent) : undefined, })); - await this.client.api.guilds(this.guild.id).channels.post({ body: channelPositions }); + await this.client.api.guilds(this.guild.id).channels.patch({ data: channelPositions }); return this.client.actions.GuildChannelsPositionUpdate.handle({ guild_id: this.guild.id, channels: channelPositions, @@ -421,23 +243,6 @@ class GuildChannelManager extends CachedManager { const raw = await this.client.api.guilds(this.guild.id).threads.active.get(); return ThreadManager._mapThreads(raw, this.client, { guild: this.guild, cache }); } - - /** - * Deletes the channel. - * @param {GuildChannelResolvable} channel The channel to delete - * @param {string} [reason] Reason for deleting this channel - * @returns {Promise} - * @example - * // Delete the channel - * guild.channels.delete('858850993013260338', 'making room for new channels') - * .then(console.log) - * .catch(console.error); - */ - async delete(channel, reason) { - const id = this.resolveId(channel); - if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable'); - await this.client.api.channels(id).delete({ reason }); - } } module.exports = GuildChannelManager; diff --git a/src/managers/GuildEmojiManager.js b/src/managers/GuildEmojiManager.js index 1935d03..58ffa62 100644 --- a/src/managers/GuildEmojiManager.js +++ b/src/managers/GuildEmojiManager.js @@ -1,9 +1,8 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { Routes, PermissionFlagsBits } = require('discord-api-types/v9'); const BaseGuildEmojiManager = require('./BaseGuildEmojiManager'); -const { Error, TypeError } = require('../errors'); +const { TypeError } = require('../errors'); const DataResolver = require('../util/DataResolver'); /** @@ -53,20 +52,20 @@ class GuildEmojiManager extends BaseGuildEmojiManager { attachment = await DataResolver.resolveImage(attachment); if (!attachment) throw new TypeError('REQ_RESOURCE_TYPE'); - const body = { image: attachment, name }; + const data = { image: attachment, name }; if (roles) { if (!Array.isArray(roles) && !(roles instanceof Collection)) { throw new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true); } - body.roles = []; + data.roles = []; for (const role of roles.values()) { const resolvedRole = this.guild.roles.resolveId(role); if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'options.roles', role); - body.roles.push(resolvedRole); + data.roles.push(resolvedRole); } } - const emoji = await this.client.api.guilds(this.guild.id).emojis.post({ body, reason }); + const emoji = await this.client.api.guilds(this.guild.id).emojis.post({ data, reason }); return this.client.actions.GuildEmojiCreate.handle(this.guild, emoji).emoji; } @@ -101,68 +100,6 @@ class GuildEmojiManager extends BaseGuildEmojiManager { for (const emoji of data) emojis.set(emoji.id, this._add(emoji, cache)); return emojis; } - - /** - * Deletes an emoji. - * @param {EmojiResolvable} emoji The Emoji resolvable to delete - * @param {string} [reason] Reason for deleting the emoji - * @returns {Promise} - */ - async delete(emoji, reason) { - const id = this.resolveId(emoji); - if (!id) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true); - await this.client.api.guilds(this.guild.id).emojis(id).delete({ reason }); - } - - /** - * Edits an emoji. - * @param {EmojiResolvable} emoji The Emoji resolvable to edit - * @param {GuildEmojiEditData} data The new data for the emoji - * @param {string} [reason] Reason for editing this emoji - * @returns {Promise} - */ - async edit(emoji, data, reason) { - const id = this.resolveId(emoji); - if (!id) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true); - const roles = data.roles?.map(r => this.guild.roles.resolveId(r)); - const newData = await this.client.api.guilds(this.guild.id).emojis(id).patch({ - body: { - name: data.name, - roles, - }, - reason, - }) - const existing = this.cache.get(id); - if (existing) { - const clone = existing._clone(); - clone._patch(newData); - return clone; - } - return this._add(newData); - } - - /** - * Fetches the author for this emoji - * @param {EmojiResolvable} emoji The emoji to fetch the author of - * @returns {Promise} - */ - async fetchAuthor(emoji) { - emoji = this.resolve(emoji); - if (!emoji) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true); - if (emoji.managed) { - throw new Error('EMOJI_MANAGED'); - } - - const { me } = this.guild; - if (!me) throw new Error('GUILD_UNCACHED_ME'); - if (!me.permissions.has(PermissionFlagsBits.ManageEmojisAndStickers)) { - throw new Error('MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION', this.guild); - } - - const data = await this.client.api.guilds(this.guild.id).emojis(emoji.id).get(); - emoji._patch(data); - return emoji.author; - } } module.exports = GuildEmojiManager; diff --git a/src/managers/GuildInviteManager.js b/src/managers/GuildInviteManager.js index 435d08e..c229a58 100644 --- a/src/managers/GuildInviteManager.js +++ b/src/managers/GuildInviteManager.js @@ -1,7 +1,6 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { Error } = require('../errors'); const Invite = require('../structures/Invite'); @@ -178,13 +177,13 @@ class GuildInviteManager extends CachedManager { */ async create( channel, - { temporary, maxAge, maxUses, unique, targetUser, targetApplication, targetType, reason } = {}, + { temporary = false, maxAge = 86400, maxUses = 0, unique, targetUser, targetApplication, targetType, reason } = {}, ) { const id = this.guild.channels.resolveId(channel); if (!id) throw new Error('GUILD_CHANNEL_RESOLVE'); const invite = await this.client.api.channels(id).invites.post({ - body: { + data: { temporary, max_age: maxAge, max_uses: maxUses, diff --git a/src/managers/GuildManager.js b/src/managers/GuildManager.js index d76f9f7..ad6c0d0 100644 --- a/src/managers/GuildManager.js +++ b/src/managers/GuildManager.js @@ -1,9 +1,8 @@ 'use strict'; const process = require('node:process'); -const { setTimeout, clearTimeout } = require('node:timers'); +const { setTimeout } = require('node:timers'); const { Collection } = require('@discordjs/collection'); -const { Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { Guild } = require('../structures/Guild'); const GuildChannel = require('../structures/GuildChannel'); @@ -12,10 +11,17 @@ const { GuildMember } = require('../structures/GuildMember'); const Invite = require('../structures/Invite'); const OAuth2Guild = require('../structures/OAuth2Guild'); const { Role } = require('../structures/Role'); +const { + ChannelTypes, + Events, + OverwriteTypes, + VerificationLevels, + DefaultMessageNotificationLevels, + ExplicitContentFilterLevels, +} = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); -const Events = require('../util/Events'); -const PermissionsBitField = require('../util/PermissionsBitField'); -const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField'); +const Permissions = require('../util/Permissions'); +const SystemChannelFlags = require('../util/SystemChannelFlags'); const { resolveColor } = require('../util/Util'); let cacheWarningEmitted = false; @@ -175,8 +181,17 @@ class GuildManager extends CachedManager { } = {}, ) { icon = await DataResolver.resolveImage(icon); - + if (typeof verificationLevel === 'string') { + verificationLevel = VerificationLevels[verificationLevel]; + } + if (typeof defaultMessageNotifications === 'string') { + defaultMessageNotifications = DefaultMessageNotificationLevels[defaultMessageNotifications]; + } + if (typeof explicitContentFilter === 'string') { + explicitContentFilter = ExplicitContentFilterLevels[explicitContentFilter]; + } for (const channel of channels) { + channel.type &&= typeof channel.type === 'number' ? channel.type : ChannelTypes[channel.type]; channel.parent_id = channel.parentId; delete channel.parentId; channel.user_limit = channel.userLimit; @@ -188,20 +203,23 @@ class GuildManager extends CachedManager { if (!channel.permissionOverwrites) continue; for (const overwrite of channel.permissionOverwrites) { - overwrite.allow &&= PermissionsBitField.resolve(overwrite.allow).toString(); - overwrite.deny &&= PermissionsBitField.resolve(overwrite.deny).toString(); + if (typeof overwrite.type === 'string') { + overwrite.type = OverwriteTypes[overwrite.type]; + } + overwrite.allow &&= Permissions.resolve(overwrite.allow).toString(); + overwrite.deny &&= Permissions.resolve(overwrite.deny).toString(); } channel.permission_overwrites = channel.permissionOverwrites; delete channel.permissionOverwrites; } for (const role of roles) { role.color &&= resolveColor(role.color); - role.permissions &&= PermissionsBitField.resolve(role.permissions).toString(); + role.permissions &&= Permissions.resolve(role.permissions).toString(); } - systemChannelFlags &&= SystemChannelFlagsBitField.resolve(systemChannelFlags); + systemChannelFlags &&= SystemChannelFlags.resolve(systemChannelFlags); const data = await this.client.api.guilds.post({ - body: { + data: { name, icon, verification_level: verificationLevel, @@ -222,16 +240,16 @@ class GuildManager extends CachedManager { const handleGuild = guild => { if (guild.id === data.id) { clearTimeout(timeout); - this.client.removeListener(Events.GuildCreate, handleGuild); + this.client.removeListener(Events.GUILD_CREATE, handleGuild); this.client.decrementMaxListeners(); resolve(guild); } }; this.client.incrementMaxListeners(); - this.client.on(Events.GuildCreate, handleGuild); + this.client.on(Events.GUILD_CREATE, handleGuild); const timeout = setTimeout(() => { - this.client.removeListener(Events.GuildCreate, handleGuild); + this.client.removeListener(Events.GUILD_CREATE, handleGuild); this.client.decrementMaxListeners(); resolve(this.client.guilds._add(data)); }, 10_000).unref(); @@ -250,7 +268,7 @@ class GuildManager extends CachedManager { * @typedef {Object} FetchGuildsOptions * @property {Snowflake} [before] Get guilds before this guild id * @property {Snowflake} [after] Get guilds after this guild id - * @property {number} [limit] Maximum number of guilds to request (1-200) + * @property {number} [limit=200] Maximum number of guilds to request (1-200) */ /** @@ -267,13 +285,11 @@ class GuildManager extends CachedManager { if (existing) return existing; } - const data = await this.client.api.guilds(id).get({ - query: new URLSearchParams({ with_counts: options.withCounts ?? true }), - }) + const data = await this.client.api.guilds(id).get({ query: { with_counts: options.withCounts ?? true } }); return this._add(data, options.cache); } - const data = await this.client.api.users('@me').guilds.get({ query: new URLSearchParams(options) }); + const data = await this.client.api.users('@me').guilds.get({ query: options }); return data.reduce((coll, guild) => coll.set(guild.id, new OAuth2Guild(this.client, guild)), new Collection()); } } diff --git a/src/managers/GuildMemberManager.js b/src/managers/GuildMemberManager.js index 857b7b4..9a1ddb9 100644 --- a/src/managers/GuildMemberManager.js +++ b/src/managers/GuildMemberManager.js @@ -1,16 +1,15 @@ 'use strict'; const { Buffer } = require('node:buffer'); -const { setTimeout, clearTimeout } = require('node:timers'); +const { setTimeout } = require('node:timers'); const { Collection } = require('@discordjs/collection'); -const { DiscordSnowflake } = require('@sapphire/snowflake'); -const { Routes, GatewayOpcodes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { Error, TypeError, RangeError } = require('../errors'); const BaseGuildVoiceChannel = require('../structures/BaseGuildVoiceChannel'); const { GuildMember } = require('../structures/GuildMember'); const { Role } = require('../structures/Role'); -const Events = require('../util/Events'); +const { Events, Opcodes } = require('../util/Constants'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Manages API methods for GuildMembers and stores their cache. @@ -114,7 +113,7 @@ class GuildMemberManager extends CachedManager { } resolvedOptions.roles = resolvedRoles; } - const data = await this.client.rest.put(Routes.guildMember(this.guild.id, userId), { body: resolvedOptions }); + const data = await this.client.api.guilds(this.guild.id).members(userId).put({ data: resolvedOptions }); // Data is an empty buffer if the member is already part of the guild. return data instanceof Buffer ? (options.fetchWhenExisting === false ? null : this.fetch(userId)) : this._add(data); } @@ -194,7 +193,7 @@ class GuildMemberManager extends CachedManager { * Options used for searching guild members. * @typedef {Object} GuildSearchMembersOptions * @property {string} query Filter members whose username or nickname start with this query - * @property {number} [limit] Maximum number of members to search + * @property {number} [limit=1] Maximum number of members to search * @property {boolean} [cache=true] Whether or not to cache the fetched member(s) */ @@ -203,10 +202,8 @@ class GuildMemberManager extends CachedManager { * @param {GuildSearchMembersOptions} options Options for searching members * @returns {Promise>} */ - async search({ query, limit, cache = true } = {}) { - const data = await this.client.api.guilds(this.guild.id).members.search.get({ - query: new URLSearchParams({ query, limit }), - }) + async search({ query, limit = 1, cache = true } = {}) { + const data = await this.client.api.guilds(this.guild.id).members.search.get({ query: { query, limit } }); return data.reduce((col, member) => col.set(member.user.id, this._add(member, cache)), new Collection()); } @@ -214,7 +211,7 @@ class GuildMemberManager extends CachedManager { * Options used for listing guild members. * @typedef {Object} GuildListMembersOptions * @property {Snowflake} [after] Limit fetching members to those with an id greater than the supplied id - * @property {number} [limit] Maximum number of members to list + * @property {number} [limit=1] Maximum number of members to list * @property {boolean} [cache=true] Whether or not to cache the fetched member(s) */ @@ -223,12 +220,8 @@ class GuildMemberManager extends CachedManager { * @param {GuildListMembersOptions} [options] Options for listing members * @returns {Promise>} */ - async list({ after, limit, cache = true } = {}) { - const query = new URLSearchParams({ limit }); - if (after) { - query.set('after', after); - } - const data = await this.client.api.guilds(this.guild.id).members.get({ query }); + async list({ after, limit = 1, cache = true } = {}) { + const data = await this.client.api.guilds(this.guild.id).members.get({ query: { after, limit } }); return data.reduce((col, member) => col.set(member.user.id, this._add(member, cache)), new Collection()); } @@ -273,10 +266,7 @@ class GuildMemberManager extends CachedManager { _data.roles &&= _data.roles.map(role => (role instanceof Role ? role.id : role)); _data.communication_disabled_until = - // eslint-disable-next-line eqeqeq - _data.communicationDisabledUntil != null - ? new Date(_data.communicationDisabledUntil).toISOString() - : _data.communicationDisabledUntil; + _data.communicationDisabledUntil && new Date(_data.communicationDisabledUntil).toISOString(); let endpoint = this.client.api.guilds(this.guild.id); if (id === this.client.user.id) { @@ -298,9 +288,9 @@ class GuildMemberManager extends CachedManager { * It's recommended to set {@link GuildPruneMembersOptions#count options.count} * to `false` for large guilds. * @typedef {Object} GuildPruneMembersOptions - * @property {number} [days] Number of days of inactivity required to kick + * @property {number} [days=7] Number of days of inactivity required to kick * @property {boolean} [dry=false] Get the number of users that will be kicked, without actually kicking them - * @property {boolean} [count] Whether or not to return the number of users that have been kicked. + * @property {boolean} [count=true] Whether or not to return the number of users that have been kicked. * @property {RoleResolvable[]} [roles] Array of roles to bypass the "...and no roles" constraint when pruning * @property {string} [reason] Reason for this prune */ @@ -325,7 +315,7 @@ class GuildMemberManager extends CachedManager { * .then(pruned => console.log(`I just pruned ${pruned} people!`)) * .catch(console.error); */ - async prune({ days, dry = false, count: compute_prune_count, roles = [], reason } = {}) { + async prune({ days = 7, dry = false, count: compute_prune_count = true, roles = [], reason } = {}) { if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE'); const query = { days }; @@ -346,8 +336,8 @@ class GuildMemberManager extends CachedManager { const endpoint = this.client.api.guilds(this.guild.id).prune; const { pruned } = await (dry - ? endpoint.get({ query: new URLSearchParams(query), reason }) - : endpoint.post({ body: { ...query, compute_prune_count }, reason })); + ? endpoint.get({ query, reason }) + : endpoint.post({ data: { ...query, compute_prune_count }, reason })); return pruned; } @@ -363,14 +353,14 @@ class GuildMemberManager extends CachedManager { * @example * // Kick a user by id (or with a user/guild member object) * guild.members.kick('84484653687267328') - * .then(kickInfo => console.log(`Kicked ${kickInfo.user?.tag ?? kickInfo.tag ?? kickInfo}`)) + * .then(banInfo => console.log(`Kicked ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`)) * .catch(console.error); */ async kick(user, reason) { const id = this.client.users.resolveId(user); if (!id) return Promise.reject(new TypeError('INVALID_TYPE', 'user', 'UserResolvable')); - await this.clinet.api.guilds(this.guild.id).members(id).delete({ reason }); + await this.client.api.guilds(this.guild.id).members(id).delete({ reason }); return this.resolve(user) ?? this.client.users.resolve(user) ?? id; } @@ -386,10 +376,10 @@ class GuildMemberManager extends CachedManager { * @example * // Ban a user by id (or with a user/guild member object) * guild.members.ban('84484653687267328') - * .then(banInfo => console.log(`Banned ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`)) + * .then(kickInfo => console.log(`Banned ${kickInfo.user?.tag ?? kickInfo.tag ?? kickInfo}`)) * .catch(console.error); */ - ban(user, options) { + ban(user, options = { days: 0 }) { return this.guild.bans.create(user, options); } @@ -424,13 +414,13 @@ class GuildMemberManager extends CachedManager { user: user_ids, query, time = 120e3, - nonce = DiscordSnowflake.generate().toString(), + nonce = SnowflakeUtil.generate(), } = {}) { return new Promise((resolve, reject) => { if (!query && !user_ids) query = ''; if (nonce.length > 32) throw new RangeError('MEMBER_FETCH_NONCE_LENGTH'); this.guild.shard.send({ - op: GatewayOpcodes.RequestGuildMembers, + op: Opcodes.REQUEST_GUILD_MEMBERS, d: { guild_id: this.guild.id, presences, @@ -451,7 +441,7 @@ class GuildMemberManager extends CachedManager { } if (members.size < 1_000 || (limit && fetchedMembers.size >= limit) || i === chunk.count) { clearTimeout(timeout); - this.client.removeListener(Events.GuildMembersChunk, handler); + this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler); this.client.decrementMaxListeners(); let fetched = fetchedMembers; if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first(); @@ -459,12 +449,12 @@ class GuildMemberManager extends CachedManager { } }; const timeout = setTimeout(() => { - this.client.removeListener(Events.GuildMembersChunk, handler); + this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler); this.client.decrementMaxListeners(); reject(new Error('GUILD_MEMBERS_TIMEOUT')); }, time).unref(); this.client.incrementMaxListeners(); - this.client.on(Events.GuildMembersChunk, handler); + this.client.on(Events.GUILD_MEMBERS_CHUNK, handler); }); } } diff --git a/src/managers/GuildMemberRoleManager.js b/src/managers/GuildMemberRoleManager.js index ce645b1..bdb60ae 100644 --- a/src/managers/GuildMemberRoleManager.js +++ b/src/managers/GuildMemberRoleManager.js @@ -1,7 +1,6 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { Routes } = require('discord-api-types/v9'); const DataManager = require('./DataManager'); const { TypeError } = require('../errors'); const { Role } = require('../structures/Role'); diff --git a/src/managers/GuildScheduledEventManager.js b/src/managers/GuildScheduledEventManager.js index 98fdb2b..b35e45c 100644 --- a/src/managers/GuildScheduledEventManager.js +++ b/src/managers/GuildScheduledEventManager.js @@ -1,11 +1,10 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { GuildScheduledEventEntityType, Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { TypeError, Error } = require('../errors'); const { GuildScheduledEvent } = require('../structures/GuildScheduledEvent'); -const DataResolver = require('../util/DataResolver'); +const { PrivacyLevels, GuildScheduledEventEntityTypes, GuildScheduledEventStatuses } = require('../util/Constants'); /** * Manages API methods for GuildScheduledEvents and stores their cache. @@ -41,17 +40,15 @@ class GuildScheduledEventManager extends CachedManager { * @property {string} name The name of the guild scheduled event * @property {DateResolvable} scheduledStartTime The time to schedule the event at * @property {DateResolvable} [scheduledEndTime] The time to end the event at - * This is required if `entityType` is {@link GuildScheduledEventEntityType.External} + * This is required if `entityType` is 'EXTERNAL' * @property {PrivacyLevel|number} privacyLevel The privacy level of the guild scheduled event * @property {GuildScheduledEventEntityType|number} entityType The scheduled entity type of the event * @property {string} [description] The description of the guild scheduled event * @property {GuildVoiceChannelResolvable} [channel] The channel of the guild scheduled event - * This is required if `entityType` is {@link GuildScheduledEventEntityType.StageInstance} or - * {@link GuildScheduledEventEntityType.Voice} + * This is required if `entityType` is 'STAGE_INSTANCE' or `VOICE` * @property {GuildScheduledEventEntityMetadataOptions} [entityMetadata] The entity metadata of the * guild scheduled event - * This is required if `entityType` is {@link GuildScheduledEventEntityType.External} - * @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event + * This is required if `entityType` is 'EXTERNAL' * @property {string} [reason] The reason for creating the guild scheduled event */ @@ -59,7 +56,7 @@ class GuildScheduledEventManager extends CachedManager { * Options used to set entity metadata of a guild scheduled event. * @typedef {Object} GuildScheduledEventEntityMetadataOptions * @property {string} [location] The location of the guild scheduled event - * This is required if `entityType` is {@link GuildScheduledEventEntityType.External} + * This is required if `entityType` is 'EXTERNAL' */ /** @@ -79,11 +76,13 @@ class GuildScheduledEventManager extends CachedManager { scheduledEndTime, entityMetadata, reason, - image, } = options; + if (typeof privacyLevel === 'string') privacyLevel = PrivacyLevels[privacyLevel]; + if (typeof entityType === 'string') entityType = GuildScheduledEventEntityTypes[entityType]; + let entity_metadata, channel_id; - if (entityType === GuildScheduledEventEntityType.External) { + if (entityType === GuildScheduledEventEntityTypes.EXTERNAL) { channel_id = typeof channel === 'undefined' ? channel : null; entity_metadata = { location: entityMetadata?.location }; } else { @@ -93,7 +92,7 @@ class GuildScheduledEventManager extends CachedManager { } const data = await this.client.api.guilds(this.guild.id, 'scheduled-events').post({ - body: { + data: { channel_id, name, privacy_level: privacyLevel, @@ -102,10 +101,9 @@ class GuildScheduledEventManager extends CachedManager { description, entity_type: entityType, entity_metadata, - image: image && (await DataResolver.resolveImage(image)), }, reason, - }) + }); return this._add(data); } @@ -140,15 +138,15 @@ class GuildScheduledEventManager extends CachedManager { if (existing) return existing; } - const data = await this.client.api.guilds(this.guild.id, 'scheduled-events', id).get({ - query: new URLSearchParams({ with_user_count: options.withUserCount ?? true }), - }) + const data = await this.client.api + .guilds(this.guild.id, 'scheduled-events', id) + .get({ query: { with_user_count: options.withUserCount ?? true } }); return this._add(data, options.cache); } - const data = await this.client.api.guilds(this.guild.id, 'scheduled-events').get({ - query: new URLSearchParams({ with_user_count: options.withUserCount ?? true }), - }) + const data = await this.client.api + .guilds(this.guild.id, 'scheduled-events') + .get({ query: { with_user_count: options.withUserCount ?? true } }); return data.reduce( (coll, rawGuildScheduledEventData) => @@ -173,9 +171,7 @@ class GuildScheduledEventManager extends CachedManager { * @property {GuildScheduledEventStatus|number} [status] The status of the guild scheduled event * @property {GuildScheduledEventEntityMetadataOptions} [entityMetadata] The entity metadata of the * guild scheduled event - * This can be modified only if `entityType` of the `GuildScheduledEvent` to be edited is - * {@link GuildScheduledEventEntityType.External} - * @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event + * This can be modified only if `entityType` of the `GuildScheduledEvent` to be edited is 'EXTERNAL' * @property {string} [reason] The reason for editing the guild scheduled event */ @@ -201,9 +197,12 @@ class GuildScheduledEventManager extends CachedManager { scheduledEndTime, entityMetadata, reason, - image, } = options; + if (typeof privacyLevel === 'string') privacyLevel = PrivacyLevels[privacyLevel]; + if (typeof entityType === 'string') entityType = GuildScheduledEventEntityTypes[entityType]; + if (typeof status === 'string') status = GuildScheduledEventStatuses[status]; + let entity_metadata; if (entityMetadata) { entity_metadata = { @@ -212,7 +211,7 @@ class GuildScheduledEventManager extends CachedManager { } const data = await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).patch({ - body: { + data: { channel_id: typeof channel === 'undefined' ? channel : this.guild.channels.resolveId(channel), name, privacy_level: privacyLevel, @@ -221,11 +220,10 @@ class GuildScheduledEventManager extends CachedManager { description, entity_type: entityType, status, - image: image && (await DataResolver.resolveImage(image)), entity_metadata, }, reason, - }) + }); return this._add(data); } @@ -272,26 +270,8 @@ class GuildScheduledEventManager extends CachedManager { let { limit, withMember, before, after } = options; - const query = new URLSearchParams(); - - if (limit) { - query.set('limit', limit); - } - - if (typeof withMember !== 'undefined') { - query.set('with_member', withMember); - } - - if (before) { - query.set('before', before); - } - - if (after) { - query.set('after', after); - } - const data = await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).users.get({ - query, + query: { limit, with_member: withMember, before, after }, }); return data.reduce( diff --git a/src/managers/GuildStickerManager.js b/src/managers/GuildStickerManager.js index 29202cf..68abfb0 100644 --- a/src/managers/GuildStickerManager.js +++ b/src/managers/GuildStickerManager.js @@ -1,7 +1,6 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { TypeError } = require('../errors'); const MessagePayload = require('../structures/MessagePayload'); @@ -62,14 +61,11 @@ class GuildStickerManager extends CachedManager { if (!resolvedFile) throw new TypeError('REQ_RESOURCE_TYPE'); file = { ...resolvedFile, key: 'file' }; - const body = { name, tags, description: description ?? '' }; + const data = { name, tags, description: description ?? '' }; - const sticker = await this.client.api.guilds(this.guild.id).stickers.post({ - appendToFormData: true, - body, - files: [file], - reason, - }); + const sticker = await this.client.api + .guilds(this.guild.id) + .stickers.post({ data, files: [file], reason, dontUsePayloadJSON: true }); return this.client.actions.GuildStickerCreate.handle(this.guild, sticker).sticker; } @@ -110,7 +106,7 @@ class GuildStickerManager extends CachedManager { if (!stickerId) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable'); const d = await this.client.api.guilds(this.guild.id).stickers(stickerId).patch({ - body: data, + data, reason, }); @@ -165,19 +161,6 @@ class GuildStickerManager extends CachedManager { const data = await this.client.api.guilds(this.guild.id).stickers.get(); return new Collection(data.map(sticker => [sticker.id, this._add(sticker, cache)])); } - - /** - * Fetches the user who uploaded this sticker, if this is a guild sticker. - * @param {StickerResolvable} sticker The sticker to fetch the user for - * @returns {Promise} - */ - async fetchUser(sticker) { - sticker = this.resolve(sticker); - if (!sticker) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable'); - const data = await this.client.api.guilds(this.guild.id).stickers(sticker.id).get(); - sticker._patch(data); - return sticker.user; - } } module.exports = GuildStickerManager; diff --git a/src/managers/MessageManager.js b/src/managers/MessageManager.js index 9331d05..a1e9a8b 100644 --- a/src/managers/MessageManager.js +++ b/src/managers/MessageManager.js @@ -1,13 +1,11 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { TypeError } = require('../errors'); const { Message } = require('../structures/Message'); const MessagePayload = require('../structures/MessagePayload'); const Util = require('../util/Util'); -const DiscordAPIError = require('../rest/DiscordAPIError'); /** * Manages API methods for Messages and holds their cache. @@ -38,7 +36,7 @@ class MessageManager extends CachedManager { * 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] Number of messages to acquire + * @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 @@ -84,7 +82,7 @@ class MessageManager extends CachedManager { * .catch(console.error); */ async fetchPinned(cache = true) { - const data = await this.client.api.channels(this.channel.id).pins.get(); + 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; @@ -125,13 +123,14 @@ class MessageManager extends CachedManager { const messageId = this.resolveId(message); if (!messageId) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); - const { body, files } = await (options instanceof MessagePayload + const { data, files } = await (options instanceof MessagePayload ? options : MessagePayload.create(message instanceof Message ? message : this, options) ) - .resolveBody() + .resolveData() .resolveFiles(); - const d = await this.client.api.channels(this.channel.id).messages(messageId).patch({ body, files }); + 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(); @@ -157,27 +156,25 @@ class MessageManager extends CachedManager { /** * Pins a message to the channel's pinned messages, even if it's not cached. * @param {MessageResolvable} message The message to pin - * @param {string} [reason] Reason for pinning * @returns {Promise} */ - async pin(message, reason) { + 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({ reason }); + 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 - * @param {string} [reason] Reason for unpinning * @returns {Promise} */ - async unpin(message, reason) { + 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({ reason }); + await this.client.api.channels(this.channel.id).pins(message).delete(); } /** @@ -197,6 +194,7 @@ class MessageManager extends CachedManager { ? `${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(); } @@ -218,14 +216,12 @@ class MessageManager extends CachedManager { if (existing && !existing.partial) return existing; } - const data = await this.client.api.channels(this.channel.id).messages(messageId).get(); + const data = await this.client.api.channels[this.channel.id].messages[messageId].get(); return this._add(data, cache); } async _fetchMany(options = {}, cache) { - const data = await this.client.api.channels(this.channel.id).messages.get({ - query: new URLSearchParams(options), - }); + 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; diff --git a/src/managers/PermissionOverwriteManager.js b/src/managers/PermissionOverwriteManager.js index 1dfb767..8dbc881 100644 --- a/src/managers/PermissionOverwriteManager.js +++ b/src/managers/PermissionOverwriteManager.js @@ -2,11 +2,11 @@ const process = require('node:process'); const { Collection } = require('@discordjs/collection'); -const { OverwriteType, Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { TypeError } = require('../errors'); const PermissionOverwrites = require('../structures/PermissionOverwrites'); const { Role } = require('../structures/Role'); +const { OverwriteTypes } = require('../util/Constants'); let cacheWarningEmitted = false; @@ -58,7 +58,7 @@ class PermissionOverwriteManager extends CachedManager { * message.channel.permissionOverwrites.set([ * { * id: message.author.id, - * deny: [PermissionsFlagsBit.ViewChannel], + * deny: [Permissions.FLAGS.VIEW_CHANNEL], * }, * ], 'Needed to change permissions'); */ @@ -94,15 +94,18 @@ class PermissionOverwriteManager extends CachedManager { if (typeof type !== 'number') { userOrRole = this.channel.guild.roles.resolve(userOrRole) ?? this.client.users.resolve(userOrRole); if (!userOrRole) throw new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role'); - type = userOrRole instanceof Role ? OverwriteType.Role : OverwriteType.Member; + type = userOrRole instanceof Role ? OverwriteTypes.role : OverwriteTypes.member; } const { allow, deny } = PermissionOverwrites.resolveOverwriteOptions(options, existing); - await this.client.api.channels(this.channel.id).permissions(userOrRoleId).put({ - body: { id: userOrRoleId, type, allow, deny }, - reason, - }); + await this.client.api + .channels(this.channel.id) + .permissions(userOrRoleId) + .put({ + data: { id: userOrRoleId, type, allow, deny }, + reason, + }); return this.channel; } diff --git a/src/managers/ReactionManager.js b/src/managers/ReactionManager.js index b30635e..b587a0e 100644 --- a/src/managers/ReactionManager.js +++ b/src/managers/ReactionManager.js @@ -1,6 +1,5 @@ 'use strict'; -const { Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const MessageReaction = require('../structures/MessageReaction'); diff --git a/src/managers/ReactionUserManager.js b/src/managers/ReactionUserManager.js index 41f6a03..cc86187 100644 --- a/src/managers/ReactionUserManager.js +++ b/src/managers/ReactionUserManager.js @@ -1,7 +1,6 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { Error } = require('../errors'); const User = require('../structures/User'); @@ -41,11 +40,9 @@ class ReactionUserManager extends CachedManager { */ async fetch({ limit = 100, after } = {}) { const message = this.reaction.message; - const query = new URLSearchParams({ limit }); - if (after) { - query.set('after', after); - } - const data = await this.client.api.channels(message.channelId).messages(message.id).reactions(this.reaction.emoji.identifier).get({ query }); + const data = await this.client.api.channels[message.channelId].messages[message.id].reactions[ + this.reaction.emoji.identifier + ].get({ query: { limit, after } }); const users = new Collection(); for (const rawUser of data) { const user = this.client.users._add(rawUser); @@ -64,10 +61,9 @@ class ReactionUserManager extends CachedManager { const userId = this.client.users.resolveId(user); if (!userId) throw new Error('REACTION_RESOLVE_USER'); const message = this.reaction.message; - await this.client.api.channels[message.channelId] - .messages[message.id] - .reactions[this.reaction.emoji.identifier][userId === this.client.user.id ? '@me' : userId] - .delete(); + await this.client.api.channels[message.channelId].messages[message.id].reactions[this.reaction.emoji.identifier][ + userId === this.client.user.id ? '@me' : userId + ].delete(); return this.reaction; } } diff --git a/src/managers/RoleManager.js b/src/managers/RoleManager.js index 1dd64eb..94f35b4 100644 --- a/src/managers/RoleManager.js +++ b/src/managers/RoleManager.js @@ -2,14 +2,12 @@ const process = require('node:process'); const { Collection } = require('@discordjs/collection'); -const { Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { TypeError } = require('../errors'); const { Role } = require('../structures/Role'); const DataResolver = require('../util/DataResolver'); -const PermissionsBitField = require('../util/PermissionsBitField'); -const { resolveColor } = require('../util/Util'); -const Util = require('../util/Util'); +const Permissions = require('../util/Permissions'); +const { resolveColor, setPosition } = require('../util/Util'); let cacheWarningEmitted = false; @@ -129,7 +127,7 @@ class RoleManager extends CachedManager { * // Create a new role with data and a reason * guild.roles.create({ * name: 'Super Cool Blue People', - * color: Colors.Blue, + * color: 'BLUE', * reason: 'we needed a role for Super Cool People', * }) * .then(console.log) @@ -138,7 +136,7 @@ class RoleManager extends CachedManager { async create(options = {}) { let { name, color, hoist, permissions, position, mentionable, reason, icon, unicodeEmoji } = options; color &&= resolveColor(color); - if (typeof permissions !== 'undefined') permissions = new PermissionsBitField(permissions); + if (typeof permissions !== 'undefined') permissions = new Permissions(permissions); if (icon) { const guildEmojiURL = this.guild.emojis.resolve(icon)?.url; icon = guildEmojiURL ? await DataResolver.resolveImage(guildEmojiURL) : await DataResolver.resolveImage(icon); @@ -146,7 +144,7 @@ class RoleManager extends CachedManager { } const data = await this.client.api.guilds(this.guild.id).roles.post({ - body: { + data: { name, color, hoist, @@ -156,12 +154,12 @@ class RoleManager extends CachedManager { unicode_emoji: unicodeEmoji, }, reason, - }) + }); const { role } = this.client.actions.GuildRoleCreate.handle({ guild_id: this.guild.id, role: data, }); - if (position) return this.setPosition(role, position, { reason }); + if (position) return role.setPosition(position, reason); return role; } @@ -182,7 +180,19 @@ class RoleManager extends CachedManager { if (!role) throw new TypeError('INVALID_TYPE', 'role', 'RoleResolvable'); if (typeof data.position === 'number') { - await this.setPosition(role, data.position, { reason }); + const updatedRoles = await setPosition( + role, + data.position, + false, + this.guild._sortedRoles(), + this.client.api.guilds(this.guild.id).roles, + reason, + ); + + this.client.actions.GuildRolesPositionUpdate.handle({ + guild_id: this.guild.id, + roles: updatedRoles, + }); } let icon = data.icon; @@ -192,17 +202,17 @@ class RoleManager extends CachedManager { if (typeof icon !== 'string') icon = undefined; } - const body = { + const _data = { name: data.name, color: typeof data.color === 'undefined' ? undefined : resolveColor(data.color), hoist: data.hoist, - permissions: typeof data.permissions === 'undefined' ? undefined : new PermissionsBitField(data.permissions), + permissions: typeof data.permissions === 'undefined' ? undefined : new Permissions(data.permissions), mentionable: data.mentionable, icon, unicode_emoji: data.unicodeEmoji, }; - const d = await this.client.api.guilds(this.guild.id).roles(role.id).patch({ body, reason }); + const d = await this.client.api.guilds(this.guild.id).roles(role.id).patch({ data: _data, reason }); const clone = role._clone(); clone._patch(d); @@ -217,54 +227,15 @@ class RoleManager extends CachedManager { * @example * // Delete a role * guild.roles.delete('222079219327434752', 'The role needed to go') - * .then(() => console.log('Deleted the role')) + * .then(deleted => console.log(`Deleted role ${deleted.name}`)) * .catch(console.error); */ async delete(role, reason) { const id = this.resolveId(role); - await this.client.api.guilds(this.guild.id).roles(id).delete({ reason }); + await this.client.api.guilds[this.guild.id].roles[id].delete({ reason }); this.client.actions.GuildRoleDelete.handle({ guild_id: this.guild.id, role_id: id }); } - /** - * Sets the new position of the role. - * @param {RoleResolvable} role The role to change the position of - * @param {number} position The new position for the role - * @param {SetRolePositionOptions} [options] Options for setting the position - * @returns {Promise} - * @example - * // Set the position of the role - * guild.roles.setPosition('222197033908436994', 1) - * .then(updated => console.log(`Role position: ${updated.position}`)) - * .catch(console.error); - */ - async setPosition(role, position, { relative, reason } = {}) { - role = this.resolve(role); - if (!role) throw new TypeError('INVALID_TYPE', 'role', 'RoleResolvable'); - const updatedRoles = await Util.setPosition( - role, - position, - relative, - this.guild._sortedRoles(), - this.client, - Routes.guildRoles(this.guild.id), - reason, - ); - - this.client.actions.GuildRolesPositionUpdate.handle({ - guild_id: this.guild.id, - roles: updatedRoles, - }); - return role; - } - - /** - * 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 @@ -282,7 +253,9 @@ class RoleManager extends CachedManager { })); // Call the API to update role positions - await this.client.api.guilds(this.guild.id).roles.patch({ body: rolePositions }); + await this.client.api.guilds(this.guild.id).roles.patch({ + data: rolePositions, + }); return this.client.actions.GuildRolesPositionUpdate.handle({ guild_id: this.guild.id, roles: rolePositions, diff --git a/src/managers/StageInstanceManager.js b/src/managers/StageInstanceManager.js index 68f1f77..478f26f 100644 --- a/src/managers/StageInstanceManager.js +++ b/src/managers/StageInstanceManager.js @@ -1,9 +1,9 @@ 'use strict'; -const { Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { TypeError, Error } = require('../errors'); const { StageInstance } = require('../structures/StageInstance'); +const { PrivacyLevels } = require('../util/Constants'); /** * Manages API methods for {@link StageInstance} objects and holds their cache. @@ -49,7 +49,7 @@ class StageInstanceManager extends CachedManager { * // Create a stage instance * guild.stageInstances.create('1234567890123456789', { * topic: 'A very creative topic', - * privacyLevel: GuildPrivacyLevel.GuildOnly + * privacyLevel: 'GUILD_ONLY' * }) * .then(stageInstance => console.log(stageInstance)) * .catch(console.error); @@ -60,8 +60,10 @@ class StageInstanceManager extends CachedManager { if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true); let { topic, privacyLevel } = options; + privacyLevel &&= typeof privacyLevel === 'number' ? privacyLevel : PrivacyLevels[privacyLevel]; + const data = await this.client.api['stage-instances'].post({ - body: { + data: { channel_id: channelId, topic, privacy_level: privacyLevel, @@ -120,12 +122,14 @@ class StageInstanceManager extends CachedManager { let { topic, privacyLevel } = options; + privacyLevel &&= typeof privacyLevel === 'number' ? privacyLevel : PrivacyLevels[privacyLevel]; + const data = await this.client.api('stage-instances', channelId).patch({ - body: { + data: { topic, privacy_level: privacyLevel, }, - }) + }); if (this.cache.has(data.id)) { const clone = this.cache.get(data.id)._clone(); diff --git a/src/managers/ThreadManager.js b/src/managers/ThreadManager.js index 34cd12e..9cd4f04 100644 --- a/src/managers/ThreadManager.js +++ b/src/managers/ThreadManager.js @@ -1,10 +1,10 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { ChannelType, Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { TypeError } = require('../errors'); const ThreadChannel = require('../structures/ThreadChannel'); +const { ChannelTypes } = require('../util/Constants'); /** * Manages API methods for {@link ThreadChannel} objects and stores their cache. @@ -64,12 +64,12 @@ class ThreadManager extends CachedManager { * @typedef {StartThreadOptions} ThreadCreateOptions * @property {MessageResolvable} [startMessage] The message to start a thread from. If this is defined then type * of thread gets automatically defined and cannot be changed. The provided `type` field will be ignored - * @property {ThreadChannelTypes|number} [type] The type of thread to create. - * Defaults to {@link ChannelType.GuildPublicThread} if created in a {@link TextChannel} - * When creating threads in a {@link NewsChannel} this is ignored and is always - * {@link ChannelType.GuildNewsThread} + * @property {ThreadChannelTypes|number} [type] The type of thread to create. Defaults to `GUILD_PUBLIC_THREAD` if + * created in a {@link TextChannel} When creating threads in a {@link NewsChannel} this is ignored and is always + * `GUILD_NEWS_THREAD` * @property {boolean} [invitable] Whether non-moderators can add other non-moderators to the thread - * Can only be set when type will be {@link ChannelType.GuildPrivateThread} + * Can only be set when type will be `GUILD_PRIVATE_THREAD` + * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the new channel in seconds */ /** @@ -92,7 +92,7 @@ class ThreadManager extends CachedManager { * .create({ * name: 'mod-talk', * autoArchiveDuration: 60, - * type: ChannelType.GuildPrivateThread, + * type: 'GUILD_PRIVATE_THREAD', * reason: 'Needed a separate thread for moderation', * }) * .then(threadChannel => console.log(threadChannel)) @@ -107,17 +107,18 @@ class ThreadManager extends CachedManager { reason, rateLimitPerUser, } = {}) { + let path = this.client.api.channels(this.channel.id); if (type && typeof type !== 'string' && typeof type !== 'number') { throw new TypeError('INVALID_TYPE', 'type', 'ThreadChannelType or Number'); } let resolvedType = - this.channel.type === ChannelType.GuildNews ? ChannelType.GuildNewsThread : ChannelType.GuildPublicThread; - let startMessageId; + this.channel.type === 'GUILD_NEWS' ? ChannelTypes.GUILD_NEWS_THREAD : ChannelTypes.GUILD_PUBLIC_THREAD; if (startMessage) { - startMessageId = this.channel.messages.resolveId(startMessage); + const startMessageId = this.channel.messages.resolveId(startMessage); if (!startMessageId) throw new TypeError('INVALID_TYPE', 'startMessage', 'MessageResolvable'); - } else if (this.channel.type !== ChannelType.GuildNews) { - resolvedType = type ?? resolvedType; + path = path.messages(startMessageId); + } else if (this.channel.type !== 'GUILD_NEWS') { + resolvedType = typeof type === 'string' ? ChannelTypes[type] : type ?? resolvedType; } if (autoArchiveDuration === 'MAX') { autoArchiveDuration = 1440; @@ -128,12 +129,12 @@ class ThreadManager extends CachedManager { } } - const data = await this.client.api.channels(this.channel.id).messages(startMessageId).threads.post({ - body: { + const data = await path.threads.post({ + data: { name, auto_archive_duration: autoArchiveDuration, type: resolvedType, - invitable: resolvedType === ChannelType.GuildPrivateThread ? invitable : undefined, + invitable: resolvedType === ChannelTypes.GUILD_PRIVATE_THREAD ? invitable : undefined, rate_limit_per_user: rateLimitPerUser, }, reason, @@ -211,31 +212,21 @@ class ThreadManager extends CachedManager { } let timestamp; let id; - const query = new URLSearchParams(); if (typeof before !== 'undefined') { if (before instanceof ThreadChannel || /^\d{16,19}$/.test(String(before))) { id = this.resolveId(before); timestamp = this.resolve(before)?.archivedAt?.toISOString(); - const toUse = type === 'private' && !fetchAll ? id : timestamp; - if (toUse) { - query.set('before', toUse); - } } else { try { timestamp = new Date(before).toISOString(); - if (type === 'public' || fetchAll) { - query.set('before', timestamp); - } } catch { throw new TypeError('INVALID_TYPE', 'before', 'DateResolvable or ThreadChannelResolvable'); } } } - - if (limit) { - query.set('limit', limit); - } - const raw = await path.threads.archived(type).get({ query }); + const raw = await path.threads + .archived(type) + .get({ query: { before: type === 'private' && !fetchAll ? id : timestamp, limit } }); return this.constructor._mapThreads(raw, this.client, { parent: this.channel, cache }); } diff --git a/src/managers/ThreadMemberManager.js b/src/managers/ThreadMemberManager.js index 366894d..ce6988e 100644 --- a/src/managers/ThreadMemberManager.js +++ b/src/managers/ThreadMemberManager.js @@ -1,7 +1,6 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { TypeError } = require('../errors'); const ThreadMember = require('../structures/ThreadMember'); @@ -104,22 +103,19 @@ class ThreadMemberManager extends CachedManager { } async _fetchMany(cache) { - const raw = await this.client.api.channels(this.thread.id, 'thread-members'); + const raw = await this.client.api.channels(this.thread.id, 'thread-members').get(); return raw.reduce((col, member) => col.set(member.user_id, this._add(member, cache)), new Collection()); } - /** - * @typedef {BaseFetchOptions} ThreadMemberFetchOptions - * @property {UserResolvable} [member] The specific user to fetch from the thread - */ - /** * Fetches member(s) for the thread from Discord, requires access to the `GUILD_MEMBERS` gateway intent. - * @param {ThreadMemberFetchOptions|boolean} [options] Additional options for this fetch, when a `boolean` is provided - * all members are fetched with `options.cache` set to the boolean value + * @param {UserResolvable|boolean} [member] The member to fetch. If `undefined`, all members + * in the thread are fetched, and will be cached based on `options.cache`. If boolean, this serves + * the purpose of `options.cache`. + * @param {BaseFetchOptions} [options] Additional options for this fetch * @returns {Promise>} */ - fetch({ member, cache = true, force = false } = {}) { + fetch(member, { cache = true, force = false } = {}) { const id = this.resolveId(member); return id ? this._fetchOne(id, cache, force) : this._fetchMany(member ?? cache); } diff --git a/src/managers/UserManager.js b/src/managers/UserManager.js index fc6b8e9..90d82af 100644 --- a/src/managers/UserManager.js +++ b/src/managers/UserManager.js @@ -1,6 +1,5 @@ 'use strict'; -const { ChannelType, Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { GuildMember } = require('../structures/GuildMember'); const { Message } = require('../structures/Message'); @@ -39,7 +38,7 @@ class UserManager extends CachedManager { * @private */ dmChannel(userId) { - return this.client.channels.cache.find(c => c.type === ChannelType.DM && c.recipient.id === userId) ?? null; + return this.client.channels.cache.find(c => c.type === 'DM' && c.recipient.id === userId) ?? null; } /** @@ -56,7 +55,11 @@ class UserManager extends CachedManager { if (dmChannel && !dmChannel.partial) return dmChannel; } - const data = await this.client.api.users('@me').channels.post({ body: { recipient_id: id } }); + const data = await this.client.api.users(this.client.user.id).channels.post({ + data: { + recipient_id: id, + }, + }); return this.client.channels._add(data, null, { cache }); } @@ -69,7 +72,7 @@ class UserManager extends CachedManager { const id = this.resolveId(user); const dmChannel = this.dmChannel(id); if (!dmChannel) throw new Error('USER_NO_DM_CHANNEL'); - await this.client.channels(dmChannel.id).delete(); + await this.client.api.channels(dmChannel.id).delete(); this.client.channels._remove(dmChannel.id); return dmChannel; } @@ -97,7 +100,7 @@ class UserManager extends CachedManager { * Fetches a user's flags. * @param {UserResolvable} user The UserResolvable to identify * @param {BaseFetchOptions} [options] Additional options for this fetch - * @returns {Promise} + * @returns {Promise} */ async fetchFlags(user, options) { return (await this.fetch(user, options)).flags; diff --git a/src/rest/APIRequest.js b/src/rest/APIRequest.js index 4a73346..d3b5d5e 100644 --- a/src/rest/APIRequest.js +++ b/src/rest/APIRequest.js @@ -2,7 +2,8 @@ const https = require('node:https'); const { setTimeout } = require('node:timers'); -const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); +const FormData = require('form-data'); +const fetch = require('node-fetch'); const { UserAgent } = require('../util/Constants'); let agent = null; @@ -16,6 +17,9 @@ class APIRequest { this.options = options; this.retries = 0; + const { userAgentSuffix } = this.client.options; + this.fullUserAgent = `${UserAgent}${userAgentSuffix.length ? `, ${userAgentSuffix.join(', ')}` : ''}`; + let queryString = ''; if (options.query) { const query = Object.entries(options.query) @@ -26,7 +30,7 @@ class APIRequest { this.path = `${path}${queryString && `?${queryString}`}`; } - async make() { + make() { agent ??= new https.Agent({ ...this.client.options.http.agent, keepAlive: true }); const API = @@ -37,7 +41,7 @@ class APIRequest { let headers = { ...this.client.options.http.headers, - 'User-Agent': UserAgent, + 'User-Agent': this.fullUserAgent, }; if (this.options.auth !== false) headers.Authorization = this.rest.getAuth(); @@ -46,32 +50,23 @@ class APIRequest { let body; if (this.options.files?.length) { - body = new FormData(); - for (const [index, file] of this.options.files.entries()) { - if (file?.file) - body.append(file.key ?? `files[${index}]`, file.file, file.name); - } - if ( - typeof this.options.data !== 'undefined' || - typeof this.options.body !== 'undefined' - ) { - if (this.options.dontUsePayloadJSON) { - for (const [key, value] of Object.entries(this.options.data || this.options.body)) { - body.append(key, value); - } - } else { - body.append('payload_json', JSON.stringify(this.options.data || this.options.body)); - } - } - headers = Object.assign(headers, body.getHeaders()); - // eslint-disable-next-line eqeqeq - } else if (this.options.data != null) { - body = JSON.stringify(this.options.data); - headers['Content-Type'] = 'application/json'; - } else if (this.options.body != null) { - body = JSON.stringify(this.options.body); - headers['Content-Type'] = 'application/json'; - } + body = new FormData(); + for (const [index, file] of this.options.files.entries()) { + if (file?.file) body.append(file.key ?? `files[${index}]`, file.file, file.name); + } + if (typeof this.options.data !== 'undefined') { + if (this.options.dontUsePayloadJSON) { + for (const [key, value] of Object.entries(this.options.data)) body.append(key, value); + } else { + body.append('payload_json', JSON.stringify(this.options.data)); + } + } + headers = Object.assign(headers, body.getHeaders()); + // eslint-disable-next-line eqeqeq + } else if (this.options.data != null) { + body = JSON.stringify(this.options.data); + headers['Content-Type'] = 'application/json'; + } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), this.client.options.restRequestTimeout).unref(); @@ -85,4 +80,4 @@ class APIRequest { } } -module.exports = APIRequest; \ No newline at end of file +module.exports = APIRequest; diff --git a/src/rest/APIRouter.js b/src/rest/APIRouter.js index 6923a30..b22b37f 100644 --- a/src/rest/APIRouter.js +++ b/src/rest/APIRouter.js @@ -50,4 +50,4 @@ function buildRoute(manager) { return new Proxy(noop, handler); } -module.exports = buildRoute; \ No newline at end of file +module.exports = buildRoute; diff --git a/src/rest/DiscordAPIError.js b/src/rest/DiscordAPIError.js index 4e75562..3137b7b 100644 --- a/src/rest/DiscordAPIError.js +++ b/src/rest/DiscordAPIError.js @@ -79,4 +79,4 @@ module.exports = DiscordAPIError; /** * @external APIError * @see {@link https://discord.com/developers/docs/reference#error-messages} - */ \ No newline at end of file + */ diff --git a/src/rest/HTTPError.js b/src/rest/HTTPError.js index cec44e9..0e9ab9f 100644 --- a/src/rest/HTTPError.js +++ b/src/rest/HTTPError.js @@ -58,4 +58,4 @@ class HTTPError extends Error { } } -module.exports = HTTPError; \ No newline at end of file +module.exports = HTTPError; diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js index 84cecf2..485b7bb 100644 --- a/src/rest/RESTManager.js +++ b/src/rest/RESTManager.js @@ -29,9 +29,11 @@ class RESTManager { } getAuth() { - const token = this.client.token ?? this.client.accessToken; - if (token && !this.client.bot) return `${token}`; - else if(token && this.client.bot) return `Bot ${token}`; + if (this.client.token && this.client.user && this.client.user.bot) { + return `Bot ${this.client.token}`; + } else if (this.client.token) { + return this.client.token; + } throw new Error('TOKEN_MISSING'); } @@ -60,4 +62,4 @@ class RESTManager { } } -module.exports = RESTManager; \ No newline at end of file +module.exports = RESTManager; diff --git a/src/rest/RateLimitError.js b/src/rest/RateLimitError.js index 0be61d0..494954c 100644 --- a/src/rest/RateLimitError.js +++ b/src/rest/RateLimitError.js @@ -52,4 +52,4 @@ class RateLimitError extends Error { } } -module.exports = RateLimitError; \ No newline at end of file +module.exports = RateLimitError; diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 59d197f..355e0ea 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -376,4 +376,4 @@ module.exports = RequestHandler; /** * @external Response * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Response} - */ \ No newline at end of file + */ diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index ceaab75..67d9d03 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -3,7 +3,7 @@ const EventEmitter = require('node:events'); const path = require('node:path'); const process = require('node:process'); -const { setTimeout, clearTimeout } = require('node:timers'); +const { setTimeout } = require('node:timers'); const { setTimeout: sleep } = require('node:timers/promises'); const { Error } = require('../errors'); const Util = require('../util/Util'); @@ -249,18 +249,14 @@ class Shard extends EventEmitter { const listener = message => { if (message?._fetchProp !== prop) return; child.removeListener('message', listener); - this.decrementMaxListeners(child); this._fetches.delete(prop); if (!message._error) resolve(message._result); else reject(Util.makeError(message._error)); }; - - this.incrementMaxListeners(child); child.on('message', listener); this.send({ _fetchProp: prop }).catch(err => { child.removeListener('message', listener); - this.decrementMaxListeners(child); this._fetches.delete(prop); reject(err); }); @@ -292,18 +288,14 @@ class Shard extends EventEmitter { const listener = message => { if (message?._eval !== _eval) return; child.removeListener('message', listener); - this.decrementMaxListeners(child); this._evals.delete(_eval); if (!message._error) resolve(message._result); else reject(Util.makeError(message._error)); }; - - this.incrementMaxListeners(child); child.on('message', listener); this.send({ _eval }).catch(err => { child.removeListener('message', listener); - this.decrementMaxListeners(child); this._evals.delete(_eval); reject(err); }); @@ -414,30 +406,6 @@ class Shard extends EventEmitter { if (respawn) this.spawn(timeout).catch(err => this.emit('error', err)); } - - /** - * Increments max listeners by one for a given emitter, if they are not zero. - * @param {EventEmitter|process} emitter The emitter that emits the events. - * @private - */ - incrementMaxListeners(emitter) { - const maxListeners = emitter.getMaxListeners(); - if (maxListeners !== 0) { - emitter.setMaxListeners(maxListeners + 1); - } - } - - /** - * Decrements max listeners by one for a given emitter, if they are not zero. - * @param {EventEmitter|process} emitter The emitter that emits the events. - * @private - */ - decrementMaxListeners(emitter) { - const maxListeners = emitter.getMaxListeners(); - if (maxListeners !== 0) { - emitter.setMaxListeners(maxListeners - 1); - } - } } module.exports = Shard; diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 0772af6..81cba42 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -2,7 +2,7 @@ const process = require('node:process'); const { Error } = require('../errors'); -const Events = require('../util/Events'); +const { Events } = require('../util/Constants'); const Util = require('../util/Util'); /** @@ -111,16 +111,13 @@ class ShardClientUtil { const listener = message => { if (message?._sFetchProp !== prop || message._sFetchPropShard !== shard) return; parent.removeListener('message', listener); - this.decrementMaxListeners(parent); if (!message._error) resolve(message._result); else reject(Util.makeError(message._error)); }; - this.incrementMaxListeners(parent); parent.on('message', listener); this.send({ _sFetchProp: prop, _sFetchPropShard: shard }).catch(err => { parent.removeListener('message', listener); - this.decrementMaxListeners(parent); reject(err); }); }); @@ -149,15 +146,13 @@ class ShardClientUtil { const listener = message => { if (message?._sEval !== script || message._sEvalShard !== options.shard) return; parent.removeListener('message', listener); - this.decrementMaxListeners(parent); if (!message._error) resolve(message._result); else reject(Util.makeError(message._error)); }; - this.incrementMaxListeners(parent); parent.on('message', listener); + this.send({ _sEval: script, _sEvalShard: options.shard }).catch(err => { parent.removeListener('message', listener); - this.decrementMaxListeners(parent); reject(err); }); }); @@ -210,13 +205,10 @@ class ShardClientUtil { error.stack = err.stack; /** * Emitted when the client encounters an error. - * Errors thrown within this event do not have a catch handler, it is - * recommended to not use async functions as `error` event handlers. See the - * [Node.js docs](https://nodejs.org/api/events.html#capture-rejections-of-promises) for details. * @event Client#error * @param {Error} error The error encountered */ - this.client.emit(Events.Error, error); + this.client.emit(Events.ERROR, error); }); } @@ -231,7 +223,7 @@ class ShardClientUtil { this._singleton = new this(client, mode); } else { client.emit( - Events.Warn, + Events.WARN, 'Multiple clients created in child process/worker; only the first will handle sharding helpers.', ); } @@ -249,30 +241,6 @@ class ShardClientUtil { if (shard < 0) throw new Error('SHARDING_SHARD_MISCALCULATION', shard, guildId, shardCount); return shard; } - - /** - * Increments max listeners by one for a given emitter, if they are not zero. - * @param {EventEmitter|process} emitter The emitter that emits the events. - * @private - */ - incrementMaxListeners(emitter) { - const maxListeners = emitter.getMaxListeners(); - if (maxListeners !== 0) { - emitter.setMaxListeners(maxListeners + 1); - } - } - - /** - * Decrements max listeners by one for a given emitter, if they are not zero. - * @param {EventEmitter|process} emitter The emitter that emits the events. - * @private - */ - decrementMaxListeners(emitter) { - const maxListeners = emitter.getMaxListeners(); - if (maxListeners !== 0) { - emitter.setMaxListeners(maxListeners - 1); - } - } } module.exports = ShardClientUtil; diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index db8581c..5ddf5ab 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -36,7 +36,7 @@ class ShardingManager extends EventEmitter { * @property {boolean} [respawn=true] Whether shards should automatically respawn upon exiting * @property {string[]} [shardArgs=[]] Arguments to pass to the shard script when spawning * (only available when mode is set to 'process') - * @property {string[]} [execArgv=[]] Arguments to pass to the shard script executable when spawning + * @property {string} [execArgv=[]] Arguments to pass to the shard script executable when spawning * (only available when mode is set to 'process') * @property {string} [token] Token to use for automatic shard count and passing to shards */ diff --git a/src/structures/ActionRow.js b/src/structures/ActionRow.js deleted file mode 100644 index 450a9f3..00000000 --- a/src/structures/ActionRow.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const { ActionRow: BuildersActionRow } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); - -class ActionRow extends BuildersActionRow { - constructor(data) { - super(Transformers.toSnakeCase(data)); - } -} - -module.exports = ActionRow; diff --git a/src/structures/AnonymousGuild.js b/src/structures/AnonymousGuild.js index b919d82..5415e54 100644 --- a/src/structures/AnonymousGuild.js +++ b/src/structures/AnonymousGuild.js @@ -1,6 +1,7 @@ 'use strict'; const BaseGuild = require('./BaseGuild'); +const { VerificationLevels, NSFWLevels } = require('../util/Constants'); /** * Bundles common attributes and methods between {@link Guild} and {@link InviteGuild} @@ -43,9 +44,9 @@ class AnonymousGuild extends BaseGuild { if ('verification_level' in data) { /** * The verification level of the guild - * @type {GuildVerificationLevel} + * @type {VerificationLevel} */ - this.verificationLevel = data.verification_level; + this.verificationLevel = VerificationLevels[data.verification_level]; } if ('vanity_url_code' in data) { @@ -59,36 +60,28 @@ class AnonymousGuild extends BaseGuild { if ('nsfw_level' in data) { /** * The NSFW level of this guild - * @type {GuildNSFWLevel} + * @type {NSFWLevel} */ - this.nsfwLevel = data.nsfw_level; - } - - if ('premium_subscription_count' in data) { - /** - * The total number of boosts for this server - * @type {?number} - */ - this.premiumSubscriptionCount = data.premium_subscription_count; + this.nsfwLevel = NSFWLevels[data.nsfw_level]; } } /** * The URL to this guild's banner. - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {StaticImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - bannerURL(options = {}) { - return this.banner && this.client.rest.cdn.banner(this.id, this.banner, options); + bannerURL({ format, size } = {}) { + return this.banner && this.client.rest.cdn.Banner(this.id, this.banner, format, size); } /** * The URL to this guild's invite splash image. - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {StaticImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - splashURL(options = {}) { - return this.splash && this.client.rest.cdn.splash(this.id, this.splash, options); + splashURL({ format, size } = {}) { + return this.splash && this.client.rest.cdn.Splash(this.id, this.splash, format, size); } } diff --git a/src/structures/ApplicationCommand.js b/src/structures/ApplicationCommand.js index f43304d..ea4558e 100644 --- a/src/structures/ApplicationCommand.js +++ b/src/structures/ApplicationCommand.js @@ -1,9 +1,9 @@ 'use strict'; -const { DiscordSnowflake } = require('@sapphire/snowflake'); -const { ApplicationCommandOptionType } = require('discord-api-types/v9'); const Base = require('./Base'); const ApplicationCommandPermissionsManager = require('../managers/ApplicationCommandPermissionsManager'); +const { ApplicationCommandOptionTypes, ApplicationCommandTypes, ChannelTypes } = require('../util/Constants'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents an application command. @@ -48,7 +48,7 @@ class ApplicationCommand extends Base { * The type of this application command * @type {ApplicationCommandType} */ - this.type = data.type; + this.type = ApplicationCommandTypes[data.type]; this._patch(data); } @@ -103,7 +103,7 @@ class ApplicationCommand extends Base { * @readonly */ get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); + return SnowflakeUtil.timestampFrom(this.id); } /** @@ -127,13 +127,11 @@ class ApplicationCommand extends Base { /** * Data for creating or editing an application command. * @typedef {Object} ApplicationCommandData - * @property {string} name The name of the command, must be in all lowercase if type is - * {@link ApplicationCommandType.ChatInput} - * @property {string} description The description of the command, if type is {@link ApplicationCommandType.ChatInput} - * @property {ApplicationCommandType} [type=ApplicationCommandType.ChatInput] The type of the command + * @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=true] Whether the command is enabled by default when the app is added to a - * guild + * @property {boolean} [defaultPermission] Whether the command is enabled by default when the app is added to a guild */ /** @@ -143,21 +141,17 @@ class ApplicationCommand extends Base { * 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} type The type of the option + * @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 autocomplete interaction is enabled for a - * {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or - * {@link ApplicationCommandOptionType.Number} 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[]} [channelTypes] When the option type is channel, + * @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 {@link ApplicationCommandOptionType.Integer} or - * {@link ApplicationCommandOptionType.Number} option - * @property {number} [maxValue] The maximum value for an {@link ApplicationCommandOptionType.Integer} or - * {@link ApplicationCommandOptionType.Number} option + * @property {number} [minValue] The minimum value for an `INTEGER` or `NUMBER` option + * @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option */ /** @@ -239,12 +233,13 @@ class ApplicationCommand extends Base { 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) || - (command.type && command.type !== this.type) || + (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) || @@ -294,15 +289,14 @@ class ApplicationCommand extends Base { * @private */ static _optionEquals(existing, option, enforceOptionOrder = false) { + const optionType = typeof option.type === 'string' ? option.type : ApplicationCommandOptionTypes[option.type]; if ( option.name !== existing.name || - option.type !== existing.type || + optionType !== existing.type || option.description !== existing.description || option.autocomplete !== existing.autocomplete || - (option.required ?? - ([ApplicationCommandOptionType.Subcommand, ApplicationCommandOptionType.SubcommandGroup].includes(option.type) - ? undefined - : false)) !== existing.required || + (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 || @@ -331,7 +325,9 @@ class ApplicationCommand extends Base { } if (existing.channelTypes) { - const newTypes = option.channelTypes ?? option.channel_types; + 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; } @@ -350,17 +346,13 @@ class ApplicationCommand extends Base { * @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 autocomplete interaction is enabled for a - * {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or - * {@link ApplicationCommandOptionType.Number} option + * @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 {@link ApplicationCommandOptionType.Integer} or - * {@link ApplicationCommandOptionType.Number} option - * @property {number} [maxValue] The maximum value for an {@link ApplicationCommandOptionType.Integer} or - * {@link ApplicationCommandOptionType.Number} option + * @property {number} [minValue] The minimum value for an `INTEGER` or `NUMBER` option + * @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option */ /** @@ -378,23 +370,24 @@ class ApplicationCommand extends Base { * @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: option.type, + type: typeof option.type === 'number' && !received ? option.type : ApplicationCommandOptionTypes[option.type], name: option.name, description: option.description, required: - option.required ?? - (option.type === ApplicationCommandOptionType.Subcommand || - option.type === ApplicationCommandOptionType.SubcommandGroup - ? undefined - : false), + 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]: option.channelTypes ?? option.channel_types, + [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, }; diff --git a/src/structures/AutocompleteInteraction.js b/src/structures/AutocompleteInteraction.js index c05afcb..e942a6d 100644 --- a/src/structures/AutocompleteInteraction.js +++ b/src/structures/AutocompleteInteraction.js @@ -1,8 +1,8 @@ 'use strict'; -const { InteractionResponseType, Routes } = require('discord-api-types/v9'); const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver'); const Interaction = require('./Interaction'); +const { InteractionResponseTypes, ApplicationCommandOptionTypes } = require('../util/Constants'); /** * Represents an autocomplete interaction. @@ -30,12 +30,6 @@ class AutocompleteInteraction extends Interaction { */ this.commandName = data.data.name; - /** - * The invoked application command's type - * @type {ApplicationCommandType.ChatInput} - */ - this.commandType = data.data.type; - /** * Whether this interaction has already received a response * @type {boolean} @@ -46,7 +40,10 @@ class AutocompleteInteraction extends Interaction { * The options passed to the command * @type {CommandInteractionOptionResolver} */ - this.options = new CommandInteractionOptionResolver(this.client, data.data.options ?? []); + this.options = new CommandInteractionOptionResolver( + this.client, + data.data.options?.map(option => this.transformOption(option, data.data.resolved)) ?? [], + ); } /** @@ -58,6 +55,25 @@ class AutocompleteInteraction extends Interaction { return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null; } + /** + * Transforms an option received from the API. + * @param {APIApplicationCommandOption} option The received option + * @returns {CommandInteractionOption} + * @private + */ + transformOption(option) { + const result = { + name: option.name, + type: ApplicationCommandOptionTypes[option.type], + }; + + if ('value' in option) result.value = option.value; + if ('options' in option) result.options = option.options.map(opt => this.transformOption(opt)); + if ('focused' in option) result.focused = option.focused; + + return result; + } + /** * Sends results for the autocomplete of this interaction. * @param {ApplicationCommandOptionChoice[]} options The options for the autocomplete @@ -77,14 +93,14 @@ class AutocompleteInteraction extends Interaction { if (this.responded) throw new Error('INTERACTION_ALREADY_REPLIED'); await this.client.api.interactions(this.id, this.token).callback.post({ - body: { - type: InteractionResponseType.ApplicationCommandAutocompleteResult, + data: { + type: InteractionResponseTypes.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT, data: { choices: options, }, }, auth: false, - }) + }); this.responded = true; } } diff --git a/src/structures/BaseCommandInteraction.js b/src/structures/BaseCommandInteraction.js new file mode 100644 index 00000000..0ddaf6b --- /dev/null +++ b/src/structures/BaseCommandInteraction.js @@ -0,0 +1,195 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const Interaction = require('./Interaction'); +const InteractionWebhook = require('./InteractionWebhook'); +const InteractionResponses = require('./interfaces/InteractionResponses'); +const { ApplicationCommandOptionTypes } = require('../util/Constants'); + +/** + * Represents a command interaction. + * @extends {Interaction} + * @implements {InteractionResponses} + * @abstract + */ +class BaseCommandInteraction extends Interaction { + constructor(client, data) { + super(client, data); + + /** + * The id of the channel this interaction was sent in + * @type {Snowflake} + * @name BaseCommandInteraction#channelId + */ + + /** + * The invoked application command's id + * @type {Snowflake} + */ + this.commandId = data.data.id; + + /** + * The invoked application command's name + * @type {string} + */ + this.commandName = data.data.name; + + /** + * Whether the reply to this interaction has been deferred + * @type {boolean} + */ + this.deferred = false; + + /** + * Whether this interaction has already been replied to + * @type {boolean} + */ + this.replied = false; + + /** + * Whether the reply to this interaction is ephemeral + * @type {?boolean} + */ + this.ephemeral = null; + + /** + * An associated interaction webhook, can be used to further interact with this interaction + * @type {InteractionWebhook} + */ + this.webhook = new InteractionWebhook(this.client, this.applicationId, this.token); + } + + /** + * The invoked application command, if it was fetched before + * @type {?ApplicationCommand} + */ + get command() { + const id = this.commandId; + return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null; + } + + /** + * Represents the resolved data of a received command interaction. + * @typedef {Object} CommandInteractionResolvedData + * @property {Collection} [users] The resolved users + * @property {Collection} [members] The resolved guild members + * @property {Collection} [roles] The resolved roles + * @property {Collection} [channels] The resolved channels + * @property {Collection} [messages] The resolved messages + */ + + /** + * Transforms the resolved received from the API. + * @param {APIInteractionDataResolved} resolved The received resolved objects + * @returns {CommandInteractionResolvedData} + * @private + */ + transformResolved({ members, users, channels, roles, messages }) { + const result = {}; + + if (members) { + result.members = new Collection(); + for (const [id, member] of Object.entries(members)) { + const user = users[id]; + result.members.set(id, this.guild?.members._add({ user, ...member }) ?? member); + } + } + + if (users) { + result.users = new Collection(); + for (const user of Object.values(users)) { + result.users.set(user.id, this.client.users._add(user)); + } + } + + if (roles) { + result.roles = new Collection(); + for (const role of Object.values(roles)) { + result.roles.set(role.id, this.guild?.roles._add(role) ?? role); + } + } + + if (channels) { + result.channels = new Collection(); + for (const channel of Object.values(channels)) { + result.channels.set(channel.id, this.client.channels._add(channel, this.guild) ?? channel); + } + } + + if (messages) { + result.messages = new Collection(); + for (const message of Object.values(messages)) { + result.messages.set(message.id, this.channel?.messages?._add(message) ?? message); + } + } + + return result; + } + + /** + * Represents an option of a received command interaction. + * @typedef {Object} CommandInteractionOption + * @property {string} name The name of the option + * @property {ApplicationCommandOptionType} type The type of the option + * @property {boolean} [autocomplete] Whether the option is an autocomplete option + * @property {string|number|boolean} [value] The value of the option + * @property {CommandInteractionOption[]} [options] Additional options if this option is a + * subcommand (group) + * @property {User} [user] The resolved user + * @property {GuildMember|APIGuildMember} [member] The resolved member + * @property {GuildChannel|ThreadChannel|APIChannel} [channel] The resolved channel + * @property {Role|APIRole} [role] The resolved role + */ + + /** + * Transforms an option received from the API. + * @param {APIApplicationCommandOption} option The received option + * @param {APIInteractionDataResolved} resolved The resolved interaction data + * @returns {CommandInteractionOption} + * @private + */ + transformOption(option, resolved) { + const result = { + name: option.name, + type: ApplicationCommandOptionTypes[option.type], + }; + + if ('value' in option) result.value = option.value; + if ('options' in option) result.options = option.options.map(opt => this.transformOption(opt, resolved)); + + if (resolved) { + const user = resolved.users?.[option.value]; + if (user) result.user = this.client.users._add(user); + + const member = resolved.members?.[option.value]; + if (member) result.member = this.guild?.members._add({ user, ...member }) ?? member; + + const channel = resolved.channels?.[option.value]; + if (channel) result.channel = this.client.channels._add(channel, this.guild) ?? channel; + + const role = resolved.roles?.[option.value]; + if (role) result.role = this.guild?.roles._add(role) ?? role; + } + + return result; + } + + // These are here only for documentation purposes - they are implemented by InteractionResponses + /* eslint-disable no-empty-function */ + deferReply() {} + reply() {} + fetchReply() {} + editReply() {} + deleteReply() {} + followUp() {} +} + +InteractionResponses.applyToClass(BaseCommandInteraction, ['deferUpdate', 'update']); + +module.exports = BaseCommandInteraction; + +/* eslint-disable max-len */ +/** + * @external APIInteractionDataResolved + * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure} + */ diff --git a/src/structures/BaseGuild.js b/src/structures/BaseGuild.js index e9d43d5..a39c44e 100644 --- a/src/structures/BaseGuild.js +++ b/src/structures/BaseGuild.js @@ -1,8 +1,7 @@ 'use strict'; -const { DiscordSnowflake } = require('@sapphire/snowflake'); -const { Routes } = require('discord-api-types/v9'); const Base = require('./Base'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * The base class for {@link Guild}, {@link OAuth2Guild} and {@link InviteGuild}. @@ -33,7 +32,7 @@ class BaseGuild extends Base { /** * An array of features available to this guild - * @type {GuildFeature[]} + * @type {Features[]} */ this.features = data.features; } @@ -44,7 +43,7 @@ class BaseGuild extends Base { * @readonly */ get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); + return SnowflakeUtil.timestampFrom(this.id); } /** @@ -88,11 +87,12 @@ class BaseGuild extends Base { /** * The URL to this guild's icon. - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - iconURL(options = {}) { - return this.icon && this.client.rest.cdn.icon(this.id, this.icon, options); + iconURL({ format, size, dynamic } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.Icon(this.id, this.icon, format, size, dynamic); } /** @@ -100,9 +100,7 @@ class BaseGuild extends Base { * @returns {Promise} */ async fetch() { - const data = await this.client.api.guilds(this.id).get({ - query: new URLSearchParams({ with_counts: true }), - }); + const data = await this.client.api.guilds(this.id).get({ query: { with_counts: true } }); return this.client.guilds._add(data); } diff --git a/src/structures/BaseGuildTextChannel.js b/src/structures/BaseGuildTextChannel.js index d0f28bb..abb4075 100644 --- a/src/structures/BaseGuildTextChannel.js +++ b/src/structures/BaseGuildTextChannel.js @@ -1,9 +1,12 @@ 'use strict'; +const { Collection } = require('@discordjs/collection'); const GuildChannel = require('./GuildChannel'); +const Webhook = require('./Webhook'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const MessageManager = require('../managers/MessageManager'); const ThreadManager = require('../managers/ThreadManager'); +const DataResolver = require('../util/DataResolver'); /** * Represents a text-based guild channel on Discord. @@ -63,7 +66,7 @@ class BaseGuildTextChannel extends GuildChannel { * The timestamp when the last pinned message was pinned, if there was one * @type {?number} */ - this.lastPinTimestamp = data.last_pin_timestamp ? Date.parse(data.last_pin_timestamp) : null; + this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; } if ('default_auto_archive_duration' in data) { @@ -118,8 +121,11 @@ class BaseGuildTextChannel extends GuildChannel { * .then(hooks => console.log(`This channel has ${hooks.size} hooks`)) * .catch(console.error); */ - fetchWebhooks() { - return this.guild.channels.fetchWebhooks(this.id); + async fetchWebhooks() { + const data = await this.client.api.channels[this.id].webhooks.get(); + const hooks = new Collection(); + for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); + return hooks; } /** @@ -143,8 +149,18 @@ class BaseGuildTextChannel extends GuildChannel { * .then(console.log) * .catch(console.error) */ - createWebhook(name, options = {}) { - return this.guild.channels.createWebhook(this.id, name, options); + async createWebhook(name, { avatar, reason } = {}) { + if (typeof avatar === 'string' && !avatar.startsWith('data:')) { + avatar = await DataResolver.resolveImage(avatar); + } + const data = await this.client.api.channels[this.id].webhooks.post({ + data: { + name, + avatar, + }, + reason, + }); + return new Webhook(this.client, data); } /** @@ -162,28 +178,19 @@ class BaseGuildTextChannel extends GuildChannel { return this.edit({ topic }, reason); } - /** - * Data that can be resolved to an Application. This can be: - * * An Application - * * An Activity with associated Application - * * A Snowflake - * @typedef {Application|Snowflake} ApplicationResolvable - */ - /** * Options used to create an invite to a guild channel. * @typedef {Object} CreateInviteOptions - * @property {boolean} [temporary] Whether members that joined via the invite should be automatically + * @property {boolean} [temporary=false] Whether members that joined via the invite should be automatically * kicked after 24 hours if they have not yet received a role - * @property {number} [maxAge] How long the invite should last (in seconds, 0 for forever) - * @property {number} [maxUses] Maximum number of uses - * @property {boolean} [unique] Create a unique invite, or use an existing one with similar settings + * @property {number} [maxAge=86400] How long the invite should last (in seconds, 0 for forever) + * @property {number} [maxUses=0] Maximum number of uses + * @property {boolean} [unique=false] Create a unique invite, or use an existing one with similar settings * @property {UserResolvable} [targetUser] The user whose stream to display for this invite, - * required if `targetType` is {@link InviteTargetType.Stream}, the user must be streaming in the channel + * required if `targetType` is 1, the user must be streaming in the channel * @property {ApplicationResolvable} [targetApplication] The embedded application to open for this invite, - * required if `targetType` is {@link InviteTargetType.Stream}, the application must have the - * {@link InviteTargetType.EmbeddedApplication} flag - * @property {InviteTargetType} [targetType] The type of the target for this voice channel invite + * required if `targetType` is 2, the application must have the `EMBEDDED` flag + * @property {TargetType} [targetType] The type of the target for this voice channel invite * @property {string} [reason] The reason for creating the invite */ diff --git a/src/structures/BaseGuildVoiceChannel.js b/src/structures/BaseGuildVoiceChannel.js index ad905cd..e048eb3 100644 --- a/src/structures/BaseGuildVoiceChannel.js +++ b/src/structures/BaseGuildVoiceChannel.js @@ -1,8 +1,8 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { PermissionFlagsBits } = require('discord-api-types/v9'); const GuildChannel = require('./GuildChannel'); +const Permissions = require('../util/Permissions'); /** * Represents a voice-based guild channel on Discord. @@ -72,11 +72,11 @@ class BaseGuildVoiceChannel extends GuildChannel { if (!permissions) return false; // This flag allows joining even if timed out - if (permissions.has(PermissionFlagsBits.Administrator, false)) return true; + if (permissions.has(Permissions.FLAGS.ADMINISTRATOR, false)) return true; return ( this.guild.me.communicationDisabledUntilTimestamp < Date.now() && - permissions.has(PermissionFlagsBits.Connect, false) + permissions.has(Permissions.FLAGS.CONNECT, false) ); } diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js new file mode 100644 index 00000000..c2470e0 --- /dev/null +++ b/src/structures/BaseMessageComponent.js @@ -0,0 +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; diff --git a/src/structures/ButtonComponent.js b/src/structures/ButtonComponent.js deleted file mode 100644 index a6cce10..00000000 --- a/src/structures/ButtonComponent.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const { ButtonComponent: BuildersButtonComponent } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); - -class ButtonComponent extends BuildersButtonComponent { - constructor(data) { - super(Transformers.toSnakeCase(data)); - } -} - -module.exports = ButtonComponent; diff --git a/src/structures/CategoryChannel.js b/src/structures/CategoryChannel.js index 6f53858..ce5b5f6 100644 --- a/src/structures/CategoryChannel.js +++ b/src/structures/CategoryChannel.js @@ -1,7 +1,6 @@ 'use strict'; const GuildChannel = require('./GuildChannel'); -const CategoryChannelChildManager = require('../managers/CategoryChannelChildManager'); /** * Represents a guild category channel on Discord. @@ -9,12 +8,12 @@ const CategoryChannelChildManager = require('../managers/CategoryChannelChildMan */ class CategoryChannel extends GuildChannel { /** - * A manager of the channels belonging to this category - * @type {CategoryChannelChildManager} + * Channels that are a part of this category + * @type {Collection} * @readonly */ get children() { - return new CategoryChannelChildManager(this); + return this.guild.channels.cache.filter(c => c.parentId === this.id); } /** @@ -27,6 +26,36 @@ class CategoryChannel extends GuildChannel { * @param {SetParentOptions} [options={}] The options for setting the parent * @returns {Promise} */ + + /** + * Options for creating a channel using {@link CategoryChannel#createChannel}. + * @typedef {Object} CategoryCreateChannelOptions + * @property {ChannelType|number} [type='GUILD_TEXT'] The type of the new channel. + * @property {string} [topic] The topic for the new channel + * @property {boolean} [nsfw] Whether the new channel is NSFW + * @property {number} [bitrate] Bitrate of the new channel in bits (only voice) + * @property {number} [userLimit] Maximum amount of users allowed in the new channel (only voice) + * @property {OverwriteResolvable[]|Collection} [permissionOverwrites] + * Permission overwrites of the new channel + * @property {number} [position] Position of the new channel + * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the new channel in seconds + * @property {string} [rtcRegion] The specific region of the new channel. + * @property {string} [reason] Reason for creating the new channel + */ + + /** + * Creates a new channel within this category. + * You cannot create a channel of type `GUILD_CATEGORY` inside a CategoryChannel. + * @param {string} name The name of the new channel + * @param {CategoryCreateChannelOptions} options Options for creating the new channel + * @returns {Promise} + */ + createChannel(name, options) { + return this.guild.channels.create(name, { + ...options, + parent: this.id, + }); + } } module.exports = CategoryChannel; diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 01086d3..cec50c7 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -1,9 +1,7 @@ 'use strict'; -const { DiscordSnowflake } = require('@sapphire/snowflake'); -const { ChannelType, Routes } = require('discord-api-types/v9'); +const process = require('node:process'); const Base = require('./Base'); -const { ThreadChannelTypes } = require('../util/Constants'); let CategoryChannel; let DMChannel; let NewsChannel; @@ -12,6 +10,16 @@ let StoreChannel; let TextChannel; let ThreadChannel; let VoiceChannel; +const { ChannelTypes, ThreadChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); + +/** + * @type {WeakSet} + * @private + * @internal + */ +const deletedChannels = new WeakSet(); +let deprecationEmittedForDeleted = false; /** * Represents any channel on Discord. @@ -22,11 +30,12 @@ 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 = data.type; + this.type = type ?? 'UNKNOWN'; if (data && immediatePatch) this._patch(data); } @@ -45,7 +54,7 @@ class Channel extends Base { * @readonly */ get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); + return SnowflakeUtil.timestampFrom(this.id); } /** @@ -58,12 +67,33 @@ class Channel extends Base { } /** - * The URL to the channel - * @type {string} - * @readonly + * 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 url() { - return `https://discord.com/channels/${this.isDMBased() ? '@me' : this.guildId}/${this.id}`; + 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); } /** @@ -111,59 +141,19 @@ class Channel extends Base { } /** - * Indicates whether this channel is a {@link TextChannel}. + * Indicates whether this channel is {@link TextBasedChannels text-based}. * @returns {boolean} */ isText() { - return this.type === ChannelType.GuildText; + return 'messages' in this; } /** - * Indicates whether this channel is a {@link DMChannel}. - * @returns {boolean} - */ - isDM() { - return this.type === ChannelType.DM; - } - - /** - * Indicates whether this channel is a {@link VoiceChannel}. + * Indicates whether this channel is {@link BaseGuildVoiceChannel voice-based}. * @returns {boolean} */ isVoice() { - return this.type === ChannelType.GuildVoice; - } - - /** - * Indicates whether this channel is a {@link PartialGroupDMChannel}. - * @returns {boolean} - */ - isGroupDM() { - return this.type === ChannelType.GroupDM; - } - - /** - * Indicates whether this channel is a {@link CategoryChannel}. - * @returns {boolean} - */ - isCategory() { - return this.type === ChannelType.GuildCategory; - } - - /** - * Indicates whether this channel is a {@link NewsChannel}. - * @returns {boolean} - */ - isNews() { - return this.type === ChannelType.GuildNews; - } - - /** - * Indicates whether this channel is a {@link StoreChannel}. - * @returns {boolean} - */ - isStore() { - return this.type === ChannelType.GuildStore; + return VoiceBasedChannelTypes.includes(this.type); } /** @@ -174,38 +164,6 @@ class Channel extends Base { return ThreadChannelTypes.includes(this.type); } - /** - * Indicates whether this channel is a {@link StageChannel}. - * @returns {boolean} - */ - isStage() { - return this.type === ChannelType.GuildStageVoice; - } - - /** - * Indicates whether this channel is {@link TextBasedChannels text-based}. - * @returns {boolean} - */ - isTextBased() { - return 'messages' in this; - } - - /** - * Indicates whether this channel is DM-based (either a {@link DMChannel} or a {@link PartialGroupDMChannel}). - * @returns {boolean} - */ - isDMBased() { - return [ChannelType.DM, ChannelType.GroupDM].includes(this.type); - } - - /** - * Indicates whether this channel is {@link BaseGuildVoiceChannel voice-based}. - * @returns {boolean} - */ - isVoiceBased() { - return 'bitrate' in this; - } - static create(client, data, guild, { allowUnknownGuild, fromInteraction } = {}) { CategoryChannel ??= require('./CategoryChannel'); DMChannel ??= require('./DMChannel'); @@ -218,9 +176,9 @@ class Channel extends Base { let channel; if (!data.guild_id && !guild) { - if ((data.recipients && data.type !== ChannelType.GroupDM) || data.type === ChannelType.DM) { + if ((data.recipients && data.type !== ChannelTypes.GROUP_DM) || data.type === ChannelTypes.DM) { channel = new DMChannel(client, data); - } else if (data.type === ChannelType.GroupDM) { + } else if (data.type === ChannelTypes.GROUP_DM) { const PartialGroupDMChannel = require('./PartialGroupDMChannel'); channel = new PartialGroupDMChannel(client, data); } @@ -229,33 +187,33 @@ class Channel extends Base { if (guild || allowUnknownGuild) { switch (data.type) { - case ChannelType.GuildText: { + case ChannelTypes.GUILD_TEXT: { channel = new TextChannel(guild, data, client); break; } - case ChannelType.GuildVoice: { + case ChannelTypes.GUILD_VOICE: { channel = new VoiceChannel(guild, data, client); break; } - case ChannelType.GuildCategory: { + case ChannelTypes.GUILD_CATEGORY: { channel = new CategoryChannel(guild, data, client); break; } - case ChannelType.GuildNews: { + case ChannelTypes.GUILD_NEWS: { channel = new NewsChannel(guild, data, client); break; } - case ChannelType.GuildStore: { + case ChannelTypes.GUILD_STORE: { channel = new StoreChannel(guild, data, client); break; } - case ChannelType.GuildStageVoice: { + case ChannelTypes.GUILD_STAGE_VOICE: { channel = new StageChannel(guild, data, client); break; } - case ChannelType.GuildNewsThread: - case ChannelType.GuildPublicThread: - case ChannelType.GuildPrivateThread: { + 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; @@ -273,6 +231,7 @@ class Channel extends Base { } exports.Channel = Channel; +exports.deletedChannels = deletedChannels; /** * @external APIChannel diff --git a/src/structures/ChatInputCommandInteraction.js b/src/structures/ChatInputCommandInteraction.js deleted file mode 100644 index 35175e4..00000000 --- a/src/structures/ChatInputCommandInteraction.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -const CommandInteraction = require('./CommandInteraction'); -const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver'); - -/** - * Represents a command interaction. - * @extends {CommandInteraction} - */ -class ChatInputCommandInteraction extends CommandInteraction { - constructor(client, data) { - super(client, data); - - /** - * The options passed to the command. - * @type {CommandInteractionOptionResolver} - */ - this.options = new CommandInteractionOptionResolver( - this.client, - data.data.options?.map(option => this.transformOption(option, data.data.resolved)) ?? [], - this.transformResolved(data.data.resolved ?? {}), - ); - } - - /** - * Returns a string representation of the command interaction. - * This can then be copied by a user and executed again in a new command while keeping the option order. - * @returns {string} - */ - toString() { - const properties = [ - this.commandName, - this.options._group, - this.options._subcommand, - ...this.options._hoistedOptions.map(o => `${o.name}:${o.value}`), - ]; - return `/${properties.filter(Boolean).join(' ')}`; - } -} - -module.exports = ChatInputCommandInteraction; diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index de8270e..80dfd36 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -3,8 +3,8 @@ const Team = require('./Team'); const { Error } = require('../errors/DJSError'); const Application = require('./interfaces/Application'); -const ApplicationFlagsBitField = require('../util/ApplicationFlagsBitField'); const ApplicationCommandManager = require('../managers/ApplicationCommandManager'); +const ApplicationFlags = require('../util/ApplicationFlags'); /** * Represents a Client OAuth2 Application. @@ -24,14 +24,12 @@ class ClientApplication extends Application { _patch(data) { super._patch(data); - if(!data) return; - if ('flags' in data) { /** * The flags this application has - * @type {ApplicationFlagsBitField} + * @type {ApplicationFlags} */ - this.flags = new ApplicationFlagsBitField(data.flags).freeze(); + this.flags = new ApplicationFlags(data.flags).freeze(); } if ('cover_image' in data) { diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index c4dc743..897fd8e 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -1,8 +1,8 @@ 'use strict'; -const { GatewayOpcodes } = require('discord-api-types/v9'); const { Presence } = require('./Presence'); const { TypeError } = require('../errors'); +const { ActivityTypes, Opcodes } = require('../util/Constants'); /** * Represents the client's presence. @@ -10,7 +10,7 @@ const { TypeError } = require('../errors'); */ class ClientPresence extends Presence { constructor(client, data = {}) { - super(client, Object.assign(data, { status: data.status || client.setting.status || 'online', user: { id: null } })); + super(client, Object.assign(data, { status: data.status ?? 'online', user: { id: null } })); } /** @@ -22,13 +22,13 @@ class ClientPresence extends Presence { const packet = this._parse(presence); this._patch(packet); if (typeof presence.shardId === 'undefined') { - this.client.ws.broadcast({ op: GatewayOpcodes.PresenceUpdate, d: packet }); + this.client.ws.broadcast({ op: Opcodes.STATUS_UPDATE, d: packet }); } else if (Array.isArray(presence.shardId)) { for (const shardId of presence.shardId) { - this.client.ws.shards.get(shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet }); + this.client.ws.shards.get(shardId).send({ op: Opcodes.STATUS_UPDATE, d: packet }); } } else { - this.client.ws.shards.get(presence.shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet }); + this.client.ws.shards.get(presence.shardId).send({ op: Opcodes.STATUS_UPDATE, d: packet }); } return this; } diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 30b82a4..9136752 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -1,6 +1,5 @@ 'use strict'; -const { Routes } = require('discord-api-types/v9'); const User = require('./User'); const DataResolver = require('../util/DataResolver'); @@ -159,7 +158,7 @@ class ClientUser extends User { * @returns {ClientPresence} * @example * // Set the client user's activity - * client.user.setActivity('discord.js', { type: ActivityType.Watching }); + * client.user.setActivity('discord.js', { type: 'WATCHING' }); */ setActivity(name, options = {}) { if (!name) return this.setPresence({ activities: [], shardId: options.shardId }); diff --git a/src/structures/CommandInteraction.js b/src/structures/CommandInteraction.js index 4987d09..c5dcfa2 100644 --- a/src/structures/CommandInteraction.js +++ b/src/structures/CommandInteraction.js @@ -1,216 +1,41 @@ 'use strict'; -const { Collection } = require('@discordjs/collection'); -const Interaction = require('./Interaction'); -const InteractionWebhook = require('./InteractionWebhook'); -const MessageAttachment = require('./MessageAttachment'); -const InteractionResponses = require('./interfaces/InteractionResponses'); +const BaseCommandInteraction = require('./BaseCommandInteraction'); +const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver'); /** * Represents a command interaction. - * @extends {Interaction} - * @implements {InteractionResponses} - * @abstract + * @extends {BaseCommandInteraction} */ -class CommandInteraction extends Interaction { +class CommandInteraction extends BaseCommandInteraction { constructor(client, data) { super(client, data); /** - * The id of the channel this interaction was sent in - * @type {Snowflake} - * @name CommandInteraction#channelId + * The options passed to the command. + * @type {CommandInteractionOptionResolver} */ - - /** - * The invoked application command's id - * @type {Snowflake} - */ - this.commandId = data.data.id; - - /** - * The invoked application command's name - * @type {string} - */ - this.commandName = data.data.name; - - /** - * The invoked application command's type - * @type {ApplicationCommandType} - */ - this.commandType = data.data.type; - - /** - * Whether the reply to this interaction has been deferred - * @type {boolean} - */ - this.deferred = false; - - /** - * Whether this interaction has already been replied to - * @type {boolean} - */ - this.replied = false; - - /** - * Whether the reply to this interaction is ephemeral - * @type {?boolean} - */ - this.ephemeral = null; - - /** - * An associated interaction webhook, can be used to further interact with this interaction - * @type {InteractionWebhook} - */ - this.webhook = new InteractionWebhook(this.client, this.applicationId, this.token); + this.options = new CommandInteractionOptionResolver( + this.client, + data.data.options?.map(option => this.transformOption(option, data.data.resolved)) ?? [], + this.transformResolved(data.data.resolved ?? {}), + ); } /** - * The invoked application command, if it was fetched before - * @type {?ApplicationCommand} + * Returns a string representation of the command interaction. + * This can then be copied by a user and executed again in a new command while keeping the option order. + * @returns {string} */ - get command() { - const id = this.commandId; - return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null; + toString() { + const properties = [ + this.commandName, + this.options._group, + this.options._subcommand, + ...this.options._hoistedOptions.map(o => `${o.name}:${o.value}`), + ]; + return `/${properties.filter(Boolean).join(' ')}`; } - - /** - * Represents the resolved data of a received command interaction. - * @typedef {Object} CommandInteractionResolvedData - * @property {Collection} [users] The resolved users - * @property {Collection} [members] The resolved guild members - * @property {Collection} [roles] The resolved roles - * @property {Collection} [channels] The resolved channels - * @property {Collection} [messages] The resolved messages - * @property {Collection} [attachments] The resolved attachments - */ - - /** - * Transforms the resolved received from the API. - * @param {APIInteractionDataResolved} resolved The received resolved objects - * @returns {CommandInteractionResolvedData} - * @private - */ - transformResolved({ members, users, channels, roles, messages, attachments }) { - const result = {}; - - if (members) { - result.members = new Collection(); - for (const [id, member] of Object.entries(members)) { - const user = users[id]; - result.members.set(id, this.guild?.members._add({ user, ...member }) ?? member); - } - } - - if (users) { - result.users = new Collection(); - for (const user of Object.values(users)) { - result.users.set(user.id, this.client.users._add(user)); - } - } - - if (roles) { - result.roles = new Collection(); - for (const role of Object.values(roles)) { - result.roles.set(role.id, this.guild?.roles._add(role) ?? role); - } - } - - if (channels) { - result.channels = new Collection(); - for (const channel of Object.values(channels)) { - result.channels.set(channel.id, this.client.channels._add(channel, this.guild) ?? channel); - } - } - - if (messages) { - result.messages = new Collection(); - for (const message of Object.values(messages)) { - result.messages.set(message.id, this.channel?.messages?._add(message) ?? message); - } - } - - if (attachments) { - result.attachments = new Collection(); - for (const attachment of Object.values(attachments)) { - const patched = new MessageAttachment(attachment.url, attachment.filename, attachment); - result.attachments.set(attachment.id, patched); - } - } - - return result; - } - - /** - * Represents an option of a received command interaction. - * @typedef {Object} CommandInteractionOption - * @property {string} name The name of the option - * @property {ApplicationCommandOptionType} type The type of the option - * @property {boolean} [autocomplete] Whether the autocomplete interaction is enabled for a - * {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or - * {@link ApplicationCommandOptionType.Number} option - * @property {string|number|boolean} [value] The value of the option - * @property {CommandInteractionOption[]} [options] Additional options if this option is a - * subcommand (group) - * @property {User} [user] The resolved user - * @property {GuildMember|APIGuildMember} [member] The resolved member - * @property {GuildChannel|ThreadChannel|APIChannel} [channel] The resolved channel - * @property {Role|APIRole} [role] The resolved role - * @property {MessageAttachment} [attachment] The resolved attachment - */ - - /** - * Transforms an option received from the API. - * @param {APIApplicationCommandOption} option The received option - * @param {APIInteractionDataResolved} resolved The resolved interaction data - * @returns {CommandInteractionOption} - * @private - */ - transformOption(option, resolved) { - const result = { - name: option.name, - type: option.type, - }; - - if ('value' in option) result.value = option.value; - if ('options' in option) result.options = option.options.map(opt => this.transformOption(opt, resolved)); - - if (resolved) { - const user = resolved.users?.[option.value]; - if (user) result.user = this.client.users._add(user); - - const member = resolved.members?.[option.value]; - if (member) result.member = this.guild?.members._add({ user, ...member }) ?? member; - - const channel = resolved.channels?.[option.value]; - if (channel) result.channel = this.client.channels._add(channel, this.guild) ?? channel; - - const role = resolved.roles?.[option.value]; - if (role) result.role = this.guild?.roles._add(role) ?? role; - - const attachment = resolved.attachments?.[option.value]; - if (attachment) result.attachment = new MessageAttachment(attachment.url, attachment.filename, attachment); - } - - return result; - } - - // These are here only for documentation purposes - they are implemented by InteractionResponses - /* eslint-disable no-empty-function */ - deferReply() {} - reply() {} - fetchReply() {} - editReply() {} - deleteReply() {} - followUp() {} } -InteractionResponses.applyToClass(CommandInteraction, ['deferUpdate', 'update']); - module.exports = CommandInteraction; - -/* eslint-disable max-len */ -/** - * @external APIInteractionDataResolved - * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure} - */ diff --git a/src/structures/CommandInteractionOptionResolver.js b/src/structures/CommandInteractionOptionResolver.js index 2aa6938..c83fd8e 100644 --- a/src/structures/CommandInteractionOptionResolver.js +++ b/src/structures/CommandInteractionOptionResolver.js @@ -1,6 +1,5 @@ 'use strict'; -const { ApplicationCommandOptionType } = require('discord-api-types/v9'); const { TypeError } = require('../errors'); /** @@ -39,12 +38,12 @@ class CommandInteractionOptionResolver { this._hoistedOptions = options; // Hoist subcommand group if present - if (this._hoistedOptions[0]?.type === ApplicationCommandOptionType.SubcommandGroup) { + if (this._hoistedOptions[0]?.type === 'SUB_COMMAND_GROUP') { this._group = this._hoistedOptions[0].name; this._hoistedOptions = this._hoistedOptions[0].options ?? []; } // Hoist subcommand if present - if (this._hoistedOptions[0]?.type === ApplicationCommandOptionType.Subcommand) { + if (this._hoistedOptions[0]?.type === 'SUB_COMMAND') { this._subcommand = this._hoistedOptions[0].name; this._hoistedOptions = this._hoistedOptions[0].options ?? []; } @@ -117,10 +116,10 @@ class CommandInteractionOptionResolver { /** * Gets the selected subcommand group. - * @param {boolean} [required=false] Whether to throw an error if there is no subcommand group. + * @param {boolean} [required=true] Whether to throw an error if there is no subcommand group. * @returns {?string} The name of the selected subcommand group, or null if not set and not required. */ - getSubcommandGroup(required = false) { + getSubcommandGroup(required = true) { if (required && !this._group) { throw new TypeError('COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP'); } @@ -134,7 +133,7 @@ class CommandInteractionOptionResolver { * @returns {?boolean} The value of the option, or null if not set and not required. */ getBoolean(name, required = false) { - const option = this._getTypedOption(name, ApplicationCommandOptionType.Boolean, ['value'], required); + const option = this._getTypedOption(name, 'BOOLEAN', ['value'], required); return option?.value ?? null; } @@ -146,7 +145,7 @@ class CommandInteractionOptionResolver { * The value of the option, or null if not set and not required. */ getChannel(name, required = false) { - const option = this._getTypedOption(name, ApplicationCommandOptionType.Channel, ['channel'], required); + const option = this._getTypedOption(name, 'CHANNEL', ['channel'], required); return option?.channel ?? null; } @@ -157,7 +156,7 @@ class CommandInteractionOptionResolver { * @returns {?string} The value of the option, or null if not set and not required. */ getString(name, required = false) { - const option = this._getTypedOption(name, ApplicationCommandOptionType.String, ['value'], required); + const option = this._getTypedOption(name, 'STRING', ['value'], required); return option?.value ?? null; } @@ -168,7 +167,7 @@ class CommandInteractionOptionResolver { * @returns {?number} The value of the option, or null if not set and not required. */ getInteger(name, required = false) { - const option = this._getTypedOption(name, ApplicationCommandOptionType.Integer, ['value'], required); + const option = this._getTypedOption(name, 'INTEGER', ['value'], required); return option?.value ?? null; } @@ -179,7 +178,7 @@ class CommandInteractionOptionResolver { * @returns {?number} The value of the option, or null if not set and not required. */ getNumber(name, required = false) { - const option = this._getTypedOption(name, ApplicationCommandOptionType.Number, ['value'], required); + const option = this._getTypedOption(name, 'NUMBER', ['value'], required); return option?.value ?? null; } @@ -190,18 +189,19 @@ class CommandInteractionOptionResolver { * @returns {?User} The value of the option, or null if not set and not required. */ getUser(name, required = false) { - const option = this._getTypedOption(name, ApplicationCommandOptionType.User, ['user'], required); + const option = this._getTypedOption(name, 'USER', ['user'], required); return option?.user ?? null; } /** * Gets a member option. * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. * @returns {?(GuildMember|APIGuildMember)} - * The value of the option, or null if the user is not present in the guild or the option is not set. + * The value of the option, or null if not set and not required. */ - getMember(name) { - const option = this._getTypedOption(name, ApplicationCommandOptionType.User, ['member'], false); + getMember(name, required = false) { + const option = this._getTypedOption(name, 'USER', ['member'], required); return option?.member ?? null; } @@ -212,21 +212,10 @@ class CommandInteractionOptionResolver { * @returns {?(Role|APIRole)} The value of the option, or null if not set and not required. */ getRole(name, required = false) { - const option = this._getTypedOption(name, ApplicationCommandOptionType.Role, ['role'], required); + const option = this._getTypedOption(name, 'ROLE', ['role'], required); return option?.role ?? null; } - /** - * Gets an attachment option. - * @param {string} name The name of the option. - * @param {boolean} [required=false] Whether to throw an error if the option is not found. - * @returns {?MessageAttachment} The value of the option, or null if not set and not required. - */ - getAttachment(name, required = false) { - const option = this._getTypedOption(name, ApplicationCommandOptionType.Attachment, ['attachment'], required); - return option?.attachment ?? null; - } - /** * Gets a mentionable option. * @param {string} name The name of the option. @@ -235,12 +224,7 @@ class CommandInteractionOptionResolver { * The value of the option, or null if not set and not required. */ getMentionable(name, required = false) { - const option = this._getTypedOption( - name, - ApplicationCommandOptionType.Mentionable, - ['user', 'member', 'role'], - required, - ); + const option = this._getTypedOption(name, 'MENTIONABLE', ['user', 'member', 'role'], required); return option?.member ?? option?.user ?? option?.role ?? null; } diff --git a/src/structures/ContextMenuCommandInteraction.js b/src/structures/ContextMenuInteraction.js similarity index 71% rename from src/structures/ContextMenuCommandInteraction.js rename to src/structures/ContextMenuInteraction.js index 360f97e..f4f6ef4 100644 --- a/src/structures/ContextMenuCommandInteraction.js +++ b/src/structures/ContextMenuInteraction.js @@ -1,14 +1,14 @@ 'use strict'; -const { ApplicationCommandOptionType } = require('discord-api-types/v9'); -const CommandInteraction = require('./CommandInteraction'); +const BaseCommandInteraction = require('./BaseCommandInteraction'); const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver'); +const { ApplicationCommandOptionTypes, ApplicationCommandTypes } = require('../util/Constants'); /** * Represents a context menu interaction. - * @extends {CommandInteraction} + * @extends {BaseCommandInteraction} */ -class ContextMenuCommandInteraction extends CommandInteraction { +class ContextMenuInteraction extends BaseCommandInteraction { constructor(client, data) { super(client, data); /** @@ -26,6 +26,12 @@ class ContextMenuCommandInteraction extends CommandInteraction { * @type {Snowflake} */ this.targetId = data.data.target_id; + + /** + * The type of the target of the interaction; either USER or MESSAGE + * @type {ApplicationCommandType} + */ + this.targetType = ApplicationCommandTypes[data.data.type]; } /** @@ -39,7 +45,7 @@ class ContextMenuCommandInteraction extends CommandInteraction { if (resolved.users?.[target_id]) { result.push( - this.transformOption({ name: 'user', type: ApplicationCommandOptionType.User, value: target_id }, resolved), + this.transformOption({ name: 'user', type: ApplicationCommandOptionTypes.USER, value: target_id }, resolved), ); } @@ -56,4 +62,4 @@ class ContextMenuCommandInteraction extends CommandInteraction { } } -module.exports = ContextMenuCommandInteraction; +module.exports = ContextMenuInteraction; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 5117a0e..555de4f 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -1,6 +1,5 @@ 'use strict'; -const { ChannelType } = require('discord-api-types/v9'); const { Channel } = require('./Channel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const MessageManager = require('../managers/MessageManager'); @@ -15,7 +14,7 @@ class DMChannel extends Channel { super(client, data); // Override the channel type so partials have a known type - this.type = ChannelType.DM; + this.type = 'DM'; /** * A manager of the messages belonging to this channel @@ -48,7 +47,7 @@ class DMChannel extends Channel { * The timestamp when the last pinned message was pinned, if there was one * @type {?number} */ - this.lastPinTimestamp = Date.parse(data.last_pin_timestamp); + this.lastPinTimestamp = new Date(data.last_pin_timestamp).getTime(); } else { this.lastPinTimestamp ??= null; } diff --git a/src/structures/Embed.js b/src/structures/Embed.js deleted file mode 100644 index b200237..00000000 --- a/src/structures/Embed.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const { Embed: BuildersEmbed } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); - -class Embed extends BuildersEmbed { - constructor(data) { - super(Transformers.toSnakeCase(data)); - } -} - -module.exports = Embed; diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 409d292..be7ae17 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -1,7 +1,16 @@ 'use strict'; -const { DiscordSnowflake } = require('@sapphire/snowflake'); +const process = require('node:process'); const Base = require('./Base'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); + +/** + * @type {WeakSet} + * @private + * @internal + */ +const deletedEmojis = new WeakSet(); +let deprecationEmittedForDeleted = false; /** * Represents raw emoji data from the API @@ -37,6 +46,36 @@ class Emoji extends Base { this.id = emoji.id; } + /** + * 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( + 'Emoji#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + return deletedEmojis.has(this); + } + + set deleted(value) { + if (!deprecationEmittedForDeleted) { + deprecationEmittedForDeleted = true; + process.emitWarning( + 'Emoji#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + if (value) deletedEmojis.add(this); + else deletedEmojis.delete(this); + } + /** * The identifier of this emoji, used for message reactions * @type {string} @@ -53,7 +92,7 @@ class Emoji extends Base { * @readonly */ get url() { - return this.id && this.client.rest.cdn.emoji(this.id, this.animated ? 'gif' : 'png'); + return this.id && this.client.rest.cdn.Emoji(this.id, this.animated ? 'gif' : 'png'); } /** @@ -62,7 +101,7 @@ class Emoji extends Base { * @readonly */ get createdTimestamp() { - return this.id && DiscordSnowflake.timestampFrom(this.id); + return this.id && SnowflakeUtil.timestampFrom(this.id); } /** @@ -101,6 +140,7 @@ class Emoji extends Base { } exports.Emoji = Emoji; +exports.deletedEmojis = deletedEmojis; /** * @external APIEmoji diff --git a/src/structures/Guild.js b/src/structures/Guild.js index a9b919a..9c00fb8 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -1,7 +1,7 @@ 'use strict'; +const process = require('node:process'); const { Collection } = require('@discordjs/collection'); -const { ChannelType, GuildPremiumTier, Routes } = require('discord-api-types/v9'); const AnonymousGuild = require('./AnonymousGuild'); const GuildAuditLogs = require('./GuildAuditLogs'); const GuildPreview = require('./GuildPreview'); @@ -9,7 +9,7 @@ const GuildTemplate = require('./GuildTemplate'); const Integration = require('./Integration'); const Webhook = require('./Webhook'); const WelcomeScreen = require('./WelcomeScreen'); -const { Error, TypeError } = require('../errors'); +const { Error } = require('../errors'); const GuildApplicationCommandManager = require('../managers/GuildApplicationCommandManager'); const GuildBanManager = require('../managers/GuildBanManager'); const GuildChannelManager = require('../managers/GuildChannelManager'); @@ -22,12 +22,31 @@ const PresenceManager = require('../managers/PresenceManager'); const RoleManager = require('../managers/RoleManager'); const StageInstanceManager = require('../managers/StageInstanceManager'); const VoiceStateManager = require('../managers/VoiceStateManager'); +const { + ChannelTypes, + DefaultMessageNotificationLevels, + PartialTypes, + VerificationLevels, + ExplicitContentFilterLevels, + Status, + MFALevels, + PremiumTiers, +} = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); -const Partials = require('../util/Partials'); -const Status = require('../util/Status'); -const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField'); +const SystemChannelFlags = require('../util/SystemChannelFlags'); const Util = require('../util/Util'); +let deprecationEmittedForSetChannelPositions = false; +let deprecationEmittedForSetRolePositions = false; +let deprecationEmittedForDeleted = false; + +/** + * @type {WeakSet} + * @private + * @internal + */ +const deletedGuilds = new WeakSet(); + /** * Represents a guild (or a server) on Discord. * It's recommended to see if a guild is available before performing operations or reading data from it. You can @@ -35,1132 +54,1130 @@ const Util = require('../util/Util'); * @extends {AnonymousGuild} */ class Guild extends AnonymousGuild { - constructor(client, data) { - super(client, data, false); - - /** - * A manager of the application commands belonging to this guild - * @type {GuildApplicationCommandManager} - */ - this.commands = new GuildApplicationCommandManager(this); - - /** - * 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; - } - - /** - * 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; - } - - 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 {GuildPremiumTier} - */ - this.premiumTier = data.premium_tier; - } - - 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 {GuildExplicitContentFilter} - */ - this.explicitContentFilter = data.explicit_content_filter; - } - - if ('mfa_level' in data) { - /** - * The required MFA level for this guild - * @type {MFALevel} - */ - this.mfaLevel = data.mfa_level; - } - - if ('joined_at' in data) { - /** - * The timestamp the client user joined the guild at - * @type {number} - */ - this.joinedTimestamp = Date.parse(data.joined_at); - } - - if ('default_message_notifications' in data) { - /** - * The default message notification level of the guild - * @type {GuildDefaultMessageNotifications} - */ - this.defaultMessageNotifications = 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 SystemChannelFlagsBitField( - 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 (this is `null` for all but the largest of guilds) - * 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; - } 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/reference#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); - } - - /** - * Positions of the guild [User Account] - * @type {number} - * @readonly - */ - get position() { - return ( - this.client.setting.guildMetadata.get(this.id.toString())?.guildIndex || - null - ); - } - - /** - * Folder Guilds - * @type {object} - * @readonly - */ - get folder() { - return this.client.setting.guildMetadata.get(this.id.toString()) || {}; - } - - /** - * The URL to this guild's discovery splash image. - * @param {ImageURLOptions} [options={}] Options for the image URL - * @returns {?string} - */ - discoverySplashURL(options = {}) { - return ( - this.discoverySplash && - this.client.rest.cdn.discoverySplash( - this.id, - this.discoverySplash, - options, - ) - ); - } - - /** - * 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(Partials.GuildMember) - ? 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 (this.premiumTier) { - case GuildPremiumTier.Tier1: - return 128_000; - case GuildPremiumTier.Tier2: - return 256_000; - case GuildPremiumTier.Tier3: - return 384_000; - default: - return 96_000; - } - } - - /** - * 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({ body: { 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 {?AuditLogEvent} [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; - - const query = new URLSearchParams(); - - if (options.before) { - query.set('before', options.before); - } - - if (options.limit) { - query.set('limit', options.limit); - } - - if (options.user) { - const id = this.client.users.resolveId(options.user); - if (!id) throw new TypeError('INVALID_TYPE', 'user', 'UserResolvable'); - query.set('user_id', id); - } - - if (options.type) { - query.set('action_type', options.type); - } - - const data = await this.client.api - .guilds(this.id) - ['audit-logs'].get({ query }); - 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 {GuildFeature[]} [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 = 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 = data.explicitContentFilter; - } - if (typeof data.defaultMessageNotifications !== 'undefined') { - _data.default_message_notifications = data.defaultMessageNotifications; - } - if (typeof data.systemChannelFlags !== 'undefined') { - _data.system_channel_flags = SystemChannelFlagsBitField.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({ body: _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({ - body: { - 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} 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); - } - + constructor(client, data) { + super(client, data, false); + + /** + * A manager of the application commands belonging to this guild + * @type {GuildApplicationCommandManager} + */ + this.commands = new GuildApplicationCommandManager(this); + + /** + * 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; + } + + /** + * 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; + } + } + + /** + * 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 @@ -1216,191 +1233,257 @@ class Guild extends AnonymousGuild { 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 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 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); - } + /** + * 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); + } - /** - * 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({ - body: { - enabled: settings.enabled, - channel_id: this.channels.resolveId(settings.channel), - }, - reason, - }); - return this; - } + /** + * Data that can be resolved to give a Category Channel object. This can be: + * * A CategoryChannel object + * * A Snowflake + * @typedef {CategoryChannel|Snowflake} CategoryChannelResolvable + */ - /** - * 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; - } + /** + * 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 + */ - /** - * 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; - } + /** + * 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', + ); - /** - * 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]))) - ); - } + deprecationEmittedForSetChannelPositions = true; + } - 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; - } + return this.channels.setPositions(channelPositions); + } - /** - * 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); - }, - }; - }; - } + /** + * 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 + */ - /** - * Creates a collection of this guild's roles, sorted by their position and ids. - * @returns {Collection} - * @private - */ - _sortedRoles() { - return Util.discordSort(this.roles.cache); - } + /** + * 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', + ); - /** - * 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 === ChannelType.GuildCategory; - const channelTypes = [ - ChannelType.GuildText, - ChannelType.GuildNews, - ChannelType.GuildStore, - ]; - return Util.discordSort( - this.channels.cache.filter( - (c) => - (channelTypes.includes(channel.type) - ? channelTypes.includes(c.type) - : c.type === channel.type) && - (category || c.parent === channel.parent), - ), - ); - } + 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]))) + ); + } + + 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; +exports.deletedGuilds = deletedGuilds; /** * @external APIGuild diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 199e9d1..3937184 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -1,32 +1,31 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { DiscordSnowflake } = require('@sapphire/snowflake'); -const { OverwriteType, AuditLogEvent } = require('discord-api-types/v9'); const { GuildScheduledEvent } = require('./GuildScheduledEvent'); const Integration = require('./Integration'); const Invite = require('./Invite'); const { StageInstance } = require('./StageInstance'); const { Sticker } = require('./Sticker'); const Webhook = require('./Webhook'); -const Partials = require('../util/Partials'); +const { OverwriteTypes, PartialTypes } = require('../util/Constants'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); const Util = require('../util/Util'); /** * The target type of an entry. Here are the available types: - * * Guild - * * Channel - * * User - * * Role - * * Invite - * * Webhook - * * Emoji - * * Message - * * Integration - * * StageInstance - * * Sticker - * * Thread - * * GuildScheduledEvent + * * GUILD + * * CHANNEL + * * USER + * * ROLE + * * INVITE + * * WEBHOOK + * * EMOJI + * * MESSAGE + * * INTEGRATION + * * STAGE_INSTANCE + * * STICKER + * * THREAD + * * GUILD_SCHEDULED_EVENT * @typedef {string} AuditLogTargetType */ @@ -36,21 +35,131 @@ const Util = require('../util/Util'); * @type {Object} */ const Targets = { - All: 'All', - Guild: 'Guild', - GuildScheduledEvent: 'GuildScheduledEvent', - Channel: 'Channel', - User: 'User', - Role: 'Role', - Invite: 'Invite', - Webhook: 'Webhook', - Emoji: 'Emoji', - Message: 'Message', - Integration: 'Integration', - StageInstance: 'StageInstance', - Sticker: 'Sticker', - Thread: 'Thread', - Unknown: 'Unknown', + ALL: 'ALL', + GUILD: 'GUILD', + GUILD_SCHEDULED_EVENT: 'GUILD_SCHEDULED_EVENT', + CHANNEL: 'CHANNEL', + USER: 'USER', + ROLE: 'ROLE', + INVITE: 'INVITE', + WEBHOOK: 'WEBHOOK', + EMOJI: 'EMOJI', + MESSAGE: 'MESSAGE', + INTEGRATION: 'INTEGRATION', + STAGE_INSTANCE: 'STAGE_INSTANCE', + STICKER: 'STICKER', + THREAD: 'THREAD', + UNKNOWN: 'UNKNOWN', +}; + +/** + * The action of an entry. Here are the available actions: + * * ALL: null + * * GUILD_UPDATE: 1 + * * CHANNEL_CREATE: 10 + * * CHANNEL_UPDATE: 11 + * * CHANNEL_DELETE: 12 + * * CHANNEL_OVERWRITE_CREATE: 13 + * * CHANNEL_OVERWRITE_UPDATE: 14 + * * CHANNEL_OVERWRITE_DELETE: 15 + * * MEMBER_KICK: 20 + * * MEMBER_PRUNE: 21 + * * MEMBER_BAN_ADD: 22 + * * MEMBER_BAN_REMOVE: 23 + * * MEMBER_UPDATE: 24 + * * MEMBER_ROLE_UPDATE: 25 + * * MEMBER_MOVE: 26 + * * MEMBER_DISCONNECT: 27 + * * BOT_ADD: 28, + * * ROLE_CREATE: 30 + * * ROLE_UPDATE: 31 + * * ROLE_DELETE: 32 + * * INVITE_CREATE: 40 + * * INVITE_UPDATE: 41 + * * INVITE_DELETE: 42 + * * WEBHOOK_CREATE: 50 + * * WEBHOOK_UPDATE: 51 + * * WEBHOOK_DELETE: 52 + * * EMOJI_CREATE: 60 + * * EMOJI_UPDATE: 61 + * * EMOJI_DELETE: 62 + * * MESSAGE_DELETE: 72 + * * MESSAGE_BULK_DELETE: 73 + * * MESSAGE_PIN: 74 + * * MESSAGE_UNPIN: 75 + * * INTEGRATION_CREATE: 80 + * * INTEGRATION_UPDATE: 81 + * * INTEGRATION_DELETE: 82 + * * STAGE_INSTANCE_CREATE: 83 + * * STAGE_INSTANCE_UPDATE: 84 + * * STAGE_INSTANCE_DELETE: 85 + * * STICKER_CREATE: 90 + * * STICKER_UPDATE: 91 + * * STICKER_DELETE: 92 + * * GUILD_SCHEDULED_EVENT_CREATE: 100 + * * GUILD_SCHEDULED_EVENT_UPDATE: 101 + * * GUILD_SCHEDULED_EVENT_DELETE: 102 + * * THREAD_CREATE: 110 + * * THREAD_UPDATE: 111 + * * THREAD_DELETE: 112 + * @typedef {?(number|string)} AuditLogAction + * @see {@link https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events} + */ + +/** + * All available actions keyed under their names to their numeric values. + * @name GuildAuditLogs.Actions + * @type {Object} + */ +const Actions = { + ALL: null, + GUILD_UPDATE: 1, + CHANNEL_CREATE: 10, + CHANNEL_UPDATE: 11, + CHANNEL_DELETE: 12, + CHANNEL_OVERWRITE_CREATE: 13, + CHANNEL_OVERWRITE_UPDATE: 14, + CHANNEL_OVERWRITE_DELETE: 15, + MEMBER_KICK: 20, + MEMBER_PRUNE: 21, + MEMBER_BAN_ADD: 22, + MEMBER_BAN_REMOVE: 23, + MEMBER_UPDATE: 24, + MEMBER_ROLE_UPDATE: 25, + MEMBER_MOVE: 26, + MEMBER_DISCONNECT: 27, + BOT_ADD: 28, + ROLE_CREATE: 30, + ROLE_UPDATE: 31, + ROLE_DELETE: 32, + INVITE_CREATE: 40, + INVITE_UPDATE: 41, + INVITE_DELETE: 42, + WEBHOOK_CREATE: 50, + WEBHOOK_UPDATE: 51, + WEBHOOK_DELETE: 52, + EMOJI_CREATE: 60, + EMOJI_UPDATE: 61, + EMOJI_DELETE: 62, + MESSAGE_DELETE: 72, + MESSAGE_BULK_DELETE: 73, + MESSAGE_PIN: 74, + MESSAGE_UNPIN: 75, + INTEGRATION_CREATE: 80, + INTEGRATION_UPDATE: 81, + INTEGRATION_DELETE: 82, + STAGE_INSTANCE_CREATE: 83, + STAGE_INSTANCE_UPDATE: 84, + STAGE_INSTANCE_DELETE: 85, + STICKER_CREATE: 90, + STICKER_UPDATE: 91, + STICKER_DELETE: 92, + GUILD_SCHEDULED_EVENT_CREATE: 100, + GUILD_SCHEDULED_EVENT_UPDATE: 101, + GUILD_SCHEDULED_EVENT_DELETE: 102, + THREAD_CREATE: 110, + THREAD_UPDATE: 111, + THREAD_DELETE: 112, }; /** @@ -132,28 +241,28 @@ class GuildAuditLogs { * @returns {AuditLogTargetType} */ static targetType(target) { - if (target < 10) return Targets.Guild; - if (target < 20) return Targets.Channel; - if (target < 30) return Targets.User; - if (target < 40) return Targets.Role; - if (target < 50) return Targets.Invite; - if (target < 60) return Targets.Webhook; - if (target < 70) return Targets.Emoji; - if (target < 80) return Targets.Message; - if (target < 83) return Targets.Integration; - if (target < 86) return Targets.StageInstance; - if (target < 100) return Targets.Sticker; - if (target < 110) return Targets.GuildScheduledEvent; - if (target < 120) return Targets.Thread; - return Targets.Unknown; + if (target < 10) return Targets.GUILD; + if (target < 20) return Targets.CHANNEL; + if (target < 30) return Targets.USER; + if (target < 40) return Targets.ROLE; + if (target < 50) return Targets.INVITE; + if (target < 60) return Targets.WEBHOOK; + if (target < 70) return Targets.EMOJI; + if (target < 80) return Targets.MESSAGE; + if (target < 83) return Targets.INTEGRATION; + if (target < 86) return Targets.STAGE_INSTANCE; + if (target < 100) return Targets.STICKER; + if (target < 110) return Targets.GUILD_SCHEDULED_EVENT; + if (target < 120) return Targets.THREAD; + return Targets.UNKNOWN; } /** - * The action type of an entry, e.g. `Create`. Here are the available types: - * * Create - * * Delete - * * Update - * * All + * The action type of an entry, e.g. `CREATE`. Here are the available types: + * * CREATE + * * DELETE + * * UPDATE + * * ALL * @typedef {string} AuditLogActionType */ @@ -165,73 +274,73 @@ class GuildAuditLogs { static actionType(action) { if ( [ - AuditLogEvent.ChannelCreate, - AuditLogEvent.ChannelOverwriteCreate, - AuditLogEvent.MemberBanRemove, - AuditLogEvent.BotAdd, - AuditLogEvent.RoleCreate, - AuditLogEvent.InviteCreate, - AuditLogEvent.WebhookCreate, - AuditLogEvent.EmojiCreate, - AuditLogEvent.MessagePin, - AuditLogEvent.IntegrationCreate, - AuditLogEvent.StageInstanceCreate, - AuditLogEvent.StickerCreate, - AuditLogEvent.GuildScheduledEventCreate, - AuditLogEvent.ThreadCreate, + Actions.CHANNEL_CREATE, + Actions.CHANNEL_OVERWRITE_CREATE, + Actions.MEMBER_BAN_REMOVE, + Actions.BOT_ADD, + Actions.ROLE_CREATE, + Actions.INVITE_CREATE, + Actions.WEBHOOK_CREATE, + Actions.EMOJI_CREATE, + Actions.MESSAGE_PIN, + Actions.INTEGRATION_CREATE, + Actions.STAGE_INSTANCE_CREATE, + Actions.STICKER_CREATE, + Actions.GUILD_SCHEDULED_EVENT_CREATE, + Actions.THREAD_CREATE, ].includes(action) ) { - return 'Create'; + return 'CREATE'; } if ( [ - AuditLogEvent.ChannelDelete, - AuditLogEvent.ChannelOverwriteDelete, - AuditLogEvent.MemberKick, - AuditLogEvent.MemberPrune, - AuditLogEvent.MemberBanAdd, - AuditLogEvent.MemberDisconnect, - AuditLogEvent.RoleDelete, - AuditLogEvent.InviteDelete, - AuditLogEvent.WebhookDelete, - AuditLogEvent.EmojiDelete, - AuditLogEvent.MessageDelete, - AuditLogEvent.MessageBulkDelete, - AuditLogEvent.MessageUnpin, - AuditLogEvent.IntegrationDelete, - AuditLogEvent.StageInstanceDelete, - AuditLogEvent.StickerDelete, - AuditLogEvent.GuildScheduledEventDelete, - AuditLogEvent.ThreadDelete, + Actions.CHANNEL_DELETE, + Actions.CHANNEL_OVERWRITE_DELETE, + Actions.MEMBER_KICK, + Actions.MEMBER_PRUNE, + Actions.MEMBER_BAN_ADD, + Actions.MEMBER_DISCONNECT, + Actions.ROLE_DELETE, + Actions.INVITE_DELETE, + Actions.WEBHOOK_DELETE, + Actions.EMOJI_DELETE, + Actions.MESSAGE_DELETE, + Actions.MESSAGE_BULK_DELETE, + Actions.MESSAGE_UNPIN, + Actions.INTEGRATION_DELETE, + Actions.STAGE_INSTANCE_DELETE, + Actions.STICKER_DELETE, + Actions.GUILD_SCHEDULED_EVENT_DELETE, + Actions.THREAD_DELETE, ].includes(action) ) { - return 'Delete'; + return 'DELETE'; } if ( [ - AuditLogEvent.GuildUpdate, - AuditLogEvent.ChannelUpdate, - AuditLogEvent.ChannelOverwriteUpdate, - AuditLogEvent.MemberUpdate, - AuditLogEvent.MemberRoleUpdate, - AuditLogEvent.MemberMove, - AuditLogEvent.RoleUpdate, - AuditLogEvent.InviteUpdate, - AuditLogEvent.WebhookUpdate, - AuditLogEvent.EmojiUpdate, - AuditLogEvent.IntegrationUpdate, - AuditLogEvent.StageInstanceUpdate, - AuditLogEvent.StickerUpdate, - AuditLogEvent.GuildScheduledEventUpdate, - AuditLogEvent.ThreadUpdate, + Actions.GUILD_UPDATE, + Actions.CHANNEL_UPDATE, + Actions.CHANNEL_OVERWRITE_UPDATE, + Actions.MEMBER_UPDATE, + Actions.MEMBER_ROLE_UPDATE, + Actions.MEMBER_MOVE, + Actions.ROLE_UPDATE, + Actions.INVITE_UPDATE, + Actions.WEBHOOK_UPDATE, + Actions.EMOJI_UPDATE, + Actions.INTEGRATION_UPDATE, + Actions.STAGE_INSTANCE_UPDATE, + Actions.STICKER_UPDATE, + Actions.GUILD_SCHEDULED_EVENT_UPDATE, + Actions.THREAD_UPDATE, ].includes(action) ) { - return 'Update'; + return 'UPDATE'; } - return 'All'; + return 'ALL'; } toJSON() { @@ -261,7 +370,7 @@ class GuildAuditLogsEntry { * Specific action type of this entry in its string presentation * @type {AuditLogAction} */ - this.action = Object.keys(AuditLogEvent).find(k => AuditLogEvent[k] === data.action_type); + this.action = Object.keys(Actions).find(k => Actions[k] === data.action_type); /** * The reason of this entry @@ -274,7 +383,7 @@ class GuildAuditLogsEntry { * @type {?User} */ this.executor = data.user_id - ? guild.client.options.partials.includes(Partials.User) + ? guild.client.options.partials.includes(PartialTypes.USER) ? guild.client.users._add({ id: data.user_id }) : guild.client.users.cache.get(data.user_id) : null; @@ -305,52 +414,52 @@ class GuildAuditLogsEntry { */ this.extra = null; switch (data.action_type) { - case AuditLogEvent.MemberPrune: + case Actions.MEMBER_PRUNE: this.extra = { removed: Number(data.options.members_removed), days: Number(data.options.delete_member_days), }; break; - case AuditLogEvent.MemberMove: - case AuditLogEvent.MessageDelete: - case AuditLogEvent.MessageBulkDelete: + case Actions.MEMBER_MOVE: + case Actions.MESSAGE_DELETE: + case Actions.MESSAGE_BULK_DELETE: this.extra = { channel: guild.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id }, count: Number(data.options.count), }; break; - case AuditLogEvent.MessagePin: - case AuditLogEvent.MessageUnpin: + case Actions.MESSAGE_PIN: + case Actions.MESSAGE_UNPIN: this.extra = { channel: guild.client.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id }, messageId: data.options.message_id, }; break; - case AuditLogEvent.MemberDisconnect: + case Actions.MEMBER_DISCONNECT: this.extra = { count: Number(data.options.count), }; break; - case AuditLogEvent.ChannelOverwriteCreate: - case AuditLogEvent.ChannelOverwriteUpdate: - case AuditLogEvent.ChannelOverwriteDelete: - switch (data.options.type) { - case OverwriteType.Role: + case Actions.CHANNEL_OVERWRITE_CREATE: + case Actions.CHANNEL_OVERWRITE_UPDATE: + case Actions.CHANNEL_OVERWRITE_DELETE: + switch (Number(data.options.type)) { + case OverwriteTypes.role: this.extra = guild.roles.cache.get(data.options.id) ?? { id: data.options.id, name: data.options.role_name, - type: OverwriteType.Role, + type: OverwriteTypes[OverwriteTypes.role], }; break; - case OverwriteType.Member: + case OverwriteTypes.member: this.extra = guild.members.cache.get(data.options.id) ?? { id: data.options.id, - type: OverwriteType.Member, + type: OverwriteTypes[OverwriteTypes.member], }; break; @@ -359,9 +468,9 @@ class GuildAuditLogsEntry { } break; - case AuditLogEvent.StageInstanceCreate: - case AuditLogEvent.StageInstanceDelete: - case AuditLogEvent.StageInstanceUpdate: + case Actions.STAGE_INSTANCE_CREATE: + case Actions.STAGE_INSTANCE_DELETE: + case Actions.STAGE_INSTANCE_UPDATE: this.extra = { channel: guild.client.channels.cache.get(data.options?.channel_id) ?? { id: data.options?.channel_id }, }; @@ -376,20 +485,20 @@ class GuildAuditLogsEntry { * @type {?AuditLogEntryTarget} */ this.target = null; - if (targetType === Targets.Unknown) { + if (targetType === Targets.UNKNOWN) { this.target = this.changes.reduce((o, c) => { o[c.key] = c.new ?? c.old; return o; }, {}); this.target.id = data.target_id; - // MemberDisconnect and similar types do not provide a target_id. - } else if (targetType === Targets.User && data.target_id) { - this.target = guild.client.options.partials.includes(Partials.User) + // MEMBER_DISCONNECT and similar types do not provide a target_id. + } else if (targetType === Targets.USER && data.target_id) { + this.target = guild.client.options.partials.includes(PartialTypes.USER) ? guild.client.users._add({ id: data.target_id }) : guild.client.users.cache.get(data.target_id); - } else if (targetType === Targets.Guild) { + } else if (targetType === Targets.GUILD) { this.target = guild.client.guilds.cache.get(data.target_id); - } else if (targetType === Targets.Webhook) { + } else if (targetType === Targets.WEBHOOK) { this.target = logs.webhooks.get(data.target_id) ?? new Webhook( @@ -405,7 +514,7 @@ class GuildAuditLogsEntry { }, ), ); - } else if (targetType === Targets.Invite) { + } else if (targetType === Targets.INVITE) { let change = this.changes.find(c => c.key === 'code'); change = change.new ?? change.old; @@ -421,13 +530,13 @@ class GuildAuditLogsEntry { { guild }, ), ); - } else if (targetType === Targets.Message) { - // Discord sends a channel id for the MessageBulkDelete action type. + } else if (targetType === Targets.MESSAGE) { + // Discord sends a channel id for the MESSAGE_BULK_DELETE action type. this.target = - data.action_type === AuditLogEvent.MessageBulkDelete + data.action_type === Actions.MESSAGE_BULK_DELETE ? guild.channels.cache.get(data.target_id) ?? { id: data.target_id } : guild.client.users.cache.get(data.target_id); - } else if (targetType === Targets.Integration) { + } else if (targetType === Targets.INTEGRATION) { this.target = logs.integrations.get(data.target_id) ?? new Integration( @@ -441,7 +550,7 @@ class GuildAuditLogsEntry { ), guild, ); - } else if (targetType === Targets.Channel || targetType === Targets.Thread) { + } else if (targetType === Targets.CHANNEL || targetType === Targets.THREAD) { this.target = guild.channels.cache.get(data.target_id) ?? this.changes.reduce( @@ -451,7 +560,7 @@ class GuildAuditLogsEntry { }, { id: data.target_id }, ); - } else if (targetType === Targets.StageInstance) { + } else if (targetType === Targets.STAGE_INSTANCE) { this.target = guild.stageInstances.cache.get(data.target_id) ?? new StageInstance( @@ -468,7 +577,7 @@ class GuildAuditLogsEntry { }, ), ); - } else if (targetType === Targets.Sticker) { + } else if (targetType === Targets.STICKER) { this.target = guild.stickers.cache.get(data.target_id) ?? new Sticker( @@ -481,7 +590,7 @@ class GuildAuditLogsEntry { { id: data.target_id }, ), ); - } else if (targetType === Targets.GuildScheduledEvent) { + } else if (targetType === Targets.GUILD_SCHEDULED_EVENT) { this.target = guild.scheduledEvents.cache.get(data.target_id) ?? new GuildScheduledEvent( @@ -505,7 +614,7 @@ class GuildAuditLogsEntry { * @readonly */ get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); + return SnowflakeUtil.timestampFrom(this.id); } /** @@ -522,6 +631,7 @@ class GuildAuditLogsEntry { } } +GuildAuditLogs.Actions = Actions; GuildAuditLogs.Targets = Targets; GuildAuditLogs.Entry = GuildAuditLogsEntry; diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 67c2f9e..03e86d3 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -1,11 +1,12 @@ 'use strict'; -const { PermissionFlagsBits } = require('discord-api-types/v9'); const { Channel } = require('./Channel'); +const PermissionOverwrites = require('./PermissionOverwrites'); const { Error } = require('../errors'); const PermissionOverwriteManager = require('../managers/PermissionOverwriteManager'); -const { VoiceBasedChannelTypes } = require('../util/Constants'); -const PermissionsBitField = require('../util/PermissionsBitField'); +const { ChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants'); +const Permissions = require('../util/Permissions'); +const Util = require('../util/Util'); /** * Represents a guild channel from any of the following: @@ -120,11 +121,11 @@ class GuildChannel extends Channel { // Handle empty overwrite if ( (!channelVal && - parentVal.deny.bitfield === PermissionsBitField.defaultBit && - parentVal.allow.bitfield === PermissionsBitField.defaultBit) || + parentVal.deny.bitfield === Permissions.defaultBit && + parentVal.allow.bitfield === Permissions.defaultBit) || (!parentVal && - channelVal.deny.bitfield === PermissionsBitField.defaultBit && - channelVal.allow.bitfield === PermissionsBitField.defaultBit) + channelVal.deny.bitfield === Permissions.defaultBit && + channelVal.allow.bitfield === Permissions.defaultBit) ) { return true; } @@ -153,7 +154,7 @@ class GuildChannel extends Channel { * Gets the overall set of permissions for a member or role in this channel, taking into account channel overwrites. * @param {GuildMemberResolvable|RoleResolvable} memberOrRole The member or role to obtain the overall permissions for * @param {boolean} [checkAdmin=true] Whether having `ADMINISTRATOR` will return all permissions - * @returns {?Readonly} + * @returns {?Readonly} */ permissionsFor(memberOrRole, checkAdmin = true) { const member = this.guild.members.resolve(memberOrRole); @@ -192,30 +193,28 @@ class GuildChannel extends Channel { * Gets the overall set of permissions for a member in this channel, taking into account channel overwrites. * @param {GuildMember} member The member to obtain the overall permissions for * @param {boolean} checkAdmin=true Whether having `ADMINISTRATOR` will return all permissions - * @returns {Readonly} + * @returns {Readonly} * @private */ memberPermissions(member, checkAdmin) { - if (checkAdmin && member.id === this.guild.ownerId) { - return new PermissionsBitField(PermissionsBitField.All).freeze(); - } + if (checkAdmin && member.id === this.guild.ownerId) return new Permissions(Permissions.ALL).freeze(); const roles = member.roles.cache; - const permissions = new PermissionsBitField(roles.map(role => role.permissions)); + const permissions = new Permissions(roles.map(role => role.permissions)); - if (checkAdmin && permissions.has(PermissionFlagsBits.Administrator)) { - return new PermissionsBitField(PermissionsBitField.All).freeze(); + if (checkAdmin && permissions.has(Permissions.FLAGS.ADMINISTRATOR)) { + return new Permissions(Permissions.ALL).freeze(); } const overwrites = this.overwritesFor(member, true, roles); return permissions - .remove(overwrites.everyone?.deny ?? PermissionsBitField.defaultBit) - .add(overwrites.everyone?.allow ?? PermissionsBitField.defaultBit) - .remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.deny) : PermissionsBitField.defaultBit) - .add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allow) : PermissionsBitField.defaultBit) - .remove(overwrites.member?.deny ?? PermissionsBitField.defaultBit) - .add(overwrites.member?.allow ?? PermissionsBitField.defaultBit) + .remove(overwrites.everyone?.deny ?? Permissions.defaultBit) + .add(overwrites.everyone?.allow ?? Permissions.defaultBit) + .remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.deny) : Permissions.defaultBit) + .add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allow) : Permissions.defaultBit) + .remove(overwrites.member?.deny ?? Permissions.defaultBit) + .add(overwrites.member?.allow ?? Permissions.defaultBit) .freeze(); } @@ -223,22 +222,22 @@ class GuildChannel extends Channel { * Gets the overall set of permissions for a role in this channel, taking into account channel overwrites. * @param {Role} role The role to obtain the overall permissions for * @param {boolean} checkAdmin Whether having `ADMINISTRATOR` will return all permissions - * @returns {Readonly} + * @returns {Readonly} * @private */ rolePermissions(role, checkAdmin) { - if (checkAdmin && role.permissions.has(PermissionFlagsBits.Administrator)) { - return new PermissionsBitField(PermissionsBitField.All).freeze(); + if (checkAdmin && role.permissions.has(Permissions.FLAGS.ADMINISTRATOR)) { + return new Permissions(Permissions.ALL).freeze(); } const everyoneOverwrites = this.permissionOverwrites.cache.get(this.guild.id); const roleOverwrites = this.permissionOverwrites.cache.get(role.id); return role.permissions - .remove(everyoneOverwrites?.deny ?? PermissionsBitField.defaultBit) - .add(everyoneOverwrites?.allow ?? PermissionsBitField.defaultBit) - .remove(roleOverwrites?.deny ?? PermissionsBitField.defaultBit) - .add(roleOverwrites?.allow ?? PermissionsBitField.defaultBit) + .remove(everyoneOverwrites?.deny ?? Permissions.defaultBit) + .add(everyoneOverwrites?.allow ?? Permissions.defaultBit) + .remove(roleOverwrites?.deny ?? Permissions.defaultBit) + .add(roleOverwrites?.allow ?? Permissions.defaultBit) .freeze(); } @@ -260,9 +259,30 @@ class GuildChannel extends Channel { * @readonly */ get members() { - return this.guild.members.cache.filter(m => this.permissionsFor(m).has(PermissionFlagsBits.ViewChannel, false)); + return this.guild.members.cache.filter(m => this.permissionsFor(m).has(Permissions.FLAGS.VIEW_CHANNEL, false)); } + /** + * The data for a guild channel. + * @typedef {Object} ChannelData + * @property {string} [name] The name of the channel + * @property {ChannelType} [type] The type of the channel (only conversion between text and news is supported) + * @property {number} [position] The position of the channel + * @property {string} [topic] The topic of the text channel + * @property {boolean} [nsfw] Whether the channel is NSFW + * @property {number} [bitrate] The bitrate of the voice channel + * @property {number} [userLimit] The user limit of the voice channel + * @property {?CategoryChannelResolvable} [parent] The parent of the channel + * @property {boolean} [lockPermissions] + * Lock the permissions of the channel to what the parent's permissions are + * @property {OverwriteResolvable[]|Collection} [permissionOverwrites] + * Permission overwrites for the channel + * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the channel in seconds + * @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration] + * The default auto archive duration for all new threads in this channel + * @property {?string} [rtcRegion] The RTC region of the channel + */ + /** * Edits the channel. * @param {ChannelData} data The new data for the channel @@ -274,8 +294,64 @@ class GuildChannel extends Channel { * .then(console.log) * .catch(console.error); */ - edit(data, reason) { - return this.guild.channels.edit(this, data, reason); + async edit(data, reason) { + data.parent &&= this.client.channels.resolveId(data.parent); + + if (typeof data.position !== 'undefined') { + const updatedChannels = await Util.setPosition( + this, + data.position, + false, + this.guild._sortedChannels(this), + this.client.api.guilds(this.guild.id).channels, + reason, + ); + this.client.actions.GuildChannelsPositionUpdate.handle({ + guild_id: this.guild.id, + channels: updatedChannels, + }); + } + + let permission_overwrites; + + if (data.permissionOverwrites) { + permission_overwrites = data.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild)); + } + + if (data.lockPermissions) { + if (data.parent) { + const newParent = this.guild.channels.resolve(data.parent); + if (newParent?.type === 'GUILD_CATEGORY') { + permission_overwrites = newParent.permissionOverwrites.cache.map(o => + PermissionOverwrites.resolve(o, this.guild), + ); + } + } else if (this.parent) { + permission_overwrites = this.parent.permissionOverwrites.cache.map(o => + PermissionOverwrites.resolve(o, this.guild), + ); + } + } + + const newData = await this.client.api.channels(this.id).patch({ + data: { + name: (data.name ?? this.name).trim(), + type: ChannelTypes[data.type], + topic: data.topic, + nsfw: data.nsfw, + bitrate: data.bitrate ?? this.bitrate, + user_limit: data.userLimit ?? this.userLimit, + rtc_region: data.rtcRegion ?? this.rtcRegion, + parent_id: data.parent, + lock_permissions: data.lockPermissions, + rate_limit_per_user: data.rateLimitPerUser, + default_auto_archive_duration: data.defaultAutoArchiveDuration, + permission_overwrites, + }, + reason, + }); + + return this.client.actions.ChannelUpdate.handle(newData).updated; } /** @@ -339,10 +415,30 @@ class GuildChannel extends Channel { * .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`)) * .catch(console.error); */ - setPosition(position, options = {}) { - return this.guild.channels.setPosition(this, position, options); + async setPosition(position, { relative, reason } = {}) { + const updatedChannels = await Util.setPosition( + this, + position, + relative, + this.guild._sortedChannels(this), + this.client.api.guilds(this.guild.id).channels, + reason, + ); + this.client.actions.GuildChannelsPositionUpdate.handle({ + guild_id: this.guild.id, + channels: updatedChannels, + }); + return this; } + /** + * Data that can be resolved to an Application. This can be: + * * An Application + * * An Activity with associated Application + * * A Snowflake + * @typedef {Application|Snowflake} ApplicationResolvable + */ + /** * Options used to clone a guild channel. * @typedef {GuildChannelCreateOptions} GuildChannelCloneOptions @@ -416,12 +512,12 @@ class GuildChannel extends Channel { if (!permissions) return false; // This flag allows managing even if timed out - if (permissions.has(PermissionFlagsBits.Administrator, false)) return true; + if (permissions.has(Permissions.FLAGS.ADMINISTRATOR, false)) return true; if (this.guild.me.communicationDisabledUntilTimestamp > Date.now()) return false; const bitfield = VoiceBasedChannelTypes.includes(this.type) - ? PermissionFlagsBits.ManageChannels | PermissionFlagsBits.Connect - : PermissionFlagsBits.ViewChannel | PermissionFlagsBits.ManageChannels; + ? Permissions.FLAGS.MANAGE_CHANNELS | Permissions.FLAGS.CONNECT + : Permissions.FLAGS.VIEW_CHANNEL | Permissions.FLAGS.MANAGE_CHANNELS; return permissions.has(bitfield, false); } @@ -434,7 +530,7 @@ class GuildChannel extends Channel { if (this.client.user.id === this.guild.ownerId) return true; const permissions = this.permissionsFor(this.client.user); if (!permissions) return false; - return permissions.has(PermissionFlagsBits.ViewChannel, false); + return permissions.has(Permissions.FLAGS.VIEW_CHANNEL, false); } /** @@ -448,7 +544,7 @@ class GuildChannel extends Channel { * .catch(console.error); */ async delete(reason) { - await this.guild.channels.delete(this.id, reason); + await this.client.api.channels(this.id).delete({ reason }); return this; } } diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index 609083b..e5629fd 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -1,9 +1,9 @@ 'use strict'; -const { PermissionFlagsBits } = require('discord-api-types/v9'); const BaseGuildEmoji = require('./BaseGuildEmoji'); const { Error } = require('../errors'); const GuildEmojiRoleManager = require('../managers/GuildEmojiRoleManager'); +const Permissions = require('../util/Permissions'); /** * Represents a custom emoji. @@ -56,7 +56,7 @@ class GuildEmoji extends BaseGuildEmoji { */ get deletable() { if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME'); - return !this.managed && this.guild.me.permissions.has(PermissionFlagsBits.ManageEmojisAndStickers); + return !this.managed && this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS); } /** @@ -72,8 +72,18 @@ class GuildEmoji extends BaseGuildEmoji { * Fetches the author for this emoji * @returns {Promise} */ - fetchAuthor() { - return this.guild.emojis.fetchAuthor(this); + async fetchAuthor() { + if (this.managed) { + throw new Error('EMOJI_MANAGED'); + } else { + if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME'); + if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS)) { + throw new Error('MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION', this.guild); + } + } + const data = await this.client.api.guilds(this.guild.id).emojis(this.id).get(); + this._patch(data); + return this.author; } /** @@ -94,8 +104,21 @@ class GuildEmoji extends BaseGuildEmoji { * .then(e => console.log(`Edited emoji ${e}`)) * .catch(console.error); */ - edit(data, reason) { - return this.guild.emojis.edit(this.id, data, reason); + async edit(data, reason) { + const roles = data.roles?.map(r => r.id ?? r); + const newData = await this.client.api + .guilds(this.guild.id) + .emojis(this.id) + .patch({ + data: { + name: data.name, + roles, + }, + reason, + }); + const clone = this._clone(); + clone._patch(newData); + return clone; } /** @@ -114,7 +137,7 @@ class GuildEmoji extends BaseGuildEmoji { * @returns {Promise} */ async delete(reason) { - await this.guild.emojis.delete(this.id, reason); + await this.client.api.guilds(this.guild.id).emojis(this.id).delete({ reason }); return this; } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index a02fd7a..f271def 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -1,12 +1,20 @@ 'use strict'; -const { PermissionFlagsBits } = require('discord-api-types/v9'); +const process = require('node:process'); const Base = require('./Base'); const VoiceState = require('./VoiceState'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const { Error } = require('../errors'); const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager'); -const PermissionsBitField = require('../util/PermissionsBitField'); +const Permissions = require('../util/Permissions'); + +/** + * @type {WeakSet} + * @private + * @internal + */ +const deletedGuildMembers = new WeakSet(); +let deprecationEmittedForDeleted = false; /** * Represents a member of a guild on Discord. @@ -43,9 +51,9 @@ class GuildMember extends Base { /** * Whether this member has yet to pass the guild's membership gate - * @type {?boolean} + * @type {boolean} */ - this.pending = null; + this.pending = false; /** * The timestamp this member's timeout will be removed @@ -76,18 +84,12 @@ class GuildMember extends Base { } else if (typeof this.avatar !== 'string') { this.avatar = null; } - if ('joined_at' in data) this.joinedTimestamp = Date.parse(data.joined_at); + if ('joined_at' in data) this.joinedTimestamp = new Date(data.joined_at).getTime(); if ('premium_since' in data) { - this.premiumSinceTimestamp = data.premium_since ? Date.parse(data.premium_since) : null; + this.premiumSinceTimestamp = data.premium_since ? new Date(data.premium_since).getTime() : null; } if ('roles' in data) this._roles = data.roles; - - if ('pending' in data) { - this.pending = data.pending; - } else if (!this.partial) { - // See https://github.com/discordjs/discord.js/issues/6546 for more info. - this.pending ??= false; - } + this.pending = data.pending ?? false; if ('communication_disabled_until' in data) { this.communicationDisabledUntilTimestamp = @@ -101,6 +103,36 @@ class GuildMember extends Base { return clone; } + /** + * 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( + 'GuildMember#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + return deletedGuildMembers.has(this); + } + + set deleted(value) { + if (!deprecationEmittedForDeleted) { + deprecationEmittedForDeleted = true; + process.emitWarning( + 'GuildMember#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + if (value) deletedGuildMembers.add(this); + else deletedGuildMembers.delete(this); + } + /** * Whether this GuildMember is a partial * @type {boolean} @@ -130,11 +162,12 @@ class GuildMember extends Base { /** * A link to the member's guild avatar. - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - avatarURL(options = {}) { - return this.avatar && this.client.rest.cdn.guildMemberAvatar(this.guild.id, this.id, this.avatar, options); + avatarURL({ format, size, dynamic } = {}) { + if (!this.avatar) return null; + return this.client.rest.cdn.GuildMemberAvatar(this.guild.id, this.id, this.avatar, format, size, dynamic); } /** @@ -153,7 +186,7 @@ class GuildMember extends Base { * @readonly */ get joinedAt() { - return this.joinedTimestamp && new Date(this.joinedTimestamp); + return this.joinedTimestamp ? new Date(this.joinedTimestamp) : null; } /** @@ -171,7 +204,7 @@ class GuildMember extends Base { * @readonly */ get premiumSince() { - return this.premiumSinceTimestamp && new Date(this.premiumSinceTimestamp); + return this.premiumSinceTimestamp ? new Date(this.premiumSinceTimestamp) : null; } /** @@ -221,12 +254,12 @@ class GuildMember extends Base { /** * The overall set of permissions for this member, taking only roles and owner status into account - * @type {Readonly} + * @type {Readonly} * @readonly */ get permissions() { - if (this.user.id === this.guild.ownerId) return new PermissionsBitField(PermissionsBitField.All).freeze(); - return new PermissionsBitField(this.roles.cache.map(role => role.permissions)).freeze(); + if (this.user.id === this.guild.ownerId) return new Permissions(Permissions.ALL).freeze(); + return new Permissions(this.roles.cache.map(role => role.permissions)).freeze(); } /** @@ -249,8 +282,7 @@ class GuildMember extends Base { * @readonly */ get kickable() { - if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME'); - return this.manageable && this.guild.me.permissions.has(PermissionFlagsBits.KickMembers); + return this.manageable && this.guild.me.permissions.has(Permissions.FLAGS.KICK_MEMBERS); } /** @@ -259,8 +291,7 @@ class GuildMember extends Base { * @readonly */ get bannable() { - if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME'); - return this.manageable && this.guild.me.permissions.has(PermissionFlagsBits.BanMembers); + return this.manageable && this.guild.me.permissions.has(Permissions.FLAGS.BAN_MEMBERS); } /** @@ -269,11 +300,7 @@ class GuildMember extends Base { * @readonly */ get moderatable() { - return ( - !this.permissions.has(PermissionFlagsBits.Administrator) && - this.manageable && - (this.guild.me?.permissions.has(PermissionFlagsBits.ModerateMembers) ?? false) - ); + return this.manageable && (this.guild.me?.permissions.has(Permissions.FLAGS.MODERATE_MEMBERS) ?? false); } /** @@ -288,7 +315,7 @@ class GuildMember extends Base { * Returns `channel.permissionsFor(guildMember)`. Returns permissions for a member in a guild channel, * taking into account roles and permission overwrites. * @param {GuildChannelResolvable} channel The guild channel to use as context - * @returns {Readonly} + * @returns {Readonly} */ permissionsIn(channel) { channel = this.guild.channels.resolve(channel); @@ -348,7 +375,7 @@ class GuildMember extends Base { * @returns {Promise} * @example * // ban a guild member - * guildMember.ban({ deleteMessageDays: 7, reason: 'They deserved it' }) + * guildMember.ban({ days: 7, reason: 'They deserved it' }) * .then(console.log) * .catch(console.error); */ @@ -451,6 +478,7 @@ class GuildMember extends Base { TextBasedChannel.applyToClass(GuildMember); exports.GuildMember = GuildMember; +exports.deletedGuildMembers = deletedGuildMembers; /** * @external APIGuildMember diff --git a/src/structures/GuildPreview.js b/src/structures/GuildPreview.js index 8047971..4627faf 100644 --- a/src/structures/GuildPreview.js +++ b/src/structures/GuildPreview.js @@ -1,11 +1,9 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { DiscordSnowflake } = require('@sapphire/snowflake'); -const { Routes } = require('discord-api-types/v9'); const Base = require('./Base'); const GuildPreviewEmoji = require('./GuildPreviewEmoji'); -const { Sticker } = require('./Sticker'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents the data about the guild any bot can preview, connected to the specified guild. @@ -62,7 +60,7 @@ class GuildPreview extends Base { if ('features' in data) { /** * An array of enabled guild features - * @type {GuildFeature[]} + * @type {Features[]} */ this.features = data.features; } @@ -105,24 +103,14 @@ class GuildPreview extends Base { for (const emoji of data.emojis) { this.emojis.set(emoji.id, new GuildPreviewEmoji(this.client, emoji, this)); } - - /** - * Collection of stickers belonging to this guild - * @type {Collection} - */ - this.stickers = data.stickers.reduce( - (stickers, sticker) => stickers.set(sticker.id, new Sticker(this.client, sticker)), - new Collection(), - ); } - /** * The timestamp this guild was created at * @type {number} * @readonly */ get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); + return SnowflakeUtil.timestampFrom(this.id); } /** @@ -136,29 +124,29 @@ class GuildPreview extends Base { /** * The URL to this guild's splash. - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {StaticImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - splashURL(options = {}) { - return this.splash && this.client.rest.cdn.splash(this.id, this.splash, options); + splashURL({ format, size } = {}) { + return this.splash && this.client.rest.cdn.Splash(this.id, this.splash, format, size); } /** * The URL to this guild's discovery splash. - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {StaticImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - discoverySplashURL(options = {}) { - return this.discoverySplash && this.client.rest.cdn.discoverySplash(this.id, this.discoverySplash, options); + discoverySplashURL({ format, size } = {}) { + return this.discoverySplash && this.client.rest.cdn.DiscoverySplash(this.id, this.discoverySplash, format, size); } /** * The URL to this guild's icon. - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - iconURL(options = {}) { - return this.icon && this.client.rest.cdn.icon(this.id, this.icon, options); + iconURL({ format, size, dynamic } = {}) { + return this.icon && this.client.rest.cdn.Icon(this.id, this.icon, format, size, dynamic); } /** diff --git a/src/structures/GuildScheduledEvent.js b/src/structures/GuildScheduledEvent.js index b082ed3..ff862b8 100644 --- a/src/structures/GuildScheduledEvent.js +++ b/src/structures/GuildScheduledEvent.js @@ -1,9 +1,14 @@ 'use strict'; -const { DiscordSnowflake } = require('@sapphire/snowflake'); -const { GuildScheduledEventStatus, GuildScheduledEventEntityType, RouteBases } = require('discord-api-types/v9'); const Base = require('./Base'); const { Error } = require('../errors'); +const { + GuildScheduledEventEntityTypes, + GuildScheduledEventStatuses, + GuildScheduledEventPrivacyLevels, + Endpoints, +} = require('../util/Constants'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents a scheduled event in a {@link Guild}. @@ -31,8 +36,7 @@ class GuildScheduledEvent extends Base { _patch(data) { if ('channel_id' in data) { /** - * The channel id in which the scheduled event will be hosted, - * or `null` if entity type is {@link GuildScheduledEventEntityType.External} + * The channel id in which the scheduled event will be hosted, or `null` if entity type is `EXTERNAL` * @type {?Snowflake} */ this.channelId = data.channel_id; @@ -82,21 +86,21 @@ class GuildScheduledEvent extends Base { /** * The privacy level of the guild scheduled event - * @type {GuildScheduledEventPrivacyLevel} + * @type {PrivacyLevel} */ - this.privacyLevel = data.privacy_level; + this.privacyLevel = GuildScheduledEventPrivacyLevels[data.privacy_level]; /** * The status of the guild scheduled event * @type {GuildScheduledEventStatus} */ - this.status = data.status; + this.status = GuildScheduledEventStatuses[data.status]; /** * The type of hosting entity associated with the scheduled event * @type {GuildScheduledEventEntityType} */ - this.entityType = data.entity_type; + this.entityType = GuildScheduledEventEntityTypes[data.entity_type]; if ('entity_id' in data) { /** @@ -152,21 +156,6 @@ class GuildScheduledEvent extends Base { } else { this.entityMetadata ??= null; } - - /** - * The cover image hash for this scheduled event - * @type {?string} - */ - this.image = data.image ?? null; - } - - /** - * The URL of this scheduled event's cover image - * @param {BaseImageURLOptions} [options={}] Options for image URL - * @returns {?string} - */ - coverImageURL(options = {}) { - return this.image && this.client.rest.cdn.guildScheduledEventCover(this.id, this.image, options); } /** @@ -175,7 +164,7 @@ class GuildScheduledEvent extends Base { * @readonly */ get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); + return SnowflakeUtil.timestampFrom(this.id); } /** @@ -230,15 +219,14 @@ class GuildScheduledEvent extends Base { * @readonly */ get url() { - return `${RouteBases.scheduledEvent}/${this.guildId}/${this.id}`; + return Endpoints.scheduledEvent(this.client.options.http.scheduledEvent, this.guildId, this.id); } /** * Options used to create an invite URL to a {@link GuildScheduledEvent} * @typedef {CreateInviteOptions} CreateGuildScheduledEventInviteURLOptions * @property {GuildInvitableChannelResolvable} [channel] The channel to create the invite in. - * This is required when the `entityType` of `GuildScheduledEvent` is - * {@link GuildScheduledEventEntityType.External}, gets ignored otherwise + * This is required when the `entityType` of `GuildScheduledEvent` is `EXTERNAL`, gets ignored otherwise */ /** @@ -248,13 +236,13 @@ class GuildScheduledEvent extends Base { */ async createInviteURL(options) { let channelId = this.channelId; - if (this.entityType === GuildScheduledEventEntityType.External) { + if (this.entityType === 'EXTERNAL') { if (!options?.channel) throw new Error('INVITE_OPTIONS_MISSING_CHANNEL'); channelId = this.guild.channels.resolveId(options.channel); if (!channelId) throw new Error('GUILD_CHANNEL_RESOLVE'); } const invite = await this.guild.invites.create(channelId, options); - return `${RouteBases.invite}/${invite.code}?event=${this.id}`; + return Endpoints.invite(this.client.options.http.invite, invite.code, this.id); } /** @@ -355,7 +343,7 @@ class GuildScheduledEvent extends Base { * @returns {Promise} * @example * // Set status of a guild scheduled event - * guildScheduledEvent.setStatus(GuildScheduledEventStatus.Active) + * guildScheduledEvent.setStatus('ACTIVE') * .then(guildScheduledEvent => console.log(`Set the status to: ${guildScheduledEvent.status}`)) * .catch(console.error); */ @@ -399,35 +387,35 @@ class GuildScheduledEvent extends Base { } /** - * Indicates whether this guild scheduled event has an {@link GuildScheduledEventStatus.Active} status. + * Indicates whether this guild scheduled event has an `ACTIVE` status. * @returns {boolean} */ isActive() { - return this.status === GuildScheduledEventStatus.Active; + return GuildScheduledEventStatuses[this.status] === GuildScheduledEventStatuses.ACTIVE; } /** - * Indicates whether this guild scheduled event has a {@link GuildScheduledEventStatus.Canceled} status. + * Indicates whether this guild scheduled event has a `CANCELED` status. * @returns {boolean} */ isCanceled() { - return this.status === GuildScheduledEventStatus.Canceled; + return GuildScheduledEventStatuses[this.status] === GuildScheduledEventStatuses.CANCELED; } /** - * Indicates whether this guild scheduled event has a {@link GuildScheduledEventStatus.Completed} status. + * Indicates whether this guild scheduled event has a `COMPLETED` status. * @returns {boolean} */ isCompleted() { - return this.status === GuildScheduledEventStatus.Completed; + return GuildScheduledEventStatuses[this.status] === GuildScheduledEventStatuses.COMPLETED; } /** - * Indicates whether this guild scheduled event has a {@link GuildScheduledEventStatus.Scheduled} status. + * Indicates whether this guild scheduled event has a `SCHEDULED` status. * @returns {boolean} */ isScheduled() { - return this.status === GuildScheduledEventStatus.Scheduled; + return GuildScheduledEventStatuses[this.status] === GuildScheduledEventStatuses.SCHEDULED; } } diff --git a/src/structures/GuildTemplate.js b/src/structures/GuildTemplate.js index 88b4e7b..08f327d 100644 --- a/src/structures/GuildTemplate.js +++ b/src/structures/GuildTemplate.js @@ -1,10 +1,9 @@ 'use strict'; -const { setTimeout, clearTimeout } = require('node:timers'); -const { RouteBases, Routes } = require('discord-api-types/v9'); +const { setTimeout } = require('node:timers'); const Base = require('./Base'); +const { Events } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); -const Events = require('../util/Events'); /** * Represents the template for a guild. @@ -67,18 +66,18 @@ class GuildTemplate extends Base { if ('created_at' in data) { /** - * The timestamp of when this template was created at - * @type {number} + * The time when this template was created at + * @type {Date} */ - this.createdTimestamp = Date.parse(data.created_at); + this.createdAt = new Date(data.created_at); } if ('updated_at' in data) { /** - * The timestamp of when this template was last synced to the guild - * @type {number} + * The time when this template was last synced to the guild + * @type {Date} */ - this.updatedTimestamp = Date.parse(data.updated_at); + this.updatedAt = new Date(data.updated_at); } if ('source_guild_id' in data) { @@ -115,8 +114,8 @@ class GuildTemplate extends Base { */ async createGuild(name, icon) { const { client } = this; - const data = await client.rest.post(Routes.template(this.code), { - body: { + const data = await client.api.guilds.templates(this.code).post({ + data: { name, icon: await DataResolver.resolveImage(icon), }, @@ -126,7 +125,7 @@ class GuildTemplate extends Base { return new Promise(resolve => { const resolveGuild = guild => { - client.off(Events.GuildCreate, handleGuild); + client.off(Events.GUILD_CREATE, handleGuild); client.decrementMaxListeners(); resolve(guild); }; @@ -139,7 +138,7 @@ class GuildTemplate extends Base { }; client.incrementMaxListeners(); - client.on(Events.GuildCreate, handleGuild); + client.on(Events.GUILD_CREATE, handleGuild); const timeout = setTimeout(() => resolveGuild(client.guilds._add(data)), 10_000).unref(); }); @@ -158,7 +157,7 @@ class GuildTemplate extends Base { * @returns {Promise} */ async edit({ name, description } = {}) { - const data = await this.client.api.guilds(this.guildId).templates(this.code).patch({ body: { name, description } }); + const data = await this.client.api.guilds(this.guildId).templates(this.code).patch({ data: { name, description } }); return this._patch(data); } @@ -181,21 +180,21 @@ class GuildTemplate extends Base { } /** - * The time when this template was created at - * @type {Date} + * The timestamp of when this template was created at + * @type {number} * @readonly */ - get createdAt() { - return new Date(this.createdTimestamp); + get createdTimestamp() { + return this.createdAt.getTime(); } /** - * The time when this template was last synced to the guild - * @type {Date} + * The timestamp of when this template was last synced to the guild + * @type {number} * @readonly */ - get updatedAt() { - return new Date(this.updatedTimestamp); + get updatedTimestamp() { + return this.updatedAt.getTime(); } /** @@ -213,7 +212,7 @@ class GuildTemplate extends Base { * @readonly */ get url() { - return `${RouteBases.template}/${this.code}`; + return `${this.client.options.http.template}/${this.code}`; } /** diff --git a/src/structures/Integration.js b/src/structures/Integration.js index fd68c41..5773de8 100644 --- a/src/structures/Integration.js +++ b/src/structures/Integration.js @@ -1,6 +1,5 @@ 'use strict'; -const { Routes } = require('discord-api-types/v9'); const Base = require('./Base'); const IntegrationApplication = require('./IntegrationApplication'); @@ -56,21 +55,17 @@ class Integration extends Base { */ this.enabled = data.enabled; - if ('syncing' in data) { - /** - * Whether this integration is syncing - * @type {?boolean} - */ - this.syncing = data.syncing; - } else { - this.syncing ??= null; - } + /** + * Whether this integration is syncing + * @type {?boolean} + */ + this.syncing = data.syncing; /** * The role that this integration uses for subscribers * @type {?Role} */ - this.role = this.guild.roles.resolve(data.role_id); + this.role = this.guild.roles.cache.get(data.role_id); if ('enable_emoticons' in data) { /** @@ -89,7 +84,7 @@ class Integration extends Base { */ this.user = this.client.users._add(data.user); } else { - this.user ??= null; + this.user = null; } /** @@ -98,15 +93,11 @@ class Integration extends Base { */ this.account = data.account; - if ('synced_at' in data) { - /** - * The timestamp at which this integration was last synced at - * @type {?number} - */ - this.syncedTimestamp = Date.parse(data.synced_at); - } else { - this.syncedTimestamp ??= null; - } + /** + * The last time this integration was last synced + * @type {?number} + */ + this.syncedAt = data.synced_at; if ('subscriber_count' in data) { /** @@ -131,15 +122,6 @@ class Integration extends Base { this._patch(data); } - /** - * The date at which this integration was last synced at - * @type {?Date} - * @readonly - */ - get syncedAt() { - return this.syncedTimestamp && new Date(this.syncedTimestamp); - } - /** * All roles that are managed by this integration * @type {Collection} @@ -154,21 +136,17 @@ class Integration extends Base { if ('expire_behavior' in data) { /** * The behavior of expiring subscribers - * @type {?IntegrationExpireBehavior} + * @type {?number} */ this.expireBehavior = data.expire_behavior; - } else { - this.expireBehavior ??= null; } if ('expire_grace_period' in data) { /** - * The grace period (in days) before expiring subscribers + * The grace period before expiring subscribers * @type {?number} */ this.expireGracePeriod = data.expire_grace_period; - } else { - this.expireGracePeriod ??= null; } if ('application' in data) { diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index a341708..2a19cad 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -1,9 +1,9 @@ 'use strict'; -const { DiscordSnowflake } = require('@sapphire/snowflake'); -const { InteractionType, ApplicationCommandType, ComponentType } = require('discord-api-types/v9'); const Base = require('./Base'); -const PermissionsBitField = require('../util/PermissionsBitField'); +const { InteractionTypes, MessageComponentTypes, ApplicationCommandTypes } = require('../util/Constants'); +const Permissions = require('../util/Permissions'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents an interaction. @@ -17,7 +17,7 @@ class Interaction extends Base { * The interaction's type * @type {InteractionType} */ - this.type = data.type; + this.type = InteractionTypes[data.type]; /** * The interaction's id @@ -71,16 +71,14 @@ class Interaction extends Base { /** * The permissions of the member, if one exists, in the channel this interaction was executed in - * @type {?Readonly} + * @type {?Readonly} */ - this.memberPermissions = data.member?.permissions - ? new PermissionsBitField(data.member.permissions).freeze() - : null; + this.memberPermissions = data.member?.permissions ? new Permissions(data.member.permissions).freeze() : null; /** * The locale of the user who invoked this interaction * @type {string} - * @see {@link https://discord.com/developers/docs/reference#locales} + * @see {@link https://discord.com/developers/docs/dispatch/field-values#predefined-field-values-accepted-locales} */ this.locale = data.locale; @@ -97,7 +95,7 @@ class Interaction extends Base { * @readonly */ get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); + return SnowflakeUtil.timestampFrom(this.id); } /** @@ -151,44 +149,44 @@ class Interaction extends Base { return Boolean(this.guildId && !this.guild && this.member); } + /** + * Indicates whether this interaction is a {@link BaseCommandInteraction}. + * @returns {boolean} + */ + isApplicationCommand() { + return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND; + } + /** * Indicates whether this interaction is a {@link CommandInteraction}. * @returns {boolean} */ isCommand() { - return this.type === InteractionType.ApplicationCommand; + return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND && typeof this.targetId === 'undefined'; } /** - * Indicates whether this interaction is a {@link ChatInputCommandInteraction}. + * Indicates whether this interaction is a {@link ContextMenuInteraction} * @returns {boolean} */ - isChatInputCommand() { - return this.isCommand() && this.commandType === ApplicationCommandType.ChatInput; + isContextMenu() { + return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND && typeof this.targetId !== 'undefined'; } /** - * Indicates whether this interaction is a {@link ContextMenuCommandInteraction} + * Indicates whether this interaction is a {@link UserContextMenuInteraction} * @returns {boolean} */ - isContextMenuCommand() { - return this.isCommand() && [ApplicationCommandType.User, ApplicationCommandType.Message].includes(this.commandType); + isUserContextMenu() { + return this.isContextMenu() && ApplicationCommandTypes[this.targetType] === ApplicationCommandTypes.USER; } /** - * Indicates whether this interaction is a {@link UserContextMenuCommandInteraction} + * Indicates whether this interaction is a {@link MessageContextMenuInteraction} * @returns {boolean} */ - isUserContextMenuCommand() { - return this.isContextMenuCommand() && this.commandType === ApplicationCommandType.User; - } - - /** - * Indicates whether this interaction is a {@link MessageContextMenuCommandInteraction} - * @returns {boolean} - */ - isMessageContextMenuCommand() { - return this.isContextMenuCommand() && this.commandType === ApplicationCommandType.Message; + isMessageContextMenu() { + return this.isContextMenu() && ApplicationCommandTypes[this.targetType] === ApplicationCommandTypes.MESSAGE; } /** @@ -196,7 +194,7 @@ class Interaction extends Base { * @returns {boolean} */ isAutocomplete() { - return this.type === InteractionType.ApplicationCommandAutocomplete; + return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE; } /** @@ -204,7 +202,7 @@ class Interaction extends Base { * @returns {boolean} */ isMessageComponent() { - return this.type === InteractionType.MessageComponent; + return InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT; } /** @@ -212,7 +210,10 @@ class Interaction extends Base { * @returns {boolean} */ isButton() { - return this.isMessageComponent() && this.componentType === ComponentType.Button; + return ( + InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT && + MessageComponentTypes[this.componentType] === MessageComponentTypes.BUTTON + ); } /** @@ -220,15 +221,10 @@ class Interaction extends Base { * @returns {boolean} */ isSelectMenu() { - return this.isMessageComponent() && this.componentType === ComponentType.SelectMenu; - } - - /** - * Indicates whether this interaction can be replied to. - * @returns {boolean} - */ - isRepliable() { - return ![InteractionType.Ping, InteractionType.ApplicationCommandAutocomplete].includes(this.type); + return ( + InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT && + MessageComponentTypes[this.componentType] === MessageComponentTypes.SELECT_MENU + ); } } diff --git a/src/structures/InteractionCollector.js b/src/structures/InteractionCollector.js index 56821f3..574c047 100644 --- a/src/structures/InteractionCollector.js +++ b/src/structures/InteractionCollector.js @@ -2,13 +2,14 @@ const { Collection } = require('@discordjs/collection'); const Collector = require('./interfaces/Collector'); -const Events = require('../util/Events'); +const { Events } = require('../util/Constants'); +const { InteractionTypes, MessageComponentTypes } = require('../util/Constants'); /** * @typedef {CollectorOptions} InteractionCollectorOptions - * @property {TextBasedChannelResolvable} [channel] The channel to listen to interactions from - * @property {ComponentType} [componentType] The type of component to listen for - * @property {GuildResolvable} [guild] The guild to listen to interactions from + * @property {TextBasedChannels} [channel] The channel to listen to interactions from + * @property {MessageComponentType} [componentType] The type of component to listen for + * @property {Guild} [guild] The guild to listen to interactions from * @property {InteractionType} [interactionType] The type of interaction to listen for * @property {number} [max] The maximum total amount of interactions to collect * @property {number} [maxComponents] The maximum number of components to collect @@ -63,13 +64,19 @@ class InteractionCollector extends Collector { * The type of interaction to collect * @type {?InteractionType} */ - this.interactionType = options.interactionType ?? null; + this.interactionType = + typeof options.interactionType === 'number' + ? InteractionTypes[options.interactionType] + : options.interactionType ?? null; /** * The type of component to collect - * @type {?ComponentType} + * @type {?MessageComponentType} */ - this.componentType = options.componentType ?? null; + this.componentType = + typeof options.componentType === 'number' + ? MessageComponentTypes[options.componentType] + : options.componentType ?? null; /** * The users that have interacted with this collector @@ -92,31 +99,31 @@ class InteractionCollector extends Collector { if (this.messageId) { this._handleMessageDeletion = this._handleMessageDeletion.bind(this); - this.client.on(Events.MessageDelete, this._handleMessageDeletion); - this.client.on(Events.MessageBulkDelete, bulkDeleteListener); + this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion); + this.client.on(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); } if (this.channelId) { this._handleChannelDeletion = this._handleChannelDeletion.bind(this); this._handleThreadDeletion = this._handleThreadDeletion.bind(this); - this.client.on(Events.ChannelDelete, this._handleChannelDeletion); - this.client.on(Events.ThreadDelete, this._handleThreadDeletion); + this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.on(Events.THREAD_DELETE, this._handleThreadDeletion); } if (this.guildId) { this._handleGuildDeletion = this._handleGuildDeletion.bind(this); - this.client.on(Events.GuildDelete, this._handleGuildDeletion); + this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); } - this.client.on(Events.InteractionCreate, this.handleCollect); + this.client.on(Events.INTERACTION_CREATE, this.handleCollect); this.once('end', () => { - this.client.removeListener(Events.InteractionCreate, this.handleCollect); - this.client.removeListener(Events.MessageDelete, this._handleMessageDeletion); - this.client.removeListener(Events.MessageBulkDelete, bulkDeleteListener); - this.client.removeListener(Events.ChannelDelete, this._handleChannelDeletion); - this.client.removeListener(Events.ThreadDelete, this._handleThreadDeletion); - this.client.removeListener(Events.GuildDelete, this._handleGuildDeletion); + this.client.removeListener(Events.INTERACTION_CREATE, this.handleCollect); + this.client.removeListener(Events.MESSAGE_DELETE, this._handleMessageDeletion); + this.client.removeListener(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); + this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.removeListener(Events.THREAD_DELETE, this._handleThreadDeletion); + this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); this.client.decrementMaxListeners(); }); diff --git a/src/structures/Invite.js b/src/structures/Invite.js index 7dcb613..0ed8b45 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -1,11 +1,14 @@ 'use strict'; -const { RouteBases, Routes, PermissionFlagsBits } = require('discord-api-types/v9'); const Base = require('./Base'); const { GuildScheduledEvent } = require('./GuildScheduledEvent'); const IntegrationApplication = require('./IntegrationApplication'); const InviteStageInstance = require('./InviteStageInstance'); const { Error } = require('../errors'); +const { Endpoints } = require('../util/Constants'); +const Permissions = require('../util/Permissions'); + +// TODO: Convert `inviter` and `channel` in this class to a getter. /** * Represents an invitation to a guild channel. @@ -112,13 +115,20 @@ class Invite extends Base { * @type {?Snowflake} */ this.inviterId = data.inviter_id; + this.inviter = this.client.users.resolve(data.inviter_id); } else { this.inviterId ??= null; } if ('inviter' in data) { - this.client.users._add(data.inviter); + /** + * The user who created this invite + * @type {?User} + */ + this.inviter ??= this.client.users._add(data.inviter); this.inviterId = data.inviter.id; + } else { + this.inviter ??= null; } if ('target_user' in data) { @@ -141,10 +151,18 @@ class Invite extends Base { this.targetApplication ??= null; } + /** + * The type of the invite target: + * * 1: STREAM + * * 2: EMBEDDED_APPLICATION + * @typedef {number} TargetType + * @see {@link https://discord.com/developers/docs/resources/invite#invite-object-invite-target-types} + */ + if ('target_type' in data) { /** * The target type - * @type {?InviteTargetType} + * @type {?TargetType} */ this.targetType = data.target_type; } else { @@ -153,21 +171,19 @@ class Invite extends Base { if ('channel_id' in data) { /** - * The id of the channel this invite is for - * @type {?Snowflake} + * The channel's id this invite is for + * @type {Snowflake} */ this.channelId = data.channel_id; + this.channel = this.client.channels.cache.get(data.channel_id); } if ('channel' in data) { /** * The channel this invite is for - * @type {?Channel} + * @type {Channel} */ - this.channel = - this.client.channels._add(data.channel, this.guild, { cache: false }) ?? - this.client.channels.resolve(this.channelId); - + this.channel ??= this.client.channels._add(data.channel, this.guild, { cache: false }); this.channelId ??= data.channel.id; } @@ -176,19 +192,18 @@ class Invite extends Base { * The timestamp this invite was created at * @type {?number} */ - this.createdTimestamp = Date.parse(data.created_at); + this.createdTimestamp = new Date(data.created_at).getTime(); } else { this.createdTimestamp ??= null; } - if ('expires_at' in data) this._expiresTimestamp = Date.parse(data.expires_at); + if ('expires_at' in data) this._expiresTimestamp = new Date(data.expires_at).getTime(); else this._expiresTimestamp ??= null; if ('stage_instance' in data) { /** * The stage instance data if there is a public {@link StageInstance} in the stage channel this invite is for * @type {?InviteStageInstance} - * @deprecated */ this.stageInstance = new InviteStageInstance(this.client, data.stage_instance, this.channel.id, this.guild.id); } else { @@ -212,7 +227,7 @@ class Invite extends Base { * @readonly */ get createdAt() { - return this.createdTimestamp && new Date(this.createdTimestamp); + return this.createdTimestamp ? new Date(this.createdTimestamp) : null; } /** @@ -224,9 +239,9 @@ class Invite extends Base { const guild = this.guild; if (!guild || !this.client.guilds.cache.has(guild.id)) return false; if (!guild.me) throw new Error('GUILD_UNCACHED_ME'); - return Boolean( - this.channel?.permissionsFor(this.client.user).has(PermissionFlagsBits.ManageChannels, false) || - guild.me.permissions.has(PermissionFlagsBits.ManageGuild), + return ( + this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS, false) || + guild.me.permissions.has(Permissions.FLAGS.MANAGE_GUILD) ); } @@ -248,16 +263,8 @@ class Invite extends Base { * @readonly */ get expiresAt() { - return this.expiresTimestamp && new Date(this.expiresTimestamp); - } - - /** - * The user who created this invite - * @type {?User} - * @readonly - */ - get inviter() { - return this.inviterId && this.client.users.resolve(this.inviterId); + const { expiresTimestamp } = this; + return expiresTimestamp ? new Date(expiresTimestamp) : null; } /** @@ -266,7 +273,7 @@ class Invite extends Base { * @readonly */ get url() { - return `${RouteBases.invite}/${this.code}`; + return Endpoints.invite(this.client.options.http.invite, this.code); } /** diff --git a/src/structures/InviteStageInstance.js b/src/structures/InviteStageInstance.js index 21ede43..73db63a 100644 --- a/src/structures/InviteStageInstance.js +++ b/src/structures/InviteStageInstance.js @@ -6,7 +6,6 @@ const Base = require('./Base'); /** * Represents the data about a public {@link StageInstance} in an {@link Invite}. * @extends {Base} - * @deprecated */ class InviteStageInstance extends Base { constructor(client, data, channelId, guildId) { diff --git a/src/structures/Message.js b/src/structures/Message.js index dd8d287..d02f3fc 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -1,30 +1,33 @@ 'use strict'; -const { createComponent, Embed } = require('@discordjs/builders'); +const process = require('node:process'); const { Collection } = require('@discordjs/collection'); -const { DiscordSnowflake } = require('@sapphire/snowflake'); -const { - InteractionType, - ChannelType, - MessageType, - MessageFlags, - PermissionFlagsBits, -} = require('discord-api-types/v9'); 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 { NonSystemMessageTypes } = require('../util/Constants'); -const MessageFlagsBitField = require('../util/MessageFlagsBitField'); -const PermissionsBitField = require('../util/PermissionsBitField'); +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'); +/** + * @type {WeakSet} + * @private + * @internal + */ +const deletedMessages = new WeakSet(); +let deprecationEmittedForDeleted = false; + /** * Represents a message on Discord. * @extends {Base} @@ -59,20 +62,20 @@ class Message extends Base { * The timestamp the message was sent at * @type {number} */ - this.createdTimestamp = DiscordSnowflake.timestampFrom(this.id); + this.createdTimestamp = SnowflakeUtil.timestampFrom(this.id); if ('type' in data) { /** * The type of the message * @type {?MessageType} */ - this.type = data.type; + 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 = !NonSystemMessageTypes.includes(this.type); + this.system = SystemMessageTypes.includes(this.type); } else { this.system ??= null; this.type ??= null; @@ -133,9 +136,9 @@ class Message extends Base { if ('embeds' in data) { /** * A list of embeds in the message - e.g. YouTube Player - * @type {Embed[]} + * @type {MessageEmbed[]} */ - this.embeds = data.embeds.map(e => new Embed(e)); + this.embeds = data.embeds.map(e => new Embed(e, true)); } else { this.embeds = this.embeds?.slice() ?? []; } @@ -143,9 +146,9 @@ class Message extends Base { if ('components' in data) { /** * A list of MessageActionRows in the message - * @type {ActionRow[]} + * @type {MessageActionRow[]} */ - this.components = data.components.map(c => createComponent(c)); + this.components = data.components.map(c => BaseMessageComponent.create(c, this.client)); } else { this.components = this.components?.slice() ?? []; } @@ -183,7 +186,7 @@ class Message extends Base { * The timestamp the message was last edited at (if applicable) * @type {?number} */ - this.editedTimestamp = Date.parse(data.edited_timestamp); + this.editedTimestamp = new Date(data.edited_timestamp).getTime(); } else { this.editedTimestamp ??= null; } @@ -283,21 +286,21 @@ class Message extends Base { if ('flags' in data) { /** * Flags that are applied to the message - * @type {Readonly} + * @type {Readonly} */ - this.flags = new MessageFlagsBitField(data.flags).freeze(); + this.flags = new MessageFlags(data.flags).freeze(); } else { - this.flags = new MessageFlagsBitField(this.flags).freeze(); + 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 (`MessageFlags.Crossposted`) - * * {@link MessageType.ChannelFollowAdd} - * * {@link MessageType.ChannelPinnedMessage} - * * {@link MessageType.Reply} - * * {@link MessageType.ThreadStarterMessage} + * * 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 @@ -339,7 +342,7 @@ class Message extends Base { */ this.interaction = { id: data.interaction.id, - type: data.interaction.type, + type: InteractionTypes[data.interaction.type], commandName: data.interaction.name, user: this.client.users._add(data.interaction.user), }; @@ -348,6 +351,36 @@ class Message extends Base { } } + /** + * 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} @@ -391,7 +424,7 @@ class Message extends Base { * @readonly */ get editedAt() { - return this.editedTimestamp && new Date(this.editedTimestamp); + return this.editedTimestamp ? new Date(this.editedTimestamp) : null; } /** @@ -409,7 +442,7 @@ class Message extends Base { * @readonly */ get hasThread() { - return this.flags.has(MessageFlags.HasThread); + return this.flags.has(MessageFlags.FLAGS.HAS_THREAD); } /** @@ -488,7 +521,7 @@ class Message extends Base { /** * @typedef {CollectorOptions} MessageComponentCollectorOptions - * @property {ComponentType} [componentType] The type of component to listen for + * @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 @@ -508,7 +541,7 @@ class Message extends Base { createMessageComponentCollector(options = {}) { return new InteractionCollector(this.client, { ...options, - interactionType: InteractionType.MessageComponent, + interactionType: InteractionTypes.MESSAGE_COMPONENT, message: this, }); } @@ -518,7 +551,7 @@ class Message extends Base { * @typedef {Object} AwaitMessageComponentOptions * @property {CollectorFilter} [filter] The filter applied to this collector * @property {number} [time] Time to wait for an interaction before rejecting - * @property {ComponentType} [componentType] The type of component interaction to collect + * @property {MessageComponentType} [componentType] The type of component interaction to collect */ /** @@ -551,7 +584,9 @@ class Message extends Base { * @readonly */ get editable() { - const precheck = Boolean(this.author.id === this.client.user.id && (!this.guild || this.channel?.viewable)); + 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()) { @@ -566,6 +601,9 @@ class Message extends Base { * @readonly */ get deletable() { + if (deletedMessages.has(this)) { + return false; + } if (!this.guild) { return this.author.id === this.client.user.id; } @@ -577,11 +615,11 @@ class Message extends Base { const permissions = this.channel?.permissionsFor(this.client.user); if (!permissions) return false; // This flag allows deleting even if timed out - if (permissions.has(PermissionFlagsBits.Administrator, false)) return true; + if (permissions.has(Permissions.FLAGS.ADMINISTRATOR, false)) return true; return Boolean( this.author.id === this.client.user.id || - (permissions.has(PermissionFlagsBits.ManageMessages, false) && + (permissions.has(Permissions.FLAGS.MANAGE_MESSAGES, false) && this.guild.me.communicationDisabledUntilTimestamp < Date.now()), ); } @@ -595,9 +633,10 @@ class Message extends Base { const { channel } = this; return Boolean( !this.system && + !deletedMessages.has(this) && (!this.guild || (channel?.viewable && - channel?.permissionsFor(this.client.user)?.has(PermissionFlagsBits.ManageMessages, false))), + channel?.permissionsFor(this.client.user)?.has(Permissions.FLAGS.MANAGE_MESSAGES, false))), ); } @@ -621,15 +660,16 @@ class Message extends Base { */ get crosspostable() { const bitfield = - PermissionFlagsBits.SendMessages | - (this.author.id === this.client.user.id ? PermissionsBitField.defaultBit : PermissionFlagsBits.ManageMessages); + Permissions.FLAGS.SEND_MESSAGES | + (this.author.id === this.client.user.id ? Permissions.defaultBit : Permissions.FLAGS.MANAGE_MESSAGES); const { channel } = this; return Boolean( - channel?.type === ChannelType.GuildNews && - !this.flags.has(MessageFlags.Crossposted) && - this.type === MessageType.Default && + channel?.type === 'GUILD_NEWS' && + !this.flags.has(MessageFlags.FLAGS.CROSSPOSTED) && + this.type === 'DEFAULT' && channel.viewable && - channel.permissionsFor(this.client.user)?.has(bitfield, false), + channel.permissionsFor(this.client.user)?.has(bitfield, false) && + !deletedMessages.has(this), ); } @@ -637,14 +677,13 @@ class Message extends Base { * Options that can be passed into {@link Message#edit}. * @typedef {Object} MessageEditOptions * @property {?string} [content] Content to be edited - * @property {Embed[]|APIEmbed[]} [embeds] Embeds to be added/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 `MessageFlags.SuppressEmbeds` can be edited. + * @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 {ActionRow[]|ActionRowOptions[]} [components] + * @property {MessageActionRow[]|MessageActionRowOptions[]} [components] * Action rows containing interactive components for the message (buttons, select menus) */ @@ -668,7 +707,7 @@ class Message extends Base { * @returns {Promise} * @example * // Crosspost a message - * if (message.channel.type === ChannelType.GuildNews) { + * if (message.channel.type === 'GUILD_NEWS') { * message.crosspost() * .then(() => console.log('Crossposted message')) * .catch(console.error); @@ -681,7 +720,6 @@ class Message extends Base { /** * Pins this message to the channel's pinned messages. - * @param {string} [reason] Reason for pinning * @returns {Promise} * @example * // Pin a message @@ -689,15 +727,14 @@ class Message extends Base { * .then(console.log) * .catch(console.error) */ - async pin(reason) { + async pin() { if (!this.channel) throw new Error('CHANNEL_NOT_CACHED'); - await this.channel.messages.pin(this.id, reason); + await this.channel.messages.pin(this.id); return this; } /** * Unpins this message from the channel's pinned messages. - * @param {string} [reason] Reason for unpinning * @returns {Promise} * @example * // Unpin a message @@ -705,9 +742,9 @@ class Message extends Base { * .then(console.log) * .catch(console.error) */ - async unpin(reason) { + async unpin() { if (!this.channel) throw new Error('CHANNEL_NOT_CACHED'); - await this.channel.messages.unpin(this.id, reason); + await this.channel.messages.unpin(this.id); return this; } @@ -759,8 +796,8 @@ class Message extends Base { /** * Options provided when sending a message as an inline reply. * @typedef {BaseMessageOptions} ReplyMessageOptions - * @property {boolean} [failIfNotExists=this.client.options.failIfNotExists] Whether to error if the referenced - * message does not exist (creates a standard message in this case when false) + * @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 */ @@ -820,7 +857,7 @@ class Message extends Base { */ startThread(options = {}) { if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED')); - if (![ChannelType.GuildText, ChannelType.GuildNews].includes(this.channel.type)) { + 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')); @@ -853,12 +890,12 @@ class Message extends Base { * @returns {Promise} */ suppressEmbeds(suppress = true) { - const flags = new MessageFlagsBitField(this.flags.bitfield); + const flags = new MessageFlags(this.flags.bitfield); if (suppress) { - flags.add(MessageFlags.SuppressEmbeds); + flags.add(MessageFlags.FLAGS.SUPPRESS_EMBEDS); } else { - flags.remove(MessageFlags.SuppressEmbeds); + flags.remove(MessageFlags.FLAGS.SUPPRESS_EMBEDS); } return this.edit({ flags }); @@ -906,8 +943,8 @@ class Message extends Base { if (equal && rawData) { equal = this.mentions.everyone === message.mentions.everyone && - this.createdTimestamp === Date.parse(rawData.timestamp) && - this.editedTimestamp === Date.parse(rawData.edited_timestamp); + this.createdTimestamp === new Date(rawData.timestamp).getTime() && + this.editedTimestamp === new Date(rawData.edited_timestamp).getTime(); } return equal; @@ -946,3 +983,4 @@ class Message extends Base { } exports.Message = Message; +exports.deletedMessages = deletedMessages; diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js new file mode 100644 index 00000000..c44b32a --- /dev/null +++ b/src/structures/MessageActionRow.js @@ -0,0 +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} + */ diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js index 3426a17..79e87bf 100644 --- a/src/structures/MessageAttachment.js +++ b/src/structures/MessageAttachment.js @@ -124,7 +124,7 @@ class MessageAttachment { if ('content_type' in data) { /** - * The media type of this attachment + * This media type of this attachment * @type {?string} */ this.contentType = data.content_type; diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js new file mode 100644 index 00000000..a94c1ec --- /dev/null +++ b/src/structures/MessageButton.js @@ -0,0 +1,165 @@ +'use strict'; + +const BaseMessageComponent = require('./BaseMessageComponent'); +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]; + } +} + +module.exports = MessageButton; diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index 736bd68..34deaff 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -1,7 +1,7 @@ 'use strict'; const Collector = require('./interfaces/Collector'); -const Events = require('../util/Events'); +const { Events } = require('../util/Constants'); /** * @typedef {CollectorOptions} MessageCollectorOptions @@ -46,20 +46,20 @@ class MessageCollector extends Collector { this._handleGuildDeletion = this._handleGuildDeletion.bind(this); this.client.incrementMaxListeners(); - this.client.on(Events.MessageCreate, this.handleCollect); - this.client.on(Events.MessageDelete, this.handleDispose); - this.client.on(Events.MessageBulkDelete, bulkDeleteListener); - this.client.on(Events.ChannelDelete, this._handleChannelDeletion); - this.client.on(Events.ThreadDelete, this._handleThreadDeletion); - this.client.on(Events.GuildDelete, this._handleGuildDeletion); + this.client.on(Events.MESSAGE_CREATE, this.handleCollect); + this.client.on(Events.MESSAGE_DELETE, this.handleDispose); + this.client.on(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); + this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.on(Events.THREAD_DELETE, this._handleThreadDeletion); + this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); this.once('end', () => { - this.client.removeListener(Events.MessageCreate, this.handleCollect); - this.client.removeListener(Events.MessageDelete, this.handleDispose); - this.client.removeListener(Events.MessageBulkDelete, bulkDeleteListener); - this.client.removeListener(Events.ChannelDelete, this._handleChannelDeletion); - this.client.removeListener(Events.ThreadDelete, this._handleThreadDeletion); - this.client.removeListener(Events.GuildDelete, this._handleGuildDeletion); + this.client.removeListener(Events.MESSAGE_CREATE, this.handleCollect); + this.client.removeListener(Events.MESSAGE_DELETE, this.handleDispose); + this.client.removeListener(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); + this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.removeListener(Events.THREAD_DELETE, this._handleThreadDeletion); + this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); this.client.decrementMaxListeners(); }); } diff --git a/src/structures/MessageComponentInteraction.js b/src/structures/MessageComponentInteraction.js index 1e393b3..dae0107 100644 --- a/src/structures/MessageComponentInteraction.js +++ b/src/structures/MessageComponentInteraction.js @@ -3,6 +3,7 @@ const Interaction = require('./Interaction'); const InteractionWebhook = require('./InteractionWebhook'); const InteractionResponses = require('./interfaces/InteractionResponses'); +const { MessageComponentTypes } = require('../util/Constants'); /** * Represents a message component interaction. @@ -33,9 +34,9 @@ class MessageComponentInteraction extends Interaction { /** * The type of component which was interacted with - * @type {ComponentType} + * @type {string} */ - this.componentType = data.data.component_type; + this.componentType = MessageComponentInteraction.resolveType(data.data.component_type); /** * Whether the reply to this interaction has been deferred @@ -80,6 +81,16 @@ class MessageComponentInteraction extends Interaction { .find(component => (component.customId ?? component.custom_id) === this.customId); } + /** + * 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]; + } + // These are here only for documentation purposes - they are implemented by InteractionResponses /* eslint-disable no-empty-function */ deferReply() {} @@ -105,28 +116,3 @@ module.exports = MessageComponentInteraction; * @external APIMessageButton * @see {@link https://discord.com/developers/docs/interactions/message-components#button-object} */ - -/** - * @external ButtonComponent - * @see {@link https://discord.js.org/#/docs/builders/main/class/ButtonComponent} - */ - -/** - * @external SelectMenuComponent - * @see {@link https://discord.js.org/#/docs/builders/main/class/SelectMenuComponent} - */ - -/** - * @external SelectMenuOption - * @see {@link https://discord.js.org/#/docs/builders/main/class/SelectMenuComponent} - */ - -/** - * @external ActionRow - * @see {@link https://discord.js.org/#/docs/builders/main/class/ActionRow} - */ - -/** - * @external Embed - * @see {@link https://discord.js.org/#/docs/builders/main/class/Embed} - */ diff --git a/src/structures/MessageContextMenuCommandInteraction.js b/src/structures/MessageContextMenuCommandInteraction.js deleted file mode 100644 index 1100591..00000000 --- a/src/structures/MessageContextMenuCommandInteraction.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -const ContextMenuCommandInteraction = require('./ContextMenuCommandInteraction'); - -/** - * Represents a message context menu interaction. - * @extends {ContextMenuCommandInteraction} - */ -class MessageContextMenuCommandInteraction extends ContextMenuCommandInteraction { - /** - * The message this interaction was sent from - * @type {Message|APIMessage} - * @readonly - */ - get targetMessage() { - return this.options.getMessage('message'); - } -} - -module.exports = MessageContextMenuCommandInteraction; diff --git a/src/structures/MessageContextMenuInteraction.js b/src/structures/MessageContextMenuInteraction.js new file mode 100644 index 00000000..0855f30 --- /dev/null +++ b/src/structures/MessageContextMenuInteraction.js @@ -0,0 +1,20 @@ +'use strict'; + +const ContextMenuInteraction = require('./ContextMenuInteraction'); + +/** + * Represents a message context menu interaction. + * @extends {ContextMenuInteraction} + */ +class MessageContextMenuInteraction extends ContextMenuInteraction { + /** + * The message this interaction was sent from + * @type {Message|APIMessage} + * @readonly + */ + get targetMessage() { + return this.options.getMessage('message'); + } +} + +module.exports = MessageContextMenuInteraction; diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js new file mode 100644 index 00000000..39fef44 --- /dev/null +++ b/src/structures/MessageEmbed.js @@ -0,0 +1,575 @@ +'use strict'; + +const process = require('node:process'); +const { RangeError } = require('../errors'); +const Util = require('../util/Util'); + +let deprecationEmittedForSetAuthor = false; +let deprecationEmittedForSetFooter = false; + +// TODO: Remove the deprecated code for `setAuthor()` and `setFooter()`. + +/** + * Represents an embed in a message (image/video preview, rich embed, etc.) + */ +class MessageEmbed { + /** + * A `Partial` object is a representation of any existing object. + * This object contains between 0 and all of the original objects parameters. + * This is true regardless of whether the parameters are optional in the base object. + * @typedef {Object} Partial + */ + + /** + * Represents the possible options for a MessageEmbed + * @typedef {Object} MessageEmbedOptions + * @property {string} [title] The title of this embed + * @property {string} [description] The description of this embed + * @property {string} [url] The URL of this embed + * @property {Date|number} [timestamp] The timestamp of this embed + * @property {ColorResolvable} [color] The color of this embed + * @property {EmbedFieldData[]} [fields] The fields of this embed + * @property {Partial} [author] The author of this embed + * @property {Partial} [thumbnail] The thumbnail of this embed + * @property {Partial} [image] The image of this embed + * @property {Partial} [video] The video of this embed + * @property {Partial} [footer] The footer of this embed + */ + + // eslint-disable-next-line valid-jsdoc + /** + * @param {MessageEmbed|MessageEmbedOptions|APIEmbed} [data={}] MessageEmbed to clone or raw embed data + */ + constructor(data = {}, skipValidation = false) { + this.setup(data, skipValidation); + } + + setup(data, skipValidation) { + /** + * The type of this embed, either: + * * `rich` - a generic embed rendered from embed attributes + * * `image` - an image embed + * * `video` - a video embed + * * `gifv` - an animated gif image embed rendered as a video embed + * * `article` - an article embed + * * `link` - a link embed + * @type {string} + * @see {@link https://discord.com/developers/docs/resources/channel#embed-object-embed-types} + * @deprecated + */ + this.type = data.type ?? 'rich'; + + /** + * 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; + + /** + * The timestamp of this embed + * @type {?number} + */ + this.timestamp = 'timestamp' in data ? new Date(data.timestamp).getTime() : null; + + /** + * Represents a field of a MessageEmbed + * @typedef {Object} EmbedField + * @property {string} name The name of this field + * @property {string} value The value of this field + * @property {boolean} inline If this field will be displayed inline + */ + + /** + * The fields of this embed + * @type {EmbedField[]} + */ + this.fields = []; + if (data.fields) { + this.fields = skipValidation ? data.fields.map(Util.cloneObject) : this.constructor.normalizeFields(data.fields); + } + + /** + * Represents the thumbnail of a MessageEmbed + * @typedef {Object} MessageEmbedThumbnail + * @property {string} url URL for this thumbnail + * @property {string} proxyURL ProxyURL for this thumbnail + * @property {number} height Height of this thumbnail + * @property {number} width Width of this thumbnail + */ + + /** + * 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 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; + + /** + * 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, + iconURL: data.author.iconURL ?? data.author.icon_url, + proxyIconURL: data.author.proxyIconURL ?? data.author.proxy_icon_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; + + /** + * Represents the footer field of a MessageEmbed + * @typedef {Object} MessageEmbedFooter + * @property {string} text The text of this footer + * @property {string} iconURL URL of the icon for this footer + * @property {string} proxyIconURL Proxied URL of the icon for this footer + */ + + /** + * The footer of this embed + * @type {?MessageEmbedFooter} + */ + this.footer = data.footer + ? { + text: data.footer.text, + iconURL: data.footer.iconURL ?? data.footer.icon_url, + proxyIconURL: data.footer.proxyIconURL ?? data.footer.proxy_icon_url, + } + : null; + } + + /** + * The date displayed on this embed + * @type {?Date} + * @readonly + */ + get createdAt() { + return this.timestamp ? new Date(this.timestamp) : null; + } + + /** + * The hexadecimal version of the embed color, with a leading hash + * @type {?string} + * @readonly + */ + get hexColor() { + return this.color ? `#${this.color.toString(16).padStart(6, '0')}` : null; + } + + /** + * The accumulated length for the embed title, description, fields, footer text, and author name + * @type {number} + * @readonly + */ + get length() { + return ( + (this.title?.length ?? 0) + + (this.description?.length ?? 0) + + (this.fields.length >= 1 + ? this.fields.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) + : 0) + + (this.footer?.text.length ?? 0) + + (this.author?.name.length ?? 0) + ); + } + + /** + * Checks if this embed is equal to another one by comparing every single one of their properties. + * @param {MessageEmbed|APIEmbed} embed The embed to compare with + * @returns {boolean} + */ + equals(embed) { + return ( + this.type === embed.type && + this.author?.name === embed.author?.name && + this.author?.url === embed.author?.url && + this.author?.iconURL === (embed.author?.iconURL ?? embed.author?.icon_url) && + this.color === embed.color && + this.title === embed.title && + this.description === embed.description && + this.url === embed.url && + this.timestamp === embed.timestamp && + this.fields.length === embed.fields.length && + this.fields.every((field, i) => this._fieldEquals(field, embed.fields[i])) && + this.footer?.text === embed.footer?.text && + this.footer?.iconURL === (embed.footer?.iconURL ?? embed.footer?.icon_url) && + this.image?.url === embed.image?.url && + this.thumbnail?.url === embed.thumbnail?.url && + this.video?.url === embed.video?.url && + this.provider?.name === embed.provider?.name && + this.provider?.url === embed.provider?.url + ); + } + + /** + * Compares two given embed fields to see if they are equal + * @param {EmbedFieldData} field The first field to compare + * @param {EmbedFieldData} other The second field to compare + * @returns {boolean} + * @private + */ + _fieldEquals(field, other) { + return field.name === other.name && field.value === other.value && field.inline === other.inline; + } + + /** + * Adds a field to the embed (max 25). + * @param {string} name The name of this field + * @param {string} value The value of this field + * @param {boolean} [inline=false] If this field will be displayed inline + * @returns {MessageEmbed} + */ + addField(name, value, inline) { + return this.addFields({ name, value, inline }); + } + + /** + * Adds fields to the embed (max 25). + * @param {...EmbedFieldData|EmbedFieldData[]} fields The fields to add + * @returns {MessageEmbed} + */ + addFields(...fields) { + this.fields.push(...this.constructor.normalizeFields(fields)); + return this; + } + + /** + * Removes, replaces, and inserts fields in the embed (max 25). + * @param {number} index The index to start at + * @param {number} deleteCount The number of fields to remove + * @param {...EmbedFieldData|EmbedFieldData[]} [fields] The replacing field objects + * @returns {MessageEmbed} + */ + spliceFields(index, deleteCount, ...fields) { + this.fields.splice(index, deleteCount, ...this.constructor.normalizeFields(...fields)); + return this; + } + + /** + * Sets the embed's fields (max 25). + * @param {...EmbedFieldData|EmbedFieldData[]} fields The fields to set + * @returns {MessageEmbed} + */ + setFields(...fields) { + this.spliceFields(0, this.fields.length, fields); + return this; + } + + /** + * The options to provide for setting an author for a {@link MessageEmbed}. + * @typedef {Object} EmbedAuthorData + * @property {string} name The name of this author. + * @property {string} [url] The URL of this author. + * @property {string} [iconURL] The icon URL 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. + * @param {string} [deprecatedIconURL] The icon URL of this author. + * This parameter is **deprecated**. Use the `options` parameter instead. + * @param {string} [deprecatedURL] The URL of this author. + * This parameter is **deprecated**. Use the `options` parameter instead. + * @returns {MessageEmbed} + */ + setAuthor(options, deprecatedIconURL, deprecatedURL) { + if (options === null) { + this.author = {}; + return this; + } + + if (typeof options === 'string') { + if (!deprecationEmittedForSetAuthor) { + process.emitWarning( + 'Passing strings for MessageEmbed#setAuthor is deprecated. Pass a sole object instead.', + 'DeprecationWarning', + ); + + deprecationEmittedForSetAuthor = true; + } + + options = { name: options, url: deprecatedURL, iconURL: deprecatedIconURL }; + } + + const { name, url, iconURL } = options; + this.author = { name: Util.verifyString(name, RangeError, 'EMBED_AUTHOR_NAME'), url, iconURL }; + 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 + * @returns {MessageEmbed} + */ + setDescription(description) { + this.description = Util.verifyString(description, RangeError, 'EMBED_DESCRIPTION'); + return this; + } + + /** + * The options to provide for setting a footer for a {@link MessageEmbed}. + * @typedef {Object} EmbedFooterData + * @property {string} text The text of the footer. + * @property {string} [iconURL] The icon URL of the footer. + */ + + /** + * Sets the footer of this embed. + * @param {string|EmbedFooterData|null} options The options to provide for the footer. + * Provide `null` to remove the footer data. + * @param {string} [deprecatedIconURL] The icon URL of this footer. + * This parameter is **deprecated**. Use the `options` parameter instead. + * @returns {MessageEmbed} + */ + setFooter(options, deprecatedIconURL) { + if (options === null) { + this.footer = {}; + return this; + } + + if (typeof options === 'string') { + if (!deprecationEmittedForSetFooter) { + process.emitWarning( + 'Passing strings for MessageEmbed#setFooter is deprecated. Pass a sole object instead.', + 'DeprecationWarning', + ); + + deprecationEmittedForSetFooter = true; + } + + options = { text: options, iconURL: deprecatedIconURL }; + } + + const { text, iconURL } = options; + this.footer = { text: Util.verifyString(text, RangeError, 'EMBED_FOOTER_TEXT'), iconURL }; + return this; + } + + /** + * Sets the image of this embed. + * @param {string} url The URL of the image + * @returns {MessageEmbed} + */ + setImage(url) { + this.image = { url }; + return this; + } + + /** + * Sets the thumbnail of this embed. + * @param {string} url The URL of the thumbnail + * @returns {MessageEmbed} + */ + setThumbnail(url) { + this.thumbnail = { url }; + return this; + } + + /** + * Sets the timestamp of this embed. + * @param {Date|number|null} [timestamp=Date.now()] The timestamp or date. + * If `null` then the timestamp will be unset (i.e. when editing an existing {@link MessageEmbed}) + * @returns {MessageEmbed} + */ + setTimestamp(timestamp = Date.now()) { + if (timestamp instanceof Date) timestamp = timestamp.getTime(); + this.timestamp = timestamp; + 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; + } + + /** + * Transforms the embed to a plain object. + * @returns {APIEmbed} The raw data of this embed + */ + toJSON() { + return { + title: this.title, + type: 'rich', + description: this.description, + url: this.url, + timestamp: this.timestamp && new Date(this.timestamp), + color: this.color, + fields: this.fields, + thumbnail: this.thumbnail, + image: this.image, + author: this.author && { + name: this.author.name, + url: this.author.url, + icon_url: this.author.iconURL, + }, + footer: this.footer && { + text: this.footer.text, + icon_url: this.footer.iconURL, + }, + }; + } + + /** + * Normalizes field input and verifies strings. + * @param {string} name The name of the field + * @param {string} value The value of the field + * @param {boolean} [inline=false] Set the field to display inline + * @returns {EmbedField} + */ + static normalizeField(name, value, inline = false) { + return { + name: Util.verifyString(name, RangeError, 'EMBED_FIELD_NAME', false), + value: Util.verifyString(value, RangeError, 'EMBED_FIELD_VALUE', false), + inline, + }; + } + + /** + * @typedef {Object} EmbedFieldData + * @property {string} name The name of this field + * @property {string} value The value of this field + * @property {boolean} [inline] If this field will be displayed inline + */ + + /** + * Normalizes field input and resolves strings. + * @param {...EmbedFieldData|EmbedFieldData[]} fields Fields to normalize + * @returns {EmbedField[]} + */ + static normalizeFields(...fields) { + return fields + .flat(2) + .map(field => + this.normalizeField(field.name, field.value, typeof field.inline === 'boolean' ? field.inline : false), + ); + } +} + +module.exports = MessageEmbed; + +/** + * @external APIEmbed + * @see {@link https://discord.com/developers/docs/resources/channel#embed-object} + */ diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 6f1588d..9b935f9 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -1,6 +1,7 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); +const { ChannelTypes } = require('../util/Constants'); const Util = require('../util/Util'); /** @@ -111,11 +112,13 @@ class MessageMentions { 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: d.type, + type: type ?? 'UNKNOWN', name: d.name, }); } @@ -170,35 +173,28 @@ class MessageMentions { * @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} [ignoreRepliedUser=false] Whether to ignore replied user mention to an user - * @property {boolean} [ignoreEveryone=false] Whether to ignore `@everyone`/`@here` mentions + * @property {boolean} [ignoreEveryone=false] Whether to ignore everyone/here mentions */ /** - * Checks if a user, guild member, thread member, role, or channel is mentioned. - * Takes into account user mentions, role mentions, channel mentions, - * replied user mention, and `@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, ignoreRepliedUser = false, ignoreEveryone = false } = {}) { - const user = this.client.users.resolve(data); - const role = this.guild?.roles.resolve(data); - const channel = this.client.channels.resolve(data); - - if (!ignoreRepliedUser && this.users.has(this.repliedUser?.id) && this.repliedUser?.id === user?.id) return true; - if (!ignoreDirect) { - if (this.users.has(user?.id)) return true; - if (this.roles.has(role?.id)) return true; - if (this.channels.has(channel?.id)) return true; + 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 (user && !ignoreEveryone && this.everyone) return true; - if (!ignoreRoles) { - const member = this.guild?.members.resolve(data); - if (member) { - for (const mentionedRole of this.roles.values()) if (member.roles.cache.has(mentionedRole.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; diff --git a/src/structures/MessagePayload.js b/src/structures/MessagePayload.js index 0713a2a..339b8f9 100644 --- a/src/structures/MessagePayload.js +++ b/src/structures/MessagePayload.js @@ -1,304 +1,270 @@ 'use strict'; const { Buffer } = require('node:buffer'); -const { BaseMessageComponent, MessageEmbed } = require('discord.js'); -const { MessageFlags } = require('discord-api-types/v9'); +const BaseMessageComponent = require('./BaseMessageComponent'); +const MessageEmbed = require('./MessageEmbed'); const { RangeError } = require('../errors'); const DataResolver = require('../util/DataResolver'); -const MessageFlagsBitField = require('../util/MessageFlagsBitField'); +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; + /** + * @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; + /** + * Options passed in from send + * @type {MessageOptions|WebhookMessageOptions} + */ + this.options = options; - /** - * Body sendable to the API - * @type {?APIMessage} - */ - this.body = null; + /** + * Data sendable to the API + * @type {?APIMessage} + */ + this.data = null; - /** - * Files sendable to the API - * @type {?RawFile[]} - */ - this.files = 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 + */ - /** - * 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 - ); - } + /** + * Files sendable to the API + * @type {?MessageFile[]} + */ + this.files = null; + } - /** - * 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 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 Message} - * @type {boolean} - * @readonly - */ - get isMessage() { - const { Message } = require('./Message'); - return this.target instanceof Message; - } + /** + * 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 MessageManager} - * @type {boolean} - * @readonly - */ - get isMessageManager() { - const MessageManager = require('../managers/MessageManager'); - return this.target instanceof MessageManager; - } + /** + * 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 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 - ); - } + /** + * Whether or not the target is a {@link MessageManager} + * @type {boolean} + * @readonly + */ + get isMessageManager() { + const MessageManager = require('../managers/MessageManager'); + return this.target instanceof MessageManager; + } - /** - * 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, - ); - } + /** + * 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; + } - return content; - } - /** - * Resolves the body. - * @returns {MessagePayload} - */ - resolveBody() { - if (this.data) return this; - const isInteraction = this.isInteraction; - const isWebhook = this.isWebhook; + /** + * 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); + } - const content = this.makeContent(); - const tts = Boolean(this.options.tts); + return content; + } - 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'); - } - } + /** + * Resolves data. + * @returns {MessagePayload} + */ + resolveData() { + if (this.data) return this; + const isInteraction = this.isInteraction; + const isWebhook = this.isWebhook; - const components = this.options.components?.map((c) => - BaseMessageComponent.create(c).toJSON(), - ); + const content = this.makeContent(); + const tts = Boolean(this.options.tts); - let username; - let avatarURL; - if (isWebhook) { - username = this.options.username ?? this.target.name; - if (this.options.avatarURL) avatarURL = this.options.avatarURL; - } + 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'); + } + } - let flags; - if ( - typeof this.options.flags !== 'undefined' || - (this.isMessage && typeof this.options.reply === 'undefined') || - this.isMessageManager - ) { - flags = - // eslint-disable-next-line eqeqeq - this.options.flags != null - ? new MessageFlagsBitField(this.options.flags).bitfield - : this.target.flags?.bitfield; - } + const components = this.options.components?.map(c => BaseMessageComponent.create(c).toJSON()); - if (isInteraction && this.options.ephemeral) { - flags |= MessageFlags.Ephemeral; - } + let username; + let avatarURL; + if (isWebhook) { + username = this.options.username ?? this.target.name; + if (this.options.avatarURL) avatarURL = this.options.avatarURL; + } - let allowedMentions = - typeof this.options.allowedMentions === 'undefined' - ? this.target.client.options.allowedMentions - : this.options.allowedMentions; + 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; + } - if (allowedMentions) { - allowedMentions = Util.cloneObject(allowedMentions); - allowedMentions.replied_user = allowedMentions.repliedUser; - delete allowedMentions.repliedUser; - } + let allowedMentions = + typeof this.options.allowedMentions === 'undefined' + ? this.target.client.options.allowedMentions + : this.options.allowedMentions; - 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, - }; - } - } + if (allowedMentions) { + allowedMentions = Util.cloneObject(allowedMentions); + allowedMentions.replied_user = allowedMentions.repliedUser; + delete allowedMentions.repliedUser; + } - 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; - } + 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, + }; + } + } - this.body = { - 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; - } + 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; + } - /** - * Resolves files. - * @returns {Promise} - */ - async resolveFiles() { - if (this.files) return this; + 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; + } - this.files = await Promise.all( - this.options.files?.map((file) => this.constructor.resolveFile(file)) ?? - [], - ); - return this; - } + /** + * Resolves files. + * @returns {Promise} + */ + async resolveFiles() { + if (this.files) 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; + this.files = await Promise.all(this.options.files?.map(file => this.constructor.resolveFile(file)) ?? []); + return this; + } - const findName = (thing) => { - if (typeof thing === 'string') { - return Util.basename(thing); - } + /** + * 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; - if (thing.path) { - return Util.basename(thing.path); - } + const findName = thing => { + if (typeof thing === 'string') { + return Util.basename(thing); + } - return 'file.jpg'; - }; + if (thing.path) { + return Util.basename(thing.path); + } - 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); - } + return 'file.jpg'; + }; - 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 }, - ); - } + 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; @@ -313,8 +279,3 @@ module.exports = MessagePayload; * @external APIMessage * @see {@link https://discord.com/developers/docs/resources/channel#message-object} */ - -/** - * @external RawFile - * @see {@link https://discord.js.org/#/docs/rest/main/typedef/RawFile} - */ \ No newline at end of file diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index 729b284..8a294be 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -1,6 +1,5 @@ 'use strict'; -const { Routes } = require('discord-api-types/v9'); const GuildEmoji = require('./GuildEmoji'); const ReactionEmoji = require('./ReactionEmoji'); const ReactionUserManager = require('../managers/ReactionUserManager'); @@ -57,7 +56,11 @@ class MessageReaction { * @returns {Promise} */ async remove() { - await this.client.api.channels(this.message.channelId).messages(this.message.id).reactions(this._emoji.identifier).delete() + await this.client.api + .channels(this.message.channelId) + .messages(this.message.id) + .reactions(this._emoji.identifier) + .delete(); return this; } @@ -111,7 +114,7 @@ class MessageReaction { if (this.partial) return; this.users.cache.set(user.id, user); if (!this.me || user.id !== this.message.client.user.id || this.count === 0) this.count++; - this.me ||= user.id === this.message.client.user.id; + this.me ??= user.id === this.message.client.user.id; } _remove(user) { diff --git a/src/structures/MessageSelectMenu.js b/src/structures/MessageSelectMenu.js new file mode 100644 index 00000000..ae1e7a6 --- /dev/null +++ b/src/structures/MessageSelectMenu.js @@ -0,0 +1,212 @@ +'use strict'; + +const BaseMessageComponent = require('./BaseMessageComponent'); +const { MessageComponentTypes } = require('../util/Constants'); +const Util = require('../util/Util'); + +/** + * 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)); + } +} + +module.exports = MessageSelectMenu; diff --git a/src/structures/NewsChannel.js b/src/structures/NewsChannel.js index 3833ef0..e2ef0a8 100644 --- a/src/structures/NewsChannel.js +++ b/src/structures/NewsChannel.js @@ -1,6 +1,5 @@ 'use strict'; -const { Routes } = require('discord-api-types/v9'); const BaseGuildTextChannel = require('./BaseGuildTextChannel'); const { Error } = require('../errors'); @@ -15,7 +14,7 @@ class NewsChannel extends BaseGuildTextChannel { * @param {string} [reason] Reason for creating the webhook * @returns {Promise} * @example - * if (channel.type === ChannelType.GuildNews) { + * if (channel.type === 'GUILD_NEWS') { * channel.addFollower('222197033908436994', 'Important announcements') * .then(() => console.log('Added follower')) * .catch(console.error); @@ -24,7 +23,7 @@ class NewsChannel extends BaseGuildTextChannel { async addFollower(channel, reason) { const channelId = this.guild.channels.resolveId(channel); if (!channelId) throw new Error('GUILD_CHANNEL_RESOLVE'); - await this.client.api.channels(this.id).followers.post({ body: { webhook_channel_id: channelId }, reason }); + await this.client.api.channels(this.id).followers.post({ data: { webhook_channel_id: channelId }, reason }); return this; } } diff --git a/src/structures/OAuth2Guild.js b/src/structures/OAuth2Guild.js index d5104ac..3f44ad4 100644 --- a/src/structures/OAuth2Guild.js +++ b/src/structures/OAuth2Guild.js @@ -1,7 +1,7 @@ 'use strict'; const BaseGuild = require('./BaseGuild'); -const PermissionsBitField = require('../util/PermissionsBitField'); +const Permissions = require('../util/Permissions'); /** * A partial guild received when using {@link GuildManager#fetch} to fetch multiple guilds. @@ -19,9 +19,9 @@ class OAuth2Guild extends BaseGuild { /** * The permissions that the client user has in this guild - * @type {Readonly} + * @type {Readonly} */ - this.permissions = new PermissionsBitField(BigInt(data.permissions)).freeze(); + this.permissions = new Permissions(BigInt(data.permissions)).freeze(); } } diff --git a/src/structures/PartialGroupDMChannel.js b/src/structures/PartialGroupDMChannel.js index f604e72..715d0b6 100644 --- a/src/structures/PartialGroupDMChannel.js +++ b/src/structures/PartialGroupDMChannel.js @@ -38,11 +38,11 @@ class PartialGroupDMChannel extends Channel { /** * The URL to this channel's icon. - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {StaticImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - iconURL(options = {}) { - return this.icon && this.client.rest.cdn.channelIcon(this.id, this.icon, options); + iconURL({ format, size } = {}) { + return this.icon && this.client.rest.cdn.GDMIcon(this.id, this.icon, format, size); } delete() { diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index 01d141e..017bf26 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -1,10 +1,10 @@ 'use strict'; -const { OverwriteType } = require('discord-api-types/v9'); const Base = require('./Base'); const { Role } = require('./Role'); const { TypeError } = require('../errors'); -const PermissionsBitField = require('../util/PermissionsBitField'); +const { OverwriteTypes } = require('../util/Constants'); +const Permissions = require('../util/Permissions'); /** * Represents a permission overwrite for a role or member in a guild channel. @@ -37,23 +37,23 @@ class PermissionOverwrites extends Base { * The type of this overwrite * @type {OverwriteType} */ - this.type = data.type; + this.type = typeof data.type === 'number' ? OverwriteTypes[data.type] : data.type; } if ('deny' in data) { /** * The permissions that are denied for the user or role. - * @type {Readonly} + * @type {Readonly} */ - this.deny = new PermissionsBitField(BigInt(data.deny)).freeze(); + this.deny = new Permissions(BigInt(data.deny)).freeze(); } if ('allow' in data) { /** * The permissions that are allowed for the user or role. - * @type {Readonly} + * @type {Readonly} */ - this.allow = new PermissionsBitField(BigInt(data.allow)).freeze(); + this.allow = new Permissions(BigInt(data.allow)).freeze(); } } @@ -71,7 +71,7 @@ class PermissionOverwrites extends Base { * .catch(console.error); */ async edit(options, reason) { - await this.channel.permissionOverwrites.upsert(this.id, options, { type: this.type, reason }, this); + await this.channel.permissionOverwrites.upsert(this.id, options, { type: OverwriteTypes[this.type], reason }, this); return this; } @@ -88,7 +88,7 @@ class PermissionOverwrites extends Base { toJSON() { return { id: this.id, - type: this.type, + type: OverwriteTypes[this.type], allow: this.allow, deny: this.deny, }; @@ -98,9 +98,9 @@ class PermissionOverwrites extends Base { * An object mapping permission flags to `true` (enabled), `null` (unset) or `false` (disabled). * ```js * { - * 'SendMessages': true, - * 'EmbedLinks': null, - * 'AttachFiles': false, + * 'SEND_MESSAGES': true, + * 'EMBED_LINKS': null, + * 'ATTACH_FILES': false, * } * ``` * @typedef {Object} PermissionOverwriteOptions @@ -108,8 +108,8 @@ class PermissionOverwrites extends Base { /** * @typedef {Object} ResolvedOverwriteOptions - * @property {PermissionsBitField} allow The allowed permissions - * @property {PermissionsBitField} deny The denied permissions + * @property {Permissions} allow The allowed permissions + * @property {Permissions} deny The denied permissions */ /** @@ -119,8 +119,8 @@ class PermissionOverwrites extends Base { * @returns {ResolvedOverwriteOptions} */ static resolveOverwriteOptions(options, { allow, deny } = {}) { - allow = new PermissionsBitField(allow); - deny = new PermissionsBitField(deny); + allow = new Permissions(allow); + deny = new Permissions(deny); for (const [perm, value] of Object.entries(options)) { if (value === true) { @@ -171,24 +171,24 @@ class PermissionOverwrites extends Base { */ static resolve(overwrite, guild) { if (overwrite instanceof this) return overwrite.toJSON(); - if (typeof overwrite.id === 'string' && overwrite.type in OverwriteType) { + if (typeof overwrite.id === 'string' && overwrite.type in OverwriteTypes) { return { id: overwrite.id, - type: overwrite.type, - allow: PermissionsBitField.resolve(overwrite.allow ?? PermissionsBitField.defaultBit).toString(), - deny: PermissionsBitField.resolve(overwrite.deny ?? PermissionsBitField.defaultBit).toString(), + type: OverwriteTypes[overwrite.type], + allow: Permissions.resolve(overwrite.allow ?? Permissions.defaultBit).toString(), + deny: Permissions.resolve(overwrite.deny ?? Permissions.defaultBit).toString(), }; } const userOrRole = guild.roles.resolve(overwrite.id) ?? guild.client.users.resolve(overwrite.id); if (!userOrRole) throw new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role'); - const type = userOrRole instanceof Role ? OverwriteType.Role : OverwriteType.Member; + const type = userOrRole instanceof Role ? OverwriteTypes.role : OverwriteTypes.member; return { id: userOrRole.id, type, - allow: PermissionsBitField.resolve(overwrite.allow ?? PermissionsBitField.defaultBit).toString(), - deny: PermissionsBitField.resolve(overwrite.deny ?? PermissionsBitField.defaultBit).toString(), + allow: Permissions.resolve(overwrite.allow ?? Permissions.defaultBit).toString(), + deny: Permissions.resolve(overwrite.deny ?? Permissions.defaultBit).toString(), }; } } diff --git a/src/structures/Presence.js b/src/structures/Presence.js index e7da989..ce7620b 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -2,7 +2,8 @@ const Base = require('./Base'); const { Emoji } = require('./Emoji'); -const ActivityFlagsBitField = require('../util/ActivityFlagsBitField'); +const ActivityFlags = require('../util/ActivityFlags'); +const { ActivityTypes } = require('../util/Constants'); const Util = require('../util/Util'); /** @@ -167,7 +168,7 @@ class Activity { * The activity status's type * @type {ActivityType} */ - this.type = data.type; + this.type = typeof data.type === 'number' ? ActivityTypes[data.type] : data.type; /** * If the activity is being streamed, a link to the stream @@ -244,9 +245,9 @@ class Activity { /** * Flags that describe the activity - * @type {Readonly} + * @type {Readonly} */ - this.flags = new ActivityFlagsBitField(data.flags).freeze(); + this.flags = new ActivityFlags(data.flags).freeze(); /** * Emoji for a custom activity @@ -270,7 +271,7 @@ class Activity { * Creation date of the activity * @type {number} */ - this.createdTimestamp = Date.parse(data.created_at); + this.createdTimestamp = new Date(data.created_at).getTime(); } /** @@ -346,48 +347,35 @@ class RichPresenceAssets { /** * Gets the URL of the small image asset - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {StaticImageURLOptions} [options] Options for the image URL * @returns {?string} */ - smallImageURL(options = {}) { - if (!this.smallImage) return null; - if (this.smallImage.includes(':')) { - const [platform, id] = this.smallImage.split(':'); - switch (platform) { - case 'mp': - return `https://media.discordapp.net/${id}`; - default: - return null; - } - } - - return this.activity.presence.client.rest.cdn.appAsset(this.activity.applicationId, this.smallImage, options); + smallImageURL({ format, size } = {}) { + return ( + this.smallImage && + this.activity.presence.client.rest.cdn.AppAsset(this.activity.applicationId, this.smallImage, { + format, + size, + }) + ); } /** * Gets the URL of the large image asset - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {StaticImageURLOptions} [options] Options for the image URL * @returns {?string} */ - largeImageURL(options = {}) { + largeImageURL({ format, size } = {}) { if (!this.largeImage) return null; - if (this.largeImage.includes(':')) { - const [platform, id] = this.largeImage.split(':'); - switch (platform) { - case 'mp': - return `https://media.discordapp.net/${id}`; - case 'spotify': - return `https://i.scdn.co/image/${id}`; - case 'youtube': - return `https://i.ytimg.com/vi/${id}/hqdefault_live.jpg`; - case 'twitch': - return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${id}.png`; - default: - return null; - } + if (/^spotify:/.test(this.largeImage)) { + return `https://i.scdn.co/image/${this.largeImage.slice(8)}`; + } else if (/^twitch:/.test(this.largeImage)) { + return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${this.largeImage.slice(7)}.png`; } - - return this.activity.presence.client.rest.cdn.appAsset(this.activity.applicationId, this.largeImage, options); + return this.activity.presence.client.rest.cdn.AppAsset(this.activity.applicationId, this.largeImage, { + format, + size, + }); } } diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js index 0c0b9e0..1b0b5f5 100644 --- a/src/structures/ReactionCollector.js +++ b/src/structures/ReactionCollector.js @@ -2,7 +2,7 @@ const { Collection } = require('@discordjs/collection'); const Collector = require('./interfaces/Collector'); -const Events = require('../util/Events'); +const { Events } = require('../util/Constants'); /** * @typedef {CollectorOptions} ReactionCollectorOptions @@ -57,24 +57,24 @@ class ReactionCollector extends Collector { }; this.client.incrementMaxListeners(); - this.client.on(Events.MessageReactionAdd, this.handleCollect); - this.client.on(Events.MessageReactionRemove, this.handleDispose); - this.client.on(Events.MessageReactionRemoveAll, this.empty); - this.client.on(Events.MessageDelete, this._handleMessageDeletion); - this.client.on(Events.MessageBulkDelete, bulkDeleteListener); - this.client.on(Events.ChannelDelete, this._handleChannelDeletion); - this.client.on(Events.ThreadDelete, this._handleThreadDeletion); - this.client.on(Events.GuildDelete, this._handleGuildDeletion); + this.client.on(Events.MESSAGE_REACTION_ADD, this.handleCollect); + this.client.on(Events.MESSAGE_REACTION_REMOVE, this.handleDispose); + this.client.on(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty); + this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion); + this.client.on(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); + this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.on(Events.THREAD_DELETE, this._handleThreadDeletion); + this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); this.once('end', () => { - this.client.removeListener(Events.MessageReactionAdd, this.handleCollect); - this.client.removeListener(Events.MessageReactionRemove, this.handleDispose); - this.client.removeListener(Events.MessageReactionRemoveAll, this.empty); - this.client.removeListener(Events.MessageDelete, this._handleMessageDeletion); - this.client.removeListener(Events.MessageBulkDelete, bulkDeleteListener); - this.client.removeListener(Events.ChannelDelete, this._handleChannelDeletion); - this.client.removeListener(Events.ThreadDelete, this._handleThreadDeletion); - this.client.removeListener(Events.GuildDelete, this._handleGuildDeletion); + this.client.removeListener(Events.MESSAGE_REACTION_ADD, this.handleCollect); + this.client.removeListener(Events.MESSAGE_REACTION_REMOVE, this.handleDispose); + this.client.removeListener(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty); + this.client.removeListener(Events.MESSAGE_DELETE, this._handleMessageDeletion); + this.client.removeListener(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); + this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.removeListener(Events.THREAD_DELETE, this._handleThreadDeletion); + this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); this.client.decrementMaxListeners(); }); diff --git a/src/structures/Role.js b/src/structures/Role.js index 5325973..f0ee3a8 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -1,10 +1,21 @@ 'use strict'; -const { DiscordSnowflake } = require('@sapphire/snowflake'); -const { PermissionFlagsBits } = require('discord-api-types/v9'); +const process = require('node:process'); const Base = require('./Base'); const { Error } = require('../errors'); -const PermissionsBitField = require('../util/PermissionsBitField'); +const Permissions = require('../util/Permissions'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); +const Util = require('../util/Util'); + +let deprecationEmittedForComparePositions = false; + +/** + * @type {WeakSet} + * @private + * @internal + */ +const deletedRoles = new WeakSet(); +let deprecationEmittedForDeleted = false; /** * Represents a role on Discord. @@ -76,9 +87,9 @@ class Role extends Base { if ('permissions' in data) { /** * The permissions of the role - * @type {Readonly} + * @type {Readonly} */ - this.permissions = new PermissionsBitField(BigInt(data.permissions)).freeze(); + this.permissions = new Permissions(BigInt(data.permissions)).freeze(); } if ('managed' in data) { @@ -128,7 +139,7 @@ class Role extends Base { * @readonly */ get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); + return SnowflakeUtil.timestampFrom(this.id); } /** @@ -140,6 +151,36 @@ class Role extends Base { return new Date(this.createdTimestamp); } + /** + * Whether or not the role 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( + 'Role#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + return deletedRoles.has(this); + } + + set deleted(value) { + if (!deprecationEmittedForDeleted) { + deprecationEmittedForDeleted = true; + process.emitWarning( + 'Role#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + if (value) deletedRoles.add(this); + else deletedRoles.delete(this); + } + /** * The hexadecimal version of the role color, with a leading hashtag * @type {string} @@ -166,7 +207,7 @@ class Role extends Base { get editable() { if (this.managed) return false; const clientMember = this.guild.members.resolve(this.client.user); - if (!clientMember.permissions.has(PermissionFlagsBits.ManageRoles)) return false; + if (!clientMember.permissions.has(Permissions.FLAGS.MANAGE_ROLES)) return false; return clientMember.roles.highest.comparePositionTo(this) > 0; } @@ -225,7 +266,7 @@ class Role extends Base { * taking into account permission overwrites. * @param {GuildChannel|Snowflake} channel The guild channel to use as context * @param {boolean} [checkAdmin=true] Whether having `ADMINISTRATOR` will return all permissions - * @returns {Readonly} + * @returns {Readonly} */ permissionsIn(channel, checkAdmin = true) { channel = this.guild.channels.resolve(channel); @@ -285,7 +326,7 @@ class Role extends Base { * @returns {Promise} * @example * // Set the permissions of the role - * role.setPermissions([PermissionFlagsBits.KickMembers, PermissionFlagsBits.BanMembers]) + * role.setPermissions([Permissions.FLAGS.KICK_MEMBERS, Permissions.FLAGS.BAN_MEMBERS]) * .then(updated => console.log(`Updated permissions to ${updated.permissions.bitfield}`)) * .catch(console.error); * @example @@ -358,8 +399,20 @@ class Role extends Base { * .then(updated => console.log(`Role position: ${updated.position}`)) * .catch(console.error); */ - setPosition(position, options = {}) { - return this.guild.roles.setPosition(this, position, options); + async setPosition(position, { relative, reason } = {}) { + const updatedRoles = await Util.setPosition( + this, + position, + relative, + this.guild._sortedRoles(), + this.client.api.guilds(this.guild.id).roles, + reason, + ); + this.client.actions.GuildRolesPositionUpdate.handle({ + guild_id: this.guild.id, + roles: updatedRoles, + }); + return this; } /** @@ -379,11 +432,12 @@ class Role extends Base { /** * A link to the role's icon - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {StaticImageURLOptions} [options={}] Options for the image URL * @returns {?string} */ - iconURL(options = {}) { - return this.icon && this.client.rest.cdn.roleIcon(this.id, this.icon, options); + iconURL({ format, size } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.RoleIcon(this.id, this.icon, format, size); } /** @@ -426,9 +480,31 @@ class Role extends Base { permissions: this.permissions.toJSON(), }; } + + /** + * Compares the positions of two roles. + * @param {Role} role1 First role to compare + * @param {Role} role2 Second role to compare + * @returns {number} Negative number if the first role's position is lower (second role's is higher), + * positive number if the first's is higher (second's is lower), 0 if equal + * @deprecated Use {@link RoleManager#comparePositions} instead. + */ + static comparePositions(role1, role2) { + if (!deprecationEmittedForComparePositions) { + process.emitWarning( + 'The Role.comparePositions method is deprecated. Use RoleManager#comparePositions instead.', + 'DeprecationWarning', + ); + + deprecationEmittedForComparePositions = true; + } + + return role1.guild.roles.comparePositions(role1, role2); + } } exports.Role = Role; +exports.deletedRoles = deletedRoles; /** * @external APIRole diff --git a/src/structures/SelectMenuComponent.js b/src/structures/SelectMenuComponent.js deleted file mode 100644 index 7758962..00000000 --- a/src/structures/SelectMenuComponent.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const { SelectMenuComponent: BuildersSelectMenuComponent } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); - -class SelectMenuComponent extends BuildersSelectMenuComponent { - constructor(data) { - super(Transformers.toSnakeCase(data)); - } -} - -module.exports = SelectMenuComponent; diff --git a/src/structures/StageInstance.js b/src/structures/StageInstance.js index 883ee54..040d1e0 100644 --- a/src/structures/StageInstance.js +++ b/src/structures/StageInstance.js @@ -1,7 +1,17 @@ 'use strict'; -const { DiscordSnowflake } = require('@sapphire/snowflake'); +const process = require('node:process'); const Base = require('./Base'); +const { PrivacyLevels } = require('../util/Constants'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); + +/** + * @type {WeakSet} + * @private + * @internal + */ +const deletedStageInstances = new WeakSet(); +let deprecationEmittedForDeleted = false; /** * Represents a stage instance. @@ -48,16 +58,15 @@ class StageInstance extends Base { if ('privacy_level' in data) { /** * The privacy level of the stage instance - * @type {StageInstancePrivacyLevel} + * @type {PrivacyLevel} */ - this.privacyLevel = data.privacy_level; + this.privacyLevel = PrivacyLevels[data.privacy_level]; } if ('discoverable_disabled' in data) { /** * Whether or not stage discovery is disabled * @type {?boolean} - * @deprecated See https://github.com/discord/discord-api-docs/pull/4296 for more information */ this.discoverableDisabled = data.discoverable_disabled; } else { @@ -74,6 +83,36 @@ class StageInstance extends Base { return this.client.channels.resolve(this.channelId); } + /** + * Whether or not the stage instance 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( + 'StageInstance#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + return deletedStageInstances.has(this); + } + + set deleted(value) { + if (!deprecationEmittedForDeleted) { + deprecationEmittedForDeleted = true; + process.emitWarning( + 'StageInstance#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + if (value) deletedStageInstances.add(this); + else deletedStageInstances.delete(this); + } + /** * The guild this stage instance belongs to * @type {?Guild} @@ -109,6 +148,7 @@ class StageInstance extends Base { async delete() { await this.guild.stageInstances.delete(this.channelId); const clone = this._clone(); + deletedStageInstances.add(clone); return clone; } @@ -132,7 +172,7 @@ class StageInstance extends Base { * @readonly */ get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); + return SnowflakeUtil.timestampFrom(this.id); } /** @@ -146,3 +186,4 @@ class StageInstance extends Base { } exports.StageInstance = StageInstance; +exports.deletedStageInstances = deletedStageInstances; diff --git a/src/structures/Sticker.js b/src/structures/Sticker.js index c58fafd..a334737 100644 --- a/src/structures/Sticker.js +++ b/src/structures/Sticker.js @@ -1,8 +1,17 @@ 'use strict'; -const { DiscordSnowflake } = require('@sapphire/snowflake'); -const { Routes, StickerFormatType } = require('discord-api-types/v9'); +const process = require('node:process'); const Base = require('./Base'); +const { StickerFormatTypes, StickerTypes } = require('../util/Constants'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); + +/** + * @type {WeakSet} + * @private + * @internal + */ +const deletedStickers = new WeakSet(); +let deprecationEmittedForDeleted = false; /** * Represents a Sticker. @@ -37,7 +46,7 @@ class Sticker extends Base { * The type of the sticker * @type {?StickerType} */ - this.type = sticker.type; + this.type = StickerTypes[sticker.type]; } else { this.type ??= null; } @@ -47,7 +56,7 @@ class Sticker extends Base { * The format of the sticker * @type {StickerFormatType} */ - this.format = sticker.format_type; + this.format = StickerFormatTypes[sticker.format_type]; } if ('name' in sticker) { @@ -125,7 +134,7 @@ class Sticker extends Base { * @readonly */ get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); + return SnowflakeUtil.timestampFrom(this.id); } /** @@ -137,6 +146,36 @@ class Sticker extends Base { return new Date(this.createdTimestamp); } + /** + * Whether or not the sticker 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( + 'Sticker#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + return deletedStickers.has(this); + } + + set deleted(value) { + if (!deprecationEmittedForDeleted) { + deprecationEmittedForDeleted = true; + process.emitWarning( + 'Sticker#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.', + 'DeprecationWarning', + ); + } + + if (value) deletedStickers.add(this); + else deletedStickers.delete(this); + } + /** * Whether this sticker is partial * @type {boolean} @@ -157,13 +196,11 @@ class Sticker extends Base { /** * A link to the sticker - * If the sticker's format is {@link StickerFormatType.Lottie}, it returns - * the URL of the Lottie JSON file. + * If the sticker's format is LOTTIE, it returns the URL of the Lottie JSON file. * @type {string} - * @readonly */ get url() { - return this.client.rest.cdn.sticker(this.id, this.format === StickerFormatType.Lottie ? 'json' : 'png'); + return this.client.rest.cdn.Sticker(this.id, this.format); } /** @@ -191,7 +228,10 @@ class Sticker extends Base { async fetchUser() { if (this.partial) await this.fetch(); if (!this.guildId) throw new Error('NOT_GUILD_STICKER'); - return this.guild.stickers.fetchUser(this); + + const data = await this.client.api.guilds(this.guildId).stickers(this.id).get(); + this._patch(data); + return this.user; } /** @@ -264,6 +304,7 @@ class Sticker extends Base { } exports.Sticker = Sticker; +exports.deletedStickers = deletedStickers; /** * @external APISticker diff --git a/src/structures/StickerPack.js b/src/structures/StickerPack.js index 7e599b7..6fd6a5b 100644 --- a/src/structures/StickerPack.js +++ b/src/structures/StickerPack.js @@ -1,9 +1,9 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); const { Sticker } = require('./Sticker'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents a pack of standard stickers. @@ -61,7 +61,7 @@ class StickerPack extends Base { * @readonly */ get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); + return SnowflakeUtil.timestampFrom(this.id); } /** @@ -84,11 +84,11 @@ class StickerPack extends Base { /** * The URL to this sticker pack's banner. - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {StaticImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - bannerURL(options = {}) { - return this.bannerId && this.client.rest.cdn.stickerPackBanner(this.bannerId, options); + bannerURL({ format, size } = {}) { + return this.bannerId && this.client.rest.cdn.StickerPackBanner(this.bannerId, format, size); } } diff --git a/src/structures/Team.js b/src/structures/Team.js index 98eb199..ccb8175 100644 --- a/src/structures/Team.js +++ b/src/structures/Team.js @@ -1,9 +1,9 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); const TeamMember = require('./TeamMember'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents a Client OAuth2 Application Team. @@ -76,7 +76,7 @@ class Team extends Base { * @readonly */ get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); + return SnowflakeUtil.timestampFrom(this.id); } /** @@ -90,11 +90,12 @@ class Team extends Base { /** * A link to the team's icon. - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {StaticImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - iconURL(options = {}) { - return this.icon && this.client.rest.cdn.teamIcon(this.id, this.icon, options); + iconURL({ format, size } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.TeamIcon(this.id, this.icon, { format, size }); } /** diff --git a/src/structures/TeamMember.js b/src/structures/TeamMember.js index 9270418..9bd5993 100644 --- a/src/structures/TeamMember.js +++ b/src/structures/TeamMember.js @@ -1,6 +1,7 @@ 'use strict'; const Base = require('./Base'); +const { MembershipStates } = require('../util/Constants'); /** * Represents a Client OAuth2 Application Team Member. @@ -31,9 +32,9 @@ class TeamMember extends Base { if ('membership_state' in data) { /** * The permissions this Team Member has with regard to the team - * @type {TeamMemberMembershipState} + * @type {MembershipState} */ - this.membershipState = data.membership_state; + this.membershipState = MembershipStates[data.membership_state]; } if ('user' in data) { diff --git a/src/structures/ThreadChannel.js b/src/structures/ThreadChannel.js index 397f646..7c45cbc 100644 --- a/src/structures/ThreadChannel.js +++ b/src/structures/ThreadChannel.js @@ -1,11 +1,11 @@ 'use strict'; -const { ChannelType, PermissionFlagsBits, Routes } = require('discord-api-types/v9'); const { Channel } = require('./Channel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const { RangeError } = require('../errors'); const MessageManager = require('../managers/MessageManager'); const ThreadMemberManager = require('../managers/ThreadMemberManager'); +const Permissions = require('../util/Permissions'); /** * Represents a thread channel on Discord. @@ -79,7 +79,7 @@ class ThreadChannel extends Channel { * Always `null` in public threads * @type {?boolean} */ - this.invitable = this.type === ChannelType.GuildPrivateThread ? data.thread_metadata.invitable ?? false : null; + this.invitable = this.type === 'GUILD_PRIVATE_THREAD' ? data.thread_metadata.invitable ?? false : null; /** * Whether the thread is archived @@ -99,12 +99,7 @@ class ThreadChannel extends Channel { * created * @type {?number} */ - this.archiveTimestamp = Date.parse(data.thread_metadata.archive_timestamp); - - if ('create_timestamp' in data.thread_metadata) { - // Note: this is needed because we can't assign directly to getters - this._createdTimestamp = Date.parse(data.thread_metadata.create_timestamp); - } + this.archiveTimestamp = new Date(data.thread_metadata.archive_timestamp).getTime(); } else { this.locked ??= null; this.archived ??= null; @@ -113,8 +108,6 @@ class ThreadChannel extends Channel { this.invitable ??= null; } - this._createdTimestamp ??= this.type === ChannelType.GuildPrivateThread ? super.createdTimestamp : null; - if ('owner_id' in data) { /** * The id of the member who created this thread @@ -140,7 +133,7 @@ class ThreadChannel extends Channel { * The timestamp when the last pinned message was pinned, if there was one * @type {?number} */ - this.lastPinTimestamp = data.last_pin_timestamp ? Date.parse(data.last_pin_timestamp) : null; + this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; } else { this.lastPinTimestamp ??= null; } @@ -183,16 +176,6 @@ class ThreadChannel extends Channel { if (data.messages) for (const message of data.messages) this.messages._add(message); } - /** - * The timestamp when this thread was created. This isn't available for threads - * created before 2022-01-09 - * @type {?number} - * @readonly - */ - get createdTimestamp() { - return this._createdTimestamp; - } - /** * A collection of associated guild member objects of this thread's members * @type {Collection} @@ -209,16 +192,8 @@ class ThreadChannel extends Channel { * @readonly */ get archivedAt() { - return this.archiveTimestamp && new Date(this.archiveTimestamp); - } - - /** - * The time the thread was created at - * @type {?Date} - * @readonly - */ - get createdAt() { - return this.createdTimestamp && new Date(this.createdTimestamp); + if (!this.archiveTimestamp) return null; + return new Date(this.archiveTimestamp); } /** @@ -253,7 +228,7 @@ class ThreadChannel extends Channel { * account. * @param {GuildMemberResolvable|RoleResolvable} memberOrRole The member or role to obtain the overall permissions for * @param {boolean} [checkAdmin=true] Whether having `ADMINISTRATOR` will return all permissions - * @returns {?Readonly} + * @returns {?Readonly} */ permissionsFor(memberOrRole, checkAdmin) { return this.parent?.permissionsFor(memberOrRole, checkAdmin) ?? null; @@ -297,7 +272,7 @@ class ThreadChannel extends Channel { * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the thread in seconds * @property {boolean} [locked] Whether the thread is locked * @property {boolean} [invitable] Whether non-moderators can add other non-moderators to a thread - * Can only be edited on {@link ChannelType.GuildPrivateThread} + * Can only be edited on `GUILD_PRIVATE_THREAD` */ /** @@ -322,13 +297,13 @@ class ThreadChannel extends Channel { } } const newData = await this.client.api.channels(this.id).patch({ - body: { + data: { name: (data.name ?? this.name).trim(), archived: data.archived, auto_archive_duration: autoArchiveDuration, rate_limit_per_user: data.rateLimitPerUser, locked: data.locked, - invitable: this.type === ChannelType.GuildPrivateThread ? data.invitable : undefined, + invitable: this.type === 'GUILD_PRIVATE_THREAD' ? data.invitable : undefined, }, reason, }); @@ -377,9 +352,7 @@ class ThreadChannel extends Channel { * @returns {Promise} */ setInvitable(invitable = true, reason) { - if (this.type !== ChannelType.GuildPrivateThread) { - return Promise.reject(new RangeError('THREAD_INVITABLE_TYPE', this.type)); - } + if (this.type !== 'GUILD_PRIVATE_THREAD') return Promise.reject(new RangeError('THREAD_INVITABLE_TYPE', this.type)); return this.edit({ invitable }, reason); } @@ -440,8 +413,7 @@ class ThreadChannel extends Channel { */ get editable() { return ( - (this.ownerId === this.client.user.id && (this.type !== ChannelType.GuildPrivateThread || this.joined)) || - this.manageable + (this.ownerId === this.client.user.id && (this.type !== 'GUILD_PRIVATE_THREAD' || this.joined)) || this.manageable ); } @@ -455,9 +427,7 @@ class ThreadChannel extends Channel { !this.archived && !this.joined && this.permissionsFor(this.client.user)?.has( - this.type === ChannelType.GuildPrivateThread - ? PermissionFlagsBits.ManageThreads - : PermissionFlagsBits.ViewChannel, + this.type === 'GUILD_PRIVATE_THREAD' ? Permissions.FLAGS.MANAGE_THREADS : Permissions.FLAGS.VIEW_CHANNEL, false, ) ); @@ -472,11 +442,11 @@ class ThreadChannel extends Channel { const permissions = this.permissionsFor(this.client.user); if (!permissions) return false; // This flag allows managing even if timed out - if (permissions.has(PermissionFlagsBits.Administrator, false)) return true; + if (permissions.has(Permissions.FLAGS.ADMINISTRATOR, false)) return true; return ( this.guild.me.communicationDisabledUntilTimestamp < Date.now() && - permissions.has(PermissionFlagsBits.ManageThreads, false) + permissions.has(Permissions.FLAGS.MANAGE_THREADS, false) ); } @@ -489,7 +459,7 @@ class ThreadChannel extends Channel { if (this.client.user.id === this.guild.ownerId) return true; const permissions = this.permissionsFor(this.client.user); if (!permissions) return false; - return permissions.has(PermissionFlagsBits.ViewChannel, false); + return permissions.has(Permissions.FLAGS.VIEW_CHANNEL, false); } /** @@ -501,12 +471,12 @@ class ThreadChannel extends Channel { const permissions = this.permissionsFor(this.client.user); if (!permissions) return false; // This flag allows sending even if timed out - if (permissions.has(PermissionFlagsBits.Administrator, false)) return true; + if (permissions.has(Permissions.FLAGS.ADMINISTRATOR, false)) return true; return ( !(this.archived && this.locked && !this.manageable) && - (this.type !== ChannelType.GuildPrivateThread || this.joined || this.manageable) && - permissions.has(PermissionFlagsBits.SendMessagesInThreads, false) && + (this.type !== 'GUILD_PRIVATE_THREAD' || this.joined || this.manageable) && + permissions.has(Permissions.FLAGS.SEND_MESSAGES_IN_THREADS, false) && this.guild.me.communicationDisabledUntilTimestamp < Date.now() ); } @@ -517,15 +487,7 @@ class ThreadChannel extends Channel { * @readonly */ get unarchivable() { - return this.archived && this.sendable && (!this.locked || this.manageable); - } - - /** - * Whether this thread is a private thread - * @returns {boolean} - */ - isPrivate() { - return this.type === ChannelType.GuildPrivateThread; + return this.archived && (this.locked ? this.manageable : this.sendable); } /** @@ -539,7 +501,7 @@ class ThreadChannel extends Channel { * .catch(console.error); */ async delete(reason) { - await this.guild.channels.delete(this.id, reason); + await this.client.api.channels(this.id).delete({ reason }); return this; } diff --git a/src/structures/ThreadMember.js b/src/structures/ThreadMember.js index 9da8677..3dcf3da 100644 --- a/src/structures/ThreadMember.js +++ b/src/structures/ThreadMember.js @@ -1,7 +1,7 @@ 'use strict'; const Base = require('./Base'); -const ThreadMemberFlagsBitField = require('../util/ThreadMemberFlagsBitField'); +const ThreadMemberFlags = require('../util/ThreadMemberFlags'); /** * Represents a Member for a Thread. @@ -33,14 +33,14 @@ class ThreadMember extends Base { } _patch(data) { - if ('join_timestamp' in data) this.joinedTimestamp = Date.parse(data.join_timestamp); + if ('join_timestamp' in data) this.joinedTimestamp = new Date(data.join_timestamp).getTime(); if ('flags' in data) { /** * The flags for this thread member - * @type {ThreadMemberFlagsBitField} + * @type {ThreadMemberFlags} */ - this.flags = new ThreadMemberFlagsBitField(data.flags).freeze(); + this.flags = new ThreadMemberFlags(data.flags).freeze(); } } @@ -59,7 +59,7 @@ class ThreadMember extends Base { * @readonly */ get joinedAt() { - return this.joinedTimestamp && new Date(this.joinedTimestamp); + return this.joinedTimestamp ? new Date(this.joinedTimestamp) : null; } /** diff --git a/src/structures/User.js b/src/structures/User.js index c4d76d4..ecb9987 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -1,11 +1,11 @@ 'use strict'; const Base = require('./Base'); -const { Error } = require('../errors/DJSError'); -const { DiscordSnowflake } = require('@sapphire/snowflake'); -const UserFlagsBitField = require('../util/UserFlagsBitField'); -const { default: Collection } = require('@discordjs/collection'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const { Error } = require('../errors'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); +const UserFlags = require('../util/UserFlags'); +const { default: Collection } = require('@discordjs/collection'); /** * Represents a user on Discord. @@ -13,22 +13,22 @@ const TextBasedChannel = require('./interfaces/TextBasedChannel'); * @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; - this.friend = client.friends.cache.has(this.id); + this.friend = client.friends.cache.has(this.id); this.blocked = client.blocked.cache.has(this.id); @@ -38,91 +38,92 @@ class User extends Base { this.premiumGuildSince = null; this.mutualGuilds = new Collection(); - this._patch(data); - } + 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); - } 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); + } 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 {?UserFlagsBitField} - */ - this.flags = new UserFlagsBitField(data.public_flags); - } - } + if ('public_flags' in data) { + /** + * The flags for this user + * @type {?UserFlags} + */ + this.flags = new UserFlags(data.public_flags); + } + } + // Code written by https://github.com/aiko-chan-ai _ProfilePatch(data) { if (!data) return; @@ -217,224 +218,209 @@ class User extends Base { .relationships[this.id].delete.then((_) => _); } - /** - * 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 DiscordSnowflake.timestampFrom(this.id); - } + /** + * Whether this User is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return typeof this.username !== 'string'; + } - /** - * The time the user was created at - * @type {Date} - * @readonly - */ - get createdAt() { - return new Date(this.createdTimestamp); - } + /** + * The timestamp the user was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return SnowflakeUtil.timestampFrom(this.id); + } - /** - * 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, - ); - } + /** + * The time the user was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } - /** - * If the user is a bot then it'll return the slash commands else return null - * @readonly - */ - get slashCommands() { - if (this.bot) { - return this.client.api.applications(this.id).commands.get(); - } else return null; - } + /** + * 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. See {@link User#banner} for more info - * @param {ImageURLOptions} [options={}] Options for the image URL - * @returns {?string} - */ - bannerURL(options = {}) { - return ( - this.banner && this.client.rest.cdn.banner(this.id, this.banner, options) - ); - } + /** + * 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; + } - // 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/UserContextMenuCommandInteraction.js b/src/structures/UserContextMenuInteraction.js similarity index 61% rename from src/structures/UserContextMenuCommandInteraction.js rename to src/structures/UserContextMenuInteraction.js index a074de1..98dad00 100644 --- a/src/structures/UserContextMenuCommandInteraction.js +++ b/src/structures/UserContextMenuInteraction.js @@ -1,12 +1,12 @@ 'use strict'; -const ContextMenuCommandInteraction = require('./ContextMenuCommandInteraction'); +const ContextMenuInteraction = require('./ContextMenuInteraction'); /** * Represents a user context menu interaction. - * @extends {ContextMenuCommandInteraction} + * @extends {ContextMenuInteraction} */ -class UserContextMenuCommandInteraction extends ContextMenuCommandInteraction { +class UserContextMenuInteraction extends ContextMenuInteraction { /** * The user this interaction was sent from * @type {User} @@ -26,4 +26,4 @@ class UserContextMenuCommandInteraction extends ContextMenuCommandInteraction { } } -module.exports = UserContextMenuCommandInteraction; +module.exports = UserContextMenuInteraction; diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 1b5bc20..d353771 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -1,13 +1,35 @@ 'use strict'; -const { PermissionFlagsBits } = require('discord-api-types/v9'); +const process = require('node:process'); const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel'); +const Permissions = require('../util/Permissions'); + +let deprecationEmittedForEditable = false; /** * Represents a guild voice channel on Discord. * @extends {BaseGuildVoiceChannel} */ class VoiceChannel extends BaseGuildVoiceChannel { + /** + * Whether the channel is editable by the client user + * @type {boolean} + * @readonly + * @deprecated Use {@link VoiceChannel#manageable} instead + */ + get editable() { + if (!deprecationEmittedForEditable) { + process.emitWarning( + 'The VoiceChannel#editable getter is deprecated. Use VoiceChannel#manageable instead.', + 'DeprecationWarning', + ); + + deprecationEmittedForEditable = true; + } + + return this.manageable; + } + /** * Whether the channel is joinable by the client user * @type {boolean} @@ -15,7 +37,7 @@ class VoiceChannel extends BaseGuildVoiceChannel { */ get joinable() { if (!super.joinable) return false; - if (this.full && !this.permissionsFor(this.client.user).has(PermissionFlagsBits.MoveMembers, false)) return false; + if (this.full && !this.permissionsFor(this.client.user).has(Permissions.FLAGS.MOVE_MEMBERS, false)) return false; return true; } @@ -28,11 +50,10 @@ class VoiceChannel extends BaseGuildVoiceChannel { const permissions = this.permissionsFor(this.client.user); if (!permissions) return false; // This flag allows speaking even if timed out - if (permissions.has(PermissionFlagsBits.Administrator, false)) return true; + if (permissions.has(Permissions.FLAGS.ADMINISTRATOR, false)) return true; return ( - this.guild.me.communicationDisabledUntilTimestamp < Date.now() && - permissions.has(PermissionFlagsBits.Speak, false) + this.guild.me.communicationDisabledUntilTimestamp < Date.now() && permissions.has(Permissions.FLAGS.SPEAK, false) ); } diff --git a/src/structures/VoiceRegion.js b/src/structures/VoiceRegion.js index be46b4d..fe399b4 100644 --- a/src/structures/VoiceRegion.js +++ b/src/structures/VoiceRegion.js @@ -19,6 +19,12 @@ class VoiceRegion { */ this.name = data.name; + /** + * Whether the region is VIP-only + * @type {boolean} + */ + this.vip = data.vip; + /** * Whether the region is deprecated * @type {boolean} diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 73762d0..78d9bf5 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -1,6 +1,5 @@ 'use strict'; -const { ChannelType, Routes } = require('discord-api-types/v9'); const Base = require('./Base'); const { Error, TypeError } = require('../errors'); @@ -89,7 +88,7 @@ class VoiceState extends Base { if ('self_video' in data) { /** * Whether this member is streaming using "Screen Share" - * @type {?boolean} + * @type {boolean} */ this.streaming = data.self_stream ?? false; } else { @@ -109,11 +108,9 @@ class VoiceState extends Base { if ('suppress' in data) { /** * Whether this member is suppressed from speaking. This property is specific to stage channels only. - * @type {?boolean} + * @type {boolean} */ this.suppress = data.suppress; - } else { - this.suppress ??= null; } if ('request_to_speak_timestamp' in data) { @@ -121,7 +118,7 @@ class VoiceState extends Base { * The time at which the member requested to speak. This property is specific to stage channels only. * @type {?number} */ - this.requestToSpeakTimestamp = Date.parse(data.request_to_speak_timestamp); + this.requestToSpeakTimestamp = new Date(data.request_to_speak_timestamp).getTime(); } else { this.requestToSpeakTimestamp ??= null; } @@ -218,16 +215,16 @@ class VoiceState extends Base { * @returns {Promise} */ async setRequestToSpeak(request = true) { - if (this.channel?.type !== ChannelType.GuildStageVoice) throw new Error('VOICE_NOT_STAGE_CHANNEL'); + if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL'); if (this.client.user.id !== this.id) throw new Error('VOICE_STATE_NOT_OWN'); await this.client.api.guilds(this.guild.id, 'voice-states', '@me').patch({ - body: { + data: { channel_id: this.channelId, request_to_speak_timestamp: request ? new Date().toISOString() : null, - } - }) + }, + }); } /** @@ -250,15 +247,15 @@ class VoiceState extends Base { async setSuppressed(suppressed = true) { if (typeof suppressed !== 'boolean') throw new TypeError('VOICE_STATE_INVALID_TYPE', 'suppressed'); - if (this.channel?.type !== ChannelType.GuildStageVoice) throw new Error('VOICE_NOT_STAGE_CHANNEL'); + if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL'); const target = this.client.user.id === this.id ? '@me' : this.id; await this.client.api.guilds(this.guild.id, 'voice-states', target).patch({ - body: { + data: { channel_id: this.channelId, suppress: suppressed, - } + }, }); } diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 72b1478..022270a 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,499 +1,449 @@ 'use strict'; -const { DiscordSnowflake } = require('@sapphire/snowflake'); -const { Routes, WebhookType } = require('discord-api-types/v9'); +const process = require('node:process'); const MessagePayload = require('./MessagePayload'); const { Error } = require('../errors'); +const { WebhookTypes } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); -const DiscordAPIError = require('../rest/DiscordAPIError'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); + +let deprecationEmittedForFetchMessage = false; /** * Represents a webhook. */ class Webhook { - constructor(client, data) { - /** - * The client that instantiated the webhook - * @name Webhook#client - * @type {Client} - * @readonly - */ - Object.defineProperty(this, 'client', { value: client }); - if (data) this._patch(data); - } + constructor(client, data) { + /** + * The client that instantiated the webhook + * @name Webhook#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + if (data) this._patch(data); + } - _patch(data) { - if ('name' in data) { - /** - * The name of the webhook - * @type {string} - */ - this.name = data.name; - } + _patch(data) { + if ('name' in data) { + /** + * The name of the webhook + * @type {string} + */ + this.name = data.name; + } - /** - * The token for the webhook, unavailable for follower webhooks and webhooks owned by another application. - * @name Webhook#token - * @type {?string} - */ - Object.defineProperty(this, 'token', { - value: data.token ?? null, - writable: true, - configurable: true, - }); + /** + * The token for the webhook, unavailable for follower webhooks and webhooks owned by another application. + * @name Webhook#token + * @type {?string} + */ + Object.defineProperty(this, 'token', { value: data.token ?? null, writable: true, configurable: true }); - if ('avatar' in data) { - /** - * The avatar for the webhook - * @type {?string} - */ - this.avatar = data.avatar; - } + if ('avatar' in data) { + /** + * The avatar for the webhook + * @type {?string} + */ + this.avatar = data.avatar; + } - /** - * The webhook's id - * @type {Snowflake} - */ - this.id = data.id; + /** + * The webhook's id + * @type {Snowflake} + */ + this.id = data.id; - if ('type' in data) { - /** - * The type of the webhook - * @type {WebhookType} - */ - this.type = data.type; - } + if ('type' in data) { + /** + * The type of the webhook + * @type {WebhookType} + */ + this.type = WebhookTypes[data.type]; + } - if ('guild_id' in data) { - /** - * The guild the webhook belongs to - * @type {Snowflake} - */ - this.guildId = data.guild_id; - } + if ('guild_id' in data) { + /** + * The guild the webhook belongs to + * @type {Snowflake} + */ + this.guildId = data.guild_id; + } - if ('channel_id' in data) { - /** - * The channel the webhook belongs to - * @type {Snowflake} - */ - this.channelId = data.channel_id; - } + if ('channel_id' in data) { + /** + * The channel the webhook belongs to + * @type {Snowflake} + */ + this.channelId = data.channel_id; + } - if ('user' in data) { - /** - * The owner of the webhook - * @type {?(User|APIUser)} - */ - this.owner = this.client.users?._add(data.user) ?? data.user; - } else { - this.owner ??= null; - } + if ('user' in data) { + /** + * The owner of the webhook + * @type {?(User|APIUser)} + */ + this.owner = this.client.users?._add(data.user) ?? data.user; + } else { + this.owner ??= null; + } - if ('application_id' in data) { - /** - * The application that created this webhook - * @type {?Snowflake} - */ - this.applicationId = data.application_id; - } else { - this.applicationId ??= null; - } + if ('source_guild' in data) { + /** + * The source guild of the webhook + * @type {?(Guild|APIGuild)} + */ + this.sourceGuild = this.client.guilds?.resolve(data.source_guild.id) ?? data.source_guild; + } else { + this.sourceGuild ??= null; + } - if ('source_guild' in data) { - /** - * The source guild of the webhook - * @type {?(Guild|APIGuild)} - */ - this.sourceGuild = - this.client.guilds?.resolve(data.source_guild.id) ?? data.source_guild; - } else { - this.sourceGuild ??= null; - } + if ('source_channel' in data) { + /** + * The source channel of the webhook + * @type {?(NewsChannel|APIChannel)} + */ + this.sourceChannel = this.client.channels?.resolve(data.source_channel?.id) ?? data.source_channel; + } else { + this.sourceChannel ??= null; + } + } - if ('source_channel' in data) { - /** - * The source channel of the webhook - * @type {?(NewsChannel|APIChannel)} - */ - this.sourceChannel = - this.client.channels?.resolve(data.source_channel?.id) ?? - data.source_channel; - } else { - this.sourceChannel ??= null; - } - } + /** + * Options that can be passed into send. + * @typedef {BaseMessageOptions} WebhookMessageOptions + * @property {string} [username=this.name] Username override for the message + * @property {string} [avatarURL] Avatar URL override for the message + * @property {Snowflake} [threadId] The id of the thread in the channel to send to. + * For interaction webhooks, this property is ignored + */ - /** - * Options that can be passed into send. - * @typedef {BaseMessageOptions} WebhookMessageOptions - * @property {string} [username=this.name] Username override for the message - * @property {string} [avatarURL] Avatar URL override for the message - * @property {Snowflake} [threadId] The id of the thread in the channel to send to. - * For interaction webhooks, this property is ignored - * @property {MessageFlags} [flags] Which flags to set for the message. Only `SUPPRESS_EMBEDS` can be set. - */ + /** + * Options that can be passed into editMessage. + * @typedef {Object} WebhookEditMessageOptions + * @property {MessageEmbed[]|APIEmbed[]} [embeds] See {@link WebhookMessageOptions#embeds} + * @property {string} [content] See {@link BaseMessageOptions#content} + * @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] See {@link BaseMessageOptions#files} + * @property {MessageMentionOptions} [allowedMentions] See {@link BaseMessageOptions#allowedMentions} + * @property {MessageAttachment[]} [attachments] Attachments to send with the message + * @property {MessageActionRow[]|MessageActionRowOptions[]} [components] + * Action rows containing interactive components for the message (buttons, select menus) + * @property {Snowflake} [threadId] The id of the thread this message belongs to + * For interaction webhooks, this property is ignored + */ - /** - * Options that can be passed into editMessage. - * @typedef {Object} WebhookEditMessageOptions - * @property {Embed[]|APIEmbed[]} [embeds] See {@link WebhookMessageOptions#embeds} - * @property {string} [content] See {@link BaseMessageOptions#content} - * @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] See {@link BaseMessageOptions#files} - * @property {MessageMentionOptions} [allowedMentions] See {@link BaseMessageOptions#allowedMentions} - * @property {MessageAttachment[]} [attachments] Attachments to send with the message - * @property {ActionRow[]|ActionRowOptions[]} [components] - * Action rows containing interactive components for the message (buttons, select menus) - * @property {Snowflake} [threadId] The id of the thread this message belongs to - * For interaction webhooks, this property is ignored - */ + /** + * Sends a message with this webhook. + * @param {string|MessagePayload|WebhookMessageOptions} options The options to provide + * @returns {Promise} + * @example + * // Send a basic message + * webhook.send('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + * @example + * // Send a basic message in a thread + * webhook.send({ content: 'hello!', threadId: '836856309672348295' }) + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + * @example + * // Send a remote file + * webhook.send({ + * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send a local file + * webhook.send({ + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * }] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send an embed with a local image inside + * webhook.send({ + * content: 'This is an embed', + * embeds: [{ + * thumbnail: { + * url: 'attachment://file.jpg' + * } + * }], + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * }] + * }) + * .then(console.log) + * .catch(console.error); + */ + async send(options) { + if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); - /** - * Sends a message with this webhook. - * @param {string|MessagePayload|WebhookMessageOptions} options The options to provide - * @returns {Promise} - * @example - * // Send a basic message - * webhook.send('hello!') - * .then(message => console.log(`Sent message: ${message.content}`)) - * .catch(console.error); - * @example - * // Send a basic message in a thread - * webhook.send({ content: 'hello!', threadId: '836856309672348295' }) - * .then(message => console.log(`Sent message: ${message.content}`)) - * .catch(console.error); - * @example - * // Send a remote file - * webhook.send({ - * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] - * }) - * .then(console.log) - * .catch(console.error); - * @example - * // Send a local file - * webhook.send({ - * files: [{ - * attachment: 'entire/path/to/file.jpg', - * name: 'file.jpg' - * }] - * }) - * .then(console.log) - * .catch(console.error); - * @example - * // Send an embed with a local image inside - * webhook.send({ - * content: 'This is an embed', - * embeds: [{ - * thumbnail: { - * url: 'attachment://file.jpg' - * } - * }], - * files: [{ - * attachment: 'entire/path/to/file.jpg', - * name: 'file.jpg' - * }] - * }) - * .then(console.log) - * .catch(console.error); - */ - async send(options) { - if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); + let messagePayload; - let messagePayload; + if (options instanceof MessagePayload) { + messagePayload = options.resolveData(); + } else { + messagePayload = MessagePayload.create(this, options).resolveData(); + } - if (options instanceof MessagePayload) { - messagePayload = options.resolveBody(); - } else { - messagePayload = MessagePayload.create(this, options).resolveBody(); - } + const { data, files } = await messagePayload.resolveFiles(); + const d = await this.client.api.webhooks(this.id, this.token).post({ + data, + files, + query: { thread_id: messagePayload.options.threadId, wait: true }, + auth: false, + }); + return this.client.channels?.cache.get(d.channel_id)?.messages._add(d, false) ?? d; + } - const query = new URLSearchParams({ wait: true }); + /** + * Sends a raw slack message with this webhook. + * @param {Object} body The raw body to send + * @returns {Promise} + * @example + * // Send a slack message + * webhook.sendSlackMessage({ + * 'username': 'Wumpus', + * 'attachments': [{ + * 'pretext': 'this looks pretty cool', + * 'color': '#F0F', + * 'footer_icon': 'http://snek.s3.amazonaws.com/topSnek.png', + * 'footer': 'Powered by sneks', + * 'ts': Date.now() / 1_000 + * }] + * }).catch(console.error); + * @see {@link https://api.slack.com/messaging/webhooks} + */ + async sendSlackMessage(body) { + if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); - if (messagePayload.options.threadId) { - query.set('thread_id', messagePayload.options.threadId); - } + const data = await this.client.api.webhooks(this.id, this.token).slack.post({ + query: { wait: true }, + auth: false, + data: body, + }); + return data.toString() === 'ok'; + } - const { body, files } = await messagePayload.resolveFiles(); - const d = await this.client.api.webhooks(this.id, this.token).post({ - body, - files, - query, - auth: false, - }); - return ( - this.client.channels?.cache.get(d.channel_id)?.messages._add(d, false) ?? - d - ); - } + /** + * Options used to edit a {@link Webhook}. + * @typedef {Object} WebhookEditData + * @property {string} [name=this.name] The new name for the webhook + * @property {?(BufferResolvable)} [avatar] The new avatar for the webhook + * @property {GuildTextChannelResolvable} [channel] The new channel for the webhook + */ - /** - * Sends a raw slack message with this webhook. - * @param {Object} body The raw body to send - * @returns {Promise} - * @example - * // Send a slack message - * webhook.sendSlackMessage({ - * 'username': 'Wumpus', - * 'attachments': [{ - * 'pretext': 'this looks pretty cool', - * 'color': '#F0F', - * 'footer_icon': 'http://snek.s3.amazonaws.com/topSnek.png', - * 'footer': 'Powered by sneks', - * 'ts': Date.now() / 1_000 - * }] - * }).catch(console.error); - * @see {@link https://api.slack.com/messaging/webhooks} - */ - async sendSlackMessage(body) { - if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); + /** + * Edits this webhook. + * @param {WebhookEditData} options Options for editing the webhook + * @param {string} [reason] Reason for editing the webhook + * @returns {Promise} + */ + async edit({ name = this.name, avatar, channel }, reason) { + if (avatar && !(typeof avatar === 'string' && avatar.startsWith('data:'))) { + avatar = await DataResolver.resolveImage(avatar); + } + channel &&= channel.id ?? channel; + const data = await this.client.api.webhooks(this.id, channel ? undefined : this.token).patch({ + data: { name, avatar, channel_id: channel }, + reason, + auth: !this.token || Boolean(channel), + }); - const data = await this.client.api - .webhooks(this.id, this.token) - .slack.post({ - query: new URLSearchParams({ wait: true }), - auth: false, - body, - }); - return data.toString() === 'ok'; - } + this.name = data.name; + this.avatar = data.avatar; + this.channelId = data.channel_id; + return this; + } - /** - * Options used to edit a {@link Webhook}. - * @typedef {Object} WebhookEditData - * @property {string} [name=this.name] The new name for the webhook - * @property {?(BufferResolvable)} [avatar] The new avatar for the webhook - * @property {GuildTextChannelResolvable} [channel] The new channel for the webhook - */ + /** + * Options that can be passed into fetchMessage. + * @typedef {options} WebhookFetchMessageOptions + * @property {boolean} [cache=true] Whether to cache the message. + * @property {Snowflake} [threadId] The id of the thread this message belongs to. + * For interaction webhooks, this property is ignored + */ - /** - * Edits this webhook. - * @param {WebhookEditData} options Options for editing the webhook - * @param {string} [reason] Reason for editing the webhook - * @returns {Promise} - */ - async edit({ name = this.name, avatar, channel }, reason) { - if (avatar && !(typeof avatar === 'string' && avatar.startsWith('data:'))) { - avatar = await DataResolver.resolveImage(avatar); - } - channel &&= channel.id ?? channel; - const data = await this.client.api - .webhooks(this.id, channel ? undefined : this.token) - .patch({ - data: { name, avatar, channel_id: channel }, - reason, - auth: !this.token || Boolean(channel), - }); + /** + * Gets a message that was sent by this webhook. + * @param {Snowflake|'@original'} message The id of the message to fetch + * @param {WebhookFetchMessageOptions|boolean} [cacheOrOptions={}] The options to provide to fetch the message. + * A **deprecated** boolean may be passed instead to specify whether to cache the message. + * @returns {Promise} Returns the raw message data if the webhook was instantiated as a + * {@link WebhookClient} or if the channel is uncached, otherwise a {@link Message} will be returned + */ + async fetchMessage(message, cacheOrOptions = { cache: true }) { + if (typeof cacheOrOptions === 'boolean') { + if (!deprecationEmittedForFetchMessage) { + process.emitWarning( + 'Passing a boolean to cache the message in Webhook#fetchMessage is deprecated. Pass an object instead.', + 'DeprecationWarning', + ); - this.name = data.name; - this.avatar = data.avatar; - this.channelId = data.channel_id; - return this; - } + deprecationEmittedForFetchMessage = true; + } - /** - * Options that can be passed into fetchMessage. - * @typedef {options} WebhookFetchMessageOptions - * @property {boolean} [cache=true] Whether to cache the message. - * @property {Snowflake} [threadId] The id of the thread this message belongs to. - * For interaction webhooks, this property is ignored - */ + cacheOrOptions = { cache: cacheOrOptions }; + } - /** - * Gets a message that was sent by this webhook. - * @param {Snowflake|'@original'} message The id of the message to fetch - * @param {WebhookFetchMessageOptions} [options={}] The options to provide to fetch the message. - * @returns {Promise} Returns the raw message data if the webhook was instantiated as a - * {@link WebhookClient} or if the channel is uncached, otherwise a {@link Message} will be returned - */ - async fetchMessage(message, { cache = true, threadId } = {}) { - if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); + if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); - const data = await this.client.api - .webhooks(this.id, this.token) - .messages(message) - .get({ - query: threadId - ? new URLSearchParams({ - thread_id: threadId, - }) - : undefined, - auth: false, - }); - return ( - this.client.channels?.cache - .get(data.channel_id) - ?.messages._add(data, cache) ?? data - ); - } + const data = await this.client.api + .webhooks(this.id, this.token) + .messages(message) + .get({ + query: { + thread_id: cacheOrOptions.threadId, + }, + auth: false, + }); + return this.client.channels?.cache.get(data.channel_id)?.messages._add(data, cacheOrOptions.cache) ?? data; + } - /** - * Edits a message that was sent by this webhook. - * @param {MessageResolvable|'@original'} message The message to edit - * @param {string|MessagePayload|WebhookEditMessageOptions} options The options to provide - * @returns {Promise} Returns the raw message data if the webhook was instantiated as a - * {@link WebhookClient} or if the channel is uncached, otherwise a {@link Message} will be returned - */ - async editMessage(message, options) { - if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); + /** + * Edits a message that was sent by this webhook. + * @param {MessageResolvable|'@original'} message The message to edit + * @param {string|MessagePayload|WebhookEditMessageOptions} options The options to provide + * @returns {Promise} Returns the raw message data if the webhook was instantiated as a + * {@link WebhookClient} or if the channel is uncached, otherwise a {@link Message} will be returned + */ + async editMessage(message, options) { + if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); - let messagePayload; + let messagePayload; - if (options instanceof MessagePayload) messagePayload = options; - else messagePayload = MessagePayload.create(this, options); + if (options instanceof MessagePayload) messagePayload = options; + else messagePayload = MessagePayload.create(this, options); - const { body, files } = await messagePayload.resolveBody().resolveFiles(); + const { data, files } = await messagePayload.resolveData().resolveFiles(); - const d = await this.client.api - .webhooks(this.id, this.token) - .messages(typeof message === 'string' ? message : message.id) - .patch({ - body, - files, - query: messagePayload.options.threadId - ? new URLSearchParams({ thread_id: messagePayload.options.threadId }) - : undefined, - auth: false, - }); + const d = await this.client.api + .webhooks(this.id, this.token) + .messages(typeof message === 'string' ? message : message.id) + .patch({ + data, + files, + query: { + thread_id: messagePayload.options.threadId, + }, + auth: false, + }); - const messageManager = this.client.channels?.cache.get( - d.channel_id, - )?.messages; - if (!messageManager) return d; + const messageManager = this.client.channels?.cache.get(d.channel_id)?.messages; + if (!messageManager) return d; - const existing = messageManager.cache.get(d.id); - if (!existing) return messageManager._add(d); + const existing = messageManager.cache.get(d.id); + if (!existing) return messageManager._add(d); - const clone = existing._clone(); - clone._patch(d); - return clone; - } + const clone = existing._clone(); + clone._patch(d); + return clone; + } - /** - * Deletes the webhook. - * @param {string} [reason] Reason for deleting this webhook - * @returns {Promise} - */ - async delete(reason) { - await this.client.api - .webhooks(this.id, this.token) - .delete({ reason, auth: !this.token }); - } + /** + * Deletes the webhook. + * @param {string} [reason] Reason for deleting this webhook + * @returns {Promise} + */ + async delete(reason) { + await this.client.api.webhooks(this.id, this.token).delete({ reason, auth: !this.token }); + } - /** - * Delete a message that was sent by this webhook. - * @param {MessageResolvable|'@original'} message The message to delete - * @param {Snowflake} [threadId] The id of the thread this message belongs to - * @returns {Promise} - */ - async deleteMessage(message, threadId) { - if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); + /** + * Delete a message that was sent by this webhook. + * @param {MessageResolvable|'@original'} message The message to delete + * @param {Snowflake} [threadId] The id of the thread this message belongs to + * @returns {Promise} + */ + async deleteMessage(message, threadId) { + if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE'); - await this.client.api - .webhooks(this.id, this.token) - .messages(typeof message === 'string' ? message : message.id) - .delete({ - query: threadId - ? new URLSearchParams({ - thread_id: threadId, - }) - : undefined, - auth: false, - }); - } + await this.client.api + .webhooks(this.id, this.token) + .messages(typeof message === 'string' ? message : message.id) + .delete({ + query: { + thread_id: threadId, + }, + auth: false, + }); + } - /** - * The timestamp the webhook was created at - * @type {number} - * @readonly - */ - get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); - } + /** + * The timestamp the webhook was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return SnowflakeUtil.timestampFrom(this.id); + } - /** - * The time the webhook was created at - * @type {Date} - * @readonly - */ - get createdAt() { - return new Date(this.createdTimestamp); - } + /** + * The time the webhook was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } - /** - * The URL of this webhook - * @type {string} - * @readonly - */ - get url() { - return this.client.options.rest.api + Routes.webhook(this.id, this.token); - } + /** + * The URL of this webhook + * @type {string} + * @readonly + */ + get url() { + return this.client.options.http.api + this.client.api.webhooks(this.id, this.token); + } - /** - * A link to the webhook's avatar. - * @param {ImageURLOptions} [options={}] Options for the image URL - * @returns {?string} - */ - avatarURL(options = {}) { - return ( - this.avatar && this.client.rest.cdn.avatar(this.id, this.avatar, options) - ); - } + /** + * A link to the webhook's avatar. + * @param {StaticImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + avatarURL({ format, size } = {}) { + if (!this.avatar) return null; + return this.client.rest.cdn.Avatar(this.id, this.avatar, format, size); + } - /** - * Whether this webhook is created by a user. - * @returns {boolean} - */ - isUserCreated() { - return Boolean( - this.type === WebhookType.Incoming && this.owner && !this.owner.bot, - ); - } + /** + * Whether or not this webhook is a channel follower webhook. + * @returns {boolean} + */ + isChannelFollower() { + return this.type === 'Channel Follower'; + } - /** - * Whether this webhook is created by an application. - * @returns {boolean} - */ - isApplicationCreated() { - return this.type === WebhookType.Application; - } + /** + * Whether or not this webhook is an incoming webhook. + * @returns {boolean} + */ + isIncoming() { + return this.type === 'Incoming'; + } - /** - * Whether or not this webhook is a channel follower webhook. - * @returns {boolean} - */ - isChannelFollower() { - return this.type === WebhookType.ChannelFollower; - } - - /** - * Whether or not this webhook is an incoming webhook. - * @returns {boolean} - */ - isIncoming() { - return this.type === WebhookType.Incoming; - } - - static applyToClass(structure, ignore = []) { - for (const prop of [ - 'send', - 'sendSlackMessage', - 'fetchMessage', - 'edit', - 'editMessage', - 'delete', - 'deleteMessage', - 'createdTimestamp', - 'createdAt', - 'url', - ]) { - if (ignore.includes(prop)) continue; - Object.defineProperty( - structure.prototype, - prop, - Object.getOwnPropertyDescriptor(Webhook.prototype, prop), - ); - } - } + static applyToClass(structure, ignore = []) { + for (const prop of [ + 'send', + 'sendSlackMessage', + 'fetchMessage', + 'edit', + 'editMessage', + 'delete', + 'deleteMessage', + 'createdTimestamp', + 'createdAt', + 'url', + ]) { + if (ignore.includes(prop)) continue; + Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(Webhook.prototype, prop)); + } + } } module.exports = Webhook; diff --git a/src/structures/Widget.js b/src/structures/Widget.js index bf6b725..7373d0a 100644 --- a/src/structures/Widget.js +++ b/src/structures/Widget.js @@ -1,7 +1,6 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { Routes } = require('discord-api-types/v9'); const Base = require('./Base'); const WidgetMember = require('./WidgetMember'); diff --git a/src/structures/interfaces/Application.js b/src/structures/interfaces/Application.js index 766da07..f1c02e0 100644 --- a/src/structures/interfaces/Application.js +++ b/src/structures/interfaces/Application.js @@ -1,8 +1,11 @@ 'use strict'; -const { DiscordSnowflake } = require('@sapphire/snowflake'); +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 @@ -10,12 +13,13 @@ const Base = require('../Base'); class Application extends Base { constructor(client, data) { super(client); - this._patch(data); + + if (data) { + this._patch(data); + } } _patch(data) { - if(!data) return; - /** * The application's id * @type {Snowflake} @@ -59,7 +63,7 @@ class Application extends Base { * @readonly */ get createdTimestamp() { - return DiscordSnowflake.timestampFrom(this.id); + return SnowflakeUtil.timestampFrom(this.id); } /** @@ -73,20 +77,43 @@ class Application extends Base { /** * A link to the application's icon. - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {StaticImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - iconURL(options = {}) { - return this.icon && this.client.rest.cdn.appIcon(this.id, this.icon, options); + 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 {ImageURLOptions} [options={}] Options for the image URL + * @param {StaticImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - coverURL(options = {}) { - return this.cover && this.client.rest.cdn.appIcon(this.id, this.cover, options); + 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], + })); } /** diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 20cb71e..c0793fd 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -1,7 +1,7 @@ 'use strict'; const EventEmitter = require('node:events'); -const { setTimeout, clearTimeout } = require('node:timers'); +const { setTimeout } = require('node:timers'); const { Collection } = require('@discordjs/collection'); const { TypeError } = require('../../errors'); const Util = require('../../util/Util'); @@ -236,7 +236,7 @@ class Collector extends EventEmitter { */ async *[Symbol.asyncIterator]() { const queue = []; - const onCollect = (...item) => queue.push(item); + const onCollect = item => queue.push(item); this.on('collect', onCollect); try { diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index 15c8830..dbe7c23 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -1,7 +1,8 @@ 'use strict'; -const { InteractionResponseType, MessageFlags, Routes } = require('discord-api-types/v9'); const { Error } = require('../../errors'); +const { InteractionResponseTypes } = require('../../util/Constants'); +const MessageFlags = require('../../util/MessageFlags'); const MessagePayload = require('../MessagePayload'); /** @@ -27,8 +28,6 @@ class InteractionResponses { * @typedef {BaseMessageOptions} InteractionReplyOptions * @property {boolean} [ephemeral] Whether the reply should be ephemeral * @property {boolean} [fetchReply] Whether to fetch the reply - * @property {MessageFlags} [flags] Which flags to set for the message. - * Only `MessageFlags.SuppressEmbeds` and `MessageFlags.Ephemeral` can be set. */ /** @@ -56,14 +55,14 @@ class InteractionResponses { if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); this.ephemeral = options.ephemeral ?? false; await this.client.api.interactions(this.id, this.token).callback.post({ - body: { - type: InteractionResponseType.DeferredChannelMessageWithSource, + data: { + type: InteractionResponseTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, data: { - flags: options.ephemeral ? MessageFlags.Ephemeral : undefined, + flags: options.ephemeral ? MessageFlags.FLAGS.EPHEMERAL : undefined, }, }, auth: false, - }) + }); this.deferred = true; return options.fetchReply ? this.fetchReply() : undefined; @@ -81,7 +80,7 @@ class InteractionResponses { * .catch(console.error); * @example * // Create an ephemeral reply with an embed - * const embed = new Embed().setDescription('Pong!'); + * const embed = new MessageEmbed().setDescription('Pong!'); * * interaction.reply({ embeds: [embed], ephemeral: true }) * .then(() => console.log('Reply sent.')) @@ -95,11 +94,11 @@ class InteractionResponses { if (options instanceof MessagePayload) messagePayload = options; else messagePayload = MessagePayload.create(this, options); - const { body: data, files } = await messagePayload.resolveBody().resolveFiles(); + const { data, files } = await messagePayload.resolveData().resolveFiles(); await this.client.api.interactions(this.id, this.token).callback.post({ - body: { - type: InteractionResponseType.ChannelMessageWithSource, + data: { + type: InteractionResponseTypes.CHANNEL_MESSAGE_WITH_SOURCE, data, }, files, @@ -180,8 +179,8 @@ class InteractionResponses { async deferUpdate(options = {}) { if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); await this.client.api.interactions(this.id, this.token).callback.post({ - body: { - type: InteractionResponseType.DeferredMessageUpdate, + data: { + type: InteractionResponseTypes.DEFERRED_MESSAGE_UPDATE, }, auth: false, }); @@ -210,11 +209,11 @@ class InteractionResponses { if (options instanceof MessagePayload) messagePayload = options; else messagePayload = MessagePayload.create(this, options); - const { body: data, files } = await messagePayload.resolveBody().resolveFiles(); + const { data, files } = await messagePayload.resolveData().resolveFiles(); await this.client.api.interactions(this.id, this.token).callback.post({ - body: { - type: InteractionResponseType.UpdateMessage, + data: { + type: InteractionResponseTypes.UPDATE_MESSAGE, data, }, files, diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 484cf00..5d2daaa 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -1,377 +1,360 @@ 'use strict'; -const FormData = require('form-data'); -const { Collection } = require('@discordjs/collection'); -const { DiscordSnowflake } = require('@sapphire/snowflake'); -const { InteractionType, Routes } = require('discord-api-types/v9'); -const { TypeError, Error } = require('../../errors'); -const InteractionCollector = require('../InteractionCollector'); + +/* eslint-disable import/order */ const MessageCollector = require('../MessageCollector'); const MessagePayload = require('../MessagePayload'); -const DiscordAPIError = require('../../rest/DiscordAPIError'); +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); + 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 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 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 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); - } + /** + * 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 {Embed[]|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 {ActionRow[]|ActionRowOptions[]} [components] - * Action rows containing interactive components for the message (buttons, select menus) - * @property {MessageAttachment[]} [attachments] Attachments to send in the message - */ + /** + * 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 {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 - * @property {MessageFlags} [flags] Which flags to set for the message. Only `MessageFlags.SuppressEmbeds` can be set. - */ + /** + * 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 - */ + /** + * 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 - */ + /** + * 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 - */ + /** + * @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=this.client.options.failIfNotExists] Whether to error if the referenced - * message does not exist (creates a standard message in this case when false) - */ + /** + * 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) { - await this.client.api.channels(this.id).typing.post(); - const User = require('../User'); - const { GuildMember } = require('../GuildMember'); + /** + * 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); - } + if (this instanceof User || this instanceof GuildMember) { + const dm = await this.createDM(); + return dm.send(options); + } - let messagePayload; + let messagePayload; - if (options instanceof MessagePayload) { - messagePayload = options.resolveBody(); - } else { - messagePayload = MessagePayload.create(this, options).resolveBody(); - } + if (options instanceof MessagePayload) { + messagePayload = options.resolveData(); + } else { + messagePayload = MessagePayload.create(this, options).resolveData(); + } - const { body, files } = await messagePayload.resolveFiles(); - console.log(body) - const d = await this.client.api.channels[this.id].messages.post({ body, files }); - return this.messages.cache.get(d.id) ?? this.messages._add(d); - } + const { data, files } = await messagePayload.resolveFiles(); + const d = await this.client.api.channels[this.id].messages.post({ data, files }); - // Patch send message [fck :(] - /** - * 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(); - } + return this.messages.cache.get(d.id) ?? this.messages._add(d); + } - /** - * 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); - } + /** + * 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(); + } - /** - * 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 - */ + /** + * 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); + } - /** - * 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); - } - }); - }); - } + /** + * 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 + */ - /** - * Creates a component 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: InteractionType.MessageComponent, - channel: this, - }); - } + /** + * 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); + } + }); + }); + } - /** - * 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)); - }); - }); - } + /** + * 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, + }); + } - /** - * 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() - DiscordSnowflake.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({ body: { 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'); - } + /** + * 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)); + }); + }); + } - 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), - ); - } - } + /** + * 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 -// eslint-disable-next-line import/order const MessageManager = require('../../managers/MessageManager'); diff --git a/src/util/ActivityFlags.js b/src/util/ActivityFlags.js new file mode 100644 index 00000000..e874ed2 --- /dev/null +++ b/src/util/ActivityFlags.js @@ -0,0 +1,44 @@ +'use strict'; + +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with an {@link Activity#flags} bitfield. + * @extends {BitField} + */ +class ActivityFlags extends BitField {} + +/** + * @name ActivityFlags + * @kind constructor + * @memberof ActivityFlags + * @param {BitFieldResolvable} [bits=0] Bit(s) to read from + */ + +/** + * Numeric activity flags. All available properties: + * * `INSTANCE` + * * `JOIN` + * * `SPECTATE` + * * `JOIN_REQUEST` + * * `SYNC` + * * `PLAY` + * * `PARTY_PRIVACY_FRIENDS` + * * `PARTY_PRIVACY_VOICE_CHANNEL` + * * `EMBEDDED` + * @type {Object} + * @see {@link https://discord.com/developers/docs/topics/gateway#activity-object-activity-flags} + */ +ActivityFlags.FLAGS = { + INSTANCE: 1 << 0, + JOIN: 1 << 1, + SPECTATE: 1 << 2, + JOIN_REQUEST: 1 << 3, + SYNC: 1 << 4, + PLAY: 1 << 5, + PARTY_PRIVACY_FRIENDS: 1 << 6, + PARTY_PRIVACY_VOICE_CHANNEL: 1 << 7, + EMBEDDED: 1 << 8, +}; + +module.exports = ActivityFlags; diff --git a/src/util/ActivityFlagsBitField.js b/src/util/ActivityFlagsBitField.js deleted file mode 100644 index 2de552f..00000000 --- a/src/util/ActivityFlagsBitField.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const { ActivityFlags } = require('discord-api-types/v9'); -const BitField = require('./BitField'); - -/** - * Data structure that makes it easy to interact with an {@link Activity#flags} bitfield. - * @extends {BitField} - */ -class ActivityFlagsBitField extends BitField {} - -/** - * @name ActivityFlagsBitField - * @kind constructor - * @memberof ActivityFlagsBitField - * @param {BitFieldResolvable} [bits=0] Bit(s) to read from - */ - -/** - * Numeric activity flags. - * @type {ActivityFlags} - */ -ActivityFlagsBitField.Flags = ActivityFlags; - -module.exports = ActivityFlagsBitField; diff --git a/src/util/ApplicationFlags.js b/src/util/ApplicationFlags.js new file mode 100644 index 00000000..43e1682 --- /dev/null +++ b/src/util/ApplicationFlags.js @@ -0,0 +1,48 @@ +'use strict'; + +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with a {@link ClientApplication#flags} bitfield. + * @extends {BitField} + */ +class ApplicationFlags extends BitField {} + +/** + * @name ApplicationFlags + * @kind constructor + * @memberof ApplicationFlags + * @param {BitFieldResolvable} [bits=0] Bit(s) to read from + */ + +/** + * Bitfield of the packed bits + * @type {number} + * @name ApplicationFlags#bitfield + */ + +/** + * Numeric application flags. All available properties: + * * `GATEWAY_PRESENCE` + * * `GATEWAY_PRESENCE_LIMITED` + * * `GATEWAY_GUILD_MEMBERS` + * * `GATEWAY_GUILD_MEMBERS_LIMITED` + * * `VERIFICATION_PENDING_GUILD_LIMIT` + * * `EMBEDDED` + * * `GATEWAY_MESSAGE_CONTENT` + * * `GATEWAY_MESSAGE_CONTENT_LIMITED` + * @type {Object} + * @see {@link https://discord.com/developers/docs/resources/application#application-object-application-flags} + */ +ApplicationFlags.FLAGS = { + GATEWAY_PRESENCE: 1 << 12, + GATEWAY_PRESENCE_LIMITED: 1 << 13, + GATEWAY_GUILD_MEMBERS: 1 << 14, + GATEWAY_GUILD_MEMBERS_LIMITED: 1 << 15, + VERIFICATION_PENDING_GUILD_LIMIT: 1 << 16, + EMBEDDED: 1 << 17, + GATEWAY_MESSAGE_CONTENT: 1 << 18, + GATEWAY_MESSAGE_CONTENT_LIMITED: 1 << 19, +}; + +module.exports = ApplicationFlags; diff --git a/src/util/ApplicationFlagsBitField.js b/src/util/ApplicationFlagsBitField.js deleted file mode 100644 index 885260e..00000000 --- a/src/util/ApplicationFlagsBitField.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const { ApplicationFlags } = require('discord-api-types/v9'); -const BitField = require('./BitField'); - -/** - * Data structure that makes it easy to interact with a {@link ClientApplication#flags} bitfield. - * @extends {BitField} - */ -class ApplicationFlagsBitField extends BitField {} - -/** - * @name ApplicationFlagsBitField - * @kind constructor - * @memberof ApplicationFlagsBitField - * @param {BitFieldResolvable} [bits=0] Bit(s) to read from - */ - -/** - * Bitfield of the packed bits - * @type {number} - * @name ApplicationFlagsBitField#bitfield - */ - -/** - * Numeric application flags. All available properties: - * @type {ApplicationFlags} - */ -ApplicationFlagsBitField.Flags = ApplicationFlags; - -module.exports = ApplicationFlagsBitField; diff --git a/src/util/BitField.js b/src/util/BitField.js index c444d2d..c34f362 100644 --- a/src/util/BitField.js +++ b/src/util/BitField.js @@ -101,7 +101,7 @@ class BitField { */ serialize(...hasParams) { const serialized = {}; - for (const [flag, bit] of Object.entries(this.constructor.Flags)) serialized[flag] = this.has(bit, ...hasParams); + for (const [flag, bit] of Object.entries(this.constructor.FLAGS)) serialized[flag] = this.has(bit, ...hasParams); return serialized; } @@ -111,7 +111,7 @@ class BitField { * @returns {string[]} */ toArray(...hasParams) { - return Object.keys(this.constructor.Flags).filter(bit => this.has(bit, ...hasParams)); + return Object.keys(this.constructor.FLAGS).filter(bit => this.has(bit, ...hasParams)); } toJSON() { @@ -128,7 +128,7 @@ class BitField { /** * Data that can be resolved to give a bitfield. This can be: - * * A bit number (this can be a number literal or a value taken from {@link BitField.Flags}) + * * A bit number (this can be a number literal or a value taken from {@link BitField.FLAGS}) * * A string bit number * * An instance of BitField * * An Array of BitFieldResolvable @@ -146,7 +146,7 @@ class BitField { if (bit instanceof BitField) return bit.bitfield; if (Array.isArray(bit)) return bit.map(p => this.resolve(p)).reduce((prev, p) => prev | p, defaultBit); if (typeof bit === 'string') { - if (typeof this.Flags[bit] !== 'undefined') return this.Flags[bit]; + if (typeof this.FLAGS[bit] !== 'undefined') return this.FLAGS[bit]; if (!isNaN(bit)) return typeof defaultBit === 'bigint' ? BigInt(bit) : Number(bit); } throw new RangeError('BITFIELD_INVALID', bit); @@ -159,7 +159,7 @@ class BitField { * @type {Object} * @abstract */ -BitField.Flags = {}; +BitField.FLAGS = {}; /** * @type {number|bigint} diff --git a/src/util/Colors.js b/src/util/Colors.js deleted file mode 100644 index 5b4a383..00000000 --- a/src/util/Colors.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -module.exports = { - Default: 0x000000, - White: 0xffffff, - Aqua: 0x1abc9c, - Green: 0x57f287, - Blue: 0x3498db, - Yellow: 0xfee75c, - Purple: 0x9b59b6, - LuminousVividPink: 0xe91e63, - Fuchsia: 0xeb459e, - Gold: 0xf1c40f, - Orange: 0xe67e22, - Red: 0xed4245, - Grey: 0x95a5a6, - Navy: 0x34495e, - DarkAqua: 0x11806a, - DarkGreen: 0x1f8b4c, - DarkBlue: 0x206694, - DarkPurple: 0x71368a, - DarkVividPink: 0xad1457, - DarkGold: 0xc27c0e, - DarkOrange: 0xa84300, - DarkRed: 0x992d22, - DarkGrey: 0x979c9f, - DarkerGrey: 0x7f8c8d, - LightGrey: 0xbcc0c0, - DarkNavy: 0x2c3e50, - Blurple: 0x5865f2, - Greyple: 0x99aab5, - DarkButNotBlack: 0x2c2f33, - NotQuiteBlack: 0x23272a, -}; diff --git a/src/util/Components.js b/src/util/Components.js deleted file mode 100644 index 59a816b..00000000 --- a/src/util/Components.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -/** - * @typedef {Object} BaseComponentData - * @property {ComponentType} type - */ - -/** - * @typedef {BaseComponentData} ActionRowData - * @property {ComponentData[]} components - */ - -/** - * @typedef {BaseComponentData} ButtonComponentData - * @property {ButtonStyle} style - * @property {?boolean} disabled - * @property {string} label - * @property {?APIComponentEmoji} emoji - * @property {?string} customId - * @property {?string} url - */ - -/** - * @typedef {object} SelectMenuComponentOptionData - * @property {string} label - * @property {string} value - * @property {?string} description - * @property {?APIComponentEmoji} emoji - * @property {?boolean} default - */ - -/** - * @typedef {BaseComponentData} SelectMenuComponentData - * @property {string} customId - * @property {?boolean} disabled - * @property {?number} maxValues - * @property {?number} minValues - * @property {?SelectMenuComponentOptionData[]} options - * @property {?string} placeholder - */ - -/** - * @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData} ComponentData - */ diff --git a/src/util/Constants.js b/src/util/Constants.js index 6bbe129..11c455e 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -1,117 +1,128 @@ 'use strict'; const process = require('node:process'); -const { ChannelType, MessageType } = require('discord-api-types/v9'); const Package = (exports.Package = require('../../package.json')); +const { Error, RangeError, TypeError } = require('../errors'); -exports.UserAgent = `Mozilla/5.0 (iPhone; CPU iPhone OS 15_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/90.0.4430.78 Mobile/15E148 Safari/604.1`; +exports.UserAgent = `DiscordBot (${Package.homepage}, ${Package.version}) Node.js/${process.version}`; + +exports.WSCodes = { + 1000: 'WS_CLOSE_REQUESTED', + 4004: 'TOKEN_INVALID', + 4010: 'SHARDING_INVALID', + 4011: 'SHARDING_REQUIRED', + 4013: 'INVALID_INTENTS', + 4014: 'DISALLOWED_INTENTS', +}; + +const AllowedImageFormats = ['webp', 'png', 'jpg', 'jpeg', 'gif']; + +const AllowedImageSizes = [16, 32, 56, 64, 96, 128, 256, 300, 512, 600, 1024, 2048, 4096]; + +function makeImageUrl(root, { format = 'webp', size } = {}) { + if (!['undefined', 'number'].includes(typeof size)) throw new TypeError('INVALID_TYPE', 'size', 'number'); + if (format && !AllowedImageFormats.includes(format)) throw new Error('IMAGE_FORMAT', format); + if (size && !AllowedImageSizes.includes(size)) throw new RangeError('IMAGE_SIZE', size); + return `${root}.${format}${size ? `?size=${size}` : ''}`; +} /** - * The name of an item to be swept in Sweepers - * * `applicationCommands` - both global and guild commands - * * `bans` - * * `emojis` - * * `invites` - accepts the `lifetime` property, using it will sweep based on expires timestamp - * * `guildMembers` - * * `messages` - accepts the `lifetime` property, using it will sweep based on edited or created timestamp - * * `presences` - * * `reactions` - * * `stageInstances` - * * `stickers` - * * `threadMembers` - * * `threads` - accepts the `lifetime` property, using it will sweep archived threads based on archived timestamp - * * `users` - * * `voiceStates` - * @typedef {string} SweeperKey - */ -exports.SweeperKeys = [ - 'applicationCommands', - 'bans', - 'emojis', - 'invites', - 'guildMembers', - 'messages', - 'presences', - 'reactions', - 'stageInstances', - 'stickers', - 'threadMembers', - 'threads', - 'users', - 'voiceStates', -]; - -/** - * The types of messages that are not `System`. The available types are: - * * {@link MessageType.Default} - * * {@link MessageType.Reply} - * * {@link MessageType.ChatInputCommand} - * * {@link MessageType.ContextMenuCommand} - * @typedef {MessageType[]} NonSystemMessageTypes - */ -exports.NonSystemMessageTypes = [ - MessageType.Default, - MessageType.Reply, - MessageType.ChatInputCommand, - MessageType.ContextMenuCommand, -]; - -/** - * The channels that are text-based. - * * DMChannel - * * TextChannel - * * NewsChannel - * * ThreadChannel - * @typedef {DMChannel|TextChannel|NewsChannel|ThreadChannel} TextBasedChannels + * Options for Image URLs. + * @typedef {StaticImageURLOptions} ImageURLOptions + * @property {boolean} [dynamic=false] If true, the format will dynamically change to `gif` for animated avatars. */ /** - * The types of channels that are text-based. The available types are: - * * {@link ChannelType.DM} - * * {@link ChannelType.GuildText} - * * {@link ChannelType.GuildNews} - * * {@link ChannelType.GuildNewsThread} - * * {@link ChannelType.GuildPublicThread} - * * {@link ChannelType.GuildPrivateThread} - * @typedef {ChannelType} TextBasedChannelTypes + * Options for static Image URLs. + * @typedef {Object} StaticImageURLOptions + * @property {string} [format='webp'] One of `webp`, `png`, `jpg`, `jpeg`. + * @property {number} [size] One of `16`, `32`, `56`, `64`, `96`, `128`, `256`, `300`, `512`, `600`, `1024`, `2048`, + * `4096` */ -exports.TextBasedChannelTypes = [ - ChannelType.DM, - ChannelType.GuildText, - ChannelType.GuildNews, - ChannelType.GuildNewsThread, - ChannelType.GuildPublicThread, - ChannelType.GuildPrivateThread, -]; + +// https://discord.com/developers/docs/reference#image-formatting-cdn-endpoints +exports.Endpoints = { + CDN(root) { + return { + Emoji: (emojiId, format = 'webp') => `${root}/emojis/${emojiId}.${format}`, + Asset: name => `${root}/assets/${name}`, + DefaultAvatar: discriminator => `${root}/embed/avatars/${discriminator}.png`, + Avatar: (userId, hash, format, size, dynamic = false) => { + if (dynamic && hash.startsWith('a_')) format = 'gif'; + return makeImageUrl(`${root}/avatars/${userId}/${hash}`, { format, size }); + }, + GuildMemberAvatar: (guildId, memberId, hash, format = 'webp', size, dynamic = false) => { + if (dynamic && hash.startsWith('a_')) format = 'gif'; + return makeImageUrl(`${root}/guilds/${guildId}/users/${memberId}/avatars/${hash}`, { format, size }); + }, + Banner: (id, hash, format, size, dynamic = false) => { + if (dynamic && hash.startsWith('a_')) format = 'gif'; + return makeImageUrl(`${root}/banners/${id}/${hash}`, { format, size }); + }, + Icon: (guildId, hash, format, size, dynamic = false) => { + if (dynamic && hash.startsWith('a_')) format = 'gif'; + return makeImageUrl(`${root}/icons/${guildId}/${hash}`, { format, size }); + }, + AppIcon: (appId, hash, options) => makeImageUrl(`${root}/app-icons/${appId}/${hash}`, options), + AppAsset: (appId, hash, options) => makeImageUrl(`${root}/app-assets/${appId}/${hash}`, options), + StickerPackBanner: (bannerId, format, size) => + makeImageUrl(`${root}/app-assets/710982414301790216/store/${bannerId}`, { size, format }), + GDMIcon: (channelId, hash, format, size) => + makeImageUrl(`${root}/channel-icons/${channelId}/${hash}`, { size, format }), + Splash: (guildId, hash, format, size) => makeImageUrl(`${root}/splashes/${guildId}/${hash}`, { size, format }), + DiscoverySplash: (guildId, hash, format, size) => + makeImageUrl(`${root}/discovery-splashes/${guildId}/${hash}`, { size, format }), + TeamIcon: (teamId, hash, options) => makeImageUrl(`${root}/team-icons/${teamId}/${hash}`, options), + Sticker: (stickerId, stickerFormat) => + `${root}/stickers/${stickerId}.${stickerFormat === 'LOTTIE' ? 'json' : 'png'}`, + RoleIcon: (roleId, hash, format = 'webp', size) => + makeImageUrl(`${root}/role-icons/${roleId}/${hash}`, { size, format }), + }; + }, + invite: (root, code, eventId) => (eventId ? `${root}/${code}?event=${eventId}` : `${root}/${code}`), + scheduledEvent: (root, guildId, eventId) => `${root}/${guildId}/${eventId}`, + botGateway: '/gateway/bot', +}; /** - * The types of channels that are threads. The available types are: - * * {@link ChannelType.GuildNewsThread} - * * {@link ChannelType.GuildPublicThread} - * * {@link ChannelType.GuildPrivateThread} - * @typedef {ChannelType[]} ThreadChannelTypes - */ -exports.ThreadChannelTypes = [ - ChannelType.GuildNewsThread, - ChannelType.GuildPublicThread, - ChannelType.GuildPrivateThread, -]; - -/** - * The types of channels that are voice-based. The available types are: - * * {@link ChannelType.GuildVoice} - * * {@link ChannelType.GuildStageVoice} - * @typedef {ChannelType[]} VoiceBasedChannelTypes - */ -exports.VoiceBasedChannelTypes = [ChannelType.GuildVoice, ChannelType.GuildStageVoice]; - -/* eslint-enable max-len */ - -/** - * @typedef {Object} Constants Constants that can be used in an enum or object-like way. - * @property {Status} Status The available statuses of the client. + * The current status of the client. Here are the available statuses: + * * READY: 0 + * * CONNECTING: 1 + * * RECONNECTING: 2 + * * IDLE: 3 + * * NEARLY: 4 + * * DISCONNECTED: 5 + * * WAITING_FOR_GUILDS: 6 + * * IDENTIFYING: 7 + * * RESUMING: 8 + * @typedef {number} Status */ +exports.Status = { + READY: 0, + CONNECTING: 1, + RECONNECTING: 2, + IDLE: 3, + NEARLY: 4, + DISCONNECTED: 5, + WAITING_FOR_GUILDS: 6, + IDENTIFYING: 7, + RESUMING: 8, +}; +exports.Opcodes = { + DISPATCH: 0, + HEARTBEAT: 1, + IDENTIFY: 2, + STATUS_UPDATE: 3, + VOICE_STATE_UPDATE: 4, + VOICE_GUILD_PING: 5, + RESUME: 6, + RECONNECT: 7, + REQUEST_GUILD_MEMBERS: 8, + INVALID_SESSION: 9, + HELLO: 10, + HEARTBEAT_ACK: 11, +}; exports.Events = { RATE_LIMIT: 'rateLimit', @@ -200,73 +211,1030 @@ exports.Events = { GUILD_SCHEDULED_EVENT_USER_REMOVE: 'guildScheduledEventUserRemove', }; -const AllowedImageFormats = ['webp', 'png', 'jpg', 'jpeg', 'gif']; +exports.ShardEvents = { + CLOSE: 'close', + DESTROYED: 'destroyed', + INVALID_SESSION: 'invalidSession', + READY: 'ready', + RESUMED: 'resumed', + ALL_READY: 'allReady', +}; -const AllowedImageSizes = [ - 16, 32, 56, 64, 96, 128, 256, 300, 512, 600, 1024, 2048, 4096, +/** + * The type of Structure allowed to be a partial: + * * USER + * * CHANNEL (only affects DMChannels) + * * GUILD_MEMBER + * * MESSAGE + * * REACTION + * * GUILD_SCHEDULED_EVENT + * Partials require you to put checks in place when handling data. See the "Partial Structures" topic on the + * [guide](https://discordjs.guide/popular-topics/partials.html) for more information. + * @typedef {string} PartialType + */ +exports.PartialTypes = keyMirror(['USER', 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE', 'REACTION', 'GUILD_SCHEDULED_EVENT']); + +/** + * The type of a WebSocket message event, e.g. `MESSAGE_CREATE`. Here are the available events: + * * READY + * * RESUMED + * * APPLICATION_COMMAND_CREATE (deprecated) + * * APPLICATION_COMMAND_DELETE (deprecated) + * * APPLICATION_COMMAND_UPDATE (deprecated) + * * GUILD_CREATE + * * GUILD_DELETE + * * GUILD_UPDATE + * * INVITE_CREATE + * * INVITE_DELETE + * * GUILD_MEMBER_ADD + * * GUILD_MEMBER_REMOVE + * * GUILD_MEMBER_UPDATE + * * GUILD_MEMBERS_CHUNK + * * GUILD_INTEGRATIONS_UPDATE + * * GUILD_ROLE_CREATE + * * GUILD_ROLE_DELETE + * * GUILD_ROLE_UPDATE + * * GUILD_BAN_ADD + * * GUILD_BAN_REMOVE + * * GUILD_EMOJIS_UPDATE + * * CHANNEL_CREATE + * * CHANNEL_DELETE + * * CHANNEL_UPDATE + * * CHANNEL_PINS_UPDATE + * * MESSAGE_CREATE + * * MESSAGE_DELETE + * * MESSAGE_UPDATE + * * MESSAGE_DELETE_BULK + * * MESSAGE_REACTION_ADD + * * MESSAGE_REACTION_REMOVE + * * MESSAGE_REACTION_REMOVE_ALL + * * MESSAGE_REACTION_REMOVE_EMOJI + * * THREAD_CREATE + * * THREAD_UPDATE + * * THREAD_DELETE + * * THREAD_LIST_SYNC + * * THREAD_MEMBER_UPDATE + * * THREAD_MEMBERS_UPDATE + * * USER_UPDATE + * * PRESENCE_UPDATE + * * TYPING_START + * * VOICE_STATE_UPDATE + * * VOICE_SERVER_UPDATE + * * WEBHOOKS_UPDATE + * * INTERACTION_CREATE + * * STAGE_INSTANCE_CREATE + * * STAGE_INSTANCE_UPDATE + * * STAGE_INSTANCE_DELETE + * * GUILD_STICKERS_UPDATE + * * GUILD_SCHEDULED_EVENT_CREATE + * * GUILD_SCHEDULED_EVENT_UPDATE + * * GUILD_SCHEDULED_EVENT_DELETE + * * GUILD_SCHEDULED_EVENT_USER_ADD + * * GUILD_SCHEDULED_EVENT_USER_REMOVE + * @typedef {string} WSEventType + * @see {@link https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-events} + */ +exports.WSEvents = keyMirror([ + 'READY', + 'RESUMED', + 'APPLICATION_COMMAND_CREATE', + 'APPLICATION_COMMAND_DELETE', + 'APPLICATION_COMMAND_UPDATE', + 'GUILD_CREATE', + 'GUILD_DELETE', + 'GUILD_UPDATE', + 'INVITE_CREATE', + 'INVITE_DELETE', + 'GUILD_MEMBER_ADD', + 'GUILD_MEMBER_REMOVE', + 'GUILD_MEMBER_UPDATE', + 'GUILD_MEMBERS_CHUNK', + 'GUILD_INTEGRATIONS_UPDATE', + 'GUILD_ROLE_CREATE', + 'GUILD_ROLE_DELETE', + 'GUILD_ROLE_UPDATE', + 'GUILD_BAN_ADD', + 'GUILD_BAN_REMOVE', + 'GUILD_EMOJIS_UPDATE', + 'CHANNEL_CREATE', + 'CHANNEL_DELETE', + 'CHANNEL_UPDATE', + 'CHANNEL_PINS_UPDATE', + 'MESSAGE_CREATE', + 'MESSAGE_DELETE', + 'MESSAGE_UPDATE', + 'MESSAGE_DELETE_BULK', + 'MESSAGE_REACTION_ADD', + 'MESSAGE_REACTION_REMOVE', + 'MESSAGE_REACTION_REMOVE_ALL', + 'MESSAGE_REACTION_REMOVE_EMOJI', + 'THREAD_CREATE', + 'THREAD_UPDATE', + 'THREAD_DELETE', + 'THREAD_LIST_SYNC', + 'THREAD_MEMBER_UPDATE', + 'THREAD_MEMBERS_UPDATE', + 'USER_UPDATE', + 'PRESENCE_UPDATE', + 'TYPING_START', + 'VOICE_STATE_UPDATE', + 'VOICE_SERVER_UPDATE', + 'WEBHOOKS_UPDATE', + 'INTERACTION_CREATE', + 'STAGE_INSTANCE_CREATE', + 'STAGE_INSTANCE_UPDATE', + 'STAGE_INSTANCE_DELETE', + 'GUILD_STICKERS_UPDATE', + 'GUILD_SCHEDULED_EVENT_CREATE', + 'GUILD_SCHEDULED_EVENT_UPDATE', + 'GUILD_SCHEDULED_EVENT_DELETE', + 'GUILD_SCHEDULED_EVENT_USER_ADD', + 'GUILD_SCHEDULED_EVENT_USER_REMOVE', +]); + +/** + * A valid scope to request when generating an invite link. + * Scopes that require whitelist are not considered valid for this generator + * * `applications.builds.read`: allows reading build data for a users applications + * * `applications.commands`: allows this bot to create commands in the server + * * `applications.entitlements`: allows reading entitlements for a users applications + * * `applications.store.update`: allows reading and updating of store data for a users applications + * * `bot`: makes the bot join the selected guild + * * `connections`: makes the endpoint for getting a users connections available + * * `email`: allows the `/users/@me` endpoint return with an email + * * `identify`: allows the `/users/@me` endpoint without an email + * * `guilds`: makes the `/users/@me/guilds` endpoint available for a user + * * `guilds.join`: allows the bot to join the user to any guild it is in using Guild#addMember + * * `gdm.join`: allows joining the user to a group dm + * * `webhook.incoming`: generates a webhook to a channel + * @typedef {string} InviteScope + * @see {@link https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes} + */ +exports.InviteScopes = [ + 'applications.builds.read', + 'applications.commands', + 'applications.entitlements', + 'applications.store.update', + 'bot', + 'connections', + 'email', + 'identify', + 'guilds', + 'guilds.join', + 'gdm.join', + 'webhook.incoming', ]; -function makeImageUrl(root, { format = 'webp', size } = {}) { - if (!['undefined', 'number'].includes(typeof size)) throw new TypeError('INVALID_TYPE', 'size', 'number'); - if (format && !AllowedImageFormats.includes(format)) throw new Error('IMAGE_FORMAT', format); - if (size && !AllowedImageSizes.includes(size)) throw new RangeError('IMAGE_SIZE', size); - return `${root}.${format}${size ? `?size=${size}` : ''}`; +// TODO: change Integration#expireBehavior to this and clean up Integration +/** + * The behavior of expiring subscribers for Integrations. This can be: + * * REMOVE_ROLE + * * KICK + * @typedef {string} IntegrationExpireBehavior + * @see {@link https://discord.com/developers/docs/resources/guild#integration-object-integration-expire-behaviors} + */ +exports.IntegrationExpireBehaviors = createEnum(['REMOVE_ROLE', 'KICK']); + +/** + * The type of a message, e.g. `DEFAULT`. Here are the available types: + * * DEFAULT + * * RECIPIENT_ADD + * * RECIPIENT_REMOVE + * * CALL + * * CHANNEL_NAME_CHANGE + * * CHANNEL_ICON_CHANGE + * * CHANNEL_PINNED_MESSAGE + * * GUILD_MEMBER_JOIN + * * USER_PREMIUM_GUILD_SUBSCRIPTION + * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 + * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 + * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 + * * CHANNEL_FOLLOW_ADD + * * GUILD_DISCOVERY_DISQUALIFIED + * * GUILD_DISCOVERY_REQUALIFIED + * * GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING + * * GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING + * * THREAD_CREATED + * * REPLY + * * APPLICATION_COMMAND + * * THREAD_STARTER_MESSAGE + * * GUILD_INVITE_REMINDER + * * CONTEXT_MENU_COMMAND + * @typedef {string} MessageType + * @see {@link https://discord.com/developers/docs/resources/channel#message-object-message-types} + */ +exports.MessageTypes = [ + 'DEFAULT', + 'RECIPIENT_ADD', + 'RECIPIENT_REMOVE', + 'CALL', + 'CHANNEL_NAME_CHANGE', + 'CHANNEL_ICON_CHANGE', + 'CHANNEL_PINNED_MESSAGE', + 'GUILD_MEMBER_JOIN', + 'USER_PREMIUM_GUILD_SUBSCRIPTION', + 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1', + 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2', + 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3', + 'CHANNEL_FOLLOW_ADD', + null, + 'GUILD_DISCOVERY_DISQUALIFIED', + 'GUILD_DISCOVERY_REQUALIFIED', + 'GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING', + 'GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING', + 'THREAD_CREATED', + 'REPLY', + 'APPLICATION_COMMAND', + 'THREAD_STARTER_MESSAGE', + 'GUILD_INVITE_REMINDER', + 'CONTEXT_MENU_COMMAND', +]; + +/** + * The name of an item to be swept in Sweepers + * * `applicationCommands` - both global and guild commands + * * `bans` + * * `emojis` + * * `invites` - accepts the `lifetime` property, using it will sweep based on expires timestamp + * * `guildMembers` + * * `messages` - accepts the `lifetime` property, using it will sweep based on edited or created timestamp + * * `presences` + * * `reactions` + * * `stageInstances` + * * `stickers` + * * `threadMembers` + * * `threads` - accepts the `lifetime` property, using it will sweep archived threads based on archived timestamp + * * `users` + * * `voiceStates` + * @typedef {string} SweeperKey + */ +exports.SweeperKeys = [ + 'applicationCommands', + 'bans', + 'emojis', + 'invites', + 'guildMembers', + 'messages', + 'presences', + 'reactions', + 'stageInstances', + 'stickers', + 'threadMembers', + 'threads', + 'users', + 'voiceStates', +]; + +/** + * The types of messages that are `System`. The available types are `MessageTypes` excluding: + * * DEFAULT + * * REPLY + * * APPLICATION_COMMAND + * * CONTEXT_MENU_COMMAND + * @typedef {string} SystemMessageType + */ +exports.SystemMessageTypes = exports.MessageTypes.filter( + type => type && !['DEFAULT', 'REPLY', 'APPLICATION_COMMAND', 'CONTEXT_MENU_COMMAND'].includes(type), +); + +/** + * Bots cannot set a `CUSTOM` activity type, it is only for custom statuses received from users + * The type of an activity of a user's presence. Here are the available types: + * * PLAYING + * * STREAMING + * * LISTENING + * * WATCHING + * * CUSTOM + * * COMPETING + * @typedef {string} ActivityType + * @see {@link https://discord.com/developers/docs/game-sdk/activities#data-models-activitytype-enum} + */ +exports.ActivityTypes = createEnum(['PLAYING', 'STREAMING', 'LISTENING', 'WATCHING', 'CUSTOM', 'COMPETING']); + +/** + * All available channel types: + * * `GUILD_TEXT` - a guild text channel + * * `DM` - a DM channel + * * `GUILD_VOICE` - a guild voice channel + * * `GROUP_DM` - a group DM channel + * * `GUILD_CATEGORY` - a guild category channel + * * `GUILD_NEWS` - a guild news channel + * * `GUILD_STORE` - a guild store channel + * Store channels are deprecated and will be removed from Discord in March 2022. See + * [Self-serve Game Selling Deprecation](https://support-dev.discord.com/hc/en-us/articles/4414590563479) + * for more information. + * * `GUILD_NEWS_THREAD` - a guild news channel's public thread channel + * * `GUILD_PUBLIC_THREAD` - a guild text channel's public thread channel + * * `GUILD_PRIVATE_THREAD` - a guild text channel's private thread channel + * * `GUILD_STAGE_VOICE` - a guild stage voice channel + * * `UNKNOWN` - a generic channel of unknown type, could be Channel or GuildChannel + * @typedef {string} ChannelType + * @see {@link https://discord.com/developers/docs/resources/channel#channel-object-channel-types} + */ +exports.ChannelTypes = createEnum([ + 'GUILD_TEXT', + 'DM', + 'GUILD_VOICE', + 'GROUP_DM', + 'GUILD_CATEGORY', + 'GUILD_NEWS', + 'GUILD_STORE', + ...Array(3).fill(null), + // 10 + 'GUILD_NEWS_THREAD', + 'GUILD_PUBLIC_THREAD', + 'GUILD_PRIVATE_THREAD', + 'GUILD_STAGE_VOICE', +]); + +/** + * The channels that are text-based. + * * DMChannel + * * TextChannel + * * NewsChannel + * * ThreadChannel + * @typedef {DMChannel|TextChannel|NewsChannel|ThreadChannel} TextBasedChannels + */ + +/** + * The types of channels that are text-based. The available types are: + * * DM + * * GUILD_TEXT + * * GUILD_NEWS + * * GUILD_NEWS_THREAD + * * GUILD_PUBLIC_THREAD + * * GUILD_PRIVATE_THREAD + * @typedef {string} TextBasedChannelTypes + */ +exports.TextBasedChannelTypes = [ + 'DM', + 'GUILD_TEXT', + 'GUILD_NEWS', + 'GUILD_NEWS_THREAD', + 'GUILD_PUBLIC_THREAD', + 'GUILD_PRIVATE_THREAD', +]; + +/** + * The types of channels that are threads. The available types are: + * * GUILD_NEWS_THREAD + * * GUILD_PUBLIC_THREAD + * * GUILD_PRIVATE_THREAD + * @typedef {string} ThreadChannelTypes + */ +exports.ThreadChannelTypes = ['GUILD_NEWS_THREAD', 'GUILD_PUBLIC_THREAD', 'GUILD_PRIVATE_THREAD']; + +/** + * The types of channels that are voice-based. The available types are: + * * GUILD_VOICE + * * GUILD_STAGE_VOICE + * @typedef {string} VoiceBasedChannelTypes + */ +exports.VoiceBasedChannelTypes = ['GUILD_VOICE', 'GUILD_STAGE_VOICE']; + +exports.ClientApplicationAssetTypes = { + SMALL: 1, + BIG: 2, +}; + +exports.Colors = { + DEFAULT: 0x000000, + WHITE: 0xffffff, + AQUA: 0x1abc9c, + GREEN: 0x57f287, + BLUE: 0x3498db, + YELLOW: 0xfee75c, + PURPLE: 0x9b59b6, + LUMINOUS_VIVID_PINK: 0xe91e63, + FUCHSIA: 0xeb459e, + GOLD: 0xf1c40f, + ORANGE: 0xe67e22, + RED: 0xed4245, + GREY: 0x95a5a6, + NAVY: 0x34495e, + DARK_AQUA: 0x11806a, + DARK_GREEN: 0x1f8b4c, + DARK_BLUE: 0x206694, + DARK_PURPLE: 0x71368a, + DARK_VIVID_PINK: 0xad1457, + DARK_GOLD: 0xc27c0e, + DARK_ORANGE: 0xa84300, + DARK_RED: 0x992d22, + DARK_GREY: 0x979c9f, + DARKER_GREY: 0x7f8c8d, + LIGHT_GREY: 0xbcc0c0, + DARK_NAVY: 0x2c3e50, + BLURPLE: 0x5865f2, + GREYPLE: 0x99aab5, + DARK_BUT_NOT_BLACK: 0x2c2f33, + NOT_QUITE_BLACK: 0x23272a, +}; + +/** + * The value set for the explicit content filter levels for a guild: + * * DISABLED + * * MEMBERS_WITHOUT_ROLES + * * ALL_MEMBERS + * @typedef {string} ExplicitContentFilterLevel + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-explicit-content-filter-level} + */ +exports.ExplicitContentFilterLevels = createEnum(['DISABLED', 'MEMBERS_WITHOUT_ROLES', 'ALL_MEMBERS']); + +/** + * The value set for the verification levels for a guild: + * * NONE + * * LOW + * * MEDIUM + * * HIGH + * * VERY_HIGH + * @typedef {string} VerificationLevel + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-verification-level} + */ +exports.VerificationLevels = createEnum(['NONE', 'LOW', 'MEDIUM', 'HIGH', 'VERY_HIGH']); + +/** + * An error encountered while performing an API request. Here are the potential errors: + * * UNKNOWN_ACCOUNT + * * UNKNOWN_APPLICATION + * * UNKNOWN_CHANNEL + * * UNKNOWN_GUILD + * * UNKNOWN_INTEGRATION + * * UNKNOWN_INVITE + * * UNKNOWN_MEMBER + * * UNKNOWN_MESSAGE + * * UNKNOWN_OVERWRITE + * * UNKNOWN_PROVIDER + * * UNKNOWN_ROLE + * * UNKNOWN_TOKEN + * * UNKNOWN_USER + * * UNKNOWN_EMOJI + * * UNKNOWN_WEBHOOK + * * UNKNOWN_WEBHOOK_SERVICE + * * UNKNOWN_SESSION + * * UNKNOWN_BAN + * * UNKNOWN_SKU + * * UNKNOWN_STORE_LISTING + * * UNKNOWN_ENTITLEMENT + * * UNKNOWN_BUILD + * * UNKNOWN_LOBBY + * * UNKNOWN_BRANCH + * * UNKNOWN_STORE_DIRECTORY_LAYOUT + * * UNKNOWN_REDISTRIBUTABLE + * * UNKNOWN_GIFT_CODE + * * UNKNOWN_STREAM + * * UNKNOWN_PREMIUM_SERVER_SUBSCRIBE_COOLDOWN + * * UNKNOWN_GUILD_TEMPLATE + * * UNKNOWN_DISCOVERABLE_SERVER_CATEGORY + * * UNKNOWN_STICKER + * * UNKNOWN_INTERACTION + * * UNKNOWN_APPLICATION_COMMAND + * * UNKNOWN_APPLICATION_COMMAND_PERMISSIONS + * * UNKNOWN_STAGE_INSTANCE + * * UNKNOWN_GUILD_MEMBER_VERIFICATION_FORM + * * UNKNOWN_GUILD_WELCOME_SCREEN + * * UNKNOWN_GUILD_SCHEDULED_EVENT + * * UNKNOWN_GUILD_SCHEDULED_EVENT_USER + * * BOT_PROHIBITED_ENDPOINT + * * BOT_ONLY_ENDPOINT + * * CANNOT_SEND_EXPLICIT_CONTENT + * * NOT_AUTHORIZED + * * SLOWMODE_RATE_LIMIT + * * ACCOUNT_OWNER_ONLY + * * ANNOUNCEMENT_EDIT_LIMIT_EXCEEDED + * * CHANNEL_HIT_WRITE_RATELIMIT + * * SERVER_HIT_WRITE_RATELIMIT + * * CONTENT_NOT_ALLOWED + * * GUILD_PREMIUM_LEVEL_TOO_LOW + * * MAXIMUM_GUILDS + * * MAXIMUM_FRIENDS + * * MAXIMUM_PINS + * * MAXIMUM_RECIPIENTS + * * MAXIMUM_ROLES + * * MAXIMUM_WEBHOOKS + * * MAXIMUM_EMOJIS + * * MAXIMUM_REACTIONS + * * MAXIMUM_CHANNELS + * * MAXIMUM_ATTACHMENTS + * * MAXIMUM_INVITES + * * MAXIMUM_ANIMATED_EMOJIS + * * MAXIMUM_SERVER_MEMBERS + * * MAXIMUM_NUMBER_OF_SERVER_CATEGORIES + * * GUILD_ALREADY_HAS_TEMPLATE + * * MAXIMUM_THREAD_PARTICIPANTS + * * MAXIMUM_NON_GUILD_MEMBERS_BANS + * * MAXIMUM_BAN_FETCHES + * * MAXIMUM_NUMBER_OF_UNCOMPLETED_GUILD_SCHEDULED_EVENTS_REACHED + * * MAXIMUM_NUMBER_OF_STICKERS_REACHED + * * MAXIMUM_PRUNE_REQUESTS + * * MAXIMUM_GUILD_WIDGET_SETTINGS_UPDATE + * * UNAUTHORIZED + * * ACCOUNT_VERIFICATION_REQUIRED + * * DIRECT_MESSAGES_TOO_FAST + * * REQUEST_ENTITY_TOO_LARGE + * * FEATURE_TEMPORARILY_DISABLED + * * USER_BANNED + * * TARGET_USER_NOT_CONNECTED_TO_VOICE + * * ALREADY_CROSSPOSTED + * * MISSING_ACCESS + * * INVALID_ACCOUNT_TYPE + * * CANNOT_EXECUTE_ON_DM + * * EMBED_DISABLED + * * CANNOT_EDIT_MESSAGE_BY_OTHER + * * CANNOT_SEND_EMPTY_MESSAGE + * * CANNOT_MESSAGE_USER + * * CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL + * * CHANNEL_VERIFICATION_LEVEL_TOO_HIGH + * * OAUTH2_APPLICATION_BOT_ABSENT + * * MAXIMUM_OAUTH2_APPLICATIONS + * * INVALID_OAUTH_STATE + * * MISSING_PERMISSIONS + * * INVALID_AUTHENTICATION_TOKEN + * * NOTE_TOO_LONG + * * INVALID_BULK_DELETE_QUANTITY + * * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL + * * INVALID_OR_TAKEN_INVITE_CODE + * * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE + * * CANNOT_EXECUTE_ON_CHANNEL_TYPE + * * INVALID_OAUTH_TOKEN + * * MISSING_OAUTH_SCOPE + * * INVALID_WEBHOOK_TOKEN + * * INVALID_ROLE + * * INVALID_RECIPIENTS + * * BULK_DELETE_MESSAGE_TOO_OLD + * * INVALID_FORM_BODY + * * INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT + * * INVALID_API_VERSION + * * FILE_UPLOADED_EXCEEDS_MAXIMUM_SIZE + * * INVALID_FILE_UPLOADED + * * CANNOT_SELF_REDEEM_GIFT + * * INVALID_GUILD + * * PAYMENT_SOURCE_REQUIRED + * * CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL + * * INVALID_STICKER_SENT + * * INVALID_OPERATION_ON_ARCHIVED_THREAD + * * INVALID_THREAD_NOTIFICATION_SETTINGS + * * PARAMETER_EARLIER_THAN_CREATION + * * GUILD_NOT_AVAILABLE_IN_LOCATION + * * GUILD_MONETIZATION_REQUIRED + * * INSUFFICIENT_BOOSTS + * * INVALID_JSON + * * TWO_FACTOR_REQUIRED + * * NO_USERS_WITH_DISCORDTAG_EXIST + * * REACTION_BLOCKED + * * RESOURCE_OVERLOADED + * * STAGE_ALREADY_OPEN + * * CANNOT_REPLY_WITHOUT_READ_MESSAGE_HISTORY_PERMISSION + * * MESSAGE_ALREADY_HAS_THREAD + * * THREAD_LOCKED + * * MAXIMUM_ACTIVE_THREADS + * * MAXIMUM_ACTIVE_ANNOUNCEMENT_THREAD + * * INVALID_JSON_FOR_UPLOADED_LOTTIE_FILE + * * UPLOADED_LOTTIES_CANNOT_CONTAIN_RASTERIZED_IMAGES + * * STICKER_MAXIMUM_FRAMERATE_EXCEEDED + * * STICKER_FRAME_COUNT_EXCEEDS_MAXIMUM_OF_1000_FRAMES + * * LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS_EXCEEDED + * * STICKER_FRAME_RATE_IS_TOO_SMALL_OR_TOO_LARGE + * * STICKER_ANIMATION_DURATION_EXCEEDS_MAXIMUM_OF_5_SECONDS + * * CANNOT_UPDATE_A_FINISHED_EVENT + * * FAILED_TO_CREATE_STAGE_NEEDED_FOR_STAGE_EVENT + * @typedef {string} APIError + * @see {@link https://discord.com/developers/docs/topics/opcodes-and-status-codes#json-json-error-codes} + */ +exports.APIErrors = { + UNKNOWN_ACCOUNT: 10001, + UNKNOWN_APPLICATION: 10002, + UNKNOWN_CHANNEL: 10003, + UNKNOWN_GUILD: 10004, + UNKNOWN_INTEGRATION: 10005, + UNKNOWN_INVITE: 10006, + UNKNOWN_MEMBER: 10007, + UNKNOWN_MESSAGE: 10008, + UNKNOWN_OVERWRITE: 10009, + UNKNOWN_PROVIDER: 10010, + UNKNOWN_ROLE: 10011, + UNKNOWN_TOKEN: 10012, + UNKNOWN_USER: 10013, + UNKNOWN_EMOJI: 10014, + UNKNOWN_WEBHOOK: 10015, + UNKNOWN_WEBHOOK_SERVICE: 10016, + UNKNOWN_SESSION: 10020, + UNKNOWN_BAN: 10026, + UNKNOWN_SKU: 10027, + UNKNOWN_STORE_LISTING: 10028, + UNKNOWN_ENTITLEMENT: 10029, + UNKNOWN_BUILD: 10030, + UNKNOWN_LOBBY: 10031, + UNKNOWN_BRANCH: 10032, + UNKNOWN_STORE_DIRECTORY_LAYOUT: 10033, + UNKNOWN_REDISTRIBUTABLE: 10036, + UNKNOWN_GIFT_CODE: 10038, + UNKNOWN_STREAM: 10049, + UNKNOWN_PREMIUM_SERVER_SUBSCRIBE_COOLDOWN: 10050, + UNKNOWN_GUILD_TEMPLATE: 10057, + UNKNOWN_DISCOVERABLE_SERVER_CATEGORY: 10059, + UNKNOWN_STICKER: 10060, + UNKNOWN_INTERACTION: 10062, + UNKNOWN_APPLICATION_COMMAND: 10063, + UNKNOWN_APPLICATION_COMMAND_PERMISSIONS: 10066, + UNKNOWN_STAGE_INSTANCE: 10067, + UNKNOWN_GUILD_MEMBER_VERIFICATION_FORM: 10068, + UNKNOWN_GUILD_WELCOME_SCREEN: 10069, + UNKNOWN_GUILD_SCHEDULED_EVENT: 10070, + UNKNOWN_GUILD_SCHEDULED_EVENT_USER: 10071, + BOT_PROHIBITED_ENDPOINT: 20001, + BOT_ONLY_ENDPOINT: 20002, + CANNOT_SEND_EXPLICIT_CONTENT: 20009, + NOT_AUTHORIZED: 20012, + SLOWMODE_RATE_LIMIT: 20016, + ACCOUNT_OWNER_ONLY: 20018, + ANNOUNCEMENT_EDIT_LIMIT_EXCEEDED: 20022, + CHANNEL_HIT_WRITE_RATELIMIT: 20028, + SERVER_HIT_WRITE_RATELIMIT: 20029, + CONTENT_NOT_ALLOWED: 20031, + GUILD_PREMIUM_LEVEL_TOO_LOW: 20035, + MAXIMUM_GUILDS: 30001, + MAXIMUM_FRIENDS: 30002, + MAXIMUM_PINS: 30003, + MAXIMUM_RECIPIENTS: 30004, + MAXIMUM_ROLES: 30005, + MAXIMUM_WEBHOOKS: 30007, + MAXIMUM_EMOJIS: 30008, + MAXIMUM_REACTIONS: 30010, + MAXIMUM_CHANNELS: 30013, + MAXIMUM_ATTACHMENTS: 30015, + MAXIMUM_INVITES: 30016, + MAXIMUM_ANIMATED_EMOJIS: 30018, + MAXIMUM_SERVER_MEMBERS: 30019, + MAXIMUM_NUMBER_OF_SERVER_CATEGORIES: 30030, + GUILD_ALREADY_HAS_TEMPLATE: 30031, + MAXIMUM_THREAD_PARTICIPANTS: 30033, + MAXIMUM_NON_GUILD_MEMBERS_BANS: 30035, + MAXIMUM_BAN_FETCHES: 30037, + MAXIMUM_NUMBER_OF_UNCOMPLETED_GUILD_SCHEDULED_EVENTS_REACHED: 30038, + MAXIMUM_NUMBER_OF_STICKERS_REACHED: 30039, + MAXIMUM_PRUNE_REQUESTS: 30040, + MAXIMUM_GUILD_WIDGET_SETTINGS_UPDATE: 30042, + UNAUTHORIZED: 40001, + ACCOUNT_VERIFICATION_REQUIRED: 40002, + DIRECT_MESSAGES_TOO_FAST: 40003, + REQUEST_ENTITY_TOO_LARGE: 40005, + FEATURE_TEMPORARILY_DISABLED: 40006, + USER_BANNED: 40007, + TARGET_USER_NOT_CONNECTED_TO_VOICE: 40032, + ALREADY_CROSSPOSTED: 40033, + MISSING_ACCESS: 50001, + INVALID_ACCOUNT_TYPE: 50002, + CANNOT_EXECUTE_ON_DM: 50003, + EMBED_DISABLED: 50004, + CANNOT_EDIT_MESSAGE_BY_OTHER: 50005, + CANNOT_SEND_EMPTY_MESSAGE: 50006, + CANNOT_MESSAGE_USER: 50007, + CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: 50008, + CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: 50009, + OAUTH2_APPLICATION_BOT_ABSENT: 50010, + MAXIMUM_OAUTH2_APPLICATIONS: 50011, + INVALID_OAUTH_STATE: 50012, + MISSING_PERMISSIONS: 50013, + INVALID_AUTHENTICATION_TOKEN: 50014, + NOTE_TOO_LONG: 50015, + INVALID_BULK_DELETE_QUANTITY: 50016, + CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019, + INVALID_OR_TAKEN_INVITE_CODE: 50020, + CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021, + CANNOT_EXECUTE_ON_CHANNEL_TYPE: 50024, + INVALID_OAUTH_TOKEN: 50025, + MISSING_OAUTH_SCOPE: 50026, + INVALID_WEBHOOK_TOKEN: 50027, + INVALID_ROLE: 50028, + INVALID_RECIPIENTS: 50033, + BULK_DELETE_MESSAGE_TOO_OLD: 50034, + INVALID_FORM_BODY: 50035, + INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036, + INVALID_API_VERSION: 50041, + FILE_UPLOADED_EXCEEDS_MAXIMUM_SIZE: 50045, + INVALID_FILE_UPLOADED: 50046, + CANNOT_SELF_REDEEM_GIFT: 50054, + INVALID_GUILD: 50055, + PAYMENT_SOURCE_REQUIRED: 50070, + CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: 50074, + INVALID_STICKER_SENT: 50081, + INVALID_OPERATION_ON_ARCHIVED_THREAD: 50083, + INVALID_THREAD_NOTIFICATION_SETTINGS: 50084, + PARAMETER_EARLIER_THAN_CREATION: 50085, + GUILD_NOT_AVAILABLE_IN_LOCATION: 50095, + GUILD_MONETIZATION_REQUIRED: 50097, + INSUFFICIENT_BOOSTS: 50101, + INVALID_JSON: 50109, + TWO_FACTOR_REQUIRED: 60003, + NO_USERS_WITH_DISCORDTAG_EXIST: 80004, + REACTION_BLOCKED: 90001, + RESOURCE_OVERLOADED: 130000, + STAGE_ALREADY_OPEN: 150006, + CANNOT_REPLY_WITHOUT_READ_MESSAGE_HISTORY_PERMISSION: 160002, + MESSAGE_ALREADY_HAS_THREAD: 160004, + THREAD_LOCKED: 160005, + MAXIMUM_ACTIVE_THREADS: 160006, + MAXIMUM_ACTIVE_ANNOUNCEMENT_THREADS: 160007, + INVALID_JSON_FOR_UPLOADED_LOTTIE_FILE: 170001, + UPLOADED_LOTTIES_CANNOT_CONTAIN_RASTERIZED_IMAGES: 170002, + STICKER_MAXIMUM_FRAMERATE_EXCEEDED: 170003, + STICKER_FRAME_COUNT_EXCEEDS_MAXIMUM_OF_1000_FRAMES: 170004, + LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS_EXCEEDED: 170005, + STICKER_FRAME_RATE_IS_TOO_SMALL_OR_TOO_LARGE: 170006, + STICKER_ANIMATION_DURATION_EXCEEDS_MAXIMUM_OF_5_SECONDS: 170007, + CANNOT_UPDATE_A_FINISHED_EVENT: 180000, + FAILED_TO_CREATE_STAGE_NEEDED_FOR_STAGE_EVENT: 180002, +}; + +/** + * The value set for a guild's default message notifications, e.g. `ALL_MESSAGES`. Here are the available types: + * * ALL_MESSAGES + * * ONLY_MENTIONS + * @typedef {string} DefaultMessageNotificationLevel + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-default-message-notification-level} + */ +exports.DefaultMessageNotificationLevels = createEnum(['ALL_MESSAGES', 'ONLY_MENTIONS']); + +/** + * The value set for a team member's membership state: + * * INVITED + * * ACCEPTED + * @typedef {string} MembershipState + * @see {@link https://discord.com/developers/docs/topics/teams#data-models-membership-state-enum} + */ +exports.MembershipStates = createEnum([null, 'INVITED', 'ACCEPTED']); + +/** + * The value set for a webhook's type: + * * Incoming + * * Channel Follower + * * Application + * @typedef {string} WebhookType + * @see {@link https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types} + */ +exports.WebhookTypes = createEnum([null, 'Incoming', 'Channel Follower', 'Application']); + +/** + * The value set for a sticker's type: + * * STANDARD + * * GUILD + * @typedef {string} StickerType + * @see {@link https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types} + */ +exports.StickerTypes = createEnum([null, 'STANDARD', 'GUILD']); + +/** + * The value set for a sticker's format type: + * * PNG + * * APNG + * * LOTTIE + * @typedef {string} StickerFormatType + * @see {@link https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types} + */ +exports.StickerFormatTypes = createEnum([null, 'PNG', 'APNG', 'LOTTIE']); + +/** + * An overwrite type: + * * role + * * member + * @typedef {string} OverwriteType + * @see {@link https://discord.com/developers/docs/resources/channel#overwrite-object-overwrite-structure} + */ +exports.OverwriteTypes = createEnum(['role', 'member']); + +/* eslint-disable max-len */ +/** + * The type of an {@link ApplicationCommand} object: + * * CHAT_INPUT + * * USER + * * MESSAGE + * @typedef {string} ApplicationCommandType + * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-types} + */ +exports.ApplicationCommandTypes = createEnum([null, 'CHAT_INPUT', 'USER', 'MESSAGE']); + +/** + * The type of an {@link ApplicationCommandOption} object: + * * SUB_COMMAND + * * SUB_COMMAND_GROUP + * * STRING + * * INTEGER + * * BOOLEAN + * * USER + * * CHANNEL + * * ROLE + * * MENTIONABLE + * * NUMBER + * @typedef {string} ApplicationCommandOptionType + * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type} + */ +exports.ApplicationCommandOptionTypes = createEnum([ + null, + 'SUB_COMMAND', + 'SUB_COMMAND_GROUP', + 'STRING', + 'INTEGER', + 'BOOLEAN', + 'USER', + 'CHANNEL', + 'ROLE', + 'MENTIONABLE', + 'NUMBER', +]); + +/** + * The type of an {@link ApplicationCommandPermissions} object: + * * ROLE + * * USER + * @typedef {string} ApplicationCommandPermissionType + * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permission-type} + */ +exports.ApplicationCommandPermissionTypes = createEnum([null, 'ROLE', 'USER']); + +/** + * The type of an {@link Interaction} object: + * * PING + * * APPLICATION_COMMAND + * * MESSAGE_COMPONENT + * * APPLICATION_COMMAND_AUTOCOMPLETE + * @typedef {string} InteractionType + * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-type} + */ +exports.InteractionTypes = createEnum([ + null, + 'PING', + 'APPLICATION_COMMAND', + 'MESSAGE_COMPONENT', + 'APPLICATION_COMMAND_AUTOCOMPLETE', +]); + +/** + * The type of an interaction response: + * * PONG + * * CHANNEL_MESSAGE_WITH_SOURCE + * * DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE + * * DEFERRED_MESSAGE_UPDATE + * * UPDATE_MESSAGE + * * APPLICATION_COMMAND_AUTOCOMPLETE_RESULT + * @typedef {string} InteractionResponseType + * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-interaction-callback-type} + */ +exports.InteractionResponseTypes = createEnum([ + null, + 'PONG', + null, + null, + 'CHANNEL_MESSAGE_WITH_SOURCE', + 'DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE', + 'DEFERRED_MESSAGE_UPDATE', + 'UPDATE_MESSAGE', + 'APPLICATION_COMMAND_AUTOCOMPLETE_RESULT', +]); + +/** + * The type of a message component + * * ACTION_ROW + * * BUTTON + * * SELECT_MENU + * @typedef {string} MessageComponentType + * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types} + */ +exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON', 'SELECT_MENU']); + +/** + * The style of a message button + * * PRIMARY + * * SECONDARY + * * SUCCESS + * * DANGER + * * LINK + * @typedef {string} MessageButtonStyle + * @see {@link https://discord.com/developers/docs/interactions/message-components#button-object-button-styles} + */ +exports.MessageButtonStyles = createEnum([null, 'PRIMARY', 'SECONDARY', 'SUCCESS', 'DANGER', 'LINK']); + +/** + * The required MFA level for a guild + * * NONE + * * ELEVATED + * @typedef {string} MFALevel + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-mfa-level} + */ +exports.MFALevels = createEnum(['NONE', 'ELEVATED']); + +/** + * NSFW level of a Guild: + * * DEFAULT + * * EXPLICIT + * * SAFE + * * AGE_RESTRICTED + * @typedef {string} NSFWLevel + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-guild-nsfw-level} + */ +exports.NSFWLevels = createEnum(['DEFAULT', 'EXPLICIT', 'SAFE', 'AGE_RESTRICTED']); + +/** + * Privacy level of a {@link StageInstance} object: + * * PUBLIC + * * GUILD_ONLY + * @typedef {string} PrivacyLevel + * @see {@link https://discord.com/developers/docs/resources/stage-instance#stage-instance-object-privacy-level} + */ +exports.PrivacyLevels = createEnum([null, 'PUBLIC', 'GUILD_ONLY']); + +/** + * Privacy level of a {@link GuildScheduledEvent} object: + * * GUILD_ONLY + * @typedef {string} GuildScheduledEventPrivacyLevel + * @see {@link https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-privacy-level} + */ +exports.GuildScheduledEventPrivacyLevels = createEnum([null, null, 'GUILD_ONLY']); + +/** + * The premium tier (Server Boost level) of a guild: + * * NONE + * * TIER_1 + * * TIER_2 + * * TIER_3 + * @typedef {string} PremiumTier + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-premium-tier} + */ +exports.PremiumTiers = createEnum(['NONE', 'TIER_1', 'TIER_2', 'TIER_3']); + +/** + * The status of a {@link GuildScheduledEvent}: + * * SCHEDULED + * * ACTIVE + * * COMPLETED + * * CANCELED + * @typedef {string} GuildScheduledEventStatus + * @see {@link https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-status} + */ +exports.GuildScheduledEventStatuses = createEnum([null, 'SCHEDULED', 'ACTIVE', 'COMPLETED', 'CANCELED']); + +/** + * The entity type of a {@link GuildScheduledEvent}: + * * NONE + * * STAGE_INSTANCE + * * VOICE + * * EXTERNAL + * @typedef {string} GuildScheduledEventEntityType + * @see {@link https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-entity-types} + */ +exports.GuildScheduledEventEntityTypes = createEnum([null, 'STAGE_INSTANCE', 'VOICE', 'EXTERNAL']); +/* eslint-enable max-len */ + +exports._cleanupSymbol = Symbol('djsCleanup'); + +function keyMirror(arr) { + let tmp = Object.create(null); + for (const value of arr) tmp[value] = value; + return tmp; +} + +function createEnum(keys) { + const obj = {}; + for (const [index, key] of keys.entries()) { + if (key === null) continue; + obj[key] = index; + obj[index] = key; + } + return obj; } /** - * Options for Image URLs. - * @typedef {StaticImageURLOptions} ImageURLOptions - * @property {boolean} [dynamic=false] If true, the format will dynamically change to `gif` for animated avatars. + * @typedef {Object} Constants Constants that can be used in an enum or object-like way. + * @property {ActivityType} ActivityTypes The type of an activity of a users presence. + * @property {APIError} APIErrors An error encountered while performing an API request. + * @property {ApplicationCommandOptionType} ApplicationCommandOptionTypes + * The type of an {@link ApplicationCommandOption} object. + * @property {ApplicationCommandPermissionType} ApplicationCommandPermissionTypes + * The type of an {@link ApplicationCommandPermissions} object. + * @property {ChannelType} ChannelTypes All available channel types. + * @property {DefaultMessageNotificationLevel} DefaultMessageNotificationLevels + * The value set for a guild's default message notifications. + * @property {ExplicitContentFilterLevel} ExplicitContentFilterLevels + * The value set for the explicit content filter levels for a guild. + * @property {GuildScheduledEventStatus} GuildScheduledEventStatuses The status of a {@link GuildScheduledEvent} object. + * @property {GuildScheduledEventEntityType} GuildScheduledEventEntityTypes The entity type of a + * {@link GuildScheduledEvent} object. + * @property {GuildScheduledEventPrivacyLevel} GuildScheduledEventPrivacyLevels Privacy level of a + * {@link GuildScheduledEvent} object. + * @property {InteractionResponseType} InteractionResponseTypes The type of an interaction response. + * @property {InteractionType} InteractionTypes The type of an {@link Interaction} object. + * @property {MembershipState} MembershipStates The value set for a team member's membership state. + * @property {MessageButtonStyle} MessageButtonStyles The style of a message button. + * @property {MessageComponentType} MessageComponentTypes The type of a message component. + * @property {MFALevel} MFALevels The required MFA level for a guild. + * @property {NSFWLevel} NSFWLevels NSFW level of a guild. + * @property {OverwriteType} OverwriteTypes An overwrite type. + * @property {PartialType} PartialTypes The type of Structure allowed to be a partial. + * @property {PremiumTier} PremiumTiers The premium tier (Server Boost level) of a guild. + * @property {PrivacyLevel} PrivacyLevels Privacy level of a {@link StageInstance} object. + * @property {Status} Status The available statuses of the client. + * @property {StickerFormatType} StickerFormatTypes The value set for a sticker's format type. + * @property {StickerType} StickerTypes The value set for a sticker's type. + * @property {VerificationLevel} VerificationLevels The value set for the verification levels for a guild. + * @property {WebhookType} WebhookTypes The value set for a webhook's type. + * @property {WSEventType} WSEvents The type of a WebSocket message event. */ - -/** - * Options for static Image URLs. - * @typedef {Object} StaticImageURLOptions - * @property {string} [format='webp'] One of `webp`, `png`, `jpg`, `jpeg`. - * @property {number} [size] One of `16`, `32`, `56`, `64`, `96`, `128`, `256`, `300`, `512`, `600`, `1024`, `2048`, - * `4096` - */ - -// https://discord.com/developers/docs/reference#image-formatting-cdn-endpoints -exports.Endpoints = { - CDN(root) { - return { - Emoji: (emojiId, format = 'webp') => `${root}/emojis/${emojiId}.${format}`, - Asset: name => `${root}/assets/${name}`, - DefaultAvatar: discriminator => `${root}/embed/avatars/${discriminator}.png`, - Avatar: (userId, hash, format, size, dynamic = false) => { - if (dynamic && hash.startsWith('a_')) format = 'gif'; - return makeImageUrl(`${root}/avatars/${userId}/${hash}`, { format, size }); - }, - GuildMemberAvatar: (guildId, memberId, hash, format = 'webp', size, dynamic = false) => { - if (dynamic && hash.startsWith('a_')) format = 'gif'; - return makeImageUrl(`${root}/guilds/${guildId}/users/${memberId}/avatars/${hash}`, { format, size }); - }, - Banner: (id, hash, format, size, dynamic = false) => { - if (dynamic && hash.startsWith('a_')) format = 'gif'; - return makeImageUrl(`${root}/banners/${id}/${hash}`, { format, size }); - }, - Icon: (guildId, hash, format, size, dynamic = false) => { - if (dynamic && hash.startsWith('a_')) format = 'gif'; - return makeImageUrl(`${root}/icons/${guildId}/${hash}`, { format, size }); - }, - AppIcon: (appId, hash, options) => makeImageUrl(`${root}/app-icons/${appId}/${hash}`, options), - AppAsset: (appId, hash, options) => makeImageUrl(`${root}/app-assets/${appId}/${hash}`, options), - StickerPackBanner: (bannerId, format, size) => - makeImageUrl(`${root}/app-assets/710982414301790216/store/${bannerId}`, { size, format }), - GDMIcon: (channelId, hash, format, size) => - makeImageUrl(`${root}/channel-icons/${channelId}/${hash}`, { size, format }), - Splash: (guildId, hash, format, size) => makeImageUrl(`${root}/splashes/${guildId}/${hash}`, { size, format }), - DiscoverySplash: (guildId, hash, format, size) => - makeImageUrl(`${root}/discovery-splashes/${guildId}/${hash}`, { size, format }), - TeamIcon: (teamId, hash, options) => makeImageUrl(`${root}/team-icons/${teamId}/${hash}`, options), - Sticker: (stickerId, stickerFormat) => - `${root}/stickers/${stickerId}.${stickerFormat === 'LOTTIE' ? 'json' : 'png'}`, - RoleIcon: (roleId, hash, format = 'webp', size) => - makeImageUrl(`${root}/role-icons/${roleId}/${hash}`, { size, format }), - }; - }, - invite: (root, code, eventId) => (eventId ? `${root}/${code}?event=${eventId}` : `${root}/${code}`), - scheduledEvent: (root, guildId, eventId) => `${root}/${guildId}/${eventId}`, - botGateway: '/gateway/bot', -}; \ No newline at end of file diff --git a/src/util/DataResolver.js b/src/util/DataResolver.js index e3ab586..d7d9eb7 100644 --- a/src/util/DataResolver.js +++ b/src/util/DataResolver.js @@ -1,10 +1,10 @@ 'use strict'; const { Buffer } = require('node:buffer'); -const fs = require('node:fs/promises'); +const fs = require('node:fs'); const path = require('node:path'); const stream = require('node:stream'); -const { fetch } = require('undici'); +const fetch = require('node-fetch'); const { Error: DiscordError, TypeError } = require('../errors'); const Invite = require('../structures/Invite'); @@ -66,8 +66,8 @@ class DataResolver extends null { if (typeof image === 'string' && image.startsWith('data:')) { return image; } - const file = await this.resolveFile(image); - return this.resolveBase64(file); + const file = await this.resolveFileAsBuffer(image); + return DataResolver.resolveBase64(file); } /** @@ -102,34 +102,44 @@ class DataResolver extends null { */ /** - * Resolves a BufferResolvable to a Buffer. + * Resolves a BufferResolvable to a Buffer or a Stream. * @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve - * @returns {Promise} + * @returns {Promise} */ static async resolveFile(resource) { - if (Buffer.isBuffer(resource)) return resource; - - if (resource instanceof stream.Readable) { - const buffers = []; - for await (const data of resource) buffers.push(data); - return Buffer.concat(buffers); - } - + if (Buffer.isBuffer(resource) || resource instanceof stream.Readable) return resource; if (typeof resource === 'string') { if (/^https?:\/\//.test(resource)) { const res = await fetch(resource); - return Buffer.from(await res.arrayBuffer()); + return res.body; } - const file = path.resolve(resource); - - const stats = await fs.stat(file); - if (!stats.isFile()) throw new DiscordError('FILE_NOT_FOUND', file); - return fs.readFile(file); + return new Promise((resolve, reject) => { + const file = path.resolve(resource); + fs.stat(file, (err, stats) => { + if (err) return reject(err); + if (!stats.isFile()) return reject(new DiscordError('FILE_NOT_FOUND', file)); + return resolve(fs.createReadStream(file)); + }); + }); } throw new TypeError('REQ_RESOURCE_TYPE'); } + + /** + * Resolves a BufferResolvable to a Buffer. + * @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve + * @returns {Promise} + */ + static async resolveFileAsBuffer(resource) { + const file = await this.resolveFile(resource); + if (Buffer.isBuffer(file)) return file; + + const buffers = []; + for await (const data of file) buffers.push(data); + return Buffer.concat(buffers); + } } module.exports = DataResolver; diff --git a/src/util/Embeds.js b/src/util/Embeds.js deleted file mode 100644 index d6294b2..00000000 --- a/src/util/Embeds.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -/** - * @typedef {Object} EmbedData - * @property {?string} title - * @property {?EmbedType} type - * @property {?string} description - * @property {?string} url - * @property {?string} timestamp - * @property {?number} color - * @property {?EmbedFooterData} footer - * @property {?EmbedImageData} image - * @property {?EmbedImageData} thumbnail - * @property {?EmbedProviderData} provider - * @property {?EmbedAuthorData} author - * @property {?EmbedFieldData[]} fields - */ - -/** - * @typedef {Object} EmbedFooterData - * @property {string} text - * @property {?string} iconURL - */ - -/** - * @typedef {Object} EmbedImageData - * @property {?string} url - */ - -/** - * @typedef {Object} EmbedProviderData - * @property {?string} name - * @property {?string} url - */ - -/** - * @typedef {Object} EmbedAuthorData - * @property {string} name - * @property {?string} url - * @property {?string} iconURL - */ - -/** - * @typedef {Object} EmbedFieldData - * @property {string} name - * @property {string} value - * @property {?boolean} inline - */ diff --git a/src/util/EnumResolvers.js b/src/util/EnumResolvers.js deleted file mode 100644 index 92684af..00000000 --- a/src/util/EnumResolvers.js +++ /dev/null @@ -1,819 +0,0 @@ -'use strict'; - -const { - ApplicationCommandType, - InteractionType, - ComponentType, - ButtonStyle, - ApplicationCommandOptionType, - ChannelType, - ApplicationCommandPermissionType, - MessageType, - GuildNSFWLevel, - GuildVerificationLevel, - GuildDefaultMessageNotifications, - GuildExplicitContentFilter, - GuildPremiumTier, - GuildScheduledEventStatus, - StageInstancePrivacyLevel, - GuildMFALevel, - TeamMemberMembershipState, - GuildScheduledEventEntityType, - IntegrationExpireBehavior, - AuditLogEvent, -} = require('discord-api-types/v9'); - -function unknownKeyStrategy(val) { - throw new Error(`Could not resolve enum value for ${val}`); -} - -/** - * Holds a bunch of methods to resolve enum values to readable strings. - */ -class EnumResolvers extends null { - /** - * A string that can be resolved to a {@link ChannelType} enum value. Here are the available types: - * * GUILD_TEXT - * * DM - * * GUILD_VOICE - * * GROUP_DM - * * GUILD_CATEGORY - * * GUILD_NEWS - * * GUILD_NEWS_THREAD - * * GUILD_PUBLIC_THREAD - * * GUILD_PRIVATE_THREAD - * * GUILD_STAGE_VOICE - * @typedef {string} ChannelTypeEnumResolvable - */ - - /** - * Resolves enum key to {@link ChannelType} enum value - * @param {ChannelTypeEnumResolvable|ChannelType} key The key to resolve - * @returns {ChannelType} - */ - static resolveChannelType(key) { - switch (key) { - case 'GUILD_TEXT': - return ChannelType.GuildText; - case 'DM': - return ChannelType.DM; - case 'GUILD_VOICE': - return ChannelType.GuildVoice; - case 'GROUP_DM': - return ChannelType.GroupDM; - case 'GUILD_CATEGORY': - return ChannelType.GuildCategory; - case 'GUILD_NEWS': - return ChannelType.GuildNews; - case 'GUILD_NEWS_THREAD': - return ChannelType.GuildNewsThread; - case 'GUILD_PUBLIC_THREAD': - return ChannelType.GuildPublicThread; - case 'GUILD_PRIVATE_THREAD': - return ChannelType.GuildPrivateThread; - case 'GUILD_STAGE_VOICE': - return ChannelType.GuildStageVoice; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to an {@link InteractionType} enum value. Here are the available types: - * * PING - * * APPLICATION_COMMAND - * * MESSAGE_COMPONENT - * * APPLICATION_COMMAND_AUTOCOMPLETE - * @typedef {string} InteractionTypeEnumResolvable - */ - - /** - * Resolves enum key to {@link InteractionType} enum value - * @param {InteractionTypeEnumResolvable|InteractionType} key The key to resolve - * @returns {InteractionType} - */ - static resolveInteractionType(key) { - switch (key) { - case 'PING': - return InteractionType.Ping; - case 'APPLICATION_COMMAND': - return InteractionType.ApplicationCommand; - case 'MESSAGE_COMPONENT': - return InteractionType.MessageComponent; - case 'APPLICATION_COMMAND_AUTOCOMPLETE': - return InteractionType.ApplicationCommandAutocomplete; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to an {@link ApplicationCommandType} enum value. Here are the available types: - * * CHAT_INPUT - * * USER - * * MESSAGE - * @typedef {string} ApplicationCommandTypeEnumResolvable - */ - - /** - * Resolves enum key to {@link ApplicationCommandType} enum value - * @param {ApplicationCommandTypeEnumResolvable|ApplicationCommandType} key The key to resolve - * @returns {ApplicationCommandType} - */ - static resolveApplicationCommandType(key) { - switch (key) { - case 'CHAT_INPUT': - return ApplicationCommandType.ChatInput; - case 'USER': - return ApplicationCommandType.User; - case 'MESSAGE': - return ApplicationCommandType.Message; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to an {@link ApplicationCommandOptionType} enum value. Here are the available types: - * * SUB_COMMAND - * * SUB_COMMAND_GROUP - * * STRING - * * INTEGER - * * BOOLEAN - * * USER - * * CHANNEL - * * ROLE - * * NUMBER - * * MENTIONABLE - * @typedef {string} ApplicationCommandOptionTypeEnumResolvable - */ - - /** - * Resolves enum key to {@link ApplicationCommandOptionType} enum value - * @param {ApplicationCommandOptionTypeEnumResolvable|ApplicationCommandOptionType} key The key to resolve - * @returns {ApplicationCommandOptionType} - */ - static resolveApplicationCommandOptionType(key) { - switch (key) { - case 'SUB_COMMAND': - return ApplicationCommandOptionType.Subcommand; - case 'SUB_COMMAND_GROUP': - return ApplicationCommandOptionType.SubcommandGroup; - case 'STRING': - return ApplicationCommandOptionType.String; - case 'INTEGER': - return ApplicationCommandOptionType.Integer; - case 'BOOLEAN': - return ApplicationCommandOptionType.Boolean; - case 'USER': - return ApplicationCommandOptionType.User; - case 'CHANNEL': - return ApplicationCommandOptionType.Channel; - case 'ROLE': - return ApplicationCommandOptionType.Role; - case 'NUMBER': - return ApplicationCommandOptionType.Number; - case 'MENTIONABLE': - return ApplicationCommandOptionType.Mentionable; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to an {@link ApplicationCommandPermissionType} enum value. - * Here are the available types: - * * ROLE - * * USER - * @typedef {string} ApplicationCommandPermissionTypeEnumResolvable - */ - - /** - * Resolves enum key to {@link ApplicationCommandPermissionType} enum value - * @param {ApplicationCommandPermissionTypeEnumResolvable|ApplicationCommandPermissionType} key The key to resolve - * @returns {ApplicationCommandPermissionType} - */ - static resolveApplicationCommandPermissionType(key) { - switch (key) { - case 'ROLE': - return ApplicationCommandPermissionType.Role; - case 'USER': - return ApplicationCommandPermissionType.User; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link ComponentType} enum value. Here are the available types: - * * ACTION_ROW - * * BUTTON - * * SELECT_MENU - * @typedef {string} ComponentTypeEnumResolvable - */ - - /** - * Resolves enum key to {@link ComponentType} enum value - * @param {ComponentTypeEnumResolvable|ComponentType} key The key to resolve - * @returns {ComponentType} - */ - static resolveComponentType(key) { - switch (key) { - case 'ACTION_ROW': - return ComponentType.ActionRow; - case 'BUTTON': - return ComponentType.Button; - case 'SELECT_MENU': - return ComponentType.SelectMenu; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link ButtonStyle} enum value. Here are the available types: - * * PRIMARY - * * SECONDARY - * * SUCCESS - * * DANGER - * * LINK - * @typedef {string} ButtonStyleEnumResolvable - */ - - /** - * Resolves enum key to {@link ButtonStyle} enum value - * @param {ButtonStyleEnumResolvable|ButtonStyle} key The key to resolve - * @returns {ButtonStyle} - */ - static resolveButtonStyle(key) { - switch (key) { - case 'PRIMARY': - return ButtonStyle.Primary; - case 'SECONDARY': - return ButtonStyle.Secondary; - case 'SUCCESS': - return ButtonStyle.Success; - case 'DANGER': - return ButtonStyle.Danger; - case 'LINK': - return ButtonStyle.Link; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link MessageType} enum value. Here are the available types: - * * DEFAULT - * * RECIPIENT_ADD - * * RECIPIENT_REMOVE - * * CALL - * * CHANNEL_NAME_CHANGE - * * CHANNEL_ICON_CHANGE - * * CHANNEL_PINNED_MESSAGE - * * GUILD_MEMBER_JOIN - * * USER_PREMIUM_GUILD_SUBSCRIPTION - * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 - * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 - * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 - * * CHANNEL_FOLLOW_ADD - * * GUILD_DISCOVERY_DISQUALIFIED - * * GUILD_DISCOVERY_REQUALIFIED - * * GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING - * * GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING - * * THREAD_CREATED - * * REPLY - * * CHAT_INPUT_COMMAND - * * THREAD_STARTER_MESSAGE - * * GUILD_INVITE_REMINDER - * * CONTEXT_MENU_COMMAND - * @typedef {string} MessageTypeEnumResolvable - */ - - /** - * Resolves enum key to {@link MessageType} enum value - * @param {MessageTypeEnumResolvable|MessageType} key The key to lookup - * @returns {MessageType} - */ - static resolveMessageType(key) { - switch (key) { - case 'DEFAULT': - return MessageType.Default; - case 'RECIPIENT_ADD': - return MessageType.RecipientAdd; - case 'RECIPIENT_REMOVE': - return MessageType.RecipientRemove; - case 'CALL': - return MessageType.Call; - case 'CHANNEL_NAME_CHANGE': - return MessageType.ChannelNameChange; - case 'CHANNEL_ICON_CHANGE': - return MessageType.ChannelIconChange; - case 'CHANNEL_PINNED_MESSAGE': - return MessageType.ChannelPinnedMessage; - case 'GUILD_MEMBER_JOIN': - return MessageType.GuildMemberJoin; - case 'USER_PREMIUM_GUILD_SUBSCRIPTION': - return MessageType.UserPremiumGuildSubscription; - case 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1': - return MessageType.UserPremiumGuildSubscriptionTier1; - case 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2': - return MessageType.UserPremiumGuildSubscriptionTier2; - case 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3': - return MessageType.UserPremiumGuildSubscriptionTier3; - case 'CHANNEL_FOLLOW_ADD': - return MessageType.ChannelFollowAdd; - case 'GUILD_DISCOVERY_DISQUALIFIED': - return MessageType.GuildDiscoveryDisqualified; - case 'GUILD_DISCOVERY_REQUALIFIED': - return MessageType.GuildDiscoveryRequalified; - case 'GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING': - return MessageType.GuildDiscoveryGracePeriodInitialWarning; - case 'GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING': - return MessageType.GuildDiscoveryGracePeriodFinalWarning; - case 'THREAD_CREATED': - return MessageType.ThreadCreated; - case 'REPLY': - return MessageType.Reply; - case 'CHAT_INPUT_COMMAND': - return MessageType.ChatInputCommand; - case 'THREAD_STARTER_MESSAGE': - return MessageType.ThreadStarterMessage; - case 'GUILD_INVITE_REMINDER': - return MessageType.GuildInviteReminder; - case 'CONTEXT_MENU_COMMAND': - return MessageType.ContextMenuCommand; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link GuildNSFWLevel} enum value. Here are the available types: - * * DEFAULT - * * EXPLICIT - * * SAFE - * * AGE_RESTRICTED - * @typedef {string} GuildNSFWLevelEnumResolvable - */ - - /** - * Resolves enum key to {@link GuildNSFWLevel} enum value - * @param {GuildNSFWLevelEnumResolvable|GuildNSFWLevel} key The key to lookup - * @returns {GuildNSFWLevel} - */ - static resolveGuildNSFWLevel(key) { - switch (key) { - case 'DEFAULT': - return GuildNSFWLevel.Default; - case 'EXPLICIT': - return GuildNSFWLevel.Explicit; - case 'SAFE': - return GuildNSFWLevel.Safe; - case 'AGE_RESTRICTED': - return GuildNSFWLevel.AgeRestricted; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link GuildVerificationLevel} enum value. Here are the available types: - * * NONE - * * LOW - * * MEDIUM - * * HIGH - * * VERY_HIGH - * @typedef {string} GuildVerificationLevelEnumResolvable - */ - - /** - * Resolves enum key to {@link GuildVerificationLevel} enum value - * @param {GuildVerificationLevelEnumResolvable|GuildVerificationLevel} key The key to lookup - * @returns {GuildVerificationLevel} - */ - static resolveGuildVerificationLevel(key) { - switch (key) { - case 'NONE': - return GuildVerificationLevel.None; - case 'LOW': - return GuildVerificationLevel.Low; - case 'MEDIUM': - return GuildVerificationLevel.Medium; - case 'HIGH': - return GuildVerificationLevel.High; - case 'VERY_HIGH': - return GuildVerificationLevel.VeryHigh; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link GuildDefaultMessageNotifications} enum value. - * Here are the available types: - * * ALL_MESSAGES - * * ONLY_MENTIONS - * @typedef {string} GuildDefaultMessageNotificationsEnumResolvable - */ - - /** - * Resolves enum key to {@link GuildDefaultMessageNotifications} enum value - * @param {GuildDefaultMessageNotificationsEnumResolvable|GuildDefaultMessageNotifications} key The key to lookup - * @returns {GuildDefaultMessageNotifications} - */ - static resolveGuildDefaultMessageNotifications(key) { - switch (key) { - case 'ALL_MESSAGES': - return GuildDefaultMessageNotifications.AllMessages; - case 'ONLY_MENTIONS': - return GuildDefaultMessageNotifications.OnlyMentions; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link GuildExplicitContentFilter} enum value. Here are the available types: - * * DISABLED - * * MEMBERS_WITHOUT_ROLES - * * ALL_MEMBERS - * @typedef {string} GuildExplicitContentFilterEnumResolvable - */ - - /** - * Resolves enum key to {@link GuildExplicitContentFilter} enum value - * @param {GuildExplicitContentFilterEnumResolvable|GuildExplicitContentFilter} key The key to lookup - * @returns {GuildExplicitContentFilter} - */ - static resolveGuildExplicitContentFilter(key) { - switch (key) { - case 'DISABLED': - return GuildExplicitContentFilter.Disabled; - case 'MEMBERS_WITHOUT_ROLES': - return GuildExplicitContentFilter.MembersWithoutRoles; - case 'ALL_MEMBERS': - return GuildExplicitContentFilter.AllMembers; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link GuildPremiumTier} enum value. Here are the available types: - * * NONE - * * TIER_1 - * * TIER_2 - * * TIER_3 - * @typedef {string} GuildPremiumTierEnumResolvable - */ - - /** - * Resolves enum key to {@link GuildPremiumTier} enum value - * @param {GuildPremiumTierEnumResolvable|GuildPremiumTier} key The key to lookup - * @returns {GuildPremiumTier} - */ - static resolveGuildPremiumTier(key) { - switch (key) { - case 'NONE': - return GuildPremiumTier.None; - case 'TIER_1': - return GuildPremiumTier.Tier1; - case 'TIER_2': - return GuildPremiumTier.Tier2; - case 'TIER_3': - return GuildPremiumTier.Tier3; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link GuildScheduledEventStatus} enum value. Here are the available types: - * * SCHEDULED - * * ACTIVE - * * COMPLETED - * * CANCELED - * @typedef {string} GuildScheduledEventStatusEnumResolvable - */ - - /** - * Resolves enum key to {@link GuildScheduledEventStatus} enum value - * @param {GuildScheduledEventStatusEnumResolvable|GuildScheduledEventStatus} key The key to lookup - * @returns {GuildScheduledEventStatus} - */ - static resolveGuildScheduledEventStatus(key) { - switch (key) { - case 'SCHEDULED': - return GuildScheduledEventStatus.Scheduled; - case 'ACTIVE': - return GuildScheduledEventStatus.Active; - case 'COMPLETED': - return GuildScheduledEventStatus.Completed; - case 'CANCELED': - return GuildScheduledEventStatus.Canceled; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link StageInstancePrivacyLevel} enum value. Here are the available types: - * * PUBLIC - * * GUILD_ONLY - * @typedef {string} StageInstancePrivacyLevelEnumResolvable - */ - - /** - * Resolves enum key to {@link StageInstancePrivacyLevel} enum value - * @param {StageInstancePrivacyLevelEnumResolvable|StageInstancePrivacyLevel} key The key to lookup - * @returns {StageInstancePrivacyLevel} - */ - static resolveStageInstancePrivacyLevel(key) { - switch (key) { - case 'PUBLIC': - return StageInstancePrivacyLevel.Public; - case 'GUILD_ONLY': - return StageInstancePrivacyLevel.GuildOnly; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link GuildMFALevel} enum value. Here are the available types: - * * NONE - * * ELEVATED - * @typedef {string} GuildMFALevelEnumResolvable - */ - - /** - * Resolves enum key to {@link GuildMFALevel} enum value - * @param {GuildMFALevelEnumResolvable|GuildMFALevel} key The key to lookup - * @returns {GuildMFALevel} - */ - static resolveGuildMFALevel(key) { - switch (key) { - case 'NONE': - return GuildMFALevel.None; - case 'ELEVATED': - return GuildMFALevel.Elevated; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link TeamMemberMembershipState} enum value. Here are the available types: - * * INVITED - * * ACCEPTED - * @typedef {string} TeamMemberMembershipStateEnumResolvable - */ - - /** - * Resolves enum key to {@link TeamMemberMembershipState} enum value - * @param {TeamMemberMembershipStateEnumResolvable|TeamMemberMembershipState} key The key to lookup - * @returns {TeamMemberMembershipState} - */ - static resolveTeamMemberMembershipState(key) { - switch (key) { - case 'INVITED': - return TeamMemberMembershipState.Invited; - case 'ACCEPTED': - return TeamMemberMembershipState.Accepted; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link GuildScheduledEventEntityType} enum value. Here are the available types: - * * STAGE_INSTANCE - * * VOICE - * * EXTERNAL - * @typedef {string} GuildScheduledEventEntityTypeEnumResolvable - */ - - /** - * Resolves enum key to {@link GuildScheduledEventEntityType} enum value - * @param {GuildScheduledEventEntityTypeEnumResolvable|GuildScheduledEventEntityType} key The key to lookup - * @returns {GuildScheduledEventEntityType} - */ - static resolveGuildScheduledEventEntityType(key) { - switch (key) { - case 'STAGE_INSTANCE': - return GuildScheduledEventEntityType.StageInstance; - case 'VOICE': - return GuildScheduledEventEntityType.Voice; - case 'EXTERNAL': - return GuildScheduledEventEntityType.External; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link IntegrationExpireBehavior} enum value. Here are the available types: - * * REMOVE_ROLE - * * KICK - * @typedef {string} IntegrationExpireBehaviorEnumResolvable - */ - - /** - * Resolves enum key to {@link IntegrationExpireBehavior} enum value - * @param {IntegrationExpireBehaviorEnumResolvable|IntegrationExpireBehavior} key The key to lookup - * @returns {IntegrationExpireBehavior} - */ - static resolveIntegrationExpireBehavior(key) { - switch (key) { - case 'REMOVE_ROLE': - return IntegrationExpireBehavior.RemoveRole; - case 'KICK': - return IntegrationExpireBehavior.Kick; - default: - return unknownKeyStrategy(key); - } - } - - /** - * A string that can be resolved to a {@link AuditLogEvent} enum value. Here are the available types: - * * GUILD_UPDATE - * * CHANNEL_CREATE - * * CHANNEL_UPDATE - * * CHANNEL_DELETE - * * CHANNEL_OVERWRITE_CREATE - * * CHANNEL_OVERWRITE_UPDATE - * * CHANNEL_OVERWRITE_DELETE - * * MEMBER_KICK - * * MEMBER_PRUNE - * * MEMBER_BAN_ADD - * * MEMBER_BAN_REMOVE - * * MEMBER_UPDATE - * * MEMBER_ROLE_UPDATE - * * MEMBER_MOVE - * * MEMBER_DISCONNECT - * * BOT_ADD - * * ROLE_CREATE - * * ROLE_UPDATE - * * ROLE_DELETE - * * INVITE_CREATE - * * INVITE_UPDATE - * * INVITE_DELETE - * * WEBHOOK_CREATE - * * WEBHOOK_UPDATE - * * WEBHOOK_DELETE - * * INTEGRATION_CREATE - * * INTEGRATION_UPDATE - * * INTEGRATION_DELETE - * * STAGE_INSTANCE_CREATE - * * STAGE_INSTANCE_UPDATE - * * STAGE_INSTANCE_DELETE - * * STICKER_CREATE - * * STICKER_UPDATE - * * STICKER_DELETE - * * GUILD_SCHEDULED_EVENT_CREATE - * * GUILD_SCHEDULED_EVENT_UPDATE - * * GUILD_SCHEDULED_EVENT_DELETE - * * THREAD_CREATE - * * THREAD_UPDATE - * * THREAD_DELETE - * @typedef {string} AuditLogEventEnumResolvable - */ - - /** - * Resolves enum key to {@link AuditLogEvent} enum value - * @param {AuditLogEventEnumResolvable|AuditLogEvent} key The key to lookup - * @returns {AuditLogEvent} - */ - static resolveAuditLogEvent(key) { - switch (key) { - case 'GUILD_UPDATE': - return AuditLogEvent.GuildUpdate; - case 'CHANNEL_CREATE': - return AuditLogEvent.ChannelCreate; - case 'CHANNEL_UPDATE': - return AuditLogEvent.ChannelUpdate; - case 'CHANNEL_DELETE': - return AuditLogEvent.ChannelDelete; - case 'CHANNEL_OVERWRITE_CREATE': - return AuditLogEvent.ChannelOverwriteCreate; - case 'CHANNEL_OVERWRITE_UPDATE': - return AuditLogEvent.ChannelOverwriteUpdate; - case 'CHANNEL_OVERWRITE_DELETE': - return AuditLogEvent.ChannelOverwriteDelete; - case 'MEMBER_KICK': - return AuditLogEvent.MemberKick; - case 'MEMBER_PRUNE': - return AuditLogEvent.MemberPrune; - case 'MEMBER_BAN_ADD': - return AuditLogEvent.MemberBanAdd; - case 'MEMBER_BAN_REMOVE': - return AuditLogEvent.MemberBanRemove; - case 'MEMBER_UPDATE': - return AuditLogEvent.MemberUpdate; - case 'MEMBER_ROLE_UPDATE': - return AuditLogEvent.MemberRoleUpdate; - case 'MEMBER_MOVE': - return AuditLogEvent.MemberMove; - case 'MEMBER_DISCONNECT': - return AuditLogEvent.MemberDisconnect; - case 'BOT_ADD': - return AuditLogEvent.BotAdd; - case 'ROLE_CREATE': - return AuditLogEvent.RoleCreate; - case 'ROLE_UPDATE': - return AuditLogEvent.RoleUpdate; - case 'ROLE_DELETE': - return AuditLogEvent.RoleDelete; - case 'INVITE_CREATE': - return AuditLogEvent.InviteCreate; - case 'INVITE_UPDATE': - return AuditLogEvent.InviteUpdate; - case 'INVITE_DELETE': - return AuditLogEvent.InviteDelete; - case 'WEBHOOK_CREATE': - return AuditLogEvent.WebhookCreate; - case 'WEBHOOK_UPDATE': - return AuditLogEvent.WebhookUpdate; - case 'WEBHOOK_DELETE': - return AuditLogEvent.WebhookDelete; - case 'EMOJI_CREATE': - return AuditLogEvent.EmojiCreate; - case 'EMOJI_UPDATE': - return AuditLogEvent.EmojiUpdate; - case 'EMOJI_DELETE': - return AuditLogEvent.EmojiDelete; - case 'MESSAGE_DELETE': - return AuditLogEvent.MessageDelete; - case 'MESSAGE_BULK_DELETE': - return AuditLogEvent.MessageBulkDelete; - case 'MESSAGE_PIN': - return AuditLogEvent.MessagePin; - case 'MESSAGE_UNPIN': - return AuditLogEvent.MessageUnpin; - case 'INTEGRATION_CREATE': - return AuditLogEvent.IntegrationCreate; - case 'INTEGRATION_UPDATE': - return AuditLogEvent.IntegrationUpdate; - case 'INTEGRATION_DELETE': - return AuditLogEvent.IntegrationDelete; - case 'STAGE_INSTANCE_CREATE': - return AuditLogEvent.StageInstanceCreate; - case 'STAGE_INSTANCE_UPDATE': - return AuditLogEvent.StageInstanceUpdate; - case 'STAGE_INSTANCE_DELETE': - return AuditLogEvent.StageInstanceDelete; - case 'STICKER_CREATE': - return AuditLogEvent.StickerCreate; - case 'STICKER_UPDATE': - return AuditLogEvent.StickerUpdate; - case 'STICKER_DELETE': - return AuditLogEvent.StickerDelete; - case 'GUILD_SCHEDULED_EVENT_CREATE': - return AuditLogEvent.GuildScheduledEventCreate; - case 'GUILD_SCHEDULED_EVENT_UPDATE': - return AuditLogEvent.GuildScheduledEventUpdate; - case 'GUILD_SCHEDULED_EVENT_DELETE': - return AuditLogEvent.GuildScheduledEventDelete; - case 'THREAD_CREATE': - return AuditLogEvent.ThreadCreate; - case 'THREAD_UPDATE': - return AuditLogEvent.ThreadUpdate; - case 'THREAD_DELETE': - return AuditLogEvent.ThreadDelete; - default: - return unknownKeyStrategy(key); - } - } -} - -// Precondition logic wrapper -function preconditioner(func) { - return key => { - if (typeof key !== 'string' && typeof key !== 'number') { - throw new Error('Enum value must be string or number'); - } - - if (typeof key === 'number') { - return key; - } - - return func(key); - }; -} - -// Injects wrapper into class static methods. -function applyPreconditioner(obj) { - for (const name in Object.getOwnPropertyNames(obj)) { - if (typeof obj[name] !== 'function') { - return; - } - - obj[name] = preconditioner(obj[name]); - } -} - -// Apply precondition logic -applyPreconditioner(EnumResolvers); - -module.exports = EnumResolvers; diff --git a/src/util/Enums.js b/src/util/Enums.js deleted file mode 100644 index e3e5cac..00000000 --- a/src/util/Enums.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -function createEnum(keys) { - const obj = {}; - for (const [index, key] of keys.entries()) { - if (key === null) continue; - obj[key] = index; - obj[index] = key; - } - return obj; -} - -module.exports = { createEnum }; diff --git a/src/util/Events.js b/src/util/Events.js deleted file mode 100644 index 11d980d..00000000 --- a/src/util/Events.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; - -module.exports = { - ClientReady: 'ready', - GuildCreate: 'guildCreate', - GuildDelete: 'guildDelete', - GuildUpdate: 'guildUpdate', - GuildUnavailable: 'guildUnavailable', - GuildMemberAdd: 'guildMemberAdd', - GuildMemberRemove: 'guildMemberRemove', - GuildMemberUpdate: 'guildMemberUpdate', - GuildMemberAvailable: 'guildMemberAvailable', - GuildMembersChunk: 'guildMembersChunk', - GuildIntegrationsUpdate: 'guildIntegrationsUpdate', - GuildRoleCreate: 'roleCreate', - GuildRoleDelete: 'roleDelete', - InviteCreate: 'inviteCreate', - InviteDelete: 'inviteDelete', - GuildRoleUpdate: 'roleUpdate', - GuildEmojiCreate: 'emojiCreate', - GuildEmojiDelete: 'emojiDelete', - GuildEmojiUpdate: 'emojiUpdate', - GuildBanAdd: 'guildBanAdd', - GuildBanRemove: 'guildBanRemove', - ChannelCreate: 'channelCreate', - ChannelDelete: 'channelDelete', - ChannelUpdate: 'channelUpdate', - ChannelPinsUpdate: 'channelPinsUpdate', - MessageCreate: 'messageCreate', - MessageDelete: 'messageDelete', - MessageUpdate: 'messageUpdate', - MessageBulkDelete: 'messageDeleteBulk', - MessageReactionAdd: 'messageReactionAdd', - MessageReactionRemove: 'messageReactionRemove', - MessageReactionRemoveAll: 'messageReactionRemoveAll', - MessageReactionRemoveEmoji: 'messageReactionRemoveEmoji', - ThreadCreate: 'threadCreate', - ThreadDelete: 'threadDelete', - ThreadUpdate: 'threadUpdate', - ThreadListSync: 'threadListSync', - ThreadMemberUpdate: 'threadMemberUpdate', - ThreadMembersUpdate: 'threadMembersUpdate', - UserUpdate: 'userUpdate', - PresenceUpdate: 'presenceUpdate', - VoiceServerUpdate: 'voiceServerUpdate', - VoiceStateUpdate: 'voiceStateUpdate', - TypingStart: 'typingStart', - WebhooksUpdate: 'webhookUpdate', - InteractionCreate: 'interactionCreate', - Error: 'error', - Warn: 'warn', - Debug: 'debug', - CacheSweep: 'cacheSweep', - ShardDisconnect: 'shardDisconnect', - ShardError: 'shardError', - ShardReconnecting: 'shardReconnecting', - ShardReady: 'shardReady', - ShardResume: 'shardResume', - Invalidated: 'invalidated', - Raw: 'raw', - StageInstanceCreate: 'stageInstanceCreate', - StageInstanceUpdate: 'stageInstanceUpdate', - StageInstanceDelete: 'stageInstanceDelete', - GuildStickerCreate: 'stickerCreate', - GuildStickerDelete: 'stickerDelete', - GuildStickerUpdate: 'stickerUpdate', - GuildScheduledEventCreate: 'guildScheduledEventCreate', - GuildScheduledEventUpdate: 'guildScheduledEventUpdate', - GuildScheduledEventDelete: 'guildScheduledEventDelete', - GuildScheduledEventUserAdd: 'guildScheduledEventUserAdd', - GuildScheduledEventUserRemove: 'guildScheduledEventUserRemove', -}; diff --git a/src/util/Intents.js b/src/util/Intents.js new file mode 100644 index 00000000..359e10b --- /dev/null +++ b/src/util/Intents.js @@ -0,0 +1,66 @@ +'use strict'; +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to calculate intents. + * @extends {BitField} + */ +class Intents extends BitField {} + +/** + * @name Intents + * @kind constructor + * @memberof Intents + * @param {IntentsResolvable} [bits=0] Bit(s) to read from + */ + +/** + * Data that can be resolved to give a permission number. This can be: + * * A string (see {@link Intents.FLAGS}) + * * An intents flag + * * An instance of Intents + * * An array of IntentsResolvable + * @typedef {string|number|Intents|IntentsResolvable[]} IntentsResolvable + */ + +/** + * Numeric WebSocket intents. All available properties: + * * `GUILDS` + * * `GUILD_MEMBERS` + * * `GUILD_BANS` + * * `GUILD_EMOJIS_AND_STICKERS` + * * `GUILD_INTEGRATIONS` + * * `GUILD_WEBHOOKS` + * * `GUILD_INVITES` + * * `GUILD_VOICE_STATES` + * * `GUILD_PRESENCES` + * * `GUILD_MESSAGES` + * * `GUILD_MESSAGE_REACTIONS` + * * `GUILD_MESSAGE_TYPING` + * * `DIRECT_MESSAGES` + * * `DIRECT_MESSAGE_REACTIONS` + * * `DIRECT_MESSAGE_TYPING` + * * `GUILD_SCHEDULED_EVENTS` + * @type {Object} + * @see {@link https://discord.com/developers/docs/topics/gateway#list-of-intents} + */ +Intents.FLAGS = { + GUILDS: 1 << 0, + GUILD_MEMBERS: 1 << 1, + GUILD_BANS: 1 << 2, + GUILD_EMOJIS_AND_STICKERS: 1 << 3, + GUILD_INTEGRATIONS: 1 << 4, + GUILD_WEBHOOKS: 1 << 5, + GUILD_INVITES: 1 << 6, + GUILD_VOICE_STATES: 1 << 7, + GUILD_PRESENCES: 1 << 8, + GUILD_MESSAGES: 1 << 9, + GUILD_MESSAGE_REACTIONS: 1 << 10, + GUILD_MESSAGE_TYPING: 1 << 11, + DIRECT_MESSAGES: 1 << 12, + DIRECT_MESSAGE_REACTIONS: 1 << 13, + DIRECT_MESSAGE_TYPING: 1 << 14, + GUILD_SCHEDULED_EVENTS: 1 << 16, +}; + +module.exports = Intents; diff --git a/src/util/IntentsBitField.js b/src/util/IntentsBitField.js deleted file mode 100644 index a173176..00000000 --- a/src/util/IntentsBitField.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; -const { GatewayIntentBits } = require('discord-api-types/v9'); -const BitField = require('./BitField'); - -/** - * Data structure that makes it easy to calculate intents. - * @extends {BitField} - */ -class IntentsBitField extends BitField {} - -/** - * @name IntentsBitField - * @kind constructor - * @memberof IntentsBitField - * @param {IntentsResolvable} [bits=0] Bit(s) to read from - */ - -/** - * Data that can be resolved to give a permission number. This can be: - * * A string (see {@link IntentsBitField.Flags}) - * * An intents flag - * * An instance of {@link IntentsBitField} - * * An array of IntentsResolvable - * @typedef {string|number|IntentsBitField|IntentsResolvable[]} IntentsResolvable - */ - -/** - * Numeric WebSocket intents - * @type {GatewayIntentBits} - */ -IntentsBitField.Flags = GatewayIntentBits; - -module.exports = IntentsBitField; diff --git a/src/util/LimitedCollection.js b/src/util/LimitedCollection.js index 1fa6798..8ebda0e 100644 --- a/src/util/LimitedCollection.js +++ b/src/util/LimitedCollection.js @@ -1,18 +1,35 @@ 'use strict'; +const { setInterval } = require('node:timers'); const { Collection } = require('@discordjs/collection'); +const { _cleanupSymbol } = require('./Constants.js'); +const Sweepers = require('./Sweepers.js'); const { TypeError } = require('../errors/DJSError.js'); +/** + * @typedef {Function} SweepFilter + * @param {LimitedCollection} collection The collection being swept + * @returns {Function|null} Return `null` to skip sweeping, otherwise a function passed to `sweep()`, + * See {@link [Collection#sweep](https://discord.js.org/#/docs/collection/main/class/Collection?scrollTo=sweep)} + * for the definition of this function. + */ + /** * Options for defining the behavior of a LimitedCollection * @typedef {Object} LimitedCollectionOptions * @property {?number} [maxSize=Infinity] The maximum size of the Collection * @property {?Function} [keepOverLimit=null] A function, which is passed the value and key of an entry, ran to decide * to keep an entry past the maximum size + * @property {?SweepFilter} [sweepFilter=null] DEPRECATED: There is no direct alternative to this, + * however most of its purpose is fulfilled by {@link Client#sweepers} + * A function ran every `sweepInterval` to determine how to sweep + * @property {?number} [sweepInterval=0] DEPRECATED: There is no direct alternative to this, + * however most of its purpose is fulfilled by {@link Client#sweepers} + * How frequently, in seconds, to sweep the collection. */ /** - * A Collection which holds a max amount of entries. + * A Collection which holds a max amount of entries and sweeps periodically. * @extends {Collection} * @param {LimitedCollectionOptions} [options={}] Options for constructing the Collection. * @param {Iterable} [iterable=null] Optional entries passed to the Map constructor. @@ -22,7 +39,7 @@ class LimitedCollection extends Collection { if (typeof options !== 'object' || options === null) { throw new TypeError('INVALID_TYPE', 'options', 'object', true); } - const { maxSize = Infinity, keepOverLimit = null } = options; + const { maxSize = Infinity, keepOverLimit = null, sweepInterval = 0, sweepFilter = null } = options; if (typeof maxSize !== 'number') { throw new TypeError('INVALID_TYPE', 'maxSize', 'number'); @@ -30,6 +47,12 @@ class LimitedCollection extends Collection { if (keepOverLimit !== null && typeof keepOverLimit !== 'function') { throw new TypeError('INVALID_TYPE', 'keepOverLimit', 'function'); } + if (typeof sweepInterval !== 'number') { + throw new TypeError('INVALID_TYPE', 'sweepInterval', 'number'); + } + if (sweepFilter !== null && typeof sweepFilter !== 'function') { + throw new TypeError('INVALID_TYPE', 'sweepFilter', 'function'); + } super(iterable); @@ -44,6 +67,28 @@ class LimitedCollection extends Collection { * @type {?Function} */ this.keepOverLimit = keepOverLimit; + + /** + * A function called every sweep interval that returns a function passed to `sweep`. + * @deprecated in favor of {@link Client#sweepers} + * @type {?SweepFilter} + */ + this.sweepFilter = sweepFilter; + + /** + * The id of the interval being used to sweep. + * @deprecated in favor of {@link Client#sweepers} + * @type {?Timeout} + */ + this.interval = + sweepInterval > 0 && sweepInterval !== Infinity && sweepFilter + ? setInterval(() => { + const sweepFn = this.sweepFilter(this); + if (sweepFn === null) return; + if (typeof sweepFn !== 'function') throw new TypeError('SWEEP_FILTER_RETURN'); + this.sweep(sweepFn); + }, sweepInterval * 1_000).unref() + : null; } set(key, value) { @@ -60,6 +105,24 @@ class LimitedCollection extends Collection { return super.set(key, value); } + /** + * Create a sweepFilter function that uses a lifetime to determine sweepability. + * @param {LifetimeFilterOptions} [options={}] The options used to generate the filter function + * @deprecated Use {@link Sweepers.filterByLifetime} instead + * @returns {SweepFilter} + */ + static filterByLifetime({ + lifetime = 14400, + getComparisonTimestamp = e => e?.createdTimestamp, + excludeFromSweep = () => false, + } = {}) { + return Sweepers.filterByLifetime({ lifetime, getComparisonTimestamp, excludeFromSweep }); + } + + [_cleanupSymbol]() { + return this.interval ? () => clearInterval(this.interval) : null; + } + static get [Symbol.species]() { return Collection; } diff --git a/src/util/MessageFlags.js b/src/util/MessageFlags.js new file mode 100644 index 00000000..b91a1fc --- /dev/null +++ b/src/util/MessageFlags.js @@ -0,0 +1,48 @@ +'use strict'; + +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with a {@link Message#flags} bitfield. + * @extends {BitField} + */ +class MessageFlags extends BitField {} + +/** + * @name MessageFlags + * @kind constructor + * @memberof MessageFlags + * @param {BitFieldResolvable} [bits=0] Bit(s) to read from + */ + +/** + * Bitfield of the packed bits + * @type {number} + * @name MessageFlags#bitfield + */ + +/** + * Numeric message flags. All available properties: + * * `CROSSPOSTED` + * * `IS_CROSSPOST` + * * `SUPPRESS_EMBEDS` + * * `SOURCE_MESSAGE_DELETED` + * * `URGENT` + * * `HAS_THREAD` + * * `EPHEMERAL` + * * `LOADING` + * @type {Object} + * @see {@link https://discord.com/developers/docs/resources/channel#message-object-message-flags} + */ +MessageFlags.FLAGS = { + CROSSPOSTED: 1 << 0, + IS_CROSSPOST: 1 << 1, + SUPPRESS_EMBEDS: 1 << 2, + SOURCE_MESSAGE_DELETED: 1 << 3, + URGENT: 1 << 4, + HAS_THREAD: 1 << 5, + EPHEMERAL: 1 << 6, + LOADING: 1 << 7, +}; + +module.exports = MessageFlags; diff --git a/src/util/MessageFlagsBitField.js b/src/util/MessageFlagsBitField.js deleted file mode 100644 index a9f1e7f..00000000 --- a/src/util/MessageFlagsBitField.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const { MessageFlags } = require('discord-api-types/v9'); -const BitField = require('./BitField'); - -/** - * Data structure that makes it easy to interact with a {@link Message#flags} bitfield. - * @extends {BitField} - */ -class MessageFlagsBitField extends BitField {} - -/** - * @name MessageFlagsBitField - * @kind constructor - * @memberof MessageFlagsBitField - * @param {BitFieldResolvable} [bits=0] Bit(s) to read from - */ - -/** - * Bitfield of the packed bits - * @type {number} - * @name MessageFlagsBitField#bitfield - */ - -/** - * Numeric message flags. - * @type {MessageFlags} - */ -MessageFlagsBitField.Flags = MessageFlags; - -module.exports = MessageFlagsBitField; diff --git a/src/util/Options.js b/src/util/Options.js index f92c14d..4a3c9cc 100644 --- a/src/util/Options.js +++ b/src/util/Options.js @@ -1,8 +1,24 @@ 'use strict'; const process = require('node:process'); -const Transformers = require('./Transformers'); const JSONBig = require('json-bigint'); +/** + * Rate limit data + * @typedef {Object} RateLimitData + * @property {number} timeout Time until this rate limit ends, in ms + * @property {number} limit The maximum amount of requests of this endpoint + * @property {string} method The HTTP method of this request + * @property {string} path The path of the request relative to the HTTP endpoint + * @property {string} route The route of the request relative to the HTTP endpoint + * @property {boolean} global Whether this is a global rate limit + */ + +/** + * Whether this rate limit should throw an Error + * @typedef {Function} RateLimitQueueFilter + * @param {RateLimitData} rateLimitData The data of this rate limit + * @returns {boolean|Promise} + */ /** * @typedef {Function} CacheFactory @@ -23,20 +39,44 @@ const JSONBig = require('json-bigint'); * You can use your own function, or the {@link Options} class to customize the Collection used for the cache. * Overriding the cache used in `GuildManager`, `ChannelManager`, `GuildChannelManager`, `RoleManager`, * and `PermissionOverwriteManager` is unsupported and **will** break functionality + * @property {number} [messageCacheLifetime=0] DEPRECATED: Pass `lifetime` to `sweepers.messages` instead. + * How long a message should stay in the cache until it is considered sweepable (in seconds, 0 for forever) + * @property {number} [messageSweepInterval=0] DEPRECATED: Pass `interval` to `sweepers.messages` instead. + * How frequently to remove messages from the cache that are older than the message cache lifetime + * (in seconds, 0 for never) * @property {MessageMentionOptions} [allowedMentions] Default value for {@link MessageOptions#allowedMentions} - * @property {Partials[]} [partials] Structures allowed to be partial. This means events can be emitted even when + * @property {number} [invalidRequestWarningInterval=0] The number of invalid REST requests (those that return + * 401, 403, or 429) in a 10 minute window between emitted warnings (0 for no warnings). That is, if set to 500, + * warnings will be emitted at invalid request number 500, 1000, 1500, and so on. + * @property {PartialType[]} [partials] Structures allowed to be partial. This means events can be emitted even when * they're missing all the data for a particular structure. See the "Partial Structures" topic on the * [guide](https://discordjs.guide/popular-topics/partials.html) for some * important usage information, as partials require you to put checks in place when handling data. + * @property {number} [restWsBridgeTimeout=5000] Maximum time permitted between REST responses and their + * corresponding WebSocket events + * @property {number} [restTimeOffset=500] Extra time in milliseconds to wait before continuing to make REST + * requests (higher values will reduce rate-limiting errors on bad connections) + * @property {number} [restRequestTimeout=15000] Time to wait before cancelling a REST request, in milliseconds + * @property {number} [restSweepInterval=60] How frequently to delete inactive request buckets, in seconds + * (or 0 for never) + * @property {number} [restGlobalRateLimit=0] How many requests to allow sending per second (0 for unlimited, 50 for + * the standard global limit used by Discord) + * @property {string[]|RateLimitQueueFilter} [rejectOnRateLimit] Decides how rate limits and pre-emptive throttles + * should be handled. If this option is an array containing the prefix of the request route (e.g. /channels to match any + * route starting with /channels, such as /channels/222197033908436994/messages) or a function returning true, a + * {@link RateLimitError} will be thrown. Otherwise the request will be queued for later + * @property {number} [retryLimit=1] How many times to retry on 5XX errors + * (Infinity for an indefinite amount of retries) * @property {boolean} [failIfNotExists=true] Default value for {@link ReplyMessageOptions#failIfNotExists} + * @property {string[]} [userAgentSuffix] An array of additional bot info to be appended to the end of the required + * [User Agent](https://discord.com/developers/docs/reference#user-agent) header * @property {PresenceData} [presence={}] Presence data to use upon login * @property {IntentsResolvable} intents Intents to enable for this connection * @property {number} [waitGuildTimeout=15_000] Time in milliseconds that Clients with the GUILDS intent should wait for - * missing guilds to be received before starting the bot. If not specified, the default is 15 seconds. + * missing guilds to be recieved before starting the bot. If not specified, the default is 15 seconds. * @property {SweeperOptions} [sweepers={}] Options for cache sweeping * @property {WebsocketOptions} [ws] Options for the WebSocket - * @property {RESTOptions} [rest] Options for the REST manager - * @property {Function} [jsonTransformer] A function used to transform outgoing json data + * @property {HTTPOptions} [http] HTTP options */ /** @@ -62,6 +102,26 @@ const JSONBig = require('json-bigint'); * sent in the initial guild member list, must be between 50 and 250 */ +/** + * HTTPS Agent options. + * @typedef {Object} AgentOptions + * @see {@link https://nodejs.org/api/https.html#https_class_https_agent} + * @see {@link https://nodejs.org/api/http.html#http_new_agent_options} + */ + +/** + * HTTP options + * @typedef {Object} HTTPOptions + * @property {number} [version=9] API version to use + * @property {AgentOptions} [agent={}] HTTPS Agent options + * @property {string} [api='https://discord.com/api'] Base URL of the API + * @property {string} [cdn='https://cdn.discordapp.com'] Base URL of the CDN + * @property {string} [invite='https://discord.gg'] Base URL of invites + * @property {string} [template='https://discord.new'] Base URL of templates + * @property {Object} [headers] Additional headers to send for all API requests + * @property {string} [scheduledEvent='https://discord.com/events'] Base URL of guild scheduled events + */ + /** * Contains various utilities for client options. */ @@ -72,25 +132,28 @@ class Options extends null { */ static createDefault() { return { - waitGuildTimeout: 15_000, - shardCount: 1, - makeCache: this.cacheWithLimits(this.defaultMakeCacheSettings), - messageCacheLifetime: 0, - messageSweepInterval: 0, - invalidRequestWarningInterval: 0, - intents: 32767, - partials: [], - restWsBridgeTimeout: 5_000, - restRequestTimeout: 15_000, - restGlobalRateLimit: 0, - retryLimit: 1, - restTimeOffset: 500, - restSweepInterval: 60, - failIfNotExists: true, - userAgentSuffix: [], - presence: {}, - sweepers: {}, - ws: { + jsonTransformer: (object) => JSONBig.stringify(object), + checkUpdate: true, + readyStatus: false, + waitGuildTimeout: 15_000, + shardCount: 1, + makeCache: this.cacheWithLimits(this.defaultMakeCacheSettings), + messageCacheLifetime: 0, + messageSweepInterval: 0, + invalidRequestWarningInterval: 0, + intents: 65535, + partials: [], + restWsBridgeTimeout: 5_000, + restRequestTimeout: 15_000, + restGlobalRateLimit: 0, + retryLimit: 1, + restTimeOffset: 500, + restSweepInterval: 60, + failIfNotExists: true, + userAgentSuffix: [], + presence: {}, + sweepers: {}, + ws: { large_threshold: 50, compress: false, properties: { @@ -121,6 +184,7 @@ class Options extends null { 'X-Debug-Options': 'bugReporterEnabled', 'X-Discord-Locale': 'en-US', Origin: 'https://discord.com', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36', }, agent: {}, version: 10, @@ -130,10 +194,7 @@ class Options extends null { template: 'https://discord.new', scheduledEvent: 'https://discord.com/events', }, - jsonTransformer: (object) => JSONBig.stringify(object), - checkUpdate: true, - readyStatus: false, - }; + }; } /** @@ -144,14 +205,32 @@ class Options extends null { * If LimitedCollectionOptions are provided for a manager, it uses those settings to form a LimitedCollection. * @returns {CacheFactory} * @example - * // Store up to 200 messages per channel and 200 members per guild, always keeping the client member. + * // Store up to 200 messages per channel and discard archived threads if they were archived more than 4 hours ago. + * // Note archived threads will remain in the guild and client caches with these settings * Options.cacheWithLimits({ * MessageManager: 200, - * GuildMemberManager: { - * maxSize: 200, - * keepOverLimit: (member) => member.id === client.user.id, + * ThreadManager: { + * sweepInterval: 3600, + * sweepFilter: LimitedCollection.filterByLifetime({ + * getComparisonTimestamp: e => e.archiveTimestamp, + * excludeFromSweep: e => !e.archived, + * }), * }, * }); + * @example + * // Sweep messages every 5 minutes, removing messages that have not been edited or created in the last 30 minutes + * Options.cacheWithLimits({ + * // Keep default thread sweeping behavior + * ...Options.defaultMakeCacheSettings, + * // Override MessageManager + * MessageManager: { + * sweepInterval: 300, + * sweepFilter: LimitedCollection.filterByLifetime({ + * lifetime: 1800, + * getComparisonTimestamp: e => e.editedTimestamp ?? e.createdTimestamp, + * }) + * } + * }); */ static cacheWithLimits(settings = {}) { const { Collection } = require('@discordjs/collection'); @@ -169,9 +248,15 @@ class Options extends null { } return new LimitedCollection({ maxSize: setting }); } - /* eslint-disable-next-line eqeqeq */ + /* eslint-disable eqeqeq */ + const noSweeping = + setting.sweepFilter == null || + setting.sweepInterval == null || + setting.sweepInterval <= 0 || + setting.sweepInterval === Infinity; const noLimit = setting.maxSize == null || setting.maxSize === Infinity; - if (noLimit) { + /* eslint-enable eqeqeq */ + if (noSweeping && noLimit) { return new Collection(); } return new LimitedCollection(setting); @@ -201,6 +286,20 @@ class Options extends null { static get defaultMakeCacheSettings() { return { MessageManager: 200, + /* + ChannelManager: { + sweepInterval: 3600, + sweepFilter: require('./Util').archivedThreadSweepFilter(), + }, + GuildChannelManager: { + sweepInterval: 3600, + sweepFilter: require('./Util').archivedThreadSweepFilter(), + }, + ThreadManager: { + sweepInterval: 3600, + sweepFilter: require('./Util').archivedThreadSweepFilter(), + }, + */ }; } } @@ -221,8 +320,3 @@ Options.defaultSweeperSettings = { }; module.exports = Options; - -/** - * @external RESTOptions - * @see {@link https://discord.js.org/#/docs/rest/main/typedef/RESTOptions} - */ diff --git a/src/util/Partials.js b/src/util/Partials.js deleted file mode 100644 index 7bbe517..00000000 --- a/src/util/Partials.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -const { createEnum } = require('./Enums'); - -module.exports = createEnum(['User', 'Channel', 'GuildMember', 'Message', 'Reaction', 'GuildScheduledEvent']); diff --git a/src/util/Permissions.js b/src/util/Permissions.js new file mode 100644 index 00000000..c7a9134 --- /dev/null +++ b/src/util/Permissions.js @@ -0,0 +1,182 @@ +'use strict'; + +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with a permission bitfield. All {@link GuildMember}s have a set of + * permissions in their guild, and each channel in the guild may also have {@link PermissionOverwrites} for the member + * that override their default permissions. + * @extends {BitField} + */ +class Permissions extends BitField { + /** + * Bitfield of the packed bits + * @type {bigint} + * @name Permissions#bitfield + */ + + /** + * Data that can be resolved to give a permission number. This can be: + * * A string (see {@link Permissions.FLAGS}) + * * A permission number + * * An instance of Permissions + * * An Array of PermissionResolvable + * @typedef {string|bigint|Permissions|PermissionResolvable[]} PermissionResolvable + */ + + /** + * Gets all given bits that are missing from the bitfield. + * @param {BitFieldResolvable} bits Bit(s) to check for + * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override + * @returns {string[]} + */ + missing(bits, checkAdmin = true) { + return checkAdmin && this.has(this.constructor.FLAGS.ADMINISTRATOR) ? [] : super.missing(bits); + } + + /** + * Checks whether the bitfield has a permission, or any of multiple permissions. + * @param {PermissionResolvable} permission Permission(s) to check for + * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override + * @returns {boolean} + */ + any(permission, checkAdmin = true) { + return (checkAdmin && super.has(this.constructor.FLAGS.ADMINISTRATOR)) || super.any(permission); + } + + /** + * Checks whether the bitfield has a permission, or multiple permissions. + * @param {PermissionResolvable} permission Permission(s) to check for + * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override + * @returns {boolean} + */ + has(permission, checkAdmin = true) { + return (checkAdmin && super.has(this.constructor.FLAGS.ADMINISTRATOR)) || super.has(permission); + } + + /** + * Gets an {@link Array} of bitfield names based on the permissions available. + * @returns {string[]} + */ + toArray() { + return super.toArray(false); + } +} + +/** + * Numeric permission flags. All available properties: + * * `CREATE_INSTANT_INVITE` (create invitations to the guild) + * * `KICK_MEMBERS` + * * `BAN_MEMBERS` + * * `ADMINISTRATOR` (implicitly has *all* permissions, and bypasses all channel overwrites) + * * `MANAGE_CHANNELS` (edit and reorder channels) + * * `MANAGE_GUILD` (edit the guild information, region, etc.) + * * `ADD_REACTIONS` (add new reactions to messages) + * * `VIEW_AUDIT_LOG` + * * `PRIORITY_SPEAKER` + * * `STREAM` + * * `VIEW_CHANNEL` + * * `SEND_MESSAGES` + * * `SEND_TTS_MESSAGES` + * * `MANAGE_MESSAGES` (delete messages and reactions) + * * `EMBED_LINKS` (links posted will have a preview embedded) + * * `ATTACH_FILES` + * * `READ_MESSAGE_HISTORY` (view messages that were posted prior to opening Discord) + * * `MENTION_EVERYONE` + * * `USE_EXTERNAL_EMOJIS` (use emojis from different guilds) + * * `VIEW_GUILD_INSIGHTS` + * * `CONNECT` (connect to a voice channel) + * * `SPEAK` (speak in a voice channel) + * * `MUTE_MEMBERS` (mute members across all voice channels) + * * `DEAFEN_MEMBERS` (deafen members across all voice channels) + * * `MOVE_MEMBERS` (move members between voice channels) + * * `USE_VAD` (use voice activity detection) + * * `CHANGE_NICKNAME` + * * `MANAGE_NICKNAMES` (change other members' nicknames) + * * `MANAGE_ROLES` + * * `MANAGE_WEBHOOKS` + * * `MANAGE_EMOJIS_AND_STICKERS` + * * `USE_APPLICATION_COMMANDS` + * * `REQUEST_TO_SPEAK` + * * `MANAGE_EVENTS` + * * `MANAGE_THREADS` + * * `USE_PUBLIC_THREADS` (deprecated) + * * `CREATE_PUBLIC_THREADS` + * * `USE_PRIVATE_THREADS` (deprecated) + * * `CREATE_PRIVATE_THREADS` + * * `USE_EXTERNAL_STICKERS` (use stickers from different guilds) + * * `SEND_MESSAGES_IN_THREADS` + * * `START_EMBEDDED_ACTIVITIES` + * * `MODERATE_MEMBERS` + * @type {Object} + * @see {@link https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags} + */ +Permissions.FLAGS = { + CREATE_INSTANT_INVITE: 1n << 0n, + KICK_MEMBERS: 1n << 1n, + BAN_MEMBERS: 1n << 2n, + ADMINISTRATOR: 1n << 3n, + MANAGE_CHANNELS: 1n << 4n, + MANAGE_GUILD: 1n << 5n, + ADD_REACTIONS: 1n << 6n, + VIEW_AUDIT_LOG: 1n << 7n, + PRIORITY_SPEAKER: 1n << 8n, + STREAM: 1n << 9n, + VIEW_CHANNEL: 1n << 10n, + SEND_MESSAGES: 1n << 11n, + SEND_TTS_MESSAGES: 1n << 12n, + MANAGE_MESSAGES: 1n << 13n, + EMBED_LINKS: 1n << 14n, + ATTACH_FILES: 1n << 15n, + READ_MESSAGE_HISTORY: 1n << 16n, + MENTION_EVERYONE: 1n << 17n, + USE_EXTERNAL_EMOJIS: 1n << 18n, + VIEW_GUILD_INSIGHTS: 1n << 19n, + CONNECT: 1n << 20n, + SPEAK: 1n << 21n, + MUTE_MEMBERS: 1n << 22n, + DEAFEN_MEMBERS: 1n << 23n, + MOVE_MEMBERS: 1n << 24n, + USE_VAD: 1n << 25n, + CHANGE_NICKNAME: 1n << 26n, + MANAGE_NICKNAMES: 1n << 27n, + MANAGE_ROLES: 1n << 28n, + MANAGE_WEBHOOKS: 1n << 29n, + MANAGE_EMOJIS_AND_STICKERS: 1n << 30n, + USE_APPLICATION_COMMANDS: 1n << 31n, + REQUEST_TO_SPEAK: 1n << 32n, + MANAGE_EVENTS: 1n << 33n, + MANAGE_THREADS: 1n << 34n, + // TODO: Remove deprecated USE_*_THREADS flags in v14 + USE_PUBLIC_THREADS: 1n << 35n, + CREATE_PUBLIC_THREADS: 1n << 35n, + USE_PRIVATE_THREADS: 1n << 36n, + CREATE_PRIVATE_THREADS: 1n << 36n, + USE_EXTERNAL_STICKERS: 1n << 37n, + SEND_MESSAGES_IN_THREADS: 1n << 38n, + START_EMBEDDED_ACTIVITIES: 1n << 39n, + MODERATE_MEMBERS: 1n << 40n, +}; + +/** + * Bitfield representing every permission combined + * @type {bigint} + */ +Permissions.ALL = Object.values(Permissions.FLAGS).reduce((all, p) => all | p, 0n); + +/** + * Bitfield representing the default permissions for users + * @type {bigint} + */ +Permissions.DEFAULT = BigInt(104324673); + +/** + * Bitfield representing the permissions required for moderators of stage channels + * @type {bigint} + */ +Permissions.STAGE_MODERATOR = + Permissions.FLAGS.MANAGE_CHANNELS | Permissions.FLAGS.MUTE_MEMBERS | Permissions.FLAGS.MOVE_MEMBERS; + +Permissions.defaultBit = BigInt(0); + +module.exports = Permissions; diff --git a/src/util/PermissionsBitField.js b/src/util/PermissionsBitField.js deleted file mode 100644 index ff101c3..00000000 --- a/src/util/PermissionsBitField.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; - -const { PermissionFlagsBits } = require('discord-api-types/v9'); -const BitField = require('./BitField'); - -/** - * Data structure that makes it easy to interact with a permission bitfield. All {@link GuildMember}s have a set of - * permissions in their guild, and each channel in the guild may also have {@link PermissionOverwrites} for the member - * that override their default permissions. - * @extends {BitField} - */ -class PermissionsBitField extends BitField { - /** - * Bitfield of the packed bits - * @type {bigint} - * @name Permissions#bitfield - */ - - /** - * Data that can be resolved to give a permission number. This can be: - * * A string (see {@link PermissionsBitField.Flags}) - * * A permission number - * * An instance of {@link PermissionsBitField} - * * An Array of PermissionResolvable - * @typedef {string|bigint|PermissionsBitField|PermissionResolvable[]} PermissionResolvable - */ - - /** - * Gets all given bits that are missing from the bitfield. - * @param {BitFieldResolvable} bits Bit(s) to check for - * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override - * @returns {string[]} - */ - missing(bits, checkAdmin = true) { - return checkAdmin && this.has(PermissionFlagsBits.Administrator) ? [] : super.missing(bits); - } - - /** - * Checks whether the bitfield has a permission, or any of multiple permissions. - * @param {PermissionResolvable} permission Permission(s) to check for - * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override - * @returns {boolean} - */ - any(permission, checkAdmin = true) { - return (checkAdmin && super.has(PermissionFlagsBits.Administrator)) || super.any(permission); - } - - /** - * Checks whether the bitfield has a permission, or multiple permissions. - * @param {PermissionResolvable} permission Permission(s) to check for - * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override - * @returns {boolean} - */ - has(permission, checkAdmin = true) { - return (checkAdmin && super.has(PermissionFlagsBits.Administrator)) || super.has(permission); - } - - /** - * Gets an {@link Array} of bitfield names based on the permissions available. - * @returns {string[]} - */ - toArray() { - return super.toArray(false); - } -} - -/** - * Numeric permission flags. - * @type {PermissionFlagsBits} - * @see {@link https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags} - */ -PermissionsBitField.Flags = PermissionFlagsBits; - -/** - * Bitfield representing every permission combined - * @type {bigint} - */ -PermissionsBitField.All = Object.values(PermissionFlagsBits).reduce((all, p) => all | p, 0n); - -/** - * Bitfield representing the default permissions for users - * @type {bigint} - */ -PermissionsBitField.Default = BigInt(104324673); - -/** - * Bitfield representing the permissions required for moderators of stage channels - * @type {bigint} - */ -PermissionsBitField.StageModerator = - PermissionFlagsBits.ManageChannels | PermissionFlagsBits.MuteMembers | PermissionFlagsBits.MoveMembers; - -PermissionsBitField.defaultBit = BigInt(0); - -module.exports = PermissionsBitField; diff --git a/src/util/ShardEvents.js b/src/util/ShardEvents.js deleted file mode 100644 index 102c722..00000000 --- a/src/util/ShardEvents.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -module.exports = { - Close: 'close', - Destroyed: 'destroyed', - InvalidSession: 'invalidSession', - Ready: 'ready', - Resumed: 'resumed', - AllReady: 'allReady', -}; diff --git a/src/util/SnowflakeUtil.js b/src/util/SnowflakeUtil.js new file mode 100644 index 00000000..3e89efd --- /dev/null +++ b/src/util/SnowflakeUtil.js @@ -0,0 +1,92 @@ +'use strict'; + +// Discord epoch (2015-01-01T00:00:00.000Z) +const EPOCH = 1_420_070_400_000; +let INCREMENT = BigInt(0); + +/** + * A container for useful snowflake-related methods. + */ +class SnowflakeUtil extends null { + /** + * A {@link https://developer.twitter.com/en/docs/twitter-ids Twitter snowflake}, + * except the epoch is 2015-01-01T00:00:00.000Z. + * + * If we have a snowflake '266241948824764416' we can represent it as binary: + * ``` + * 64 22 17 12 0 + * 000000111011000111100001101001000101000000 00001 00000 000000000000 + * number of ms since Discord epoch worker pid increment + * ``` + * @typedef {string} Snowflake + */ + + /** + * Generates a Discord snowflake. + * This hardcodes the worker's id as 1 and the process's id as 0. + * @param {number|Date} [timestamp=Date.now()] Timestamp or date of the snowflake to generate + * @returns {Snowflake} The generated snowflake + */ + static generate(timestamp = Date.now()) { + if (timestamp instanceof Date) timestamp = timestamp.getTime(); + if (typeof timestamp !== 'number' || isNaN(timestamp)) { + throw new TypeError( + `"timestamp" argument must be a number (received ${isNaN(timestamp) ? 'NaN' : typeof timestamp})`, + ); + } + if (INCREMENT >= 4095n) INCREMENT = BigInt(0); + + // Assign WorkerId as 1 and ProcessId as 0: + return ((BigInt(timestamp - EPOCH) << 22n) | (1n << 17n) | INCREMENT++).toString(); + } + + /** + * A deconstructed snowflake. + * @typedef {Object} DeconstructedSnowflake + * @property {number} timestamp Timestamp the snowflake was created + * @property {Date} date Date the snowflake was created + * @property {number} workerId The worker's id in the snowflake + * @property {number} processId The process's id in the snowflake + * @property {number} increment Increment in the snowflake + * @property {string} binary Binary representation of the snowflake + */ + + /** + * Deconstructs a Discord snowflake. + * @param {Snowflake} snowflake Snowflake to deconstruct + * @returns {DeconstructedSnowflake} + */ + static deconstruct(snowflake) { + const bigIntSnowflake = BigInt(snowflake); + return { + timestamp: Number(bigIntSnowflake >> 22n) + EPOCH, + get date() { + return new Date(this.timestamp); + }, + workerId: Number((bigIntSnowflake >> 17n) & 0b11111n), + processId: Number((bigIntSnowflake >> 12n) & 0b11111n), + increment: Number(bigIntSnowflake & 0b111111111111n), + binary: bigIntSnowflake.toString(2).padStart(64, '0'), + }; + } + + /** + * Retrieves the timestamp field's value from a Discord snowflake. + * @param {Snowflake} snowflake Snowflake to get the timestamp value from + * @returns {number} + */ + static timestampFrom(snowflake) { + return Number(BigInt(snowflake) >> 22n) + EPOCH; + } + + /** + * Discord's epoch value (2015-01-01T00:00:00.000Z). + * @type {number} + * @readonly + */ + static get EPOCH() { + return EPOCH; + } +} + +module.exports = SnowflakeUtil; diff --git a/src/util/Status.js b/src/util/Status.js deleted file mode 100644 index d614c72..00000000 --- a/src/util/Status.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const { createEnum } = require('./Enums'); - -module.exports = createEnum([ - 'Ready', - 'Connecting', - 'Reconnecting', - 'Idle', - 'Nearly', - 'Disconnected', - 'WaitingForGuilds', - 'Identifying', - 'Resuming', -]); diff --git a/src/util/Sweepers.js b/src/util/Sweepers.js index bcd7df0..ab91bf9 100644 --- a/src/util/Sweepers.js +++ b/src/util/Sweepers.js @@ -1,8 +1,7 @@ 'use strict'; -const { setInterval, clearInterval } = require('node:timers'); -const { ThreadChannelTypes, SweeperKeys } = require('./Constants'); -const Events = require('./Events'); +const { setInterval } = require('node:timers'); +const { Events, ThreadChannelTypes, SweeperKeys } = require('./Constants'); const { TypeError } = require('../errors/DJSError.js'); /** @@ -72,7 +71,7 @@ class Sweepers { const globalCommands = this.client.application?.commands.cache.sweep(filter) ?? 0; this.client.emit( - Events.CacheSweep, + Events.CACHE_SWEEP, `Swept ${globalCommands} global application commands and ${guildCommands} guild commands in ${guilds} guilds.`, ); return guildCommands + globalCommands; @@ -137,12 +136,12 @@ class Sweepers { let messages = 0; for (const channel of this.client.channels.cache.values()) { - if (!channel.isTextBased()) continue; + if (!channel.isText()) continue; channels++; messages += channel.messages.cache.sweep(filter); } - this.client.emit(Events.CacheSweep, `Swept ${messages} messages in ${channels} text-based channels.`); + this.client.emit(Events.CACHE_SWEEP, `Swept ${messages} messages in ${channels} text-based channels.`); return messages; } @@ -169,7 +168,7 @@ class Sweepers { let reactions = 0; for (const channel of this.client.channels.cache.values()) { - if (!channel.isTextBased()) continue; + if (!channel.isText()) continue; channels++; for (const message of channel.messages.cache.values()) { @@ -178,7 +177,7 @@ class Sweepers { } } this.client.emit( - Events.CacheSweep, + Events.CACHE_SWEEP, `Swept ${reactions} reactions on ${messages} messages in ${channels} text-based channels.`, ); return reactions; @@ -193,15 +192,6 @@ class Sweepers { return this._sweepGuildDirectProp('stageInstances', filter, { outputName: 'stage instances' }).items; } - /** - * Sweeps all guild stickers and removes the ones which are indicated by the filter. - * @param {Function} filter The function used to determine which stickers will be removed from the caches. - * @returns {number} Amount of stickers that were removed from the caches - */ - sweepStickers(filter) { - return this._sweepGuildDirectProp('stickers', filter).items; - } - /** * Sweeps all thread members and removes the ones which are indicated by the filter. * It is highly recommended to keep the client thread member cached @@ -220,7 +210,7 @@ class Sweepers { threads++; members += channel.members.cache.sweep(filter); } - this.client.emit(Events.CacheSweep, `Swept ${members} thread members in ${threads} threads.`); + this.client.emit(Events.CACHE_SWEEP, `Swept ${members} thread members in ${threads} threads.`); return members; } @@ -251,7 +241,7 @@ class Sweepers { this.client.channels._remove(key); } } - this.client.emit(Events.CacheSweep, `Swept ${threads} threads.`); + this.client.emit(Events.CACHE_SWEEP, `Swept ${threads} threads.`); return threads; } @@ -267,7 +257,7 @@ class Sweepers { const users = this.client.users.cache.sweep(filter); - this.client.emit(Events.CacheSweep, `Swept ${users} users.`); + this.client.emit(Events.CACHE_SWEEP, `Swept ${users} users.`); return users; } @@ -405,7 +395,7 @@ class Sweepers { } if (emit) { - this.client.emit(Events.CacheSweep, `Swept ${items} ${outputName ?? key} in ${guilds} guilds.`); + this.client.emit(Events.CACHE_SWEEP, `Swept ${items} ${outputName ?? key} in ${guilds} guilds.`); } return { guilds, items }; diff --git a/src/util/SystemChannelFlags.js b/src/util/SystemChannelFlags.js new file mode 100644 index 00000000..bf5a56a --- /dev/null +++ b/src/util/SystemChannelFlags.js @@ -0,0 +1,51 @@ +'use strict'; + +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with a {@link Guild#systemChannelFlags} bitfield. + * Note that all event message types are enabled by default, + * and by setting their corresponding flags you are disabling them + * @extends {BitField} + */ +class SystemChannelFlags extends BitField {} + +/** + * @name SystemChannelFlags + * @kind constructor + * @memberof SystemChannelFlags + * @param {SystemChannelFlagsResolvable} [bits=0] Bit(s) to read from + */ + +/** + * Bitfield of the packed bits + * @type {number} + * @name SystemChannelFlags#bitfield + */ + +/** + * Data that can be resolved to give a system channel flag bitfield. This can be: + * * A string (see {@link SystemChannelFlags.FLAGS}) + * * A system channel flag + * * An instance of SystemChannelFlags + * * An Array of SystemChannelFlagsResolvable + * @typedef {string|number|SystemChannelFlags|SystemChannelFlagsResolvable[]} SystemChannelFlagsResolvable + */ + +/** + * Numeric system channel flags. All available properties: + * * `SUPPRESS_JOIN_NOTIFICATIONS` (Suppress member join notifications) + * * `SUPPRESS_PREMIUM_SUBSCRIPTIONS` (Suppress server boost notifications) + * * `SUPPRESS_GUILD_REMINDER_NOTIFICATIONS` (Suppress server setup tips) + * * `SUPPRESS_JOIN_NOTIFICATION_REPLIES` (Hide member join sticker reply buttons) + * @type {Object} + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-system-channel-flags} + */ +SystemChannelFlags.FLAGS = { + SUPPRESS_JOIN_NOTIFICATIONS: 1 << 0, + SUPPRESS_PREMIUM_SUBSCRIPTIONS: 1 << 1, + SUPPRESS_GUILD_REMINDER_NOTIFICATIONS: 1 << 2, + SUPPRESS_JOIN_NOTIFICATION_REPLIES: 1 << 3, +}; + +module.exports = SystemChannelFlags; diff --git a/src/util/SystemChannelFlagsBitField.js b/src/util/SystemChannelFlagsBitField.js deleted file mode 100644 index 90f2cca..00000000 --- a/src/util/SystemChannelFlagsBitField.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -const { GuildSystemChannelFlags } = require('discord-api-types/v9'); -const BitField = require('./BitField'); - -/** - * Data structure that makes it easy to interact with a {@link Guild#systemChannelFlags} bitfield. - * Note that all event message types are enabled by default, - * and by setting their corresponding flags you are disabling them - * @extends {BitField} - */ -class SystemChannelFlagsBitField extends BitField {} - -/** - * @name SystemChannelFlagsBitField - * @kind constructor - * @memberof SystemChannelFlagsBitField - * @param {SystemChannelFlagsResolvable} [bits=0] Bit(s) to read from - */ - -/** - * Bitfield of the packed bits - * @type {number} - * @name SystemChannelFlagsBitField#bitfield - */ - -/** - * Data that can be resolved to give a system channel flag bitfield. This can be: - * * A string (see {@link SystemChannelFlagsBitField.Flags}) - * * A system channel flag - * * An instance of SystemChannelFlagsBitField - * * An Array of SystemChannelFlagsResolvable - * @typedef {string|number|SystemChannelFlagsBitField|SystemChannelFlagsResolvable[]} SystemChannelFlagsResolvable - */ - -/** - * Numeric system channel flags. - * @type {GuildSystemChannelFlags} - */ -SystemChannelFlagsBitField.Flags = GuildSystemChannelFlags; - -module.exports = SystemChannelFlagsBitField; diff --git a/src/util/ThreadMemberFlagsBitField.js b/src/util/ThreadMemberFlags.js similarity index 64% rename from src/util/ThreadMemberFlagsBitField.js rename to src/util/ThreadMemberFlags.js index 4f94b52..309ea59 100644 --- a/src/util/ThreadMemberFlagsBitField.js +++ b/src/util/ThreadMemberFlags.js @@ -6,25 +6,25 @@ const BitField = require('./BitField'); * Data structure that makes it easy to interact with a {@link ThreadMember#flags} bitfield. * @extends {BitField} */ -class ThreadMemberFlagsBitField extends BitField {} +class ThreadMemberFlags extends BitField {} /** - * @name ThreadMemberFlagsBitField + * @name ThreadMemberFlags * @kind constructor - * @memberof ThreadMemberFlagsBitField + * @memberof ThreadMemberFlags * @param {BitFieldResolvable} [bits=0] Bit(s) to read from */ /** * Bitfield of the packed bits * @type {number} - * @name ThreadMemberFlagsBitField#bitfield + * @name ThreadMemberFlags#bitfield */ /** * Numeric thread member flags. There are currently no bitflags relevant to bots for this. * @type {Object} */ -ThreadMemberFlagsBitField.Flags = {}; +ThreadMemberFlags.FLAGS = {}; -module.exports = ThreadMemberFlagsBitField; +module.exports = ThreadMemberFlags; diff --git a/src/util/Transformers.js b/src/util/Transformers.js deleted file mode 100644 index f7eed82..00000000 --- a/src/util/Transformers.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -const snakeCase = require('lodash.snakecase'); - -class Transformers extends null { - /** - * Transforms camel-cased keys into snake cased keys - * @param {*} obj The object to transform - * @returns {*} - */ - static toSnakeCase(obj) { - if (typeof obj !== 'object' || !obj) return obj; - if (Array.isArray(obj)) return obj.map(Transformers.toSnakeCase); - return Object.fromEntries( - Object.entries(obj).map(([key, value]) => [snakeCase(key), Transformers.toSnakeCase(value)]), - ); - } -} - -module.exports = Transformers; diff --git a/src/util/UserFlags.js b/src/util/UserFlags.js new file mode 100644 index 00000000..5849750 --- /dev/null +++ b/src/util/UserFlags.js @@ -0,0 +1,59 @@ +'use strict'; +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with a {@link User#flags} bitfield. + * @extends {BitField} + */ +class UserFlags extends BitField {} + +/** + * @name UserFlags + * @kind constructor + * @memberof UserFlags + * @param {BitFieldResolvable} [bits=0] Bit(s) to read from + */ + +/** + * Bitfield of the packed bits + * @type {number} + * @name UserFlags#bitfield + */ + +/** + * Numeric user flags. All available properties: + * * `DISCORD_EMPLOYEE` + * * `PARTNERED_SERVER_OWNER` + * * `HYPESQUAD_EVENTS` + * * `BUGHUNTER_LEVEL_1` + * * `HOUSE_BRAVERY` + * * `HOUSE_BRILLIANCE` + * * `HOUSE_BALANCE` + * * `EARLY_SUPPORTER` + * * `TEAM_USER` + * * `BUGHUNTER_LEVEL_2` + * * `VERIFIED_BOT` + * * `EARLY_VERIFIED_BOT_DEVELOPER` + * * `DISCORD_CERTIFIED_MODERATOR` + * * `BOT_HTTP_INTERACTIONS` + * @type {Object} + * @see {@link https://discord.com/developers/docs/resources/user#user-object-user-flags} + */ +UserFlags.FLAGS = { + DISCORD_EMPLOYEE: 1 << 0, + PARTNERED_SERVER_OWNER: 1 << 1, + HYPESQUAD_EVENTS: 1 << 2, + BUGHUNTER_LEVEL_1: 1 << 3, + HOUSE_BRAVERY: 1 << 6, + HOUSE_BRILLIANCE: 1 << 7, + HOUSE_BALANCE: 1 << 8, + EARLY_SUPPORTER: 1 << 9, + TEAM_USER: 1 << 10, + BUGHUNTER_LEVEL_2: 1 << 14, + VERIFIED_BOT: 1 << 16, + EARLY_VERIFIED_BOT_DEVELOPER: 1 << 17, + DISCORD_CERTIFIED_MODERATOR: 1 << 18, + BOT_HTTP_INTERACTIONS: 1 << 19, +}; + +module.exports = UserFlags; diff --git a/src/util/UserFlagsBitField.js b/src/util/UserFlagsBitField.js deleted file mode 100644 index dc2a943..00000000 --- a/src/util/UserFlagsBitField.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const { UserFlags } = require('discord-api-types/v9'); -const BitField = require('./BitField'); - -/** - * Data structure that makes it easy to interact with a {@link User#flags} bitfield. - * @extends {BitField} - */ -class UserFlagsBitField extends BitField {} - -/** - * @name UserFlagsBitField - * @kind constructor - * @memberof UserFlagsBitField - * @param {BitFieldResolvable} [bits=0] Bit(s) to read from - */ - -/** - * Bitfield of the packed bits - * @type {number} - * @name UserFlagsBitField#bitfield - */ - -/** - * Numeric user flags. - * @type {UserFlags} - */ -UserFlagsBitField.Flags = UserFlags; - -module.exports = UserFlagsBitField; diff --git a/src/util/Util.js b/src/util/Util.js index 2ab39b4..7901f77 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -1,13 +1,17 @@ 'use strict'; const { parse } = require('node:path'); +const process = require('node:process'); const { Collection } = require('@discordjs/collection'); -const { ChannelType, RouteBases, Routes } = require('discord-api-types/v9'); -const { fetch } = require('undici'); -const Colors = require('./Colors'); +const fetch = require('node-fetch'); +const { Colors, Endpoints } = require('./Constants'); +const Options = require('./Options'); const { Error: DiscordError, RangeError, TypeError } = require('../errors'); +const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k); const isObject = d => typeof d === 'object' && d !== null; +let deprecationEmittedForRemoveMentions = false; + /** * Contains various general-purpose utility methods. */ @@ -268,7 +272,8 @@ class Util extends null { */ static async fetchRecommendedShards(token, { guildsPerShard = 1_000, multipleOf = 1 } = {}) { if (!token) throw new DiscordError('TOKEN_MISSING'); - const response = await fetch(RouteBases.api + Routes.gatewayBot(), { + const defaults = Options.createDefault(); + const response = await fetch(`${defaults.http.api}/v${defaults.http.version}${Endpoints.botGateway}`, { method: 'GET', headers: { Authorization: `Bot ${token.replace(/^Bot\s*/i, '')}` }, }); @@ -330,7 +335,7 @@ class Util extends null { static mergeDefault(def, given) { if (!given) return def; for (const key in def) { - if (!Object.hasOwn(given, key) || given[key] === undefined) { + if (!has(given, key) || given[key] === undefined) { given[key] = def[key]; } else if (given[key] === Object(given[key])) { given[key] = Util.mergeDefault(def[key], given[key]); @@ -419,37 +424,37 @@ class Util extends null { * [255, 0, 255] // purple * ``` * or one of the following strings: - * - `Default` - * - `White` - * - `Aqua` - * - `Green` - * - `Blue` - * - `Yellow` - * - `Purple` - * - `LuminousVividPink` - * - `Fuchsia` - * - `Gold` - * - `Orange` - * - `Red` - * - `Grey` - * - `Navy` - * - `DarkAqua` - * - `DarkGreen` - * - `DarkBlue` - * - `DarkPurple` - * - `DarkVividPink` - * - `DarkGold` - * - `DarkOrange` - * - `DarkRed` - * - `DarkGrey` - * - `DarkerGrey` - * - `LightGrey` - * - `DarkNavy` - * - `Blurple` - * - `Greyple` - * - `DarkButNotBlack` - * - `NotQuiteBlack` - * - `Random` + * - `DEFAULT` + * - `WHITE` + * - `AQUA` + * - `GREEN` + * - `BLUE` + * - `YELLOW` + * - `PURPLE` + * - `LUMINOUS_VIVID_PINK` + * - `FUCHSIA` + * - `GOLD` + * - `ORANGE` + * - `RED` + * - `GREY` + * - `NAVY` + * - `DARK_AQUA` + * - `DARK_GREEN` + * - `DARK_BLUE` + * - `DARK_PURPLE` + * - `DARK_VIVID_PINK` + * - `DARK_GOLD` + * - `DARK_ORANGE` + * - `DARK_RED` + * - `DARK_GREY` + * - `DARKER_GREY` + * - `LIGHT_GREY` + * - `DARK_NAVY` + * - `BLURPLE` + * - `GREYPLE` + * - `DARK_BUT_NOT_BLACK` + * - `NOT_QUITE_BLACK` + * - `RANDOM` * @typedef {string|number|number[]} ColorResolvable */ @@ -460,8 +465,8 @@ class Util extends null { */ static resolveColor(color) { if (typeof color === 'string') { - if (color === 'Random') return Math.floor(Math.random() * (0xffffff + 1)); - if (color === 'Default') return 0; + if (color === 'RANDOM') return Math.floor(Math.random() * (0xffffff + 1)); + if (color === 'DEFAULT') return 0; color = Colors[color] ?? parseInt(color.replace('#', ''), 16); } else if (Array.isArray(color)) { color = (color[0] << 16) + (color[1] << 8) + color[2]; @@ -493,17 +498,16 @@ class Util extends null { * @param {number} position New position for the object * @param {boolean} relative Whether `position` is relative to its current position * @param {Collection} sorted A collection of the objects sorted properly - * @param {Client} client The client to use to patch the data - * @param {string} route Route to call PATCH on + * @param {APIRouter} route Route to call PATCH on * @param {string} [reason] Reason for the change * @returns {Promise} Updated item list, with `id` and `position` properties * @private */ - static async setPosition(item, position, relative, sorted, client, route, reason) { + static async setPosition(item, position, relative, sorted, route, reason) { let updatedItems = [...sorted.values()]; Util.moveElementInArray(updatedItems, item, position, relative); updatedItems = updatedItems.map((r, i) => ({ id: r.id, position: i })); - await client.rest.patch(route, { body: updatedItems, reason }); + await route.patch({ data: updatedItems, reason }); return updatedItems; } @@ -518,8 +522,34 @@ class Util extends null { const res = parse(path); return ext && res.ext.startsWith(ext) ? res.name : res.base.split('?')[0]; } + + /** + * Breaks user, role and everyone/here mentions by adding a zero width space after every @ character + * @param {string} str The string to sanitize + * @returns {string} + * @deprecated Use {@link BaseMessageOptions#allowedMentions} instead. + */ + static removeMentions(str) { + if (!deprecationEmittedForRemoveMentions) { + process.emitWarning( + 'The Util.removeMentions method is deprecated. Use MessageOptions#allowedMentions instead.', + 'DeprecationWarning', + ); + + deprecationEmittedForRemoveMentions = true; + } + + return Util._removeMentions(str); + } + + static _removeMentions(str) { + return str.replaceAll('@', '@\u200b'); + } + /** * The content to have all mentions replaced by the equivalent text. + * When {@link Util.removeMentions} is removed, this method will no longer sanitize mentions. + * Use {@link BaseMessageOptions#allowedMentions} instead to prevent mentions when sending a message. * @param {string} str The string to be converted * @param {TextBasedChannels} channel The channel the string was sent in * @returns {string} @@ -528,17 +558,17 @@ class Util extends null { str = str .replace(/<@!?[0-9]+>/g, input => { const id = input.replace(/<|!|>|@/g, ''); - if (channel.type === ChannelType.DM) { + if (channel.type === 'DM') { const user = channel.client.users.cache.get(id); - return user ? `@${user.username}` : input; + return user ? Util._removeMentions(`@${user.username}`) : input; } const member = channel.guild.members.cache.get(id); if (member) { - return `@${member.displayName}`; + return Util._removeMentions(`@${member.displayName}`); } else { const user = channel.client.users.cache.get(id); - return user ? `@${user.username}` : input; + return user ? Util._removeMentions(`@${user.username}`) : input; } }) .replace(/<#[0-9]+>/g, input => { @@ -546,7 +576,7 @@ class Util extends null { return mentionedChannel ? `#${mentionedChannel.name}` : input; }) .replace(/<@&[0-9]+>/g, input => { - if (channel.type === ChannelType.DM) return input; + if (channel.type === 'DM') return input; const role = channel.guild.roles.cache.get(input.replace(/<|@|>|&/g, '')); return role ? `@${role.name}` : input; }); @@ -561,6 +591,18 @@ class Util extends null { static cleanCodeBlockContent(text) { return text.replaceAll('```', '`\u200b``'); } + + /** + * Creates a sweep filter that sweeps archived threads + * @param {number} [lifetime=14400] How long a thread has to be archived to be valid for sweeping + * @deprecated When not using with `makeCache` use `Sweepers.archivedThreadSweepFilter` instead + * @returns {SweepFilter} + */ + static archivedThreadSweepFilter(lifetime = 14400) { + const filter = require('./Sweepers').archivedThreadSweepFilter(lifetime); + filter.isDefault = true; + return filter; + } } module.exports = Util; diff --git a/typings/index.d.ts b/typings/index.d.ts index 899ce76..8990f3e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -144,6 +144,7 @@ import { } from './rawDataTypes'; //#region Classes + export class Activity { private constructor(presence: Presence, data?: RawActivityData); public applicationId: Snowflake | null; @@ -537,13 +538,15 @@ export class Client extends BaseClient { private actions: unknown; private presence: ClientPresence; private _eval(script: string): unknown; + private _validateOptions(options: ClientOptions): void; public application: If; - public bot: Boolean; - public channels: ChannelManager; // Added public setting: ClientUserSettingManager; + public friends: FriendsManager; + public blocked: BlockedManager; // End + public channels: ChannelManager; public readonly emojis: BaseGuildEmojiManager; public guilds: GuildManager; public options: ClientOptions; @@ -552,12 +555,9 @@ export class Client extends BaseClient { public sweepers: Sweepers; public shard: ShardClientUtil | null; public token: If; - public session_id: String; public uptime: If; public user: If; public users: UserManager; - public friends: FriendsManager; - public blocked: BlockedManager; public voice: ClientVoiceManager; public ws: WebSocketManager; public destroy(): void; @@ -569,7 +569,8 @@ export class Client extends BaseClient { public fetchPremiumStickerPacks(): Promise>; public fetchWebhook(id: Snowflake, token?: string): Promise; public fetchGuildWidget(guild: GuildResolvable): Promise; - public login(token?: string, bot?: Boolean): Promise; + public generateInvite(options?: InviteGenerationOptions): string; + public login(token?: string): Promise; public isReady(): this is Client; /** @deprecated Use {@link Sweepers#sweepMessages} instead */ public sweepMessages(lifetime?: number): number; @@ -1127,6 +1128,10 @@ export class GuildMember extends PartialTextBasedChannel(Base) { public deleteDM(): Promise; public displayAvatarURL(options?: ImageURLOptions): string; public edit(data: GuildMemberEditData, reason?: string): Promise; + public isCommunicationDisabled(): this is GuildMember & { + communicationDisabledUntilTimestamp: number; + readonly communicationDisabledUntil: Date; + }; public kick(reason?: string): Promise; public permissionsIn(channel: GuildChannelResolvable): Readonly; public setNickname(nickname: string | null, reason?: string): Promise; @@ -1330,6 +1335,8 @@ export class Interaction extends Base { public user: User; public version: number; public memberPermissions: CacheTypeReducer>; + public locale: string; + public guildLocale: CacheTypeReducer; public inGuild(): this is Interaction<'present'>; public inCachedGuild(): this is Interaction<'cached'>; public inRawGuild(): this is Interaction<'raw'>; @@ -1694,11 +1701,13 @@ export class MessageEmbed { public addField(name: string, value: string, inline?: boolean): this; public addFields(...fields: EmbedFieldData[] | EmbedFieldData[][]): this; public setFields(...fields: EmbedFieldData[] | EmbedFieldData[][]): this; - public setAuthor(options: string | EmbedAuthorData | null): this; - /** @deprecated Supply a lone object of interface {@link EmbedAuthorData} instead of more parameters. */ + public setAuthor(options: EmbedAuthorData | null): this; + /** @deprecated Supply a lone object of interface {@link EmbedAuthorData} instead. */ public setAuthor(name: string, iconURL?: string, url?: string): this; public setColor(color: ColorResolvable): this; public setDescription(description: string): this; + public setFooter(options: EmbedFooterData | null): this; + /** @deprecated Supply a lone object of interface {@link EmbedFooterData} instead. */ public setFooter(text: string, iconURL?: string): this; public setImage(url: string): this; public setThumbnail(url: string): this; @@ -1856,7 +1865,7 @@ export class Permissions extends BitField { public has(permission: PermissionResolvable, checkAdmin?: boolean): boolean; public missing(bits: BitFieldResolvable, checkAdmin?: boolean): PermissionString[]; public serialize(checkAdmin?: boolean): Record; - public toArray(checkAdmin?: boolean): PermissionString[]; + public toArray(): PermissionString[]; public static ALL: bigint; public static DEFAULT: bigint; @@ -2389,12 +2398,6 @@ export class User extends PartialTextBasedChannel(Base) { public system: boolean; public readonly tag: string; public username: string; - public readonly friended: Boolean; - public readonly blocked: Boolean; - public readonly connectedAccounts: Readonly; - public readonly premiumSince: number | null; - public readonly premiumGuildSince: number | null; - public readonly mutualGuilds: Collection; public avatarURL(options?: ImageURLOptions): string | null; public bannerURL(options?: ImageURLOptions): string | null; public createDM(force?: boolean): Promise; @@ -2433,6 +2436,7 @@ export class Util extends null { public static cleanContent(str: string, channel: TextBasedChannel): string; /** @deprecated Use {@link MessageOptions.allowedMentions} to control mentions in a message instead. */ public static removeMentions(str: string): string; + private static _removeMentions(str: string): string; public static cloneObject(obj: unknown): unknown; public static discordSort( collection: Collection, @@ -4049,6 +4053,7 @@ export interface ClientOptions { userAgentSuffix?: string[]; presence?: PresenceData; intents: BitFieldResolvable; + waitGuildTimeout?: number; sweepers?: SweeperOptions; ws?: WebSocketOptions; http?: HTTPOptions; @@ -4356,6 +4361,11 @@ export interface EmbedFieldData { inline?: boolean; } +export interface EmbedFooterData { + text: string; + iconURL?: string; +} + export type EmojiIdentifierResolvable = string | EmojiResolvable; export type EmojiResolvable = Snowflake | GuildEmoji | ReactionEmoji; @@ -5407,7 +5417,6 @@ export type PresenceStatus = PresenceStatusData | 'offline'; export type PrivacyLevel = keyof typeof PrivacyLevels; -export type LocaleStrings = "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" export interface RateLimitData { timeout: number; limit: number; diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index ccfbc3a..51c9900 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -949,12 +949,17 @@ client.on('interactionCreate', async interaction => { expectAssignable(interaction.member); expectNotType>(interaction); expectAssignable(interaction); + expectType(interaction.guildLocale); } else if (interaction.inRawGuild()) { expectAssignable(interaction.member); expectNotAssignable>(interaction); + expectType(interaction.guildLocale); + } else if (interaction.inGuild()) { + expectType(interaction.guildLocale); } else { expectType(interaction.member); expectNotAssignable>(interaction); + expectType(interaction.guildId); } if (interaction.isContextMenu()) { diff --git a/typings/rawDataTypes.d.ts b/typings/rawDataTypes.d.ts index d7a9ec7..885566d 100644 --- a/typings/rawDataTypes.d.ts +++ b/typings/rawDataTypes.d.ts @@ -75,7 +75,7 @@ import { RESTPostAPIInteractionFollowupJSONBody, RESTPostAPIWebhookWithTokenJSONBody, Snowflake, - APIGuildScheduledEvent + APIGuildScheduledEvent, } from 'discord-api-types/v9'; import { GuildChannel, Guild, PermissionOverwrites } from '.';