Skip to content

feat!: create forwards and add ChannelManager#createMessage() #10559

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions packages/discord.js/src/managers/ChannelManager.js
Original file line number Diff line number Diff line change
@@ -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.js');
const { BaseChannel } = require('../structures/BaseChannel.js');
const { MessagePayload } = require('../structures/MessagePayload.js');
const { createChannel } = require('../util/Channels.js');
const { ThreadChannelTypes } = require('../util/Constants.js');
const { Events } = require('../util/Events.js');

const getMessage = lazy(() => require('../structures/Message.js').Message);

let cacheWarningEmitted = false;

/**
Expand Down Expand Up @@ -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://github.com/discordjs.png']
* })
* .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);
}
}

exports.ChannelManager = ChannelManager;
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,6 @@ class BaseGuildTextChannel extends GuildChannel {
setNSFW() {}
}

TextBasedChannel.applyToClass(BaseGuildTextChannel, true);
TextBasedChannel.applyToClass(BaseGuildTextChannel);

exports.BaseGuildTextChannel = BaseGuildTextChannel;
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,6 @@ class BaseGuildVoiceChannel extends GuildChannel {
setNSFW() {}
}

TextBasedChannel.applyToClass(BaseGuildVoiceChannel, true, ['lastPinAt']);
TextBasedChannel.applyToClass(BaseGuildVoiceChannel, ['lastPinAt']);

exports.BaseGuildVoiceChannel = BaseGuildVoiceChannel;
2 changes: 1 addition & 1 deletion packages/discord.js/src/structures/DMChannel.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class DMChannel extends BaseChannel {
// Doesn't work on DM channels; setNSFW() {}
}

TextBasedChannel.applyToClass(DMChannel, true, [
TextBasedChannel.applyToClass(DMChannel, [
'bulkDelete',
'fetchWebhooks',
'createWebhook',
Expand Down
34 changes: 16 additions & 18 deletions packages/discord.js/src/structures/GuildMember.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
const { PermissionFlagsBits } = require('discord-api-types/v10');
const { Base } = require('./Base.js');
const { VoiceState } = require('./VoiceState.js');
const { TextBasedChannel } = require('./interfaces/TextBasedChannel.js');
const { DiscordjsError, ErrorCodes } = require('../errors/index.js');
const { GuildMemberRoleManager } = require('../managers/GuildMemberRoleManager.js');
const { GuildMemberFlagsBitField } = require('../util/GuildMemberFlagsBitField.js');
const { PermissionsBitField } = require('../util/PermissionsBitField.js');

/**
* Represents a member of a guild on Discord.
* @implements {TextBasedChannel}
* @extends {Base}
*/
class GuildMember extends Base {
Expand Down Expand Up @@ -476,6 +474,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
Expand Down Expand Up @@ -527,20 +541,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;
35 changes: 29 additions & 6 deletions packages/discord.js/src/structures/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
ChannelType,
MessageType,
MessageFlags,
MessageReferenceType,
PermissionFlagsBits,
} = require('discord-api-types/v10');
const { Attachment } = require('./Attachment.js');
Expand Down Expand Up @@ -680,7 +681,11 @@ class Message extends Base {
* @readonly
*/
get editable() {
const precheck = Boolean(this.author.id === this.client.user.id && (!this.guild || this.channel?.viewable));
const precheck = Boolean(
this.author.id === this.client.user.id &&
(!this.guild || this.channel?.viewable) &&
this.reference?.type !== MessageReferenceType.Forward,
);

// Regardless of permissions thread messages cannot be edited if
// the thread is archived or the thread is locked and the bot does not have permission to manage threads.
Expand Down Expand Up @@ -915,21 +920,39 @@ class Message extends Base {
* .then(() => console.log(`Replied to message "${message.content}"`))
* .catch(console.error);
*/
async reply(options) {
if (!this.channel) throw new DiscordjsError(ErrorCodes.ChannelNotCached);
reply(options) {
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 ?? undefined,
type: MessageReferenceType.Default,
failIfNotExists: options?.failIfNotExists ?? this.client.options.failIfNotExists,
},
});
}
return this.channel.send(data);
return this.client.channels.createMessage(this.channelId, data);
}

/**
* Forwards this message.
* @param {TextChannelResolvable} channel The channel to forward this message to.
* @returns {Promise<Message>}
*/
forward(channel) {
return this.client.channels.createMessage(channel, {
messageReference: {
messageId: this.id,
channelId: this.channelId,
guildId: this.guildId ?? undefined,
type: MessageReferenceType.Forward,
},
});
}

/**
Expand Down
19 changes: 11 additions & 8 deletions packages/discord.js/src/structures/MessagePayload.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ class MessagePayload {
if (
// eslint-disable-next-line eqeqeq
this.options.flags != null ||
(this.isMessage && this.options.reply === undefined) ||
(this.isMessage && this.options.messageReference === undefined) ||
this.isMessageManager
) {
flags = new MessageFlagsBitField(this.options.flags).bitfield;
Expand All @@ -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,
};
}
}
Expand Down Expand Up @@ -292,7 +295,7 @@ exports.MessagePayload = MessagePayload;

/**
* A target for a message.
* @typedef {TextBasedChannels|User|GuildMember|Webhook|WebhookClient|BaseInteraction|InteractionWebhook|
* @typedef {TextBasedChannels|ChannelManager|Webhook|WebhookClient|BaseInteraction|InteractionWebhook|
* Message|MessageManager} MessageTarget
*/

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class PartialGroupDMChannel extends BaseChannel {
awaitMessageComponent() {}
}

TextBasedChannel.applyToClass(PartialGroupDMChannel, true, [
TextBasedChannel.applyToClass(PartialGroupDMChannel, [
'bulkDelete',
'send',
'sendTyping',
Expand Down
2 changes: 1 addition & 1 deletion packages/discord.js/src/structures/ThreadChannel.js
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,6 @@ class ThreadChannel extends BaseChannel {
// Doesn't work on Thread channels; setNSFW() {}
}

TextBasedChannel.applyToClass(ThreadChannel, true, ['fetchWebhooks', 'setRateLimitPerUser', 'setNSFW']);
TextBasedChannel.applyToClass(ThreadChannel, ['fetchWebhooks', 'setRateLimitPerUser', 'setNSFW']);

exports.ThreadChannel = ThreadChannel;
2 changes: 1 addition & 1 deletion packages/discord.js/src/structures/ThreadOnlyChannel.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ class ThreadOnlyChannel extends GuildChannel {
setRateLimitPerUser() {}
}

TextBasedChannel.applyToClass(ThreadOnlyChannel, true, [
TextBasedChannel.applyToClass(ThreadOnlyChannel, [
'send',
'lastMessage',
'lastPinAt',
Expand Down
34 changes: 16 additions & 18 deletions packages/discord.js/src/structures/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ const { userMention } = require('@discordjs/formatters');
const { calculateUserDefaultAvatarIndex } = require('@discordjs/rest');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Base } = require('./Base.js');
const { TextBasedChannel } = require('./interfaces/TextBasedChannel.js');
const { UserFlagsBitField } = require('../util/UserFlagsBitField.js');

/**
* Represents a user on Discord.
* @implements {TextBasedChannel}
* @extends {Base}
*/
class User extends Base {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);

exports.User = User;
2 changes: 1 addition & 1 deletion packages/discord.js/src/structures/Webhook.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ class Webhook {
* @example
* // Send a remote file
* webhook.send({
* files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048']
* files: ['https://github.com/discordjs.png']
* })
* .then(console.log)
* .catch(console.error);
Expand Down
Loading