Compare commits

...

56 Commits

Author SHA1 Message Date
iCrawl
e3cbd45e7d chore: release 2022-05-13 11:49:56 +02:00
Synbulat Biishev
ea28638a0c fix(MessageEmbed): fix a typo (#7906) 2022-05-12 10:24:54 +02:00
Almeida
43a7870b23 docs(shardingmanager): fix type of execArgv option (v13) (#7863) 2022-05-02 09:38:11 +02:00
Hyro
6dcf0bda05 docs: fix and improve localization docs (v13 backport) (#7807) 2022-04-21 19:06:28 +02:00
Almeida
816936eafb fix(GuildEditData): some fields can be null for v13 (#7633)
* fix(GuildEditData): some fields can be null for v13

* fix: make even more things nullable
2022-04-19 16:01:59 +02:00
Sasial
1d09ad4652 types: fix ModalSubmitInteraction (#7768) 2022-04-19 15:59:58 +02:00
Jiralite
5165b18b85 feat: backport (#7776) 2022-04-19 15:59:05 +02:00
Rodry
7afcd9594a types(threadchannel): fix autoArchiveDuration types (#7817) 2022-04-19 15:54:39 +02:00
Jiralite
b9802f4b6f refactor: deprecate v13 properties and methods (#7782)
* refactor: deprecate splitting

* refactor: deprecate `IntegrationApplication#summary`

https://github.com/discordjs/discord.js/pull/7729

* docs: amend store channel wording

* refactor: deprecate fetching of application assets

* docs: deprecate vip field in voice regions
2022-04-17 10:52:50 +02:00
Jiralite
1040ce0e71 docs(ApplicationCommand): Fix ApplicationCommandOptionChoice (#7798) 2022-04-17 10:47:34 +02:00
Jiralite
3eb45e30b3 feat: backport (#7787) 2022-04-14 12:48:31 +02:00
Jiralite
ab324ea6ae feat: backport (#7786) 2022-04-14 12:48:10 +02:00
Hyro
022e138b9a feat: add support for localized slash commands (v13 backport) (#7766) 2022-04-14 12:47:46 +02:00
Superchupu
9e4a900e6d feat: app authorization links and tags for v13 (#7731) 2022-04-14 12:47:11 +02:00
Jiralite
6c5613255a feat: backport (#7777) 2022-04-14 12:45:54 +02:00
Jiralite
ff49b82db7 feat: backport (#7778) 2022-04-14 12:45:35 +02:00
Jiralite
ae7f991e8d feat: backport (#7779) 2022-04-14 12:45:16 +02:00
Jiralite
cedc333940 feat: backport (#7783) 2022-04-14 12:44:24 +02:00
Jiralite
6daee1b235 feat(VoiceChannel): Support video_quality_mode (v13) (#7785) 2022-04-14 12:43:25 +02:00
Jiralite
68498a87be feat(StageInstance): add support for associated guild event (#7713) 2022-04-12 17:19:59 +02:00
Jiralite
ab6c2bad84 fix: apply v14 fix (#7756) 2022-04-12 17:11:57 +02:00
Almeida
c9e4562fd5 fix(GuildChannelManager): delete method accessing wrong id (#7771) 2022-04-12 17:08:57 +02:00
Ryan Munro
e1cdcfa9a6 feat(modals): modals, input text components and modal submits, v13 style (#7431) 2022-04-09 11:36:49 +02:00
Jiralite
5e8162a137 feat: Backport Interaction#isRepliable (#7563) 2022-04-09 11:36:15 +02:00
Rodry
9f09702854 feat: add methods to managers for v13 (#7611) 2022-04-09 11:35:17 +02:00
Jiralite
8e7d15e49d feat: Add premiumSubscriptionCount to InviteGuild (#7629) 2022-04-09 11:34:24 +02:00
Jiralite
b9c5676006 refactor: remove non-breaking stuff (#7636) 2022-04-09 11:33:44 +02:00
Almeida
dfea9c27ce fix(GuildScheduledEvent): handle missing image for v13 (#7627) 2022-03-24 20:59:19 +01:00
Jiralite
78140748ce types(InteractionCollector): Fix guild and channel types (#7624) 2022-03-10 09:00:58 +01:00
Ben
a7535a2232 feat(scheduledevents): Event cover images for v13 (#7613)
Co-authored-by: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com>
2022-03-07 19:26:57 +01:00
Rodry
7a52785f7d fix(messagementions): fix has method for v13 (#7591)
Co-authored-by: Almeida <almeidx@pm.me>
Co-authored-by: Synbulat Biishev <syjalo.dev@gmail.com>
2022-03-06 16:26:57 +01:00
Ben
13dd82d7fa fix: check if member has admininistrator on moderatable (v13) (#7578) 2022-03-02 10:38:04 +01:00
Jiralite
93cdb2f2fa feat: Backport MessageMentions channel type fixes (#7562) 2022-03-02 10:32:57 +01:00
Jiralite
611d3a7b2f feat: Backport cache types resolving to never (#7561) 2022-03-02 10:32:46 +01:00
Jiralite
29d42ed319 feat: Backport sending message flags (#7560) 2022-03-02 10:32:36 +01:00
Jiralite
1d97dcff08 feat(ThreadChannel): Backport creation timestamp (#7559) 2022-03-02 10:32:25 +01:00
Jiralite
679b87c4f8 feat: Add custom image support to version 13 (#7557) 2022-03-02 10:32:13 +01:00
Jiralite
b231bece0e feat: Backport reason on pin and unpin (#7556) 2022-03-02 10:32:03 +01:00
Jiralite
49397c0ca4 fix(ThreadChannel): Require sendable for unarchivable (#7555) 2022-03-02 10:31:51 +01:00
Jiralite
215dfe02d5 feat(GuildPreview): Add stickers to version 13 (#7554) 2022-03-02 10:31:41 +01:00
Jiralite
69ba067a65 docs: Backport version 13 fixes (#7552)
Co-authored-by: Rodry <38259440+ImRodry@users.noreply.github.com>
2022-03-02 10:31:28 +01:00
Jiralite
5f621c1995 fix: Backport MessageReaction#me being incorrectly false (#7553) 2022-03-02 10:30:13 +01:00
Jiralite
ee1698d928 feat: Backport sweepStickers method (#7558) 2022-03-02 10:29:59 +01:00
Ben
2fcf8af421 feat(scheduledevents): add image option (v13) (#7549) 2022-02-26 11:14:48 +01:00
EhsanFox
f0960698d2 fix(typings): sweepStageInstances typo (#7521) 2022-02-23 08:39:05 +01:00
ckohen
30baff7ecb fix(MessagePayload): v13 don't set reply flags to target flags (#7515) 2022-02-23 08:37:59 +01:00
Jiralite
2b3db734df feat(thread): v13 add newlyCreated to threadCreate event (#7481) 2022-02-20 13:42:23 +01:00
Jiralite
0b54089c43 types: V13 channel create overloads fix (#7480) 2022-02-20 13:39:20 +01:00
Jiralite
77b8e01911 fix(Shard): V13 EventEmitter listener warning (#7479) 2022-02-17 17:46:06 +01:00
Parbez
bc5ddc36fa fix(MessageEmbed): set footer to undefined (#7358) 2022-02-13 12:44:16 +01:00
Ryan Munro
5bcca8b97f feat(commands): attachment options (#7441) 2022-02-13 12:41:41 +01:00
iCrawl
988a51b764 chore(release): version 2022-01-13 18:24:17 +01:00
Rodry
1f4e633ce3 docs(interaction): add locale list link (#7261) 2022-01-13 18:20:35 +01:00
Suneet Tipirneni
233084a601 feat: add Locales to Interactions (#7131)
Co-authored-by: Rodry <38259440+ImRodry@users.noreply.github.com>
2022-01-13 18:18:02 +01:00
iCrawl
ac8c122c2a chore(release): version 2022-01-07 23:57:17 +01:00
ckohen
2dabd82e26 fix(sweepers): provide default for object param (#7182) 2022-01-07 23:53:27 +01:00
82 changed files with 5284 additions and 3455 deletions

1
.gitignore vendored
View File

@@ -28,3 +28,4 @@ docs/docs.json
.tmp/ .tmp/
.idea/ .idea/
.DS_Store .DS_Store
.yarn/

File diff suppressed because it is too large Load Diff

5030
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "discord.js", "name": "discord.js",
"version": "14.0.0-dev", "version": "13.7.0",
"description": "A powerful library for interacting with the Discord API", "description": "A powerful library for interacting with the Discord API",
"scripts": { "scripts": {
"test": "npm run lint && npm run docs:test && npm run lint:typings && npm run test:typescript", "test": "npm run lint && npm run docs:test && npm run lint:typings && npm run test:typescript",
@@ -13,7 +13,7 @@
"docs": "docgen --source src --custom docs/index.yml --output docs/docs.json", "docs": "docgen --source src --custom docs/index.yml --output docs/docs.json",
"docs:test": "docgen --source src --custom docs/index.yml", "docs:test": "docgen --source src --custom docs/index.yml",
"prepublishOnly": "npm run test", "prepublishOnly": "npm run test",
"changelog": "git cliff --prepend CHANGELOG.md -l" "changelog": "git cliff --prepend CHANGELOG.md -u"
}, },
"main": "./src/index.js", "main": "./src/index.js",
"types": "./typings/index.d.ts", "types": "./typings/index.d.ts",
@@ -50,36 +50,36 @@
}, },
"homepage": "https://discord.js.org", "homepage": "https://discord.js.org",
"dependencies": { "dependencies": {
"@discordjs/builders": "^0.11.0", "@discordjs/builders": "^0.13.0",
"@discordjs/collection": "^0.4.0", "@discordjs/collection": "^0.6.0",
"@sapphire/async-queue": "^1.1.9", "@sapphire/async-queue": "^1.3.1",
"@types/node-fetch": "^2.5.12", "@types/node-fetch": "^2.6.1",
"@types/ws": "^8.2.2", "@types/ws": "^8.5.3",
"discord-api-types": "^0.26.0", "discord-api-types": "^0.30.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"ws": "^8.4.0" "ws": "^8.6.0"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^16.0.1", "@commitlint/cli": "^16.2.4",
"@commitlint/config-angular": "^16.0.0", "@commitlint/config-angular": "^16.2.4",
"@discordjs/docgen": "^0.11.0", "@discordjs/docgen": "^0.11.1",
"@favware/npm-deprecate": "^1.0.4", "@favware/npm-deprecate": "^1.0.4",
"@types/node": "^16.11.12", "@types/node": "^16.11.12",
"conventional-changelog-cli": "^2.2.2", "conventional-changelog-cli": "^2.2.2",
"dtslint": "^4.2.1", "dtslint": "^4.2.1",
"eslint": "^8.5.0", "eslint": "^8.15.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.25.3", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"husky": "^7.0.4", "husky": "^8.0.1",
"is-ci": "^3.0.1", "is-ci": "^3.0.1",
"jest": "^27.4.5", "jest": "^28.1.0",
"lint-staged": "^12.1.4", "lint-staged": "^12.4.1",
"prettier": "^2.5.1", "prettier": "^2.6.2",
"tsd": "^0.19.0", "tsd": "^0.20.0",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"typescript": "^4.5.4" "typescript": "^4.6.4"
}, },
"engines": { "engines": {
"node": ">=16.6.0", "node": ">=16.6.0",

View File

@@ -6,6 +6,7 @@ const AutocompleteInteraction = require('../../structures/AutocompleteInteractio
const ButtonInteraction = require('../../structures/ButtonInteraction'); const ButtonInteraction = require('../../structures/ButtonInteraction');
const CommandInteraction = require('../../structures/CommandInteraction'); const CommandInteraction = require('../../structures/CommandInteraction');
const MessageContextMenuInteraction = require('../../structures/MessageContextMenuInteraction'); const MessageContextMenuInteraction = require('../../structures/MessageContextMenuInteraction');
const ModalSubmitInteraction = require('../../structures/ModalSubmitInteraction');
const SelectMenuInteraction = require('../../structures/SelectMenuInteraction'); const SelectMenuInteraction = require('../../structures/SelectMenuInteraction');
const UserContextMenuInteraction = require('../../structures/UserContextMenuInteraction'); const UserContextMenuInteraction = require('../../structures/UserContextMenuInteraction');
const { Events, InteractionTypes, MessageComponentTypes, ApplicationCommandTypes } = require('../../util/Constants'); const { Events, InteractionTypes, MessageComponentTypes, ApplicationCommandTypes } = require('../../util/Constants');
@@ -17,9 +18,11 @@ class InteractionCreateAction extends Action {
const client = this.client; const client = this.client;
// Resolve and cache partial channels for Interaction#channel getter // Resolve and cache partial channels for Interaction#channel getter
this.getChannel(data); const channel = this.getChannel(data);
// Do not emit this for interactions that cache messages that are non-text-based.
let InteractionType; let InteractionType;
switch (data.type) { switch (data.type) {
case InteractionTypes.APPLICATION_COMMAND: case InteractionTypes.APPLICATION_COMMAND:
switch (data.data.type) { switch (data.data.type) {
@@ -30,6 +33,7 @@ class InteractionCreateAction extends Action {
InteractionType = UserContextMenuInteraction; InteractionType = UserContextMenuInteraction;
break; break;
case ApplicationCommandTypes.MESSAGE: case ApplicationCommandTypes.MESSAGE:
if (channel && !channel.isText()) return;
InteractionType = MessageContextMenuInteraction; InteractionType = MessageContextMenuInteraction;
break; break;
default: default:
@@ -41,6 +45,8 @@ class InteractionCreateAction extends Action {
} }
break; break;
case InteractionTypes.MESSAGE_COMPONENT: case InteractionTypes.MESSAGE_COMPONENT:
if (channel && !channel.isText()) return;
switch (data.data.component_type) { switch (data.data.component_type) {
case MessageComponentTypes.BUTTON: case MessageComponentTypes.BUTTON:
InteractionType = ButtonInteraction; InteractionType = ButtonInteraction;
@@ -59,6 +65,9 @@ class InteractionCreateAction extends Action {
case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE: case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE:
InteractionType = AutocompleteInteraction; InteractionType = AutocompleteInteraction;
break; break;
case InteractionTypes.MODAL_SUBMIT:
InteractionType = ModalSubmitInteraction;
break;
default: default:
client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`); client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`);
return; return;

View File

@@ -13,8 +13,9 @@ class ThreadCreateAction extends Action {
* Emitted whenever a thread is created or when the client user is added to a thread. * Emitted whenever a thread is created or when the client user is added to a thread.
* @event Client#threadCreate * @event Client#threadCreate
* @param {ThreadChannel} thread The thread that was created * @param {ThreadChannel} thread The thread that was created
* @param {boolean} newlyCreated Whether the thread was newly created
*/ */
client.emit(Events.THREAD_CREATE, thread); client.emit(Events.THREAD_CREATE, thread, data.newly_created ?? false);
} }
return { thread }; return { thread };
} }

View File

@@ -58,6 +58,14 @@ const Messages = {
SELECT_OPTION_VALUE: 'MessageSelectOption value must be a string', SELECT_OPTION_VALUE: 'MessageSelectOption value must be a string',
SELECT_OPTION_DESCRIPTION: 'MessageSelectOption description must be a string', SELECT_OPTION_DESCRIPTION: 'MessageSelectOption description must be a string',
TEXT_INPUT_CUSTOM_ID: 'TextInputComponent customId must be a string',
TEXT_INPUT_LABEL: 'TextInputComponent label must be a string',
TEXT_INPUT_PLACEHOLDER: 'TextInputComponent placeholder must be a string',
TEXT_INPUT_VALUE: 'TextInputComponent value must be a string',
MODAL_CUSTOM_ID: 'Modal customId must be a string',
MODAL_TITLE: 'Modal title must be a string',
INTERACTION_COLLECTOR_ERROR: reason => `Collector received no interactions before ending with reason: ${reason}`, INTERACTION_COLLECTOR_ERROR: reason => `Collector received no interactions before ending with reason: ${reason}`,
FILE_NOT_FOUND: file => `File could not be found: ${file}`, FILE_NOT_FOUND: file => `File could not be found: ${file}`,
@@ -148,6 +156,10 @@ const Messages = {
COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP: 'No subcommand group specified for interaction.', COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP: 'No subcommand group specified for interaction.',
AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION: 'No focused option for autocomplete interaction.', AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION: 'No focused option for autocomplete interaction.',
MODAL_SUBMIT_INTERACTION_FIELD_NOT_FOUND: customId => `Required field with custom id "${customId}" not found.`,
MODAL_SUBMIT_INTERACTION_FIELD_TYPE: (customId, type, expected) =>
`Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`,
INVITE_MISSING_SCOPES: 'At least one valid scope must be provided for the invite', INVITE_MISSING_SCOPES: 'At least one valid scope must be provided for the invite',
NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`, NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`,

View File

@@ -122,6 +122,8 @@ exports.MessageMentions = require('./structures/MessageMentions');
exports.MessagePayload = require('./structures/MessagePayload'); exports.MessagePayload = require('./structures/MessagePayload');
exports.MessageReaction = require('./structures/MessageReaction'); exports.MessageReaction = require('./structures/MessageReaction');
exports.MessageSelectMenu = require('./structures/MessageSelectMenu'); exports.MessageSelectMenu = require('./structures/MessageSelectMenu');
exports.Modal = require('./structures/Modal');
exports.ModalSubmitInteraction = require('./structures/ModalSubmitInteraction');
exports.NewsChannel = require('./structures/NewsChannel'); exports.NewsChannel = require('./structures/NewsChannel');
exports.OAuth2Guild = require('./structures/OAuth2Guild'); exports.OAuth2Guild = require('./structures/OAuth2Guild');
exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel'); exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel');
@@ -140,6 +142,7 @@ exports.StoreChannel = require('./structures/StoreChannel');
exports.Team = require('./structures/Team'); exports.Team = require('./structures/Team');
exports.TeamMember = require('./structures/TeamMember'); exports.TeamMember = require('./structures/TeamMember');
exports.TextChannel = require('./structures/TextChannel'); exports.TextChannel = require('./structures/TextChannel');
exports.TextInputComponent = require('./structures/TextInputComponent');
exports.ThreadChannel = require('./structures/ThreadChannel'); exports.ThreadChannel = require('./structures/ThreadChannel');
exports.ThreadMember = require('./structures/ThreadMember'); exports.ThreadMember = require('./structures/ThreadMember');
exports.Typing = require('./structures/Typing'); exports.Typing = require('./structures/Typing');

View File

@@ -64,6 +64,8 @@ class ApplicationCommandManager extends CachedManager {
* Options used to fetch Application Commands from Discord * Options used to fetch Application Commands from Discord
* @typedef {BaseFetchOptions} FetchApplicationCommandOptions * @typedef {BaseFetchOptions} FetchApplicationCommandOptions
* @property {Snowflake} [guildId] The guild's id to fetch commands for, for when the guild is not cached * @property {Snowflake} [guildId] The guild's id to fetch commands for, for when the guild is not cached
* @property {LocaleString} [locale] The locale to use when fetching this command
* @property {boolean} [withLocalizations] Whether to fetch all localization data
*/ */
/** /**
@@ -82,9 +84,9 @@ class ApplicationCommandManager extends CachedManager {
* .then(commands => console.log(`Fetched ${commands.size} commands`)) * .then(commands => console.log(`Fetched ${commands.size} commands`))
* .catch(console.error); * .catch(console.error);
*/ */
async fetch(id, { guildId, cache = true, force = false } = {}) { async fetch(id, { guildId, cache = true, force = false, locale, withLocalizations } = {}) {
if (typeof id === 'object') { if (typeof id === 'object') {
({ guildId, cache = true } = id); ({ guildId, cache = true, locale, withLocalizations } = id);
} else if (id) { } else if (id) {
if (!force) { if (!force) {
const existing = this.cache.get(id); const existing = this.cache.get(id);
@@ -94,7 +96,15 @@ class ApplicationCommandManager extends CachedManager {
return this._add(command, cache); return this._add(command, cache);
} }
const data = await this.commandPath({ guildId }).get(); const data = await this.commandPath({ guildId }).get({
headers: {
'X-Discord-Locale': locale,
},
query:
typeof withLocalizations === 'boolean'
? new URLSearchParams({ with_localizations: withLocalizations })
: undefined,
});
return data.reduce((coll, command) => coll.set(command.id, this._add(command, cache, guildId)), new Collection()); return data.reduce((coll, command) => coll.set(command.id, this._add(command, cache, guildId)), new Collection());
} }
@@ -206,7 +216,9 @@ class ApplicationCommandManager extends CachedManager {
static transformCommand(command) { static transformCommand(command) {
return { return {
name: command.name, name: command.name,
name_localizations: command.nameLocalizations ?? command.name_localizations,
description: command.description, description: command.description,
description_localizations: command.descriptionLocalizations ?? command.description_localizations,
type: typeof command.type === 'number' ? command.type : ApplicationCommandTypes[command.type], type: typeof command.type === 'number' ? command.type : ApplicationCommandTypes[command.type],
options: command.options?.map(o => ApplicationCommand.transformOption(o)), options: command.options?.map(o => ApplicationCommand.transformOption(o)),
default_permission: command.defaultPermission ?? command.default_permission, default_permission: command.defaultPermission ?? command.default_permission,

View File

@@ -54,9 +54,12 @@ class GuildBanManager extends CachedManager {
*/ */
/** /**
* Options used to fetch all bans from a guild. * Options used to fetch multiple bans from a guild.
* @typedef {Object} FetchBansOptions * @typedef {Object} FetchBansOptions
* @property {boolean} cache Whether or not to cache the fetched bans * @property {number} [limit] The maximum number of bans to return
* @property {Snowflake} [before] Consider only bans before this id
* @property {Snowflake} [after] Consider only bans after this id
* @property {boolean} [cache] Whether to cache the fetched bans
*/ */
/** /**
@@ -64,13 +67,13 @@ class GuildBanManager extends CachedManager {
* @param {UserResolvable|FetchBanOptions|FetchBansOptions} [options] Options for fetching guild ban(s) * @param {UserResolvable|FetchBanOptions|FetchBansOptions} [options] Options for fetching guild ban(s)
* @returns {Promise<GuildBan|Collection<Snowflake, GuildBan>>} * @returns {Promise<GuildBan|Collection<Snowflake, GuildBan>>}
* @example * @example
* // Fetch all bans from a guild * // Fetch multiple bans from a guild
* guild.bans.fetch() * guild.bans.fetch()
* .then(console.log) * .then(console.log)
* .catch(console.error); * .catch(console.error);
* @example * @example
* // Fetch all bans from a guild without caching * // Fetch a maximum of 5 bans from a guild without caching
* guild.bans.fetch({ cache: false }) * guild.bans.fetch({ limit: 5, cache: false })
* .then(console.log) * .then(console.log)
* .catch(console.error); * .catch(console.error);
* @example * @example
@@ -91,14 +94,15 @@ class GuildBanManager extends CachedManager {
*/ */
fetch(options) { fetch(options) {
if (!options) return this._fetchMany(); if (!options) return this._fetchMany();
const user = this.client.users.resolveId(options); const { user, cache, force, limit, before, after } = options;
if (user) return this._fetchSingle({ user, cache: true }); const resolvedUser = this.client.users.resolveId(user ?? options);
options.user &&= this.client.users.resolveId(options.user); if (resolvedUser) return this._fetchSingle({ user: resolvedUser, cache, force });
if (!options.user) {
if ('cache' in options) return this._fetchMany(options.cache); if (!before && !after && !limit && typeof cache === 'undefined') {
return Promise.reject(new Error('FETCH_BAN_RESOLVE_ID')); return Promise.reject(new Error('FETCH_BAN_RESOLVE_ID'));
} }
return this._fetchSingle(options);
return this._fetchMany(options);
} }
async _fetchSingle({ user, cache, force = false }) { async _fetchSingle({ user, cache, force = false }) {
@@ -111,11 +115,13 @@ class GuildBanManager extends CachedManager {
return this._add(data, cache); return this._add(data, cache);
} }
async _fetchMany(cache) { async _fetchMany(options = {}) {
const data = await this.client.api.guilds(this.guild.id).bans.get(); const data = await this.client.api.guilds(this.guild.id).bans.get({
return data.reduce((col, ban) => col.set(ban.user.id, this._add(ban, cache)), new Collection()); query: options,
} });
return data.reduce((col, ban) => col.set(ban.user.id, this._add(ban, options.cache)), new Collection());
}
/** /**
* Options used to ban a user from a guild. * Options used to ban a user from a guild.
* @typedef {Object} BanOptions * @typedef {Object} BanOptions

View File

@@ -4,11 +4,15 @@ const process = require('node:process');
const { Collection } = require('@discordjs/collection'); const { Collection } = require('@discordjs/collection');
const CachedManager = require('./CachedManager'); const CachedManager = require('./CachedManager');
const ThreadManager = require('./ThreadManager'); const ThreadManager = require('./ThreadManager');
const { Error } = require('../errors'); const { Error, TypeError } = require('../errors');
const GuildChannel = require('../structures/GuildChannel'); const GuildChannel = require('../structures/GuildChannel');
const PermissionOverwrites = require('../structures/PermissionOverwrites'); const PermissionOverwrites = require('../structures/PermissionOverwrites');
const ThreadChannel = require('../structures/ThreadChannel'); const ThreadChannel = require('../structures/ThreadChannel');
const { ChannelTypes, ThreadChannelTypes } = require('../util/Constants'); const Webhook = require('../structures/Webhook');
const { ThreadChannelTypes, ChannelTypes, VideoQualityModes } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
const Util = require('../util/Util');
const { resolveAutoArchiveMaxLimit } = require('../util/Util');
let cacheWarningEmitted = false; let cacheWarningEmitted = false;
let storeChannelDeprecationEmitted = false; let storeChannelDeprecationEmitted = false;
@@ -169,6 +173,153 @@ class GuildChannelManager extends CachedManager {
return this.client.actions.ChannelCreate.handle(data).channel; return this.client.actions.ChannelCreate.handle(data).channel;
} }
/**
* Creates a webhook for the channel.
* @param {GuildChannelResolvable} channel The channel to create the webhook for
* @param {string} name The name of the webhook
* @param {ChannelWebhookCreateOptions} [options] Options for creating the webhook
* @returns {Promise<Webhook>} Returns the created Webhook
* @example
* // Create a webhook for the current channel
* guild.channels.createWebhook('222197033908436994', 'Snek', {
* avatar: 'https://i.imgur.com/mI8XcpG.jpg',
* reason: 'Needed a cool new Webhook'
* })
* .then(console.log)
* .catch(console.error)
*/
async createWebhook(channel, name, { avatar, reason } = {}) {
const id = this.resolveId(channel);
if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
if (typeof avatar === 'string' && !avatar.startsWith('data:')) {
avatar = await DataResolver.resolveImage(avatar);
}
const data = await this.client.api.channels[id].webhooks.post({
data: {
name,
avatar,
},
reason,
});
return new Webhook(this.client, data);
}
/**
* The data for a guild channel.
* @typedef {Object} ChannelData
* @property {string} [name] The name of the channel
* @property {ChannelType} [type] The type of the channel (only conversion between text and news is supported)
* @property {number} [position] The position of the channel
* @property {string} [topic] The topic of the text channel
* @property {boolean} [nsfw] Whether the channel is NSFW
* @property {number} [bitrate] The bitrate of the voice channel
* @property {number} [userLimit] The user limit of the voice channel
* @property {?CategoryChannelResolvable} [parent] The parent of the channel
* @property {boolean} [lockPermissions]
* Lock the permissions of the channel to what the parent's permissions are
* @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites]
* Permission overwrites for the channel
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the channel in seconds
* @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration]
* The default auto archive duration for all new threads in this channel
* @property {?string} [rtcRegion] The RTC region of the channel
* @property {?VideoQualityMode|number} [videoQualityMode] The camera video quality mode of the channel
*/
/**
* Edits the channel.
* @param {GuildChannelResolvable} channel The channel to edit
* @param {ChannelData} data The new data for the channel
* @param {string} [reason] Reason for editing this channel
* @returns {Promise<GuildChannel>}
* @example
* // Edit a channel
* guild.channels.edit('222197033908436994', { name: 'new-channel' })
* .then(console.log)
* .catch(console.error);
*/
async edit(channel, data, reason) {
channel = this.resolve(channel);
if (!channel) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
const parent = data.parent && this.client.channels.resolveId(data.parent);
if (typeof data.position !== 'undefined') await this.setPosition(channel, data.position, { reason });
let permission_overwrites = data.permissionOverwrites?.map(o => PermissionOverwrites.resolve(o, this.guild));
if (data.lockPermissions) {
if (parent) {
const newParent = this.guild.channels.resolve(parent);
if (newParent?.type === 'GUILD_CATEGORY') {
permission_overwrites = newParent.permissionOverwrites.cache.map(o =>
PermissionOverwrites.resolve(o, this.guild),
);
}
} else if (channel.parent) {
permission_overwrites = this.parent.permissionOverwrites.cache.map(o =>
PermissionOverwrites.resolve(o, this.guild),
);
}
}
let defaultAutoArchiveDuration = data.defaultAutoArchiveDuration;
if (defaultAutoArchiveDuration === 'MAX') defaultAutoArchiveDuration = resolveAutoArchiveMaxLimit(this.guild);
const newData = await this.client.api.channels(channel.id).patch({
data: {
name: (data.name ?? channel.name).trim(),
type: data.type,
topic: data.topic,
nsfw: data.nsfw,
bitrate: data.bitrate ?? channel.bitrate,
user_limit: data.userLimit ?? channel.userLimit,
rtc_region: data.rtcRegion ?? channel.rtcRegion,
video_quality_mode:
typeof data.videoQualityMode === 'string' ? VideoQualityModes[data.videoQualityMode] : data.videoQualityMode,
parent_id: parent,
lock_permissions: data.lockPermissions,
rate_limit_per_user: data.rateLimitPerUser,
default_auto_archive_duration: defaultAutoArchiveDuration,
permission_overwrites,
},
reason,
});
return this.client.actions.ChannelUpdate.handle(newData).updated;
}
/**
* Sets a new position for the guild channel.
* @param {GuildChannelResolvable} channel The channel to set the position for
* @param {number} position The new position for the guild channel
* @param {SetChannelPositionOptions} [options] Options for setting position
* @returns {Promise<GuildChannel>}
* @example
* // Set a new channel position
* guild.channels.setPosition('222078374472843266', 2)
* .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))
* .catch(console.error);
*/
async setPosition(channel, position, { relative, reason } = {}) {
channel = this.resolve(channel);
if (!channel) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
const updatedChannels = await Util.setPosition(
channel,
position,
relative,
this.guild._sortedChannels(this),
this.client.api.guilds(this.guild.id).channels,
reason,
);
this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: this.guild.id,
channels: updatedChannels,
});
return channel;
}
/** /**
* Obtains one or more guild channels from Discord, or the channel cache if they're already available. * Obtains one or more guild channels from Discord, or the channel cache if they're already available.
* @param {Snowflake} [id] The channel's id * @param {Snowflake} [id] The channel's id
@@ -204,6 +355,39 @@ class GuildChannelManager extends CachedManager {
return channels; return channels;
} }
/**
* Fetches all webhooks for the channel.
* @param {GuildChannelResolvable} channel The channel to fetch webhooks for
* @returns {Promise<Collection<Snowflake, Webhook>>}
* @example
* // Fetch webhooks
* guild.channels.fetchWebhooks('769862166131245066')
* .then(hooks => console.log(`This channel has ${hooks.size} hooks`))
* .catch(console.error);
*/
async fetchWebhooks(channel) {
const id = this.resolveId(channel);
if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
const data = await this.client.api.channels[id].webhooks.get();
return data.reduce((hooks, hook) => hooks.set(hook.id, new Webhook(this.client, hook)), new Collection());
}
/**
* Data that can be resolved to give a Category Channel object. This can be:
* * A CategoryChannel object
* * A Snowflake
* @typedef {CategoryChannel|Snowflake} CategoryChannelResolvable
*/
/**
* The data needed for updating a channel's position.
* @typedef {Object} ChannelPosition
* @property {GuildChannel|Snowflake} channel Channel to update
* @property {number} [position] New position for the channel
* @property {CategoryChannelResolvable} [parent] Parent channel for this channel
* @property {boolean} [lockPermissions] If the overwrites should be locked to the parents overwrites
*/
/** /**
* Batch-updates the guild's channels' positions. * Batch-updates the guild's channels' positions.
* <info>Only one channel's parent can be changed at a time</info> * <info>Only one channel's parent can be changed at a time</info>
@@ -243,6 +427,23 @@ class GuildChannelManager extends CachedManager {
const raw = await this.client.api.guilds(this.guild.id).threads.active.get(); const raw = await this.client.api.guilds(this.guild.id).threads.active.get();
return ThreadManager._mapThreads(raw, this.client, { guild: this.guild, cache }); return ThreadManager._mapThreads(raw, this.client, { guild: this.guild, cache });
} }
/**
* Deletes the channel.
* @param {GuildChannelResolvable} channel The channel to delete
* @param {string} [reason] Reason for deleting this channel
* @returns {Promise<void>}
* @example
* // Delete the channel
* guild.channels.delete('858850993013260338', 'making room for new channels')
* .then(console.log)
* .catch(console.error);
*/
async delete(channel, reason) {
const id = this.resolveId(channel);
if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
await this.client.api.channels(id).delete({ reason });
}
} }
module.exports = GuildChannelManager; module.exports = GuildChannelManager;

View File

@@ -2,8 +2,9 @@
const { Collection } = require('@discordjs/collection'); const { Collection } = require('@discordjs/collection');
const BaseGuildEmojiManager = require('./BaseGuildEmojiManager'); const BaseGuildEmojiManager = require('./BaseGuildEmojiManager');
const { TypeError } = require('../errors'); const { Error, TypeError } = require('../errors');
const DataResolver = require('../util/DataResolver'); const DataResolver = require('../util/DataResolver');
const Permissions = require('../util/Permissions');
/** /**
* Manages API methods for GuildEmojis and stores their cache. * Manages API methods for GuildEmojis and stores their cache.
@@ -100,6 +101,71 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
for (const emoji of data) emojis.set(emoji.id, this._add(emoji, cache)); for (const emoji of data) emojis.set(emoji.id, this._add(emoji, cache));
return emojis; return emojis;
} }
/**
* Deletes an emoji.
* @param {EmojiResolvable} emoji The Emoji resolvable to delete
* @param {string} [reason] Reason for deleting the emoji
* @returns {Promise<void>}
*/
async delete(emoji, reason) {
const id = this.resolveId(emoji);
if (!id) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
await this.client.api.guilds(this.guild.id).emojis(id).delete({ reason });
}
/**
* Edits an emoji.
* @param {EmojiResolvable} emoji The Emoji resolvable to edit
* @param {GuildEmojiEditData} data The new data for the emoji
* @param {string} [reason] Reason for editing this emoji
* @returns {Promise<GuildEmoji>}
*/
async edit(emoji, data, reason) {
const id = this.resolveId(emoji);
if (!id) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
const roles = data.roles?.map(r => this.guild.roles.resolveId(r));
const newData = await this.client.api
.guilds(this.guild.id)
.emojis(id)
.patch({
data: {
name: data.name,
roles,
},
reason,
});
const existing = this.cache.get(id);
if (existing) {
const clone = existing._clone();
clone._patch(newData);
return clone;
}
return this._add(newData);
}
/**
* Fetches the author for this emoji
* @param {EmojiResolvable} emoji The emoji to fetch the author of
* @returns {Promise<User>}
*/
async fetchAuthor(emoji) {
emoji = this.resolve(emoji);
if (!emoji) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
if (emoji.managed) {
throw new Error('EMOJI_MANAGED');
}
const { me } = this.guild;
if (!me) throw new Error('GUILD_UNCACHED_ME');
if (!me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS)) {
throw new Error('MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION', this.guild);
}
const data = await this.client.api.guilds(this.guild.id).emojis(emoji.id).get();
emoji._patch(data);
return emoji.author;
}
} }
module.exports = GuildEmojiManager; module.exports = GuildEmojiManager;

View File

@@ -18,6 +18,7 @@ const {
VerificationLevels, VerificationLevels,
DefaultMessageNotificationLevels, DefaultMessageNotificationLevels,
ExplicitContentFilterLevels, ExplicitContentFilterLevels,
VideoQualityModes,
} = require('../util/Constants'); } = require('../util/Constants');
const DataResolver = require('../util/DataResolver'); const DataResolver = require('../util/DataResolver');
const Permissions = require('../util/Permissions'); const Permissions = require('../util/Permissions');
@@ -94,6 +95,7 @@ class GuildManager extends CachedManager {
* @property {number} [bitrate] The bitrate of the voice channel * @property {number} [bitrate] The bitrate of the voice channel
* @property {number} [userLimit] The user limit of the channel * @property {number} [userLimit] The user limit of the channel
* @property {?string} [rtcRegion] The RTC region of the channel * @property {?string} [rtcRegion] The RTC region of the channel
* @property {VideoQualityMode|number} [videoQualityMode] The camera video quality mode of the channel
* @property {PartialOverwriteData[]} [permissionOverwrites] * @property {PartialOverwriteData[]} [permissionOverwrites]
* Overwrites of the channel * Overwrites of the channel
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) of the channel in seconds * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) of the channel in seconds
@@ -200,6 +202,11 @@ class GuildManager extends CachedManager {
delete channel.rateLimitPerUser; delete channel.rateLimitPerUser;
channel.rtc_region = channel.rtcRegion; channel.rtc_region = channel.rtcRegion;
delete channel.rtcRegion; delete channel.rtcRegion;
channel.video_quality_mode =
typeof channel.videoQualityMode === 'string'
? VideoQualityModes[channel.videoQualityMode]
: channel.videoQualityMode;
delete channel.videoQualityMode;
if (!channel.permissionOverwrites) continue; if (!channel.permissionOverwrites) continue;
for (const overwrite of channel.permissionOverwrites) { for (const overwrite of channel.permissionOverwrites) {

View File

@@ -353,7 +353,7 @@ class GuildMemberManager extends CachedManager {
* @example * @example
* // Kick a user by id (or with a user/guild member object) * // Kick a user by id (or with a user/guild member object)
* guild.members.kick('84484653687267328') * guild.members.kick('84484653687267328')
* .then(banInfo => console.log(`Kicked ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`)) * .then(kickInfo => console.log(`Kicked ${kickInfo.user?.tag ?? kickInfo.tag ?? kickInfo}`))
* .catch(console.error); * .catch(console.error);
*/ */
async kick(user, reason) { async kick(user, reason) {
@@ -376,7 +376,7 @@ class GuildMemberManager extends CachedManager {
* @example * @example
* // Ban a user by id (or with a user/guild member object) * // Ban a user by id (or with a user/guild member object)
* guild.members.ban('84484653687267328') * guild.members.ban('84484653687267328')
* .then(kickInfo => console.log(`Banned ${kickInfo.user?.tag ?? kickInfo.tag ?? kickInfo}`)) * .then(banInfo => console.log(`Banned ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`))
* .catch(console.error); * .catch(console.error);
*/ */
ban(user, options = { days: 0 }) { ban(user, options = { days: 0 }) {
@@ -387,7 +387,7 @@ class GuildMemberManager extends CachedManager {
* Unbans a user from the guild. Internally calls the {@link GuildBanManager#remove} method. * Unbans a user from the guild. Internally calls the {@link GuildBanManager#remove} method.
* @param {UserResolvable} user The user to unban * @param {UserResolvable} user The user to unban
* @param {string} [reason] Reason for unbanning user * @param {string} [reason] Reason for unbanning user
* @returns {Promise<User>} The user that was unbanned * @returns {Promise<?User>} The user that was unbanned
* @example * @example
* // Unban a user by id (or with a user/guild member object) * // Unban a user by id (or with a user/guild member object)
* guild.members.unban('84484653687267328') * guild.members.unban('84484653687267328')

View File

@@ -5,6 +5,7 @@ const CachedManager = require('./CachedManager');
const { TypeError, Error } = require('../errors'); const { TypeError, Error } = require('../errors');
const { GuildScheduledEvent } = require('../structures/GuildScheduledEvent'); const { GuildScheduledEvent } = require('../structures/GuildScheduledEvent');
const { PrivacyLevels, GuildScheduledEventEntityTypes, GuildScheduledEventStatuses } = require('../util/Constants'); const { PrivacyLevels, GuildScheduledEventEntityTypes, GuildScheduledEventStatuses } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
/** /**
* Manages API methods for GuildScheduledEvents and stores their cache. * Manages API methods for GuildScheduledEvents and stores their cache.
@@ -49,6 +50,7 @@ class GuildScheduledEventManager extends CachedManager {
* @property {GuildScheduledEventEntityMetadataOptions} [entityMetadata] The entity metadata of the * @property {GuildScheduledEventEntityMetadataOptions} [entityMetadata] The entity metadata of the
* guild scheduled event * guild scheduled event
* <warn>This is required if `entityType` is 'EXTERNAL'</warn> * <warn>This is required if `entityType` is 'EXTERNAL'</warn>
* @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event
* @property {string} [reason] The reason for creating the guild scheduled event * @property {string} [reason] The reason for creating the guild scheduled event
*/ */
@@ -76,6 +78,7 @@ class GuildScheduledEventManager extends CachedManager {
scheduledEndTime, scheduledEndTime,
entityMetadata, entityMetadata,
reason, reason,
image,
} = options; } = options;
if (typeof privacyLevel === 'string') privacyLevel = PrivacyLevels[privacyLevel]; if (typeof privacyLevel === 'string') privacyLevel = PrivacyLevels[privacyLevel];
@@ -99,6 +102,7 @@ class GuildScheduledEventManager extends CachedManager {
scheduled_start_time: new Date(scheduledStartTime).toISOString(), scheduled_start_time: new Date(scheduledStartTime).toISOString(),
scheduled_end_time: scheduledEndTime ? new Date(scheduledEndTime).toISOString() : scheduledEndTime, scheduled_end_time: scheduledEndTime ? new Date(scheduledEndTime).toISOString() : scheduledEndTime,
description, description,
image: image && (await DataResolver.resolveImage(image)),
entity_type: entityType, entity_type: entityType,
entity_metadata, entity_metadata,
}, },
@@ -172,6 +176,7 @@ class GuildScheduledEventManager extends CachedManager {
* @property {GuildScheduledEventEntityMetadataOptions} [entityMetadata] The entity metadata of the * @property {GuildScheduledEventEntityMetadataOptions} [entityMetadata] The entity metadata of the
* guild scheduled event * guild scheduled event
* <warn>This can be modified only if `entityType` of the `GuildScheduledEvent` to be edited is 'EXTERNAL'</warn> * <warn>This can be modified only if `entityType` of the `GuildScheduledEvent` to be edited is 'EXTERNAL'</warn>
* @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event
* @property {string} [reason] The reason for editing the guild scheduled event * @property {string} [reason] The reason for editing the guild scheduled event
*/ */
@@ -197,6 +202,7 @@ class GuildScheduledEventManager extends CachedManager {
scheduledEndTime, scheduledEndTime,
entityMetadata, entityMetadata,
reason, reason,
image,
} = options; } = options;
if (typeof privacyLevel === 'string') privacyLevel = PrivacyLevels[privacyLevel]; if (typeof privacyLevel === 'string') privacyLevel = PrivacyLevels[privacyLevel];
@@ -220,6 +226,7 @@ class GuildScheduledEventManager extends CachedManager {
description, description,
entity_type: entityType, entity_type: entityType,
status, status,
image: image && (await DataResolver.resolveImage(image)),
entity_metadata, entity_metadata,
}, },
reason, reason,

View File

@@ -161,6 +161,19 @@ class GuildStickerManager extends CachedManager {
const data = await this.client.api.guilds(this.guild.id).stickers.get(); const data = await this.client.api.guilds(this.guild.id).stickers.get();
return new Collection(data.map(sticker => [sticker.id, this._add(sticker, cache)])); return new Collection(data.map(sticker => [sticker.id, this._add(sticker, cache)]));
} }
/**
* Fetches the user who uploaded this sticker, if this is a guild sticker.
* @param {StickerResolvable} sticker The sticker to fetch the user for
* @returns {Promise<?User>}
*/
async fetchUser(sticker) {
sticker = this.resolve(sticker);
if (!sticker) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
const data = await this.client.api.guilds(this.guildId).stickers(sticker.id).get();
sticker._patch(data);
return sticker.user;
}
} }
module.exports = GuildStickerManager; module.exports = GuildStickerManager;

View File

@@ -156,25 +156,27 @@ class MessageManager extends CachedManager {
/** /**
* Pins a message to the channel's pinned messages, even if it's not cached. * Pins a message to the channel's pinned messages, even if it's not cached.
* @param {MessageResolvable} message The message to pin * @param {MessageResolvable} message The message to pin
* @param {string} [reason] Reason for pinning
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async pin(message) { async pin(message, reason) {
message = this.resolveId(message); message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).pins(message).put(); await this.client.api.channels(this.channel.id).pins(message).put({ reason });
} }
/** /**
* Unpins a message from the channel's pinned messages, even if it's not cached. * Unpins a message from the channel's pinned messages, even if it's not cached.
* @param {MessageResolvable} message The message to unpin * @param {MessageResolvable} message The message to unpin
* @param {string} [reason] Reason for unpinning
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async unpin(message) { async unpin(message, reason) {
message = this.resolveId(message); message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).pins(message).delete(); await this.client.api.channels(this.channel.id).pins(message).delete({ reason });
} }
/** /**

View File

@@ -7,7 +7,8 @@ const { TypeError } = require('../errors');
const { Role } = require('../structures/Role'); const { Role } = require('../structures/Role');
const DataResolver = require('../util/DataResolver'); const DataResolver = require('../util/DataResolver');
const Permissions = require('../util/Permissions'); const Permissions = require('../util/Permissions');
const { resolveColor, setPosition } = require('../util/Util'); const { resolveColor } = require('../util/Util');
const Util = require('../util/Util');
let cacheWarningEmitted = false; let cacheWarningEmitted = false;
@@ -159,7 +160,7 @@ class RoleManager extends CachedManager {
guild_id: this.guild.id, guild_id: this.guild.id,
role: data, role: data,
}); });
if (position) return role.setPosition(position, reason); if (position) return this.setPosition(role, position, { reason });
return role; return role;
} }
@@ -179,21 +180,7 @@ class RoleManager extends CachedManager {
role = this.resolve(role); role = this.resolve(role);
if (!role) throw new TypeError('INVALID_TYPE', 'role', 'RoleResolvable'); if (!role) throw new TypeError('INVALID_TYPE', 'role', 'RoleResolvable');
if (typeof data.position === 'number') { if (typeof data.position === 'number') await this.setPosition(role, data.position, { reason });
const updatedRoles = await setPosition(
role,
data.position,
false,
this.guild._sortedRoles(),
this.client.api.guilds(this.guild.id).roles,
reason,
);
this.client.actions.GuildRolesPositionUpdate.handle({
guild_id: this.guild.id,
roles: updatedRoles,
});
}
let icon = data.icon; let icon = data.icon;
if (icon) { if (icon) {
@@ -227,7 +214,7 @@ class RoleManager extends CachedManager {
* @example * @example
* // Delete a role * // Delete a role
* guild.roles.delete('222079219327434752', 'The role needed to go') * guild.roles.delete('222079219327434752', 'The role needed to go')
* .then(deleted => console.log(`Deleted role ${deleted.name}`)) * .then(() => console.log('Deleted the role.'))
* .catch(console.error); * .catch(console.error);
*/ */
async delete(role, reason) { async delete(role, reason) {
@@ -236,6 +223,44 @@ class RoleManager extends CachedManager {
this.client.actions.GuildRoleDelete.handle({ guild_id: this.guild.id, role_id: id }); this.client.actions.GuildRoleDelete.handle({ guild_id: this.guild.id, role_id: id });
} }
/**
* Sets the new position of the role.
* @param {RoleResolvable} role The role to change the position of
* @param {number} position The new position for the role
* @param {SetRolePositionOptions} [options] Options for setting the position
* @returns {Promise<Role>}
* @example
* // Set the position of the role
* guild.roles.setPosition('222197033908436994', 1)
* .then(updated => console.log(`Role position: ${updated.position}`))
* .catch(console.error);
*/
async setPosition(role, position, { relative, reason } = {}) {
role = this.resolve(role);
if (!role) throw new TypeError('INVALID_TYPE', 'role', 'RoleResolvable');
const updatedRoles = await Util.setPosition(
role,
position,
relative,
this.guild._sortedRoles(),
this.client.api.guilds(this.guild.id).roles,
reason,
);
this.client.actions.GuildRolesPositionUpdate.handle({
guild_id: this.guild.id,
roles: updatedRoles,
});
return role;
}
/**
* The data needed for updating a guild role's position
* @typedef {Object} GuildRolePosition
* @property {RoleResolvable} role The role's id
* @property {number} position The position to update
*/
/** /**
* Batch-updates the guild's role positions * Batch-updates the guild's role positions
* @param {GuildRolePosition[]} rolePositions Role positions to update * @param {GuildRolePosition[]} rolePositions Role positions to update

View File

@@ -31,6 +31,7 @@ class StageInstanceManager extends CachedManager {
* @typedef {Object} StageInstanceCreateOptions * @typedef {Object} StageInstanceCreateOptions
* @property {string} topic The topic of the stage instance * @property {string} topic The topic of the stage instance
* @property {PrivacyLevel|number} [privacyLevel] The privacy level of the stage instance * @property {PrivacyLevel|number} [privacyLevel] The privacy level of the stage instance
* @property {boolean} [sendStartNotification] Whether to notify `@everyone` that the stage instance has started
*/ */
/** /**
@@ -58,7 +59,7 @@ class StageInstanceManager extends CachedManager {
const channelId = this.guild.channels.resolveId(channel); const channelId = this.guild.channels.resolveId(channel);
if (!channelId) throw new Error('STAGE_CHANNEL_RESOLVE'); if (!channelId) throw new Error('STAGE_CHANNEL_RESOLVE');
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true); if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
let { topic, privacyLevel } = options; let { topic, privacyLevel, sendStartNotification } = options;
privacyLevel &&= typeof privacyLevel === 'number' ? privacyLevel : PrivacyLevels[privacyLevel]; privacyLevel &&= typeof privacyLevel === 'number' ? privacyLevel : PrivacyLevels[privacyLevel];
@@ -67,6 +68,7 @@ class StageInstanceManager extends CachedManager {
channel_id: channelId, channel_id: channelId,
topic, topic,
privacy_level: privacyLevel, privacy_level: privacyLevel,
send_start_notification: sendStartNotification,
}, },
}); });

View File

@@ -5,6 +5,7 @@ const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors'); const { TypeError } = require('../errors');
const ThreadChannel = require('../structures/ThreadChannel'); const ThreadChannel = require('../structures/ThreadChannel');
const { ChannelTypes } = require('../util/Constants'); const { ChannelTypes } = require('../util/Constants');
const { resolveAutoArchiveMaxLimit } = require('../util/Util');
/** /**
* Manages API methods for {@link ThreadChannel} objects and stores their cache. * Manages API methods for {@link ThreadChannel} objects and stores their cache.
@@ -120,14 +121,8 @@ class ThreadManager extends CachedManager {
} else if (this.channel.type !== 'GUILD_NEWS') { } else if (this.channel.type !== 'GUILD_NEWS') {
resolvedType = typeof type === 'string' ? ChannelTypes[type] : type ?? resolvedType; resolvedType = typeof type === 'string' ? ChannelTypes[type] : type ?? resolvedType;
} }
if (autoArchiveDuration === 'MAX') {
autoArchiveDuration = 1440; if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild);
if (this.channel.guild.features.includes('SEVEN_DAY_THREAD_ARCHIVE')) {
autoArchiveDuration = 10080;
} else if (this.channel.guild.features.includes('THREE_DAY_THREAD_ARCHIVE')) {
autoArchiveDuration = 4320;
}
}
const data = await path.threads.post({ const data = await path.threads.post({
data: { data: {

View File

@@ -15,7 +15,7 @@ class RateLimitError extends Error {
this.name = 'RateLimitError'; this.name = 'RateLimitError';
/** /**
* Time until this rate limit ends, in ms * Time until this rate limit ends, in milliseconds
* @type {number} * @type {number}
*/ */
this.timeout = timeout; this.timeout = timeout;

View File

@@ -280,7 +280,7 @@ class RequestHandler {
/** /**
* @typedef {Object} InvalidRequestWarningData * @typedef {Object} InvalidRequestWarningData
* @property {number} count Number of invalid requests that have been made in the window * @property {number} count Number of invalid requests that have been made in the window
* @property {number} remainingTime Time in ms remaining before the count resets * @property {number} remainingTime Time in milliseconds remaining before the count resets
*/ */
/** /**

View File

@@ -249,14 +249,18 @@ class Shard extends EventEmitter {
const listener = message => { const listener = message => {
if (message?._fetchProp !== prop) return; if (message?._fetchProp !== prop) return;
child.removeListener('message', listener); child.removeListener('message', listener);
this.decrementMaxListeners(child);
this._fetches.delete(prop); this._fetches.delete(prop);
if (!message._error) resolve(message._result); if (!message._error) resolve(message._result);
else reject(Util.makeError(message._error)); else reject(Util.makeError(message._error));
}; };
this.incrementMaxListeners(child);
child.on('message', listener); child.on('message', listener);
this.send({ _fetchProp: prop }).catch(err => { this.send({ _fetchProp: prop }).catch(err => {
child.removeListener('message', listener); child.removeListener('message', listener);
this.decrementMaxListeners(child);
this._fetches.delete(prop); this._fetches.delete(prop);
reject(err); reject(err);
}); });
@@ -288,14 +292,18 @@ class Shard extends EventEmitter {
const listener = message => { const listener = message => {
if (message?._eval !== _eval) return; if (message?._eval !== _eval) return;
child.removeListener('message', listener); child.removeListener('message', listener);
this.decrementMaxListeners(child);
this._evals.delete(_eval); this._evals.delete(_eval);
if (!message._error) resolve(message._result); if (!message._error) resolve(message._result);
else reject(Util.makeError(message._error)); else reject(Util.makeError(message._error));
}; };
this.incrementMaxListeners(child);
child.on('message', listener); child.on('message', listener);
this.send({ _eval }).catch(err => { this.send({ _eval }).catch(err => {
child.removeListener('message', listener); child.removeListener('message', listener);
this.decrementMaxListeners(child);
this._evals.delete(_eval); this._evals.delete(_eval);
reject(err); reject(err);
}); });
@@ -406,6 +414,30 @@ class Shard extends EventEmitter {
if (respawn) this.spawn(timeout).catch(err => this.emit('error', err)); if (respawn) this.spawn(timeout).catch(err => this.emit('error', err));
} }
/**
* Increments max listeners by one for a given emitter, if they are not zero.
* @param {EventEmitter|process} emitter The emitter that emits the events.
* @private
*/
incrementMaxListeners(emitter) {
const maxListeners = emitter.getMaxListeners();
if (maxListeners !== 0) {
emitter.setMaxListeners(maxListeners + 1);
}
}
/**
* Decrements max listeners by one for a given emitter, if they are not zero.
* @param {EventEmitter|process} emitter The emitter that emits the events.
* @private
*/
decrementMaxListeners(emitter) {
const maxListeners = emitter.getMaxListeners();
if (maxListeners !== 0) {
emitter.setMaxListeners(maxListeners - 1);
}
}
} }
module.exports = Shard; module.exports = Shard;

View File

@@ -111,13 +111,16 @@ class ShardClientUtil {
const listener = message => { const listener = message => {
if (message?._sFetchProp !== prop || message._sFetchPropShard !== shard) return; if (message?._sFetchProp !== prop || message._sFetchPropShard !== shard) return;
parent.removeListener('message', listener); parent.removeListener('message', listener);
this.decrementMaxListeners(parent);
if (!message._error) resolve(message._result); if (!message._error) resolve(message._result);
else reject(Util.makeError(message._error)); else reject(Util.makeError(message._error));
}; };
this.incrementMaxListeners(parent);
parent.on('message', listener); parent.on('message', listener);
this.send({ _sFetchProp: prop, _sFetchPropShard: shard }).catch(err => { this.send({ _sFetchProp: prop, _sFetchPropShard: shard }).catch(err => {
parent.removeListener('message', listener); parent.removeListener('message', listener);
this.decrementMaxListeners(parent);
reject(err); reject(err);
}); });
}); });
@@ -146,13 +149,15 @@ class ShardClientUtil {
const listener = message => { const listener = message => {
if (message?._sEval !== script || message._sEvalShard !== options.shard) return; if (message?._sEval !== script || message._sEvalShard !== options.shard) return;
parent.removeListener('message', listener); parent.removeListener('message', listener);
this.decrementMaxListeners(parent);
if (!message._error) resolve(message._result); if (!message._error) resolve(message._result);
else reject(Util.makeError(message._error)); else reject(Util.makeError(message._error));
}; };
this.incrementMaxListeners(parent);
parent.on('message', listener); parent.on('message', listener);
this.send({ _sEval: script, _sEvalShard: options.shard }).catch(err => { this.send({ _sEval: script, _sEvalShard: options.shard }).catch(err => {
parent.removeListener('message', listener); parent.removeListener('message', listener);
this.decrementMaxListeners(parent);
reject(err); reject(err);
}); });
}); });
@@ -241,6 +246,30 @@ class ShardClientUtil {
if (shard < 0) throw new Error('SHARDING_SHARD_MISCALCULATION', shard, guildId, shardCount); if (shard < 0) throw new Error('SHARDING_SHARD_MISCALCULATION', shard, guildId, shardCount);
return shard; return shard;
} }
/**
* Increments max listeners by one for a given emitter, if they are not zero.
* @param {EventEmitter|process} emitter The emitter that emits the events.
* @private
*/
incrementMaxListeners(emitter) {
const maxListeners = emitter.getMaxListeners();
if (maxListeners !== 0) {
emitter.setMaxListeners(maxListeners + 1);
}
}
/**
* Decrements max listeners by one for a given emitter, if they are not zero.
* @param {EventEmitter|process} emitter The emitter that emits the events.
* @private
*/
decrementMaxListeners(emitter) {
const maxListeners = emitter.getMaxListeners();
if (maxListeners !== 0) {
emitter.setMaxListeners(maxListeners - 1);
}
}
} }
module.exports = ShardClientUtil; module.exports = ShardClientUtil;

View File

@@ -36,7 +36,7 @@ class ShardingManager extends EventEmitter {
* @property {boolean} [respawn=true] Whether shards should automatically respawn upon exiting * @property {boolean} [respawn=true] Whether shards should automatically respawn upon exiting
* @property {string[]} [shardArgs=[]] Arguments to pass to the shard script when spawning * @property {string[]} [shardArgs=[]] Arguments to pass to the shard script when spawning
* (only available when mode is set to 'process') * (only available when mode is set to 'process')
* @property {string} [execArgv=[]] Arguments to pass to the shard script executable when spawning * @property {string[]} [execArgv=[]] Arguments to pass to the shard script executable when spawning
* (only available when mode is set to 'process') * (only available when mode is set to 'process')
* @property {string} [token] Token to use for automatic shard count and passing to shards * @property {string} [token] Token to use for automatic shard count and passing to shards
*/ */

View File

@@ -64,6 +64,16 @@ class AnonymousGuild extends BaseGuild {
*/ */
this.nsfwLevel = NSFWLevels[data.nsfw_level]; this.nsfwLevel = NSFWLevels[data.nsfw_level];
} }
if ('premium_subscription_count' in data) {
/**
* The total number of boosts for this server
* @type {?number}
*/
this.premiumSubscriptionCount = data.premium_subscription_count;
} else {
this.premiumSubscriptionCount ??= null;
}
} }
/** /**

View File

@@ -62,6 +62,26 @@ class ApplicationCommand extends Base {
this.name = data.name; this.name = data.name;
} }
if ('name_localizations' in data) {
/**
* The name localizations for this command
* @type {?Object<Locale, string>}
*/
this.nameLocalizations = data.name_localizations;
} else {
this.nameLocalizations ??= null;
}
if ('name_localized' in data) {
/**
* The localized name for this command
* @type {?string}
*/
this.nameLocalized = data.name_localized;
} else {
this.nameLocalized ??= null;
}
if ('description' in data) { if ('description' in data) {
/** /**
* The description of this command * The description of this command
@@ -70,6 +90,26 @@ class ApplicationCommand extends Base {
this.description = data.description; this.description = data.description;
} }
if ('description_localizations' in data) {
/**
* The description localizations for this command
* @type {?Object<Locale, string>}
*/
this.descriptionLocalizations = data.description_localizations;
} else {
this.descriptionLocalizations ??= null;
}
if ('description_localized' in data) {
/**
* The localized description for this command
* @type {?string}
*/
this.descriptionLocalized = data.description_localized;
} else {
this.descriptionLocalized ??= null;
}
if ('options' in data) { if ('options' in data) {
/** /**
* The options of this command * The options of this command
@@ -128,7 +168,9 @@ class ApplicationCommand extends Base {
* Data for creating or editing an application command. * Data for creating or editing an application command.
* @typedef {Object} ApplicationCommandData * @typedef {Object} ApplicationCommandData
* @property {string} name The name of the command * @property {string} name The name of the command
* @property {Object<Locale, string>} [nameLocalizations] The localizations for the command name
* @property {string} description The description of the command * @property {string} description The description of the command
* @property {Object<Locale, string>} [descriptionLocalizations] The localizations for the command description
* @property {ApplicationCommandType} [type] The type of the command * @property {ApplicationCommandType} [type] The type of the command
* @property {ApplicationCommandOptionData[]} [options] Options for the command * @property {ApplicationCommandOptionData[]} [options] Options for the command
* @property {boolean} [defaultPermission] Whether the command is enabled by default when the app is added to a guild * @property {boolean} [defaultPermission] Whether the command is enabled by default when the app is added to a guild
@@ -143,10 +185,12 @@ class ApplicationCommand extends Base {
* @typedef {Object} ApplicationCommandOptionData * @typedef {Object} ApplicationCommandOptionData
* @property {ApplicationCommandOptionType|number} type The type of the option * @property {ApplicationCommandOptionType|number} type The type of the option
* @property {string} name The name of the option * @property {string} name The name of the option
* @property {Object<Locale, string>} [nameLocalizations] The name localizations for the option
* @property {string} description The description of the option * @property {string} description The description of the option
* @property {Object<Locale, string>} [descriptionLocalizations] The description localizations for the option
* @property {boolean} [autocomplete] Whether the option is an autocomplete option * @property {boolean} [autocomplete] Whether the option is an autocomplete option
* @property {boolean} [required] Whether the option is required * @property {boolean} [required] Whether the option is required
* @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from * @property {ApplicationCommandOptionChoiceData[]} [choices] The choices of the option for the user to pick from
* @property {ApplicationCommandOptionData[]} [options] Additional options if this option is a subcommand (group) * @property {ApplicationCommandOptionData[]} [options] Additional options if this option is a subcommand (group)
* @property {ChannelType[]|number[]} [channelTypes] When the option type is channel, * @property {ChannelType[]|number[]} [channelTypes] When the option type is channel,
* the allowed types of channels that can be selected * the allowed types of channels that can be selected
@@ -154,6 +198,13 @@ class ApplicationCommand extends Base {
* @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option * @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option
*/ */
/**
* @typedef {Object} ApplicationCommandOptionChoiceData
* @property {string} name The name of the choice
* @property {Object<Locale, string>} [nameLocalizations] The localized names for this choice
* @property {string|number} value The value of the choice
*/
/** /**
* Edits this application command. * Edits this application command.
* @param {ApplicationCommandData} data The data to update the command with * @param {ApplicationCommandData} data The data to update the command with
@@ -179,6 +230,23 @@ class ApplicationCommand extends Base {
return this.edit({ name }); return this.edit({ name });
} }
/**
* Edits the localized names of this ApplicationCommand
* @param {Object<Locale, string>} nameLocalizations The new localized names for the command
* @returns {Promise<ApplicationCommand>}
* @example
* // Edit the name localizations of this command
* command.setLocalizedNames({
* 'en-GB': 'test',
* 'pt-BR': 'teste',
* })
* .then(console.log)
* .catch(console.error)
*/
setNameLocalizations(nameLocalizations) {
return this.edit({ nameLocalizations });
}
/** /**
* Edits the description of this ApplicationCommand * Edits the description of this ApplicationCommand
* @param {string} description The new description of the command * @param {string} description The new description of the command
@@ -188,6 +256,23 @@ class ApplicationCommand extends Base {
return this.edit({ description }); return this.edit({ description });
} }
/**
* Edits the localized descriptions of this ApplicationCommand
* @param {Object<Locale, string>} descriptionLocalizations The new localized descriptions for the command
* @returns {Promise<ApplicationCommand>}
* @example
* // Edit the description localizations of this command
* command.setLocalizedDescriptions({
* 'en-GB': 'A test command',
* 'pt-BR': 'Um comando de teste',
* })
* .then(console.log)
* .catch(console.error)
*/
setDescriptionLocalizations(descriptionLocalizations) {
return this.edit({ descriptionLocalizations });
}
/** /**
* Edits the default permission of this ApplicationCommand * Edits the default permission of this ApplicationCommand
* @param {boolean} [defaultPermission=true] The default permission for this command * @param {boolean} [defaultPermission=true] The default permission for this command
@@ -344,7 +429,11 @@ class ApplicationCommand extends Base {
* @typedef {Object} ApplicationCommandOption * @typedef {Object} ApplicationCommandOption
* @property {ApplicationCommandOptionType} type The type of the option * @property {ApplicationCommandOptionType} type The type of the option
* @property {string} name The name of the option * @property {string} name The name of the option
* @property {Object<string, string>} [nameLocalizations] The localizations for the option name
* @property {string} [nameLocalized] The localized name for this option
* @property {string} description The description of the option * @property {string} description The description of the option
* @property {Object<string, string>} [descriptionLocalizations] The localizations for the option description
* @property {string} [descriptionLocalized] The localized description for this option
* @property {boolean} [required] Whether the option is required * @property {boolean} [required] Whether the option is required
* @property {boolean} [autocomplete] Whether the option is an autocomplete option * @property {boolean} [autocomplete] Whether the option is an autocomplete option
* @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from * @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from
@@ -359,12 +448,14 @@ class ApplicationCommand extends Base {
* A choice for an application command option. * A choice for an application command option.
* @typedef {Object} ApplicationCommandOptionChoice * @typedef {Object} ApplicationCommandOptionChoice
* @property {string} name The name of the choice * @property {string} name The name of the choice
* @property {?string} nameLocalized The localized name of the choice in the provided locale, if any
* @property {?Object<string, string>} [nameLocalizations] The localized names for this choice
* @property {string|number} value The value of the choice * @property {string|number} value The value of the choice
*/ */
/** /**
* Transforms an {@link ApplicationCommandOptionData} object into something that can be used with the API. * Transforms an {@link ApplicationCommandOptionData} object into something that can be used with the API.
* @param {ApplicationCommandOptionData} option The option to transform * @param {ApplicationCommandOptionData|ApplicationCommandOption} option The option to transform
* @param {boolean} [received] Whether this option has been received from Discord * @param {boolean} [received] Whether this option has been received from Discord
* @returns {APIApplicationCommandOption} * @returns {APIApplicationCommandOption}
* @private * @private
@@ -374,14 +465,27 @@ class ApplicationCommand extends Base {
const channelTypesKey = received ? 'channelTypes' : 'channel_types'; const channelTypesKey = received ? 'channelTypes' : 'channel_types';
const minValueKey = received ? 'minValue' : 'min_value'; const minValueKey = received ? 'minValue' : 'min_value';
const maxValueKey = received ? 'maxValue' : 'max_value'; const maxValueKey = received ? 'maxValue' : 'max_value';
const nameLocalizationsKey = received ? 'nameLocalizations' : 'name_localizations';
const nameLocalizedKey = received ? 'nameLocalized' : 'name_localized';
const descriptionLocalizationsKey = received ? 'descriptionLocalizations' : 'description_localizations';
const descriptionLocalizedKey = received ? 'descriptionLocalized' : 'description_localized';
return { return {
type: typeof option.type === 'number' && !received ? option.type : ApplicationCommandOptionTypes[option.type], type: typeof option.type === 'number' && !received ? option.type : ApplicationCommandOptionTypes[option.type],
name: option.name, name: option.name,
[nameLocalizationsKey]: option.nameLocalizations ?? option.name_localizations,
[nameLocalizedKey]: option.nameLocalized ?? option.name_localized,
description: option.description, description: option.description,
[descriptionLocalizationsKey]: option.descriptionLocalizations ?? option.description_localizations,
[descriptionLocalizedKey]: option.descriptionLocalized ?? option.description_localized,
required: required:
option.required ?? (stringType === 'SUB_COMMAND' || stringType === 'SUB_COMMAND_GROUP' ? undefined : false), option.required ?? (stringType === 'SUB_COMMAND' || stringType === 'SUB_COMMAND_GROUP' ? undefined : false),
autocomplete: option.autocomplete, autocomplete: option.autocomplete,
choices: option.choices, choices: option.choices?.map(choice => ({
name: choice.name,
[nameLocalizedKey]: choice.nameLocalized ?? choice.name_localized,
[nameLocalizationsKey]: choice.nameLocalizations ?? choice.name_localizations,
value: choice.value,
})),
options: option.options?.map(o => this.transformOption(o, received)), options: option.options?.map(o => this.transformOption(o, received)),
[channelTypesKey]: received [channelTypesKey]: received
? option.channel_types?.map(type => ChannelTypes[type]) ? option.channel_types?.map(type => ChannelTypes[type])

View File

@@ -76,7 +76,7 @@ class AutocompleteInteraction extends Interaction {
/** /**
* Sends results for the autocomplete of this interaction. * Sends results for the autocomplete of this interaction.
* @param {ApplicationCommandOptionChoice[]} options The options for the autocomplete * @param {ApplicationCommandOptionChoiceData[]} options The options for the autocomplete
* @returns {Promise<void>} * @returns {Promise<void>}
* @example * @example
* // respond to autocomplete interaction * // respond to autocomplete interaction

View File

@@ -3,6 +3,7 @@
const { Collection } = require('@discordjs/collection'); const { Collection } = require('@discordjs/collection');
const Interaction = require('./Interaction'); const Interaction = require('./Interaction');
const InteractionWebhook = require('./InteractionWebhook'); const InteractionWebhook = require('./InteractionWebhook');
const MessageAttachment = require('./MessageAttachment');
const InteractionResponses = require('./interfaces/InteractionResponses'); const InteractionResponses = require('./interfaces/InteractionResponses');
const { ApplicationCommandOptionTypes } = require('../util/Constants'); const { ApplicationCommandOptionTypes } = require('../util/Constants');
@@ -76,6 +77,7 @@ class BaseCommandInteraction extends Interaction {
* @property {Collection<Snowflake, Role|APIRole>} [roles] The resolved roles * @property {Collection<Snowflake, Role|APIRole>} [roles] The resolved roles
* @property {Collection<Snowflake, Channel|APIChannel>} [channels] The resolved channels * @property {Collection<Snowflake, Channel|APIChannel>} [channels] The resolved channels
* @property {Collection<Snowflake, Message|APIMessage>} [messages] The resolved messages * @property {Collection<Snowflake, Message|APIMessage>} [messages] The resolved messages
* @property {Collection<Snowflake, MessageAttachment>} [attachments] The resolved attachments
*/ */
/** /**
@@ -84,7 +86,7 @@ class BaseCommandInteraction extends Interaction {
* @returns {CommandInteractionResolvedData} * @returns {CommandInteractionResolvedData}
* @private * @private
*/ */
transformResolved({ members, users, channels, roles, messages }) { transformResolved({ members, users, channels, roles, messages, attachments }) {
const result = {}; const result = {};
if (members) { if (members) {
@@ -123,6 +125,14 @@ class BaseCommandInteraction extends Interaction {
} }
} }
if (attachments) {
result.attachments = new Collection();
for (const attachment of Object.values(attachments)) {
const patched = new MessageAttachment(attachment.url, attachment.filename, attachment);
result.attachments.set(attachment.id, patched);
}
}
return result; return result;
} }
@@ -139,6 +149,7 @@ class BaseCommandInteraction extends Interaction {
* @property {GuildMember|APIGuildMember} [member] The resolved member * @property {GuildMember|APIGuildMember} [member] The resolved member
* @property {GuildChannel|ThreadChannel|APIChannel} [channel] The resolved channel * @property {GuildChannel|ThreadChannel|APIChannel} [channel] The resolved channel
* @property {Role|APIRole} [role] The resolved role * @property {Role|APIRole} [role] The resolved role
* @property {MessageAttachment} [attachment] The resolved attachment
*/ */
/** /**
@@ -169,6 +180,9 @@ class BaseCommandInteraction extends Interaction {
const role = resolved.roles?.[option.value]; const role = resolved.roles?.[option.value];
if (role) result.role = this.guild?.roles._add(role) ?? role; if (role) result.role = this.guild?.roles._add(role) ?? role;
const attachment = resolved.attachments?.[option.value];
if (attachment) result.attachment = new MessageAttachment(attachment.url, attachment.filename, attachment);
} }
return result; return result;
@@ -182,6 +196,8 @@ class BaseCommandInteraction extends Interaction {
editReply() {} editReply() {}
deleteReply() {} deleteReply() {}
followUp() {} followUp() {}
showModal() {}
awaitModalSubmit() {}
} }
InteractionResponses.applyToClass(BaseCommandInteraction, ['deferUpdate', 'update']); InteractionResponses.applyToClass(BaseCommandInteraction, ['deferUpdate', 'update']);

View File

@@ -1,12 +1,9 @@
'use strict'; 'use strict';
const { Collection } = require('@discordjs/collection');
const GuildChannel = require('./GuildChannel'); const GuildChannel = require('./GuildChannel');
const Webhook = require('./Webhook');
const TextBasedChannel = require('./interfaces/TextBasedChannel'); const TextBasedChannel = require('./interfaces/TextBasedChannel');
const MessageManager = require('../managers/MessageManager'); const MessageManager = require('../managers/MessageManager');
const ThreadManager = require('../managers/ThreadManager'); const ThreadManager = require('../managers/ThreadManager');
const DataResolver = require('../util/DataResolver');
/** /**
* Represents a text-based guild channel on Discord. * Represents a text-based guild channel on Discord.
@@ -72,7 +69,7 @@ class BaseGuildTextChannel extends GuildChannel {
if ('default_auto_archive_duration' in data) { if ('default_auto_archive_duration' in data) {
/** /**
* The default auto archive duration for newly created threads in this channel * The default auto archive duration for newly created threads in this channel
* @type {?ThreadAutoArchiveDuration} * @type {?number}
*/ */
this.defaultAutoArchiveDuration = data.default_auto_archive_duration; this.defaultAutoArchiveDuration = data.default_auto_archive_duration;
} }
@@ -121,11 +118,8 @@ class BaseGuildTextChannel extends GuildChannel {
* .then(hooks => console.log(`This channel has ${hooks.size} hooks`)) * .then(hooks => console.log(`This channel has ${hooks.size} hooks`))
* .catch(console.error); * .catch(console.error);
*/ */
async fetchWebhooks() { fetchWebhooks() {
const data = await this.client.api.channels[this.id].webhooks.get(); return this.guild.channels.fetchWebhooks(this.id);
const hooks = new Collection();
for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook));
return hooks;
} }
/** /**
@@ -149,18 +143,8 @@ class BaseGuildTextChannel extends GuildChannel {
* .then(console.log) * .then(console.log)
* .catch(console.error) * .catch(console.error)
*/ */
async createWebhook(name, { avatar, reason } = {}) { createWebhook(name, options = {}) {
if (typeof avatar === 'string' && !avatar.startsWith('data:')) { return this.guild.channels.createWebhook(this.id, name, options);
avatar = await DataResolver.resolveImage(avatar);
}
const data = await this.client.api.channels[this.id].webhooks.post({
data: {
name,
avatar,
},
reason,
});
return new Webhook(this.client, data);
} }
/** /**
@@ -178,6 +162,14 @@ class BaseGuildTextChannel extends GuildChannel {
return this.edit({ topic }, reason); return this.edit({ topic }, reason);
} }
/**
* Data that can be resolved to an Application. This can be:
* * An Application
* * An Activity with associated Application
* * A Snowflake
* @typedef {Application|Snowflake} ApplicationResolvable
*/
/** /**
* Options used to create an invite to a guild channel. * Options used to create an invite to a guild channel.
* @typedef {Object} CreateInviteOptions * @typedef {Object} CreateInviteOptions

View File

@@ -82,17 +82,18 @@ class BaseGuildVoiceChannel extends GuildChannel {
/** /**
* Sets the RTC region of the channel. * Sets the RTC region of the channel.
* @param {?string} region The new region of the channel. Set to `null` to remove a specific region for the channel * @param {?string} rtcRegion The new region of the channel. Set to `null` to remove a specific region for the channel
* @param {string} [reason] The reason for modifying this region.
* @returns {Promise<BaseGuildVoiceChannel>} * @returns {Promise<BaseGuildVoiceChannel>}
* @example * @example
* // Set the RTC region to europe * // Set the RTC region to sydney
* channel.setRTCRegion('europe'); * channel.setRTCRegion('sydney');
* @example * @example
* // Remove a fixed region for this channel - let Discord decide automatically * // Remove a fixed region for this channel - let Discord decide automatically
* channel.setRTCRegion(null); * channel.setRTCRegion(null, 'We want to let Discord decide.');
*/ */
setRTCRegion(region) { setRTCRegion(rtcRegion, reason) {
return this.edit({ rtcRegion: region }); return this.edit({ rtcRegion }, reason);
} }
/** /**

View File

@@ -4,7 +4,7 @@ const { TypeError } = require('../errors');
const { MessageComponentTypes, Events } = require('../util/Constants'); const { MessageComponentTypes, Events } = require('../util/Constants');
/** /**
* Represents an interactive component of a Message. It should not be necessary to construct this directly. * Represents an interactive component of a Message or Modal. It should not be necessary to construct this directly.
* See {@link MessageComponent} * See {@link MessageComponent}
*/ */
class BaseMessageComponent { class BaseMessageComponent {
@@ -15,18 +15,20 @@ class BaseMessageComponent {
*/ */
/** /**
* Data that can be resolved into options for a MessageComponent. This can be: * Data that can be resolved into options for a component. This can be:
* * MessageActionRowOptions * * MessageActionRowOptions
* * MessageButtonOptions * * MessageButtonOptions
* * MessageSelectMenuOptions * * MessageSelectMenuOptions
* * TextInputComponentOptions
* @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions * @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions
*/ */
/** /**
* Components that can be sent in a message. These can be: * Components that can be sent in a payload. These can be:
* * MessageActionRow * * MessageActionRow
* * MessageButton * * MessageButton
* * MessageSelectMenu * * MessageSelectMenu
* * TextInputComponent
* @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent * @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent
* @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types} * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types}
*/ */
@@ -51,10 +53,10 @@ class BaseMessageComponent {
} }
/** /**
* Constructs a MessageComponent based on the type of the incoming data * Constructs a component based on the type of the incoming data
* @param {MessageComponentOptions} data Data for a MessageComponent * @param {MessageComponentOptions} data Data for a MessageComponent
* @param {Client|WebhookClient} [client] Client constructing this component * @param {Client|WebhookClient} [client] Client constructing this component
* @returns {?MessageComponent} * @returns {?(MessageComponent|ModalComponent)}
* @private * @private
*/ */
static create(data, client) { static create(data, client) {
@@ -79,6 +81,11 @@ class BaseMessageComponent {
component = data instanceof MessageSelectMenu ? data : new MessageSelectMenu(data); component = data instanceof MessageSelectMenu ? data : new MessageSelectMenu(data);
break; break;
} }
case MessageComponentTypes.TEXT_INPUT: {
const TextInputComponent = require('./TextInputComponent');
component = data instanceof TextInputComponent ? data : new TextInputComponent(data);
break;
}
default: default:
if (client) { if (client) {
client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`); client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`);
@@ -90,7 +97,7 @@ class BaseMessageComponent {
} }
/** /**
* Resolves the type of a MessageComponent * Resolves the type of a component
* @param {MessageComponentTypeResolvable} type The type to resolve * @param {MessageComponentTypeResolvable} type The type to resolve
* @returns {MessageComponentType} * @returns {MessageComponentType}
* @private * @private

View File

@@ -10,6 +10,7 @@ let StoreChannel;
let TextChannel; let TextChannel;
let ThreadChannel; let ThreadChannel;
let VoiceChannel; let VoiceChannel;
let DirectoryChannel;
const { ChannelTypes, ThreadChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants'); const { ChannelTypes, ThreadChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants');
const SnowflakeUtil = require('../util/SnowflakeUtil'); const SnowflakeUtil = require('../util/SnowflakeUtil');
@@ -164,6 +165,14 @@ class Channel extends Base {
return ThreadChannelTypes.includes(this.type); return ThreadChannelTypes.includes(this.type);
} }
/**
* Indicates whether this channel is a {@link DirectoryChannel}
* @returns {boolean}
*/
isDirectory() {
return this.type === 'GUILD_DIRECTORY';
}
static create(client, data, guild, { allowUnknownGuild, fromInteraction } = {}) { static create(client, data, guild, { allowUnknownGuild, fromInteraction } = {}) {
CategoryChannel ??= require('./CategoryChannel'); CategoryChannel ??= require('./CategoryChannel');
DMChannel ??= require('./DMChannel'); DMChannel ??= require('./DMChannel');
@@ -173,6 +182,7 @@ class Channel extends Base {
TextChannel ??= require('./TextChannel'); TextChannel ??= require('./TextChannel');
ThreadChannel ??= require('./ThreadChannel'); ThreadChannel ??= require('./ThreadChannel');
VoiceChannel ??= require('./VoiceChannel'); VoiceChannel ??= require('./VoiceChannel');
DirectoryChannel ??= require('./DirectoryChannel');
let channel; let channel;
if (!data.guild_id && !guild) { if (!data.guild_id && !guild) {
@@ -218,6 +228,10 @@ class Channel extends Base {
if (!allowUnknownGuild) channel.parent?.threads.cache.set(channel.id, channel); if (!allowUnknownGuild) channel.parent?.threads.cache.set(channel.id, channel);
break; break;
} }
case ChannelTypes.GUILD_DIRECTORY:
channel = new DirectoryChannel(client, data);
break;
} }
if (channel && !allowUnknownGuild) guild.channels?.cache.set(channel.id, channel); if (channel && !allowUnknownGuild) guild.channels?.cache.set(channel.id, channel);
} }

View File

@@ -4,6 +4,13 @@ const Team = require('./Team');
const Application = require('./interfaces/Application'); const Application = require('./interfaces/Application');
const ApplicationCommandManager = require('../managers/ApplicationCommandManager'); const ApplicationCommandManager = require('../managers/ApplicationCommandManager');
const ApplicationFlags = require('../util/ApplicationFlags'); const ApplicationFlags = require('../util/ApplicationFlags');
const Permissions = require('../util/Permissions');
/**
* @typedef {Object} ClientApplicationInstallParams
* @property {InviteScope[]} scopes The scopes to add the application to the server with
* @property {Readonly<Permissions>} permissions The permissions this bot will request upon joining
*/
/** /**
* Represents a Client OAuth2 Application. * Represents a Client OAuth2 Application.
@@ -23,6 +30,35 @@ class ClientApplication extends Application {
_patch(data) { _patch(data) {
super._patch(data); super._patch(data);
/**
* The tags this application has (max of 5)
* @type {string[]}
*/
this.tags = data.tags ?? [];
if ('install_params' in data) {
/**
* Settings for this application's default in-app authorization
* @type {?ClientApplicationInstallParams}
*/
this.installParams = {
scopes: data.install_params.scopes,
permissions: new Permissions(data.install_params.permissions).freeze(),
};
} else {
this.installParams ??= null;
}
if ('custom_install_url' in data) {
/**
* This application's custom installation URL
* @type {?string}
*/
this.customInstallURL = data.custom_install_url;
} else {
this.customInstallURL = null;
}
if ('flags' in data) { if ('flags' in data) {
/** /**
* The flags this application has * The flags this application has

View File

@@ -251,6 +251,17 @@ class CommandInteractionOptionResolver {
if (!focusedOption) throw new TypeError('AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION'); if (!focusedOption) throw new TypeError('AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION');
return getFull ? focusedOption : focusedOption.value; return getFull ? focusedOption : focusedOption.value;
} }
/**
* Gets an attachment option.
* @param {string} name The name of the option.
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
* @returns {?MessageAttachment} The value of the option, or null if not set and not required.
*/
getAttachment(name, required = false) {
const option = this._getTypedOption(name, 'ATTACHMENT', ['attachment'], required);
return option?.attachment ?? null;
}
} }
module.exports = CommandInteractionOptionResolver; module.exports = CommandInteractionOptionResolver;

View File

@@ -0,0 +1,19 @@
'use strict';
const { Channel } = require('./Channel');
/**
* Represents a channel that displays a directory of guilds
*/
class DirectoryChannel extends Channel {
_patch(data) {
super._patch(data);
/**
* The channel's name
* @type {string}
*/
this.name = data.name;
}
}
module.exports = DirectoryChannel;

View File

@@ -286,14 +286,6 @@ class Guild extends AnonymousGuild {
this.premiumTier = PremiumTiers[data.premium_tier]; this.premiumTier = PremiumTiers[data.premium_tier];
} }
if ('premium_subscription_count' in data) {
/**
* The total number of boosts for this server
* @type {?number}
*/
this.premiumSubscriptionCount = data.premium_subscription_count;
}
if ('widget_enabled' in data) { if ('widget_enabled' in data) {
/** /**
* Whether widget images are enabled on this guild * Whether widget images are enabled on this guild
@@ -419,8 +411,8 @@ class Guild extends AnonymousGuild {
if ('preferred_locale' in data) { if ('preferred_locale' in data) {
/** /**
* The preferred locale of the guild, defaults to `en-US` * The preferred locale of the guild, defaults to `en-US`
* @type {string} * @type {Locale}
* @see {@link https://discord.com/developers/docs/dispatch/field-values#predefined-field-values-accepted-locales} * @see {@link https://discord.com/developers/docs/reference#locales}
*/ */
this.preferredLocale = data.preferred_locale; this.preferredLocale = data.preferred_locale;
} }
@@ -808,24 +800,24 @@ class Guild extends AnonymousGuild {
* The data for editing a guild. * The data for editing a guild.
* @typedef {Object} GuildEditData * @typedef {Object} GuildEditData
* @property {string} [name] The name of the guild * @property {string} [name] The name of the guild
* @property {VerificationLevel|number} [verificationLevel] The verification level of the guild * @property {?(VerificationLevel|number)} [verificationLevel] The verification level of the guild
* @property {ExplicitContentFilterLevel|number} [explicitContentFilter] The level of the explicit content filter * @property {?(ExplicitContentFilterLevel|number)} [explicitContentFilter] The level of the explicit content filter
* @property {VoiceChannelResolvable} [afkChannel] The AFK channel of the guild * @property {?VoiceChannelResolvable} [afkChannel] The AFK channel of the guild
* @property {TextChannelResolvable} [systemChannel] The system channel of the guild * @property {?TextChannelResolvable} [systemChannel] The system channel of the guild
* @property {number} [afkTimeout] The AFK timeout of the guild * @property {number} [afkTimeout] The AFK timeout of the guild
* @property {?(BufferResolvable|Base64Resolvable)} [icon] The icon of the guild * @property {?(BufferResolvable|Base64Resolvable)} [icon] The icon of the guild
* @property {GuildMemberResolvable} [owner] The owner 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)} [splash] The invite splash image of the guild
* @property {?(BufferResolvable|Base64Resolvable)} [discoverySplash] The discovery 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 {?(BufferResolvable|Base64Resolvable)} [banner] The banner of the guild
* @property {DefaultMessageNotificationLevel|number} [defaultMessageNotifications] The default message notification * @property {?(DefaultMessageNotificationLevel|number)} [defaultMessageNotifications] The default message
* level of the guild * notification level of the guild
* @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild * @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild
* @property {TextChannelResolvable} [rulesChannel] The rules channel of the guild * @property {?TextChannelResolvable} [rulesChannel] The rules channel of the guild
* @property {TextChannelResolvable} [publicUpdatesChannel] The community updates channel of the guild * @property {?TextChannelResolvable} [publicUpdatesChannel] The community updates channel of the guild
* @property {string} [preferredLocale] The preferred locale 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 {boolean} [premiumProgressBarEnabled] Whether the guild's premium progress bar is enabled
* @property {string} [description] The discovery description of the guild * @property {?string} [description] The discovery description of the guild
* @property {Features[]} [features] The features of the guild * @property {Features[]} [features] The features of the guild
*/ */
@@ -906,7 +898,7 @@ class Guild extends AnonymousGuild {
if (typeof data.description !== 'undefined') { if (typeof data.description !== 'undefined') {
_data.description = data.description; _data.description = data.description;
} }
if (data.preferredLocale) _data.preferred_locale = data.preferredLocale; if (typeof data.preferredLocale !== 'undefined') _data.preferred_locale = data.preferredLocale;
if ('premiumProgressBarEnabled' in data) _data.premium_progress_bar_enabled = data.premiumProgressBarEnabled; if ('premiumProgressBarEnabled' in data) _data.premium_progress_bar_enabled = data.premiumProgressBarEnabled;
const newData = await this.client.api.guilds(this.id).patch({ data: _data, reason }); const newData = await this.client.api.guilds(this.id).patch({ data: _data, reason });
return this.client.actions.GuildUpdate.handle(newData).updated; return this.client.actions.GuildUpdate.handle(newData).updated;
@@ -984,7 +976,7 @@ class Guild extends AnonymousGuild {
/** /**
* Edits the level of the explicit content filter. * Edits the level of the explicit content filter.
* @param {ExplicitContentFilterLevel|number} explicitContentFilter The new level of the explicit content filter * @param {?(ExplicitContentFilterLevel|number)} explicitContentFilter The new level of the explicit content filter
* @param {string} [reason] Reason for changing the level of the guild's explicit content filter * @param {string} [reason] Reason for changing the level of the guild's explicit content filter
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
*/ */
@@ -995,7 +987,7 @@ class Guild extends AnonymousGuild {
/* eslint-disable max-len */ /* eslint-disable max-len */
/** /**
* Edits the setting of the default message notifications of the guild. * Edits the setting of the default message notifications of the guild.
* @param {DefaultMessageNotificationLevel|number} defaultMessageNotifications The new default message notification level of the guild * @param {?(DefaultMessageNotificationLevel|number)} defaultMessageNotifications The new default message notification level of the guild
* @param {string} [reason] Reason for changing the setting of the default message notifications * @param {string} [reason] Reason for changing the setting of the default message notifications
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
*/ */
@@ -1031,7 +1023,7 @@ class Guild extends AnonymousGuild {
/** /**
* Edits the verification level of the guild. * Edits the verification level of the guild.
* @param {VerificationLevel|number} verificationLevel The new verification level of the guild * @param {?(VerificationLevel|number)} verificationLevel The new verification level of the guild
* @param {string} [reason] Reason for changing the guild's verification level * @param {string} [reason] Reason for changing the guild's verification level
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @example
@@ -1046,7 +1038,7 @@ class Guild extends AnonymousGuild {
/** /**
* Edits the AFK channel of the guild. * Edits the AFK channel of the guild.
* @param {VoiceChannelResolvable} afkChannel The new AFK channel * @param {?VoiceChannelResolvable} afkChannel The new AFK channel
* @param {string} [reason] Reason for changing the guild's AFK channel * @param {string} [reason] Reason for changing the guild's AFK channel
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @example
@@ -1061,7 +1053,7 @@ class Guild extends AnonymousGuild {
/** /**
* Edits the system channel of the guild. * Edits the system channel of the guild.
* @param {TextChannelResolvable} systemChannel The new system channel * @param {?TextChannelResolvable} systemChannel The new system channel
* @param {string} [reason] Reason for changing the guild's system channel * @param {string} [reason] Reason for changing the guild's system channel
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @example
@@ -1166,7 +1158,7 @@ class Guild extends AnonymousGuild {
/** /**
* Edits the rules channel of the guild. * Edits the rules channel of the guild.
* @param {TextChannelResolvable} rulesChannel The new rules channel * @param {?TextChannelResolvable} rulesChannel The new rules channel
* @param {string} [reason] Reason for changing the guild's rules channel * @param {string} [reason] Reason for changing the guild's rules channel
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @example
@@ -1181,7 +1173,7 @@ class Guild extends AnonymousGuild {
/** /**
* Edits the community updates channel of the guild. * Edits the community updates channel of the guild.
* @param {TextChannelResolvable} publicUpdatesChannel The new community updates channel * @param {?TextChannelResolvable} publicUpdatesChannel The new community updates channel
* @param {string} [reason] Reason for changing the guild's community updates channel * @param {string} [reason] Reason for changing the guild's community updates channel
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @example
@@ -1196,7 +1188,7 @@ class Guild extends AnonymousGuild {
/** /**
* Edits the preferred locale of the guild. * Edits the preferred locale of the guild.
* @param {string} preferredLocale The new preferred locale of the guild * @param {?string} preferredLocale The new preferred locale of the guild
* @param {string} [reason] Reason for changing the guild's preferred locale * @param {string} [reason] Reason for changing the guild's preferred locale
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @example

View File

@@ -1,12 +1,10 @@
'use strict'; 'use strict';
const { Channel } = require('./Channel'); const { Channel } = require('./Channel');
const PermissionOverwrites = require('./PermissionOverwrites');
const { Error } = require('../errors'); const { Error } = require('../errors');
const PermissionOverwriteManager = require('../managers/PermissionOverwriteManager'); const PermissionOverwriteManager = require('../managers/PermissionOverwriteManager');
const { ChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants'); const { VoiceBasedChannelTypes } = require('../util/Constants');
const Permissions = require('../util/Permissions'); const Permissions = require('../util/Permissions');
const Util = require('../util/Util');
/** /**
* Represents a guild channel from any of the following: * Represents a guild channel from any of the following:
@@ -262,27 +260,6 @@ class GuildChannel extends Channel {
return this.guild.members.cache.filter(m => this.permissionsFor(m).has(Permissions.FLAGS.VIEW_CHANNEL, false)); return this.guild.members.cache.filter(m => this.permissionsFor(m).has(Permissions.FLAGS.VIEW_CHANNEL, false));
} }
/**
* The data for a guild channel.
* @typedef {Object} ChannelData
* @property {string} [name] The name of the channel
* @property {ChannelType} [type] The type of the channel (only conversion between text and news is supported)
* @property {number} [position] The position of the channel
* @property {string} [topic] The topic of the text channel
* @property {boolean} [nsfw] Whether the channel is NSFW
* @property {number} [bitrate] The bitrate of the voice channel
* @property {number} [userLimit] The user limit of the voice channel
* @property {?CategoryChannelResolvable} [parent] The parent of the channel
* @property {boolean} [lockPermissions]
* Lock the permissions of the channel to what the parent's permissions are
* @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites]
* Permission overwrites for the channel
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the channel in seconds
* @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration]
* The default auto archive duration for all new threads in this channel
* @property {?string} [rtcRegion] The RTC region of the channel
*/
/** /**
* Edits the channel. * Edits the channel.
* @param {ChannelData} data The new data for the channel * @param {ChannelData} data The new data for the channel
@@ -294,64 +271,8 @@ class GuildChannel extends Channel {
* .then(console.log) * .then(console.log)
* .catch(console.error); * .catch(console.error);
*/ */
async edit(data, reason) { edit(data, reason) {
data.parent &&= this.client.channels.resolveId(data.parent); return this.guild.channels.edit(this, data, reason);
if (typeof data.position !== 'undefined') {
const updatedChannels = await Util.setPosition(
this,
data.position,
false,
this.guild._sortedChannels(this),
this.client.api.guilds(this.guild.id).channels,
reason,
);
this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: this.guild.id,
channels: updatedChannels,
});
}
let permission_overwrites;
if (data.permissionOverwrites) {
permission_overwrites = data.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
}
if (data.lockPermissions) {
if (data.parent) {
const newParent = this.guild.channels.resolve(data.parent);
if (newParent?.type === 'GUILD_CATEGORY') {
permission_overwrites = newParent.permissionOverwrites.cache.map(o =>
PermissionOverwrites.resolve(o, this.guild),
);
}
} else if (this.parent) {
permission_overwrites = this.parent.permissionOverwrites.cache.map(o =>
PermissionOverwrites.resolve(o, this.guild),
);
}
}
const newData = await this.client.api.channels(this.id).patch({
data: {
name: (data.name ?? this.name).trim(),
type: ChannelTypes[data.type],
topic: data.topic,
nsfw: data.nsfw,
bitrate: data.bitrate ?? this.bitrate,
user_limit: data.userLimit ?? this.userLimit,
rtc_region: data.rtcRegion ?? this.rtcRegion,
parent_id: data.parent,
lock_permissions: data.lockPermissions,
rate_limit_per_user: data.rateLimitPerUser,
default_auto_archive_duration: data.defaultAutoArchiveDuration,
permission_overwrites,
},
reason,
});
return this.client.actions.ChannelUpdate.handle(newData).updated;
} }
/** /**
@@ -415,30 +336,10 @@ class GuildChannel extends Channel {
* .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`)) * .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))
* .catch(console.error); * .catch(console.error);
*/ */
async setPosition(position, { relative, reason } = {}) { setPosition(position, options = {}) {
const updatedChannels = await Util.setPosition( return this.guild.channels.setPosition(this, position, options);
this,
position,
relative,
this.guild._sortedChannels(this),
this.client.api.guilds(this.guild.id).channels,
reason,
);
this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: this.guild.id,
channels: updatedChannels,
});
return this;
} }
/**
* Data that can be resolved to an Application. This can be:
* * An Application
* * An Activity with associated Application
* * A Snowflake
* @typedef {Application|Snowflake} ApplicationResolvable
*/
/** /**
* Options used to clone a guild channel. * Options used to clone a guild channel.
* @typedef {GuildChannelCreateOptions} GuildChannelCloneOptions * @typedef {GuildChannelCreateOptions} GuildChannelCloneOptions
@@ -544,7 +445,7 @@ class GuildChannel extends Channel {
* .catch(console.error); * .catch(console.error);
*/ */
async delete(reason) { async delete(reason) {
await this.client.api.channels(this.id).delete({ reason }); await this.guild.channels.delete(this.id, reason);
return this; return this;
} }
} }

View File

@@ -72,18 +72,8 @@ class GuildEmoji extends BaseGuildEmoji {
* Fetches the author for this emoji * Fetches the author for this emoji
* @returns {Promise<User>} * @returns {Promise<User>}
*/ */
async fetchAuthor() { fetchAuthor() {
if (this.managed) { return this.guild.emojis.fetchAuthor(this);
throw new Error('EMOJI_MANAGED');
} else {
if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME');
if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS)) {
throw new Error('MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION', this.guild);
}
}
const data = await this.client.api.guilds(this.guild.id).emojis(this.id).get();
this._patch(data);
return this.author;
} }
/** /**
@@ -137,7 +127,7 @@ class GuildEmoji extends BaseGuildEmoji {
* @returns {Promise<GuildEmoji>} * @returns {Promise<GuildEmoji>}
*/ */
async delete(reason) { async delete(reason) {
await this.client.api.guilds(this.guild.id).emojis(this.id).delete({ reason }); await this.guild.emojis.delete(this, reason);
return this; return this;
} }

View File

@@ -300,7 +300,11 @@ class GuildMember extends Base {
* @readonly * @readonly
*/ */
get moderatable() { get moderatable() {
return this.manageable && (this.guild.me?.permissions.has(Permissions.FLAGS.MODERATE_MEMBERS) ?? false); return (
!this.permissions.has(Permissions.FLAGS.ADMINISTRATOR) &&
this.manageable &&
(this.guild.me?.permissions.has(Permissions.FLAGS.MODERATE_MEMBERS) ?? false)
);
} }
/** /**

View File

@@ -3,6 +3,7 @@
const { Collection } = require('@discordjs/collection'); const { Collection } = require('@discordjs/collection');
const Base = require('./Base'); const Base = require('./Base');
const GuildPreviewEmoji = require('./GuildPreviewEmoji'); const GuildPreviewEmoji = require('./GuildPreviewEmoji');
const { Sticker } = require('./Sticker');
const SnowflakeUtil = require('../util/SnowflakeUtil'); const SnowflakeUtil = require('../util/SnowflakeUtil');
/** /**
@@ -103,6 +104,15 @@ class GuildPreview extends Base {
for (const emoji of data.emojis) { for (const emoji of data.emojis) {
this.emojis.set(emoji.id, new GuildPreviewEmoji(this.client, emoji, this)); this.emojis.set(emoji.id, new GuildPreviewEmoji(this.client, emoji, this));
} }
/**
* Collection of stickers belonging to this guild
* @type {Collection<Snowflake, Sticker>}
*/
this.stickers = data.stickers.reduce(
(stickers, sticker) => stickers.set(sticker.id, new Sticker(this.client, sticker)),
new Collection(),
);
} }
/** /**
* The timestamp this guild was created at * The timestamp this guild was created at

View File

@@ -156,6 +156,25 @@ class GuildScheduledEvent extends Base {
} else { } else {
this.entityMetadata ??= null; this.entityMetadata ??= null;
} }
if ('image' in data) {
/**
* The cover image hash for this scheduled event
* @type {?string}
*/
this.image = data.image;
} else {
this.image ??= null;
}
}
/**
* The URL of this scheduled event's cover image
* @param {StaticImageURLOptions} [options={}] Options for image URL
* @returns {?string}
*/
coverImageURL({ format, size } = {}) {
return this.image && this.client.rest.cdn.guildScheduledEventCover(this.id, this.image, format, size);
} }
/** /**

View File

@@ -54,6 +54,7 @@ class IntegrationApplication extends Application {
/** /**
* The application's summary * The application's summary
* @type {?string} * @type {?string}
* @deprecated This property is no longer being sent by the API.
*/ */
this.summary = data.summary; this.summary = data.summary;
} else { } else {

View File

@@ -74,6 +74,54 @@ class Interaction extends Base {
* @type {?Readonly<Permissions>} * @type {?Readonly<Permissions>}
*/ */
this.memberPermissions = data.member?.permissions ? new Permissions(data.member.permissions).freeze() : null; this.memberPermissions = data.member?.permissions ? new Permissions(data.member.permissions).freeze() : null;
/**
* A Discord locale string, possible values are:
* * en-US (English, US)
* * en-GB (English, UK)
* * bg (Bulgarian)
* * zh-CN (Chinese, China)
* * zh-TW (Chinese, Taiwan)
* * hr (Croatian)
* * cs (Czech)
* * da (Danish)
* * nl (Dutch)
* * fi (Finnish)
* * fr (French)
* * de (German)
* * el (Greek)
* * hi (Hindi)
* * hu (Hungarian)
* * it (Italian)
* * ja (Japanese)
* * ko (Korean)
* * lt (Lithuanian)
* * no (Norwegian)
* * pl (Polish)
* * pt-BR (Portuguese, Brazilian)
* * ro (Romanian, Romania)
* * ru (Russian)
* * es-ES (Spanish)
* * sv-SE (Swedish)
* * th (Thai)
* * tr (Turkish)
* * uk (Ukrainian)
* * vi (Vietnamese)
* @see {@link https://discord.com/developers/docs/reference#locales}
* @typedef {string} Locale
*/
/**
* The locale of the user who invoked this interaction
* @type {Locale}
*/
this.locale = data.locale;
/**
* The preferred locale from the guild this interaction was sent in
* @type {?Locale}
*/
this.guildLocale = data.guild_locale ?? null;
} }
/** /**
@@ -160,6 +208,14 @@ class Interaction extends Base {
return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND && typeof this.targetId !== 'undefined'; return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND && typeof this.targetId !== 'undefined';
} }
/**
* Indicates whether this interaction is a {@link ModalSubmitInteraction}
* @returns {boolean}
*/
isModalSubmit() {
return InteractionTypes[this.type] === InteractionTypes.MODAL_SUBMIT;
}
/** /**
* Indicates whether this interaction is a {@link UserContextMenuInteraction} * Indicates whether this interaction is a {@link UserContextMenuInteraction}
* @returns {boolean} * @returns {boolean}
@@ -213,6 +269,16 @@ class Interaction extends Base {
MessageComponentTypes[this.componentType] === MessageComponentTypes.SELECT_MENU MessageComponentTypes[this.componentType] === MessageComponentTypes.SELECT_MENU
); );
} }
/**
* Indicates whether this interaction can be replied to.
* @returns {boolean}
*/
isRepliable() {
return ![InteractionTypes.PING, InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE].includes(
InteractionTypes[this.type],
);
}
} }
module.exports = Interaction; module.exports = Interaction;

View File

@@ -7,9 +7,9 @@ const { InteractionTypes, MessageComponentTypes } = require('../util/Constants')
/** /**
* @typedef {CollectorOptions} InteractionCollectorOptions * @typedef {CollectorOptions} InteractionCollectorOptions
* @property {TextBasedChannels} [channel] The channel to listen to interactions from * @property {TextBasedChannelsResolvable} [channel] The channel to listen to interactions from
* @property {MessageComponentType} [componentType] The type of component to listen for * @property {MessageComponentType} [componentType] The type of component to listen for
* @property {Guild} [guild] The guild to listen to interactions from * @property {GuildResolvable} [guild] The guild to listen to interactions from
* @property {InteractionType} [interactionType] The type of interaction to listen for * @property {InteractionType} [interactionType] The type of interaction to listen for
* @property {number} [max] The maximum total amount of interactions to collect * @property {number} [max] The maximum total amount of interactions to collect
* @property {number} [maxComponents] The maximum number of components to collect * @property {number} [maxComponents] The maximum number of components to collect

View File

@@ -197,8 +197,11 @@ class Invite extends Base {
this.createdTimestamp ??= null; this.createdTimestamp ??= null;
} }
if ('expires_at' in data) this._expiresTimestamp = new Date(data.expires_at).getTime(); if ('expires_at' in data) {
else this._expiresTimestamp ??= null; this._expiresTimestamp = data.expires_at && Date.parse(data.expires_at);
} else {
this._expiresTimestamp ??= null;
}
if ('stage_instance' in data) { if ('stage_instance' in data) {
/** /**

View File

@@ -720,6 +720,7 @@ class Message extends Base {
/** /**
* Pins this message to the channel's pinned messages. * Pins this message to the channel's pinned messages.
* @param {string} [reason] Reason for pinning
* @returns {Promise<Message>} * @returns {Promise<Message>}
* @example * @example
* // Pin a message * // Pin a message
@@ -727,14 +728,15 @@ class Message extends Base {
* .then(console.log) * .then(console.log)
* .catch(console.error) * .catch(console.error)
*/ */
async pin() { async pin(reason) {
if (!this.channel) throw new Error('CHANNEL_NOT_CACHED'); if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
await this.channel.messages.pin(this.id); await this.channel.messages.pin(this.id, reason);
return this; return this;
} }
/** /**
* Unpins this message from the channel's pinned messages. * Unpins this message from the channel's pinned messages.
* @param {string} [reason] Reason for unpinning
* @returns {Promise<Message>} * @returns {Promise<Message>}
* @example * @example
* // Unpin a message * // Unpin a message
@@ -742,9 +744,9 @@ class Message extends Base {
* .then(console.log) * .then(console.log)
* .catch(console.error) * .catch(console.error)
*/ */
async unpin() { async unpin(reason) {
if (!this.channel) throw new Error('CHANNEL_NOT_CACHED'); if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
await this.channel.messages.unpin(this.id); await this.channel.messages.unpin(this.id, reason);
return this; return this;
} }

View File

@@ -12,14 +12,16 @@ class MessageActionRow extends BaseMessageComponent {
* Components that can be placed in an action row * Components that can be placed in an action row
* * MessageButton * * MessageButton
* * MessageSelectMenu * * MessageSelectMenu
* @typedef {MessageButton|MessageSelectMenu} MessageActionRowComponent * * TextInputComponent
* @typedef {MessageButton|MessageSelectMenu|TextInputComponent} MessageActionRowComponent
*/ */
/** /**
* Options for components that can be placed in an action row * Options for components that can be placed in an action row
* * MessageButtonOptions * * MessageButtonOptions
* * MessageSelectMenuOptions * * MessageSelectMenuOptions
* @typedef {MessageButtonOptions|MessageSelectMenuOptions} MessageActionRowComponentOptions * * TextInputComponentOptions
* @typedef {MessageButtonOptions|MessageSelectMenuOptions|TextInputComponentOptions} MessageActionRowComponentOptions
*/ */
/** /**

View File

@@ -124,7 +124,7 @@ class MessageAttachment {
if ('content_type' in data) { if ('content_type' in data) {
/** /**
* This media type of this attachment * The media type of this attachment
* @type {?string} * @type {?string}
*/ */
this.contentType = data.content_type; this.contentType = data.content_type;

View File

@@ -101,6 +101,8 @@ class MessageComponentInteraction extends Interaction {
followUp() {} followUp() {}
deferUpdate() {} deferUpdate() {}
update() {} update() {}
showModal() {}
awaitModalSubmit() {}
} }
InteractionResponses.applyToClass(MessageComponentInteraction); InteractionResponses.applyToClass(MessageComponentInteraction);

View File

@@ -209,7 +209,7 @@ class MessageEmbed {
this.provider = data.provider this.provider = data.provider
? { ? {
name: data.provider.name, name: data.provider.name,
url: data.provider.name, url: data.provider.url,
} }
: null; : null;
@@ -430,7 +430,7 @@ class MessageEmbed {
*/ */
setFooter(options, deprecatedIconURL) { setFooter(options, deprecatedIconURL) {
if (options === null) { if (options === null) {
this.footer = {}; this.footer = undefined;
return this; return this;
} }

View File

@@ -173,28 +173,35 @@ class MessageMentions {
* @typedef {Object} MessageMentionsHasOptions * @typedef {Object} MessageMentionsHasOptions
* @property {boolean} [ignoreDirect=false] Whether to ignore direct mentions to the item * @property {boolean} [ignoreDirect=false] Whether to ignore direct mentions to the item
* @property {boolean} [ignoreRoles=false] Whether to ignore role mentions to a guild member * @property {boolean} [ignoreRoles=false] Whether to ignore role mentions to a guild member
* @property {boolean} [ignoreEveryone=false] Whether to ignore everyone/here mentions * @property {boolean} [ignoreRepliedUser=false] Whether to ignore replied user mention to an user
* @property {boolean} [ignoreEveryone=false] Whether to ignore `@everyone`/`@here` mentions
*/ */
/** /**
* Checks if a user, guild member, role, or channel is mentioned. * Checks if a user, guild member, thread member, role, or channel is mentioned.
* Takes into account user mentions, role mentions, and `@everyone`/`@here` mentions. * Takes into account user mentions, role mentions, channel mentions,
* replied user mention, and `@everyone`/`@here` mentions.
* @param {UserResolvable|RoleResolvable|ChannelResolvable} data The User/Role/Channel to check for * @param {UserResolvable|RoleResolvable|ChannelResolvable} data The User/Role/Channel to check for
* @param {MessageMentionsHasOptions} [options] The options for the check * @param {MessageMentionsHasOptions} [options] The options for the check
* @returns {boolean} * @returns {boolean}
*/ */
has(data, { ignoreDirect = false, ignoreRoles = false, ignoreEveryone = false } = {}) { has(data, { ignoreDirect = false, ignoreRoles = false, ignoreRepliedUser = false, ignoreEveryone = false } = {}) {
if (!ignoreEveryone && this.everyone) return true; const user = this.client.users.resolve(data);
const { GuildMember } = require('./GuildMember'); const role = this.guild?.roles.resolve(data);
if (!ignoreRoles && data instanceof GuildMember) { const channel = this.client.channels.resolve(data);
for (const role of this.roles.values()) if (data.roles.cache.has(role.id)) return true;
}
if (!ignoreRepliedUser && this.users.has(this.repliedUser?.id) && this.repliedUser?.id === user?.id) return true;
if (!ignoreDirect) { if (!ignoreDirect) {
const id = if (this.users.has(user?.id)) return true;
this.guild?.roles.resolveId(data) ?? this.client.channels.resolveId(data) ?? this.client.users.resolveId(data); if (this.roles.has(role?.id)) return true;
if (this.channels.has(channel?.id)) return true;
return typeof id === 'string' && (this.users.has(id) || this.channels.has(id) || this.roles.has(id)); }
if (user && !ignoreEveryone && this.everyone) return true;
if (!ignoreRoles) {
const member = this.guild?.members.resolve(data);
if (member) {
for (const mentionedRole of this.roles.values()) if (member.roles.cache.has(mentionedRole.id)) return true;
}
} }
return false; return false;

View File

@@ -148,11 +148,17 @@ class MessagePayload {
} }
let flags; let flags;
if (this.isMessage || this.isMessageManager) { if (
typeof this.options.flags !== 'undefined' ||
(this.isMessage && typeof this.options.reply === 'undefined') ||
this.isMessageManager
) {
// eslint-disable-next-line eqeqeq // eslint-disable-next-line eqeqeq
flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags?.bitfield; flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags?.bitfield;
} else if (isInteraction && this.options.ephemeral) { }
flags = MessageFlags.FLAGS.EPHEMERAL;
if (isInteraction && this.options.ephemeral) {
flags |= MessageFlags.FLAGS.EPHEMERAL;
} }
let allowedMentions = let allowedMentions =

View File

@@ -114,7 +114,7 @@ class MessageReaction {
if (this.partial) return; if (this.partial) return;
this.users.cache.set(user.id, user); this.users.cache.set(user.id, user);
if (!this.me || user.id !== this.message.client.user.id || this.count === 0) this.count++; if (!this.me || user.id !== this.message.client.user.id || this.count === 0) this.count++;
this.me ??= user.id === this.message.client.user.id; this.me ||= user.id === this.message.client.user.id;
} }
_remove(user) { _remove(user) {

103
src/structures/Modal.js Normal file
View File

@@ -0,0 +1,103 @@
'use strict';
const BaseMessageComponent = require('./BaseMessageComponent');
const Util = require('../util/Util');
/**
* Represents a modal (form) to be shown in response to an interaction
*/
class Modal {
/**
* @typedef {Object} ModalOptions
* @property {string} [customId] A unique string to be sent in the interaction when clicked
* @property {string} [title] The title to be displayed on this modal
* @property {MessageActionRow[]|MessageActionRowOptions[]} [components]
* Action rows containing interactive components for the modal (text input components)
*/
/**
* @param {Modal|ModalOptions} data Modal to clone or raw data
* @param {Client} client The client constructing this Modal, if provided
*/
constructor(data = {}, client = null) {
/**
* A list of MessageActionRows in the modal
* @type {MessageActionRow[]}
*/
this.components = data.components?.map(c => BaseMessageComponent.create(c, client)) ?? [];
/**
* A unique string to be sent in the interaction when submitted
* @type {?string}
*/
this.customId = data.custom_id ?? data.customId ?? null;
/**
* The title to be displayed on this modal
* @type {?string}
*/
this.title = data.title ?? null;
}
/**
* Adds components to the modal.
* @param {...MessageActionRowResolvable[]} components The components to add
* @returns {Modal}
*/
addComponents(...components) {
this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c)));
return this;
}
/**
* Sets the components of the modal.
* @param {...MessageActionRowResolvable[]} components The components to set
* @returns {Modal}
*/
setComponents(...components) {
this.spliceComponents(0, this.components.length, components);
return this;
}
/**
* Sets the custom id for this modal
* @param {string} customId A unique string to be sent in the interaction when submitted
* @returns {Modal}
*/
setCustomId(customId) {
this.customId = Util.verifyString(customId, RangeError, 'MODAL_CUSTOM_ID');
return this;
}
/**
* Removes, replaces, and inserts components in the modal.
* @param {number} index The index to start at
* @param {number} deleteCount The number of components to remove
* @param {...MessageActionRowResolvable[]} [components] The replacing components
* @returns {Modal}
*/
spliceComponents(index, deleteCount, ...components) {
this.components.splice(index, deleteCount, ...components.flat(Infinity).map(c => BaseMessageComponent.create(c)));
return this;
}
/**
* Sets the title of this modal
* @param {string} title The title to be displayed on this modal
* @returns {Modal}
*/
setTitle(title) {
this.title = Util.verifyString(title, RangeError, 'MODAL_TITLE');
return this;
}
toJSON() {
return {
components: this.components.map(c => c.toJSON()),
custom_id: this.customId,
title: this.title,
};
}
}
module.exports = Modal;

View File

@@ -0,0 +1,53 @@
'use strict';
const { TypeError } = require('../errors');
const { MessageComponentTypes } = require('../util/Constants');
/**
* A resolver for modal submit interaction text inputs.
*/
class ModalSubmitFieldsResolver {
constructor(components) {
/**
* The components within the modal
* @type {PartialModalActionRow[]} The components in the modal
*/
this.components = components;
}
/**
* The extracted fields from the modal
* @type {PartialInputTextData[]} The fields in the modal
* @private
*/
get _fields() {
return this.components.reduce((previous, next) => previous.concat(next.components), []);
}
/**
* Gets a field given a custom id from a component
* @param {string} customId The custom id of the component
* @returns {?PartialInputTextData}
*/
getField(customId) {
const field = this._fields.find(f => f.customId === customId);
if (!field) throw new TypeError('MODAL_SUBMIT_INTERACTION_FIELD_NOT_FOUND', customId);
return field;
}
/**
* Gets the value of a text input component given a custom id
* @param {string} customId The custom id of the text input component
* @returns {?string}
*/
getTextInputValue(customId) {
const field = this.getField(customId);
const expectedType = MessageComponentTypes[MessageComponentTypes.TEXT_INPUT];
if (field.type !== expectedType) {
throw new TypeError('MODAL_SUBMIT_INTERACTION_FIELD_TYPE', customId, field.type, expectedType);
}
return field.value;
}
}
module.exports = ModalSubmitFieldsResolver;

View File

@@ -0,0 +1,111 @@
'use strict';
const Interaction = require('./Interaction');
const InteractionWebhook = require('./InteractionWebhook');
const ModalSubmitFieldsResolver = require('./ModalSubmitFieldsResolver');
const InteractionResponses = require('./interfaces/InteractionResponses');
const { MessageComponentTypes } = require('../util/Constants');
/**
* Represents a modal submit interaction.
* @extends {Interaction}
* @implements {InteractionResponses}
*/
class ModalSubmitInteraction extends Interaction {
constructor(client, data) {
super(client, data);
/**
* The custom id of the modal.
* @type {string}
*/
this.customId = data.data.custom_id;
/**
* @typedef {Object} PartialTextInputData
* @property {string} [customId] A unique string to be sent in the interaction when submitted
* @property {MessageComponentType} [type] The type of this component
* @property {string} [value] Value of this text input component
*/
/**
* @typedef {Object} PartialModalActionRow
* @property {MessageComponentType} [type] The type of this component
* @property {PartialTextInputData[]} [components] Partial text input components
*/
/**
* The inputs within the modal
* @type {PartialModalActionRow[]}
*/
this.components =
data.data.components?.map(c => ({
type: MessageComponentTypes[c.type],
components: ModalSubmitInteraction.transformComponent(c),
})) ?? [];
/**
* The message associated with this interaction
* @type {Message|APIMessage|null}
*/
this.message = data.message ? this.channel?.messages._add(data.message) ?? data.message : null;
/**
* The fields within the modal
* @type {ModalSubmitFieldsResolver}
*/
this.fields = new ModalSubmitFieldsResolver(this.components);
/**
* Whether the reply to this interaction has been deferred
* @type {boolean}
*/
this.deferred = false;
/**
* Whether the reply to this interaction is ephemeral
* @type {?boolean}
*/
this.ephemeral = null;
/**
* Whether this interaction has already been replied to
* @type {boolean}
*/
this.replied = false;
/**
* An associated interaction webhook, can be used to further interact with this interaction
* @type {InteractionWebhook}
*/
this.webhook = new InteractionWebhook(this.client, this.applicationId, this.token);
}
/**
* Transforms component data to discord.js-compatible data
* @param {*} rawComponent The data to transform
* @returns {PartialTextInputData[]}
*/
static transformComponent(rawComponent) {
return rawComponent.components.map(c => ({
value: c.value,
type: MessageComponentTypes[c.type],
customId: c.custom_id,
}));
}
// These are here only for documentation purposes - they are implemented by InteractionResponses
/* eslint-disable no-empty-function */
deferReply() {}
reply() {}
fetchReply() {}
editReply() {}
deleteReply() {}
followUp() {}
update() {}
deferUpdate() {}
}
InteractionResponses.applyToClass(ModalSubmitInteraction, ['showModal', 'awaitModalSubmit']);
module.exports = ModalSubmitInteraction;

View File

@@ -351,13 +351,21 @@ class RichPresenceAssets {
* @returns {?string} * @returns {?string}
*/ */
smallImageURL({ format, size } = {}) { smallImageURL({ format, size } = {}) {
return ( if (!this.smallImage) return null;
this.smallImage && if (this.smallImage.includes(':')) {
this.activity.presence.client.rest.cdn.AppAsset(this.activity.applicationId, this.smallImage, { const [platform, id] = this.smallImage.split(':');
format, switch (platform) {
size, case 'mp':
}) return `https://media.discordapp.net/${id}`;
); default:
return null;
}
}
return this.activity.presence.client.rest.cdn.AppAsset(this.activity.applicationId, this.smallImage, {
format,
size,
});
} }
/** /**
@@ -367,11 +375,20 @@ class RichPresenceAssets {
*/ */
largeImageURL({ format, size } = {}) { largeImageURL({ format, size } = {}) {
if (!this.largeImage) return null; if (!this.largeImage) return null;
if (/^spotify:/.test(this.largeImage)) { if (this.largeImage.includes(':')) {
return `https://i.scdn.co/image/${this.largeImage.slice(8)}`; const [platform, id] = this.largeImage.split(':');
} else if (/^twitch:/.test(this.largeImage)) { switch (platform) {
return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${this.largeImage.slice(7)}.png`; case 'mp':
return `https://media.discordapp.net/${id}`;
case 'spotify':
return `https://i.scdn.co/image/${id}`;
case 'twitch':
return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${id}.png`;
default:
return null;
}
} }
return this.activity.presence.client.rest.cdn.AppAsset(this.activity.applicationId, this.largeImage, { return this.activity.presence.client.rest.cdn.AppAsset(this.activity.applicationId, this.largeImage, {
format, format,
size, size,

View File

@@ -5,7 +5,6 @@ const Base = require('./Base');
const { Error } = require('../errors'); const { Error } = require('../errors');
const Permissions = require('../util/Permissions'); const Permissions = require('../util/Permissions');
const SnowflakeUtil = require('../util/SnowflakeUtil'); const SnowflakeUtil = require('../util/SnowflakeUtil');
const Util = require('../util/Util');
let deprecationEmittedForComparePositions = false; let deprecationEmittedForComparePositions = false;
@@ -399,20 +398,8 @@ class Role extends Base {
* .then(updated => console.log(`Role position: ${updated.position}`)) * .then(updated => console.log(`Role position: ${updated.position}`))
* .catch(console.error); * .catch(console.error);
*/ */
async setPosition(position, { relative, reason } = {}) { setPosition(position, options = {}) {
const updatedRoles = await Util.setPosition( return this.guild.roles.setPosition(this, position, options);
this,
position,
relative,
this.guild._sortedRoles(),
this.client.api.guilds(this.guild.id).roles,
reason,
);
this.client.actions.GuildRolesPositionUpdate.handle({
guild_id: this.guild.id,
roles: updatedRoles,
});
return this;
} }
/** /**

View File

@@ -55,14 +55,15 @@ class StageChannel extends BaseGuildVoiceChannel {
/** /**
* Sets the RTC region of the channel. * Sets the RTC region of the channel.
* @name StageChannel#setRTCRegion * @name StageChannel#setRTCRegion
* @param {?string} region The new region of the channel. Set to `null` to remove a specific region for the channel * @param {?string} rtcRegion The new region of the channel. Set to `null` to remove a specific region for the channel
* @param {string} [reason] The reason for modifying this region.
* @returns {Promise<StageChannel>} * @returns {Promise<StageChannel>}
* @example * @example
* // Set the RTC region to europe * // Set the RTC region to sydney
* stageChannel.setRTCRegion('europe'); * stageChannel.setRTCRegion('sydney');
* @example * @example
* // Remove a fixed region for this channel - let Discord decide automatically * // Remove a fixed region for this channel - let Discord decide automatically
* stageChannel.setRTCRegion(null); * stageChannel.setRTCRegion(null, 'We want to let Discord decide.');
*/ */
} }

View File

@@ -72,6 +72,16 @@ class StageInstance extends Base {
} else { } else {
this.discoverableDisabled ??= null; this.discoverableDisabled ??= null;
} }
if ('guild_scheduled_event_id' in data) {
/**
* The associated guild scheduled event id of this stage instance
* @type {?Snowflake}
*/
this.guildScheduledEventId = data.guild_scheduled_event_id;
} else {
this.guildScheduledEventId ??= null;
}
} }
/** /**
@@ -83,6 +93,15 @@ class StageInstance extends Base {
return this.client.channels.resolve(this.channelId); return this.client.channels.resolve(this.channelId);
} }
/**
* The associated guild scheduled event of this stage instance
* @type {?GuildScheduledEvent}
* @readonly
*/
get guildScheduledEvent() {
return this.guild?.scheduledEvents.resolve(this.guildScheduledEventId) ?? null;
}
/** /**
* Whether or not the stage instance has been deleted * Whether or not the stage instance has been deleted
* @type {boolean} * @type {boolean}

View File

@@ -228,10 +228,7 @@ class Sticker extends Base {
async fetchUser() { async fetchUser() {
if (this.partial) await this.fetch(); if (this.partial) await this.fetch();
if (!this.guildId) throw new Error('NOT_GUILD_STICKER'); if (!this.guildId) throw new Error('NOT_GUILD_STICKER');
return this.guild.stickers.fetchUser(this);
const data = await this.client.api.guilds(this.guildId).stickers(this.id).get();
this._patch(data);
return this.user;
} }
/** /**

View File

@@ -4,7 +4,7 @@ const GuildChannel = require('./GuildChannel');
/** /**
* Represents a guild store channel on Discord. * Represents a guild store channel on Discord.
* <warn>Store channels are deprecated and will be removed from Discord in March 2022. See * <warn>Store channels have been removed from Discord. See
* [Self-serve Game Selling Deprecation](https://support-dev.discord.com/hc/en-us/articles/4414590563479) * [Self-serve Game Selling Deprecation](https://support-dev.discord.com/hc/en-us/articles/4414590563479)
* for more information.</warn> * for more information.</warn>
* @extends {GuildChannel} * @extends {GuildChannel}

View File

@@ -0,0 +1,201 @@
'use strict';
const BaseMessageComponent = require('./BaseMessageComponent');
const { RangeError } = require('../errors');
const { TextInputStyles, MessageComponentTypes } = require('../util/Constants');
const Util = require('../util/Util');
/**
* Represents a text input component in a modal
* @extends {BaseMessageComponent}
*/
class TextInputComponent extends BaseMessageComponent {
/**
* @typedef {BaseMessageComponentOptions} TextInputComponentOptions
* @property {string} [customId] A unique string to be sent in the interaction when submitted
* @property {string} [label] The text to be displayed above this text input component
* @property {number} [maxLength] Maximum length of text that can be entered
* @property {number} [minLength] Minimum length of text required to be entered
* @property {string} [placeholder] Custom placeholder text to display when no text is entered
* @property {boolean} [required] Whether or not this text input component is required
* @property {TextInputStyleResolvable} [style] The style of this text input component
* @property {string} [value] Value of this text input component
*/
/**
* @param {TextInputComponent|TextInputComponentOptions} [data={}] TextInputComponent to clone or raw data
*/
constructor(data = {}) {
super({ type: 'TEXT_INPUT' });
this.setup(data);
}
setup(data) {
/**
* A unique string to be sent in the interaction when submitted
* @type {?string}
*/
this.customId = data.custom_id ?? data.customId ?? null;
/**
* The text to be displayed above this text input component
* @type {?string}
*/
this.label = data.label ?? null;
/**
* Maximum length of text that can be entered
* @type {?number}
*/
this.maxLength = data.max_length ?? data.maxLength ?? null;
/**
* Minimum length of text required to be entered
* @type {?string}
*/
this.minLength = data.min_length ?? data.minLength ?? null;
/**
* Custom placeholder text to display when no text is entered
* @type {?string}
*/
this.placeholder = data.placeholder ?? null;
/**
* Whether or not this text input component is required
* @type {?boolean}
*/
this.required = data.required ?? false;
/**
* The style of this text input component
* @type {?TextInputStyle}
*/
this.style = data.style ? TextInputComponent.resolveStyle(data.style) : null;
/**
* Value of this text input component
* @type {?string}
*/
this.value = data.value ?? null;
}
/**
* Sets the custom id of this text input component
* @param {string} customId A unique string to be sent in the interaction when submitted
* @returns {TextInputComponent}
*/
setCustomId(customId) {
this.customId = Util.verifyString(customId, RangeError, 'TEXT_INPUT_CUSTOM_ID');
return this;
}
/**
* Sets the label of this text input component
* @param {string} label The text to be displayed above this text input component
* @returns {TextInputComponent}
*/
setLabel(label) {
this.label = Util.verifyString(label, RangeError, 'TEXT_INPUT_LABEL');
return this;
}
/**
* Sets the text input component to be required for modal submission
* @param {boolean} [required=true] Whether this text input component is required
* @returns {TextInputComponent}
*/
setRequired(required = true) {
this.required = required;
return this;
}
/**
* Sets the maximum length of text input required in this text input component
* @param {number} maxLength Maximum length of text to be required
* @returns {TextInputComponent}
*/
setMaxLength(maxLength) {
this.maxLength = maxLength;
return this;
}
/**
* Sets the minimum length of text input required in this text input component
* @param {number} minLength Minimum length of text to be required
* @returns {TextInputComponent}
*/
setMinLength(minLength) {
this.minLength = minLength;
return this;
}
/**
* Sets the placeholder of this text input component
* @param {string} placeholder Custom placeholder text to display when no text is entered
* @returns {TextInputComponent}
*/
setPlaceholder(placeholder) {
this.placeholder = Util.verifyString(placeholder, RangeError, 'TEXT_INPUT_PLACEHOLDER');
return this;
}
/**
* Sets the style of this text input component
* @param {TextInputStyleResolvable} style The style of this text input component
* @returns {TextInputComponent}
*/
setStyle(style) {
this.style = TextInputComponent.resolveStyle(style);
return this;
}
/**
* Sets the value of this text input component
* @param {string} value Value of this text input component
* @returns {TextInputComponent}
*/
setValue(value) {
this.value = Util.verifyString(value, RangeError, 'TEXT_INPUT_VALUE');
return this;
}
/**
* Transforms the text input component into a plain object
* @returns {APITextInput} The raw data of this text input component
*/
toJSON() {
return {
custom_id: this.customId,
label: this.label,
max_length: this.maxLength,
min_length: this.minLength,
placeholder: this.placeholder,
required: this.required,
style: TextInputStyles[this.style],
type: MessageComponentTypes[this.type],
value: this.value,
};
}
/**
* Data that can be resolved to a TextInputStyle. This can be
* * TextInputStyle
* * number
* @typedef {number|TextInputStyle} TextInputStyleResolvable
*/
/**
* Resolves the style of a text input component
* @param {TextInputStyleResolvable} style The style to resolve
* @returns {TextInputStyle}
* @private
*/
static resolveStyle(style) {
return typeof style === 'string' ? style : TextInputStyles[style];
}
}
module.exports = TextInputComponent;

View File

@@ -6,6 +6,7 @@ const { RangeError } = require('../errors');
const MessageManager = require('../managers/MessageManager'); const MessageManager = require('../managers/MessageManager');
const ThreadMemberManager = require('../managers/ThreadMemberManager'); const ThreadMemberManager = require('../managers/ThreadMemberManager');
const Permissions = require('../util/Permissions'); const Permissions = require('../util/Permissions');
const { resolveAutoArchiveMaxLimit } = require('../util/Util');
/** /**
* Represents a thread channel on Discord. * Represents a thread channel on Discord.
@@ -100,6 +101,11 @@ class ThreadChannel extends Channel {
* @type {?number} * @type {?number}
*/ */
this.archiveTimestamp = new Date(data.thread_metadata.archive_timestamp).getTime(); this.archiveTimestamp = new Date(data.thread_metadata.archive_timestamp).getTime();
if ('create_timestamp' in data.thread_metadata) {
// Note: this is needed because we can't assign directly to getters
this._createdTimestamp = Date.parse(data.thread_metadata.create_timestamp);
}
} else { } else {
this.locked ??= null; this.locked ??= null;
this.archived ??= null; this.archived ??= null;
@@ -108,6 +114,8 @@ class ThreadChannel extends Channel {
this.invitable ??= null; this.invitable ??= null;
} }
this._createdTimestamp ??= this.type === 'GUILD_PRIVATE_THREAD' ? super.createdTimestamp : null;
if ('owner_id' in data) { if ('owner_id' in data) {
/** /**
* The id of the member who created this thread * The id of the member who created this thread
@@ -176,6 +184,16 @@ class ThreadChannel extends Channel {
if (data.messages) for (const message of data.messages) this.messages._add(message); if (data.messages) for (const message of data.messages) this.messages._add(message);
} }
/**
* The timestamp when this thread was created. This isn't available for threads
* created before 2022-01-09
* @type {?number}
* @readonly
*/
get createdTimestamp() {
return this._createdTimestamp;
}
/** /**
* A collection of associated guild member objects of this thread's members * A collection of associated guild member objects of this thread's members
* @type {Collection<Snowflake, GuildMember>} * @type {Collection<Snowflake, GuildMember>}
@@ -196,6 +214,15 @@ class ThreadChannel extends Channel {
return new Date(this.archiveTimestamp); return new Date(this.archiveTimestamp);
} }
/**
* The time the thread was created at
* @type {?Date}
* @readonly
*/
get createdAt() {
return this.createdTimestamp && new Date(this.createdTimestamp);
}
/** /**
* The parent channel of this thread * The parent channel of this thread
* @type {?(NewsChannel|TextChannel)} * @type {?(NewsChannel|TextChannel)}
@@ -288,14 +315,8 @@ class ThreadChannel extends Channel {
*/ */
async edit(data, reason) { async edit(data, reason) {
let autoArchiveDuration = data.autoArchiveDuration; let autoArchiveDuration = data.autoArchiveDuration;
if (data.autoArchiveDuration === 'MAX') { if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.guild);
autoArchiveDuration = 1440;
if (this.guild.features.includes('SEVEN_DAY_THREAD_ARCHIVE')) {
autoArchiveDuration = 10080;
} else if (this.guild.features.includes('THREE_DAY_THREAD_ARCHIVE')) {
autoArchiveDuration = 4320;
}
}
const newData = await this.client.api.channels(this.id).patch({ const newData = await this.client.api.channels(this.id).patch({
data: { data: {
name: (data.name ?? this.name).trim(), name: (data.name ?? this.name).trim(),
@@ -487,7 +508,15 @@ class ThreadChannel extends Channel {
* @readonly * @readonly
*/ */
get unarchivable() { get unarchivable() {
return this.archived && (this.locked ? this.manageable : this.sendable); return this.archived && this.sendable && (!this.locked || this.manageable);
}
/**
* Whether this thread is a private thread
* @returns {boolean}
*/
isPrivate() {
return this.type === 'GUILD_PRIVATE_THREAD';
} }
/** /**
@@ -501,7 +530,7 @@ class ThreadChannel extends Channel {
* .catch(console.error); * .catch(console.error);
*/ */
async delete(reason) { async delete(reason) {
await this.client.api.channels(this.id).delete({ reason }); await this.guild.channels.delete(this.id, reason);
return this; return this;
} }

View File

@@ -2,6 +2,7 @@
const process = require('node:process'); const process = require('node:process');
const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel'); const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel');
const { VideoQualityModes } = require('../util/Constants');
const Permissions = require('../util/Permissions'); const Permissions = require('../util/Permissions');
let deprecationEmittedForEditable = false; let deprecationEmittedForEditable = false;
@@ -11,6 +12,20 @@ let deprecationEmittedForEditable = false;
* @extends {BaseGuildVoiceChannel} * @extends {BaseGuildVoiceChannel}
*/ */
class VoiceChannel extends BaseGuildVoiceChannel { class VoiceChannel extends BaseGuildVoiceChannel {
_patch(data) {
super._patch(data);
if ('video_quality_mode' in data) {
/**
* The camera video quality mode of the channel.
* @type {?VideoQualityMode}
*/
this.videoQualityMode = VideoQualityModes[data.videoQualityMode];
} else {
this.videoQualityMode ??= null;
}
}
/** /**
* Whether the channel is editable by the client user * Whether the channel is editable by the client user
* @type {boolean} * @type {boolean}
@@ -87,17 +102,28 @@ class VoiceChannel extends BaseGuildVoiceChannel {
return this.edit({ userLimit }, reason); return this.edit({ userLimit }, reason);
} }
/**
* Sets the camera video quality mode of the channel.
* @param {VideoQualityMode|number} videoQualityMode The new camera video quality mode.
* @param {string} [reason] Reason for changing the camera video quality mode.
* @returns {Promise<VoiceChannel>}
*/
setVideoQualityMode(videoQualityMode, reason) {
return this.edit({ videoQualityMode }, reason);
}
/** /**
* Sets the RTC region of the channel. * Sets the RTC region of the channel.
* @name VoiceChannel#setRTCRegion * @name VoiceChannel#setRTCRegion
* @param {?string} region The new region of the channel. Set to `null` to remove a specific region for the channel * @param {?string} rtcRegion The new region of the channel. Set to `null` to remove a specific region for the channel
* @param {string} [reason] The reason for modifying this region.
* @returns {Promise<VoiceChannel>} * @returns {Promise<VoiceChannel>}
* @example * @example
* // Set the RTC region to europe * // Set the RTC region to sydney
* voiceChannel.setRTCRegion('europe'); * voiceChannel.setRTCRegion('sydney');
* @example * @example
* // Remove a fixed region for this channel - let Discord decide automatically * // Remove a fixed region for this channel - let Discord decide automatically
* voiceChannel.setRTCRegion(null); * voiceChannel.setRTCRegion(null, 'We want to let Discord decide.');
*/ */
} }

View File

@@ -22,6 +22,7 @@ class VoiceRegion {
/** /**
* Whether the region is VIP-only * Whether the region is VIP-only
* @type {boolean} * @type {boolean}
* @deprecated This property is no longer being sent by the API.
*/ */
this.vip = data.vip; this.vip = data.vip;

View File

@@ -118,7 +118,7 @@ class VoiceState extends Base {
* The time at which the member requested to speak. This property is specific to stage channels only. * The time at which the member requested to speak. This property is specific to stage channels only.
* @type {?number} * @type {?number}
*/ */
this.requestToSpeakTimestamp = new Date(data.request_to_speak_timestamp).getTime(); this.requestToSpeakTimestamp = data.request_to_speak_timestamp && Date.parse(data.request_to_speak_timestamp);
} else { } else {
this.requestToSpeakTimestamp ??= null; this.requestToSpeakTimestamp ??= null;
} }

View File

@@ -116,6 +116,7 @@ class Webhook {
* @property {string} [avatarURL] Avatar URL override for the message * @property {string} [avatarURL] Avatar URL override for the message
* @property {Snowflake} [threadId] The id of the thread in the channel to send to. * @property {Snowflake} [threadId] The id of the thread in the channel to send to.
* <info>For interaction webhooks, this property is ignored</info> * <info>For interaction webhooks, this property is ignored</info>
* @property {MessageFlags} [flags] Which flags to set for the message. Only `SUPPRESS_EMBEDS` can be set.
*/ */
/** /**

View File

@@ -1,11 +1,14 @@
'use strict'; 'use strict';
const process = require('node:process');
const { ClientApplicationAssetTypes, Endpoints } = require('../../util/Constants'); const { ClientApplicationAssetTypes, Endpoints } = require('../../util/Constants');
const SnowflakeUtil = require('../../util/SnowflakeUtil'); const SnowflakeUtil = require('../../util/SnowflakeUtil');
const Base = require('../Base'); const Base = require('../Base');
const AssetTypes = Object.keys(ClientApplicationAssetTypes); const AssetTypes = Object.keys(ClientApplicationAssetTypes);
let deprecationEmittedForFetchAssets = false;
/** /**
* Represents an OAuth2 Application. * Represents an OAuth2 Application.
* @abstract * @abstract
@@ -103,8 +106,18 @@ class Application extends Base {
/** /**
* Gets the application's rich presence assets. * Gets the application's rich presence assets.
* @returns {Promise<Array<ApplicationAsset>>} * @returns {Promise<Array<ApplicationAsset>>}
* @deprecated This will be removed in the next major as it is unsupported functionality.
*/ */
async fetchAssets() { async fetchAssets() {
if (!deprecationEmittedForFetchAssets) {
process.emitWarning(
'Application#fetchAssets is deprecated as it is unsupported and will be removed in the next major version.',
'DeprecationWarning',
);
deprecationEmittedForFetchAssets = true;
}
const assets = await this.client.api.oauth2.applications(this.id).assets.get(); const assets = await this.client.api.oauth2.applications(this.id).assets.get();
return assets.map(a => ({ return assets.map(a => ({
id: a.id, id: a.id,

View File

@@ -1,9 +1,11 @@
'use strict'; 'use strict';
const { Error } = require('../../errors'); const { Error } = require('../../errors');
const { InteractionResponseTypes } = require('../../util/Constants'); const { InteractionResponseTypes, InteractionTypes } = require('../../util/Constants');
const MessageFlags = require('../../util/MessageFlags'); const MessageFlags = require('../../util/MessageFlags');
const InteractionCollector = require('../InteractionCollector');
const MessagePayload = require('../MessagePayload'); const MessagePayload = require('../MessagePayload');
const Modal = require('../Modal');
/** /**
* Interface for classes that support shared interaction response types. * Interface for classes that support shared interaction response types.
@@ -28,6 +30,8 @@ class InteractionResponses {
* @typedef {BaseMessageOptions} InteractionReplyOptions * @typedef {BaseMessageOptions} InteractionReplyOptions
* @property {boolean} [ephemeral] Whether the reply should be ephemeral * @property {boolean} [ephemeral] Whether the reply should be ephemeral
* @property {boolean} [fetchReply] Whether to fetch the reply * @property {boolean} [fetchReply] Whether to fetch the reply
* @property {MessageFlags} [flags] Which flags to set for the message.
* Only `SUPPRESS_EMBEDS` and `EPHEMERAL` can be set.
*/ */
/** /**
@@ -224,6 +228,56 @@ class InteractionResponses {
return options.fetchReply ? this.fetchReply() : undefined; return options.fetchReply ? this.fetchReply() : undefined;
} }
/**
* Shows a modal component
* @param {Modal|ModalOptions} modal The modal to show
* @returns {Promise<void>}
*/
async showModal(modal) {
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
const _modal = modal instanceof Modal ? modal : new Modal(modal);
await this.client.api.interactions(this.id, this.token).callback.post({
data: {
type: InteractionResponseTypes.MODAL,
data: _modal.toJSON(),
},
});
this.replied = true;
}
/**
* An object containing the same properties as CollectorOptions, but a few more:
* @typedef {Object} AwaitModalSubmitOptions
* @property {CollectorFilter} [filter] The filter applied to this collector
* @property {number} time Time to wait for an interaction before rejecting
*/
/**
* Collects a single modal submit interaction that passes the filter.
* The Promise will reject if the time expires.
* @param {AwaitModalSubmitOptions} options Options to pass to the internal collector
* @returns {Promise<ModalSubmitInteraction>}
* @example
* // Collect a modal submit interaction
* const filter = (interaction) => interaction.customId === 'modal';
* interaction.awaitModalSubmit({ filter, time: 15_000 })
* .then(interaction => console.log(`${interaction.customId} was submitted!`))
* .catch(console.error);
*/
awaitModalSubmit(options) {
if (typeof options.time !== 'number') throw new Error('INVALID_TYPE', 'time', 'number');
const _options = { ...options, max: 1, interactionType: InteractionTypes.MODAL_SUBMIT };
return new Promise((resolve, reject) => {
const collector = new InteractionCollector(this.client, _options);
collector.once('end', (interactions, reason) => {
const interaction = interactions.first();
if (interaction) resolve(interaction);
else reject(new Error('INTERACTION_COLLECTOR_ERROR', reason));
});
});
}
static applyToClass(structure, ignore = []) { static applyToClass(structure, ignore = []) {
const props = [ const props = [
'deferReply', 'deferReply',
@@ -234,6 +288,8 @@ class InteractionResponses {
'followUp', 'followUp',
'deferUpdate', 'deferUpdate',
'update', 'update',
'showModal',
'awaitModalSubmit',
]; ];
for (const prop of props) { for (const prop of props) {

View File

@@ -73,6 +73,7 @@ class TextBasedChannel {
* @typedef {BaseMessageOptions} MessageOptions * @typedef {BaseMessageOptions} MessageOptions
* @property {ReplyOptions} [reply] The options for replying to a message * @property {ReplyOptions} [reply] The options for replying to a message
* @property {StickerResolvable[]} [stickers=[]] Stickers to send in the message * @property {StickerResolvable[]} [stickers=[]] Stickers to send in the message
* @property {MessageFlags} [flags] Which flags to set for the message. Only `SUPPRESS_EMBEDS` can be set.
*/ */
/** /**
@@ -128,7 +129,7 @@ class TextBasedChannel {
* channel.send({ * channel.send({
* files: [{ * files: [{
* attachment: 'entire/path/to/file.jpg', * attachment: 'entire/path/to/file.jpg',
* name: 'file.jpg' * name: 'file.jpg',
* description: 'A description of the file' * description: 'A description of the file'
* }] * }]
* }) * })
@@ -147,7 +148,7 @@ class TextBasedChannel {
* ], * ],
* files: [{ * files: [{
* attachment: 'entire/path/to/file.jpg', * attachment: 'entire/path/to/file.jpg',
* name: 'file.jpg' * name: 'file.jpg',
* description: 'A description of the file' * description: 'A description of the file'
* }] * }]
* }) * })
@@ -236,7 +237,7 @@ class TextBasedChannel {
} }
/** /**
* Creates a button interaction collector. * Creates a component interaction collector.
* @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector * @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector
* @returns {InteractionCollector} * @returns {InteractionCollector}
* @example * @example

View File

@@ -77,6 +77,8 @@ exports.Endpoints = {
`${root}/stickers/${stickerId}.${stickerFormat === 'LOTTIE' ? 'json' : 'png'}`, `${root}/stickers/${stickerId}.${stickerFormat === 'LOTTIE' ? 'json' : 'png'}`,
RoleIcon: (roleId, hash, format = 'webp', size) => RoleIcon: (roleId, hash, format = 'webp', size) =>
makeImageUrl(`${root}/role-icons/${roleId}/${hash}`, { size, format }), makeImageUrl(`${root}/role-icons/${roleId}/${hash}`, { size, format }),
guildScheduledEventCover: (scheduledEventId, coverHash, format, size) =>
makeImageUrl(`${root}/guild-events/${scheduledEventId}/${coverHash}`, { size, format }),
}; };
}, },
invite: (root, code, eventId) => (eventId ? `${root}/${code}?event=${eventId}` : `${root}/${code}`), invite: (root, code, eventId) => (eventId ? `${root}/${code}?event=${eventId}` : `${root}/${code}`),
@@ -527,6 +529,7 @@ exports.ActivityTypes = createEnum(['PLAYING', 'STREAMING', 'LISTENING', 'WATCHI
* * `GUILD_PUBLIC_THREAD` - a guild text channel's public thread channel * * `GUILD_PUBLIC_THREAD` - a guild text channel's public thread channel
* * `GUILD_PRIVATE_THREAD` - a guild text channel's private thread channel * * `GUILD_PRIVATE_THREAD` - a guild text channel's private thread channel
* * `GUILD_STAGE_VOICE` - a guild stage voice channel * * `GUILD_STAGE_VOICE` - a guild stage voice channel
* * `GUILD_DIRECTORY` - the channel in a hub containing guilds
* * `UNKNOWN` - a generic channel of unknown type, could be Channel or GuildChannel * * `UNKNOWN` - a generic channel of unknown type, could be Channel or GuildChannel
* @typedef {string} ChannelType * @typedef {string} ChannelType
* @see {@link https://discord.com/developers/docs/resources/channel#channel-object-channel-types} * @see {@link https://discord.com/developers/docs/resources/channel#channel-object-channel-types}
@@ -545,6 +548,7 @@ exports.ChannelTypes = createEnum([
'GUILD_PUBLIC_THREAD', 'GUILD_PUBLIC_THREAD',
'GUILD_PRIVATE_THREAD', 'GUILD_PRIVATE_THREAD',
'GUILD_STAGE_VOICE', 'GUILD_STAGE_VOICE',
'GUILD_DIRECTORY',
]); ]);
/** /**
@@ -556,6 +560,13 @@ exports.ChannelTypes = createEnum([
* @typedef {DMChannel|TextChannel|NewsChannel|ThreadChannel} TextBasedChannels * @typedef {DMChannel|TextChannel|NewsChannel|ThreadChannel} TextBasedChannels
*/ */
/**
* Data that resolves to give a text-based channel. This can be:
* * A text-based channel
* * A snowflake
* @typedef {TextBasedChannels|Snowflake} TextBasedChannelsResolvable
*/
/** /**
* The types of channels that are text-based. The available types are: * The types of channels that are text-based. The available types are:
* * DM * * DM
@@ -1040,6 +1051,7 @@ exports.ApplicationCommandOptionTypes = createEnum([
'ROLE', 'ROLE',
'MENTIONABLE', 'MENTIONABLE',
'NUMBER', 'NUMBER',
'ATTACHMENT',
]); ]);
/** /**
@@ -1066,6 +1078,7 @@ exports.InteractionTypes = createEnum([
'APPLICATION_COMMAND', 'APPLICATION_COMMAND',
'MESSAGE_COMPONENT', 'MESSAGE_COMPONENT',
'APPLICATION_COMMAND_AUTOCOMPLETE', 'APPLICATION_COMMAND_AUTOCOMPLETE',
'MODAL_SUBMIT',
]); ]);
/** /**
@@ -1089,6 +1102,7 @@ exports.InteractionResponseTypes = createEnum([
'DEFERRED_MESSAGE_UPDATE', 'DEFERRED_MESSAGE_UPDATE',
'UPDATE_MESSAGE', 'UPDATE_MESSAGE',
'APPLICATION_COMMAND_AUTOCOMPLETE_RESULT', 'APPLICATION_COMMAND_AUTOCOMPLETE_RESULT',
'MODAL',
]); ]);
/** /**
@@ -1099,7 +1113,7 @@ exports.InteractionResponseTypes = createEnum([
* @typedef {string} MessageComponentType * @typedef {string} MessageComponentType
* @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types} * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types}
*/ */
exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON', 'SELECT_MENU']); exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON', 'SELECT_MENU', 'TEXT_INPUT']);
/** /**
* The style of a message button * The style of a message button
@@ -1142,6 +1156,15 @@ exports.NSFWLevels = createEnum(['DEFAULT', 'EXPLICIT', 'SAFE', 'AGE_RESTRICTED'
*/ */
exports.PrivacyLevels = createEnum([null, 'PUBLIC', 'GUILD_ONLY']); exports.PrivacyLevels = createEnum([null, 'PUBLIC', 'GUILD_ONLY']);
/**
* The style of a text input component
* * SHORT
* * PARAGRAPH
* @typedef {string} TextInputStyle
* @see {@link https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-styles}
*/
exports.TextInputStyles = createEnum([null, 'SHORT', 'PARAGRAPH']);
/** /**
* Privacy level of a {@link GuildScheduledEvent} object: * Privacy level of a {@link GuildScheduledEvent} object:
* * GUILD_ONLY * * GUILD_ONLY
@@ -1182,6 +1205,15 @@ exports.GuildScheduledEventStatuses = createEnum([null, 'SCHEDULED', 'ACTIVE', '
* @see {@link https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-entity-types} * @see {@link https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-entity-types}
*/ */
exports.GuildScheduledEventEntityTypes = createEnum([null, 'STAGE_INSTANCE', 'VOICE', 'EXTERNAL']); exports.GuildScheduledEventEntityTypes = createEnum([null, 'STAGE_INSTANCE', 'VOICE', 'EXTERNAL']);
/**
* The camera video quality mode of a {@link VoiceChannel}:
* * AUTO
* * FULL
* @typedef {string} VideoQualityMode
* @see {@link https://discord.com/developers/docs/resources/channel#channel-object-video-quality-modes}
*/
exports.VideoQualityModes = createEnum([null, 'AUTO', 'FULL']);
/* eslint-enable max-len */ /* eslint-enable max-len */
exports._cleanupSymbol = Symbol('djsCleanup'); exports._cleanupSymbol = Symbol('djsCleanup');
@@ -1235,6 +1267,7 @@ function createEnum(keys) {
* @property {StickerFormatType} StickerFormatTypes The value set for a sticker's format type. * @property {StickerFormatType} StickerFormatTypes The value set for a sticker's format type.
* @property {StickerType} StickerTypes The value set for a sticker's type. * @property {StickerType} StickerTypes The value set for a sticker's type.
* @property {VerificationLevel} VerificationLevels The value set for the verification levels for a guild. * @property {VerificationLevel} VerificationLevels The value set for the verification levels for a guild.
* @property {VideoQualityMode} VideoQualityModes The camera video quality mode for a {@link VoiceChannel}.
* @property {WebhookType} WebhookTypes The value set for a webhook's type. * @property {WebhookType} WebhookTypes The value set for a webhook's type.
* @property {WSEventType} WSEvents The type of a WebSocket message event. * @property {WSEventType} WSEvents The type of a WebSocket message event.
*/ */

View File

@@ -10,7 +10,6 @@ const {
hyperlink, hyperlink,
inlineCode, inlineCode,
italic, italic,
memberNicknameMention,
quote, quote,
roleMention, roleMention,
spoiler, spoiler,
@@ -111,15 +110,6 @@ Formatters.inlineCode = inlineCode;
*/ */
Formatters.italic = italic; Formatters.italic = italic;
/**
* Formats a user id into a member-nickname mention.
* @method memberNicknameMention
* @memberof Formatters
* @param {string} memberId The user id to format.
* @returns {string}
*/
Formatters.memberNicknameMention = memberNicknameMention;
/** /**
* Formats the content into a quote. This needs to be at the start of the line for Discord to format it. * Formats the content into a quote. This needs to be at the start of the line for Discord to format it.
* @method quote * @method quote

View File

@@ -5,7 +5,7 @@ const process = require('node:process');
/** /**
* Rate limit data * Rate limit data
* @typedef {Object} RateLimitData * @typedef {Object} RateLimitData
* @property {number} timeout Time until this rate limit ends, in ms * @property {number} timeout Time until this rate limit ends, in milliseconds
* @property {number} limit The maximum amount of requests of this endpoint * @property {number} limit The maximum amount of requests of this endpoint
* @property {string} method The HTTP method of this request * @property {string} method The HTTP method of this request
* @property {string} path The path of the request relative to the HTTP endpoint * @property {string} path The path of the request relative to the HTTP endpoint
@@ -73,7 +73,7 @@ const process = require('node:process');
* @property {PresenceData} [presence={}] Presence data to use upon login * @property {PresenceData} [presence={}] Presence data to use upon login
* @property {IntentsResolvable} intents Intents to enable for this connection * @property {IntentsResolvable} intents Intents to enable for this connection
* @property {number} [waitGuildTimeout=15_000] Time in milliseconds that Clients with the GUILDS intent should wait for * @property {number} [waitGuildTimeout=15_000] Time in milliseconds that Clients with the GUILDS intent should wait for
* missing guilds to be recieved before starting the bot. If not specified, the default is 15 seconds. * missing guilds to be received before starting the bot. If not specified, the default is 15 seconds.
* @property {SweeperOptions} [sweepers={}] Options for cache sweeping * @property {SweeperOptions} [sweepers={}] Options for cache sweeping
* @property {WebsocketOptions} [ws] Options for the WebSocket * @property {WebsocketOptions} [ws] Options for the WebSocket
* @property {HTTPOptions} [http] HTTP options * @property {HTTPOptions} [http] HTTP options

View File

@@ -16,7 +16,7 @@ class SnowflakeUtil extends null {
* ``` * ```
* 64 22 17 12 0 * 64 22 17 12 0
* 000000111011000111100001101001000101000000 00001 00000 000000000000 * 000000111011000111100001101001000101000000 00001 00000 000000000000
* number of ms since Discord epoch worker pid increment * number of milliseconds since Discord epoch worker pid increment
* ``` * ```
* @typedef {string} Snowflake * @typedef {string} Snowflake
*/ */

View File

@@ -192,6 +192,15 @@ class Sweepers {
return this._sweepGuildDirectProp('stageInstances', filter, { outputName: 'stage instances' }).items; return this._sweepGuildDirectProp('stageInstances', filter, { outputName: 'stage instances' }).items;
} }
/**
* Sweeps all guild stickers and removes the ones which are indicated by the filter.
* @param {Function} filter The function used to determine which stickers will be removed from the caches.
* @returns {number} Amount of stickers that were removed from the caches
*/
sweepStickers(filter) {
return this._sweepGuildDirectProp('stickers', filter).items;
}
/** /**
* Sweeps all thread members and removes the ones which are indicated by the filter. * Sweeps all thread members and removes the ones which are indicated by the filter.
* <info>It is highly recommended to keep the client thread member cached</info> * <info>It is highly recommended to keep the client thread member cached</info>
@@ -375,11 +384,11 @@ class Sweepers {
* Sweep a direct sub property of all guilds * Sweep a direct sub property of all guilds
* @param {string} key The name of the property * @param {string} key The name of the property
* @param {Function} filter Filter function passed to sweep * @param {Function} filter Filter function passed to sweep
* @param {SweepEventOptions} [eventOptions] Options for the Client event emitted here * @param {SweepEventOptions} [eventOptions={}] Options for the Client event emitted here
* @returns {Object} Object containing the number of guilds swept and the number of items swept * @returns {Object} Object containing the number of guilds swept and the number of items swept
* @private * @private
*/ */
_sweepGuildDirectProp(key, filter, { emit = true, outputName }) { _sweepGuildDirectProp(key, filter, { emit = true, outputName } = {}) {
if (typeof filter !== 'function') { if (typeof filter !== 'function') {
throw new TypeError('INVALID_TYPE', 'filter', 'function'); throw new TypeError('INVALID_TYPE', 'filter', 'function');
} }

View File

@@ -10,6 +10,7 @@ const { Error: DiscordError, RangeError, TypeError } = require('../errors');
const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k); const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
const isObject = d => typeof d === 'object' && d !== null; const isObject = d => typeof d === 'object' && d !== null;
let deprecationEmittedForSplitMessage = false;
let deprecationEmittedForRemoveMentions = false; let deprecationEmittedForRemoveMentions = false;
/** /**
@@ -70,9 +71,19 @@ class Util extends null {
* Splits a string into multiple chunks at a designated character that do not exceed a specific length. * Splits a string into multiple chunks at a designated character that do not exceed a specific length.
* @param {string} text Content to split * @param {string} text Content to split
* @param {SplitOptions} [options] Options controlling the behavior of the split * @param {SplitOptions} [options] Options controlling the behavior of the split
* @deprecated This will be removed in the next major version.
* @returns {string[]} * @returns {string[]}
*/ */
static splitMessage(text, { maxLength = 2_000, char = '\n', prepend = '', append = '' } = {}) { static splitMessage(text, { maxLength = 2_000, char = '\n', prepend = '', append = '' } = {}) {
if (!deprecationEmittedForSplitMessage) {
process.emitWarning(
'The Util.splitMessage method is deprecated and will be removed in the next major version.',
'DeprecationWarning',
);
deprecationEmittedForSplitMessage = true;
}
text = Util.verifyString(text); text = Util.verifyString(text);
if (text.length <= maxLength) return [text]; if (text.length <= maxLength) return [text];
let splitText = [text]; let splitText = [text];
@@ -603,6 +614,17 @@ class Util extends null {
filter.isDefault = true; filter.isDefault = true;
return filter; return filter;
} }
/**
* Resolves the maximum time a guild's thread channels should automatcally archive in case of no recent activity.
* @param {Guild} guild The guild to resolve this limit from.
* @returns {number}
*/
static resolveAutoArchiveMaxLimit({ features }) {
if (features.includes('SEVEN_DAY_THREAD_ARCHIVE')) return 10080;
if (features.includes('THREE_DAY_THREAD_ARCHIVE')) return 4320;
return 1440;
}
} }
module.exports = Util; module.exports = Util;

20
typings/enums.d.ts vendored
View File

@@ -27,6 +27,7 @@ export const enum ApplicationCommandOptionTypes {
ROLE = 8, ROLE = 8,
MENTIONABLE = 9, MENTIONABLE = 9,
NUMBER = 10, NUMBER = 10,
ATTACHMENT = 11,
} }
export const enum ApplicationCommandPermissionTypes { export const enum ApplicationCommandPermissionTypes {
@@ -47,6 +48,7 @@ export const enum ChannelTypes {
GUILD_PUBLIC_THREAD = 11, GUILD_PUBLIC_THREAD = 11,
GUILD_PRIVATE_THREAD = 12, GUILD_PRIVATE_THREAD = 12,
GUILD_STAGE_VOICE = 13, GUILD_STAGE_VOICE = 13,
GUILD_DIRECTORY = 14,
} }
export const enum MessageTypes { export const enum MessageTypes {
@@ -110,6 +112,7 @@ export const enum InteractionResponseTypes {
DEFERRED_MESSAGE_UPDATE = 6, DEFERRED_MESSAGE_UPDATE = 6,
UPDATE_MESSAGE = 7, UPDATE_MESSAGE = 7,
APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8, APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8,
MODAL = 9,
} }
export const enum InteractionTypes { export const enum InteractionTypes {
@@ -117,6 +120,7 @@ export const enum InteractionTypes {
APPLICATION_COMMAND = 2, APPLICATION_COMMAND = 2,
MESSAGE_COMPONENT = 3, MESSAGE_COMPONENT = 3,
APPLICATION_COMMAND_AUTOCOMPLETE = 4, APPLICATION_COMMAND_AUTOCOMPLETE = 4,
MODAL_SUBMIT = 5,
} }
export const enum InviteTargetType { export const enum InviteTargetType {
@@ -141,6 +145,12 @@ export const enum MessageComponentTypes {
ACTION_ROW = 1, ACTION_ROW = 1,
BUTTON = 2, BUTTON = 2,
SELECT_MENU = 3, SELECT_MENU = 3,
TEXT_INPUT = 4,
}
export const enum ModalComponentTypes {
ACTION_ROW = 1,
TEXT_INPUT = 4,
} }
export const enum MFALevels { export const enum MFALevels {
@@ -183,6 +193,11 @@ export const enum StickerTypes {
GUILD = 2, GUILD = 2,
} }
export const enum TextInputStyles {
SHORT = 1,
PARAGRAPH = 2,
}
export const enum VerificationLevels { export const enum VerificationLevels {
NONE = 0, NONE = 0,
LOW = 1, LOW = 1,
@@ -191,6 +206,11 @@ export const enum VerificationLevels {
VERY_HIGH = 4, VERY_HIGH = 4,
} }
export const enum VideoQualityModes {
AUTO = 1,
FULL = 2,
}
export const enum WebhookTypes { export const enum WebhookTypes {
Incoming = 1, Incoming = 1,
'Channel Follower' = 2, 'Channel Follower' = 2,

499
typings/index.d.ts vendored

File diff suppressed because it is too large Load Diff

View File

@@ -93,6 +93,9 @@ import {
MessageActionRowComponent, MessageActionRowComponent,
MessageSelectMenu, MessageSelectMenu,
PartialDMChannel, PartialDMChannel,
InteractionResponseFields,
GuildBan,
GuildBanManager,
} from '.'; } from '.';
import type { ApplicationCommandOptionTypes } from './enums'; import type { ApplicationCommandOptionTypes } from './enums';
import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd'; import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd';
@@ -678,6 +681,8 @@ client.on('interaction', async interaction => {
void new MessageActionRow(); void new MessageActionRow();
void new MessageActionRow({});
const button = new MessageButton(); const button = new MessageButton();
const actionRow = new MessageActionRow({ components: [button] }); const actionRow = new MessageActionRow({ components: [button] });
@@ -687,9 +692,6 @@ client.on('interaction', async interaction => {
// @ts-expect-error // @ts-expect-error
interaction.reply({ content: 'Hi!', components: [[button]] }); interaction.reply({ content: 'Hi!', components: [[button]] });
// @ts-expect-error
void new MessageActionRow({});
// @ts-expect-error // @ts-expect-error
await interaction.reply({ content: 'Hi!', components: [button] }); await interaction.reply({ content: 'Hi!', components: [button] });
@@ -867,6 +869,8 @@ declare const guildChannelManager: GuildChannelManager;
{ {
type AnyChannel = TextChannel | VoiceChannel | CategoryChannel | NewsChannel | StoreChannel | StageChannel; type AnyChannel = TextChannel | VoiceChannel | CategoryChannel | NewsChannel | StoreChannel | StageChannel;
expectType<Promise<TextChannel>>(guildChannelManager.create('name'));
expectType<Promise<TextChannel>>(guildChannelManager.create('name', {}));
expectType<Promise<VoiceChannel>>(guildChannelManager.create('name', { type: 'GUILD_VOICE' })); expectType<Promise<VoiceChannel>>(guildChannelManager.create('name', { type: 'GUILD_VOICE' }));
expectType<Promise<CategoryChannel>>(guildChannelManager.create('name', { type: 'GUILD_CATEGORY' })); expectType<Promise<CategoryChannel>>(guildChannelManager.create('name', { type: 'GUILD_CATEGORY' }));
expectType<Promise<TextChannel>>(guildChannelManager.create('name', { type: 'GUILD_TEXT' })); expectType<Promise<TextChannel>>(guildChannelManager.create('name', { type: 'GUILD_TEXT' }));
@@ -889,6 +893,20 @@ expectType<Promise<Collection<Snowflake, GuildEmoji>>>(guildEmojiManager.fetch()
expectType<Promise<Collection<Snowflake, GuildEmoji>>>(guildEmojiManager.fetch(undefined, {})); expectType<Promise<Collection<Snowflake, GuildEmoji>>>(guildEmojiManager.fetch(undefined, {}));
expectType<Promise<GuildEmoji>>(guildEmojiManager.fetch('0')); expectType<Promise<GuildEmoji>>(guildEmojiManager.fetch('0'));
declare const guildBanManager: GuildBanManager;
{
expectType<Promise<GuildBan>>(guildBanManager.fetch('1234567890'));
expectType<Promise<GuildBan>>(guildBanManager.fetch({ user: '1234567890' }));
expectType<Promise<GuildBan>>(guildBanManager.fetch({ user: '1234567890', cache: true, force: false }));
expectType<Promise<Collection<Snowflake, GuildBan>>>(guildBanManager.fetch());
expectType<Promise<Collection<Snowflake, GuildBan>>>(guildBanManager.fetch({}));
expectType<Promise<Collection<Snowflake, GuildBan>>>(guildBanManager.fetch({ limit: 100, before: '1234567890' }));
// @ts-expect-error
guildBanManager.fetch({ cache: true, force: false });
// @ts-expect-error
guildBanManager.fetch({ user: '1234567890', after: '1234567890', cache: true, force: false });
}
declare const typing: Typing; declare const typing: Typing;
expectType<PartialUser>(typing.user); expectType<PartialUser>(typing.user);
if (typing.user.partial) expectType<null>(typing.user.username); if (typing.user.partial) expectType<null>(typing.user.username);
@@ -942,19 +960,45 @@ expectDeprecated(sticker.deleted);
// Test interactions // Test interactions
declare const interaction: Interaction; declare const interaction: Interaction;
declare const booleanValue: boolean; declare const booleanValue: boolean;
if (interaction.inGuild()) expectType<Snowflake>(interaction.guildId); if (interaction.inGuild()) {
expectType<Snowflake>(interaction.guildId);
} else {
expectType<Snowflake | null>(interaction.guildId);
}
client.on('interactionCreate', interaction => {
// This is for testing never type resolution
if (!interaction.inGuild()) {
return;
}
if (interaction.inRawGuild()) {
expectNotType<never>(interaction);
return;
}
if (interaction.inCachedGuild()) {
expectNotType<never>(interaction);
return;
}
});
client.on('interactionCreate', async interaction => { client.on('interactionCreate', async interaction => {
if (interaction.inCachedGuild()) { if (interaction.inCachedGuild()) {
expectAssignable<GuildMember>(interaction.member); expectAssignable<GuildMember>(interaction.member);
expectNotType<CommandInteraction<'cached'>>(interaction); expectNotType<CommandInteraction<'cached'>>(interaction);
expectAssignable<Interaction>(interaction); expectAssignable<Interaction>(interaction);
expectType<string>(interaction.guildLocale);
} else if (interaction.inRawGuild()) { } else if (interaction.inRawGuild()) {
expectAssignable<APIInteractionGuildMember>(interaction.member); expectAssignable<APIInteractionGuildMember>(interaction.member);
expectNotAssignable<Interaction<'cached'>>(interaction); expectNotAssignable<Interaction<'cached'>>(interaction);
expectType<string>(interaction.guildLocale);
} else if (interaction.inGuild()) {
expectType<string>(interaction.guildLocale);
} else { } else {
expectType<APIInteractionGuildMember | GuildMember | null>(interaction.member); expectType<APIInteractionGuildMember | GuildMember | null>(interaction.member);
expectNotAssignable<Interaction<'cached'>>(interaction); expectNotAssignable<Interaction<'cached'>>(interaction);
expectType<string | null>(interaction.guildId);
} }
if (interaction.isContextMenu()) { if (interaction.isContextMenu()) {
@@ -1117,6 +1161,16 @@ client.on('interactionCreate', async interaction => {
expectType<string | null>(interaction.options.getSubcommandGroup(booleanValue)); expectType<string | null>(interaction.options.getSubcommandGroup(booleanValue));
expectType<string | null>(interaction.options.getSubcommandGroup(false)); expectType<string | null>(interaction.options.getSubcommandGroup(false));
} }
if (interaction.isRepliable()) {
expectAssignable<InteractionResponseFields>(interaction);
interaction.reply('test');
}
if (interaction.isCommand() && interaction.isRepliable()) {
expectAssignable<CommandInteraction>(interaction);
expectAssignable<InteractionResponseFields>(interaction);
}
}); });
declare const shard: Shard; declare const shard: Shard;

View File

@@ -76,8 +76,13 @@ import {
RESTPostAPIWebhookWithTokenJSONBody, RESTPostAPIWebhookWithTokenJSONBody,
Snowflake, Snowflake,
APIGuildScheduledEvent, APIGuildScheduledEvent,
APIActionRowComponent,
APITextInputComponent,
APIModalActionRowComponent,
APIModalSubmitInteraction,
} from 'discord-api-types/v9'; } from 'discord-api-types/v9';
import { GuildChannel, Guild, PermissionOverwrites } from '.'; import { GuildChannel, Guild, PermissionOverwrites, InteractionType } from '.';
import type { InteractionTypes, MessageComponentTypes } from './enums';
export type RawActivityData = GatewayActivity; export type RawActivityData = GatewayActivity;
@@ -141,6 +146,9 @@ export type RawMessageComponentInteractionData = APIMessageComponentInteraction;
export type RawMessageButtonInteractionData = APIMessageButtonInteractionData; export type RawMessageButtonInteractionData = APIMessageButtonInteractionData;
export type RawMessageSelectMenuInteractionData = APIMessageSelectMenuInteractionData; export type RawMessageSelectMenuInteractionData = APIMessageSelectMenuInteractionData;
export type RawTextInputComponentData = APITextInputComponent;
export type RawModalSubmitInteractionData = APIModalSubmitInteraction;
export type RawInviteData = export type RawInviteData =
| APIExtendedInvite | APIExtendedInvite
| APIInvite | APIInvite