From 9cb306c823216130e5bddc268ba3aee025a5b98f Mon Sep 17 00:00:00 2001 From: Timo <30553356+y21@users.noreply.github.com> Date: Wed, 26 Feb 2020 12:13:23 +0100 Subject: [PATCH] feat: replace disableEveryone with disableMentions (#3830) * add ClientOptions#disableMentions and MessageOptions#disableMentions * provide tests * don't sanitize controlled mentions * add @here mentions to tests * fix indents (6 spaces instead of 8) * add Util#cleanContent tests * add typings for removeMentions * replace @ with @\u200b AFTER cleaning content as suggested instead of using removeMentions * better explanation of this option * no newline in Util.removeMentions * fix long line * remove double space * remove comments (change has been reverted) * Use Util.removeMentions to remove mentions * use Util.removeMentions in Util.cleanContent --- src/client/Client.js | 4 +- src/structures/APIMessage.js | 14 +++--- src/structures/Webhook.js | 4 +- src/structures/interfaces/TextBasedChannel.js | 4 +- src/util/Constants.js | 4 +- src/util/Util.js | 14 ++++-- test/disableMentions.js | 47 +++++++++++++++++++ typings/index.d.ts | 7 +-- 8 files changed, 77 insertions(+), 21 deletions(-) create mode 100644 test/disableMentions.js diff --git a/src/client/Client.js b/src/client/Client.js index ec5128860..e2abbfa77 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -386,8 +386,8 @@ class Client extends BaseClient { if (typeof options.fetchAllMembers !== 'boolean') { throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', 'a boolean'); } - if (typeof options.disableEveryone !== 'boolean') { - throw new TypeError('CLIENT_INVALID_OPTION', 'disableEveryone', 'a boolean'); + if (typeof options.disableMentions !== 'boolean') { + throw new TypeError('CLIENT_INVALID_OPTION', 'disableMentions', 'a boolean'); } if (!Array.isArray(options.partials)) { throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array'); diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index b05fb68f9..3a52a4608 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -88,6 +88,13 @@ class APIMessage { content = Util.resolveString(this.options.content); } + const disableMentions = typeof this.options.disableMentions === 'undefined' ? + this.target.client.options.disableMentions : + this.options.disableMentions; + if (disableMentions) { + content = Util.removeMentions(content || ''); + } + const isSplit = typeof this.options.split !== 'undefined' && this.options.split !== false; const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false; const splitOptions = isSplit ? { ...this.options.split } : undefined; @@ -113,13 +120,6 @@ class APIMessage { content = `${mentionPart}${content || ''}`; } - const disableEveryone = typeof this.options.disableEveryone === 'undefined' ? - this.target.client.options.disableEveryone : - this.options.disableEveryone; - if (disableEveryone) { - content = (content || '').replace(/@(everyone|here)/g, '@\u200b$1'); - } - if (isSplit) { content = Util.splitMessage(content || '', splitOptions); } diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index b29607040..c76045994 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -85,8 +85,8 @@ class Webhook { * @property {string} [nonce=''] The nonce for the message * @property {Object[]} [embeds] An array of embeds for the message * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) - * @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here - * should be replaced with plain-text + * @property {boolean} [disableMentions=this.client.options.disableMentions] Whether or not a zero width space + * should be placed after every @ character to prevent unexpected mentions * @property {FileOptions[]|string[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 0fe0e2dca..38a9dcb31 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -57,8 +57,8 @@ class TextBasedChannel { * @property {string} [content=''] The content for the message * @property {MessageEmbed|Object} [embed] An embed for the message * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) - * @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here - * should be replaced with plain-text + * @property {boolean} [disableMentions=this.client.options.disableMentions] Whether or not a zero width space + * should be placed after every @ character to prevent unexpected mentions * @property {FileOptions[]|BufferResolvable[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if diff --git a/src/util/Constants.js b/src/util/Constants.js index 2bbe580d3..b38e0b580 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -21,7 +21,7 @@ const browser = exports.browser = typeof window !== 'undefined'; * the message cache lifetime (in seconds, 0 for never) * @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as * upon joining a guild (should be avoided whenever possible) - * @property {boolean} [disableEveryone=false] Default value for {@link MessageOptions#disableEveryone} + * @property {boolean} [disableMentions=false] Default value for {@link MessageOptions#disableMentions} * @property {PartialType[]} [partials] Structures allowed to be partial. This means events can be emitted even when * they're missing all the data for a particular structure. See the "Partials" topic listed in the sidebar for some * important usage information, as partials require you to put checks in place when handling data. @@ -47,7 +47,7 @@ exports.DefaultOptions = { messageCacheLifetime: 0, messageSweepInterval: 0, fetchAllMembers: false, - disableEveryone: false, + disableMentions: false, partials: [], restWsBridgeTimeout: 5000, disabledEvents: [], diff --git a/src/util/Util.js b/src/util/Util.js index 6e878f34e..bc47ada47 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -518,6 +518,15 @@ class Util { return dec; } + /** + * Breaks user, role and everyone/here mentions by adding a zero width space after every @ character + * @param {string} str The string to sanitize + * @returns {string} + */ + static removeMentions(str) { + return str.replace(/@/g, '@\u200b'); + } + /** * The content to have all mentions replaced by the equivalent text. * @param {string} str The string to be converted @@ -525,8 +534,7 @@ class Util { * @returns {string} */ static cleanContent(str, message) { - return str - .replace(/@(everyone|here)/g, '@\u200b$1') + return Util.removeMentions(str .replace(/<@!?[0-9]+>/g, input => { const id = input.replace(/<|!|>|@/g, ''); if (message.channel.type === 'dm') { @@ -550,7 +558,7 @@ class Util { if (message.channel.type === 'dm') return input; const role = message.guild.roles.cache.get(input.replace(/<|@|>|&/g, '')); return role ? `@${role.name}` : input; - }); + })); } /** diff --git a/test/disableMentions.js b/test/disableMentions.js new file mode 100644 index 000000000..281633d6e --- /dev/null +++ b/test/disableMentions.js @@ -0,0 +1,47 @@ +const Discord = require('../src'); +const { Util } = Discord; +const { token, prefix } = require('./auth'); + +const client = new Discord.Client({ + // To see a difference, comment out disableMentions and run the same tests using disableEveryone + // You will notice that all messages will mention @everyone + //disableEveryone: true + disableMentions: true +}); + +const tests = [ + // Test 1 + // See https://github.com/discordapp/discord-api-docs/issues/1189 + '@\u202eeveryone @\u202ehere', + + // Test 2 + // See https://github.com/discordapp/discord-api-docs/issues/1241 + // TL;DR: Characters like \u0300 will only be stripped if more than 299 are present + '\u0300@'.repeat(150) + '@\u0300everyone @\u0300here', + + // Test 3 + // Normal @everyone/@here mention + '@everyone @here', +]; + + + +client.on('ready', () => console.log('Ready!')); + +client.on('message', message => { + // Check if message starts with prefix + if (!message.content.startsWith(prefix)) return; + const [command, ...args] = message.content.substr(prefix.length).split(' '); + + // Clean content and log each character + console.log(Util.cleanContent(args.join(' '), message).split('')); + + if (command === 'test1') + message.reply(tests[0]); + else if (command === 'test2') + message.reply(tests[1]); + else if (command === 'test3') + message.reply(tests[2]); +}); + +client.login(token).catch(console.error); \ No newline at end of file diff --git a/typings/index.d.ts b/typings/index.d.ts index 95b0790c7..1b6597402 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1427,6 +1427,7 @@ declare module 'discord.js' { public static basename(path: string, ext?: string): string; public static binaryToID(num: string): Snowflake; public static cleanContent(str: string, message: Message): string; + public static removeMentions(str: string): string; public static cloneObject(obj: object): object; public static convertToBuffer(ab: ArrayBuffer | string): Buffer; public static delayFor(ms: number): Promise; @@ -2068,7 +2069,7 @@ declare module 'discord.js' { messageCacheLifetime?: number; messageSweepInterval?: number; fetchAllMembers?: boolean; - disableEveryone?: boolean; + disableMentions?: boolean; partials?: PartialTypes[]; restWsBridgeTimeout?: number; restTimeOffset?: number; @@ -2464,7 +2465,7 @@ declare module 'discord.js' { nonce?: string; content?: string; embed?: MessageEmbed | MessageEmbedOptions; - disableEveryone?: boolean; + disableMentions?: boolean; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; @@ -2693,7 +2694,7 @@ declare module 'discord.js' { tts?: boolean; nonce?: string; embeds?: (MessageEmbed | object)[]; - disableEveryone?: boolean; + disableMentions?: boolean; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions;