diff --git a/packages/discord.js/src/managers/ChannelManager.js b/packages/discord.js/src/managers/ChannelManager.js index 0126d914467d..e027563402fb 100644 --- a/packages/discord.js/src/managers/ChannelManager.js +++ b/packages/discord.js/src/managers/ChannelManager.js @@ -1,13 +1,17 @@ 'use strict'; const process = require('node:process'); +const { lazy } = require('@discordjs/util'); const { Routes } = require('discord-api-types/v10'); const CachedManager = require('./CachedManager'); const { BaseChannel } = require('../structures/BaseChannel'); +const MessagePayload = require('../structures/MessagePayload'); const { createChannel } = require('../util/Channels'); const { ThreadChannelTypes } = require('../util/Constants'); const Events = require('../util/Events'); +const getMessage = lazy(() => require('../structures/Message').Message); + let cacheWarningEmitted = false; /** @@ -123,6 +127,52 @@ class ChannelManager extends CachedManager { const data = await this.client.rest.get(Routes.channel(id)); return this._add(data, null, { cache, allowUnknownGuild }); } + + /** + * Creates a message in a channel. + * @param {TextChannelResolvable} channel The channel to send the message to + * @param {string|MessagePayload|MessageCreateOptions} options The options to provide + * @returns {Promise} + * @example + * // Send a basic message + * client.channels.createMessage(channel, 'hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + * @example + * // Send a remote file + * client.channels.createMessage(channel, { + * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send a local file + * client.channels.createMessage(channel, { + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg', + * description: 'A description of the file' + * }] + * }) + * .then(console.log) + * .catch(console.error); + */ + async createMessage(channel, options) { + let messagePayload; + + if (options instanceof MessagePayload) { + messagePayload = options.resolveBody(); + } else { + messagePayload = MessagePayload.create(this, options).resolveBody(); + } + + const resolvedChannelId = this.resolveId(channel); + const resolvedChannel = this.resolve(channel); + const { body, files } = await messagePayload.resolveFiles(); + const data = await this.client.rest.post(Routes.channelMessages(resolvedChannelId), { body, files }); + + return resolvedChannel?.messages._add(data) ?? new (getMessage())(this.client, data); + } } module.exports = ChannelManager; diff --git a/packages/discord.js/src/structures/GuildMember.js b/packages/discord.js/src/structures/GuildMember.js index b1f1b5283460..8ff334674ff8 100644 --- a/packages/discord.js/src/structures/GuildMember.js +++ b/packages/discord.js/src/structures/GuildMember.js @@ -3,7 +3,6 @@ const { PermissionFlagsBits } = require('discord-api-types/v10'); const Base = require('./Base'); const VoiceState = require('./VoiceState'); -const TextBasedChannel = require('./interfaces/TextBasedChannel'); const { DiscordjsError, ErrorCodes } = require('../errors'); const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager'); const { GuildMemberFlagsBitField } = require('../util/GuildMemberFlagsBitField'); @@ -11,7 +10,6 @@ const PermissionsBitField = require('../util/PermissionsBitField'); /** * Represents a member of a guild on Discord. - * @implements {TextBasedChannel} * @extends {Base} */ class GuildMember extends Base { @@ -478,6 +476,22 @@ class GuildMember extends Base { return this.guild.members.fetch({ user: this.id, cache: true, force }); } + /** + * Sends a message to this user. + * @param {string|MessagePayload|MessageCreateOptions} options The options to provide + * @returns {Promise} + * @example + * // Send a direct message + * guildMember.send('Hello!') + * .then(message => console.log(`Sent message: ${message.content} to ${guildMember.displayName}`)) + * .catch(console.error); + */ + async send(options) { + const dmChannel = await this.createDM(); + + return this.client.channels.createMessage(dmChannel, options); + } + /** * Whether this guild member equals another guild member. It compares all properties, so for most * comparison it is advisable to just compare `member.id === member2.id` as it is significantly faster @@ -529,20 +543,4 @@ class GuildMember extends Base { } } -/** - * Sends a message to this user. - * @method send - * @memberof GuildMember - * @instance - * @param {string|MessagePayload|MessageCreateOptions} options The options to provide - * @returns {Promise} - * @example - * // Send a direct message - * guildMember.send('Hello!') - * .then(message => console.log(`Sent message: ${message.content} to ${guildMember.displayName}`)) - * .catch(console.error); - */ - -TextBasedChannel.applyToClass(GuildMember); - exports.GuildMember = GuildMember; diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index f052113e2bda..efb44ac8027f 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -8,6 +8,7 @@ const { ChannelType, MessageType, MessageFlags, + MessageReferenceType, PermissionFlagsBits, } = require('discord-api-types/v10'); const Attachment = require('./Attachment'); @@ -913,20 +914,22 @@ class Message extends Base { * .catch(console.error); */ reply(options) { - if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached)); let data; if (options instanceof MessagePayload) { data = options; } else { data = MessagePayload.create(this, options, { - reply: { - messageReference: this, + messageReference: { + messageId: this.id, + channelId: this.channelId, + guildId: this.guildId, + type: MessageReferenceType.Default, failIfNotExists: options?.failIfNotExists ?? this.client.options.failIfNotExists, }, }); } - return this.channel.send(data); + return this.client.channels.createMessage(this.channelId, data); } /** diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js index 5420d250d928..22854a008d10 100644 --- a/packages/discord.js/src/structures/MessagePayload.js +++ b/packages/discord.js/src/structures/MessagePayload.js @@ -168,13 +168,16 @@ class MessagePayload { } let message_reference; - if (typeof this.options.reply === 'object') { - const reference = this.options.reply.messageReference; - const message_id = this.isMessage ? (reference.id ?? reference) : this.target.messages.resolveId(reference); - if (message_id) { + if (this.options.messageReference) { + const reference = this.options.messageReference; + + if (reference.messageId) { message_reference = { - message_id, - fail_if_not_exists: this.options.reply.failIfNotExists ?? this.target.client.options.failIfNotExists, + message_id: reference.messageId, + channel_id: reference.channelId, + guild_id: reference.guildId, + type: reference.type, + fail_if_not_exists: reference.failIfNotExists ?? this.target.client.options.failIfNotExists, }; } } @@ -292,7 +295,7 @@ module.exports = MessagePayload; /** * A target for a message. - * @typedef {TextBasedChannels|User|GuildMember|Webhook|WebhookClient|BaseInteraction|InteractionWebhook| + * @typedef {TextBasedChannels|ChannelManager|Webhook|WebhookClient|BaseInteraction|InteractionWebhook| * Message|MessageManager} MessageTarget */ diff --git a/packages/discord.js/src/structures/User.js b/packages/discord.js/src/structures/User.js index 6025410c30f1..7ea59929ac57 100644 --- a/packages/discord.js/src/structures/User.js +++ b/packages/discord.js/src/structures/User.js @@ -4,12 +4,10 @@ const { userMention } = require('@discordjs/formatters'); const { calculateUserDefaultAvatarIndex } = require('@discordjs/rest'); const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); -const TextBasedChannel = require('./interfaces/TextBasedChannel'); const UserFlagsBitField = require('../util/UserFlagsBitField'); /** * Represents a user on Discord. - * @implements {TextBasedChannel} * @extends {Base} */ class User extends Base { @@ -277,6 +275,22 @@ class User extends Base { return this.client.users.deleteDM(this.id); } + /** + * Sends a message to this user. + * @param {string|MessagePayload|MessageCreateOptions} options The options to provide + * @returns {Promise} + * @example + * // Send a direct message + * user.send('Hello!') + * .then(message => console.log(`Sent message: ${message.content} to ${user.tag}`)) + * .catch(console.error); + */ + async send(options) { + const dmChannel = await this.createDM(); + + return this.client.channels.createMessage(dmChannel, options); + } + /** * Checks if the user is equal to another. * It compares id, username, discriminator, avatar, banner, accent color, and bot flags. @@ -361,20 +375,4 @@ class User extends Base { } } -/** - * Sends a message to this user. - * @method send - * @memberof User - * @instance - * @param {string|MessagePayload|MessageCreateOptions} options The options to provide - * @returns {Promise} - * @example - * // Send a direct message - * user.send('Hello!') - * .then(message => console.log(`Sent message: ${message.content} to ${user.tag}`)) - * .catch(console.error); - */ - -TextBasedChannel.applyToClass(User); - module.exports = User; diff --git a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js index d2e408580253..8007fb4f6c44 100644 --- a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js +++ b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -7,7 +7,6 @@ const { DiscordjsTypeError, DiscordjsError, ErrorCodes } = require('../../errors const { MaxBulkDeletableMessageAge } = require('../../util/Constants'); const InteractionCollector = require('../InteractionCollector'); const MessageCollector = require('../MessageCollector'); -const MessagePayload = require('../MessagePayload'); /** * Interface for classes that have text-channel-like features. @@ -113,7 +112,7 @@ class TextBasedChannel { /** * The options for sending a message. * @typedef {BaseMessageCreateOptions} MessageCreateOptions - * @property {ReplyOptions} [reply] The options for replying to a message + * @property {MessageReference|MessageResolvable} [messageReference] The options for a reference to a message */ /** @@ -161,27 +160,8 @@ class TextBasedChannel { * .then(console.log) * .catch(console.error); */ - async send(options) { - const User = require('../User'); - const { GuildMember } = require('../GuildMember'); - - if (this instanceof User || this instanceof GuildMember) { - const dm = await this.createDM(); - return dm.send(options); - } - - let messagePayload; - - if (options instanceof MessagePayload) { - messagePayload = options.resolveBody(); - } else { - messagePayload = MessagePayload.create(this, options).resolveBody(); - } - - const { body, files } = await messagePayload.resolveFiles(); - const d = await this.client.rest.post(Routes.channelMessages(this.id), { body, files }); - - return this.messages.cache.get(d.id) ?? this.messages._add(d); + send(options) { + return this.client.channels.createMessage(this, options); } /** @@ -416,6 +396,7 @@ class TextBasedChannel { 'setNSFW', ); } + for (const prop of props) { if (ignore.includes(prop)) continue; Object.defineProperty( diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index ba0da0eea83c..3ff62d466556 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -3810,7 +3810,6 @@ export const Constants: { SweeperKeys: SweeperKey[]; NonSystemMessageTypes: NonSystemMessageType[]; TextBasedChannelTypes: TextBasedChannelTypes[]; - SendableChannels: SendableChannelTypes[]; GuildTextBasedChannelTypes: GuildTextBasedChannelTypes[]; ThreadChannelTypes: ThreadChannelType[]; VoiceBasedChannelTypes: VoiceBasedChannelTypes[]; @@ -4129,6 +4128,10 @@ export class CategoryChannelChildManager extends DataManager { private constructor(client: Client, iterable: Iterable); + public createMessage( + channel: Omit, + options: string | MessagePayload | MessageCreateOptions, + ): Promise; public fetch(id: Snowflake, options?: FetchChannelOptions): Promise; } @@ -6394,7 +6397,7 @@ export interface MessageCreateOptions extends BaseMessageOptionsWithPoll { tts?: boolean; nonce?: string | number; enforceNonce?: boolean; - reply?: ReplyOptions; + messageReference?: MessageReference & { failIfNotExists?: boolean }; stickers?: readonly StickerResolvable[]; flags?: BitFieldResolvable< Extract, @@ -6497,15 +6500,14 @@ export interface TextInputComponentData extends BaseComponentData { } export type MessageTarget = + | ChannelManager | Interaction | InteractionWebhook + | Message + | MessageManager | TextBasedChannel - | User - | GuildMember | Webhook - | WebhookClient - | Message - | MessageManager; + | WebhookClient; export interface MultipleShardRespawnOptions { shardDelay?: number; @@ -6640,12 +6642,7 @@ export interface ReactionCollectorOptions extends CollectorOptions<[MessageReact maxUsers?: number; } -export interface ReplyOptions { - messageReference: MessageResolvable; - failIfNotExists?: boolean; -} - -export interface MessageReplyOptions extends Omit { +export interface MessageReplyOptions extends Omit { failIfNotExists?: boolean; } @@ -6814,26 +6811,26 @@ export type Channel = export type TextBasedChannel = Exclude, ForumChannel | MediaChannel>; -export type SendableChannels = Extract any }>; - export type TextBasedChannels = TextBasedChannel; export type TextBasedChannelTypes = TextBasedChannel['type']; export type GuildTextBasedChannelTypes = Exclude; -export type SendableChannelTypes = SendableChannels['type']; - export type VoiceBasedChannel = Extract; export type GuildBasedChannel = Extract; +export type SendableChannels = Extract any }>; + export type CategoryChildChannel = Exclude, CategoryChannel>; export type NonThreadGuildBasedChannel = Exclude; export type GuildTextBasedChannel = Extract; +export type SendableChannelTypes = SendableChannels['type']; + export type TextChannelResolvable = Snowflake | TextChannel; export type TextBasedChannelResolvable = Snowflake | TextBasedChannel; @@ -6924,7 +6921,8 @@ export interface WebhookFetchMessageOptions { threadId?: Snowflake; } -export interface WebhookMessageCreateOptions extends Omit { +export interface WebhookMessageCreateOptions + extends Omit { username?: string; avatarURL?: string; threadId?: Snowflake; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index b587e3bf25bd..df4fb6e164e6 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -423,12 +423,20 @@ client.on('messageCreate', async message => { assertIsMessage(channel.send({})); assertIsMessage(channel.send({ embeds: [] })); + assertIsMessage(client.channels.createMessage(channel, 'string')); + assertIsMessage(client.channels.createMessage(channel, {})); + assertIsMessage(client.channels.createMessage(channel, { embeds: [] })); + const attachment = new AttachmentBuilder('file.png'); const embed = new EmbedBuilder(); assertIsMessage(channel.send({ files: [attachment] })); assertIsMessage(channel.send({ embeds: [embed] })); assertIsMessage(channel.send({ embeds: [embed], files: [attachment] })); + assertIsMessage(client.channels.createMessage(channel, { files: [attachment] })); + assertIsMessage(client.channels.createMessage(channel, { embeds: [embed] })); + assertIsMessage(client.channels.createMessage(channel, { embeds: [embed], files: [attachment] })); + if (message.inGuild()) { expectAssignable>(message); const component = await message.awaitMessageComponent({ componentType: ComponentType.Button }); @@ -458,8 +466,13 @@ client.on('messageCreate', async message => { // @ts-expect-error channel.send(); // @ts-expect-error + client.channels.createMessage(); + // @ts-expect-error channel.send({ another: 'property' }); - + // @ts-expect-error + client.channels.createMessage({ another: 'property' }); + // @ts-expect-error + client.channels.createMessage('string'); // Check collector creations. // Verify that buttons interactions are inferred. @@ -620,7 +633,7 @@ client.on('messageCreate', async message => { const embedData = { description: 'test', color: 0xff0000 }; - channel.send({ + client.channels.createMessage(channel, { components: [row, rawButtonsRow, buttonsRow, rawStringSelectMenuRow, stringSelectRow], embeds: [embed, embedData], }); @@ -1265,7 +1278,7 @@ client.on('guildCreate', async g => { ], }); - channel.send({ components: [row, row2] }); + client.channels.createMessage(channel, { components: [row, row2] }); } channel.setName('foo').then(updatedChannel => { @@ -2540,7 +2553,7 @@ declare const sku: SKU; }); } -await textChannel.send({ +await client.channels.createMessage('123', { poll: { question: { text: 'Question',