import * as Discord from "discord.js"; import * as marked from "marked"; import * as log from "npmlog"; import { DiscordBot } from "./bot"; import * as escapeStringRegexp from "escape-string-regexp"; const USER_REGEX = /<@!?([0-9]*)>/g; const CHANNEL_REGEX = /<#?([0-9]*)>/g; const EMOJI_SIZE = "1em"; const EMOJI_REGEX = /<:\w+:?([0-9]*)>/g; const MATRIX_TO_LINK = "https://matrix.to/#/"; marked.setOptions({ sanitize: true, }); export class MessageProcessorOpts { public domain: string; constructor (domain: string) { this.domain = domain; } } export class MessageProcessorMatrixResult { public formattedBody: string; public body: string; } export class MessageProcessor { private readonly opts: MessageProcessorOpts; private readonly bot: DiscordBot; constructor (opts: MessageProcessorOpts, bot: DiscordBot) { this.opts = opts; this.bot = bot; } public async FormatDiscordMessage(msg: Discord.Message): Promise<MessageProcessorMatrixResult> { const result = new MessageProcessorMatrixResult(); // Replace embeds. let content = msg.content; content = this.InsertEmbeds(content, msg); // Replace Users content = this.ReplaceMembers(content, msg); content = this.ReplaceChannels(content, msg); content = await this.ReplaceEmoji(content, msg); // Replace channels result.body = content; result.formattedBody = marked(content); return result; } public InsertEmbeds(content: string, msg: Discord.Message): string { for (const embed of msg.embeds) { let embedContent = "\n\n----"; // Horizontal rule. Two to make sure the content doesn't become a title. const embedTitle = embed.url ? `[${embed.title}](${embed.url})` : embed.title; if (embedTitle) { embedContent += "\n##### " + embedTitle; // h5 is probably best. } if (embed.description) { embedContent += "\n" + embed.description; } content += embedContent; } return content; } public ReplaceMembers(content: string, msg: Discord.Message): string { let results = USER_REGEX.exec(content); while (results !== null) { const id = results[1]; const member = msg.guild.members.get(id); const memberId = `@_discord_${id}:${this.opts.domain}`; const memberStr = member ? member.user.username : memberId; content = content.replace(results[0], memberStr); results = USER_REGEX.exec(content); } return content; } public ReplaceChannels(content: string, msg: Discord.Message): string { let results = CHANNEL_REGEX.exec(content); while (results !== null) { const id = results[1]; const channel = msg.guild.channels.get(id); const roomId = `#_discord_${msg.guild.id}_${id}:${this.opts.domain}`; const channelStr = channel ? "#" + channel.name : "#" + id; content = content.replace(results[0], `[${channelStr}](${MATRIX_TO_LINK}${roomId})`); results = CHANNEL_REGEX.exec(content); } return content; } public async ReplaceEmoji(content: string, msg: Discord.Message): Promise<string> { let results = EMOJI_REGEX.exec(content); while (results !== null) { const id = results[1]; try { const mxcUrl = await this.bot.GetGuildEmoji(msg.guild, id); content = content.replace(results[0], `<img alt="${id}" src="${mxcUrl}" style="height: ${EMOJI_SIZE};"/>`); } catch (ex) { log.warn("MessageProcessor", `Could not insert emoji ${id} for msg ${msg.id} in guild ${msg.guild.id}: ${ex}`, ); } results = EMOJI_REGEX.exec(content); } return content; } public FindMentionsInPlainBody(body: string, members: Discord.GuildMember[]): string { for (const member of members) { const matcher = escapeStringRegexp(member.user.username + "#" + member.user.discriminator) + "|" + escapeStringRegexp(member.displayName); body = body.replace( new RegExp( `\\b(${matcher})(?=\\b)` , "mig"), `<@!${member.id}>`, ); } return body; } }