diff --git a/src/client/actions/WebhooksUpdate.js b/src/client/actions/WebhooksUpdate.js index 7efccd729f9f..6c9aa35832ee 100644 --- a/src/client/actions/WebhooksUpdate.js +++ b/src/client/actions/WebhooksUpdate.js @@ -10,7 +10,7 @@ class WebhooksUpdate extends Action { /** * Emitted whenever a channel has its webhooks changed. * @event Client#webhookUpdate - * @param {TextChannel|NewsChannel} channel The channel that had a webhook update + * @param {TextChannel|NewsChannel|VoiceChannel} channel The channel that had a webhook update */ if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel); } diff --git a/src/structures/BaseGuildTextChannel.js b/src/structures/BaseGuildTextChannel.js index fd59508caf09..5e71f54f9597 100644 --- a/src/structures/BaseGuildTextChannel.js +++ b/src/structures/BaseGuildTextChannel.js @@ -89,16 +89,6 @@ class BaseGuildTextChannel extends GuildChannel { return this.edit({ defaultAutoArchiveDuration }, reason); } - /** - * Sets whether this channel is flagged as NSFW. - * @param {boolean} [nsfw=true] Whether the channel should be considered NSFW - * @param {string} [reason] Reason for changing the channel's NSFW flag - * @returns {Promise} - */ - setNSFW(nsfw = true, reason) { - return this.edit({ nsfw }, reason); - } - /** * Sets the type of this channel (only conversion between text and news is supported) * @param {string} type The new channel type @@ -109,44 +99,6 @@ class BaseGuildTextChannel extends GuildChannel { return this.edit({ type }, reason); } - /** - * Fetches all webhooks for the channel. - * @returns {Promise>} - * @example - * // Fetch webhooks - * channel.fetchWebhooks() - * .then(hooks => console.log(`This channel has ${hooks.size} hooks`)) - * .catch(console.error); - */ - fetchWebhooks() { - return this.guild.channels.fetchWebhooks(this.id); - } - - /** - * Options used to create a {@link Webhook} in a {@link TextChannel} or a {@link NewsChannel}. - * @typedef {Object} ChannelWebhookCreateOptions - * @property {?(BufferResolvable|Base64Resolvable)} [avatar] Avatar for the webhook - * @property {string} [reason] Reason for creating the webhook - */ - - /** - * Creates a webhook for the channel. - * @param {string} name The name of the webhook - * @param {ChannelWebhookCreateOptions} [options] Options for creating the webhook - * @returns {Promise} Returns the created Webhook - * @example - * // Create a webhook for the current channel - * channel.createWebhook('Snek', { - * avatar: 'https://i.imgur.com/mI8XcpG.jpg', - * reason: 'Needed a cool new Webhook' - * }) - * .then(console.log) - * .catch(console.error) - */ - createWebhook(name, options = {}) { - return this.guild.channels.createWebhook(this.id, name, options); - } - /** * Sets a new topic for the guild channel. * @param {?string} topic The new topic for the guild channel @@ -221,6 +173,10 @@ class BaseGuildTextChannel extends GuildChannel { createMessageComponentCollector() {} awaitMessageComponent() {} bulkDelete() {} + fetchWebhooks() {} + createWebhook() {} + setRateLimitPerUser() {} + setNSFW() {} } TextBasedChannel.applyToClass(BaseGuildTextChannel, true); diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 555de4f40e0a..8a1b4dda9bf7 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -94,8 +94,16 @@ class DMChannel extends Channel { createMessageComponentCollector() {} awaitMessageComponent() {} // Doesn't work on DM channels; bulkDelete() {} + // Doesn't work on DM channels; setRateLimitPerUser() {} + // Doesn't work on DM channels; setNSFW() {} } -TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']); +TextBasedChannel.applyToClass(DMChannel, true, [ + 'bulkDelete', + 'fetchWebhooks', + 'createWebhook', + 'setRateLimitPerUser', + 'setNSFW', +]); module.exports = DMChannel; diff --git a/src/structures/Message.js b/src/structures/Message.js index 2b2648a29202..4c18aeccd284 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -383,7 +383,7 @@ class Message extends Base { /** * The channel that the message was sent in - * @type {TextChannel|DMChannel|NewsChannel|ThreadChannel} + * @type {TextBasedChannel} * @readonly */ get channel() { diff --git a/src/structures/MessagePayload.js b/src/structures/MessagePayload.js index 8c1bb94b5a2f..9e30c6961b46 100644 --- a/src/structures/MessagePayload.js +++ b/src/structures/MessagePayload.js @@ -277,7 +277,7 @@ module.exports = MessagePayload; /** * A target for a message. - * @typedef {TextChannel|DMChannel|User|GuildMember|Webhook|WebhookClient|Interaction|InteractionWebhook| + * @typedef {TextBasedChannel|User|GuildMember|Webhook|WebhookClient|Interaction|InteractionWebhook| * Message|MessageManager} MessageTarget */ diff --git a/src/structures/ThreadChannel.js b/src/structures/ThreadChannel.js index 0ef83d2e648d..33ffcfad13d0 100644 --- a/src/structures/ThreadChannel.js +++ b/src/structures/ThreadChannel.js @@ -545,8 +545,10 @@ class ThreadChannel extends Channel { createMessageComponentCollector() {} awaitMessageComponent() {} bulkDelete() {} + // Doesn't work on Thread channels; setRateLimitPerUser() {} + // Doesn't work on Thread channels; setNSFW() {} } -TextBasedChannel.applyToClass(ThreadChannel, true); +TextBasedChannel.applyToClass(ThreadChannel, true, ['setRateLimitPerUser', 'setNSFW']); module.exports = ThreadChannel; diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index f8b11bff31af..bd5a07a0b37b 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -2,6 +2,8 @@ const process = require('node:process'); const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const MessageManager = require('../managers/MessageManager'); const { VideoQualityModes } = require('../util/Constants'); const Permissions = require('../util/Permissions'); @@ -10,8 +12,21 @@ let deprecationEmittedForEditable = false; /** * Represents a guild voice channel on Discord. * @extends {BaseGuildVoiceChannel} + * @implements {TextBasedChannel} */ class VoiceChannel extends BaseGuildVoiceChannel { + constructor(guild, data, client) { + super(guild, data, client, false); + + /** + * A manager of the messages sent to this channel + * @type {MessageManager} + */ + this.messages = new MessageManager(this); + + this._patch(data); + } + _patch(data) { super._patch(data); @@ -24,6 +39,26 @@ class VoiceChannel extends BaseGuildVoiceChannel { } else { this.videoQualityMode ??= null; } + + if ('last_message_id' in data) { + /** + * The last message id sent in the channel, if one was sent + * @type {?Snowflake} + */ + this.lastMessageId = data.last_message_id; + } + + if ('messages' in data) { + for (const message of data.messages) this.messages._add(message); + } + + if ('rate_limit_per_user' in data) { + /** + * The rate limit per user (slowmode) for this channel in seconds + * @type {number} + */ + this.rateLimitPerUser = data.rate_limit_per_user; + } } /** @@ -112,6 +147,21 @@ class VoiceChannel extends BaseGuildVoiceChannel { return this.edit({ videoQualityMode }, reason); } + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + get lastMessage() {} + send() {} + sendTyping() {} + createMessageCollector() {} + awaitMessages() {} + createMessageComponentCollector() {} + awaitMessageComponent() {} + bulkDelete() {} + fetchWebhooks() {} + createWebhook() {} + setRateLimitPerUser() {} + setNSFW() {} + /** * Sets the RTC region of the channel. * @name VoiceChannel#setRTCRegion @@ -127,4 +177,6 @@ class VoiceChannel extends BaseGuildVoiceChannel { */ } +TextBasedChannel.applyToClass(VoiceChannel, true, ['lastPinAt']); + module.exports = VoiceChannel; diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index c80edd271c15..b50555c9bcb1 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -330,6 +330,64 @@ class TextBasedChannel { throw new TypeError('MESSAGE_BULK_DELETE_TYPE'); } + /** + * Fetches all webhooks for the channel. + * @returns {Promise>} + * @example + * // Fetch webhooks + * channel.fetchWebhooks() + * .then(hooks => console.log(`This channel has ${hooks.size} hooks`)) + * .catch(console.error); + */ + fetchWebhooks() { + return this.guild.channels.fetchWebhooks(this.id); + } + + /** + * Options used to create a {@link Webhook} in a guild text-based channel. + * @typedef {Object} ChannelWebhookCreateOptions + * @property {?(BufferResolvable|Base64Resolvable)} [avatar] Avatar for the webhook + * @property {string} [reason] Reason for creating the webhook + */ + + /** + * Creates a webhook for the channel. + * @param {string} name The name of the webhook + * @param {ChannelWebhookCreateOptions} [options] Options for creating the webhook + * @returns {Promise} Returns the created Webhook + * @example + * // Create a webhook for the current channel + * channel.createWebhook('Snek', { + * avatar: 'https://i.imgur.com/mI8XcpG.jpg', + * reason: 'Needed a cool new Webhook' + * }) + * .then(console.log) + * .catch(console.error) + */ + createWebhook(name, options = {}) { + return this.guild.channels.createWebhook(this.id, name, options); + } + + /** + * Sets the rate limit per user (slowmode) for this channel. + * @param {number} rateLimitPerUser The new rate limit in seconds + * @param {string} [reason] Reason for changing the channel's rate limit + * @returns {Promise} + */ + setRateLimitPerUser(rateLimitPerUser, reason) { + return this.edit({ rateLimitPerUser }, reason); + } + + /** + * Sets whether this channel is flagged as NSFW. + * @param {boolean} [nsfw=true] Whether the channel should be considered NSFW + * @param {string} [reason] Reason for changing the channel's NSFW flag + * @returns {Promise} + */ + setNSFW(nsfw = true, reason) { + return this.edit({ nsfw }, reason); + } + static applyToClass(structure, full = false, ignore = []) { const props = ['send']; if (full) { @@ -342,6 +400,10 @@ class TextBasedChannel { 'awaitMessages', 'createMessageComponentCollector', 'awaitMessageComponent', + 'fetchWebhooks', + 'createWebhook', + 'setRateLimitPerUser', + 'setNSFW', ); } for (const prop of props) { diff --git a/src/util/Constants.js b/src/util/Constants.js index dc24eaa2cc19..8610f23aaa4d 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -558,7 +558,8 @@ exports.ChannelTypes = createEnum([ * * TextChannel * * NewsChannel * * ThreadChannel - * @typedef {DMChannel|TextChannel|NewsChannel|ThreadChannel} TextBasedChannels + * * VoiceChannel + * @typedef {DMChannel|TextChannel|NewsChannel|ThreadChannel|VoiceChannel} TextBasedChannels */ /** @@ -576,6 +577,7 @@ exports.ChannelTypes = createEnum([ * * GUILD_NEWS_THREAD * * GUILD_PUBLIC_THREAD * * GUILD_PRIVATE_THREAD + * * GUILD_VOICE * @typedef {string} TextBasedChannelTypes */ exports.TextBasedChannelTypes = [ @@ -585,6 +587,7 @@ exports.TextBasedChannelTypes = [ 'GUILD_NEWS_THREAD', 'GUILD_PUBLIC_THREAD', 'GUILD_PRIVATE_THREAD', + 'GUILD_VOICE', ]; /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 0a67b97455a7..ab83d04a95f9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -433,26 +433,23 @@ export class BaseGuildEmoji extends Emoji { export class BaseGuildTextChannel extends TextBasedChannelMixin(GuildChannel) { protected constructor(guild: Guild, data?: RawGuildChannelData, client?: Client, immediatePatch?: boolean); public defaultAutoArchiveDuration?: ThreadAutoArchiveDuration; - public messages: MessageManager; + public rateLimitPerUser: number | null; public nsfw: boolean; public threads: ThreadManager; public topic: string | null; public createInvite(options?: CreateInviteOptions): Promise; - public createWebhook(name: string, options?: ChannelWebhookCreateOptions): Promise; public fetchInvites(cache?: boolean): Promise>; public setDefaultAutoArchiveDuration( defaultAutoArchiveDuration: ThreadAutoArchiveDuration | 'MAX', reason?: string, ): Promise; - public setNSFW(nsfw?: boolean, reason?: string): Promise; public setTopic(topic: string | null, reason?: string): Promise; public setType(type: Pick, reason?: string): Promise; public setType(type: Pick, reason?: string): Promise; - public fetchWebhooks(): Promise>; } export class BaseGuildVoiceChannel extends GuildChannel { - protected constructor(guild: Guild, data?: RawGuildChannelData); + public constructor(guild: Guild, data?: RawGuildChannelData); public readonly members: Collection; public readonly full: boolean; public readonly joinable: boolean; @@ -893,9 +890,14 @@ export class DiscordAPIError extends Error { public requestData: HTTPErrorData; } -export class DMChannel extends TextBasedChannelMixin(Channel, ['bulkDelete']) { +export class DMChannel extends TextBasedChannelMixin(Channel, [ + 'bulkDelete', + 'fetchWebhooks', + 'createWebhook', + 'setRateLimitPerUser', + 'setNSFW', +]) { private constructor(client: Client, data?: RawDMChannelData); - public messages: MessageManager; public recipient: User; public type: 'DM'; public fetch(force?: boolean): Promise; @@ -2430,7 +2432,6 @@ export class TextChannel extends BaseGuildTextChannel { public rateLimitPerUser: number; public threads: ThreadManager; public type: 'GUILD_TEXT'; - public setRateLimitPerUser(rateLimitPerUser: number, reason?: string): Promise; } export class TextInputComponent extends BaseMessageComponent { @@ -2455,7 +2456,7 @@ export class TextInputComponent extends BaseMessageComponent { public static resolveStyle(style: TextInputStyleResolvable): TextInputStyle; } -export class ThreadChannel extends TextBasedChannelMixin(Channel) { +export class ThreadChannel extends TextBasedChannelMixin(Channel, ['fetchWebhooks', 'createWebhook', 'setNSFW']) { private constructor(guild: Guild, data?: RawThreadChannelData, client?: Client, fromInteraction?: boolean); public archived: boolean | null; public readonly archivedAt: Date | null; @@ -2477,7 +2478,6 @@ export class ThreadChannel extends TextBasedChannelMixin(Channel) { public readonly sendable: boolean; public memberCount: number | null; public messageCount: number | null; - public messages: MessageManager; public members: ThreadMemberManager; public name: string; public ownerId: Snowflake | null; @@ -2510,7 +2510,6 @@ export class ThreadChannel extends TextBasedChannelMixin(Channel) { public setInvitable(invitable?: boolean, reason?: string): Promise; public setLocked(locked?: boolean, reason?: string): Promise; public setName(name: string, reason?: string): Promise; - public setRateLimitPerUser(rateLimitPerUser: number, reason?: string): Promise; } export class ThreadMember extends Base { @@ -2655,12 +2654,13 @@ export class Formatters extends null { public static userMention: typeof userMention; } -export class VoiceChannel extends BaseGuildVoiceChannel { +export class VoiceChannel extends TextBasedChannelMixin(BaseGuildVoiceChannel, ['lastPinTimestamp', 'lastPinAt']) { public videoQualityMode: VideoQualityMode | null; /** @deprecated Use manageable instead */ public readonly editable: boolean; public readonly speakable: boolean; public type: 'GUILD_VOICE'; + public rateLimitPerUser: number | null; public setBitrate(bitrate: number, reason?: string): Promise; public setUserLimit(userLimit: number, reason?: string): Promise; public setVideoQualityMode(videoQualityMode: VideoQualityMode | number, reason?: string): Promise; @@ -3507,6 +3507,7 @@ export interface TextBasedChannelFields extends PartialTextBasedChannelFields { readonly lastMessage: Message | null; lastPinTimestamp: number | null; readonly lastPinAt: Date | null; + messages: MessageManager; awaitMessageComponent( options?: AwaitMessageCollectorOptionsParams, ): Promise; @@ -3519,6 +3520,10 @@ export interface TextBasedChannelFields extends PartialTextBasedChannelFields { options?: MessageChannelCollectorOptionsParams, ): InteractionCollector; createMessageCollector(options?: MessageCollectorOptions): MessageCollector; + createWebhook(name: string, options?: ChannelWebhookCreateOptions): Promise; + setRateLimitPerUser(rateLimitPerUser: number, reason?: string): Promise; + setNSFW(nsfw?: boolean, reason?: string): Promise; + fetchWebhooks(): Promise>; sendTyping(): Promise; } @@ -4199,7 +4204,7 @@ export interface ClientEvents extends BaseClientEvents { typingStart: [typing: Typing]; userUpdate: [oldUser: User | PartialUser, newUser: User]; voiceStateUpdate: [oldState: VoiceState, newState: VoiceState]; - webhookUpdate: [channel: TextChannel | NewsChannel]; + webhookUpdate: [channel: TextChannel | NewsChannel | VoiceChannel]; /** @deprecated Use interactionCreate instead */ interaction: [interaction: Interaction]; interactionCreate: [interaction: Interaction]; diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index 1bdb8c5433df..519f650efd32 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -770,9 +770,10 @@ declare const guildMember: GuildMember; // Test whether the structures implement send expectType(dmChannel.send); -expectType(threadChannel); -expectType(newsChannel); -expectType(textChannel); +expectType(threadChannel.send); +expectType(newsChannel.send); +expectType(textChannel.send); +expectType(voiceChannel.send); expectAssignable(user); expectAssignable(guildMember); @@ -780,6 +781,7 @@ expectType(dmChannel.lastMessage); expectType(threadChannel.lastMessage); expectType(newsChannel.lastMessage); expectType(textChannel.lastMessage); +expectType(voiceChannel.lastMessage); expectDeprecated(storeChannel.clone()); expectDeprecated(categoryChannel.createChannel('Store', { type: 'GUILD_STORE' })); @@ -1292,10 +1294,16 @@ declare const GuildBasedChannel: GuildBasedChannel; declare const NonThreadGuildBasedChannel: NonThreadGuildBasedChannel; declare const GuildTextBasedChannel: GuildTextBasedChannel; -expectType(TextBasedChannel); -expectType<'DM' | 'GUILD_NEWS' | 'GUILD_TEXT' | 'GUILD_PUBLIC_THREAD' | 'GUILD_PRIVATE_THREAD' | 'GUILD_NEWS_THREAD'>( - TextBasedChannelTypes, -); +expectType(TextBasedChannel); +expectType< + | 'DM' + | 'GUILD_NEWS' + | 'GUILD_TEXT' + | 'GUILD_PUBLIC_THREAD' + | 'GUILD_PRIVATE_THREAD' + | 'GUILD_NEWS_THREAD' + | 'GUILD_VOICE' +>(TextBasedChannelTypes); expectType(VoiceBasedChannel); expectType( GuildBasedChannel, @@ -1303,4 +1311,4 @@ expectType( NonThreadGuildBasedChannel, ); -expectType(GuildTextBasedChannel); +expectType(GuildTextBasedChannel);