This commit is contained in:
Lukian 2023-06-20 15:28:07 +02:00
parent 68f4b60012
commit 41ae7ff4bd
1010 changed files with 38622 additions and 17071 deletions

2818
node_modules/discord.js/CHANGELOG.md generated vendored

File diff suppressed because it is too large Load diff

35
node_modules/discord.js/README.md generated vendored
View file

@ -13,6 +13,7 @@
</p>
<p>
<a href="https://vercel.com/?utm_source=discordjs&utm_campaign=oss"><img src="https://raw.githubusercontent.com/discordjs/discord.js/main/.github/powered-by-vercel.svg" alt="Vercel" /></a>
<a href="https://www.cloudflare.com"><img src="https://raw.githubusercontent.com/discordjs/discord.js/main/.github/powered-by-workers.png" alt="Cloudflare Workers" height="44" /></a>
</p>
</div>
@ -30,7 +31,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
**Node.js 16.9.0 or newer is required.**
```sh-session
```sh
npm install discord.js
yarn add discord.js
pnpm add discord.js
@ -39,7 +40,6 @@ pnpm add discord.js
### Optional packages
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`)
- [erlpack](https://github.com/discord/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discord/erlpack`)
- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection (`npm install bufferutil`)
- [utf-8-validate](https://www.npmjs.com/package/utf-8-validate) in combination with `bufferutil` for much faster WebSocket processing (`npm install utf-8-validate`)
- [@discordjs/voice](https://www.npmjs.com/package/@discordjs/voice) for interacting with the Discord Voice API (`npm install @discordjs/voice`)
@ -48,7 +48,7 @@ pnpm add discord.js
Install discord.js:
```sh-session
```sh
npm install discord.js
yarn add discord.js
pnpm add discord.js
@ -57,7 +57,7 @@ pnpm add discord.js
Register a slash command against the Discord API:
```js
const { REST, Routes } = require('discord.js');
import { REST, Routes } from 'discord.js';
const commands = [
{
@ -68,23 +68,21 @@ const commands = [
const rest = new REST({ version: '10' }).setToken(TOKEN);
(async () => {
try {
console.log('Started refreshing application (/) commands.');
try {
console.log('Started refreshing application (/) commands.');
await rest.put(Routes.applicationCommands(CLIENT_ID), { body: commands });
await rest.put(Routes.applicationCommands(CLIENT_ID), { body: commands });
console.log('Successfully reloaded application (/) commands.');
} catch (error) {
console.error(error);
}
})();
console.log('Successfully reloaded application (/) commands.');
} catch (error) {
console.error(error);
}
```
Afterwards we can create a quite simple example bot:
```js
const { Client, GatewayIntentBits } = require('discord.js');
import { Client, GatewayIntentBits } from 'discord.js';
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.on('ready', () => {
@ -107,7 +105,7 @@ client.login(TOKEN);
- [Website][website] ([source][website-source])
- [Documentation][documentation]
- [Guide][guide] ([source][guide-source])
See also the [Update Guide][guide-update], including updated and removed items in the library.
Also see the v13 to v14 [Update Guide][guide-update], which includes updated and removed items from the library.
- [discord.js Discord server][discord]
- [Discord API Discord server][discord-api]
- [GitHub][source]
@ -126,12 +124,11 @@ See [the contribution guide][contributing] if you'd like to submit a PR.
## Help
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
nudge in the right direction, please don't hesitate to join our official [discord.js Server][discord].
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle nudge in the right direction, please don't hesitate to join our official [discord.js Server][discord].
[website]: https://discord.js.org/
[website]: https://discord.js.org
[website-source]: https://github.com/discordjs/discord.js/tree/main/apps/website
[documentation]: https://discord.js.org/#/docs
[documentation]: https://discord.js.org/docs/packages/discord.js/stable
[guide]: https://discordjs.guide/
[guide-source]: https://github.com/discordjs/guide
[guide-update]: https://discordjs.guide/additional-info/changes-in-v14.html

44
node_modules/discord.js/package.json generated vendored
View file

@ -1,6 +1,6 @@
{
"name": "discord.js",
"version": "14.7.1",
"version": "14.11.0",
"description": "A powerful library for interacting with the Discord API",
"scripts": {
"test": "yarn docs:test && yarn test:typescript",
@ -42,38 +42,42 @@
],
"repository": {
"type": "git",
"url": "https://github.com/discordjs/discord.js.git"
"url": "https://github.com/discordjs/discord.js.git",
"directory": "packages/discord.js"
},
"bugs": {
"url": "https://github.com/discordjs/discord.js/issues"
},
"homepage": "https://discord.js.org",
"dependencies": {
"@discordjs/builders": "^1.4.0",
"@discordjs/collection": "^1.3.0",
"@discordjs/rest": "^1.4.0",
"@discordjs/util": "^0.1.0",
"@sapphire/snowflake": "^3.2.2",
"@types/ws": "^8.5.3",
"discord-api-types": "^0.37.20",
"@discordjs/builders": "^1.6.3",
"@discordjs/collection": "^1.5.1",
"@discordjs/formatters": "^0.3.1",
"@discordjs/rest": "^1.7.1",
"@discordjs/util": "^0.3.1",
"@discordjs/ws": "^0.8.3",
"@sapphire/snowflake": "^3.4.2",
"@types/ws": "^8.5.4",
"discord-api-types": "^0.37.41",
"fast-deep-equal": "^3.1.3",
"lodash.snakecase": "^4.1.1",
"tslib": "^2.4.1",
"undici": "^5.13.0",
"ws": "^8.11.0"
"tslib": "^2.5.0",
"undici": "^5.22.0",
"ws": "^8.13.0"
},
"devDependencies": {
"@discordjs/docgen": "^0.12.1",
"@favware/cliff-jumper": "^1.9.0",
"@types/node": "16.18.3",
"@favware/cliff-jumper": "^2.0.0",
"@types/node": "16.18.25",
"dtslint": "^4.2.1",
"eslint": "^8.28.0",
"eslint-formatter-pretty": "^4.1.0",
"jest": "^29.3.1",
"prettier": "^2.8.0",
"tsd": "^0.24.1",
"eslint": "^8.39.0",
"eslint-formatter-pretty": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^2.8.8",
"tsd": "^0.28.1",
"tslint": "^6.1.3",
"typescript": "^4.9.3"
"turbo": "^1.9.4-canary.9",
"typescript": "^5.0.4"
},
"engines": {
"node": ">=16.9.0"

View file

@ -1,39 +0,0 @@
'use strict';
let erlpack;
const { Buffer } = require('node:buffer');
try {
erlpack = require('erlpack');
if (!erlpack.pack) erlpack = null;
} catch {} // eslint-disable-line no-empty
exports.WebSocket = require('ws');
const ab = new TextDecoder();
exports.encoding = erlpack ? 'etf' : 'json';
exports.pack = erlpack ? erlpack.pack : JSON.stringify;
exports.unpack = (data, type) => {
if (exports.encoding === 'json' || type === 'json') {
if (typeof data !== 'string') {
data = ab.decode(data);
}
return JSON.parse(data);
}
if (!Buffer.isBuffer(data)) data = Buffer.from(new Uint8Array(data));
return erlpack.unpack(data);
};
exports.create = (gateway, query = {}, ...args) => {
const [g, q] = gateway.split('?');
query.encoding = exports.encoding;
query = new URLSearchParams(query);
if (q) new URLSearchParams(q).forEach((v, k) => query.set(k, v));
const ws = new exports.WebSocket(`${g}?${query}`, ...args);
return ws;
};
for (const state of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']) exports[state] = exports.WebSocket[state];

View file

@ -22,7 +22,15 @@ class BaseClient extends EventEmitter {
* The options the client was instantiated with
* @type {ClientOptions}
*/
this.options = mergeDefault(Options.createDefault(), options);
this.options = mergeDefault(Options.createDefault(), {
...options,
rest: {
...options.rest,
userAgentAppendix: options.rest?.userAgentAppendix
? `${Options.userAgentAppendix} ${options.rest.userAgentAppendix}`
: undefined,
},
});
/**
* The REST manager of the client
@ -71,5 +79,5 @@ module.exports = BaseClient;
/**
* @external REST
* @see {@link https://discord.js.org/#/docs/rest/main/class/REST}
* @see {@link https://discord.js.org/docs/packages/rest/stable/REST:Class}
*/

View file

@ -307,7 +307,7 @@ class Client extends BaseClient {
* .catch(console.error);
*/
async fetchWebhook(id, token) {
const data = await this.rest.get(Routes.webhook(id, token), { auth: typeof token === 'undefined' });
const data = await this.rest.get(Routes.webhook(id, token), { auth: token === undefined });
return new Webhook(this, { token, ...data });
}
@ -411,7 +411,7 @@ class Client extends BaseClient {
if (!this.application) throw new DiscordjsError(ErrorCodes.ClientNotReady, 'generate an invite link');
const { scopes } = options;
if (typeof scopes === 'undefined') {
if (scopes === undefined) {
throw new DiscordjsTypeError(ErrorCodes.InvalidMissingScopes);
}
if (!Array.isArray(scopes)) {
@ -420,6 +420,9 @@ class Client extends BaseClient {
if (!scopes.some(scope => [OAuth2Scopes.Bot, OAuth2Scopes.ApplicationsCommands].includes(scope))) {
throw new DiscordjsTypeError(ErrorCodes.InvalidMissingScopes);
}
if (!scopes.includes(OAuth2Scopes.Bot) && options.permissions) {
throw new DiscordjsTypeError(ErrorCodes.InvalidScopesWithPermissions);
}
const validScopes = Object.values(OAuth2Scopes);
const invalidScope = scopes.find(scope => !validScopes.includes(scope));
if (invalidScope) {
@ -485,7 +488,7 @@ class Client extends BaseClient {
* @private
*/
_validateOptions(options = this.options) {
if (typeof options.intents === 'undefined') {
if (options.intents === undefined) {
throw new DiscordjsTypeError(ErrorCodes.ClientMissingIntents);
} else {
options.intents = new IntentsBitField(options.intents).freeze();
@ -512,11 +515,41 @@ class Client extends BaseClient {
if (typeof options.failIfNotExists !== 'boolean') {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'failIfNotExists', 'a boolean');
}
if (
(typeof options.allowedMentions !== 'object' && options.allowedMentions !== undefined) ||
options.allowedMentions === null
) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'allowedMentions', 'an object');
}
if (typeof options.presence !== 'object' || options.presence === null) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'presence', 'an object');
}
if (typeof options.ws !== 'object' || options.ws === null) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'ws', 'an object');
}
if (typeof options.rest !== 'object' || options.rest === null) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'rest', 'an object');
}
if (typeof options.jsonTransformer !== 'function') {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'jsonTransformer', 'a function');
}
}
}
module.exports = Client;
/**
* @class SnowflakeUtil
* @classdesc This class is an alias for {@link https://www.npmjs.com/package/@sapphire/snowflake @sapphire/snowflake}'s
* `DiscordSnowflake` class.
*
* Check their documentation
* {@link https://www.sapphirejs.dev/docs/Documentation/api-utilities/classes/sapphire_snowflake.Snowflake here}
* ({@link https://www.sapphirejs.dev/docs/Guide/utilities/snowflake guide})
* to see what you can do.
* @hideconstructor
*/
/**
* A {@link https://developer.twitter.com/en/docs/twitter-ids Twitter snowflake},
* except the epoch is 2015-01-01T00:00:00.000Z.
@ -544,15 +577,15 @@ module.exports = Client;
/**
* @external Collection
* @see {@link https://discord.js.org/#/docs/collection/main/class/Collection}
* @see {@link https://discord.js.org/docs/packages/collection/stable/Collection:Class}
*/
/**
* @external ImageURLOptions
* @see {@link https://discord.js.org/#/docs/rest/main/typedef/ImageURLOptions}
* @see {@link https://discord.js.org/docs/packages/rest/stable/ImageURLOptions:Interface}
*/
/**
* @external BaseImageURLOptions
* @see {@link https://discord.js.org/#/docs/rest/main/typedef/BaseImageURLOptions}
* @see {@link https://discord.js.org/docs/packages/rest/stable/BaseImageURLOptions:Interface}
*/

View file

@ -68,7 +68,7 @@ class WebhookClient extends BaseClient {
/* eslint-disable no-empty-function, valid-jsdoc */
/**
* Sends a message with this webhook.
* @param {string|MessagePayload|WebhookCreateMessageOptions} options The content for the reply
* @param {string|MessagePayload|WebhookMessageCreateOptions} options The content for the reply
* @returns {Promise<APIMessage>}
*/
send() {}
@ -84,7 +84,7 @@ class WebhookClient extends BaseClient {
/**
* Edits a message that was sent by this webhook.
* @param {MessageResolvable} message The message to edit
* @param {string|MessagePayload|WebhookEditMessageOptions} options The options to provide
* @param {string|MessagePayload|WebhookMessageEditOptions} options The options to provide
* @returns {Promise<APIMessage>} Returns the message edited by this webhook
*/
editMessage() {}

View file

@ -34,7 +34,7 @@ class GenericAction {
getChannel(data) {
const id = data.channel_id ?? data.id;
return (
data.channel ??
data[this.client.actions.injectedChannel] ??
this.getPayload(
{
id,
@ -51,7 +51,7 @@ class GenericAction {
getMessage(data, channel, cache) {
const id = data.message_id ?? data.id;
return (
data.message ??
data[this.client.actions.injectedMessage] ??
this.getPayload(
{
id,
@ -86,7 +86,7 @@ class GenericAction {
getUser(data) {
const id = data.user_id;
return data.user ?? this.getPayload({ id }, this.client.users, id, Partials.User);
return data[this.client.actions.injectedUser] ?? this.getPayload({ id }, this.client.users, id, Partials.User);
}
getUserFromMember(data) {

View file

@ -1,6 +1,13 @@
'use strict';
class ActionsManager {
// These symbols represent fully built data that we inject at times when calling actions manually.
// Action#getUser, for example, will return the injected data (which is assumed to be a built structure)
// instead of trying to make it from provided data
injectedUser = Symbol('djs.actions.injectedUser');
injectedChannel = Symbol('djs.actions.injectedChannel');
injectedMessage = Symbol('djs.actions.injectedMessage');
constructor(client) {
this.client = client;
@ -12,6 +19,7 @@ class ActionsManager {
this.register(require('./ChannelCreate'));
this.register(require('./ChannelDelete'));
this.register(require('./ChannelUpdate'));
this.register(require('./GuildAuditLogEntryCreate'));
this.register(require('./GuildBanAdd'));
this.register(require('./GuildBanRemove'));
this.register(require('./GuildChannelsPositionUpdate'));

View file

@ -0,0 +1,29 @@
'use strict';
const Action = require('./Action');
const GuildAuditLogsEntry = require('../../structures/GuildAuditLogsEntry');
const Events = require('../../util/Events');
class GuildAuditLogEntryCreateAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
let auditLogEntry;
if (guild) {
auditLogEntry = new GuildAuditLogsEntry(guild, data);
/**
* Emitted whenever a guild audit log entry is created.
* @event Client#guildAuditLogEntryCreate
* @param {GuildAuditLogsEntry} auditLogEntry The entry that was created
* @param {Guild} guild The guild where the entry was created
*/
client.emit(Events.GuildAuditLogEntryCreate, auditLogEntry, guild);
}
return { auditLogEntry };
}
}
module.exports = GuildAuditLogEntryCreateAction;

View file

@ -20,7 +20,7 @@ class InteractionCreateAction extends Action {
const client = this.client;
// Resolve and cache partial channels for Interaction#channel getter
const channel = this.getChannel(data);
const channel = data.channel && this.getChannel(data.channel);
// Do not emit this for interactions that cache messages that are non-text-based.
let InteractionClass;

View file

@ -10,7 +10,8 @@ class WebhooksUpdate extends Action {
/**
* Emitted whenever a channel has its webhooks changed.
* @event Client#webhookUpdate
* @param {TextChannel|NewsChannel|VoiceChannel|ForumChannel} channel The channel that had a webhook update
* @param {TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel} channel
* The channel that had a webhook update
*/
if (channel) client.emit(Events.WebhooksUpdate, channel);
}

View file

@ -1,10 +1,16 @@
'use strict';
const EventEmitter = require('node:events');
const process = require('node:process');
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/v10');
const {
WebSocketManager: WSWebSocketManager,
WebSocketShardEvents: WSWebSocketShardEvents,
CompressionMethod,
CloseCodes,
} = require('@discordjs/ws');
const { GatewayCloseCodes, GatewayDispatchEvents } = require('discord-api-types/v10');
const WebSocketShard = require('./WebSocketShard');
const PacketHandlers = require('./handlers');
const { DiscordjsError, ErrorCodes } = require('../../errors');
@ -12,6 +18,12 @@ const Events = require('../../util/Events');
const Status = require('../../util/Status');
const WebSocketShardEvents = require('../../util/WebSocketShardEvents');
let zlib;
try {
zlib = require('zlib-sync');
} catch {} // eslint-disable-line no-empty
const BeforeReadyWhitelist = [
GatewayDispatchEvents.Ready,
GatewayDispatchEvents.Resumed,
@ -22,15 +34,17 @@ const BeforeReadyWhitelist = [
GatewayDispatchEvents.GuildMemberRemove,
];
const unrecoverableErrorCodeMap = {
[GatewayCloseCodes.AuthenticationFailed]: ErrorCodes.TokenInvalid,
[GatewayCloseCodes.InvalidShard]: ErrorCodes.ShardingInvalid,
[GatewayCloseCodes.ShardingRequired]: ErrorCodes.ShardingRequired,
[GatewayCloseCodes.InvalidIntents]: ErrorCodes.InvalidIntents,
[GatewayCloseCodes.DisallowedIntents]: ErrorCodes.DisallowedIntents,
};
const WaitingForGuildEvents = [GatewayDispatchEvents.GuildCreate, GatewayDispatchEvents.GuildDelete];
const UNRESUMABLE_CLOSE_CODES = [1000, GatewayCloseCodes.AlreadyAuthenticated, GatewayCloseCodes.InvalidSeq];
const UNRESUMABLE_CLOSE_CODES = [
CloseCodes.Normal,
GatewayCloseCodes.AlreadyAuthenticated,
GatewayCloseCodes.InvalidSeq,
];
const reasonIsDeprecated = 'the reason property is deprecated, use the code property to determine the reason';
let deprecationEmittedForInvalidSessionEvent = false;
let deprecationEmittedForDestroyedEvent = false;
/**
* The WebSocket manager for this client.
@ -56,27 +70,12 @@ class WebSocketManager extends EventEmitter {
*/
this.gateway = null;
/**
* The amount of shards this manager handles
* @private
* @type {number}
*/
this.totalShards = this.client.options.shards.length;
/**
* A collection of all shards this manager handles
* @type {Collection<number, WebSocketShard>}
*/
this.shards = new Collection();
/**
* An array of shards to be connected or that need to reconnect
* @type {Set<WebSocketShard>}
* @private
* @name WebSocketManager#shardQueue
*/
Object.defineProperty(this, 'shardQueue', { value: new Set(), writable: true });
/**
* An array of queued events before this WebSocketManager became ready
* @type {Object[]}
@ -99,11 +98,11 @@ class WebSocketManager extends EventEmitter {
this.destroyed = false;
/**
* If this manager is currently reconnecting one or multiple shards
* @type {boolean}
* The internal WebSocketManager from `@discordjs/ws`.
* @type {WSWebSocketManager}
* @private
*/
this.reconnecting = false;
this._ws = null;
}
/**
@ -119,11 +118,14 @@ class WebSocketManager extends EventEmitter {
/**
* Emits a debug message.
* @param {string} message The debug message
* @param {?WebSocketShard} [shard] The shard that emitted this message, if any
* @param {?number} [shardId] The id of the shard that emitted this message, if any
* @private
*/
debug(message, shard) {
this.client.emit(Events.Debug, `[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`);
debug(message, shardId) {
this.client.emit(
Events.Debug,
`[WS => ${typeof shardId === 'number' ? `Shard ${shardId}` : 'Manager'}] ${message}`,
);
}
/**
@ -132,11 +134,37 @@ class WebSocketManager extends EventEmitter {
*/
async connect() {
const invalidToken = new DiscordjsError(ErrorCodes.TokenInvalid);
const { shards, shardCount, intents, ws } = this.client.options;
if (this._ws && this._ws.options.token !== this.client.token) {
await this._ws.destroy({ code: CloseCodes.Normal, reason: 'Login with differing token requested' });
this._ws = null;
}
if (!this._ws) {
const wsOptions = {
intents: intents.bitfield,
rest: this.client.rest,
token: this.client.token,
largeThreshold: ws.large_threshold,
version: ws.version,
shardIds: shards === 'auto' ? null : shards,
shardCount: shards === 'auto' ? null : shardCount,
initialPresence: ws.presence,
retrieveSessionInfo: shardId => this.shards.get(shardId).sessionInfo,
updateSessionInfo: (shardId, sessionInfo) => {
this.shards.get(shardId).sessionInfo = sessionInfo;
},
compression: zlib ? CompressionMethod.ZlibStream : null,
};
if (ws.buildStrategy) wsOptions.buildStrategy = ws.buildStrategy;
this._ws = new WSWebSocketManager(wsOptions);
this.attachEvents();
}
const {
url: gatewayURL,
shards: recommendedShards,
session_start_limit: sessionStartLimit,
} = await this.client.rest.get(Routes.gatewayBot()).catch(error => {
} = await this._ws.fetchGatewayInformation().catch(error => {
throw error.status === 401 ? invalidToken : error;
});
@ -152,156 +180,131 @@ class WebSocketManager extends EventEmitter {
this.gateway = `${gatewayURL}/`;
let { shards } = this.client.options;
this.client.options.shardCount = await this._ws.getShardCount();
this.client.options.shards = await this._ws.getShardIds();
this.totalShards = this.client.options.shards.length;
for (const id of this.client.options.shards) {
if (!this.shards.has(id)) {
const shard = new WebSocketShard(this, id);
this.shards.set(id, shard);
if (shards === 'auto') {
this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`);
this.totalShards = this.client.options.shardCount = recommendedShards;
shards = this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i);
}
this.totalShards = shards.length;
this.debug(`Spawning shards: ${shards.join(', ')}`);
this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id)));
return this.createShards();
}
/**
* Handles the creation of a shard.
* @returns {Promise<boolean>}
* @private
*/
async createShards() {
// If we don't have any shards to handle, return
if (!this.shardQueue.size) return false;
const [shard] = this.shardQueue;
this.shardQueue.delete(shard);
if (!shard.eventsAttached) {
shard.on(WebSocketShardEvents.AllReady, unavailableGuilds => {
/**
* Emitted when a shard turns ready.
* @event Client#shardReady
* @param {number} id The shard id that turned ready
* @param {?Set<Snowflake>} unavailableGuilds Set of unavailable guild ids, if any
*/
this.client.emit(Events.ShardReady, shard.id, unavailableGuilds);
if (!this.shardQueue.size) this.reconnecting = false;
this.checkShardsReady();
});
shard.on(WebSocketShardEvents.Close, event => {
if (event.code === 1_000 ? this.destroyed : event.code in unrecoverableErrorCodeMap) {
shard.on(WebSocketShardEvents.AllReady, unavailableGuilds => {
/**
* Emitted when a shard's WebSocket disconnects and will no longer reconnect.
* @event Client#shardDisconnect
* @param {CloseEvent} event The WebSocket close event
* @param {number} id The shard id that disconnected
* Emitted when a shard turns ready.
* @event Client#shardReady
* @param {number} id The shard id that turned ready
* @param {?Set<Snowflake>} unavailableGuilds Set of unavailable guild ids, if any
*/
this.client.emit(Events.ShardDisconnect, event, shard.id);
this.debug(GatewayCloseCodes[event.code], shard);
return;
}
this.client.emit(Events.ShardReady, shard.id, unavailableGuilds);
if (UNRESUMABLE_CLOSE_CODES.includes(event.code)) {
// These event codes cannot be resumed
shard.sessionId = null;
}
/**
* Emitted when a shard is attempting to reconnect or re-identify.
* @event Client#shardReconnecting
* @param {number} id The shard id that is attempting to reconnect
*/
this.client.emit(Events.ShardReconnecting, shard.id);
this.shardQueue.add(shard);
if (shard.sessionId) this.debug(`Session id is present, attempting an immediate reconnect...`, shard);
this.reconnect();
});
shard.on(WebSocketShardEvents.InvalidSession, () => {
this.client.emit(Events.ShardReconnecting, shard.id);
});
shard.on(WebSocketShardEvents.Destroyed, () => {
this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard);
this.client.emit(Events.ShardReconnecting, shard.id);
this.shardQueue.add(shard);
this.reconnect();
});
shard.eventsAttached = true;
}
this.shards.set(shard.id, shard);
try {
await shard.connect();
} catch (error) {
if (error?.code && error.code in unrecoverableErrorCodeMap) {
throw new DiscordjsError(unrecoverableErrorCodeMap[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);
this.shardQueue.add(shard);
} else {
throw error;
this.checkShardsReady();
});
shard.status = Status.Connecting;
}
}
// If we have more shards, add a 5s delay
if (this.shardQueue.size) {
this.debug(`Shard Queue Size: ${this.shardQueue.size}; continuing in 5 seconds...`);
await sleep(5_000);
return this.createShards();
}
return true;
await this._ws.connect();
this.shards.forEach(shard => {
if (shard.listenerCount(WebSocketShardEvents.InvalidSession) > 0 && !deprecationEmittedForInvalidSessionEvent) {
process.emitWarning(
'The WebSocketShard#invalidSession event is deprecated and will never emit.',
'DeprecationWarning',
);
deprecationEmittedForInvalidSessionEvent = true;
}
if (shard.listenerCount(WebSocketShardEvents.Destroyed) > 0 && !deprecationEmittedForDestroyedEvent) {
process.emitWarning(
'The WebSocketShard#destroyed event is deprecated and will never emit.',
'DeprecationWarning',
);
deprecationEmittedForDestroyedEvent = true;
}
});
}
/**
* Handles reconnects for this manager.
* Attaches event handlers to the internal WebSocketShardManager from `@discordjs/ws`.
* @private
* @returns {Promise<boolean>}
*/
async reconnect() {
if (this.reconnecting || this.status !== Status.Ready) return false;
this.reconnecting = true;
try {
await this.createShards();
} catch (error) {
this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`);
if (error.httpStatus !== 401) {
this.debug(`Possible network error occurred. Retrying in 5s...`);
await sleep(5_000);
this.reconnecting = false;
return this.reconnect();
attachEvents() {
this._ws.on(WSWebSocketShardEvents.Debug, ({ message, shardId }) => this.debug(message, shardId));
this._ws.on(WSWebSocketShardEvents.Dispatch, ({ data, shardId }) => {
this.client.emit(Events.Raw, data, shardId);
this.emit(data.t, data.d, shardId);
const shard = this.shards.get(shardId);
this.handlePacket(data, shard);
if (shard.status === Status.WaitingForGuilds && WaitingForGuildEvents.includes(data.t)) {
shard.gotGuild(data.d.id);
}
// If we get an error at this point, it means we cannot reconnect anymore
if (this.client.listenerCount(Events.Invalidated)) {
});
this._ws.on(WSWebSocketShardEvents.Ready, ({ data, shardId }) => {
this.shards.get(shardId).onReadyPacket(data);
});
this._ws.on(WSWebSocketShardEvents.Closed, ({ code, shardId }) => {
const shard = this.shards.get(shardId);
shard.emit(WebSocketShardEvents.Close, { code, reason: reasonIsDeprecated, wasClean: true });
if (UNRESUMABLE_CLOSE_CODES.includes(code) && this.destroyed) {
shard.status = Status.Disconnected;
/**
* 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
* Emitted when a shard's WebSocket disconnects and will no longer reconnect.
* @event Client#shardDisconnect
* @param {CloseEvent} event The WebSocket close event
* @param {number} id The shard id that disconnected
*/
this.client.emit(Events.Invalidated);
// Destroy just the shards. This means you have to handle the cleanup yourself
this.destroy();
} else {
this.client.destroy();
this.client.emit(Events.ShardDisconnect, { code, reason: reasonIsDeprecated, wasClean: true }, shardId);
this.debug(GatewayCloseCodes[code], shardId);
return;
}
} finally {
this.reconnecting = false;
}
return true;
this.shards.get(shardId).status = Status.Connecting;
/**
* Emitted when a shard is attempting to reconnect or re-identify.
* @event Client#shardReconnecting
* @param {number} id The shard id that is attempting to reconnect
*/
this.client.emit(Events.ShardReconnecting, shardId);
});
this._ws.on(WSWebSocketShardEvents.Hello, ({ shardId }) => {
const shard = this.shards.get(shardId);
if (shard.sessionInfo) {
shard.closeSequence = shard.sessionInfo.sequence;
shard.status = Status.Resuming;
} else {
shard.status = Status.Identifying;
}
});
this._ws.on(WSWebSocketShardEvents.Resumed, ({ shardId }) => {
const shard = this.shards.get(shardId);
shard.status = Status.Ready;
/**
* Emitted when the shard resumes successfully
* @event WebSocketShard#resumed
*/
shard.emit(WebSocketShardEvents.Resumed);
});
this._ws.on(WSWebSocketShardEvents.HeartbeatComplete, ({ heartbeatAt, latency, shardId }) => {
this.debug(`Heartbeat acknowledged, latency of ${latency}ms.`, shardId);
const shard = this.shards.get(shardId);
shard.lastPingTimestamp = heartbeatAt;
shard.ping = latency;
});
this._ws.on(WSWebSocketShardEvents.Error, ({ error, shardId }) => {
/**
* Emitted whenever a shard's WebSocket encounters a connection error.
* @event Client#shardError
* @param {Error} error The encountered error
* @param {number} shardId The shard that encountered this error
*/
this.client.emit(Events.ShardError, error, shardId);
});
}
/**
@ -310,7 +313,7 @@ class WebSocketManager extends EventEmitter {
* @private
*/
broadcast(packet) {
for (const shard of this.shards.values()) shard.send(packet);
for (const shardId of this.shards.keys()) this._ws.send(shardId, packet);
}
/**
@ -322,8 +325,7 @@ class WebSocketManager extends EventEmitter {
// TODO: Make a util for getting a stack
this.debug(`Manager was destroyed. Called by:\n${new Error().stack}`);
this.destroyed = true;
this.shardQueue.clear();
for (const shard of this.shards.values()) shard.destroy({ closeCode: 1_000, reset: true, emit: false, log: false });
this._ws.destroy({ code: CloseCodes.Normal });
}
/**

View file

@ -1,22 +1,13 @@
'use strict';
const EventEmitter = require('node:events');
const { setTimeout, setInterval, clearTimeout, clearInterval } = require('node:timers');
const { GatewayDispatchEvents, GatewayIntentBits, GatewayOpcodes } = require('discord-api-types/v10');
const WebSocket = require('../../WebSocket');
const Events = require('../../util/Events');
const process = require('node:process');
const { setTimeout, clearTimeout } = require('node:timers');
const { GatewayIntentBits } = require('discord-api-types/v10');
const Status = require('../../util/Status');
const WebSocketShardEvents = require('../../util/WebSocketShardEvents');
const STATUS_KEYS = Object.keys(Status);
const CONNECTION_STATE = Object.keys(WebSocket.WebSocket);
let zlib;
try {
zlib = require('zlib-sync');
} catch {} // eslint-disable-line no-empty
let deprecationEmittedForImportant = false;
/**
* Represents a Shard's WebSocket connection
* @extends {EventEmitter}
@ -43,13 +34,6 @@ class WebSocketShard extends EventEmitter {
*/
this.status = Status.Idle;
/**
* The current sequence of the shard
* @type {number}
* @private
*/
this.sequence = -1;
/**
* The sequence of the shard after close
* @type {number}
@ -57,20 +41,6 @@ class WebSocketShard extends EventEmitter {
*/
this.closeSequence = 0;
/**
* The current session id of the shard
* @type {?string}
* @private
*/
this.sessionId = null;
/**
* The resume url for this shard
* @type {?string}
* @private
*/
this.resumeURL = null;
/**
* The previous heartbeat ping of the shard
* @type {number}
@ -83,81 +53,6 @@ class WebSocketShard extends EventEmitter {
*/
this.lastPingTimestamp = -1;
/**
* If we received a heartbeat ack back. Used to identify zombie connections
* @type {boolean}
* @private
*/
this.lastHeartbeatAcked = true;
/**
* Used to prevent calling {@link WebSocketShard#event:close} twice while closing or terminating the WebSocket.
* @type {boolean}
* @private
*/
this.closeEmitted = false;
/**
* Contains the rate limit queue and metadata
* @name WebSocketShard#ratelimit
* @type {Object}
* @private
*/
Object.defineProperty(this, 'ratelimit', {
value: {
queue: [],
total: 120,
remaining: 120,
time: 60e3,
timer: null,
},
});
/**
* The WebSocket connection for the current shard
* @name WebSocketShard#connection
* @type {?WebSocket}
* @private
*/
Object.defineProperty(this, 'connection', { value: null, writable: true });
/**
* @external Inflate
* @see {@link https://www.npmjs.com/package/zlib-sync}
*/
/**
* The compression to use
* @name WebSocketShard#inflate
* @type {?Inflate}
* @private
*/
Object.defineProperty(this, 'inflate', { value: null, writable: true });
/**
* The HELLO timeout
* @name WebSocketShard#helloTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'helloTimeout', { value: null, writable: true });
/**
* The WebSocket timeout.
* @name WebSocketShard#wsCloseTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'wsCloseTimeout', { value: null, writable: true });
/**
* If the manager attached its event handlers on the shard
* @name WebSocketShard#eventsAttached
* @type {boolean}
* @private
*/
Object.defineProperty(this, 'eventsAttached', { value: false, writable: true });
/**
* A set of guild ids this shard expects to receive
* @name WebSocketShard#expectedGuilds
@ -175,12 +70,17 @@ class WebSocketShard extends EventEmitter {
Object.defineProperty(this, 'readyTimeout', { value: null, writable: true });
/**
* Time when the WebSocket connection was opened
* @name WebSocketShard#connectedAt
* @type {number}
* @external SessionInfo
* @see {@link https://discord.js.org/#/docs/ws/main/typedef/SessionInfo}
*/
/**
* The session info used by `@discordjs/ws` package.
* @name WebSocketShard#sessionInfo
* @type {?SessionInfo}
* @private
*/
Object.defineProperty(this, 'connectedAt', { value: 0, writable: true });
Object.defineProperty(this, 'sessionInfo', { value: null, writable: true });
}
/**
@ -189,161 +89,7 @@ class WebSocketShard extends EventEmitter {
* @private
*/
debug(message) {
this.manager.debug(message, this);
}
/**
* Connects the shard to the gateway.
* @private
* @returns {Promise<void>} A promise that will resolve if the shard turns ready successfully,
* or reject if we couldn't connect
*/
connect() {
const { client } = this.manager;
if (this.connection?.readyState === WebSocket.OPEN && this.status === Status.Ready) {
return Promise.resolve();
}
const gateway = this.resumeURL ?? this.manager.gateway;
return new Promise((resolve, reject) => {
const cleanup = () => {
this.removeListener(WebSocketShardEvents.Close, onClose);
this.removeListener(WebSocketShardEvents.Ready, onReady);
this.removeListener(WebSocketShardEvents.Resumed, onResumed);
this.removeListener(WebSocketShardEvents.InvalidSession, onInvalidOrDestroyed);
this.removeListener(WebSocketShardEvents.Destroyed, onInvalidOrDestroyed);
};
const onReady = () => {
cleanup();
resolve();
};
const onResumed = () => {
cleanup();
resolve();
};
const onClose = event => {
cleanup();
reject(event);
};
const onInvalidOrDestroyed = () => {
cleanup();
// eslint-disable-next-line prefer-promise-reject-errors
reject();
};
this.once(WebSocketShardEvents.Ready, onReady);
this.once(WebSocketShardEvents.Resumed, onResumed);
this.once(WebSocketShardEvents.Close, onClose);
this.once(WebSocketShardEvents.InvalidSession, onInvalidOrDestroyed);
this.once(WebSocketShardEvents.Destroyed, onInvalidOrDestroyed);
if (this.connection?.readyState === WebSocket.OPEN) {
this.debug('An open connection was found, attempting an immediate identify.');
this.identify();
return;
}
if (this.connection) {
this.debug(`A connection object was found. Cleaning up before continuing.
State: ${CONNECTION_STATE[this.connection.readyState]}`);
this.destroy({ emit: false });
}
const wsQuery = { v: client.options.ws.version };
if (zlib) {
this.inflate = new zlib.Inflate({
chunkSize: 65535,
flush: zlib.Z_SYNC_FLUSH,
to: WebSocket.encoding === 'json' ? 'string' : '',
});
wsQuery.compress = 'zlib-stream';
}
this.debug(
`[CONNECT]
Gateway : ${gateway}
Version : ${client.options.ws.version}
Encoding : ${WebSocket.encoding}
Compression: ${zlib ? 'zlib-stream' : 'none'}`,
);
this.status = this.status === Status.Disconnected ? Status.Reconnecting : Status.Connecting;
this.setHelloTimeout();
this.connectedAt = Date.now();
// Adding a handshake timeout to just make sure no zombie connection appears.
const ws = (this.connection = WebSocket.create(gateway, wsQuery, { handshakeTimeout: 30_000 }));
ws.onopen = this.onOpen.bind(this);
ws.onmessage = this.onMessage.bind(this);
ws.onerror = this.onError.bind(this);
ws.onclose = this.onClose.bind(this);
});
}
/**
* Called whenever a connection is opened to the gateway.
* @private
*/
onOpen() {
this.debug(`[CONNECTED] Took ${Date.now() - this.connectedAt}ms`);
this.status = Status.Nearly;
}
/**
* Called whenever a message is received.
* @param {MessageEvent} event Event received
* @private
*/
onMessage({ data }) {
let raw;
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
if (zlib) {
const l = data.length;
const flush =
l >= 4 && data[l - 4] === 0x00 && data[l - 3] === 0x00 && data[l - 2] === 0xff && data[l - 1] === 0xff;
this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH);
if (!flush) return;
raw = this.inflate.result;
} else {
raw = data;
}
let packet;
try {
packet = WebSocket.unpack(raw);
} catch (err) {
this.manager.client.emit(Events.ShardError, 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.onPacket(packet);
}
/**
* Called whenever an error occurs with the WebSocket.
* @param {ErrorEvent} event The error that occurred
* @private
*/
onError(event) {
const error = event?.error ?? event;
if (!error) return;
/**
* Emitted whenever a shard's WebSocket encounters a connection error.
* @event Client#shardError
* @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.debug(message, this.id);
}
/**
@ -351,39 +97,11 @@ class WebSocketShard extends EventEmitter {
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent}
*/
/**
* @external ErrorEvent
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent}
*/
/**
* @external MessageEvent
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent}
*/
/**
* Called whenever a connection to the gateway is closed.
* @param {CloseEvent} event Close event that was received
* @private
*/
onClose(event) {
this.closeEmitted = true;
if (this.sequence !== -1) this.closeSequence = this.sequence;
this.sequence = -1;
this.setHeartbeatTimer(-1);
this.setHelloTimeout(-1);
// Clearing the WebSocket close timeout as close was emitted.
this.setWsCloseTimeout(-1);
// If we still have a connection object, clean up its listeners
if (this.connection) this._cleanupConnection();
this.status = Status.Disconnected;
this.emitClose(event);
}
/**
* This method is responsible to emit close event for this shard.
* This method helps the shard reconnect.
* @param {CloseEvent} [event] Close event that was received
* @deprecated
*/
emitClose(
event = {
@ -404,94 +122,37 @@ class WebSocketShard extends EventEmitter {
*/
this.emit(WebSocketShardEvents.Close, event);
}
/**
* Called whenever a packet is received.
* Called when the shard receives the READY payload.
* @param {Object} packet The received packet
* @private
*/
onPacket(packet) {
onReadyPacket(packet) {
if (!packet) {
this.debug(`Received broken packet: '${packet}'.`);
return;
}
switch (packet.t) {
case GatewayDispatchEvents.Ready:
/**
* Emitted when the shard receives the READY payload and is now waiting for guilds
* @event WebSocketShard#ready
*/
this.emit(WebSocketShardEvents.Ready);
/**
* Emitted when the shard receives the READY payload and is now waiting for guilds
* @event WebSocketShard#ready
*/
this.emit(WebSocketShardEvents.Ready);
this.sessionId = packet.d.session_id;
this.resumeURL = packet.d.resume_gateway_url;
this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id));
this.status = Status.WaitingForGuilds;
this.debug(`[READY] Session ${this.sessionId} | Resume url ${this.resumeURL}.`);
this.lastHeartbeatAcked = true;
this.sendHeartbeat('ReadyHeartbeat');
break;
case GatewayDispatchEvents.Resumed: {
/**
* Emitted when the shard resumes successfully
* @event WebSocketShard#resumed
*/
this.emit(WebSocketShardEvents.Resumed);
this.expectedGuilds = new Set(packet.guilds.map(d => d.id));
this.status = Status.WaitingForGuilds;
}
this.status = Status.Ready;
const replayed = packet.s - this.closeSequence;
this.debug(`[RESUMED] Session ${this.sessionId} | Replayed ${replayed} events.`);
this.lastHeartbeatAcked = true;
this.sendHeartbeat('ResumeHeartbeat');
break;
}
}
if (packet.s > this.sequence) this.sequence = packet.s;
switch (packet.op) {
case GatewayOpcodes.Hello:
this.setHelloTimeout(-1);
this.setHeartbeatTimer(packet.d.heartbeat_interval);
this.identify();
break;
case GatewayOpcodes.Reconnect:
this.debug('[RECONNECT] Discord asked us to reconnect');
this.destroy({ closeCode: 4_000 });
break;
case GatewayOpcodes.InvalidSession:
this.debug(`[INVALID SESSION] Resumable: ${packet.d}.`);
// If we can resume the session, do so immediately
if (packet.d) {
this.identifyResume();
return;
}
// Reset the sequence
this.sequence = -1;
// Reset the session id as it's invalid
this.sessionId = null;
// Set the status to reconnecting
this.status = Status.Reconnecting;
// Finally, emit the INVALID_SESSION event
/**
* Emitted when the session has been invalidated.
* @event WebSocketShard#invalidSession
*/
this.emit(WebSocketShardEvents.InvalidSession);
break;
case GatewayOpcodes.HeartbeatAck:
this.ackHeartbeat();
break;
case GatewayOpcodes.Heartbeat:
this.sendHeartbeat('HeartbeatRequest', true);
break;
default:
this.manager.handlePacket(packet, this);
if (this.status === Status.WaitingForGuilds && packet.t === GatewayDispatchEvents.GuildCreate) {
this.expectedGuilds.delete(packet.d.id);
this.checkReady();
}
}
/**
* Called when a GuildCreate or GuildDelete for this shard was sent after READY payload was received,
* but before we emitted the READY event.
* @param {Snowflake} guildId the id of the Guild sent in the payload
* @private
*/
gotGuild(guildId) {
this.expectedGuilds.delete(guildId);
this.checkReady();
}
/**
@ -538,7 +199,6 @@ class WebSocketShard extends EventEmitter {
);
this.readyTimeout = null;
this.status = Status.Ready;
this.emit(WebSocketShardEvents.AllReady, this.expectedGuilds);
@ -547,187 +207,6 @@ class WebSocketShard extends EventEmitter {
).unref();
}
/**
* Sets the HELLO packet timeout.
* @param {number} [time] If set to -1, it will clear the hello timeout
* @private
*/
setHelloTimeout(time) {
if (time === -1) {
if (this.helloTimeout) {
this.debug('Clearing the HELLO timeout.');
clearTimeout(this.helloTimeout);
this.helloTimeout = null;
}
return;
}
this.debug('Setting a HELLO timeout for 20s.');
this.helloTimeout = setTimeout(() => {
this.debug('Did not receive HELLO in time. Destroying and connecting again.');
this.destroy({ reset: true, closeCode: 4009 });
}, 20_000).unref();
}
/**
* Sets the WebSocket Close timeout.
* This method is responsible for detecting any zombie connections if the WebSocket fails to close properly.
* @param {number} [time] If set to -1, it will clear the timeout
* @private
*/
setWsCloseTimeout(time) {
if (this.wsCloseTimeout) {
this.debug('[WebSocket] Clearing the close timeout.');
clearTimeout(this.wsCloseTimeout);
}
if (time === -1) {
this.wsCloseTimeout = null;
return;
}
this.wsCloseTimeout = setTimeout(() => {
this.setWsCloseTimeout(-1);
this.debug(`[WebSocket] Close Emitted: ${this.closeEmitted}`);
// Check if close event was emitted.
if (this.closeEmitted) {
this.debug(
`[WebSocket] was closed. | WS State: ${
CONNECTION_STATE[this.connection?.readyState ?? WebSocket.CLOSED]
} | Close Emitted: ${this.closeEmitted}`,
);
// Setting the variable false to check for zombie connections.
this.closeEmitted = false;
return;
}
this.debug(
`[WebSocket] did not close properly, assuming a zombie connection.\nEmitting close and reconnecting again.`,
);
this.emitClose();
// Setting the variable false to check for zombie connections.
this.closeEmitted = false;
}, time).unref();
}
/**
* Sets the heartbeat timer for this shard.
* @param {number} time If -1, clears the interval, any other number sets an interval
* @private
*/
setHeartbeatTimer(time) {
if (time === -1) {
if (this.heartbeatInterval) {
this.debug('Clearing the heartbeat interval.');
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
return;
}
this.debug(`Setting a heartbeat interval for ${time}ms.`);
// Sanity checks
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), time).unref();
}
/**
* Sends a heartbeat to the WebSocket.
* If this shard didn't receive a heartbeat last time, it will destroy it and reconnect
* @param {string} [tag='HeartbeatTimer'] What caused this heartbeat to be sent
* @param {boolean} [ignoreHeartbeatAck] If we should send the heartbeat forcefully.
* @private
*/
sendHeartbeat(
tag = 'HeartbeatTimer',
ignoreHeartbeatAck = [Status.WaitingForGuilds, 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.`);
} else if (!this.lastHeartbeatAcked) {
this.debug(
`[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting.
Status : ${STATUS_KEYS[this.status]}
Sequence : ${this.sequence}
Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}`,
);
this.destroy({ reset: true, closeCode: 4009 });
return;
}
this.debug(`[${tag}] Sending a heartbeat.`);
this.lastHeartbeatAcked = false;
this.lastPingTimestamp = Date.now();
this.send({ op: GatewayOpcodes.Heartbeat, d: this.sequence }, true);
}
/**
* Acknowledges a heartbeat.
* @private
*/
ackHeartbeat() {
this.lastHeartbeatAcked = true;
const latency = Date.now() - this.lastPingTimestamp;
this.debug(`Heartbeat acknowledged, latency of ${latency}ms.`);
this.ping = latency;
}
/**
* Identifies the client on the connection.
* @private
* @returns {void}
*/
identify() {
return this.sessionId ? this.identifyResume() : this.identifyNew();
}
/**
* Identifies as a new connection on the gateway.
* @private
*/
identifyNew() {
const { client } = this.manager;
if (!client.token) {
this.debug('[IDENTIFY] No token available to identify a new session.');
return;
}
this.status = Status.Identifying;
// Clone the identify payload and assign the token and shard info
const d = {
...client.options.ws,
intents: client.options.intents.bitfield,
token: client.token,
shard: [this.id, Number(client.options.shardCount)],
};
this.debug(`[IDENTIFY] Shard ${this.id}/${client.options.shardCount} with intents: ${d.intents}`);
this.send({ op: GatewayOpcodes.Identify, d }, true);
}
/**
* Resumes a session on the gateway.
* @private
*/
identifyResume() {
if (!this.sessionId) {
this.debug('[RESUME] No session id was present; identifying as a new session.');
this.identifyNew();
return;
}
this.status = Status.Resuming;
this.debug(`[RESUME] Session ${this.sessionId}, sequence ${this.closeSequence}`);
const d = {
token: this.manager.client.token,
session_id: this.sessionId,
seq: this.closeSequence,
};
this.send({ op: GatewayOpcodes.Resume, d }, true);
}
/**
* Adds a packet to the queue to be sent to the gateway.
* <warn>If you use this method, make sure you understand that you need to provide
@ -735,163 +214,17 @@ class WebSocketShard extends EventEmitter {
* Do not use this method if you don't know what you're doing.</warn>
* @param {Object} data The full packet to send
* @param {boolean} [important=false] If this packet should be added first in queue
* <warn>This parameter is **deprecated**. Important payloads are determined by their opcode instead.</warn>
*/
send(data, important = false) {
this.ratelimit.queue[important ? 'unshift' : 'push'](data);
this.processQueue();
}
/**
* Sends data, bypassing the queue.
* @param {Object} data Packet to send
* @returns {void}
* @private
*/
_send(data) {
if (this.connection?.readyState !== WebSocket.OPEN) {
this.debug(
`Tried to send packet '${JSON.stringify(data).replaceAll(
this.manager.client.token,
this.manager.client._censoredToken,
)}' but no WebSocket is available!`,
if (important && !deprecationEmittedForImportant) {
process.emitWarning(
'Sending important payloads explicitly is deprecated. They are determined by their opcode implicitly now.',
'DeprecationWarning',
);
this.destroy({ closeCode: 4_000 });
return;
deprecationEmittedForImportant = true;
}
this.connection.send(WebSocket.pack(data), err => {
if (err) this.manager.client.emit(Events.ShardError, err, this.id);
});
}
/**
* Processes the current WebSocket queue.
* @returns {void}
* @private
*/
processQueue() {
if (this.ratelimit.remaining === 0) return;
if (this.ratelimit.queue.length === 0) return;
if (this.ratelimit.remaining === this.ratelimit.total) {
this.ratelimit.timer = setTimeout(() => {
this.ratelimit.remaining = this.ratelimit.total;
this.processQueue();
}, this.ratelimit.time).unref();
}
while (this.ratelimit.remaining > 0) {
const item = this.ratelimit.queue.shift();
if (!item) return;
this._send(item);
this.ratelimit.remaining--;
}
}
/**
* Destroys this shard and closes its WebSocket connection.
* @param {Object} [options={ closeCode: 1000, reset: false, emit: true, log: true }] Options for destroying the shard
* @private
*/
destroy({ closeCode = 1_000, reset = false, emit = true, log = true } = {}) {
if (log) {
this.debug(`[DESTROY]
Close Code : ${closeCode}
Reset : ${reset}
Emit DESTROYED: ${emit}`);
}
// Step 0: Remove all timers
this.setHeartbeatTimer(-1);
this.setHelloTimeout(-1);
this.debug(
`[WebSocket] Destroy: Attempting to close the WebSocket. | WS State: ${
CONNECTION_STATE[this.connection?.readyState ?? WebSocket.CLOSED]
}`,
);
// Step 1: Close the WebSocket connection, if any, otherwise, emit DESTROYED
if (this.connection) {
// If the connection is currently opened, we will (hopefully) receive close
if (this.connection.readyState === WebSocket.OPEN) {
this.connection.close(closeCode);
this.debug(`[WebSocket] Close: Tried closing. | WS State: ${CONNECTION_STATE[this.connection.readyState]}`);
} else {
// Connection is not OPEN
this.debug(`WS State: ${CONNECTION_STATE[this.connection.readyState]}`);
// Remove listeners from the connection
this._cleanupConnection();
// Attempt to close the connection just in case
try {
this.connection.close(closeCode);
} catch (err) {
this.debug(
`[WebSocket] Close: Something went wrong while closing the WebSocket: ${
err.message || err
}. Forcefully terminating the connection | WS State: ${CONNECTION_STATE[this.connection.readyState]}`,
);
this.connection.terminate();
}
// Emit the destroyed event if needed
if (emit) this._emitDestroyed();
}
} else if (emit) {
// We requested a destroy, but we had no connection. Emit destroyed
this._emitDestroyed();
}
if (this.connection?.readyState === WebSocket.CLOSING || this.connection?.readyState === WebSocket.CLOSED) {
this.closeEmitted = false;
this.debug(
`[WebSocket] Adding a WebSocket close timeout to ensure a correct WS reconnect.
Timeout: ${this.manager.client.options.closeTimeout}ms`,
);
this.setWsCloseTimeout(this.manager.client.options.closeTimeout);
}
// Step 2: Null the connection object
this.connection = null;
// 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;
// Step 5: Reset the sequence, resume url and session id if requested
if (reset) {
this.sequence = -1;
this.sessionId = null;
this.resumeURL = null;
}
// Step 6: reset the rate limit data
this.ratelimit.remaining = this.ratelimit.total;
this.ratelimit.queue.length = 0;
if (this.ratelimit.timer) {
clearTimeout(this.ratelimit.timer);
this.ratelimit.timer = null;
}
}
/**
* Cleans up the WebSocket connection listeners.
* @private
*/
_cleanupConnection() {
this.connection.onopen = this.connection.onclose = this.connection.onmessage = null;
this.connection.onerror = () => null;
}
/**
* Emits the DESTROYED event on the shard
* @private
*/
_emitDestroyed() {
/**
* Emitted when a shard is destroyed, but no WebSocket connection was present.
* @private
* @event WebSocketShard#destroyed
*/
this.emit(WebSocketShardEvents.Destroyed);
this.manager._ws.send(this.id, data);
}
}

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildAuditLogEntryCreate.handle(packet.d);
};

