diff --git a/packages/discord.js/src/client/actions/WebhooksUpdate.js b/packages/discord.js/src/client/actions/WebhooksUpdate.js index b362b3b4d..e4c11daed 100644 --- a/packages/discord.js/src/client/actions/WebhooksUpdate.js +++ b/packages/discord.js/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.WebhooksUpdate, channel); } diff --git a/packages/discord.js/src/structures/BaseGuildTextChannel.js b/packages/discord.js/src/structures/BaseGuildTextChannel.js index 562cb7545..a7f274b97 100644 --- a/packages/discord.js/src/structures/BaseGuildTextChannel.js +++ b/packages/discord.js/src/structures/BaseGuildTextChannel.js @@ -109,44 +109,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 @@ -222,6 +184,8 @@ class BaseGuildTextChannel extends GuildChannel { createMessageComponentCollector() {} awaitMessageComponent() {} bulkDelete() {} + fetchWebhooks() {} + createWebhook() {} } TextBasedChannel.applyToClass(BaseGuildTextChannel, true); diff --git a/packages/discord.js/src/structures/DMChannel.js b/packages/discord.js/src/structures/DMChannel.js index 8891fa676..b7b35290f 100644 --- a/packages/discord.js/src/structures/DMChannel.js +++ b/packages/discord.js/src/structures/DMChannel.js @@ -112,8 +112,10 @@ class DMChannel extends Channel { createMessageComponentCollector() {} awaitMessageComponent() {} // Doesn't work on DM channels; bulkDelete() {} + // Doesn't work on DM channels; fetchWebhooks() {} + // Doesn't work on DM channels; createWebhook() {} } -TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']); +TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete', 'fetchWebhooks', 'createWebhook']); module.exports = DMChannel; diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index e9c484f97..f2fffb948 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -351,7 +351,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/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js index ef448e95b..2bbeb764c 100644 --- a/packages/discord.js/src/structures/MessagePayload.js +++ b/packages/discord.js/src/structures/MessagePayload.js @@ -276,7 +276,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/packages/discord.js/src/structures/VoiceChannel.js b/packages/discord.js/src/structures/VoiceChannel.js index c1f12a80e..e27bdf92e 100644 --- a/packages/discord.js/src/structures/VoiceChannel.js +++ b/packages/discord.js/src/structures/VoiceChannel.js @@ -2,12 +2,26 @@ const { PermissionFlagsBits } = require('discord-api-types/v10'); const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const MessageManager = require('../managers/MessageManager'); /** * Represents a guild voice channel on Discord. * @extends {BaseGuildVoiceChannel} */ 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); @@ -20,6 +34,18 @@ 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); + } } /** @@ -90,6 +116,19 @@ 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() {} + /** * Sets the RTC region of the channel. * @name VoiceChannel#setRTCRegion @@ -105,4 +144,6 @@ class VoiceChannel extends BaseGuildVoiceChannel { */ } +TextBasedChannel.applyToClass(VoiceChannel, true, ['lastPinAt']); + module.exports = VoiceChannel; diff --git a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js index d5fd397f9..42cf40bf6 100644 --- a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js +++ b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -329,6 +329,44 @@ 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 {@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); + } + static applyToClass(structure, full = false, ignore = []) { const props = ['send']; if (full) { @@ -341,6 +379,8 @@ class TextBasedChannel { 'awaitMessages', 'createMessageComponentCollector', 'awaitMessageComponent', + 'fetchWebhooks', + 'createWebhook', ); } for (const prop of props) { diff --git a/packages/discord.js/src/util/Constants.js b/packages/discord.js/src/util/Constants.js index 99abc72b3..0b2fbd536 100644 --- a/packages/discord.js/src/util/Constants.js +++ b/packages/discord.js/src/util/Constants.js @@ -62,7 +62,8 @@ exports.NonSystemMessageTypes = [ * * TextChannel * * NewsChannel * * ThreadChannel - * @typedef {DMChannel|TextChannel|NewsChannel|ThreadChannel} TextBasedChannels + * * VoiceChannel + * @typedef {DMChannel|TextChannel|NewsChannel|ThreadChannel|VoiceChannel} TextBasedChannels */ /** @@ -80,6 +81,7 @@ exports.NonSystemMessageTypes = [ * * {@link ChannelType.GuildNewsThread} * * {@link ChannelType.GuildPublicThread} * * {@link ChannelType.GuildPrivateThread} + * * {@link ChannelType.GuildVoice} * @typedef {ChannelType} TextBasedChannelTypes */ exports.TextBasedChannelTypes = [ @@ -89,6 +91,7 @@ exports.TextBasedChannelTypes = [ ChannelType.GuildNewsThread, ChannelType.GuildPublicThread, ChannelType.GuildPrivateThread, + ChannelType.GuildVoice, ]; /** diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 1eeab8e58..97aec6b57 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -494,12 +494,10 @@ 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 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', @@ -509,11 +507,10 @@ export class BaseGuildTextChannel extends TextBasedChannelMixin(GuildChannel) { 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 get members(): Collection; public get full(): boolean; public get joinable(): boolean; @@ -1069,9 +1066,8 @@ export class EnumResolvers extends null { public static resolveVideoQualityMode(key: VideoQualityModeEnumResolvable | VideoQualityMode): VideoQualityMode; } -export class DMChannel extends TextBasedChannelMixin(Channel, ['bulkDelete']) { +export class DMChannel extends TextBasedChannelMixin(Channel, ['bulkDelete', 'fetchWebhooks', 'createWebhook']) { private constructor(client: Client, data?: RawDMChannelData); - public messages: MessageManager; public recipientId: Snowflake; public get recipient(): User | null; public type: ChannelType.DM; @@ -2409,7 +2405,7 @@ export class TextChannel extends BaseGuildTextChannel { public setRateLimitPerUser(rateLimitPerUser: number, reason?: string): Promise; } -export class ThreadChannel extends TextBasedChannelMixin(Channel) { +export class ThreadChannel extends TextBasedChannelMixin(Channel, ['fetchWebhooks', 'createWebhook']) { private constructor(guild: Guild, data?: RawThreadChannelData, client?: Client, fromInteraction?: boolean); public archived: boolean | null; public get archivedAt(): Date | null; @@ -2431,7 +2427,6 @@ export class ThreadChannel extends TextBasedChannelMixin(Channel) { public get sendable(): boolean; public memberCount: number | null; public messageCount: number | null; - public messages: MessageManager; public members: ThreadMemberManager; public name: string; public ownerId: Snowflake | null; @@ -2624,7 +2619,7 @@ export type ComponentData = | ModalActionRowComponentData | ActionRowData; -export class VoiceChannel extends BaseGuildVoiceChannel { +export class VoiceChannel extends TextBasedChannelMixin(BaseGuildVoiceChannel, ['lastPinTimestamp', 'lastPinAt']) { public videoQualityMode: VideoQualityMode | null; public get speakable(): boolean; public type: ChannelType.GuildVoice; @@ -3374,6 +3369,7 @@ export interface TextBasedChannelFields extends PartialTextBasedChannelFields { get lastMessage(): Message | null; lastPinTimestamp: number | null; get lastPinAt(): Date | null; + messages: MessageManager; awaitMessageComponent( options?: AwaitMessageCollectorOptionsParams, ): Promise; @@ -3386,6 +3382,8 @@ export interface TextBasedChannelFields extends PartialTextBasedChannelFields { options?: MessageChannelCollectorOptionsParams, ): InteractionCollector; createMessageCollector(options?: MessageCollectorOptions): MessageCollector; + createWebhook(name: string, options?: ChannelWebhookCreateOptions): Promise; + fetchWebhooks(): Promise>; sendTyping(): Promise; } @@ -3836,7 +3834,7 @@ export interface ClientEvents { typingStart: [typing: Typing]; userUpdate: [oldUser: User | PartialUser, newUser: User]; voiceStateUpdate: [oldState: VoiceState, newState: VoiceState]; - webhookUpdate: [channel: TextChannel | NewsChannel]; + webhookUpdate: [channel: TextChannel | NewsChannel | VoiceChannel]; interactionCreate: [interaction: Interaction]; shardDisconnect: [closeEvent: CloseEvent, shardId: number]; shardError: [error: Error, shardId: number]; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index b7b31c99f..0d20f30fe 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -937,9 +937,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); @@ -947,6 +948,7 @@ expectType(dmChannel.lastMessage); expectType(threadChannel.lastMessage); expectType(newsChannel.lastMessage); expectType(textChannel.lastMessage); +expectType(voiceChannel.lastMessage); notPropertyOf(user, 'lastMessage'); notPropertyOf(user, 'lastMessageId'); @@ -1430,14 +1432,16 @@ declare const GuildBasedChannel: GuildBasedChannel; declare const NonThreadGuildBasedChannel: NonThreadGuildBasedChannel; declare const GuildTextBasedChannel: GuildTextBasedChannel; -expectType(TextBasedChannel); -expectType(TextBasedChannelTypes); +expectType(TextBasedChannel); +expectType( + TextBasedChannelTypes, +); expectType(VoiceBasedChannel); expectType( GuildBasedChannel, ); expectType(NonThreadGuildBasedChannel); -expectType(GuildTextBasedChannel); +expectType(GuildTextBasedChannel); const button = new ButtonBuilder({ label: 'test',