diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index 858d66300..5843e2908 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -22,6 +22,8 @@ const Guild = require('../../structures/Guild'); const VoiceRegion = require('../../structures/VoiceRegion'); const GuildAuditLogs = require('../../structures/GuildAuditLogs'); +const MessageFlags = require('../../util/MessageFlags'); + class RESTMethods { constructor(restManager) { this.rest = restManager; @@ -132,9 +134,11 @@ class RESTMethods { }); } - updateMessage(message, content, { embed, code, reply } = {}) { + updateMessage(message, content, { flags, embed, code, reply } = {}) { if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); + if (typeof flags !== 'undefined') flags = MessageFlags.resolve(flags); + // Wrap everything in a code block if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true); @@ -151,7 +155,7 @@ class RESTMethods { if (embed instanceof RichEmbed) embed = embed._apiTransform(); return this.rest.makeRequest('patch', Endpoints.Message(message), true, { - content, embed, + content, embed, flags, }).then(data => this.client.actions.MessageUpdate.handle(data).updated); } diff --git a/src/index.js b/src/index.js index 015e2b3ac..b5b19c75f 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,7 @@ module.exports = { Constants: require('./util/Constants'), DiscordAPIError: require('./client/rest/DiscordAPIError'), EvaluatedPermissions: require('./util/Permissions'), + MessageFlags: require('./util/MessageFlags'), Permissions: require('./util/Permissions'), Snowflake: require('./util/Snowflake'), SnowflakeUtil: require('./util/Snowflake'), diff --git a/src/structures/Message.js b/src/structures/Message.js index 10eafe650..c71844f47 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -8,6 +8,7 @@ const Util = require('../util/Util'); const Collection = require('../util/Collection'); const Constants = require('../util/Constants'); const Permissions = require('../util/Permissions'); +const MessageFlags = require('../util/MessageFlags'); let GuildMember; /** @@ -77,7 +78,9 @@ class Message { /** * A random number or string used for checking message delivery - * @type {string} + * This is only received after the message was sent successfully, and + * lost if re-fetched + * @type {?string} */ this.nonce = data.nonce; @@ -85,7 +88,7 @@ class Message { * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications) * @type {boolean} */ - this.system = data.type === 6; + this.system = data.type !== 0; /** * A list of embeds in the message - e.g. YouTube Player @@ -128,7 +131,7 @@ class Message { * All valid mentions that the message contains * @type {MessageMentions} */ - this.mentions = new Mentions(this, data.mentions, data.mention_roles, data.mention_everyone); + this.mentions = new Mentions(this, data.mentions, data.mention_roles, data.mention_everyone, data.mention_channels); /** * ID of the webhook that sent the message, if applicable @@ -142,6 +145,30 @@ class Message { */ this.hit = typeof data.hit === 'boolean' ? data.hit : null; + /** + * Flags that are applied to the message + * @type {Readonly} + */ + this.flags = new MessageFlags(data.flags).freeze(); + + /** + * Reference data sent in a crossposted message. + * @typedef {Object} MessageReference + * @property {string} channelID ID of the channel the message was crossposted from + * @property {?string} guildID ID of the guild the message was crossposted from + * @property {?string} messageID ID of the message that was crossposted + */ + + /** + * Message reference data + * @type {?MessageReference} + */ + this.reference = data.message_reference ? { + channelID: data.message_reference.channel_id, + guildID: data.message_reference.guild_id, + messageID: data.message_reference.message_id, + } : null; + /** * The previous versions of the message, sorted with the most recent first * @type {Message[]} @@ -188,8 +215,11 @@ class Message { this, 'mentions' in data ? data.mentions : this.mentions.users, 'mentions_roles' in data ? data.mentions_roles : this.mentions.roles, - 'mention_everyone' in data ? data.mention_everyone : this.mentions.everyone + 'mention_everyone' in data ? data.mention_everyone : this.mentions.everyone, + 'mention_channels' in data ? data.mention_channels : this.mentions.crosspostedChannels ); + + this.flags = new MessageFlags('flags' in data ? data.flags : 0).freeze(); } /** @@ -384,6 +414,7 @@ class Message { * @typedef {Object} MessageEditOptions * @property {Object} [embed] An embed to be added/edited * @property {string|boolean} [code] Language for optional codeblock formatting to apply + * @property {MessageFlagsResolvable} [flags] Message flags to apply */ /** @@ -528,6 +559,23 @@ class Message { return this.client.fetchWebhook(this.webhookID); } + /** + * Suppresses or unsuppresses embeds on a message + * @param {boolean} [suppress=true] If the embeds should be suppressed or not + * @returns {Promise} + */ + suppressEmbeds(suppress = true) { + const flags = new MessageFlags(this.flags.bitfield); + + if (suppress) { + flags.add(MessageFlags.FLAGS.SUPPRESS_EMBEDS); + } else { + flags.remove(MessageFlags.FLAGS.SUPPRESS_EMBEDS); + } + + return this.edit(undefined, { flags }); + } + /** * Used mainly internally. Whether two messages are identical in properties. If you want to compare messages * without checking all the properties, use `message.id === message2.id`, which is much more efficient. This @@ -542,12 +590,12 @@ class Message { if (embedUpdate) return this.id === message.id && this.embeds.length === message.embeds.length; let equal = this.id === message.id && - this.author.id === message.author.id && - this.content === message.content && - this.tts === message.tts && - this.nonce === message.nonce && - this.embeds.length === message.embeds.length && - this.attachments.length === message.attachments.length; + this.author.id === message.author.id && + this.content === message.content && + this.tts === message.tts && + this.nonce === message.nonce && + this.embeds.length === message.embeds.length && + this.attachments.length === message.attachments.length; if (equal && rawData) { equal = this.mentions.everyone === message.mentions.everyone && diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 8c80906e8..47aff55c2 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -1,10 +1,11 @@ const Collection = require('../util/Collection'); +const { ChannelTypes } = require('../util/Constants'); /** * Keeps track of mentions in a {@link Message}. */ class MessageMentions { - constructor(message, users, roles, everyone) { + constructor(message, users, roles, everyone, crosspostedChannels) { /** * Whether `@everyone` or `@here` were mentioned * @type {boolean} @@ -87,6 +88,39 @@ class MessageMentions { * @private */ this._channels = null; + + /** + * Crossposted channel data. + * @typedef {Object} CrosspostedChannel + * @property {Snowflake} channelID ID of the mentioned channel + * @property {Snowflake} guildID ID of the guild that has the channel + * @property {string} type Type of the channel + * @property {string} name Name of the channel + */ + + if (crosspostedChannels) { + if (crosspostedChannels instanceof Collection) { + /** + * A collection of crossposted channels + * @type {Collection} + */ + this.crosspostedChannels = new Collection(crosspostedChannels); + } else { + this.crosspostedChannels = new Collection(); + const channelTypes = Object.keys(ChannelTypes); + for (const d of crosspostedChannels) { + const type = channelTypes[d.type]; + this.crosspostedChannels.set(d.id, { + channelID: d.id, + guildID: d.guild_id, + type: type ? type.toLowerCase() : 'unknown', + name: d.name, + }); + } + } + } else { + this.crosspostedChannels = new Collection(); + } } /** diff --git a/src/structures/User.js b/src/structures/User.js index 43adb37df..86c2bc0ac 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -52,6 +52,13 @@ class User { */ this.bot = Boolean(data.bot); + /** + * Whether this is an Official Discord System user (part of the urgent message system) + * @type {?boolean} + * @name User#system + */ + if (typeof data.system !== 'undefined') this.system = Boolean(data.system); + /** * The ID of the last message sent by the user, if one was sent * @type {?Snowflake} diff --git a/src/util/Constants.js b/src/util/Constants.js index 79b309439..008fb9c71 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -507,6 +507,7 @@ exports.WSEvents = { * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 + * * CHANNEL_FOLLOW_ADD * @typedef {string} MessageType */ exports.MessageTypes = [ @@ -522,6 +523,7 @@ exports.MessageTypes = [ 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1', 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2', 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3', + 'CHANNEL_FOLLOW_ADD', ]; /** diff --git a/src/util/MessageFlags.js b/src/util/MessageFlags.js new file mode 100644 index 000000000..88c8014b8 --- /dev/null +++ b/src/util/MessageFlags.js @@ -0,0 +1,36 @@ +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with an {@link Message#flags} bitfield. + * @extends {BitField} + */ +class MessageFlags extends BitField {} + +/** + * Data that can be resolved to give a permission number. This can be: + * * A string (see {@link MessageFlags.FLAGS}) + * * A message flag + * * An instance of MessageFlags + * * An array of MessageFlagsResolvable + * @typedef {string|number|MessageFlags|MessageFlagsResolvable[]} MessageFlagsResolvable + */ + +/** + * Numeric message flags. All available properties: + * * `CROSSPOSTED` + * * `IS_CROSSPOST` + * * `SUPPRESS_EMBEDS` + * * `SOURCE_MESSAGE_DELETED` + * * `URGENT` + * @type {Object} + * @see {@link https://discordapp.com/developers/docs/resources/channel#message-object-message-flags} + */ +MessageFlags.FLAGS = { + CROSSPOSTED: 1 << 0, + IS_CROSSPOST: 1 << 1, + SUPPRESS_EMBEDS: 1 << 2, + SOURCE_MESSAGE_DELETED: 1 << 3, + URGENT: 1 << 4, +}; + +module.exports = MessageFlags; diff --git a/typings/index.d.ts b/typings/index.d.ts index f436b1347..de12e4f77 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -804,6 +804,7 @@ declare module 'discord.js' { public editedTimestamp: number; public readonly edits: Message[]; public embeds: MessageEmbed[]; + public flags: Readonly; public readonly guild: Guild; public hit: boolean; public id: Snowflake; @@ -813,6 +814,7 @@ declare module 'discord.js' { public readonly pinnable: boolean; public pinned: boolean; public reactions: Collection; + public reference: MessageReference | null; public system: boolean; public tts: boolean; public type: string; @@ -833,6 +835,7 @@ declare module 'discord.js' { public react(emoji: string | Emoji | ReactionEmoji): Promise; public reply(content?: StringResolvable, options?: MessageOptions): Promise; public reply(options?: MessageOptions): Promise; + public suppressEmbeds(suppress?: boolean): Promise; public toString(): string; public unpin(): Promise; } @@ -940,6 +943,11 @@ declare module 'discord.js' { public width: number; } + export class MessageFlags extends BitField { + public static FLAGS: Record; + public static resolve(bit?: BitFieldResolvable): number; + } + export class MessageMentions { private _channels: Collection; private _client: Client; @@ -948,6 +956,7 @@ declare module 'discord.js' { private _members: Collection; public readonly channels: Collection; + public crosspostedChannels: Collection; public everyone: boolean; public readonly members: Collection; public roles: Collection; @@ -1361,6 +1370,7 @@ declare module 'discord.js' { public lastMessageID: string; public readonly note: string; public readonly presence: Presence; + public system?: boolean; public readonly tag: string; public username: string; public addFriend(): Promise; @@ -1849,6 +1859,13 @@ declare module 'discord.js' { | number | string; + type CrosspostedChannel = { + channelID: Snowflake; + guildID: Snowflake; + type: Channel["type"] | 'unknown'; + name: string; + }; + type DeconstructedSnowflake = { timestamp: number; date: Date; @@ -2040,8 +2057,15 @@ declare module 'discord.js' { type MessageEditOptions = { embed?: RichEmbedOptions; code?: string | boolean; + flags?: BitFieldResolvable; }; + type MessageFlagsString = 'CROSSPOSTED' + | 'IS_CROSSPOST' + | 'SUPRRESS_EMBEDS' + | 'SOURCE_MESSAGE_DELETED' + | 'URGENT'; + type MessageNotifications = 'EVERYTHING' | 'MENTIONS' | 'NOTHING'; @@ -2100,6 +2124,12 @@ declare module 'discord.js' { nsfw?: boolean; }; + type MessageReference = { + channelID: Snowflake; + guildID: Snowflake | null; + messageID: Snowflake | null; + }; + type MessageSearchResult = { totalResults: number; messages: Message[][];