diff --git a/src/channelsyncroniser.ts b/src/channelsyncroniser.ts index 9f24d6f1d56fad4c5990ef3c7b1fcf53e417af83..8c3a434beb14a63614a35c6cbcdaa26f458d184f 100644 --- a/src/channelsyncroniser.ts +++ b/src/channelsyncroniser.ts @@ -164,6 +164,28 @@ export class ChannelSyncroniser { return rooms.map((room) => room.matrix!.getId() as string); } + public async GetAliasFromChannel(channel: Discord.Channel): Promise<string | null> { + let rooms: string[] = []; + try { + rooms = await this.GetRoomIdsFromChannel(channel); + } catch (err) { } // do nothing, our rooms array will just be empty + for (const room of rooms) { + try { + const al = (await this.bridge.getIntent().getClient() + .getStateEvent(room, "m.room.canonical_alias")).alias; + if (al) { + return al; // we are done, we found an alias + } + } catch (err) { } // do nothing, as if we error we just roll over to the next entry + } + const guildChannel = channel as Discord.TextChannel; + if (!guildChannel.guild) { + return null; // we didn't pass a guild, so we have no way of bridging this room, thus no alias + } + // at last, no known canonical aliases and we are ag uild....so we know an alias! + return `#_discord_${guildChannel.guild.id}_${channel.id}:${this.config.bridge.domain}`; + } + public async GetChannelUpdateState(channel: Discord.TextChannel, forceUpdate = false): Promise<IChannelState> { log.verbose(`State update request for ${channel.id}`); const channelState: IChannelState = Object.assign({}, DEFAULT_CHANNEL_STATE, { diff --git a/src/discordmessageprocessor.ts b/src/discordmessageprocessor.ts index d1328fe7c5aa6e11ee5fefa35d444bf444b63fc3..8e9dd555e7004de1bc79192ebdd165b0ef094573 100644 --- a/src/discordmessageprocessor.ts +++ b/src/discordmessageprocessor.ts @@ -19,18 +19,25 @@ import * as markdown from "discord-markdown"; import { DiscordBot } from "./bot"; import * as escapeHtml from "escape-html"; import { Util } from "./util"; +import { Bridge } from "matrix-appservice-bridge"; import { Log } from "./log"; const log = new Log("DiscordMessageProcessor"); const MATRIX_TO_LINK = "https://matrix.to/#/"; -const MXC_INSERT_REGEX = /\x01(\w+)\x01([01])\x01([0-9]*)\x01/g; +// somehow the regex works properly if it isn't global +// as we replace the match fully anyways this shouldn't be an issue +const MXC_INSERT_REGEX = /\x01emoji\x01(\w+)\x01([01])\x01([0-9]*)\x01/; const NAME_MXC_INSERT_REGEX_GROUP = 1; const ANIMATED_MXC_INSERT_REGEX_GROUP = 2; const ID_MXC_INSERT_REGEX_GROUP = 3; const EMOJI_SIZE = 32; const MAX_EDIT_MSG_LENGTH = 50; +// same as above, no global flag here, too +const CHANNEL_INSERT_REGEX = /\x01chan\x01([0-9]*)\x01/; +const ID_CHANNEL_INSERT_REGEX = 1; + export class DiscordMessageProcessorOpts { constructor(readonly domain: string, readonly bot?: DiscordBot) { @@ -82,10 +89,12 @@ export class DiscordMessageProcessor { }); content = this.InsertEmbeds(content, msg); content = await this.InsertMxcImages(content, msg); + content = await this.InsertChannelPills(content, msg); // parse postmark stuff contentPostmark = this.InsertEmbedsPostmark(contentPostmark, msg); contentPostmark = await this.InsertMxcImages(contentPostmark, msg, true); + contentPostmark = await this.InsertChannelPills(contentPostmark, msg, true); result.body = content; result.formattedBody = contentPostmark; @@ -221,18 +230,11 @@ export class DiscordMessageProcessor { return `<a href="${MATRIX_TO_LINK}${escapeHtml(memberId)}">${escapeHtml(memberName)}</a>`; } - public InsertChannel(node: IDiscordNode, msg: Discord.Message, html: boolean = false): string { - const id = node.id; - const channel = msg.guild.channels.get(id); - if (!channel) { - return html ? `<#${escapeHtml(id)}>` : `<#${id}>`; - } - const channelStr = escapeHtml(channel ? "#" + channel.name : "#" + id); - if (!html) { - return channelStr; - } - const roomId = escapeHtml(`#_discord_${msg.guild.id}_${id}:${this.opts.domain}`); - return `<a href="${MATRIX_TO_LINK}${roomId}">${escapeHtml(channelStr)}</a>`; + public InsertChannel(node: IDiscordNode): string { + // unfortunately these callbacks are sync, so we flag our channel with some special stuff + // and later on grab the real channel pill async + const FLAG = "\x01"; + return `${FLAG}chan${FLAG}${node.id}${FLAG}`; } public InsertRole(node: IDiscordNode, msg: Discord.Message, html: boolean = false): string { @@ -252,8 +254,7 @@ export class DiscordMessageProcessor { // unfortunately these callbacks are sync, so we flag our url with some special stuff // and later on grab the real url async const FLAG = "\x01"; - const name = escapeHtml(node.name); - return `${FLAG}${name}${FLAG}${node.animated ? 1 : 0}${FLAG}${node.id}${FLAG}`; + return `${FLAG}emoji${FLAG}${node.name}${FLAG}${node.animated ? 1 : 0}${FLAG}${node.id}${FLAG}`; } public InsertRoom(msg: Discord.Message, def: string): string { @@ -267,10 +268,12 @@ export class DiscordMessageProcessor { const animated = results[ANIMATED_MXC_INSERT_REGEX_GROUP] === "1"; const id = results[ID_MXC_INSERT_REGEX_GROUP]; let replace = ""; + const nameHtml = escapeHtml(name); try { const mxcUrl = await this.opts.bot!.GetEmoji(name, animated, id); if (html) { - replace = `<img alt="${name}" title="${name}" height="${EMOJI_SIZE}" src="${mxcUrl}" />`; + replace = `<img alt="${nameHtml}" title="${nameHtml}" ` + + `height="${EMOJI_SIZE}" src="${mxcUrl}" />`; } else { replace = `:${name}:`; } @@ -279,18 +282,39 @@ export class DiscordMessageProcessor { `Could not insert emoji ${id} for msg ${msg.id} in guild ${msg.guild.id}: ${ex}`, ); if (html) { - replace = `<${animated ? "a" : ""}:${name}:${id}>`; + replace = `<${animated ? "a" : ""}:${nameHtml}:${id}>`; } else { replace = `<${animated ? "a" : ""}:${name}:${id}>`; } } - content = content.replace(results[0], - replace); + content = content.replace(results[0], replace); results = MXC_INSERT_REGEX.exec(content); } return content; } + public async InsertChannelPills(content: string, msg: Discord.Message, html: boolean = false): Promise<string> { + let results = CHANNEL_INSERT_REGEX.exec(content); + while (results !== null) { + const id = results[ID_CHANNEL_INSERT_REGEX]; + let replace = ""; + const channel = msg.guild.channels.get(id); + if (channel) { + const alias = await this.opts.bot!.ChannelSyncroniser.GetAliasFromChannel(channel); + if (alias) { + const name = "#" + channel.name; + replace = html ? `<a href="${MATRIX_TO_LINK}${escapeHtml(alias)}">${escapeHtml(name)}</a>` : name; + } + } + if (!replace) { + replace = html ? `<#${escapeHtml(id)}>` : `<#${id}>`; + } + content = content.replace(results[0], replace); + results = CHANNEL_INSERT_REGEX.exec(content); + } + return content; + } + private isEmbedInBody(msg: Discord.Message, embed: Discord.MessageEmbed): boolean { if (!embed.url) { return false; @@ -304,8 +328,8 @@ export class DiscordMessageProcessor { private getDiscordParseCallbacks(msg: Discord.Message) { return { - channel: (node) => this.InsertChannel(node, msg), - emoji: (node) => this.InsertEmoji(node), + channel: (node) => this.InsertChannel(node), // are post-inserted + emoji: (node) => this.InsertEmoji(node), // are post-inserted everyone: (_) => this.InsertRoom(msg, "@everyone"), here: (_) => this.InsertRoom(msg, "@here"), role: (node) => this.InsertRole(node, msg), @@ -315,7 +339,7 @@ export class DiscordMessageProcessor { private getDiscordParseCallbacksHTML(msg: Discord.Message) { return { - channel: (node) => this.InsertChannel(node, msg, true), + channel: (node) => this.InsertChannel(node), // are post-inserted emoji: (node) => this.InsertEmoji(node), // are post-inserted everyone: (_) => this.InsertRoom(msg, "@everyone"), here: (_) => this.InsertRoom(msg, "@here"), diff --git a/test/test_channelsyncroniser.ts b/test/test_channelsyncroniser.ts index 7d90f6e827c782c1ca6f4ce11d618af0dd6ed124..cbc338f4dd0a326d8f1d52e828fae54b59e33e48 100644 --- a/test/test_channelsyncroniser.ts +++ b/test/test_channelsyncroniser.ts @@ -1,5 +1,5 @@ /* -Copyright 2018 matrix-appservice-discord +Copyright 2018, 2019 matrix-appservice-discord Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -83,6 +83,15 @@ function CreateChannelSync(remoteChannels: any[] = []): ChannelSyncroniser { ALIAS_DELETED = true; }, getStateEvent: async (mxid, event) => { + if (event === "m.room.canonical_alias") { + if (mxid === "!valid:localhost") { + return { + alias: "#alias:localhost", + }; + } else { + return null; + } + } return event; }, sendStateEvent: async (mxid, event, data) => { @@ -278,6 +287,43 @@ describe("ChannelSyncroniser", () => { } }); }); + describe("GetAliasFromChannel", () => { + const getIds = async (chan) => { + if (chan.id === "678") { + return ["!valid:localhost"]; + } + throw new Error("invalid"); + }; + it("Should get one canonical alias for a room", async () => { + const chan = new MockChannel(); + chan.id = "678"; + const channelSync = CreateChannelSync(); + channelSync.GetRoomIdsFromChannel = getIds; + const alias = await channelSync.GetAliasFromChannel(chan as any); + + expect(alias).to.equal("#alias:localhost"); + }); + it("Should return null if no alias found and no guild present", async () => { + const chan = new MockChannel(); + chan.id = "123"; + const channelSync = CreateChannelSync(); + channelSync.GetRoomIdsFromChannel = getIds; + const alias = await channelSync.GetAliasFromChannel(chan as any); + + expect(alias).to.equal(null); + }); + it("Should return a #_discord_ alias if a guild is present", async () => { + const chan = new MockChannel(); + const guild = new MockGuild("123"); + chan.id = "123"; + chan.guild = guild; + const channelSync = CreateChannelSync(); + channelSync.GetRoomIdsFromChannel = getIds; + const alias = await channelSync.GetAliasFromChannel(chan as any); + + expect(alias).to.equal("#_discord_123_123:localhost"); + }); + }); describe("GetChannelUpdateState", () => { it("will do nothing on no rooms", async () => { const chan = new MockChannel(); diff --git a/test/test_discordmessageprocessor.ts b/test/test_discordmessageprocessor.ts index 38649ddc80d55dcd301812710a477759d868e993..f78d66abded7dacaa4451b546796459cf7776e64 100644 --- a/test/test_discordmessageprocessor.ts +++ b/test/test_discordmessageprocessor.ts @@ -27,6 +27,14 @@ import { MockRole } from "./mocks/role"; /* tslint:disable:no-unused-expression max-file-line-count no-any */ const bot = { + ChannelSyncroniser: { + GetAliasFromChannel: async (chan) => { + if (chan.id === "456") { + return "#_discord_123_456:localhost"; + } + return null; + }, + }, GetEmoji: async (name: string, animated: boolean, id: string): Promise<string> => { if (id === "3333333") { return "mxc://image"; @@ -348,37 +356,6 @@ describe("DiscordMessageProcessor", () => { "<a href=\"https://matrix.to/#/@_discord_12345:localhost\">TestNickname</a>"); }); }); - describe("InsertChannel / HTML", () => { - it("processes unknown channel correctly", () => { - const processor = new DiscordMessageProcessor( - new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot); - const guild: any = new MockGuild("123", []); - const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"}); - const msg = new MockMessage(channel) as any; - const content = { id: "123456789" }; - let reply = processor.InsertChannel(content, msg); - Chai.assert.equal(reply, "<#123456789>"); - - reply = processor.InsertChannel(content, msg, true); - Chai.assert.equal(reply, - "<#123456789>"); - }); - it("processes channels correctly", () => { - const processor = new DiscordMessageProcessor( - new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot); - const guild: any = new MockGuild("123", []); - const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"}); - guild.channels.set("456", channel); - const msg = new MockMessage(channel) as any; - const content = { id: "456" }; - let reply = processor.InsertChannel(content, msg); - Chai.assert.equal(reply, "#TestChannel"); - - reply = processor.InsertChannel(content, msg, true); - Chai.assert.equal(reply, - "<a href=\"https://matrix.to/#/#_discord_123_456:localhost\">#TestChannel</a>"); - }); - }); describe("InsertRole / HTML", () => { it("ignores unknown roles", () => { const processor = new DiscordMessageProcessor( @@ -424,7 +401,7 @@ describe("DiscordMessageProcessor", () => { name: "blah", }; const reply = processor.InsertEmoji(content); - Chai.assert.equal(reply, "\x01blah\x010\x011234\x01"); + Chai.assert.equal(reply, "\x01emoji\x01blah\x010\x011234\x01"); }); it("inserts animated emojis to their post-parse flag", () => { const processor = new DiscordMessageProcessor( @@ -435,7 +412,18 @@ describe("DiscordMessageProcessor", () => { name: "blah", }; const reply = processor.InsertEmoji(content); - Chai.assert.equal(reply, "\x01blah\x011\x011234\x01"); + Chai.assert.equal(reply, "\x01emoji\x01blah\x011\x011234\x01"); + }); + }); + describe("InsertChannel", () => { + it("inserts channels to their post-parse flag", () => { + const processor = new DiscordMessageProcessor( + new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot); + const content = { + id: "1234", + }; + const reply = processor.InsertChannel(content); + Chai.assert.equal(reply, "\x01chan\x011234\x01"); }); }); describe("InsertMxcImages / HTML", () => { @@ -445,7 +433,7 @@ describe("DiscordMessageProcessor", () => { const guild: any = new MockGuild("123", []); const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"}); const msg = new MockMessage(channel) as any; - const content = "Hello \x01hello\x010\x01123456789\x01"; + const content = "Hello \x01emoji\x01hello\x010\x01123456789\x01"; let reply = await processor.InsertMxcImages(content, msg); Chai.assert.equal(reply, "Hello <:hello:123456789>"); @@ -459,13 +447,89 @@ describe("DiscordMessageProcessor", () => { const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"}); guild.channels.set("456", channel); const msg = new MockMessage(channel) as any; - const content = "Hello \x01hello\x010\x013333333\x01"; + const content = "Hello \x01emoji\x01hello\x010\x013333333\x01"; let reply = await processor.InsertMxcImages(content, msg); Chai.assert.equal(reply, "Hello :hello:"); reply = await processor.InsertMxcImages(content, msg, true); Chai.assert.equal(reply, "Hello <img alt=\"hello\" title=\"hello\" height=\"32\" src=\"mxc://image\" />"); }); + it("processes double-emoji correctly", async () => { + const processor = new DiscordMessageProcessor( + new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot); + const guild: any = new MockGuild("123", []); + const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"}); + guild.channels.set("456", channel); + const msg = new MockMessage(channel) as any; + const content = "Hello \x01emoji\x01hello\x010\x013333333\x01 \x01emoji\x01hello\x010\x013333333\x01"; + let reply = await processor.InsertMxcImages(content, msg); + Chai.assert.equal(reply, "Hello :hello: :hello:"); + + reply = await processor.InsertMxcImages(content, msg, true); + Chai.assert.equal(reply, "Hello <img alt=\"hello\" title=\"hello\" height=\"32\" src=\"mxc://image\" /> " + + "<img alt=\"hello\" title=\"hello\" height=\"32\" src=\"mxc://image\" />"); + }); + }); + describe("InsertChannelPills / HTML", () => { + it("processes unknown channel correctly", async () => { + const processor = new DiscordMessageProcessor( + new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot); + const guild: any = new MockGuild("123", []); + const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"}); + guild.channels.set("456", channel); + const msg = new MockMessage(channel) as any; + const content = "Hello \x01chan\x013333333\x01"; + let reply = await processor.InsertChannelPills(content, msg); + Chai.assert.equal(reply, "Hello <#3333333>"); + + reply = await processor.InsertChannelPills(content, msg, true); + Chai.assert.equal(reply, "Hello <#3333333>"); + }); + it("processes channels correctly", async () => { + const processor = new DiscordMessageProcessor( + new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot); + const guild: any = new MockGuild("123", []); + const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"}); + guild.channels.set("456", channel); + const msg = new MockMessage(channel) as any; + const content = "Hello \x01chan\x01456\x01"; + let reply = await processor.InsertChannelPills(content, msg); + Chai.assert.equal(reply, "Hello #TestChannel"); + + reply = await processor.InsertChannelPills(content, msg, true); + Chai.assert.equal(reply, "Hello <a href=\"https://matrix.to/#/#_discord_123" + + "_456:localhost\">#TestChannel</a>"); + }); + it("processes multiple channels correctly", async () => { + const processor = new DiscordMessageProcessor( + new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot); + const guild: any = new MockGuild("123", []); + const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"}); + guild.channels.set("456", channel); + const msg = new MockMessage(channel) as any; + const content = "Hello \x01chan\x01456\x01 \x01chan\x01456\x01"; + let reply = await processor.InsertChannelPills(content, msg); + Chai.assert.equal(reply, "Hello #TestChannel #TestChannel"); + + reply = await processor.InsertChannelPills(content, msg, true); + Chai.assert.equal(reply, "Hello <a href=\"https://matrix.to/#/#_discord_123" + + "_456:localhost\">#TestChannel</a> <a href=\"https://matrix.to/#/#_discord_123" + + "_456:localhost\">#TestChannel</a>"); + }); + it("processes channels without alias correctly", async () => { + const processor = new DiscordMessageProcessor( + new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot); + const guild: any = new MockGuild("123", []); + const channel = new Discord.TextChannel(guild, {id: "678", name: "TestChannel"}); + guild.channels.set("678", channel); + const msg = new MockMessage(channel) as any; + const content = "Hello \x01chan\x01678\x01"; + let reply = await processor.InsertChannelPills(content, msg); + Chai.assert.equal(reply, "Hello <#678>"); + + reply = await processor.InsertChannelPills(content, msg, true); + Chai.assert.equal(reply, "Hello <#678>"); + }); }); describe("InsertEmbeds", () => { it("processes titleless embeds properly", () => {