View file

@ -18,6 +18,8 @@ module.exports = (client, { d: data }) => {
* @typedef {Object} GuildMembersChunk
* @property {number} index Index of the received chunk
* @property {number} count Number of chunks the client should receive
* @property {Array<*>} notFound An array of whatever could not be found
* when using {@link GatewayOpcodes.RequestGuildMembers}
* @property {?string} nonce Nonce for this chunk
*/
@ -29,8 +31,9 @@ module.exports = (client, { d: data }) => {
* @param {GuildMembersChunk} chunk Properties of the received chunk
*/
client.emit(Events.GuildMembersChunk, members, guild, {
count: data.chunk_count,
index: data.chunk_index,
count: data.chunk_count,
notFound: data.not_found,
nonce: data.nonce,
});
};

View file

@ -3,7 +3,7 @@
const Events = require('../../../util/Events');
module.exports = (client, packet, shard) => {
const replayed = shard.sequence - shard.closeSequence;
const replayed = shard.sessionInfo.sequence - shard.closeSequence;
/**
* Emitted when a shard resumes successfully.
* @event Client#shardResume

View file

@ -10,6 +10,7 @@ const handlers = Object.fromEntries([
['CHANNEL_DELETE', require('./CHANNEL_DELETE')],
['CHANNEL_PINS_UPDATE', require('./CHANNEL_PINS_UPDATE')],
['CHANNEL_UPDATE', require('./CHANNEL_UPDATE')],
['GUILD_AUDIT_LOG_ENTRY_CREATE', require('./GUILD_AUDIT_LOG_ENTRY_CREATE')],
['GUILD_BAN_ADD', require('./GUILD_BAN_ADD')],
['GUILD_BAN_REMOVE', require('./GUILD_BAN_REMOVE')],
['GUILD_CREATE', require('./GUILD_CREATE')],

View file

@ -13,16 +13,23 @@
* @property {'ApplicationCommandPermissionsTokenMissing'} ApplicationCommandPermissionsTokenMissing
* @property {'WSCloseRequested'} WSCloseRequested
* <warn>This property is deprecated.</warn>
* @property {'WSConnectionExists'} WSConnectionExists
* <warn>This property is deprecated.</warn>
* @property {'WSNotOpen'} WSNotOpen
* <warn>This property is deprecated.</warn>
* @property {'ManagerDestroyed'} ManagerDestroyed
* @property {'BitFieldInvalid'} BitFieldInvalid
* @property {'ShardingInvalid'} ShardingInvalid
* <warn>This property is deprecated.</warn>
* @property {'ShardingRequired'} ShardingRequired
* <warn>This property is deprecated.</warn>
* @property {'InvalidIntents'} InvalidIntents
* <warn>This property is deprecated.</warn>
* @property {'DisallowedIntents'} DisallowedIntents
* <warn>This property is deprecated.</warn>
* @property {'ShardingNoShards'} ShardingNoShards
* @property {'ShardingInProcess'} ShardingInProcess
* @property {'ShardingInvalidEvalBroadcast'} ShardingInvalidEvalBroadcast
@ -106,7 +113,10 @@
* @property {'EmojiType'} EmojiType
* @property {'EmojiManaged'} EmojiManaged
* @property {'MissingManageGuildExpressionsPermission'} MissingManageGuildExpressionsPermission
* @property {'MissingManageEmojisAndStickersPermission'} MissingManageEmojisAndStickersPermission
* <warn>This property is deprecated. Use `MissingManageGuildExpressionsPermission` instead.</warn>
*
* @property {'NotGuildSticker'} NotGuildSticker
* @property {'ReactionResolveUser'} ReactionResolveUser
@ -135,12 +145,14 @@
* @property {'CommandInteractionOptionEmpty'} CommandInteractionOptionEmpty
* @property {'CommandInteractionOptionNoSubcommand'} CommandInteractionOptionNoSubcommand
* @property {'CommandInteractionOptionNoSubcommandGroup'} CommandInteractionOptionNoSubcommandGroup
* @property {'CommandInteractionOptionInvalidChannelType'} CommandInteractionOptionInvalidChannelType
* @property {'AutocompleteInteractionOptionNoFocusedOption'} AutocompleteInteractionOptionNoFocusedOption
* @property {'ModalSubmitInteractionFieldNotFound'} ModalSubmitInteractionFieldNotFound
* @property {'ModalSubmitInteractionFieldType'} ModalSubmitInteractionFieldType
* @property {'InvalidMissingScopes'} InvalidMissingScopes
* @property {'InvalidScopesWithPermissions'} InvalidScopesWithPermissions
* @property {'NotImplemented'} NotImplemented
@ -253,7 +265,9 @@ const keys = [
'EmojiType',
'EmojiManaged',
'MissingManageGuildExpressionsPermission',
'MissingManageEmojisAndStickersPermission',
'NotGuildSticker',
'ReactionResolveUser',
@ -281,12 +295,14 @@ const keys = [
'CommandInteractionOptionEmpty',
'CommandInteractionOptionNoSubcommand',
'CommandInteractionOptionNoSubcommandGroup',
'CommandInteractionOptionInvalidChannelType',
'AutocompleteInteractionOptionNoFocusedOption',
'ModalSubmitInteractionFieldNotFound',
'ModalSubmitInteractionFieldType',
'InvalidMissingScopes',
'InvalidScopesWithPermissions',
'NotImplemented',

View file

@ -111,8 +111,11 @@ const Messages = {
[DjsErrorCodes.EmojiType]: 'Emoji must be a string or GuildEmoji/ReactionEmoji',
[DjsErrorCodes.EmojiManaged]: 'Emoji is managed and has no Author.',
[DjsErrorCodes.MissingManageGuildExpressionsPermission]: guild =>
`Client must have Manage Guild Expressions permission in guild ${guild} to see emoji authors.`,
[DjsErrorCodes.MissingManageEmojisAndStickersPermission]: guild =>
`Client must have Manage Emojis and Stickers permission in guild ${guild} to see emoji authors.`,
[DjsErrorCodes.NotGuildSticker]: 'Sticker is a standard (non-guild) sticker and has no author.',
[DjsErrorCodes.ReactionResolveUser]: "Couldn't resolve the user id to remove from the reaction.",
@ -145,6 +148,8 @@ const Messages = {
`Required option "${name}" is of type: ${type}; expected a non-empty value.`,
[DjsErrorCodes.CommandInteractionOptionNoSubcommand]: 'No subcommand specified for interaction.',
[DjsErrorCodes.CommandInteractionOptionNoSubcommandGroup]: 'No subcommand group specified for interaction.',
[DjsErrorCodes.CommandInteractionOptionInvalidChannelType]: (name, type, expected) =>
`The type of channel of the option "${name}" is: ${type}; expected ${expected}.`,
[DjsErrorCodes.AutocompleteInteractionOptionNoFocusedOption]: 'No focused option for autocomplete interaction.',
[DjsErrorCodes.ModalSubmitInteractionFieldNotFound]: customId =>
@ -153,6 +158,7 @@ const Messages = {
`Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`,
[DjsErrorCodes.InvalidMissingScopes]: 'At least one valid scope must be provided for the invite',
[DjsErrorCodes.InvalidScopesWithPermissions]: 'Permissions cannot be set without the bot scope.',
[DjsErrorCodes.NotImplemented]: (what, name) => `Method ${what} not implemented on ${name}.`,

View file

@ -28,6 +28,7 @@ exports.Colors = require('./util/Colors');
exports.DataResolver = require('./util/DataResolver');
exports.Events = require('./util/Events');
exports.Formatters = require('./util/Formatters');
exports.GuildMemberFlagsBitField = require('./util/GuildMemberFlagsBitField').GuildMemberFlagsBitField;
exports.IntentsBitField = require('./util/IntentsBitField');
exports.LimitedCollection = require('./util/LimitedCollection');
exports.MessageFlagsBitField = require('./util/MessageFlagsBitField');
@ -88,6 +89,8 @@ exports.Activity = require('./structures/Presence').Activity;
exports.AnonymousGuild = require('./structures/AnonymousGuild');
exports.Application = require('./structures/interfaces/Application');
exports.ApplicationCommand = require('./structures/ApplicationCommand');
exports.ApplicationRoleConnectionMetadata =
require('./structures/ApplicationRoleConnectionMetadata').ApplicationRoleConnectionMetadata;
exports.AutocompleteInteraction = require('./structures/AutocompleteInteraction');
exports.AutoModerationActionExecution = require('./structures/AutoModerationActionExecution');
exports.AutoModerationRule = require('./structures/AutoModerationRule');
@ -201,10 +204,10 @@ exports.WidgetMember = require('./structures/WidgetMember');
exports.WelcomeChannel = require('./structures/WelcomeChannel');
exports.WelcomeScreen = require('./structures/WelcomeScreen');
exports.WebSocket = require('./WebSocket');
// External
__exportStar(require('discord-api-types/v10'), exports);
__exportStar(require('@discordjs/builders'), exports);
__exportStar(require('@discordjs/formatters'), exports);
__exportStar(require('@discordjs/rest'), exports);
__exportStar(require('@discordjs/util'), exports);
__exportStar(require('@discordjs/ws'), exports);

View file

@ -1,8 +1,8 @@
'use strict';
const { isJSONEncodable } = require('@discordjs/builders');
const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest');
const { isJSONEncodable } = require('@discordjs/util');
const { Routes } = require('discord-api-types/v10');
const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager');
const CachedManager = require('./CachedManager');
@ -66,12 +66,10 @@ class ApplicationCommandManager extends CachedManager {
* @typedef {ApplicationCommand|Snowflake} ApplicationCommandResolvable
*/
/* eslint-disable max-len */
/**
* Data that resolves to the data of an ApplicationCommand
* @typedef {ApplicationCommandData|APIApplicationCommand|JSONEncodable<APIApplicationCommand>} ApplicationCommandDataResolvable
* @typedef {ApplicationCommandData|APIApplicationCommand} ApplicationCommandDataResolvable
*/
/* eslint-enable max-len */
/**
* Options used to fetch data from Discord
@ -252,6 +250,7 @@ class ApplicationCommandManager extends CachedManager {
name: command.name,
name_localizations: command.nameLocalizations ?? command.name_localizations,
description: command.description,
nsfw: command.nsfw,
description_localizations: command.descriptionLocalizations ?? command.description_localizations,
type: command.type,
options: command.options?.map(o => ApplicationCommand.transformOption(o)),

View file

@ -112,14 +112,14 @@ class ApplicationCommandPermissionsManager extends BaseManager {
* Options used to set permissions for one or more Application Commands in a guild
* <warn>Omitting the `command` parameter edits the guild wide permissions
* when the manager's `commandId` is `null`</warn>
* @typedef {BaseApplicationCommandPermissionsOptions} EditApplicationCommandPermissionsOptions
* @typedef {BaseApplicationCommandPermissionsOptions} ApplicationCommandPermissionsEditOptions
* @property {ApplicationCommandPermissions[]} permissions The new permissions for the guild or overwrite
* @property {string} token The bearer token to use that authorizes the permission edit
*/
/**
* Sets the permissions for the guild or a command overwrite.
* @param {EditApplicationCommandPermissionsOptions} options Options used to set permissions
* @param {ApplicationCommandPermissionsEditOptions} options Options used to set permissions
* @returns {Promise<ApplicationCommandPermissions[]|Collection<Snowflake, ApplicationCommandPermissions[]>>}
* @example
* // Set a permission overwrite for a command
@ -179,7 +179,7 @@ class ApplicationCommandPermissionsManager extends BaseManager {
/**
* Add permissions to a command.
* @param {EditApplicationCommandPermissionsOptions} options Options used to add permissions
* @param {ApplicationCommandPermissionsEditOptions} options Options used to add permissions
* @returns {Promise<ApplicationCommandPermissions[]>}
* @example
* // Add a rule to block a role from using a command

View file

@ -26,6 +26,24 @@ class AutoModerationRuleManager extends CachedManager {
* @name AutoModerationRuleManager#cache
*/
/**
* Resolves an {@link AutoModerationRuleResolvable} to an {@link AutoModerationRule} object.
* @method resolve
* @memberof AutoModerationRuleManager
* @instance
* @param {AutoModerationRuleResolvable} autoModerationRule The AutoModerationRule resolvable to resolve
* @returns {?AutoModerationRule}
*/
/**
* Resolves an {@link AutoModerationRuleResolvable} to a {@link AutoModerationRule} id.
* @method resolveId
* @memberof AutoModerationRuleManager
* @instance
* @param {AutoModerationRuleResolvable} autoModerationRule The AutoModerationRule resolvable to resolve
* @returns {?Snowflake}
*/
_add(data, cache) {
return super._add(data, cache, { extras: [this.guild] });
}
@ -41,6 +59,7 @@ class AutoModerationRuleManager extends CachedManager {
* @property {string[]} [allowList] The substrings that will be exempt from triggering
* {@link AutoModerationRuleTriggerType.Keyword} and {@link AutoModerationRuleTriggerType.KeywordPreset}
* @property {?number} [mentionTotalLimit] The total number of role & user mentions allowed per message
* @property {boolean} [mentionRaidProtectionEnabled] Whether to automatically detect mention raids
*/
/**
@ -57,6 +76,7 @@ class AutoModerationRuleManager extends CachedManager {
* @typedef {Object} AutoModerationActionMetadataOptions
* @property {GuildTextChannelResolvable|ThreadChannel} [channel] The channel to which content will be logged
* @property {number} [durationSeconds] The timeout duration in seconds
* @property {string} [customMessage] The custom message that is shown whenever a message is blocked
*/
/**
@ -106,12 +126,14 @@ class AutoModerationRuleManager extends CachedManager {
presets: triggerMetadata.presets,
allow_list: triggerMetadata.allowList,
mention_total_limit: triggerMetadata.mentionTotalLimit,
mention_raid_protection_enabled: triggerMetadata.mentionRaidProtectionEnabled,
},
actions: actions.map(action => ({
type: action.type,
metadata: {
duration_seconds: action.metadata?.durationSeconds,
channel_id: action.metadata?.channel && this.guild.channels.resolveId(action.metadata.channel),
custom_message: action.metadata?.customMessage,
},
})),
enabled,
@ -162,12 +184,14 @@ class AutoModerationRuleManager extends CachedManager {
presets: triggerMetadata.presets,
allow_list: triggerMetadata.allowList,
mention_total_limit: triggerMetadata.mentionTotalLimit,
mention_raid_protection_enabled: triggerMetadata.mentionRaidProtectionEnabled,
},
actions: actions?.map(action => ({
type: action.type,
metadata: {
duration_seconds: action.metadata?.durationSeconds,
channel_id: action.metadata?.channel && this.guild.channels.resolveId(action.metadata.channel),
custom_message: action.metadata?.customMessage,
},
})),
enabled,
@ -259,24 +283,6 @@ class AutoModerationRuleManager extends CachedManager {
const autoModerationRuleId = this.resolveId(autoModerationRule);
await this.client.rest.delete(Routes.guildAutoModerationRule(this.guild.id, autoModerationRuleId), { reason });
}
/**
* Resolves an {@link AutoModerationRuleResolvable} to an {@link AutoModerationRule} object.
* @method resolve
* @memberof AutoModerationRuleManager
* @instance
* @param {AutoModerationRuleResolvable} autoModerationRule The AutoModerationRule resolvable to resolve
* @returns {?AutoModerationRule}
*/
/**
* Resolves an {@link AutoModerationRuleResolvable} to a {@link AutoModerationRule} id.
* @method resolveId
* @memberof AutoModerationRuleManager
* @instance
* @param {AutoModerationRuleResolvable} autoModerationRule The AutoModerationRule resolvable to resolve
* @returns {?Snowflake}
*/
}
module.exports = AutoModerationRuleManager;

View file

@ -50,9 +50,9 @@ class BaseGuildEmojiManager extends CachedManager {
/**
* Data that can be resolved to give an emoji identifier. This can be:
* * The unicode representation of an emoji
* * The `<a:name:id>`, `<:name:id>`, `a:name:id` or `name:id` emoji identifier string of an emoji
* * An EmojiResolvable
* * The `<a:name:id>`, `<:name:id>`, `a:name:id` or `name:id` emoji identifier string of an emoji
* * The Unicode representation of an emoji
* @typedef {string|EmojiResolvable} EmojiIdentifierResolvable
*/

View file

@ -11,6 +11,13 @@ class CachedManager extends DataManager {
constructor(client, holds, iterable) {
super(client, holds);
/**
* The private cache of items for this manager.
* @type {Collection}
* @private
* @readonly
* @name CachedManager#_cache
*/
Object.defineProperty(this, '_cache', { value: this.client.options.makeCache(this.constructor, this.holds) });
if (iterable) {

View file

@ -56,6 +56,7 @@ class CategoryChannelChildManager extends DataManager {
* @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration]
* The default auto archive duration for all new threads in this channel
* @property {SortOrderType} [defaultSortOrder] The default sort order mode used to order posts (forum only).
* @property {ForumLayoutType} [defaultForumLayout] The default layout used to display posts (forum only).
* @property {string} [reason] Reason for creating the new channel
*/

View file

@ -36,10 +36,10 @@ class ChannelManager extends CachedManager {
* @name ChannelManager#cache
*/
_add(data, guild, { cache = true, allowUnknownGuild = false, fromInteraction = false } = {}) {
_add(data, guild, { cache = true, allowUnknownGuild = false } = {}) {
const existing = this.cache.get(data.id);
if (existing) {
if (cache) existing._patch(data, fromInteraction);
if (cache) existing._patch(data);
guild?.channels?._add(existing);
if (ThreadChannelTypes.includes(existing.type)) {
existing.parent?.threads?._add(existing);
@ -47,7 +47,7 @@ class ChannelManager extends CachedManager {
return existing;
}
const channel = createChannel(this.client, data, guild, { allowUnknownGuild, fromInteraction });
const channel = createChannel(this.client, data, guild, { allowUnknownGuild });
if (!channel) {
this.client.emit(Events.Debug, `Failed to find guild, or unknown type for channel ${data.id} ${data.type}`);

View file

@ -12,7 +12,7 @@ const { GuildMember } = require('../structures/GuildMember');
let deprecationEmittedForDeleteMessageDays = false;
/**
* Manages API methods for GuildBans and stores their cache.
* Manages API methods for guild bans and stores their cache.
* @extends {CachedManager}
*/
class GuildBanManager extends CachedManager {
@ -103,7 +103,7 @@ class GuildBanManager extends CachedManager {
const resolvedUser = this.client.users.resolveId(user ?? options);
if (resolvedUser) return this._fetchSingle({ user: resolvedUser, cache, force });
if (!before && !after && !limit && typeof cache === 'undefined') {
if (!before && !after && !limit && cache === undefined) {
return Promise.reject(new DiscordjsError(ErrorCodes.FetchBanResolveId));
}
@ -156,7 +156,7 @@ class GuildBanManager extends CachedManager {
const id = this.client.users.resolveId(user);
if (!id) throw new DiscordjsError(ErrorCodes.BanResolveId, true);
if (typeof options.deleteMessageDays !== 'undefined' && !deprecationEmittedForDeleteMessageDays) {
if (options.deleteMessageDays !== undefined && !deprecationEmittedForDeleteMessageDays) {
process.emitWarning(
// eslint-disable-next-line max-len
'The deleteMessageDays option for GuildBanManager#create() is deprecated. Use the deleteMessageSeconds option instead.',

View file

@ -161,6 +161,7 @@ class GuildChannelManager extends CachedManager {
defaultReactionEmoji,
defaultAutoArchiveDuration,
defaultSortOrder,
defaultForumLayout,
reason,
}) {
parent &&= this.client.channels.resolveId(parent);
@ -184,6 +185,7 @@ class GuildChannelManager extends CachedManager {
default_reaction_emoji: defaultReactionEmoji && transformGuildDefaultReaction(defaultReactionEmoji),
default_auto_archive_duration: defaultAutoArchiveDuration,
default_sort_order: defaultSortOrder,
default_forum_layout: defaultForumLayout,
},
reason,
});
@ -192,7 +194,7 @@ class GuildChannelManager extends CachedManager {
/**
* @typedef {ChannelWebhookCreateOptions} WebhookCreateOptions
* @property {TextChannel|NewsChannel|VoiceChannel|ForumChannel|Snowflake} channel
* @property {TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel|Snowflake} channel
* The channel to create the webhook for
*/
@ -252,13 +254,14 @@ class GuildChannelManager extends CachedManager {
* @property {number} [defaultThreadRateLimitPerUser] The rate limit per user (slowmode) to set on forum posts
* @property {ChannelFlagsResolvable} [flags] The flags to set on the channel
* @property {?SortOrderType} [defaultSortOrder] The default sort order mode to set on the channel
* @property {ForumLayoutType} [defaultForumLayout] The default forum layout to set on the channel
* @property {string} [reason] Reason for editing this channel
*/
/**
* Edits the channel.
* @param {GuildChannelResolvable} channel The channel to edit
* @param {GuildChannelEditOptions} data Options for editing the channel
* @param {GuildChannelEditOptions} options Options for editing the channel
* @returns {Promise<GuildChannel>}
* @example
* // Edit a channel
@ -266,19 +269,19 @@ class GuildChannelManager extends CachedManager {
* .then(console.log)
* .catch(console.error);
*/
async edit(channel, data) {
async edit(channel, options) {
channel = this.resolve(channel);
if (!channel) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'channel', 'GuildChannelResolvable');
const parent = data.parent && this.client.channels.resolveId(data.parent);
const parent = options.parent && this.client.channels.resolveId(options.parent);
if (typeof data.position !== 'undefined') {
await this.setPosition(channel, data.position, { position: data.position, reason: data.reason });
if (options.position !== undefined) {
await this.setPosition(channel, options.position, { position: options.position, reason: options.reason });
}
let permission_overwrites = data.permissionOverwrites?.map(o => PermissionOverwrites.resolve(o, this.guild));
let permission_overwrites = options.permissionOverwrites?.map(o => PermissionOverwrites.resolve(o, this.guild));
if (data.lockPermissions) {
if (options.lockPermissions) {
if (parent) {
const newParent = this.guild.channels.resolve(parent);
if (newParent?.type === ChannelType.GuildCategory) {
@ -295,26 +298,28 @@ class GuildChannelManager extends CachedManager {
const newData = await this.client.rest.patch(Routes.channel(channel.id), {
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: 'rtcRegion' in data ? data.rtcRegion : channel.rtcRegion,
video_quality_mode: data.videoQualityMode,
name: (options.name ?? channel.name).trim(),
type: options.type,
topic: options.topic,
nsfw: options.nsfw,
bitrate: options.bitrate ?? channel.bitrate,
user_limit: options.userLimit ?? channel.userLimit,
rtc_region: 'rtcRegion' in options ? options.rtcRegion : channel.rtcRegion,
video_quality_mode: options.videoQualityMode,
parent_id: parent,
lock_permissions: data.lockPermissions,
rate_limit_per_user: data.rateLimitPerUser,
default_auto_archive_duration: data.defaultAutoArchiveDuration,
lock_permissions: options.lockPermissions,
rate_limit_per_user: options.rateLimitPerUser,
default_auto_archive_duration: options.defaultAutoArchiveDuration,
permission_overwrites,
available_tags: data.availableTags?.map(availableTag => transformGuildForumTag(availableTag)),
default_reaction_emoji: data.defaultReactionEmoji && transformGuildDefaultReaction(data.defaultReactionEmoji),
default_thread_rate_limit_per_user: data.defaultThreadRateLimitPerUser,
flags: 'flags' in data ? ChannelFlagsBitField.resolve(data.flags) : undefined,
default_sort_order: data.defaultSortOrder,
available_tags: options.availableTags?.map(availableTag => transformGuildForumTag(availableTag)),
default_reaction_emoji:
options.defaultReactionEmoji && transformGuildDefaultReaction(options.defaultReactionEmoji),
default_thread_rate_limit_per_user: options.defaultThreadRateLimitPerUser,
flags: 'flags' in options ? ChannelFlagsBitField.resolve(options.flags) : undefined,
default_sort_order: options.defaultSortOrder,
default_forum_layout: options.defaultForumLayout,
},
reason: data.reason,
reason: options.reason,
});
return this.client.actions.ChannelUpdate.handle(newData).updated;
@ -435,7 +440,7 @@ class GuildChannelManager extends CachedManager {
id: this.client.channels.resolveId(r.channel),
position: r.position,
lock_permissions: r.lockPermissions,
parent_id: typeof r.parent !== 'undefined' ? this.resolveId(r.parent) : undefined,
parent_id: r.parent !== undefined ? this.resolveId(r.parent) : undefined,
}));
await this.client.rest.patch(Routes.guildChannels(this.guild.id), { body: channelPositions });
@ -446,7 +451,14 @@ class GuildChannelManager extends CachedManager {
}
/**
* Obtains all active thread channels in the guild from Discord
* Data returned from fetching threads.
* @typedef {Object} FetchedThreads
* @property {Collection<Snowflake, ThreadChannel>} threads The threads that were fetched
* @property {Collection<Snowflake, ThreadMember>} members The thread members in the received threads
*/
/**
* Obtains all active thread channels in the guild.
* @param {boolean} [cache=true] Whether to cache the fetched data
* @returns {Promise<FetchedThreads>}
* @example

View file

@ -124,19 +124,19 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
/**
* Edits an emoji.
* @param {EmojiResolvable} emoji The Emoji resolvable to edit
* @param {GuildEmojiEditData} data The new data for the emoji
* @param {GuildEmojiEditOptions} options The options to provide
* @returns {Promise<GuildEmoji>}
*/
async edit(emoji, data) {
async edit(emoji, options) {
const id = this.resolveId(emoji);
if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'emoji', 'EmojiResolvable', true);
const roles = data.roles?.map(r => this.guild.roles.resolveId(r));
const roles = options.roles?.map(r => this.guild.roles.resolveId(r));
const newData = await this.client.rest.patch(Routes.guildEmoji(this.guild.id, id), {
body: {
name: data.name,
name: options.name,
roles,
},
reason: data.reason,
reason: options.reason,
});
const existing = this.cache.get(id);
if (existing) {
@ -161,8 +161,8 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
const { me } = this.guild.members;
if (!me) throw new DiscordjsError(ErrorCodes.GuildUncachedMe);
if (!me.permissions.has(PermissionFlagsBits.ManageEmojisAndStickers)) {
throw new DiscordjsError(ErrorCodes.MissingManageEmojisAndStickersPermission, this.guild);
if (!me.permissions.has(PermissionFlagsBits.ManageGuildExpressions)) {
throw new DiscordjsError(ErrorCodes.MissingManageGuildExpressionsPermission, this.guild);
}
const data = await this.client.rest.get(Routes.guildEmoji(this.guild.id, emoji.id));

View file

@ -18,8 +18,9 @@ class GuildForumThreadManager extends ThreadManager {
/**
* @typedef {BaseMessageOptions} GuildForumThreadMessageCreateOptions
* @property {stickers} [stickers] The stickers to send with the message
* @property {StickerResolvable} [stickers] The stickers to send with the message
* @property {BitFieldResolvable} [flags] The flags to send with the message
* <info>Only `MessageFlags.SuppressEmbeds` and `MessageFlags.SuppressNotifications` can be set.</info>
*/
/**

View file

@ -167,7 +167,7 @@ class GuildInviteManager extends CachedManager {
/**
* Create an invite to the guild from the provided channel.
* @param {GuildInvitableChannelResolvable} channel The options for creating the invite from a channel.
* @param {CreateInviteOptions} [options={}] The options for creating the invite from a channel.
* @param {InviteCreateOptions} [options={}] The options for creating the invite from a channel.
* @returns {Promise<Invite>}
* @example
* // Create an invite to a selected channel

View file

@ -57,14 +57,14 @@ class GuildManager extends CachedManager {
/**
* Partial data for a Role.
* @typedef {Object} PartialRoleData
* @property {Snowflake|number} [id] The role's id, used to set channel overrides,
* this is a placeholder and will be replaced by the API after consumption
* @property {Snowflake|number} [id] The role's id, used to set channel overrides.
* This is a placeholder and will be replaced by the API after consumption
* @property {string} [name] The name of the role
* @property {ColorResolvable} [color] The color of the role, either a hex string or a base 10 number
* @property {boolean} [hoist] Whether or not the role should be hoisted
* @property {boolean} [hoist] Whether the role should be hoisted
* @property {number} [position] The position of the role
* @property {PermissionResolvable} [permissions] The permissions of the role
* @property {boolean} [mentionable] Whether or not the role should be mentionable
* @property {boolean} [mentionable] Whether the role should be mentionable
*/
/**
@ -79,8 +79,8 @@ class GuildManager extends CachedManager {
/**
* Partial data for a Channel.
* @typedef {Object} PartialChannelData
* @property {Snowflake|number} [id] The channel's id, used to set its parent,
* this is a placeholder and will be replaced by the API after consumption
* @property {Snowflake|number} [id] The channel's id, used to set its parent.
* This is a placeholder and will be replaced by the API after consumption
* @property {Snowflake|number} [parentId] The parent id for this channel
* @property {ChannelType.GuildText|ChannelType.GuildVoice|ChannelType.GuildCategory} [type] The type of the channel
* @property {string} name The name of the channel
@ -141,18 +141,18 @@ class GuildManager extends CachedManager {
* Options used to create a guild.
* @typedef {Object} GuildCreateOptions
* @property {string} name The name of the guild
* @property {Snowflake|number} [afkChannelId] The AFK channel's id
* @property {number} [afkTimeout] The AFK timeout in seconds
* @property {PartialChannelData[]} [channels=[]] The channels for this guild
* @property {?(BufferResolvable|Base64Resolvable)} [icon=null] The icon for the guild
* @property {GuildVerificationLevel} [verificationLevel] The verification level for the guild
* @property {GuildDefaultMessageNotifications} [defaultMessageNotifications] The default message notifications
* for the guild
* @property {GuildExplicitContentFilter} [explicitContentFilter] The explicit content filter level for the guild
* @property {?(BufferResolvable|Base64Resolvable)} [icon=null] The icon for the guild
* @property {PartialRoleData[]} [roles=[]] The roles for this guild,
* @property {PartialChannelData[]} [channels=[]] The channels for this guild
* @property {Snowflake|number} [afkChannelId] The AFK channel's id
* @property {number} [afkTimeout] The AFK timeout in seconds
* the first element of this array is used to change properties of the guild's everyone role.
* @property {Snowflake|number} [systemChannelId] The system channel's id
* @property {SystemChannelFlagsResolvable} [systemChannelFlags] The flags of the system channel
* @property {GuildVerificationLevel} [verificationLevel] The verification level for the guild
*/
/* eslint-enable max-len */
@ -164,81 +164,80 @@ class GuildManager extends CachedManager {
*/
async create({
name,
afkChannelId,
afkTimeout,
channels = [],
icon = null,
verificationLevel,
defaultMessageNotifications,
explicitContentFilter,
icon = null,
roles = [],
channels = [],
afkChannelId,
afkTimeout,
systemChannelId,
systemChannelFlags,
verificationLevel,
}) {
icon = await DataResolver.resolveImage(icon);
for (const channel of channels) {
channel.parent_id = channel.parentId;
delete channel.parentId;
channel.user_limit = channel.userLimit;
delete channel.userLimit;
channel.rate_limit_per_user = channel.rateLimitPerUser;
delete channel.rateLimitPerUser;
channel.rtc_region = channel.rtcRegion;
delete channel.rtcRegion;
channel.video_quality_mode = channel.videoQualityMode;
delete channel.videoQualityMode;
if (!channel.permissionOverwrites) continue;
for (const overwrite of channel.permissionOverwrites) {
overwrite.allow &&= PermissionsBitField.resolve(overwrite.allow).toString();
overwrite.deny &&= PermissionsBitField.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();
}
systemChannelFlags &&= SystemChannelFlagsBitField.resolve(systemChannelFlags);
const data = await this.client.rest.post(Routes.guilds(), {
body: {
name,
icon,
icon: icon && (await DataResolver.resolveImage(icon)),
verification_level: verificationLevel,
default_message_notifications: defaultMessageNotifications,
explicit_content_filter: explicitContentFilter,
roles,
channels,
roles: roles.map(({ color, permissions, ...options }) => ({
...options,
color: color && resolveColor(color),
permissions: permissions === undefined ? undefined : PermissionsBitField.resolve(permissions).toString(),
})),
channels: channels.map(
({
parentId,
userLimit,
rtcRegion,
videoQualityMode,
permissionOverwrites,
rateLimitPerUser,
...options
}) => ({
...options,
parent_id: parentId,
user_limit: userLimit,
rtc_region: rtcRegion,
video_quality_mode: videoQualityMode,
permission_overwrites: permissionOverwrites?.map(({ allow, deny, ...permissionOverwriteOptions }) => ({
...permissionOverwriteOptions,
allow: allow === undefined ? undefined : PermissionsBitField.resolve(allow).toString(),
deny: deny === undefined ? undefined : PermissionsBitField.resolve(deny).toString(),
})),
rate_limit_per_user: rateLimitPerUser,
}),
),
afk_channel_id: afkChannelId,
afk_timeout: afkTimeout,
system_channel_id: systemChannelId,
system_channel_flags: systemChannelFlags,
system_channel_flags:
systemChannelFlags === undefined ? undefined : SystemChannelFlagsBitField.resolve(systemChannelFlags),
},
});
if (this.client.guilds.cache.has(data.id)) return this.client.guilds.cache.get(data.id);
return (
this.client.guilds.cache.get(data.id) ??
new Promise(resolve => {
const handleGuild = guild => {
if (guild.id === data.id) {
clearTimeout(timeout);
this.client.decrementMaxListeners();
resolve(guild);
}
};
this.client.incrementMaxListeners();
this.client.once(Events.GuildCreate, handleGuild);
return new Promise(resolve => {
const handleGuild = guild => {
if (guild.id === data.id) {
clearTimeout(timeout);
const timeout = setTimeout(() => {
this.client.removeListener(Events.GuildCreate, handleGuild);
this.client.decrementMaxListeners();
resolve(guild);
}
};
this.client.incrementMaxListeners();
this.client.on(Events.GuildCreate, handleGuild);
const timeout = setTimeout(() => {
this.client.removeListener(Events.GuildCreate, handleGuild);
this.client.decrementMaxListeners();
resolve(this.client.guilds._add(data));
}, 10_000).unref();
});
resolve(this.client.guilds._add(data));
}, 10_000).unref();
})
);
}
/**

View file

@ -11,6 +11,7 @@ const BaseGuildVoiceChannel = require('../structures/BaseGuildVoiceChannel');
const { GuildMember } = require('../structures/GuildMember');
const { Role } = require('../structures/Role');
const Events = require('../util/Events');
const { GuildMemberFlagsBitField } = require('../util/GuildMemberFlagsBitField');
const Partials = require('../util/Partials');
/**
@ -158,20 +159,18 @@ class GuildMemberManager extends CachedManager {
/**
* Options used to fetch multiple members from a guild.
* @typedef {Object} FetchMembersOptions
* @property {UserResolvable|UserResolvable[]} user The user(s) to fetch
* @property {?string} query Limit fetch to members with similar usernames
* @property {UserResolvable|UserResolvable[]} [user] The user(s) to fetch
* @property {?string} [query] Limit fetch to members with similar usernames
* @property {number} [limit=0] Maximum number of members to request
* @property {boolean} [withPresences=false] Whether or not to include the presences
* @property {boolean} [withPresences=false] Whether to include the presences
* @property {number} [time=120e3] Timeout for receipt of members
* @property {?string} nonce Nonce for this request (32 characters max - default to base 16 now timestamp)
* @property {boolean} [force=false] Whether to skip the cache check and request the API
* @property {?string} [nonce] Nonce for this request (32 characters max - default to base 16 now timestamp)
*/
/**
* Fetches member(s) from Discord, even if they're offline.
* @param {UserResolvable|FetchMemberOptions|FetchMembersOptions} [options] If a UserResolvable, the user to fetch.
* If undefined, fetches all members.
* If a query, it limits the results to users with similar usernames.
* Fetches member(s) from a guild.
* @param {UserResolvable|FetchMemberOptions|FetchMembersOptions} [options] Options for fetching member(s).
* Omitting the parameter or providing `undefined` will fetch all members.
* @returns {Promise<GuildMember|Collection<Snowflake, GuildMember>>}
* @example
* // Fetch all members from a guild
@ -206,18 +205,70 @@ class GuildMemberManager extends CachedManager {
*/
fetch(options) {
if (!options) return this._fetchMany();
const user = this.client.users.resolveId(options);
if (user) return this._fetchSingle({ user, cache: true });
if (options.user) {
if (Array.isArray(options.user)) {
options.user = options.user.map(u => this.client.users.resolveId(u));
return this._fetchMany(options);
} else {
options.user = this.client.users.resolveId(options.user);
}
if (!options.limit && !options.withPresences) return this._fetchSingle(options);
const { user: users, limit, withPresences, cache, force } = options;
const resolvedUser = this.client.users.resolveId(users ?? options);
if (resolvedUser && !limit && !withPresences) return this._fetchSingle({ user: resolvedUser, cache, force });
const resolvedUsers = users?.map?.(user => this.client.users.resolveId(user)) ?? resolvedUser ?? undefined;
return this._fetchMany({ ...options, users: resolvedUsers });
}
async _fetchSingle({ user, cache, force = false }) {
if (!force) {
const existing = this.cache.get(user);
if (existing && !existing.partial) return existing;
}
return this._fetchMany(options);
const data = await this.client.rest.get(Routes.guildMember(this.guild.id, user));
return this._add(data, cache);
}
_fetchMany({
limit = 0,
withPresences: presences,
users,
query,
time = 120e3,
nonce = DiscordSnowflake.generate().toString(),
} = {}) {
if (nonce.length > 32) return Promise.reject(new DiscordjsRangeError(ErrorCodes.MemberFetchNonceLength));
return new Promise((resolve, reject) => {
if (!query && !users) query = '';
this.guild.shard.send({
op: GatewayOpcodes.RequestGuildMembers,
d: {
guild_id: this.guild.id,
presences,
user_ids: users,
query,
nonce,
limit,
},
});
const fetchedMembers = new Collection();
let i = 0;
const handler = (members, _, chunk) => {
if (chunk.nonce !== nonce) return;
timeout.refresh();
i++;
for (const member of members.values()) {
fetchedMembers.set(member.id, member);
}
if (members.size < 1_000 || (limit && fetchedMembers.size >= limit) || i === chunk.count) {
clearTimeout(timeout);
this.client.removeListener(Events.GuildMembersChunk, handler);
this.client.decrementMaxListeners();
resolve(users && !Array.isArray(users) && fetchedMembers.size ? fetchedMembers.first() : fetchedMembers);
}
};
const timeout = setTimeout(() => {
this.client.removeListener(Events.GuildMembersChunk, handler);
this.client.decrementMaxListeners();
reject(new DiscordjsError(ErrorCodes.GuildMembersTimeout));
}, time).unref();
this.client.incrementMaxListeners();
this.client.on(Events.GuildMembersChunk, handler);
});
}
/**
@ -270,7 +321,7 @@ class GuildMemberManager extends CachedManager {
/**
* The data for editing a guild member.
* @typedef {Object} GuildMemberEditData
* @typedef {Object} GuildMemberEditOptions
* @property {?string} [nick] The nickname to set for the member
* @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] The roles or role ids to apply
* @property {boolean} [mute] Whether or not the member should be muted
@ -279,6 +330,7 @@ class GuildMemberManager extends CachedManager {
* (if they are connected to voice), or `null` if you want to disconnect them from voice
* @property {DateResolvable|null} [communicationDisabledUntil] The date or timestamp
* for the member's communication to be disabled until. Provide `null` to enable communication again.
* @property {GuildMemberFlagsResolvable} [flags] The flags to set for the member
* @property {string} [reason] Reason for editing this user
*/
@ -286,43 +338,47 @@ class GuildMemberManager extends CachedManager {
* Edits a member of the guild.
* <info>The user must be a member of the guild</info>
* @param {UserResolvable} user The member to edit
* @param {GuildMemberEditData} data The data to edit the member with
* @param {GuildMemberEditOptions} options The options to provide
* @returns {Promise<GuildMember>}
*/
async edit(user, { reason, ...data }) {
async edit(user, { reason, ...options }) {
const id = this.client.users.resolveId(user);
if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'user', 'UserResolvable');
if (data.channel) {
data.channel = this.guild.channels.resolve(data.channel);
if (!(data.channel instanceof BaseGuildVoiceChannel)) {
if (options.channel) {
options.channel = this.guild.channels.resolve(options.channel);
if (!(options.channel instanceof BaseGuildVoiceChannel)) {
throw new DiscordjsError(ErrorCodes.GuildVoiceChannelResolve);
}
data.channel_id = data.channel.id;
data.channel = undefined;
} else if (data.channel === null) {
data.channel_id = null;
data.channel = undefined;
options.channel_id = options.channel.id;
options.channel = undefined;
} else if (options.channel === null) {
options.channel_id = null;
options.channel = undefined;
}
data.roles &&= data.roles.map(role => (role instanceof Role ? role.id : role));
options.roles &&= options.roles.map(role => (role instanceof Role ? role.id : role));
if (typeof data.communicationDisabledUntil !== 'undefined') {
data.communication_disabled_until =
if (options.communicationDisabledUntil !== undefined) {
options.communication_disabled_until =
// eslint-disable-next-line eqeqeq
data.communicationDisabledUntil != null
? new Date(data.communicationDisabledUntil).toISOString()
: data.communicationDisabledUntil;
options.communicationDisabledUntil != null
? new Date(options.communicationDisabledUntil).toISOString()
: options.communicationDisabledUntil;
}
if (options.flags !== undefined) {
options.flags = GuildMemberFlagsBitField.resolve(options.flags);
}
let endpoint;
if (id === this.client.user.id) {
const keys = Object.keys(data);
const keys = Object.keys(options);
if (keys.length === 1 && keys[0] === 'nick') endpoint = Routes.guildMember(this.guild.id);
else endpoint = Routes.guildMember(this.guild.id, id);
} else {
endpoint = Routes.guildMember(this.guild.id, id);
}
const d = await this.client.rest.patch(endpoint, { body: data, reason });
const d = await this.client.rest.patch(endpoint, { body: options, reason });
const clone = this.cache.get(id)?._clone();
clone?._patch(d);
@ -479,66 +535,6 @@ class GuildMemberManager extends CachedManager {
return this.resolve(user) ?? this.client.users.resolve(user) ?? userId;
}
async _fetchSingle({ user, cache, force = false }) {
if (!force) {
const existing = this.cache.get(user);
if (existing && !existing.partial) return existing;
}
const data = await this.client.rest.get(Routes.guildMember(this.guild.id, user));
return this._add(data, cache);
}
_fetchMany({
limit = 0,
withPresences: presences = false,
user: user_ids,
query,
time = 120e3,
nonce = DiscordSnowflake.generate().toString(),
} = {}) {
return new Promise((resolve, reject) => {
if (!query && !user_ids) query = '';
if (nonce.length > 32) throw new DiscordjsRangeError(ErrorCodes.MemberFetchNonceLength);
this.guild.shard.send({
op: GatewayOpcodes.RequestGuildMembers,
d: {
guild_id: this.guild.id,
presences,
user_ids,
query,
nonce,
limit,
},
});
const fetchedMembers = new Collection();
let i = 0;
const handler = (members, _, chunk) => {
timeout.refresh();
if (chunk.nonce !== nonce) return;
i++;
for (const member of members.values()) {
fetchedMembers.set(member.id, member);
}
if (members.size < 1_000 || (limit && fetchedMembers.size >= limit) || i === chunk.count) {
clearTimeout(timeout);
this.client.removeListener(Events.GuildMembersChunk, handler);
this.client.decrementMaxListeners();
let fetched = fetchedMembers;
if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first();
resolve(fetched);
}
};
const timeout = setTimeout(() => {
this.client.removeListener(Events.GuildMembersChunk, handler);
this.client.decrementMaxListeners();
reject(new DiscordjsError(ErrorCodes.GuildMembersTimeout));
}, time).unref();
this.client.incrementMaxListeners();
this.client.on(Events.GuildMembersChunk, handler);
});
}
}
module.exports = GuildMemberManager;

View file

@ -85,12 +85,12 @@ class GuildScheduledEventManager extends CachedManager {
let entity_metadata, channel_id;
if (entityType === GuildScheduledEventEntityType.External) {
channel_id = typeof channel === 'undefined' ? channel : null;
channel_id = channel === undefined ? channel : null;
entity_metadata = { location: entityMetadata?.location };
} else {
channel_id = this.guild.channels.resolveId(channel);
if (!channel_id) throw new DiscordjsError(ErrorCodes.GuildVoiceChannelResolve);
entity_metadata = typeof entityMetadata === 'undefined' ? entityMetadata : null;
entity_metadata = entityMetadata === undefined ? entityMetadata : null;
}
const data = await this.client.rest.post(Routes.guildScheduledEvents(this.guild.id), {
@ -214,7 +214,7 @@ class GuildScheduledEventManager extends CachedManager {
const data = await this.client.rest.patch(Routes.guildScheduledEvent(this.guild.id, guildScheduledEventId), {
body: {
channel_id: typeof channel === 'undefined' ? channel : this.guild.channels.resolveId(channel),
channel_id: channel === undefined ? channel : this.guild.channels.resolveId(channel),
name,
privacy_level: privacyLevel,
scheduled_start_time: scheduledStartTime ? new Date(scheduledStartTime).toISOString() : undefined,

View file

@ -35,7 +35,7 @@ class GuildStickerManager extends CachedManager {
/**
* Options used to create a guild sticker.
* @typedef {Object} GuildStickerCreateOptions
* @property {BufferResolvable|Stream|JSONEncodable<AttachmentPayload>} file The file for the sticker
* @property {AttachmentPayload|BufferResolvable|Stream} file The file for the sticker
* @property {string} name The name for the sticker
* @property {string} tags The Discord name of a unicode emoji representing the sticker's expression
* @property {?string} [description] The description for the sticker
@ -101,16 +101,16 @@ class GuildStickerManager extends CachedManager {
/**
* Edits a sticker.
* @param {StickerResolvable} sticker The sticker to edit
* @param {GuildStickerEditData} [data={}] The new data for the sticker
* @param {GuildStickerEditOptions} [options={}] The new data for the sticker
* @returns {Promise<Sticker>}
*/
async edit(sticker, data = {}) {
async edit(sticker, options = {}) {
const stickerId = this.resolveId(sticker);
if (!stickerId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'sticker', 'StickerResolvable');
const d = await this.client.rest.patch(Routes.guildSticker(this.guild.id, stickerId), {
body: data,
reason: data.reason,
body: options,
reason: options.reason,
});
const existing = this.cache.get(stickerId);

View file

@ -49,6 +49,7 @@ class MessageManager extends CachedManager {
/**
* Options used to fetch multiple messages.
* <info>The `before`, `after`, and `around` parameters are mutually exclusive.</info>
* @typedef {Object} FetchMessagesOptions
* @property {number} [limit] The maximum number of messages to return
* @property {Snowflake} [before] Consider only messages before this id
@ -150,7 +151,7 @@ class MessageManager extends CachedManager {
/**
* Options that can be passed to edit a message.
* @typedef {BaseMessageOptions} MessageEditOptions
* @property {Array<JSONEncodable<AttachmentPayload>>} [attachments] An array of attachments to keep,
* @property {AttachmentPayload[]} [attachments] An array of attachments to keep,
* all attachments will be kept if omitted
* @property {MessageFlags} [flags] Which flags to set for the message
* <info>Only the {@link MessageFlags.SuppressEmbeds} flag can be modified.</info>

View file

@ -33,6 +33,7 @@ class ReactionManager extends CachedManager {
* Data that can be resolved to a MessageReaction object. This can be:
* * A MessageReaction
* * A Snowflake
* * The Unicode representation of an emoji
* @typedef {MessageReaction|Snowflake} MessageReactionResolvable
*/

View file

@ -100,7 +100,7 @@ class RoleManager extends CachedManager {
/**
* Options used to create a new role.
* @typedef {Object} CreateRoleOptions
* @typedef {Object} RoleCreateOptions
* @property {string} [name] The name of the new role
* @property {ColorResolvable} [color] The data to create the role with
* @property {boolean} [hoist] Whether or not the new role should be hoisted
@ -117,7 +117,7 @@ class RoleManager extends CachedManager {
/**
* Creates a new role in the guild with given information.
* <warn>The position will silently reset to 1 if an invalid one is provided, or none.</warn>
* @param {CreateRoleOptions} [options] Options for creating the new role
* @param {RoleCreateOptions} [options] Options for creating the new role
* @returns {Promise<Role>}
* @example
* // Create a new role
@ -137,7 +137,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 (permissions !== undefined) permissions = new PermissionsBitField(permissions);
if (icon) {
const guildEmojiURL = this.guild.emojis.resolve(icon)?.url;
icon = guildEmojiURL ? await DataResolver.resolveImage(guildEmojiURL) : await DataResolver.resolveImage(icon);
@ -166,14 +166,14 @@ class RoleManager extends CachedManager {
/**
* Options for editing a role
* @typedef {RoleData} EditRoleOptions
* @typedef {RoleData} RoleEditOptions
* @property {string} [reason] The reason for editing this role
*/
/**
* Edits a role of the guild.
* @param {RoleResolvable} role The role to edit
* @param {EditRoleOptions} data The new data for the role
* @param {RoleEditOptions} options The options to provide
* @returns {Promise<Role>}
* @example
* // Edit a role
@ -181,15 +181,15 @@ class RoleManager extends CachedManager {
* .then(updated => console.log(`Edited role name to ${updated.name}`))
* .catch(console.error);
*/
async edit(role, data) {
async edit(role, options) {
role = this.resolve(role);
if (!role) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'role', 'RoleResolvable');
if (typeof data.position === 'number') {
await this.setPosition(role, data.position, { reason: data.reason });
if (typeof options.position === 'number') {
await this.setPosition(role, options.position, { reason: options.reason });
}
let icon = data.icon;
let icon = options.icon;
if (icon) {
const guildEmojiURL = this.guild.emojis.resolve(icon)?.url;
icon = guildEmojiURL ? await DataResolver.resolveImage(guildEmojiURL) : await DataResolver.resolveImage(icon);
@ -197,16 +197,16 @@ class RoleManager extends CachedManager {
}
const body = {
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),
mentionable: data.mentionable,
name: options.name,
color: options.color === undefined ? undefined : resolveColor(options.color),
hoist: options.hoist,
permissions: options.permissions === undefined ? undefined : new PermissionsBitField(options.permissions),
mentionable: options.mentionable,
icon,
unicode_emoji: data.unicodeEmoji,
unicode_emoji: options.unicodeEmoji,
};
const d = await this.client.rest.patch(Routes.guildRole(this.guild.id, role.id), { body, reason: data.reason });
const d = await this.client.rest.patch(Routes.guildRole(this.guild.id, role.id), { body, reason: options.reason });
const clone = role._clone();
clone._patch(d);
@ -307,11 +307,14 @@ class RoleManager extends CachedManager {
throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'role', 'Role nor a Snowflake');
}
if (resolvedRole1.position === resolvedRole2.position) {
const role1Position = resolvedRole1.position;
const role2Position = resolvedRole2.position;
if (role1Position === role2Position) {
return Number(BigInt(resolvedRole2.id) - BigInt(resolvedRole1.id));
}
return resolvedRole1.position - resolvedRole2.position;
return role1Position - role2Position;
}
/**

View file

@ -75,10 +75,9 @@ class ThreadManager extends CachedManager {
*/
/**
* The options for fetching multiple threads, the properties are mutually exclusive
* Options for fetching multiple threads.
* @typedef {Object} FetchThreadsOptions
* @property {FetchArchivedThreadOptions} [archived] The options used to fetch archived threads
* @property {boolean} [active] When true, fetches active threads. <warn>If `archived` is set, this is ignored!</warn>
* @property {FetchArchivedThreadOptions} [archived] Options used to fetch archived threads
*/
/**
@ -87,17 +86,18 @@ class ThreadManager extends CachedManager {
* ThreadChannelResolvable then the specified thread will be fetched. Fetches all active threads if `undefined`
* @param {BaseFetchOptions} [cacheOptions] Additional options for this fetch. <warn>The `force` field gets ignored
* if `options` is not a {@link ThreadChannelResolvable}</warn>
* @returns {Promise<?(ThreadChannel|FetchedThreads)>}
* @returns {Promise<?(ThreadChannel|FetchedThreads|FetchedThreadsMore)>}
* {@link FetchedThreads} if active & {@link FetchedThreadsMore} if archived.
* @example
* // Fetch a thread by its id
* channel.threads.fetch('831955138126104859')
* .then(channel => console.log(channel.name))
* .catch(console.error);
*/
fetch(options, { cache = true, force = false } = {}) {
fetch(options, { cache, force } = {}) {
if (!options) return this.fetchActive(cache);
const channel = this.client.channels.resolveId(options);
if (channel) return this.client.channels.fetch(channel, cache, force);
if (channel) return this.client.channels.fetch(channel, { cache, force });
if (options.archived) {
return this.fetchArchived(options.archived, cache);
}
@ -118,16 +118,15 @@ class ThreadManager extends CachedManager {
* @property {string} [type='public'] The type of threads to fetch (`public` or `private`)
* @property {boolean} [fetchAll=false] Whether to fetch **all** archived threads when `type` is `private`
* <info>This property requires the {@link PermissionFlagsBits.ManageThreads} permission if `true`.</info>
* @property {DateResolvable|ThreadChannelResolvable} [before] Only return threads that were created before this Date
* @property {DateResolvable|ThreadChannelResolvable} [before] Only return threads that were archived before this Date
* or Snowflake
* <warn>Must be a {@link ThreadChannelResolvable} when `type` is `private` and `fetchAll` is `false`.</warn>
* @property {number} [limit] Maximum number of threads to return
*/
/**
* The data returned from a thread fetch that returns multiple threads.
* @typedef {Object} FetchedThreads
* @property {Collection<Snowflake, ThreadChannel>} threads The threads that were fetched, with any members returned
* Data returned from fetching multiple threads.
* @typedef {FetchedThreads} FetchedThreadsMore
* @property {?boolean} hasMore Whether there are potentially additional threads that require a subsequent call
*/
@ -137,7 +136,7 @@ class ThreadManager extends CachedManager {
* in the parent channel.</info>
* @param {FetchArchivedThreadOptions} [options] The options to fetch archived threads
* @param {boolean} [cache=true] Whether to cache the new thread objects if they aren't already
* @returns {Promise<FetchedThreads>}
* @returns {Promise<FetchedThreadsMore>}
*/
async fetchArchived({ type = 'public', fetchAll = false, before, limit } = {}, cache = true) {
let path = Routes.channelThreads(this.channel.id, type);
@ -147,8 +146,8 @@ class ThreadManager extends CachedManager {
let timestamp;
let id;
const query = makeURLSearchParams({ limit });
if (typeof before !== 'undefined') {
if (before instanceof ThreadChannel || /^\d{16,19}$/.test(String(before))) {
if (before !== undefined) {
if (before instanceof ThreadChannel || /^\d{17,19}$/.test(String(before))) {
id = this.resolveId(before);
timestamp = this.resolve(before)?.archivedAt?.toISOString();
const toUse = type === 'private' && !fetchAll ? id : timestamp;
@ -172,15 +171,13 @@ class ThreadManager extends CachedManager {
}
/**
* Obtains the accessible active threads from Discord.
* <info>This method requires the {@link PermissionFlagsBits.ReadMessageHistory} permission
* in the parent channel.</info>
* @param {boolean} [cache=true] Whether to cache the new thread objects if they aren't already
* Obtains all active thread channels in the guild.
* This internally calls {@link GuildChannelManager#fetchActiveThreads}.
* @param {boolean} [cache=true] Whether to cache the fetched data
* @returns {Promise<FetchedThreads>}
*/
async fetchActive(cache = true) {
const raw = await this.client.rest.get(Routes.guildActiveThreads(this.channel.guild.id));
return this.constructor._mapThreads(raw, this.client, { parent: this.channel, cache });
fetchActive(cache = true) {
return this.channel.guild.channels.fetchActiveThreads(cache);
}
static _mapThreads(rawThreads, client, { parent, guild, cache }) {
@ -189,12 +186,18 @@ class ThreadManager extends CachedManager {
if (parent && thread.parentId !== parent.id) return coll;
return coll.set(thread.id, thread);
}, new Collection());
// Discord sends the thread id as id in this object
for (const rawMember of rawThreads.members) client.channels.cache.get(rawMember.id)?.members._add(rawMember);
return {
threads,
hasMore: rawThreads.has_more ?? false,
};
const threadMembers = rawThreads.members.reduce(
(coll, raw) => coll.set(raw.user_id, threads.get(raw.id).members._add(raw)),
new Collection(),
);
const response = { threads, members: threadMembers };
// The GET `/guilds/{guild.id}/threads/active` route does not return `has_more`.
if ('has_more' in rawThreads) response.hasMore = rawThreads.has_more;
return response;
}
}

View file

@ -1,6 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v10');
const CachedManager = require('./CachedManager');
const { DiscordjsTypeError, ErrorCodes } = require('../errors');
@ -29,10 +30,10 @@ class ThreadMemberManager extends CachedManager {
_add(data, cache = true) {
const existing = this.cache.get(data.user_id);
if (cache) existing?._patch(data);
if (cache) existing?._patch(data, { cache });
if (existing) return existing;
const member = new ThreadMember(this.thread, data);
const member = new ThreadMember(this.thread, data, { cache });
if (cache) this.cache.set(data.user_id, member);
return member;
}
@ -112,15 +113,35 @@ class ThreadMemberManager extends CachedManager {
}
/**
* Options used to fetch a thread member.
* @typedef {BaseFetchOptions} FetchThreadMemberOptions
* @property {ThreadMemberResolvable} member The thread member to fetch
* @property {boolean} [withMember] Whether to also return the guild member associated with this thread member
*/
/**
* @typedef {Object} FetchThreadMembersOptions
* Options used to fetch multiple thread members with guild member data.
* <info>With `withMember` set to `true`, pagination is enabled.</info>
* @typedef {Object} FetchThreadMembersWithGuildMemberDataOptions
* @property {true} withMember Whether to also return the guild member data
* @property {Snowflake} [after] Consider only thread members after this id
* @property {number} [limit] The maximum number of thread members to return
* @property {boolean} [cache] Whether to cache the fetched thread members and guild members
*/
/**
* Options used to fetch multiple thread members without guild member data.
* @typedef {Object} FetchThreadMembersWithoutGuildMemberDataOptions
* @property {false} [withMember] Whether to also return the guild member data
* @property {boolean} [cache] Whether to cache the fetched thread members
*/
/**
* Options used to fetch multiple thread members.
* @typedef {FetchThreadMembersWithGuildMemberDataOptions|
* FetchThreadMembersWithoutGuildMemberDataOptions} FetchThreadMembersOptions
*/
/**
* Fetches thread member(s) from Discord.
* <info>This method requires the {@link GatewayIntentBits.GuildMembers} privileged gateway intent.</info>
@ -130,25 +151,31 @@ class ThreadMemberManager extends CachedManager {
*/
fetch(options) {
if (!options) return this._fetchMany();
const { member, cache, force } = options;
const { member, withMember, cache, force } = options;
const resolvedMember = this.resolveId(member ?? options);
if (resolvedMember) return this._fetchSingle({ member: resolvedMember, cache, force });
if (resolvedMember) return this._fetchSingle({ member: resolvedMember, withMember, cache, force });
return this._fetchMany(options);
}
async _fetchSingle({ member, cache, force = false }) {
async _fetchSingle({ member, withMember, cache, force = false }) {
if (!force) {
const existing = this.cache.get(member);
if (existing) return existing;
}
const data = await this.client.rest.get(Routes.threadMembers(this.thread.id, member));
const data = await this.client.rest.get(Routes.threadMembers(this.thread.id, member), {
query: makeURLSearchParams({ with_member: withMember }),
});
return this._add(data, cache);
}
async _fetchMany(options = {}) {
const data = await this.client.rest.get(Routes.threadMembers(this.thread.id));
return data.reduce((col, member) => col.set(member.user_id, this._add(member, options.cache)), new Collection());
async _fetchMany({ withMember, after, limit, cache } = {}) {
const data = await this.client.rest.get(Routes.threadMembers(this.thread.id), {
query: makeURLSearchParams({ with_member: withMember, after, limit }),
});
return data.reduce((col, member) => col.set(member.user_id, this._add(member, cache)), new Collection());
}
}

View file

@ -21,8 +21,14 @@ class Shard extends EventEmitter {
constructor(manager, id) {
super();
if (manager.mode === 'process') childProcess = require('node:child_process');
else if (manager.mode === 'worker') Worker = require('node:worker_threads').Worker;
switch (manager.mode) {
case 'process':
childProcess = require('node:child_process');
break;
case 'worker':
Worker = require('node:worker_threads').Worker;
break;
}
/**
* Manager that created the shard
@ -112,18 +118,21 @@ class Shard extends EventEmitter {
this._exitListener = this._handleExit.bind(this, undefined, timeout);
if (this.manager.mode === 'process') {
this.process = childProcess
.fork(path.resolve(this.manager.file), this.args, {
env: this.env,
execArgv: this.execArgv,
})
.on('message', this._handleMessage.bind(this))
.on('exit', this._exitListener);
} else if (this.manager.mode === 'worker') {
this.worker = new Worker(path.resolve(this.manager.file), { workerData: this.env })
.on('message', this._handleMessage.bind(this))
.on('exit', this._exitListener);
switch (this.manager.mode) {
case 'process':
this.process = childProcess
.fork(path.resolve(this.manager.file), this.args, {
env: this.env,
execArgv: this.execArgv,
})
.on('message', this._handleMessage.bind(this))
.on('exit', this._exitListener);
break;
case 'worker':
this.worker = new Worker(path.resolve(this.manager.file), { workerData: this.env })
.on('message', this._handleMessage.bind(this))
.on('exit', this._exitListener);
break;
}
this._evals.clear();

View file

@ -1,6 +1,7 @@
'use strict';
const process = require('node:process');
const { calculateShardId } = require('@discordjs/util');
const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors');
const Events = require('../util/Events');
const { makeError, makePlainError } = require('../util/Util');
@ -29,29 +30,32 @@ class ShardClientUtil {
*/
this.parentPort = null;
if (mode === 'process') {
process.on('message', this._handleMessage.bind(this));
client.on('ready', () => {
process.send({ _ready: true });
});
client.on('disconnect', () => {
process.send({ _disconnect: true });
});
client.on('reconnecting', () => {
process.send({ _reconnecting: true });
});
} else if (mode === 'worker') {
this.parentPort = require('node:worker_threads').parentPort;
this.parentPort.on('message', this._handleMessage.bind(this));
client.on('ready', () => {
this.parentPort.postMessage({ _ready: true });
});
client.on('disconnect', () => {
this.parentPort.postMessage({ _disconnect: true });
});
client.on('reconnecting', () => {
this.parentPort.postMessage({ _reconnecting: true });
});
switch (mode) {
case 'process':
process.on('message', this._handleMessage.bind(this));
client.on(Events.ShardReady, () => {
process.send({ _ready: true });
});
client.on(Events.ShardDisconnect, () => {
process.send({ _disconnect: true });
});
client.on(Events.ShardReconnecting, () => {
process.send({ _reconnecting: true });
});
break;
case 'worker':
this.parentPort = require('node:worker_threads').parentPort;
this.parentPort.on('message', this._handleMessage.bind(this));
client.on(Events.ShardReady, () => {
this.parentPort.postMessage({ _ready: true });
});
client.on(Events.ShardDisconnect, () => {
this.parentPort.postMessage({ _disconnect: true });
});
client.on(Events.ShardReconnecting, () => {
this.parentPort.postMessage({ _reconnecting: true });
});
break;
}
}
@ -81,14 +85,17 @@ class ShardClientUtil {
*/
send(message) {
return new Promise((resolve, reject) => {
if (this.mode === 'process') {
process.send(message, err => {
if (err) reject(err);
else resolve();
});
} else if (this.mode === 'worker') {
this.parentPort.postMessage(message);
resolve();
switch (this.mode) {
case 'process':
process.send(message, err => {
if (err) reject(err);
else resolve();
});
break;
case 'worker':
this.parentPort.postMessage(message);
resolve();
break;
}
});
}
@ -245,7 +252,7 @@ class ShardClientUtil {
* @returns {number}
*/
static shardIdForGuildId(guildId, shardCount) {
const shard = Number(BigInt(guildId) >> 22n) % shardCount;
const shard = calculateShardId(guildId, shardCount);
if (shard < 0) throw new DiscordjsError(ErrorCodes.ShardingShardMiscalculation, shard, guildId, shardCount);
return shard;
}

View file

@ -1,7 +1,7 @@
'use strict';
const { deprecate } = require('node:util');
const { isJSONEncodable } = require('@discordjs/builders');
const { isJSONEncodable } = require('@discordjs/util');
const Component = require('./Component');
const { createComponent } = require('../util/Components');
@ -23,16 +23,16 @@ class ActionRow extends Component {
/**
* Creates a new action row builder from JSON data
* @param {JSONEncodable<APIActionRowComponent>|APIActionRowComponent} other The other data
* @method from
* @memberof ActionRow
* @param {ActionRowBuilder|ActionRow|APIActionRowComponent} other The other data
* @returns {ActionRowBuilder}
* @deprecated Use {@link ActionRowBuilder.from()} instead.
* @deprecated Use {@link ActionRowBuilder.from} instead.
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
}
static from = deprecate(
other => new this(isJSONEncodable(other) ? other.toJSON() : other),
'ActionRow.from() is deprecated. Use ActionRowBuilder.from() instead.',
);
/**
* Returns the API-compatible JSON for this component
@ -43,6 +43,4 @@ class ActionRow extends Component {
}
}
ActionRow.from = deprecate(ActionRow.from, 'ActionRow.from() is deprecated. Use ActionRowBuilder.from() instead.');
module.exports = ActionRow;

View file

@ -1,6 +1,7 @@
'use strict';
const { ActionRowBuilder: BuildersActionRow, isJSONEncodable } = require('@discordjs/builders');
const { ActionRowBuilder: BuildersActionRow } = require('@discordjs/builders');
const { isJSONEncodable } = require('@discordjs/util');
const { createComponentBuilder } = require('../util/Components');
const { toSnakeCase } = require('../util/Transformers');
@ -18,15 +19,11 @@ class ActionRowBuilder extends BuildersActionRow {
/**
* Creates a new action row builder from JSON data
* @param {JSONEncodable<APIActionRowComponent<APIActionRowComponentTypes>>
* |APIActionRowComponent<APIActionRowComponentTypes>} other The other data
* @param {ActionRow|ActionRowBuilder|APIActionRowComponent} other The other data
* @returns {ActionRowBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
return new this(isJSONEncodable(other) ? other.toJSON() : other);
}
}
@ -34,5 +31,5 @@ module.exports = ActionRowBuilder;
/**
* @external BuildersActionRow
* @see {@link https://discord.js.org/#/docs/builders/main/class/ActionRowBuilder}
* @see {@link https://discord.js.org/docs/packages/builders/stable/ActionRowBuilder:Class}
*/

View file

@ -52,6 +52,12 @@ class ApplicationCommand extends Base {
*/
this.type = data.type;
/**
* Whether this command is age-restricted (18+)
* @type {boolean}
*/
this.nsfw = data.nsfw ?? false;
this._patch(data);
}
@ -188,6 +194,7 @@ class ApplicationCommand extends Base {
* {@link ApplicationCommandType.ChatInput}
* @property {Object<Locale, string>} [nameLocalizations] The localizations for the command name
* @property {string} description The description of the command, if type is {@link ApplicationCommandType.ChatInput}
* @property {boolean} [nsfw] Whether the command is age-restricted
* @property {Object<Locale, string>} [descriptionLocalizations] The localizations for the command description,
* if type is {@link ApplicationCommandType.ChatInput}
* @property {ApplicationCommandType} [type=ApplicationCommandType.ChatInput] The type of the command
@ -377,11 +384,12 @@ class ApplicationCommand extends Base {
('description' in command && command.description !== this.description) ||
('version' in command && command.version !== this.version) ||
(command.type && command.type !== this.type) ||
('nsfw' in command && command.nsfw !== this.nsfw) ||
// Future proof for options being nullable
// TODO: remove ?? 0 on each when nullable
(command.options?.length ?? 0) !== (this.options?.length ?? 0) ||
defaultMemberPermissions !== (this.defaultMemberPermissions?.bitfield ?? null) ||
(typeof dmPermission !== 'undefined' && dmPermission !== this.dmPermission) ||
(dmPermission !== undefined && dmPermission !== this.dmPermission) ||
!isEqual(command.nameLocalizations ?? command.name_localizations ?? {}, this.nameLocalizations ?? {}) ||
!isEqual(
command.descriptionLocalizations ?? command.description_localizations ?? {},
@ -510,7 +518,7 @@ class ApplicationCommand extends Base {
* {@link ApplicationCommandOptionType.Number} 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,
* @property {ApplicationCommandOptionAllowedChannelTypes[]} [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
@ -591,3 +599,8 @@ module.exports = ApplicationCommand;
* @external APIApplicationCommandOption
* @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure}
*/
/**
* @external ApplicationCommandOptionAllowedChannelTypes
* @see {@link https://discord.js.org/docs/packages/builders/stable/ApplicationCommandOptionAllowedChannelTypes:TypeAlias}
*/

View file

@ -0,0 +1,46 @@
'use strict';
/**
* Role connection metadata object for an application.
*/
class ApplicationRoleConnectionMetadata {
constructor(data) {
/**
* The name of this metadata field
* @type {string}
*/
this.name = data.name;
/**
* The name localizations for this metadata field
* @type {?Object<Locale, string>}
*/
this.nameLocalizations = data.name_localizations ?? null;
/**
* The description of this metadata field
* @type {string}
*/
this.description = data.description;
/**
* The description localizations for this metadata field
* @type {?Object<Locale, string>}
*/
this.descriptionLocalizations = data.description_localizations ?? null;
/**
* The dictionary key for this metadata field
* @type {string}
*/
this.key = data.key;
/**
* The type of this metadata field
* @type {ApplicationRoleConnectionMetadataType}
*/
this.type = data.type;
}
}
exports.ApplicationRoleConnectionMetadata = ApplicationRoleConnectionMetadata;

View file

@ -99,6 +99,28 @@ class Attachment {
* @type {boolean}
*/
this.ephemeral = data.ephemeral ?? false;
if ('duration_secs' in data) {
/**
* The duration of this attachment in seconds
* <info>This will only be available if the attachment is an audio file.</info>
* @type {?number}
*/
this.duration = data.duration_secs;
} else {
this.duration ??= null;
}
if ('waveform' in data) {
/**
* The base64 encoded byte array representing a sampled waveform
* <info>This will only be available if the attachment is an audio file.</info>
* @type {?string}
*/
this.waveform = data.waveform;
} else {
this.waveform ??= null;
}
}
/**

View file

@ -91,7 +91,7 @@ class AttachmentBuilder {
/**
* Makes a new builder instance from a preexisting attachment structure.
* @param {JSONEncodable<AttachmentPayload>} other The builder to construct a new instance from
* @param {AttachmentBuilder|Attachment|AttachmentPayload} other The builder to construct a new instance from
* @returns {AttachmentBuilder}
*/
static from(other) {

View file

@ -1,5 +1,7 @@
'use strict';
const { _transformAPIAutoModerationAction } = require('../util/Transformers');
/**
* Represents the structure of an executed action when an {@link AutoModerationRule} is triggered.
*/
@ -15,7 +17,7 @@ class AutoModerationActionExecution {
* The action that was executed.
* @type {AutoModerationAction}
*/
this.action = data.action;
this.action = _transformAPIAutoModerationAction(data.action);
/**
* The id of the auto moderation rule this action belongs to.
@ -43,8 +45,8 @@ class AutoModerationActionExecution {
/**
* The id of the message that triggered this action.
* @type {?Snowflake}
* <info>This will not be present if the message was blocked or the content was not part of any message.</info>
* @type {?Snowflake}
*/
this.messageId = data.message_id ?? null;
@ -82,6 +84,33 @@ class AutoModerationActionExecution {
get autoModerationRule() {
return this.guild.autoModerationRules.cache.get(this.ruleId) ?? null;
}
/**
* The channel where this action was triggered from.
* @type {?TextBasedChannel}
* @readonly
*/
get channel() {
return this.guild.channels.cache.get(this.channelId) ?? null;
}
/**
* The user that triggered this action.
* @type {?User}
* @readonly
*/
get user() {
return this.guild.client.users.cache.get(this.userId) ?? null;
}
/**
* The guild member that triggered this action.
* @type {?GuildMember}
* @readonly
*/
get member() {
return this.guild.members.cache.get(this.userId) ?? null;
}
}
module.exports = AutoModerationActionExecution;

View file

@ -2,6 +2,7 @@
const { Collection } = require('@discordjs/collection');
const Base = require('./Base');
const { _transformAPIAutoModerationAction } = require('../util/Transformers');
/**
* Represents an auto moderation rule.
@ -67,6 +68,7 @@ class AutoModerationRule extends Base {
* @property {string[]} allowList The substrings that will be exempt from triggering
* {@link AutoModerationRuleTriggerType.Keyword} and {@link AutoModerationRuleTriggerType.KeywordPreset}
* @property {?number} mentionTotalLimit The total number of role & user mentions allowed per message
* @property {boolean} mentionRaidProtectionEnabled Whether mention raid protection is enabled
*/
/**
@ -79,6 +81,7 @@ class AutoModerationRule extends Base {
presets: data.trigger_metadata.presets ?? [],
allowList: data.trigger_metadata.allow_list ?? [],
mentionTotalLimit: data.trigger_metadata.mention_total_limit ?? null,
mentionRaidProtectionEnabled: data.trigger_metadata.mention_raid_protection_enabled ?? false,
};
}
@ -95,19 +98,14 @@ class AutoModerationRule extends Base {
* @typedef {Object} AutoModerationActionMetadata
* @property {?Snowflake} channelId The id of the channel to which content will be logged
* @property {?number} durationSeconds The timeout duration in seconds
* @property {?string} customMessage The custom message that is shown whenever a message is blocked
*/
/**
* The actions of this auto moderation rule.
* @type {AutoModerationAction[]}
*/
this.actions = data.actions.map(action => ({
type: action.type,
metadata: {
durationSeconds: action.metadata.duration_seconds ?? null,
channelId: action.metadata.channel_id ?? null,
},
}));
this.actions = data.actions.map(action => _transformAPIAutoModerationAction(action));
}
if ('enabled' in data) {
@ -184,7 +182,7 @@ class AutoModerationRule extends Base {
* @returns {Promise<AutoModerationRule>}
*/
setKeywordFilter(keywordFilter, reason) {
return this.edit({ triggerMetadata: { keywordFilter }, reason });
return this.edit({ triggerMetadata: { ...this.triggerMetadata, keywordFilter }, reason });
}
/**
@ -195,7 +193,7 @@ class AutoModerationRule extends Base {
* @returns {Promise<AutoModerationRule>}
*/
setRegexPatterns(regexPatterns, reason) {
return this.edit({ triggerMetadata: { regexPatterns }, reason });
return this.edit({ triggerMetadata: { ...this.triggerMetadata, regexPatterns }, reason });
}
/**
@ -205,32 +203,44 @@ class AutoModerationRule extends Base {
* @returns {Promise<AutoModerationRule>}
*/
setPresets(presets, reason) {
return this.edit({ triggerMetadata: { presets }, reason });
return this.edit({ triggerMetadata: { ...this.triggerMetadata, presets }, reason });
}
/**
* Sets the allow list for this auto moderation rule.
* @param {string[]} allowList The allow list of this auto moderation rule
* @param {string[]} allowList The substrings that will be exempt from triggering
* {@link AutoModerationRuleTriggerType.Keyword} and {@link AutoModerationRuleTriggerType.KeywordPreset}
* @param {string} [reason] The reason for changing the allow list of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setAllowList(allowList, reason) {
return this.edit({ triggerMetadata: { allowList }, reason });
return this.edit({ triggerMetadata: { ...this.triggerMetadata, allowList }, reason });
}
/**
* Sets the mention total limit for this auto moderation rule.
* @param {number} mentionTotalLimit The mention total limit of this auto moderation rule
* @param {number} mentionTotalLimit The total number of unique role and user mentions allowed per message
* @param {string} [reason] The reason for changing the mention total limit of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setMentionTotalLimit(mentionTotalLimit, reason) {
return this.edit({ triggerMetadata: { mentionTotalLimit }, reason });
return this.edit({ triggerMetadata: { ...this.triggerMetadata, mentionTotalLimit }, reason });
}
/**
* Sets whether to enable mention raid protection for this auto moderation rule.
* @param {boolean} mentionRaidProtectionEnabled
* Whether to enable mention raid protection for this auto moderation rule
* @param {string} [reason] The reason for changing the mention raid protection of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setMentionRaidProtectionEnabled(mentionRaidProtectionEnabled, reason) {
return this.edit({ triggerMetadata: { ...this.triggerMetadata, mentionRaidProtectionEnabled }, reason });
}
/**
* Sets the actions for this auto moderation rule.
* @param {AutoModerationActionOptions} actions The actions of this auto moderation rule
* @param {AutoModerationActionOptions[]} actions The actions of this auto moderation rule
* @param {string} [reason] The reason for changing the actions of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
@ -250,7 +260,8 @@ class AutoModerationRule extends Base {
/**
* Sets the exempt roles for this auto moderation rule.
* @param {Collection<Snowflake, Role>|RoleResolvable[]} [exemptRoles] The exempt roles of this auto moderation rule
* @param {Collection<Snowflake, Role>|RoleResolvable[]} [exemptRoles]
* The roles that should not be affected by the auto moderation rule
* @param {string} [reason] The reason for changing the exempt roles of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
@ -261,7 +272,7 @@ class AutoModerationRule extends Base {
/**
* Sets the exempt channels for this auto moderation rule.
* @param {Collection<Snowflake, GuildChannel|ThreadChannel>|GuildChannelResolvable[]} [exemptChannels]
* The exempt channels of this auto moderation rule
* The channels that should not be affected by the auto moderation rule
* @param {string} [reason] The reason for changing the exempt channels of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/

View file

@ -86,9 +86,7 @@ class AutocompleteInteraction extends BaseInteraction {
await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.ApplicationCommandAutocompleteResult,
data: {
choices: options,
},
data: { choices: this.client.options.jsonTransformer(options) },
},
auth: false,
});

View file

@ -125,7 +125,7 @@ class BaseGuildTextChannel extends GuildChannel {
/**
* Options used to create an invite to a guild channel.
* @typedef {Object} CreateInviteOptions
* @typedef {Object} InviteCreateOptions
* @property {boolean} [temporary] 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)
@ -142,7 +142,7 @@ class BaseGuildTextChannel extends GuildChannel {
/**
* Creates an invite to this guild channel.
* @param {CreateInviteOptions} [options={}] The options for creating the invite
* @param {InviteCreateOptions} [options={}] The options for creating the invite
* @returns {Promise<Invite>}
* @example
* // Create an invite to a channel

View file

@ -3,12 +3,32 @@
const { Collection } = require('@discordjs/collection');
const { PermissionFlagsBits } = require('discord-api-types/v10');
const GuildChannel = require('./GuildChannel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const MessageManager = require('../managers/MessageManager');
/**
* Represents a voice-based guild channel on Discord.
* @extends {GuildChannel}
* @implements {TextBasedChannel}
*/
class BaseGuildVoiceChannel extends GuildChannel {
constructor(guild, data, client) {
super(guild, data, client, false);
/**
* A manager of the messages sent to this channel
* @type {MessageManager}
*/
this.messages = new MessageManager(this);
/**
* If the guild considers this channel NSFW
* @type {boolean}
*/
this.nsfw = Boolean(data.nsfw);
this._patch(data);
}
_patch(data) {
super._patch(data);
@ -35,6 +55,40 @@ class BaseGuildVoiceChannel extends GuildChannel {
*/
this.userLimit = data.user_limit;
}
if ('video_quality_mode' in data) {
/**
* The camera video quality mode of the channel.
* @type {?VideoQualityMode}
*/
this.videoQualityMode = data.video_quality_mode;
} else {
this.videoQualityMode ??= null;
}
if ('last_message_id' in data) {
/**
* The last message id sent in the channel, if one was sent
* @type {?Snowflake}
*/
this.lastMessageId = data.last_message_id;
}
if ('messages' in data) {
for (const message of data.messages) this.messages._add(message);
}
if ('rate_limit_per_user' in data) {
/**
* The rate limit per user (slowmode) for this channel in seconds
* @type {number}
*/
this.rateLimitPerUser = data.rate_limit_per_user;
}
if ('nsfw' in data) {
this.nsfw = data.nsfw;
}
}
/**
@ -80,6 +134,44 @@ class BaseGuildVoiceChannel extends GuildChannel {
);
}
/**
* Creates an invite to this guild channel.
* @param {InviteCreateOptions} [options={}] The options for creating the invite
* @returns {Promise<Invite>}
* @example
* // Create an invite to a channel
* channel.createInvite()
* .then(invite => console.log(`Created an invite with a code of ${invite.code}`))
* .catch(console.error);
*/
createInvite(options) {
return this.guild.invites.create(this.id, options);
}
/**
* Fetches a collection of invites to this guild channel.
* @param {boolean} [cache=true] Whether to cache the fetched invites
* @returns {Promise<Collection<string, Invite>>}
*/
fetchInvites(cache = true) {
return this.guild.invites.fetch({ channelId: this.id, cache });
}
/**
* Sets the bitrate of the channel.
* @param {number} bitrate The new bitrate
* @param {string} [reason] Reason for changing the channel's bitrate
* @returns {Promise<BaseGuildVoiceChannel>}
* @example
* // Set the bitrate of a voice channel
* channel.setBitrate(48_000)
* .then(channel => console.log(`Set bitrate to ${channel.bitrate}bps for ${channel.name}`))
* .catch(console.error);
*/
setBitrate(bitrate, reason) {
return this.edit({ bitrate, reason });
}
/**
* Sets the RTC region of the channel.
* @param {?string} rtcRegion The new region of the channel. Set to `null` to remove a specific region for the channel
@ -97,28 +189,46 @@ class BaseGuildVoiceChannel extends GuildChannel {
}
/**
* Creates an invite to this guild channel.
* @param {CreateInviteOptions} [options={}] The options for creating the invite
* @returns {Promise<Invite>}
* Sets the user limit of the channel.
* @param {number} userLimit The new user limit
* @param {string} [reason] Reason for changing the user limit
* @returns {Promise<BaseGuildVoiceChannel>}
* @example
* // Create an invite to a channel
* channel.createInvite()
* .then(invite => console.log(`Created an invite with a code of ${invite.code}`))
* // Set the user limit of a voice channel
* channel.setUserLimit(42)
* .then(channel => console.log(`Set user limit to ${channel.userLimit} for ${channel.name}`))
* .catch(console.error);
*/
createInvite(options) {
return this.guild.invites.create(this.id, options);
setUserLimit(userLimit, reason) {
return this.edit({ userLimit, reason });
}
/**
* Fetches a collection of invites to this guild channel.
* Resolves with a collection mapping invites by their codes.
* @param {boolean} [cache=true] Whether or not to cache the fetched invites
* @returns {Promise<Collection<string, Invite>>}
* Sets the camera video quality mode of the channel.
* @param {VideoQualityMode} videoQualityMode The new camera video quality mode.
* @param {string} [reason] Reason for changing the camera video quality mode.
* @returns {Promise<BaseGuildVoiceChannel>}
*/
fetchInvites(cache = true) {
return this.guild.invites.fetch({ channelId: this.id, cache });
setVideoQualityMode(videoQualityMode, reason) {
return this.edit({ videoQualityMode, reason });
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel
/* eslint-disable no-empty-function */
get lastMessage() {}
send() {}
sendTyping() {}
createMessageCollector() {}
awaitMessages() {}
createMessageComponentCollector() {}
awaitMessageComponent() {}
bulkDelete() {}
fetchWebhooks() {}
createWebhook() {}
setRateLimitPerUser() {}
setNSFW() {}
}
TextBasedChannel.applyToClass(BaseGuildVoiceChannel, true, ['lastPinAt']);
module.exports = BaseGuildVoiceChannel;

View file

@ -46,7 +46,7 @@ class BaseInteraction extends Base {
* The id of the channel this interaction was sent in
* @type {?Snowflake}
*/
this.channelId = data.channel_id ?? null;
this.channelId = data.channel?.id ?? null;
/**
* The id of the guild this interaction was sent in
@ -270,12 +270,10 @@ class BaseInteraction extends Base {
return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.Button;
}
// TODO: Get rid of this in the next major
/**
* Indicates whether this interaction is a {@link StringSelectMenuInteraction}.
* @returns {boolean}
*
* @deprecated Use {@link Interaction#isStringSelectMenu} instead
* @deprecated Use {@link BaseInteraction#isStringSelectMenu} instead.
*/
isSelectMenu() {
return this.isStringSelectMenu();
@ -322,7 +320,7 @@ class BaseInteraction extends Base {
}
/**
* Indicates whether this interaction is a {@link MenionableSelectMenuInteraction}
* Indicates whether this interaction is a {@link MentionableSelectMenuInteraction}
* @returns {boolean}
*/
isMentionableSelectMenu() {

View file

@ -45,11 +45,11 @@ class BaseSelectMenuComponent extends Component {
/**
* Whether this select menu is disabled
* @type {?boolean}
* @type {boolean}
* @readonly
*/
get disabled() {
return this.data.disabled ?? null;
return this.data.disabled ?? false;
}
}

View file

@ -1,6 +1,7 @@
'use strict';
const { ButtonBuilder: BuildersButton, isJSONEncodable } = require('@discordjs/builders');
const { ButtonBuilder: BuildersButton } = require('@discordjs/builders');
const { isJSONEncodable } = require('@discordjs/util');
const { toSnakeCase } = require('../util/Transformers');
const { resolvePartialEmoji } = require('../util/Util');
@ -27,14 +28,11 @@ class ButtonBuilder extends BuildersButton {
/**
* Creates a new button builder from JSON data
* @param {JSONEncodable<APIButtonComponent>|APIButtonComponent} other The other data
* @param {ButtonBuilder|ButtonComponent|APIButtonComponent} other The other data
* @returns {ButtonBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
return new this(isJSONEncodable(other) ? other.toJSON() : other);
}
}
@ -42,5 +40,5 @@ module.exports = ButtonBuilder;
/**
* @external BuildersButton
* @see {@link https://discord.js.org/#/docs/builders/main/class/ButtonBuilder}
* @see {@link https://discord.js.org/docs/packages/builders/stable/ButtonBuilder:Class}
*/

View file

@ -36,11 +36,11 @@ class ButtonComponent extends Component {
/**
* Whether this button is disabled
* @type {?boolean}
* @type {boolean}
* @readonly
*/
get disabled() {
return this.data.disabled ?? null;
return this.data.disabled ?? false;
}
/**

View file

@ -9,17 +9,21 @@ const CategoryChannelChildManager = require('../managers/CategoryChannelChildMan
*/
class CategoryChannel extends GuildChannel {
/**
* A manager of the channels belonging to this category
* @type {CategoryChannelChildManager}
* The id of the parent of this channel.
* @name CategoryChannel#parentId
* @type {null}
*/
/**
* The parent of this channel.
* @name CategoryChannel#parent
* @type {null}
* @readonly
*/
get children() {
return new CategoryChannelChildManager(this);
}
/**
* Sets the category parent of this channel.
* <warn>It is not currently possible to set the parent of a CategoryChannel.</warn>
* <warn>It is not possible to set the parent of a CategoryChannel.</warn>
* @method setParent
* @memberof CategoryChannel
* @instance
@ -27,6 +31,15 @@ class CategoryChannel extends GuildChannel {
* @param {SetParentOptions} [options={}] The options for setting the parent
* @returns {Promise<GuildChannel>}
*/
/**
* A manager of the channels belonging to this category
* @type {CategoryChannelChildManager}
* @readonly
*/
get children() {
return new CategoryChannelChildManager(this);
}
}
module.exports = CategoryChannel;

View file

@ -1,6 +1,7 @@
'use strict';
const { ChannelSelectMenuBuilder: BuildersChannelSelectMenu, isJSONEncodable } = require('@discordjs/builders');
const { ChannelSelectMenuBuilder: BuildersChannelSelectMenu } = require('@discordjs/builders');
const { isJSONEncodable } = require('@discordjs/util');
const { toSnakeCase } = require('../util/Transformers');
/**
@ -13,15 +14,12 @@ class ChannelSelectMenuBuilder extends BuildersChannelSelectMenu {
}
/**
* Creates a new select menu builder from json data
* @param {JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent} other The other data
* Creates a new select menu builder from JSON data
* @param {ChannelSelectMenuBuilder|ChannelSelectMenuComponent|APIChannelSelectComponent} other The other data
* @returns {ChannelSelectMenuBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
return new this(isJSONEncodable(other) ? other.toJSON() : other);
}
}
@ -29,5 +27,5 @@ module.exports = ChannelSelectMenuBuilder;
/**
* @external BuildersChannelSelectMenu
* @see {@link https://discord.js.org/#/docs/builders/main/class/ChannelSelectMenuBuilder}
* @see {@link https://discord.js.org/docs/packages/builders/stable/ChannelSelectMenuBuilder:Class}
*/

View file

@ -1,6 +1,7 @@
'use strict';
const { Routes } = require('discord-api-types/v10');
const { ApplicationRoleConnectionMetadata } = require('./ApplicationRoleConnectionMetadata');
const Team = require('./Team');
const Application = require('./interfaces/Application');
const ApplicationCommandManager = require('../managers/ApplicationCommandManager');
@ -108,6 +109,16 @@ class ClientApplication extends Application {
this.botPublic ??= null;
}
if ('role_connections_verification_url' in data) {
/**
* This application's role connection verification entry point URL
* @type {?string}
*/
this.roleConnectionsVerificationURL = data.role_connections_verification_url;
} else {
this.roleConnectionsVerificationURL ??= null;
}
/**
* The owner of this OAuth application
* @type {?(User|Team)}
@ -137,6 +148,46 @@ class ClientApplication extends Application {
this._patch(app);
return this;
}
/**
* Gets this application's role connection metadata records
* @returns {Promise<ApplicationRoleConnectionMetadata[]>}
*/
async fetchRoleConnectionMetadataRecords() {
const metadata = await this.client.rest.get(Routes.applicationRoleConnectionMetadata(this.client.user.id));
return metadata.map(data => new ApplicationRoleConnectionMetadata(data));
}
/**
* Data for creating or editing an application role connection metadata.
* @typedef {Object} ApplicationRoleConnectionMetadataEditOptions
* @property {string} name The name of the metadata field
* @property {?Object<Locale, string>} [nameLocalizations] The name localizations for the metadata field
* @property {string} description The description of the metadata field
* @property {?Object<Locale, string>} [descriptionLocalizations] The description localizations for the metadata field
* @property {string} key The dictionary key of the metadata field
* @property {ApplicationRoleConnectionMetadataType} type The type of the metadata field
*/
/**
* Updates this application's role connection metadata records
* @param {ApplicationRoleConnectionMetadataEditOptions[]} records The new role connection metadata records
* @returns {Promise<ApplicationRoleConnectionMetadata[]>}
*/
async editRoleConnectionMetadataRecords(records) {
const newRecords = await this.client.rest.put(Routes.applicationRoleConnectionMetadata(this.client.user.id), {
body: records.map(record => ({
type: record.type,
key: record.key,
name: record.name,
name_localizations: record.nameLocalizations,
description: record.description,
description_localizations: record.descriptionLocalizations,
})),
});
return newRecords.map(data => new ApplicationRoleConnectionMetadata(data));
}
}
module.exports = ClientApplication;

View file

@ -21,7 +21,7 @@ class ClientPresence extends Presence {
set(presence) {
const packet = this._parse(presence);
this._patch(packet);
if (typeof presence.shardId === 'undefined') {
if (presence.shardId === undefined) {
this.client.ws.broadcast({ op: GatewayOpcodes.PresenceUpdate, d: packet });
} else if (Array.isArray(presence.shardId)) {
for (const shardId of presence.shardId) {

View file

@ -44,22 +44,24 @@ class ClientUser extends User {
/**
* Data used to edit the logged in client
* @typedef {Object} ClientUserEditData
* @typedef {Object} ClientUserEditOptions
* @property {string} [username] The new username
* @property {?(BufferResolvable|Base64Resolvable)} [avatar] The new avatar
*/
/**
* Edits the logged in client.
* @param {ClientUserEditData} data The new data
* @param {ClientUserEditOptions} options The options to provide
* @returns {Promise<ClientUser>}
*/
async edit(data) {
if (typeof data.avatar !== 'undefined') data.avatar = await DataResolver.resolveImage(data.avatar);
const newData = await this.client.rest.patch(Routes.user(), { body: data });
this.client.token = newData.token;
this.client.rest.setToken(newData.token);
const { updated } = this.client.actions.UserUpdate.handle(newData);
async edit({ username, avatar }) {
const data = await this.client.rest.patch(Routes.user(), {
body: { username, avatar: avatar && (await DataResolver.resolveImage(avatar)) },
});
this.client.token = data.token;
this.client.rest.setToken(data.token);
const { updated } = this.client.actions.UserUpdate.handle(data);
return updated ?? this;
}

View file

@ -85,19 +85,19 @@ class CommandInteractionOptionResolver {
/**
* Gets an option by name and property and checks its type.
* @param {string} name The name of the option.
* @param {ApplicationCommandOptionType} type The type of the option.
* @param {ApplicationCommandOptionType[]} allowedTypes The allowed types of the option.
* @param {string[]} properties The properties to check for for `required`.
* @param {boolean} required Whether to throw an error if the option is not found.
* @returns {?CommandInteractionOption} The option, if found.
* @private
*/
_getTypedOption(name, type, properties, required) {
_getTypedOption(name, allowedTypes, properties, required) {
const option = this.get(name, required);
if (!option) {
return null;
} else if (option.type !== type) {
throw new DiscordjsTypeError(ErrorCodes.CommandInteractionOptionType, name, option.type, type);
} else if (required && properties.every(prop => option[prop] === null || typeof option[prop] === 'undefined')) {
} else if (!allowedTypes.includes(option.type)) {
throw new DiscordjsTypeError(ErrorCodes.CommandInteractionOptionType, name, option.type, allowedTypes.join(', '));
} else if (required && properties.every(prop => option[prop] === null || option[prop] === undefined)) {
throw new DiscordjsTypeError(ErrorCodes.CommandInteractionOptionEmpty, name, option.type);
}
return option;
@ -134,7 +134,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, [ApplicationCommandOptionType.Boolean], ['value'], required);
return option?.value ?? null;
}
@ -142,12 +142,24 @@ class CommandInteractionOptionResolver {
* Gets a channel option.
* @param {string} name The name of the option.
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
* @param {ChannelType[]} [channelTypes=[]] The allowed types of channels. If empty, all channel types are allowed.
* @returns {?(GuildChannel|ThreadChannel|APIChannel)}
* 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);
return option?.channel ?? null;
getChannel(name, required = false, channelTypes = []) {
const option = this._getTypedOption(name, [ApplicationCommandOptionType.Channel], ['channel'], required);
const channel = option?.channel ?? null;
if (channel && channelTypes.length > 0 && !channelTypes.includes(channel.type)) {
throw new DiscordjsTypeError(
ErrorCodes.CommandInteractionOptionInvalidChannelType,
name,
channel.type,
channelTypes.join(', '),
);
}
return channel;
}
/**
@ -157,7 +169,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, [ApplicationCommandOptionType.String], ['value'], required);
return option?.value ?? null;
}
@ -168,7 +180,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, [ApplicationCommandOptionType.Integer], ['value'], required);
return option?.value ?? null;
}
@ -179,7 +191,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, [ApplicationCommandOptionType.Number], ['value'], required);
return option?.value ?? null;
}
@ -190,7 +202,12 @@ 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,
[ApplicationCommandOptionType.User, ApplicationCommandOptionType.Mentionable],
['user'],
required,
);
return option?.user ?? null;
}
@ -201,7 +218,12 @@ class CommandInteractionOptionResolver {
* The value of the option, or null if the user is not present in the guild or the option is not set.
*/
getMember(name) {
const option = this._getTypedOption(name, ApplicationCommandOptionType.User, ['member'], false);
const option = this._getTypedOption(
name,
[ApplicationCommandOptionType.User, ApplicationCommandOptionType.Mentionable],
['member'],
false,
);
return option?.member ?? null;
}
@ -212,7 +234,12 @@ 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,
[ApplicationCommandOptionType.Role, ApplicationCommandOptionType.Mentionable],
['role'],
required,
);
return option?.role ?? null;
}
@ -223,7 +250,7 @@ class CommandInteractionOptionResolver {
* @returns {?Attachment} 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);
const option = this._getTypedOption(name, [ApplicationCommandOptionType.Attachment], ['attachment'], required);
return option?.attachment ?? null;
}
@ -237,7 +264,7 @@ class CommandInteractionOptionResolver {
getMentionable(name, required = false) {
const option = this._getTypedOption(
name,
ApplicationCommandOptionType.Mentionable,
[ApplicationCommandOptionType.Mentionable],
['user', 'member', 'role'],
required,
);
@ -252,7 +279,7 @@ class CommandInteractionOptionResolver {
* The value of the option, or null if not set and not required.
*/
getMessage(name, required = false) {
const option = this._getTypedOption(name, '_MESSAGE', ['message'], required);
const option = this._getTypedOption(name, ['_MESSAGE'], ['message'], required);
return option?.message ?? null;
}

View file

@ -68,7 +68,7 @@ class DMChannel extends BaseChannel {
* @readonly
*/
get partial() {
return typeof this.lastMessageId === 'undefined';
return this.lastMessageId === undefined;
}
/**

View file

@ -1,6 +1,7 @@
'use strict';
const { EmbedBuilder: BuildersEmbed, isJSONEncodable } = require('@discordjs/builders');
const { EmbedBuilder: BuildersEmbed } = require('@discordjs/builders');
const { isJSONEncodable } = require('@discordjs/util');
const { toSnakeCase } = require('../util/Transformers');
const { resolveColor } = require('../util/Util');
@ -24,14 +25,11 @@ class EmbedBuilder extends BuildersEmbed {
/**
* Creates a new embed builder from JSON data
* @param {JSONEncodable<APIEmbed>|APIEmbed} other The other data
* @param {EmbedBuilder|Embed|APIEmbed} other The other data
* @returns {EmbedBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
return new this(isJSONEncodable(other) ? other.toJSON() : other);
}
}
@ -39,5 +37,5 @@ module.exports = EmbedBuilder;
/**
* @external BuildersEmbed
* @see {@link https://discord.js.org/#/docs/builders/main/class/EmbedBuilder}
* @see {@link https://discord.js.org/docs/packages/builders/stable/EmbedBuilder:Class}
*/

View file

@ -134,6 +134,12 @@ class ForumChannel extends GuildChannel {
} else {
this.defaultSortOrder ??= null;
}
/**
* The default layout type used to display posts
* @type {ForumLayoutType}
*/
this.defaultForumLayout = data.default_forum_layout;
}
/**
@ -168,7 +174,7 @@ class ForumChannel extends GuildChannel {
/**
* Creates an invite to this guild channel.
* @param {CreateInviteOptions} [options={}] The options for creating the invite
* @param {InviteCreateOptions} [options={}] The options for creating the invite
* @returns {Promise<Invite>}
* @example
* // Create an invite to a channel
@ -225,6 +231,16 @@ class ForumChannel extends GuildChannel {
return this.edit({ defaultSortOrder, reason });
}
/**
* Sets the default forum layout type used to display posts
* @param {ForumLayoutType} defaultForumLayout The default forum layout type to set on this channel
* @param {string} [reason] Reason for changing the default forum layout
* @returns {Promise<ForumChannel>}
*/
setDefaultForumLayout(defaultForumLayout, reason) {
return this.edit({ defaultForumLayout, reason });
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel
/* eslint-disable no-empty-function */
createWebhook() {}

View file

@ -5,7 +5,6 @@ const { makeURLSearchParams } = require('@discordjs/rest');
const { ChannelType, GuildPremiumTier, Routes, GuildFeature } = require('discord-api-types/v10');
const AnonymousGuild = require('./AnonymousGuild');
const GuildAuditLogs = require('./GuildAuditLogs');
const GuildAuditLogsEntry = require('./GuildAuditLogsEntry');
const GuildPreview = require('./GuildPreview');
const GuildTemplate = require('./GuildTemplate');
const Integration = require('./Integration');
@ -308,6 +307,16 @@ class Guild extends AnonymousGuild {
this.maxVideoChannelUsers ??= null;
}
if ('max_stage_video_channel_users' in data) {
/**
* The maximum amount of users allowed in a stage video channel.
* @type {?number}
*/
this.maxStageVideoChannelUsers = data.max_stage_video_channel_users;
} else {
this.maxStageVideoChannelUsers ??= null;
}
if ('approximate_member_count' in data) {
/**
* The approximate amount of members the guild has
@ -361,6 +370,16 @@ class Guild extends AnonymousGuild {
this.preferredLocale = data.preferred_locale;
}
if ('safety_alerts_channel_id' in data) {
/**
* The safety alerts channel's id for the guild
* @type {?Snowflake}
*/
this.safetyAlertsChannelId = data.safety_alerts_channel_id;
} else {
this.safetyAlertsChannelId ??= null;
}
if (data.channels) {
this.channels.cache.clear();
for (const rawChannel of data.channels) {
@ -525,6 +544,15 @@ class Guild extends AnonymousGuild {
return this.client.channels.resolve(this.publicUpdatesChannelId);
}
/**
* Safety alerts channel for this guild
* @type {?TextChannel}
* @readonly
*/
get safetyAlertsChannel() {
return this.client.channels.resolve(this.safetyAlertsChannelId);
}
/**
* The maximum bitrate available for this guild
* @type {number}
@ -624,9 +652,6 @@ class Guild extends AnonymousGuild {
* .catch(console.error);
*/
async fetchVanityData() {
if (!this.features.includes(GuildFeature.VanityURL)) {
throw new DiscordjsError(ErrorCodes.VanityURL);
}
const data = await this.client.rest.get(Routes.guildVanityUrl(this.id));
this.vanityURLCode = data.code;
this.vanityURLUses = data.uses;
@ -700,7 +725,8 @@ class Guild extends AnonymousGuild {
/**
* Options used to fetch audit logs.
* @typedef {Object} GuildAuditLogsFetchOptions
* @property {Snowflake|GuildAuditLogsEntry} [before] Only return entries before this entry
* @property {Snowflake|GuildAuditLogsEntry} [before] Consider only entries before this entry
* @property {Snowflake|GuildAuditLogsEntry} [after] Consider only entries after 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
@ -716,19 +742,18 @@ class Guild extends AnonymousGuild {
* .then(audit => console.log(audit.entries.first()))
* .catch(console.error);
*/
async fetchAuditLogs(options = {}) {
if (options.before && options.before instanceof GuildAuditLogsEntry) options.before = options.before.id;
async fetchAuditLogs({ before, after, limit, user, type } = {}) {
const query = makeURLSearchParams({
before: options.before,
limit: options.limit,
action_type: options.type,
before: before?.id ?? before,
after: after?.id ?? after,
limit,
action_type: type,
});
if (options.user) {
const id = this.client.users.resolveId(options.user);
if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'user', 'UserResolvable');
query.set('user_id', id);
if (user) {
const userId = this.client.users.resolveId(user);
if (!userId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'user', 'UserResolvable');
query.set('user_id', userId);
}
const data = await this.client.rest.get(Routes.guildAuditLog(this.id), { query });
@ -737,30 +762,30 @@ class Guild extends AnonymousGuild {
/**
* The data for editing a guild.
* @typedef {Object} GuildEditData
* @typedef {Object} GuildEditOptions
* @property {string} [name] The name of the guild
* @property {?GuildVerificationLevel} [verificationLevel] The verification level of the guild
* @property {?GuildDefaultMessageNotifications} [defaultMessageNotifications] The default message
* notification level of the guild
* @property {?GuildExplicitContentFilter} [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 {?GuildDefaultMessageNotifications} [defaultMessageNotifications] The default message
* notification level of the guild
* @property {?TextChannelResolvable} [systemChannel] The system channel 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 {?TextChannelResolvable} [safetyAlertsChannel] The safety alerts 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
* @property {?string} [description] The discovery description of the guild
* @property {boolean} [premiumProgressBarEnabled] Whether the guild's premium progress bar is enabled
* @property {string} [reason] Reason for editing this guild
*/
/* eslint-enable max-len */
/**
* Data that can be resolved to a Text Channel object. This can be:
@ -778,7 +803,7 @@ class Guild extends AnonymousGuild {
/**
* Updates the guild with new information - e.g. a new name.
* @param {GuildEditData} data The data to update the guild with
* @param {GuildEditOptions} options The options to provide
* @returns {Promise<Guild>}
* @example
* // Set the guild name
@ -788,51 +813,52 @@ class Guild extends AnonymousGuild {
* .then(updated => console.log(`New guild name ${updated}`))
* .catch(console.error);
*/
async edit(data) {
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 (typeof data.preferredLocale !== 'undefined') _data.preferred_locale = data.preferredLocale;
if ('premiumProgressBarEnabled' in data) _data.premium_progress_bar_enabled = data.premiumProgressBarEnabled;
const newData = await this.client.rest.patch(Routes.guild(this.id), { body: _data, reason: data.reason });
return this.client.actions.GuildUpdate.handle(newData).updated;
async edit({
verificationLevel,
defaultMessageNotifications,
explicitContentFilter,
afkChannel,
afkTimeout,
icon,
owner,
splash,
discoverySplash,
banner,
systemChannel,
systemChannelFlags,
rulesChannel,
publicUpdatesChannel,
preferredLocale,
premiumProgressBarEnabled,
safetyAlertsChannel,
...options
}) {
const data = await this.client.rest.patch(Routes.guild(this.id), {
body: {
...options,
verification_level: verificationLevel,
default_message_notifications: defaultMessageNotifications,
explicit_content_filter: explicitContentFilter,
afk_channel_id: afkChannel && this.client.channels.resolveId(afkChannel),
afk_timeout: afkTimeout,
icon: icon && (await DataResolver.resolveImage(icon)),
owner_id: owner && this.client.users.resolveId(owner),
splash: splash && (await DataResolver.resolveImage(splash)),
discovery_splash: discoverySplash && (await DataResolver.resolveImage(discoverySplash)),
banner: banner && (await DataResolver.resolveImage(banner)),
system_channel_id: systemChannel && this.client.channels.resolveId(systemChannel),
system_channel_flags:
systemChannelFlags === undefined ? undefined : SystemChannelFlagsBitField.resolve(systemChannelFlags),
rules_channel_id: rulesChannel && this.client.channels.resolveId(rulesChannel),
public_updates_channel_id: publicUpdatesChannel && this.client.channels.resolveId(publicUpdatesChannel),
preferred_locale: preferredLocale,
premium_progress_bar_enabled: premiumProgressBarEnabled,
safety_alerts_channel_id: safetyAlertsChannel && this.client.channels.resolveId(safetyAlertsChannel),
},
reason: options.reason,
});
return this.client.actions.GuildUpdate.handle(data).updated;
}
/**
@ -845,7 +871,7 @@ class Guild extends AnonymousGuild {
/**
* Welcome screen edit data
* @typedef {Object} WelcomeScreenEditData
* @typedef {Object} WelcomeScreenEditOptions
* @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
@ -869,7 +895,7 @@ class Guild extends AnonymousGuild {
/**
* Updates the guild's welcome screen
* @param {WelcomeScreenEditData} data Data to edit the welcome screen with
* @param {WelcomeScreenEditOptions} options The options to provide
* @returns {Promise<WelcomeScreen>}
* @example
* guild.editWelcomeScreen({
@ -883,8 +909,8 @@ class Guild extends AnonymousGuild {
* ],
* })
*/
async editWelcomeScreen(data) {
const { enabled, description, welcomeChannels } = data;
async editWelcomeScreen(options) {
const { enabled, description, welcomeChannels } = options;
const welcome_channels = welcomeChannels?.map(welcomeChannelData => {
const emoji = this.emojis.resolve(welcomeChannelData.emoji);
return {
@ -905,7 +931,6 @@ class Guild extends AnonymousGuild {
return new WelcomeScreen(this, patchData);
}
/* eslint-disable max-len */
/**
* Edits the level of the explicit content filter.
* @param {?GuildExplicitContentFilter} explicitContentFilter The new level of the explicit content filter
@ -918,14 +943,14 @@ class Guild extends AnonymousGuild {
/**
* Edits the setting of the default message notifications of the guild.
* @param {?GuildDefaultMessageNotifications} defaultMessageNotifications The new default message notification level of the guild
* @param {?GuildDefaultMessageNotifications} 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<Guild>}
*/
setDefaultMessageNotifications(defaultMessageNotifications, reason) {
return this.edit({ defaultMessageNotifications, reason });
}
/* eslint-enable max-len */
/**
* Edits the flags of the default message notifications of the guild.
@ -1142,6 +1167,21 @@ class Guild extends AnonymousGuild {
return this.edit({ premiumProgressBarEnabled: enabled, reason });
}
/**
* Edits the safety alerts channel of the guild.
* @param {?TextChannelResolvable} safetyAlertsChannel The new safety alerts channel
* @param {string} [reason] Reason for changing the guild's safety alerts channel
* @returns {Promise<Guild>}
* @example
* // Edit the guild safety alerts channel
* guild.setSafetyAlertsChannel(channel)
* .then(updated => console.log(`Updated guild safety alerts channel to ${updated.safetyAlertsChannel.name}`))
* .catch(console.error);
*/
setSafetyAlertsChannel(safetyAlertsChannel, reason) {
return this.edit({ safetyAlertsChannel, reason });
}
/**
* Edits the guild's widget settings.
* @param {GuildWidgetSettingsData} settings The widget settings for the guild
@ -1161,9 +1201,15 @@ class Guild extends AnonymousGuild {
/**
* Sets the guild's MFA level
* <info>An elevated MFA level requires guild moderators to have 2FA enabled.</info>
* @param {GuildMFALevel} level The MFA level
* @param {string} [reason] Reason for changing the guild's MFA level
* @returns {Promise<Guild>}
* @example
* // Set the MFA level of the guild to Elevated
* guild.setMFALevel(GuildMFALevel.Elevated)
* .then(guild => console.log("Set guild's MFA level to Elevated"))
* .catch(console.error);
*/
async setMFALevel(level, reason) {
await this.client.rest.post(Routes.guildMFA(this.id), {
@ -1181,7 +1227,7 @@ class Guild extends AnonymousGuild {
* @example
* // Leave a guild
* guild.leave()
* .then(g => console.log(`Left the guild ${g}`))
* .then(guild => console.log(`Left the guild: ${guild.name}`))
* .catch(console.error);
*/
async leave() {
@ -1204,6 +1250,17 @@ class Guild extends AnonymousGuild {
return this;
}
/**
* Sets whether this guild's invites are disabled.
* @param {boolean} [disabled=true] Whether the invites are disabled
* @returns {Promise<Guild>}
*/
async disableInvites(disabled = true) {
const features = this.features.filter(feature => feature !== GuildFeature.InvitesDisabled);
if (disabled) features.push(GuildFeature.InvitesDisabled);
return this.edit({ features });
}
/**
* 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

View file

@ -78,7 +78,7 @@ class GuildAuditLogs {
*/
this.entries = new Collection();
for (const item of data.audit_log_entries) {
const entry = new GuildAuditLogsEntry(this, guild, item);
const entry = new GuildAuditLogsEntry(guild, item, this);
this.entries.set(entry.id, entry);
}
}

View file

@ -94,7 +94,7 @@ class GuildAuditLogsEntry {
*/
static Targets = Targets;
constructor(logs, guild, data) {
constructor(guild, data, logs) {
/**
* The target type of this entry
* @type {AuditLogTargetType}
@ -109,7 +109,7 @@ class GuildAuditLogsEntry {
this.actionType = GuildAuditLogsEntry.actionType(data.action_type);
/**
* The type of action that occured.
* The type of action that occurred.
* @type {AuditLogEvent}
*/
this.action = data.action_type;
@ -120,6 +120,12 @@ class GuildAuditLogsEntry {
*/
this.reason = data.reason ?? null;
/**
* The id of the user that executed this entry
* @type {?Snowflake}
*/
this.executorId = data.user_id;
/**
* The user that executed this entry
* @type {?User}
@ -127,7 +133,7 @@ class GuildAuditLogsEntry {
this.executor = data.user_id
? guild.client.options.partials.includes(Partials.User)
? guild.client.users._add({ id: data.user_id })
: guild.client.users.cache.get(data.user_id)
: guild.client.users.cache.get(data.user_id) ?? null
: null;
/**
@ -239,6 +245,12 @@ class GuildAuditLogsEntry {
break;
}
/**
* The id of the target of this entry
* @type {?Snowflake}
*/
this.targetId = data.target_id;
/**
* The target of this entry
* @type {?AuditLogEntryTarget}
@ -254,12 +266,12 @@ class GuildAuditLogsEntry {
} else if (targetType === Targets.User && data.target_id) {
this.target = guild.client.options.partials.includes(Partials.User)
? guild.client.users._add({ id: data.target_id })
: guild.client.users.cache.get(data.target_id);
: guild.client.users.cache.get(data.target_id) ?? null;
} else if (targetType === Targets.Guild) {
this.target = guild.client.guilds.cache.get(data.target_id);
} else if (targetType === Targets.Webhook) {
this.target =
logs.webhooks.get(data.target_id) ??
logs?.webhooks.get(data.target_id) ??
new Webhook(
guild.client,
this.changes.reduce(
@ -294,10 +306,10 @@ class GuildAuditLogsEntry {
this.target =
data.action_type === AuditLogEvent.MessageBulkDelete
? guild.channels.cache.get(data.target_id) ?? { id: data.target_id }
: guild.client.users.cache.get(data.target_id);
: guild.client.users.cache.get(data.target_id) ?? null;
} else if (targetType === Targets.Integration) {
this.target =
logs.integrations.get(data.target_id) ??
logs?.integrations.get(data.target_id) ??
new Integration(
guild.client,
this.changes.reduce(
@ -363,7 +375,7 @@ class GuildAuditLogsEntry {
),
);
} else if (targetType === Targets.ApplicationCommand) {
this.target = logs.applicationCommands.get(data.target_id) ?? { id: data.target_id };
this.target = logs?.applicationCommands.get(data.target_id) ?? { id: data.target_id };
} else if (targetType === Targets.AutoModeration) {
this.target =
guild.autoModerationRules.cache.get(data.target_id) ??

View file

@ -20,7 +20,7 @@ const PermissionsBitField = require('../util/PermissionsBitField');
*/
class GuildChannel extends BaseChannel {
constructor(guild, data, client, immediatePatch = true) {
super(guild?.client ?? client, data, false);
super(client, data, false);
/**
* The guild the channel is in
@ -33,8 +33,6 @@ class GuildChannel extends BaseChannel {
* @type {Snowflake}
*/
this.guildId = guild?.id ?? data.guild_id;
this.parentId = this.parentId ?? null;
/**
* A manager of permission overwrites that belong to this channel
* @type {PermissionOverwriteManager}
@ -73,6 +71,8 @@ class GuildChannel extends BaseChannel {
* @type {?Snowflake}
*/
this.parentId = data.parent_id;
} else {
this.parentId ??= null;
}
if ('permission_overwrites' in data) {
@ -131,8 +131,8 @@ class GuildChannel extends BaseChannel {
// Compare overwrites
return (
typeof channelVal !== 'undefined' &&
typeof parentVal !== 'undefined' &&
channelVal !== undefined &&
parentVal !== undefined &&
channelVal.deny.bitfield === parentVal.deny.bitfield &&
channelVal.allow.bitfield === parentVal.allow.bitfield
);
@ -268,7 +268,7 @@ class GuildChannel extends BaseChannel {
/**
* Edits the channel.
* @param {GuildChannelEditOptions} data The new data for the channel
* @param {GuildChannelEditOptions} options The options to provide
* @returns {Promise<GuildChannel>}
* @example
* // Edit a channel
@ -276,8 +276,8 @@ class GuildChannel extends BaseChannel {
* .then(console.log)
* .catch(console.error);
*/
edit(data) {
return this.guild.channels.edit(this, data);
edit(options) {
return this.guild.channels.edit(this, options);
}
/**

View file

@ -56,7 +56,7 @@ class GuildEmoji extends BaseGuildEmoji {
*/
get deletable() {
if (!this.guild.members.me) throw new DiscordjsError(ErrorCodes.GuildUncachedMe);
return !this.managed && this.guild.members.me.permissions.has(PermissionFlagsBits.ManageEmojisAndStickers);
return !this.managed && this.guild.members.me.permissions.has(PermissionFlagsBits.ManageGuildExpressions);
}
/**
@ -78,7 +78,7 @@ class GuildEmoji extends BaseGuildEmoji {
/**
* Data for editing an emoji.
* @typedef {Object} GuildEmojiEditData
* @typedef {Object} GuildEmojiEditOptions
* @property {string} [name] The name of the emoji
* @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] Roles to restrict emoji to
* @property {string} [reason] Reason for editing this emoji
@ -86,7 +86,7 @@ class GuildEmoji extends BaseGuildEmoji {
/**
* Edits the emoji.
* @param {GuildEmojiEditData} data The new data for the emoji
* @param {GuildEmojiEditOptions} options The options to provide
* @returns {Promise<GuildEmoji>}
* @example
* // Edit an emoji
@ -94,8 +94,8 @@ class GuildEmoji extends BaseGuildEmoji {
* .then(e => console.log(`Edited emoji ${e}`))
* .catch(console.error);
*/
edit(data) {
return this.guild.emojis.edit(this.id, data);
edit(options) {
return this.guild.emojis.edit(this.id, options);
}
/**

View file

@ -6,6 +6,7 @@ const VoiceState = require('./VoiceState');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const { DiscordjsError, ErrorCodes } = require('../errors');
const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager');
const { GuildMemberFlagsBitField } = require('../util/GuildMemberFlagsBitField');
const PermissionsBitField = require('../util/PermissionsBitField');
/**
@ -53,6 +54,11 @@ class GuildMember extends Base {
*/
this.communicationDisabledUntilTimestamp = null;
/**
* The role ids of the member
* @type {Snowflake[]}
* @private
*/
this._roles = [];
if (data) this._patch(data);
}
@ -93,6 +99,16 @@ class GuildMember extends Base {
this.communicationDisabledUntilTimestamp =
data.communication_disabled_until && Date.parse(data.communication_disabled_until);
}
if ('flags' in data) {
/**
* The flags of this member
* @type {Readonly<GuildMemberFlagsBitField>}
*/
this.flags = new GuildMemberFlagsBitField(data.flags).freeze();
} else {
this.flags ??= new GuildMemberFlagsBitField().freeze();
}
}
_clone() {
@ -307,11 +323,21 @@ class GuildMember extends Base {
/**
* Edits this member.
* @param {GuildMemberEditData} data The data to edit the member with
* @param {GuildMemberEditOptions} options The options to provide
* @returns {Promise<GuildMember>}
*/
edit(data) {
return this.guild.members.edit(this, data);
edit(options) {
return this.guild.members.edit(this, options);
}
/**
* Sets the flags for this member.
* @param {GuildMemberFlagsResolvable} flags The flags to set
* @param {string} [reason] Reason for setting the flags
* @returns {Promise<GuildMember>}
*/
setFlags(flags, reason) {
return this.edit({ flags, reason });
}
/**
@ -319,6 +345,16 @@ class GuildMember extends Base {
* @param {?string} nick The nickname for the guild member, or `null` if you want to reset their nickname
* @param {string} [reason] Reason for setting the nickname
* @returns {Promise<GuildMember>}
* @example
* // Set a nickname for a guild member
* guildMember.setNickname('cool nickname', 'Needed a new nickname')
* .then(member => console.log(`Set nickname of ${member.user.username}`))
* .catch(console.error);
* @example
* // Remove a nickname for a guild member
* guildMember.setNickname(null, 'No nicknames allowed!')
* .then(member => console.log(`Removed nickname for ${member.user.username}`))
* .catch(console.error);
*/
setNickname(nick, reason) {
return this.edit({ nick, reason });
@ -361,7 +397,7 @@ class GuildMember extends Base {
* .catch(console.error);
*/
ban(options) {
return this.guild.members.ban(this, options);
return this.guild.bans.create(this, options);
}
/**
@ -375,6 +411,11 @@ class GuildMember extends Base {
* guildMember.disableCommunicationUntil(Date.now() + (5 * 60 * 1000), 'They deserved it')
* .then(console.log)
* .catch(console.error);
* @example
* // Remove the timeout of a guild member
* guildMember.disableCommunicationUntil(null)
* .then(member => console.log(`Removed timeout for ${member.displayName}`))
* .catch(console.error);
*/
disableCommunicationUntil(communicationDisabledUntil, reason) {
return this.edit({ communicationDisabledUntil, reason });
@ -423,6 +464,7 @@ class GuildMember extends Base {
this.avatar === member.avatar &&
this.pending === member.pending &&
this.communicationDisabledUntilTimestamp === member.communicationDisabledUntilTimestamp &&
this.flags.bitfield === member.flags.bitfield &&
(this._roles === member._roles ||
(this._roles.length === member._roles.length && this._roles.every((role, i) => role === member._roles[i])))
);
@ -450,12 +492,22 @@ class GuildMember extends Base {
json.displayAvatarURL = this.displayAvatarURL();
return json;
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel
/* eslint-disable no-empty-function */
send() {}
}
/**
* Sends a message to this user.
* @method send
* @memberof GuildMember
* @instance
* @param {string|MessagePayload|MessageCreateOptions} options The options to provide
* @returns {Promise<Message>}
* @example
* // Send a direct message
* guildMember.send('Hello!')
* .then(message => console.log(`Sent message: ${message.content} to ${guildMember.displayName}`))
* .catch(console.error);
*/
TextBasedChannel.applyToClass(GuildMember);
exports.GuildMember = GuildMember;

View file

@ -240,7 +240,7 @@ class GuildScheduledEvent extends Base {
/**
* Options used to create an invite URL to a {@link GuildScheduledEvent}
* @typedef {CreateInviteOptions} CreateGuildScheduledEventInviteURLOptions
* @typedef {InviteCreateOptions} GuildScheduledEventInviteURLCreateOptions
* @property {GuildInvitableChannelResolvable} [channel] The channel to create the invite in.
* <warn>This is required when the `entityType` of `GuildScheduledEvent` is
* {@link GuildScheduledEventEntityType.External}, gets ignored otherwise</warn>
@ -248,7 +248,7 @@ class GuildScheduledEvent extends Base {
/**
* Creates an invite URL to this guild scheduled event.
* @param {CreateGuildScheduledEventInviteURLOptions} [options] The options to create the invite
* @param {GuildScheduledEventInviteURLCreateOptions} [options] The options to create the invite
* @returns {Promise<string>}
*/
async createInviteURL(options) {

View file

@ -155,14 +155,14 @@ class GuildTemplate extends Base {
/**
* Options used to edit a guild template.
* @typedef {Object} EditGuildTemplateOptions
* @typedef {Object} GuildTemplateEditOptions
* @property {string} [name] The name of this template
* @property {string} [description] The description of this template
*/
/**
* Updates the metadata of this template.
* @param {EditGuildTemplateOptions} [options] Options for editing the template
* @param {GuildTemplateEditOptions} [options] Options for editing the template
* @returns {Promise<GuildTemplate>}
*/
async edit({ name, description } = {}) {

View file

@ -16,6 +16,7 @@ const IntegrationApplication = require('./IntegrationApplication');
* * `twitch`
* * `youtube`
* * `discord`
* * `guild_subscription`
* @typedef {string} IntegrationType
*/

View file

@ -14,7 +14,7 @@ const Events = require('../util/Events');
* @property {number} [maxComponents] The maximum number of components to collect
* @property {number} [maxUsers] The maximum number of users to interact
* @property {Message|APIMessage} [message] The message to listen to interactions from
* @property {InteractionResponse} interactionResponse The interaction response to listen
* @property {InteractionResponse} [interactionResponse] The interaction response to listen
* to message component interactions from
*/

View file

@ -1,5 +1,6 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { InteractionType } = require('discord-api-types/v10');
const { DiscordjsError, ErrorCodes } = require('../errors');
@ -21,6 +22,24 @@ class InteractionResponse {
this.client = interaction.client;
}
/**
* The timestamp the interaction response was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
}
/**
* The time the interaction response was created at
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* Collects a single component interaction that passes the filter.
* The Promise will reject if the time expires.
@ -51,6 +70,31 @@ class InteractionResponse {
interactionType: InteractionType.MessageComponent,
});
}
/**
* Fetches the response as a {@link Message} object.
* @returns {Promise<Message>}
*/
fetch() {
return this.interaction.fetchReply();
}
/**
* Deletes the response.
* @returns {Promise<void>}
*/
delete() {
return this.interaction.deleteReply();
}
/**
* Edits the response.
* @param {string|MessagePayload|WebhookMessageEditOptions} options The new options for the response.
* @returns {Promise<Message>}
*/
edit(options) {
return this.interaction.editReply(options);
}
}
// eslint-disable-next-line import/order

View file

@ -45,7 +45,7 @@ class InteractionWebhook {
/**
* 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
* @param {string|MessagePayload|WebhookMessageEditOptions} options The options to provide
* @returns {Promise<Message>} Returns the message edited by this webhook
*/

View file

@ -15,8 +15,7 @@ class InviteGuild extends AnonymousGuild {
* The welcome screen for this invite guild
* @type {?WelcomeScreen}
*/
this.welcomeScreen =
typeof data.welcome_screen !== 'undefined' ? new WelcomeScreen(this, data.welcome_screen) : null;
this.welcomeScreen = data.welcome_screen !== undefined ? new WelcomeScreen(this, data.welcome_screen) : null;
}
}

View file

@ -1,6 +1,7 @@
'use strict';
const { MentionableSelectMenuBuilder: BuildersMentionableSelectMenu, isJSONEncodable } = require('@discordjs/builders');
const { MentionableSelectMenuBuilder: BuildersMentionableSelectMenu } = require('@discordjs/builders');
const { isJSONEncodable } = require('@discordjs/util');
const { toSnakeCase } = require('../util/Transformers');
/**
@ -13,15 +14,13 @@ class MentionableSelectMenuBuilder extends BuildersMentionableSelectMenu {
}
/**
* Creates a new select menu builder from json data
* @param {JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent} other The other data
* Creates a new select menu builder from JSON data
* @param {MentionableSelectMenuBuilder|MentionableSelectMenuComponent|APIMentionableSelectComponent} other
* The other data
* @returns {MentionableSelectMenuBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
return new this(isJSONEncodable(other) ? other.toJSON() : other);
}
}
@ -29,5 +28,5 @@ module.exports = MentionableSelectMenuBuilder;
/**
* @external BuildersMentionableSelectMenu
* @see {@link https://discord.js.org/#/docs/builders/main/class/MentionableSelectMenuBuilder}
* @see {@link https://discord.js.org/docs/packages/builders/stable/MentionableSelectMenuBuilder:Class}
*/

View file

@ -22,7 +22,7 @@ const { Sticker } = require('./Sticker');
const { DiscordjsError, ErrorCodes } = require('../errors');
const ReactionManager = require('../managers/ReactionManager');
const { createComponent } = require('../util/Components');
const { NonSystemMessageTypes, MaxBulkDeletableMessageAge } = require('../util/Constants');
const { NonSystemMessageTypes, MaxBulkDeletableMessageAge, DeletableMessageTypes } = require('../util/Constants');
const MessageFlagsBitField = require('../util/MessageFlagsBitField');
const PermissionsBitField = require('../util/PermissionsBitField');
const { cleanContent, resolvePartialEmoji } = require('../util/Util');
@ -198,6 +198,31 @@ class Message extends Base {
this.position ??= null;
}
if ('role_subscription_data' in data) {
/**
* Role subscription data found on {@link MessageType.RoleSubscriptionPurchase} messages.
* @typedef {Object} RoleSubscriptionData
* @property {Snowflake} roleSubscriptionListingId The id of the SKU and listing the user is subscribed to
* @property {string} tierName The name of the tier the user is subscribed to
* @property {number} totalMonthsSubscribed The total number of months the user has been subscribed for
* @property {boolean} isRenewal Whether this notification is a renewal
*/
/**
* The data of the role subscription purchase or renewal.
* <info>This is present on {@link MessageType.RoleSubscriptionPurchase} messages.</info>
* @type {?RoleSubscriptionData}
*/
this.roleSubscriptionData = {
roleSubscriptionListingId: data.role_subscription_data.role_subscription_listing_id,
tierName: data.role_subscription_data.tier_name,
totalMonthsSubscribed: data.role_subscription_data.total_months_subscribed,
isRenewal: data.role_subscription_data.is_renewal,
};
} else {
this.roleSubscriptionData ??= null;
}
// Discord sends null if the message has not been edited
if (data.edited_timestamp) {
/**
@ -543,7 +568,7 @@ class Message extends Base {
* @property {ComponentType} [componentType] The type of component interaction to collect
* @property {number} [idle] Time to wait without another message component interaction before ending the collector
* @property {boolean} [dispose] Whether to remove the message component interaction after collecting
* @property {InteractionResponse} [InteractionResponse] The interaction response to collect interactions from
* @property {InteractionResponse} [interactionResponse] The interaction response to collect interactions from
*/
/**
@ -577,11 +602,17 @@ class Message extends Base {
*/
get editable() {
const precheck = Boolean(this.author.id === this.client.user.id && (!this.guild || this.channel?.viewable));
// Regardless of permissions thread messages cannot be edited if
// the thread is locked.
// the thread is archived or the thread is locked and the bot does not have permission to manage threads.
if (this.channel?.isThread()) {
return precheck && !this.channel.locked;
if (this.channel.archived) return false;
if (this.channel.locked) {
const permissions = this.channel.permissionsFor(this.client.user);
if (!permissions?.has(PermissionFlagsBits.ManageThreads, true)) return false;
}
}
return precheck;
}
@ -591,6 +622,8 @@ class Message extends Base {
* @readonly
*/
get deletable() {
if (!DeletableMessageTypes.includes(this.type)) return false;
if (!this.guild) {
return this.author.id === this.client.user.id;
}
@ -604,10 +637,10 @@ class Message extends Base {
// This flag allows deleting even if timed out
if (permissions.has(PermissionFlagsBits.Administrator, false)) return true;
return Boolean(
this.author.id === this.client.user.id ||
(permissions.has(PermissionFlagsBits.ManageMessages, false) &&
this.guild.members.me.communicationDisabledUntilTimestamp < Date.now()),
// The auto moderation action message author is the reference message author
return (
(this.type !== MessageType.AutoModerationAction && this.author.id === this.client.user.id) ||
(permissions.has(PermissionFlagsBits.ManageMessages, false) && !this.guild.members.me.isCommunicationDisabled())
);
}
@ -620,12 +653,11 @@ class Message extends Base {
* channel.bulkDelete(messages.filter(message => message.bulkDeletable));
*/
get bulkDeletable() {
const permissions = this.channel?.permissionsFor(this.client.user);
return (
(this.inGuild() &&
Date.now() - this.createdTimestamp < MaxBulkDeletableMessageAge &&
this.deletable &&
permissions?.has(PermissionFlagsBits.ManageMessages, false)) ??
this.channel?.permissionsFor(this.client.user).has(PermissionFlagsBits.ManageMessages, false)) ??
false
);
}
@ -761,9 +793,9 @@ class Message extends Base {
return this.client.actions.MessageReactionAdd.handle(
{
user: this.client.user,
channel: this.channel,
message: this,
[this.client.actions.injectedUser]: this.client.user,
[this.client.actions.injectedChannel]: this.channel,
[this.client.actions.injectedMessage]: this,
emoji: resolvePartialEmoji(emoji),
},
true,
@ -790,7 +822,6 @@ class Message extends Base {
* @typedef {BaseMessageCreateOptions} MessageReplyOptions
* @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 {StickerResolvable[]} [stickers=[]] Stickers to send in the message
*/
/**

View file

@ -1,8 +1,7 @@
'use strict';
const { Buffer } = require('node:buffer');
const { isJSONEncodable } = require('@discordjs/builders');
const { lazy } = require('@discordjs/util');
const { lazy, isJSONEncodable } = require('@discordjs/util');
const { MessageFlags } = require('discord-api-types/v10');
const ActionRowBuilder = require('./ActionRowBuilder');
const { DiscordjsRangeError, ErrorCodes } = require('../errors');
@ -107,7 +106,7 @@ class MessagePayload {
let content;
if (this.options.content === null) {
content = '';
} else if (typeof this.options.content !== 'undefined') {
} else if (this.options.content !== undefined) {
content = verifyString(this.options.content, DiscordjsRangeError, ErrorCodes.MessageContentType, true);
}
@ -127,7 +126,7 @@ class MessagePayload {
const tts = Boolean(this.options.tts);
let nonce;
if (typeof this.options.nonce !== 'undefined') {
if (this.options.nonce !== undefined) {
nonce = this.options.nonce;
if (typeof nonce === 'number' ? !Number.isInteger(nonce) : typeof nonce !== 'string') {
throw new DiscordjsRangeError(ErrorCodes.MessageNonceType);
@ -147,8 +146,8 @@ class MessagePayload {
let flags;
if (
typeof this.options.flags !== 'undefined' ||
(this.isMessage && typeof this.options.reply === 'undefined') ||
this.options.flags !== undefined ||
(this.isMessage && this.options.reply === undefined) ||
this.isMessageManager
) {
flags =
@ -163,11 +162,11 @@ class MessagePayload {
}
let allowedMentions =
typeof this.options.allowedMentions === 'undefined'
this.options.allowedMentions === undefined
? this.target.client.options.allowedMentions
: this.options.allowedMentions;
if (typeof allowedMentions?.repliedUser !== 'undefined') {
if (allowedMentions?.repliedUser !== undefined) {
allowedMentions = { ...allowedMentions, replied_user: allowedMentions.repliedUser };
delete allowedMentions.repliedUser;
}
@ -204,8 +203,7 @@ class MessagePayload {
components,
username,
avatar_url: avatarURL,
allowed_mentions:
typeof content === 'undefined' && typeof message_reference === 'undefined' ? undefined : allowedMentions,
allowed_mentions: content === undefined && message_reference === undefined ? undefined : allowedMentions,
flags,
message_reference,
attachments: this.options.attachments,
@ -228,8 +226,7 @@ class MessagePayload {
/**
* Resolves a single file into an object sendable to the API.
* @param {BufferResolvable|Stream|JSONEncodable<AttachmentPayload>} fileLike Something that could
* be resolved to a file
* @param {AttachmentPayload|BufferResolvable|Stream} fileLike Something that could be resolved to a file
* @returns {Promise<RawFile>}
*/
static async resolveFile(fileLike) {
@ -287,7 +284,7 @@ module.exports = MessagePayload;
/**
* A possible payload option.
* @typedef {MessageCreateOptions|MessageEditOptions|WebhookCreateMessageOptions|WebhookEditMessageOptions|
* @typedef {MessageCreateOptions|MessageEditOptions|WebhookMessageCreateOptions|WebhookMessageEditOptions|
* InteractionReplyOptions|InteractionUpdateOptions} MessagePayloadOption
*/
@ -298,5 +295,5 @@ module.exports = MessagePayload;
/**
* @external RawFile
* @see {@link https://discord.js.org/#/docs/rest/main/typedef/RawFile}
* @see {@link https://discord.js.org/docs/packages/rest/stable/RawFile:Interface}
*/

View file

@ -117,6 +117,10 @@ class MessageReaction {
return flatten(this, { emoji: 'emojiId', message: 'messageId' });
}
valueOf() {
return this._emoji.id ?? this._emoji.name;
}
_add(user) {
if (this.partial) return;
this.users.cache.set(user.id, user);

View file

@ -1,6 +1,7 @@
'use strict';
const { ModalBuilder: BuildersModal, ComponentBuilder, isJSONEncodable } = require('@discordjs/builders');
const { ModalBuilder: BuildersModal, ComponentBuilder } = require('@discordjs/builders');
const { isJSONEncodable } = require('@discordjs/util');
const { toSnakeCase } = require('../util/Transformers');
/**
@ -17,14 +18,11 @@ class ModalBuilder extends BuildersModal {
/**
* Creates a new modal builder from JSON data
* @param {JSONEncodable<APIModalComponent>|APIModalComponent} other The other data
* @param {ModalBuilder|APIModalComponent} other The other data
* @returns {ModalBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
return new this(isJSONEncodable(other) ? other.toJSON() : other);
}
}
@ -32,5 +30,5 @@ module.exports = ModalBuilder;
/**
* @external BuildersModal
* @see {@link https://discord.js.org/#/docs/builders/main/class/ModalBuilder}
* @see {@link https://discord.js.org/docs/packages/builders/stable/ModalBuilder:Class}
*/

View file

@ -11,13 +11,13 @@ class ModalSubmitFields {
constructor(components) {
/**
* The components within the modal
* @type {ActionRowModalData[]} The components in the modal
* @type {ActionRowModalData[]}
*/
this.components = components;
/**
* The extracted fields from the modal
* @type {Collection<string, ModalData>} The fields in the modal
* @type {Collection<string, ModalData>}
*/
this.fields = components.reduce((accumulator, next) => {
next.components.forEach(c => accumulator.set(c.customId, c));

View file

@ -141,6 +141,12 @@ class Presence extends Base {
*/
class Activity {
constructor(presence, data) {
/**
* The presence of the Activity
* @type {Presence}
* @readonly
* @name Activity#presence
*/
Object.defineProperty(this, 'presence', { value: presence });
/**
@ -287,6 +293,12 @@ class Activity {
*/
class RichPresenceAssets {
constructor(activity, assets) {
/**
* The activity of the RichPresenceAssets
* @type {Activity}
* @readonly
* @name RichPresenceAssets#activity
*/
Object.defineProperty(this, 'activity', { value: activity });
/**

View file

@ -107,6 +107,9 @@ class Role extends Base {
* @property {Snowflake} [botId] The id of the bot this role belongs to
* @property {Snowflake|string} [integrationId] The id of the integration this role belongs to
* @property {true} [premiumSubscriberRole] Whether this is the guild's premium subscription role
* @property {Snowflake} [subscriptionListingId] The id of this role's subscription SKU and listing
* @property {true} [availableForPurchase] Whether this role is available for purchase
* @property {true} [guildConnections] Whether this role is a guild's linked role
*/
this.tags = data.tags ? {} : null;
if (data.tags) {
@ -119,6 +122,15 @@ class Role extends Base {
if ('premium_subscriber' in data.tags) {
this.tags.premiumSubscriberRole = true;
}
if ('subscription_listing_id' in data.tags) {
this.tags.subscriptionListingId = data.tags.subscription_listing_id;
}
if ('available_for_purchase' in data.tags) {
this.tags.availableForPurchase = true;
}
if ('guild_connections' in data.tags) {
this.tags.guildConnections = true;
}
}
}
@ -176,8 +188,14 @@ class Role extends Base {
* @readonly
*/
get position() {
const sorted = this.guild._sortedRoles();
return [...sorted.values()].indexOf(sorted.get(this.id));
return this.guild.roles.cache.reduce(
(acc, role) =>
acc +
(this.rawPosition === role.rawPosition
? BigInt(this.id) > BigInt(role.id)
: this.rawPosition > role.rawPosition),
0,
);
}
/**
@ -185,6 +203,10 @@ class Role extends Base {
* @param {RoleResolvable} role Role to compare to this one
* @returns {number} Negative number if this role's position is lower (other role's is higher),
* positive number if this one is higher (other's is lower), 0 if equal
* @example
* // Compare the position of a role to another
* const roleCompare = role.comparePositionTo(otherRole);
* if (roleCompare >= 1) console.log(`${role.name} is higher than ${otherRole.name}`);
*/
comparePositionTo(role) {
return this.guild.roles.comparePositions(this, role);
@ -207,7 +229,7 @@ class Role extends Base {
/**
* Edits the role.
* @param {EditRoleOptions} data The new data for the role
* @param {RoleEditOptions} options The options to provide
* @returns {Promise<Role>}
* @example
* // Edit a role
@ -215,8 +237,8 @@ class Role extends Base {
* .then(updated => console.log(`Edited role name to ${updated.name}`))
* .catch(console.error);
*/
edit(data) {
return this.guild.roles.edit(this, data);
edit(options) {
return this.guild.roles.edit(this, options);
}
/**

View file

@ -1,6 +1,7 @@
'use strict';
const { RoleSelectMenuBuilder: BuildersRoleSelectMenu, isJSONEncodable } = require('@discordjs/builders');
const { RoleSelectMenuBuilder: BuildersRoleSelectMenu } = require('@discordjs/builders');
const { isJSONEncodable } = require('@discordjs/util');
const { toSnakeCase } = require('../util/Transformers');
/**
@ -13,15 +14,12 @@ class RoleSelectMenuBuilder extends BuildersRoleSelectMenu {
}
/**
* Creates a new select menu builder from json data
* @param {JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent} other The other data
* Creates a new select menu builder from JSON data
* @param {RoleSelectMenuBuilder|RoleSelectMenuComponent|APIRoleSelectComponent} other The other data
* @returns {RoleSelectMenuBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
return new this(isJSONEncodable(other) ? other.toJSON() : other);
}
}
@ -29,5 +27,5 @@ module.exports = RoleSelectMenuBuilder;
/**
* @external BuildersRoleSelectMenu
* @see {@link https://discord.js.org/#/docs/builders/main/class/RoleSelectMenuBuilder}
* @see {@link https://discord.js.org/docs/packages/builders/stable/RoleSelectMenuBuilder:Class}
*/

View file

@ -15,7 +15,7 @@ class SelectMenuBuilder extends StringSelectMenuBuilder {
if (!deprecationEmitted) {
process.emitWarning(
'The SelectMenuBuilder class is deprecated, use StringSelectMenuBuilder instead.',
'The SelectMenuBuilder class is deprecated. Use StringSelectMenuBuilder instead.',
'DeprecationWarning',
);
deprecationEmitted = true;

View file

@ -15,7 +15,7 @@ class SelectMenuComponent extends StringSelectMenuComponent {
if (!deprecationEmitted) {
process.emitWarning(
'The SelectMenuComponent class is deprecated, use StringSelectMenuComponent instead.',
'The SelectMenuComponent class is deprecated. Use StringSelectMenuComponent instead.',
'DeprecationWarning',
);
deprecationEmitted = true;

View file

@ -15,7 +15,7 @@ class SelectMenuInteraction extends StringSelectMenuInteraction {
if (!deprecationEmitted) {
process.emitWarning(
'The SelectMenuInteraction class is deprecated, use StringSelectMenuInteraction instead.',
'The SelectMenuInteraction class is deprecated. Use StringSelectMenuInteraction instead.',
'DeprecationWarning',
);
deprecationEmitted = true;

View file

@ -15,7 +15,7 @@ class SelectMenuOptionBuilder extends StringSelectMenuOptionBuilder {
if (!deprecationEmitted) {
process.emitWarning(
'The SelectMenuOptionBuilder class is deprecated, use StringSelectMenuOptionBuilder instead.',
'The SelectMenuOptionBuilder class is deprecated. Use StringSelectMenuOptionBuilder instead.',
'DeprecationWarning',
);
deprecationEmitted = true;

View file

@ -41,11 +41,11 @@ class StageChannel extends BaseGuildVoiceChannel {
* Sets a new topic for the guild channel.
* @param {?string} topic The new topic for the guild channel
* @param {string} [reason] Reason for changing the guild channel's topic
* @returns {Promise<GuildChannel>}
* @returns {Promise<StageChannel>}
* @example
* // Set a new channel topic
* channel.setTopic('needs more rate limiting')
* .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`))
* stageChannel.setTopic('needs more rate limiting')
* .then(channel => console.log(`Channel's new topic is ${channel.topic}`))
* .catch(console.error);
*/
setTopic(topic, reason) {
@ -53,6 +53,21 @@ class StageChannel extends BaseGuildVoiceChannel {
}
}
/**
* Sets the bitrate of the channel.
* @method setBitrate
* @memberof StageChannel
* @instance
* @param {number} bitrate The new bitrate
* @param {string} [reason] Reason for changing the channel's bitrate
* @returns {Promise<StageChannel>}
* @example
* // Set the bitrate of a voice channel
* stageChannel.setBitrate(48_000)
* .then(channel => console.log(`Set bitrate to ${channel.bitrate}bps for ${channel.name}`))
* .catch(console.error);
*/
/**
* Sets the RTC region of the channel.
* @method setRTCRegion
@ -69,4 +84,29 @@ class StageChannel extends BaseGuildVoiceChannel {
* stageChannel.setRTCRegion(null, 'We want to let Discord decide.');
*/
/**
* Sets the user limit of the channel.
* @method setUserLimit
* @memberof StageChannel
* @instance
* @param {number} userLimit The new user limit
* @param {string} [reason] Reason for changing the user limit
* @returns {Promise<StageChannel>}
* @example
* // Set the user limit of a voice channel
* stageChannel.setUserLimit(42)
* .then(channel => console.log(`Set user limit to ${channel.userLimit} for ${channel.name}`))
* .catch(console.error);
*/
/**
* Sets the camera video quality mode of the channel.
* @method setVideoQualityMode
* @memberof StageChannel
* @instance
* @param {VideoQualityMode} videoQualityMode The new camera video quality mode.
* @param {string} [reason] Reason for changing the camera video quality mode.
* @returns {Promise<StageChannel>}
*/
module.exports = StageChannel;

View file

@ -1,9 +1,10 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes, StickerFormatType } = require('discord-api-types/v10');
const { Routes } = require('discord-api-types/v10');
const Base = require('./Base');
const { DiscordjsError, ErrorCodes } = require('../errors');
const { StickerFormatExtensionMap } = require('../util/Constants');
/**
* Represents a Sticker.
@ -164,7 +165,7 @@ class Sticker extends Base {
* @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, StickerFormatExtensionMap[this.format]);
}
/**
@ -197,7 +198,7 @@ class Sticker extends Base {
/**
* Data for editing a sticker.
* @typedef {Object} GuildStickerEditData
* @typedef {Object} GuildStickerEditOptions
* @property {string} [name] The name of the sticker
* @property {?string} [description] The description of the sticker
* @property {string} [tags] The Discord name of a unicode emoji representing the sticker's expression
@ -206,7 +207,7 @@ class Sticker extends Base {
/**
* Edits the sticker.
* @param {GuildStickerEditData} data The new data for the sticker
* @param {GuildStickerEditOptions} options The options to provide
* @returns {Promise<Sticker>}
* @example
* // Update the name of a sticker
@ -214,8 +215,8 @@ class Sticker extends Base {
* .then(s => console.log(`Updated the name of the sticker to ${s.name}`))
* .catch(console.error);
*/
edit(data) {
return this.guild.stickers.edit(this, data);
edit(options) {
return this.guild.stickers.edit(this, options);
}
/**

View file

@ -1,6 +1,7 @@
'use strict';
const { SelectMenuBuilder: BuildersSelectMenu, isJSONEncodable, normalizeArray } = require('@discordjs/builders');
const { SelectMenuBuilder: BuildersSelectMenu, normalizeArray } = require('@discordjs/builders');
const { isJSONEncodable } = require('@discordjs/util');
const { toSnakeCase } = require('../util/Transformers');
const { resolvePartialEmoji } = require('../util/Util');
@ -23,7 +24,7 @@ class StringSelectMenuBuilder extends BuildersSelectMenu {
/**
* Normalizes a select menu option emoji
* @param {SelectMenuOptionData|JSONEncodable<APISelectMenuOption>} selectMenuOption The option to normalize
* @param {SelectMenuOptionData|APISelectMenuOption} selectMenuOption The option to normalize
* @returns {SelectMenuOptionBuilder|APISelectMenuOption}
* @private
*/
@ -59,7 +60,7 @@ class StringSelectMenuBuilder extends BuildersSelectMenu {
/**
* Creates a new select menu builder from json data
* @param {JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent} other The other data
* @param {StringSelectMenuBuilder|StringSelectMenuComponent|APIStringSelectComponent} other The other data
* @returns {StringSelectMenuBuilder}
*/
static from(other) {
@ -74,5 +75,5 @@ module.exports = StringSelectMenuBuilder;
/**
* @external BuildersSelectMenu
* @see {@link https://discord.js.org/#/docs/builders/main/class/SelectMenuBuilder}
* @see {@link https://discord.js.org/docs/packages/builders/stable/StringSelectMenuBuilder:Class}
*/

View file

@ -1,6 +1,7 @@
'use strict';
const { SelectMenuOptionBuilder: BuildersSelectMenuOption, isJSONEncodable } = require('@discordjs/builders');
const { SelectMenuOptionBuilder: BuildersSelectMenuOption } = require('@discordjs/builders');
const { isJSONEncodable } = require('@discordjs/util');
const { toSnakeCase } = require('../util/Transformers');
const { resolvePartialEmoji } = require('../util/Util');
@ -32,14 +33,11 @@ class StringSelectMenuOptionBuilder extends BuildersSelectMenuOption {
/**
* Creates a new select menu option builder from JSON data
* @param {JSONEncodable<APISelectMenuOption>|APISelectMenuOption} other The other data
* @param {StringSelectMenuOptionBuilder|APISelectMenuOption} other The other data
* @returns {StringSelectMenuOptionBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
return new this(isJSONEncodable(other) ? other.toJSON() : other);
}
}
@ -47,5 +45,5 @@ module.exports = StringSelectMenuOptionBuilder;
/**
* @external BuildersSelectMenuOption
* @see {@link https://discord.js.org/#/docs/builders/main/class/SelectMenuOptionBuilder}
* @see {@link https://discord.js.org/docs/packages/builders/stable/SelectMenuOptionBuilder:Class}
*/

View file

@ -1,6 +1,7 @@
'use strict';
const { TextInputBuilder: BuildersTextInput, isJSONEncodable } = require('@discordjs/builders');
const { TextInputBuilder: BuildersTextInput } = require('@discordjs/builders');
const { isJSONEncodable } = require('@discordjs/util');
const { toSnakeCase } = require('../util/Transformers');
/**
@ -14,14 +15,11 @@ class TextInputBuilder extends BuildersTextInput {
/**
* Creates a new text input builder from JSON data
* @param {JSONEncodable<APITextInputComponent>|APITextInputComponent} other The other data
* @param {TextInputBuilder|TextInputComponent|APITextInputComponent} other The other data
* @returns {TextInputBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
return new this(isJSONEncodable(other) ? other.toJSON() : other);
}
}
@ -29,5 +27,5 @@ module.exports = TextInputBuilder;
/**
* @external BuildersTextInput
* @see {@link https://discord.js.org/#/docs/builders/main/class/TextInputBuilder}
* @see {@link https://discord.js.org/docs/packages/builders/stable/TextInputBuilder:Class}
*/

View file

@ -14,7 +14,7 @@ const ChannelFlagsBitField = require('../util/ChannelFlagsBitField');
* @implements {TextBasedChannel}
*/
class ThreadChannel extends BaseChannel {
constructor(guild, data, client, fromInteraction = false) {
constructor(guild, data, client) {
super(guild?.client ?? client, data, false);
/**
@ -40,12 +40,14 @@ class ThreadChannel extends BaseChannel {
* @type {ThreadMemberManager}
*/
this.members = new ThreadMemberManager(this);
if (data) this._patch(data, fromInteraction);
if (data) this._patch(data);
}
_patch(data, partial = false) {
_patch(data) {
super._patch(data);
if ('message' in data) this.messages._add(data.message);
if ('name' in data) {
/**
* The name of the thread
@ -147,7 +149,7 @@ class ThreadChannel extends BaseChannel {
this.lastPinTimestamp ??= null;
}
if ('rate_limit_per_user' in data || !partial) {
if ('rate_limit_per_user' in data) {
/**
* The rate limit per user (slowmode) for this thread in seconds
* @type {?number}
@ -316,7 +318,7 @@ class ThreadChannel extends BaseChannel {
/**
* The options used to edit a thread channel
* @typedef {Object} ThreadEditData
* @typedef {Object} ThreadEditOptions
* @property {string} [name] The new name for the thread
* @property {boolean} [archived] Whether the thread is archived
* @property {ThreadAutoArchiveDuration} [autoArchiveDuration] The amount of time after which the thread
@ -324,15 +326,15 @@ class ThreadChannel extends BaseChannel {
* @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
* <info>Can only be edited on {@link ChannelType.PrivateThread}</info>
* @property {Snowflake[]} [appliedTags] The tags to apply to the thread
* @property {ChannelFlagsResolvable} [flags] The flags to set on the channel
* @property {string} [reason] Reason for editing the thread
* <info>Can only be edited on {@link ChannelType.PrivateThread}</info>
*/
/**
* Edits this thread.
* @param {ThreadEditData} data The new data for this thread
* @param {ThreadEditOptions} options The options to provide
* @returns {Promise<ThreadChannel>}
* @example
* // Edit a thread
@ -340,19 +342,19 @@ class ThreadChannel extends BaseChannel {
* .then(editedThread => console.log(editedThread))
* .catch(console.error);
*/
async edit(data) {
async edit(options) {
const newData = await this.client.rest.patch(Routes.channel(this.id), {
body: {
name: (data.name ?? this.name).trim(),
archived: data.archived,
auto_archive_duration: data.autoArchiveDuration,
rate_limit_per_user: data.rateLimitPerUser,
locked: data.locked,
invitable: this.type === ChannelType.PrivateThread ? data.invitable : undefined,
applied_tags: data.appliedTags,
flags: 'flags' in data ? ChannelFlagsBitField.resolve(data.flags) : undefined,
name: (options.name ?? this.name).trim(),
archived: options.archived,
auto_archive_duration: options.autoArchiveDuration,
rate_limit_per_user: options.rateLimitPerUser,
locked: options.locked,
invitable: this.type === ChannelType.PrivateThread ? options.invitable : undefined,
applied_tags: options.appliedTags,
flags: 'flags' in options ? ChannelFlagsBitField.resolve(options.flags) : undefined,
},
reason: data.reason,
reason: options.reason,
});
return this.client.actions.ChannelUpdate.handle(newData).updated;

Some files were not shown because too many files have changed in this diff Show more