Skip to content

Commit

Permalink
feat: message forwards (#10733)
Browse files Browse the repository at this point in the history
* feat: message forwards

* fix: spelling

* feat: add guildId option for forward

* refactor: type

* refactor: do not use ID suffix for resolvables

* Update TextBasedChannel.js

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
  • Loading branch information
imnaiyar and Jiralite authored Feb 7, 2025
1 parent f224a07 commit 89c076c
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 4 deletions.
25 changes: 24 additions & 1 deletion packages/discord.js/src/structures/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
MessageType,
MessageFlags,
PermissionFlagsBits,
MessageReferenceType,
} = require('discord-api-types/v10');
const Attachment = require('./Attachment');
const Base = require('./Base');
Expand Down Expand Up @@ -704,7 +705,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 @@ -956,6 +961,24 @@ class Message extends Base {
return this.channel.send(data);
}

/**
* Forwards this message
*
* @param {TextBasedChannelResolvable} channel The channel to forward this message to.
* @returns {Promise<Message>}
*/
forward(channel) {
const resolvedChannel = this.client.channels.resolve(channel);
if (!resolvedChannel) throw new DiscordjsError(ErrorCodes.InvalidType, 'channel', 'TextBasedChannelResolvable');
return resolvedChannel.send({
forward: {
message: this.id,
channel: this.channelId,
guild: this.guildId,
},
});
}

/**
* Options for starting a thread on a message.
* @typedef {Object} StartThreadOptions
Expand Down
18 changes: 17 additions & 1 deletion packages/discord.js/src/structures/MessagePayload.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const { Buffer } = require('node:buffer');
const { lazy, isJSONEncodable } = require('@discordjs/util');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { MessageFlags } = require('discord-api-types/v10');
const { MessageFlags, MessageReferenceType } = require('discord-api-types/v10');
const ActionRowBuilder = require('./ActionRowBuilder');
const { DiscordjsError, DiscordjsRangeError, ErrorCodes } = require('../errors');
const { resolveFile } = require('../util/DataResolver');
Expand Down Expand Up @@ -199,6 +199,22 @@ class MessagePayload {
}
}

if (typeof this.options.forward === 'object') {
const reference = this.options.forward.message;
const channel_id = reference.channelId ?? this.target.client.channels.resolveId(this.options.forward.channel);
const guild_id = reference.guildId ?? this.target.client.guilds.resolveId(this.options.forward.guild);
const message_id = this.target.messages.resolveId(reference);
if (message_id) {
if (!channel_id) throw new DiscordjsError(ErrorCodes.InvalidType, 'channelId', 'TextBasedChannelResolvable');
message_reference = {
type: MessageReferenceType.Forward,
message_id,
channel_id,
guild_id: guild_id ?? undefined,
};
}
}

const attachments = this.options.files?.map((file, index) => ({
id: index.toString(),
description: file.description,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,18 @@ class TextBasedChannel {
* <info>Only `MessageFlags.SuppressEmbeds` and `MessageFlags.SuppressNotifications` can be set.</info>
*/

/**
* @typedef {Object} ForwardOptions
* @property {MessageResolvable} message The originating message
* @property {TextBasedChannelResolvable} [channel] The channel of the originating message
* @property {GuildResolvable} [guild] The guild of the originating message
*/

/**
* The options for sending a message.
* @typedef {BaseMessageCreateOptions} MessageCreateOptions
* @property {ReplyOptions} [reply] The options for replying to a message
* @property {ForwardOptions} [forward] The options for forwarding a message
*/

/**
Expand Down
21 changes: 19 additions & 2 deletions packages/discord.js/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2310,6 +2310,7 @@ export class Message<InGuild extends boolean = boolean> extends Base {
public reply(
options: string | MessagePayload | MessageReplyOptions,
): Promise<OmitPartialGroupDMChannel<Message<InGuild>>>;
public forward(channel: Exclude<TextBasedChannelResolvable, PartialGroupDMChannel>): Promise<Message>;
public resolveComponent(customId: string): MessageActionRowComponent | null;
public startThread(options: StartThreadOptions): Promise<PublicThreadChannel<false>>;
public suppressEmbeds(suppress?: boolean): Promise<OmitPartialGroupDMChannel<Message<InGuild>>>;
Expand Down Expand Up @@ -6759,6 +6760,7 @@ export interface MessageCreateOptions extends BaseMessageOptionsWithPoll {
nonce?: string | number;
enforceNonce?: boolean;
reply?: ReplyOptions;
forward?: ForwardOptions;
stickers?: readonly StickerResolvable[];
flags?:
| BitFieldResolvable<
Expand Down Expand Up @@ -7011,7 +7013,21 @@ export interface ReplyOptions {
failIfNotExists?: boolean;
}

export interface MessageReplyOptions extends Omit<MessageCreateOptions, 'reply'> {
export interface BaseForwardOptions {
message: MessageResolvable;
channel?: Exclude<TextBasedChannelResolvable, PartialGroupDMChannel>;
guild?: GuildResolvable;
}

export type ForwardOptionsWithMandatoryChannel = BaseForwardOptions & Required<Pick<BaseForwardOptions, 'channel'>>;

export interface ForwardOptionsWithOptionalChannel extends BaseForwardOptions {
message: Exclude<MessageResolvable, Snowflake>;
}

export type ForwardOptions = ForwardOptionsWithMandatoryChannel | ForwardOptionsWithOptionalChannel;

export interface MessageReplyOptions extends Omit<MessageCreateOptions, 'reply' | 'forward'> {
failIfNotExists?: boolean;
}

Expand Down Expand Up @@ -7290,7 +7306,8 @@ export interface WebhookFetchMessageOptions {
threadId?: Snowflake;
}

export interface WebhookMessageCreateOptions extends Omit<MessageCreateOptions, 'nonce' | 'reply' | 'stickers'> {
export interface WebhookMessageCreateOptions
extends Omit<MessageCreateOptions, 'nonce' | 'reply' | 'stickers' | 'forward'> {
username?: string;
avatarURL?: string;
threadId?: Snowflake;
Expand Down

0 comments on commit 89c076c

Please sign in to comment.