feat: message forwards (#10733)

* 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>
This commit is contained in:
Naiyar
2025-02-08 03:42:30 +06:00
committed by GitHub
parent f224a07381
commit 89c076c89e
4 changed files with 68 additions and 4 deletions

View File

@@ -9,6 +9,7 @@ const {
MessageType, MessageType,
MessageFlags, MessageFlags,
PermissionFlagsBits, PermissionFlagsBits,
MessageReferenceType,
} = require('discord-api-types/v10'); } = require('discord-api-types/v10');
const Attachment = require('./Attachment'); const Attachment = require('./Attachment');
const Base = require('./Base'); const Base = require('./Base');
@@ -704,7 +705,11 @@ class Message extends Base {
* @readonly * @readonly
*/ */
get editable() { 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 // 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. // the thread is archived or the thread is locked and the bot does not have permission to manage threads.
@@ -956,6 +961,24 @@ class Message extends Base {
return this.channel.send(data); 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. * Options for starting a thread on a message.
* @typedef {Object} StartThreadOptions * @typedef {Object} StartThreadOptions

View File

@@ -3,7 +3,7 @@
const { Buffer } = require('node:buffer'); const { Buffer } = require('node:buffer');
const { lazy, isJSONEncodable } = require('@discordjs/util'); const { lazy, isJSONEncodable } = require('@discordjs/util');
const { DiscordSnowflake } = require('@sapphire/snowflake'); 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 ActionRowBuilder = require('./ActionRowBuilder');
const { DiscordjsError, DiscordjsRangeError, ErrorCodes } = require('../errors'); const { DiscordjsError, DiscordjsRangeError, ErrorCodes } = require('../errors');
const { resolveFile } = require('../util/DataResolver'); const { resolveFile } = require('../util/DataResolver');
@@ -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) => ({ const attachments = this.options.files?.map((file, index) => ({
id: index.toString(), id: index.toString(),
description: file.description, description: file.description,

View File

@@ -110,10 +110,18 @@ class TextBasedChannel {
* <info>Only `MessageFlags.SuppressEmbeds` and `MessageFlags.SuppressNotifications` can be set.</info> * <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. * The options for sending a message.
* @typedef {BaseMessageCreateOptions} MessageCreateOptions * @typedef {BaseMessageCreateOptions} MessageCreateOptions
* @property {ReplyOptions} [reply] The options for replying to a message * @property {ReplyOptions} [reply] The options for replying to a message
* @property {ForwardOptions} [forward] The options for forwarding a message
*/ */
/** /**

View File

@@ -2310,6 +2310,7 @@ export class Message<InGuild extends boolean = boolean> extends Base {
public reply( public reply(
options: string | MessagePayload | MessageReplyOptions, options: string | MessagePayload | MessageReplyOptions,
): Promise<OmitPartialGroupDMChannel<Message<InGuild>>>; ): Promise<OmitPartialGroupDMChannel<Message<InGuild>>>;
public forward(channel: Exclude<TextBasedChannelResolvable, PartialGroupDMChannel>): Promise<Message>;
public resolveComponent(customId: string): MessageActionRowComponent | null; public resolveComponent(customId: string): MessageActionRowComponent | null;
public startThread(options: StartThreadOptions): Promise<PublicThreadChannel<false>>; public startThread(options: StartThreadOptions): Promise<PublicThreadChannel<false>>;
public suppressEmbeds(suppress?: boolean): Promise<OmitPartialGroupDMChannel<Message<InGuild>>>; public suppressEmbeds(suppress?: boolean): Promise<OmitPartialGroupDMChannel<Message<InGuild>>>;
@@ -6759,6 +6760,7 @@ export interface MessageCreateOptions extends BaseMessageOptionsWithPoll {
nonce?: string | number; nonce?: string | number;
enforceNonce?: boolean; enforceNonce?: boolean;
reply?: ReplyOptions; reply?: ReplyOptions;
forward?: ForwardOptions;
stickers?: readonly StickerResolvable[]; stickers?: readonly StickerResolvable[];
flags?: flags?:
| BitFieldResolvable< | BitFieldResolvable<
@@ -7011,7 +7013,21 @@ export interface ReplyOptions {
failIfNotExists?: boolean; 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; failIfNotExists?: boolean;
} }
@@ -7290,7 +7306,8 @@ export interface WebhookFetchMessageOptions {
threadId?: Snowflake; threadId?: Snowflake;
} }
export interface WebhookMessageCreateOptions extends Omit<MessageCreateOptions, 'nonce' | 'reply' | 'stickers'> { export interface WebhookMessageCreateOptions
extends Omit<MessageCreateOptions, 'nonce' | 'reply' | 'stickers' | 'forward'> {
username?: string; username?: string;
avatarURL?: string; avatarURL?: string;
threadId?: Snowflake; threadId?: Snowflake;