From a320a45cd9fb1dea71da7aac205d01c1ff5d7827 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?T=C3=86MB=C3=98?= <69138346+TAEMBO@users.noreply.github.com>
Date: Thu, 5 Dec 2024 13:35:58 -0800
Subject: [PATCH] feat: add ChannelManager#createMessage()

---
 .../discord.js/src/managers/ChannelManager.js | 50 +++++++++++++++++++
 .../discord.js/src/managers/MessageManager.js | 31 +-----------
 .../discord.js/src/managers/UserManager.js    | 10 ----
 .../discord.js/src/structures/GuildMember.js  | 34 ++++++-------
 packages/discord.js/src/structures/Message.js | 19 +------
 .../src/structures/MessagePayload.js          | 13 ++---
 packages/discord.js/src/structures/User.js    | 34 ++++++-------
 .../structures/interfaces/TextBasedChannel.js | 25 ++--------
 packages/discord.js/typings/index.d.ts        | 24 ++++-----
 packages/discord.js/typings/index.test-d.ts   | 21 ++++++--
 10 files changed, 123 insertions(+), 138 deletions(-)

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<Message>}
+   * @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/managers/MessageManager.js b/packages/discord.js/src/managers/MessageManager.js
index a2c44c446352..dcddf2857383 100644
--- a/packages/discord.js/src/managers/MessageManager.js
+++ b/packages/discord.js/src/managers/MessageManager.js
@@ -2,9 +2,9 @@
 
 const { Collection } = require('@discordjs/collection');
 const { makeURLSearchParams } = require('@discordjs/rest');
-const { MessageReferenceType, Routes } = require('discord-api-types/v10');
+const { Routes } = require('discord-api-types/v10');
 const CachedManager = require('./CachedManager');
-const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors');
+const { DiscordjsTypeError, ErrorCodes } = require('../errors');
 const { Message } = require('../structures/Message');
 const MessagePayload = require('../structures/MessagePayload');
 const { MakeCacheOverrideSymbol } = require('../util/Symbols');
@@ -209,33 +209,6 @@ class MessageManager extends CachedManager {
     return this.cache.get(data.id) ?? this._add(data);
   }
 
-  /**
-   * Forwards a message to this manager's channel.
-   * @param {Message|MessageReference} reference The message to forward
-   * @returns {Promise<Message>}
-   */
-  async forward(reference) {
-    if (!reference) throw new DiscordjsError(ErrorCodes.MessageReferenceMissing);
-    const message_id = this.resolveId(reference.messageId);
-    if (!message_id) throw new DiscordjsError(ErrorCodes.MessageReferenceMissing);
-    const channel_id = this.client.channels.resolveId(reference.channelId);
-    if (!channel_id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'channel', 'ChannelResolvable');
-    const guild_id = this.client.guilds.resolveId(reference.guildId);
-
-    const data = await this.client.rest.post(Routes.channelMessages(this.channel.id), {
-      body: {
-        message_reference: {
-          message_id,
-          channel_id,
-          guild_id,
-          type: MessageReferenceType.Forward,
-        },
-      },
-    });
-
-    return this.cache.get(data.id) ?? this._add(data);
-  }
-
   /**
    * Pins a message to the channel's pinned messages, even if it's not cached.
    * @param {MessageResolvable} message The message to pin
diff --git a/packages/discord.js/src/managers/UserManager.js b/packages/discord.js/src/managers/UserManager.js
index b0026da2c69a..f9d12ad2649c 100644
--- a/packages/discord.js/src/managers/UserManager.js
+++ b/packages/discord.js/src/managers/UserManager.js
@@ -95,16 +95,6 @@ class UserManager extends CachedManager {
     return this._add(data, cache);
   }
 
-  /**
-   * Sends a message to a user.
-   * @param {UserResolvable} user The UserResolvable to identify
-   * @param {string|MessagePayload|MessageCreateOptions} options The options to provide
-   * @returns {Promise<Message>}
-   */
-  async send(user, options) {
-    return (await this.createDM(user)).send(options);
-  }
-
   /**
    * Resolves a {@link UserResolvable} to a {@link User} object.
    * @param {UserResolvable} user The UserResolvable to identify
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<Message>}
+   * @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<Message>}
- * @example
- * // Send a direct message
- * guildMember.send('Hello!')
- *   .then(message => console.log(`Sent message: ${message.content} to ${guildMember.displayName}`))
- *   .catch(console.error);
- */
-
-TextBasedChannel.applyToClass(GuildMember);
-
 exports.GuildMember = GuildMember;
diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js
index 63906512884b..efb44ac8027f 100644
--- a/packages/discord.js/src/structures/Message.js
+++ b/packages/discord.js/src/structures/Message.js
@@ -21,7 +21,7 @@ const MessagePayload = require('./MessagePayload');
 const { Poll } = require('./Poll.js');
 const ReactionCollector = require('./ReactionCollector');
 const { Sticker } = require('./Sticker');
-const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors');
+const { DiscordjsError, ErrorCodes } = require('../errors');
 const ReactionManager = require('../managers/ReactionManager');
 const { createComponent } = require('../util/Components');
 const { NonSystemMessageTypes, MaxBulkDeletableMessageAge, UndeletableMessageTypes } = require('../util/Constants');
@@ -768,20 +768,6 @@ class Message extends Base {
     return message;
   }
 
-  /**
-   * Forwards this message.
-   * @param {ChannelResolvable} channel The channel to forward this message to
-   * @returns {Promise<Message>}
-   */
-  async forward(channel) {
-    const resolvedChannel = this.client.channels.resolve(channel);
-
-    if (!resolvedChannel) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'channel', 'ChannelResolvable');
-
-    const message = await resolvedChannel.messages.forward(this);
-    return message;
-  }
-
   /**
    * Whether the message is crosspostable by the client user
    * @type {boolean}
@@ -928,7 +914,6 @@ 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) {
@@ -944,7 +929,7 @@ class Message extends Base {
         },
       });
     }
-    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 773e6d93078c..22854a008d10 100644
--- a/packages/discord.js/src/structures/MessagePayload.js
+++ b/packages/discord.js/src/structures/MessagePayload.js
@@ -170,15 +170,12 @@ class MessagePayload {
     let message_reference;
     if (this.options.messageReference) {
       const reference = this.options.messageReference;
-      const message_id = this.target.messages.resolveId(reference.messageId);
-      const channel_id = this.target.client.channels.resolveId(reference.channelId);
-      const guild_id = this.target.client.guilds.resolveId(reference.guildId);
 
-      if (message_id) {
+      if (reference.messageId) {
         message_reference = {
-          message_id,
-          channel_id,
-          guild_id,
+          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,
         };
@@ -298,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<Message>}
+   * @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<Message>}
- * @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 b039197041f7..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.
@@ -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 0056392437b9..922155e0a767 100644
--- a/packages/discord.js/typings/index.d.ts
+++ b/packages/discord.js/typings/index.d.ts
@@ -2242,7 +2242,6 @@ export class Message<InGuild extends boolean = boolean> extends Base {
   public equals(message: Message, rawData: unknown): boolean;
   public fetchReference(): Promise<OmitPartialGroupDMChannel<Message<InGuild>>>;
   public fetchWebhook(): Promise<Webhook>;
-  public forward(channel: TextBasedChannelResolvable): Promise<Message>;
   public crosspost(): Promise<OmitPartialGroupDMChannel<Message<InGuild>>>;
   public fetch(force?: boolean): Promise<OmitPartialGroupDMChannel<Message<InGuild>>>;
   public pin(reason?: string): Promise<OmitPartialGroupDMChannel<Message<InGuild>>>;
@@ -3810,7 +3809,6 @@ export const Constants: {
   SweeperKeys: SweeperKey[];
   NonSystemMessageTypes: NonSystemMessageType[];
   TextBasedChannelTypes: TextBasedChannelTypes[];
-  SendableChannels: SendableChannelTypes[];
   GuildTextBasedChannelTypes: GuildTextBasedChannelTypes[];
   ThreadChannelTypes: ThreadChannelType[];
   VoiceBasedChannelTypes: VoiceBasedChannelTypes[];
@@ -4129,6 +4127,10 @@ export class CategoryChannelChildManager extends DataManager<Snowflake, Category
 
 export class ChannelManager extends CachedManager<Snowflake, Channel, ChannelResolvable> {
   private constructor(client: Client<true>, iterable: Iterable<RawChannelData>);
+  public createMessage(
+    channel: Omit<TextBasedChannelResolvable, 'PartialGroupDMChannel'>,
+    options: string | MessagePayload | MessageCreateOptions,
+  ): Promise<Message>;
   public fetch(id: Snowflake, options?: FetchChannelOptions): Promise<Channel | null>;
 }
 
@@ -4423,7 +4425,6 @@ export abstract class MessageManager<InGuild extends boolean = boolean> extends
   public fetch(options: MessageResolvable | FetchMessageOptions): Promise<Message<InGuild>>;
   public fetch(options?: FetchMessagesOptions): Promise<Collection<Snowflake, Message<InGuild>>>;
   public fetchPinned(cache?: boolean): Promise<Collection<Snowflake, Message<InGuild>>>;
-  public forward(reference: Omit<MessageReference, 'type'>): Promise<Message<InGuild>>;
   public react(message: MessageResolvable, emoji: EmojiIdentifierResolvable): Promise<void>;
   public pin(message: MessageResolvable, reason?: string): Promise<void>;
   public unpin(message: MessageResolvable, reason?: string): Promise<void>;
@@ -6498,15 +6499,14 @@ export interface TextInputComponentData extends BaseComponentData {
 }
 
 export type MessageTarget =
+  | ChannelManager
   | Interaction
   | InteractionWebhook
+  | Message
+  | MessageManager
   | TextBasedChannel
-  | User
-  | GuildMember
   | Webhook<WebhookType.Incoming>
-  | WebhookClient
-  | Message
-  | MessageManager;
+  | WebhookClient;
 
 export interface MultipleShardRespawnOptions {
   shardDelay?: number;
@@ -6810,26 +6810,26 @@ export type Channel =
 
 export type TextBasedChannel = Exclude<Extract<Channel, { type: TextChannelType }>, ForumChannel | MediaChannel>;
 
-export type SendableChannels = Extract<Channel, { send: (...args: any[]) => any }>;
-
 export type TextBasedChannels = TextBasedChannel;
 
 export type TextBasedChannelTypes = TextBasedChannel['type'];
 
 export type GuildTextBasedChannelTypes = Exclude<TextBasedChannelTypes, ChannelType.DM | ChannelType.GroupDM>;
 
-export type SendableChannelTypes = SendableChannels['type'];
-
 export type VoiceBasedChannel = Extract<Channel, { bitrate: number }>;
 
 export type GuildBasedChannel = Extract<Channel, { guild: Guild }>;
 
+export type SendableChannels = Extract<Channel, { send: (...args: any[]) => any }>;
+
 export type CategoryChildChannel = Exclude<Extract<Channel, { parent: CategoryChannel | null }>, CategoryChannel>;
 
 export type NonThreadGuildBasedChannel = Exclude<GuildBasedChannel, AnyThreadChannel>;
 
 export type GuildTextBasedChannel = Extract<GuildBasedChannel, TextBasedChannel>;
 
+export type SendableChannelTypes = SendableChannels['type'];
+
 export type TextChannelResolvable = Snowflake | TextChannel;
 
 export type TextBasedChannelResolvable = Snowflake | TextBasedChannel;
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<true>>(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',