diff --git a/package.json b/package.json index 1d441dab479c90f3c8d22e5ab21932498b48ed57..7bd522618090879c60f047c62c0de2b9d86d7a5c 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "escape-string-regexp": "^1.0.5", "js-yaml": "^3.10.0", "marked": "^0.3.15", - "matrix-appservice-bridge": "^1.5.0a", + "matrix-appservice-bridge": "^1.7.0", "mime": "^1.6.0", "moment": "^2.22.2", "pg-promise": "^8.5.1", diff --git a/src/bot.ts b/src/bot.ts index ec0dc412c35eb6498b67a3761170e33df64c89a8..deed4d7e4a38bd2d9b5a68eb405405d8e67935b9 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -60,7 +60,7 @@ export class DiscordBot { public setBridge(bridge: Bridge) { this.bridge = bridge; this.mxEventProcessor = new MatrixEventProcessor( - new MatrixEventProcessorOpts(this.config, bridge), + new MatrixEventProcessorOpts(this.config, bridge, this), ); } @@ -235,7 +235,8 @@ export class DiscordBot { log.warn(`User ${event.sender} has no member state. That's odd.`); } } - const embed = this.mxEventProcessor.EventToEmbed(event, profile, chan); + const embedSet = await this.mxEventProcessor.EventToEmbed(event, profile, chan); + const embed = embedSet.messageEmbed; const opts: Discord.MessageOptions = {}; const file = await this.mxEventProcessor.HandleAttachment(event, mxClient); if (typeof(file) === "string") { @@ -260,14 +261,20 @@ export class DiscordBot { } try { if (!botUser) { + opts.embed = embedSet.replyEmbed; msg = await chan.send(embed.description, opts); } else if (hook) { msg = await hook.send(embed.description, { username: embed.author.name, avatarURL: embed.author.icon_url, - file: opts.file, - }); + files: opts.file ? [opts.file] : undefined, + embeds: embedSet.replyEmbed ? [embedSet.replyEmbed] : undefined, + } as any); } else { + if (embedSet.replyEmbed) { + embed.addField("Replying to", embedSet.replyEmbed.author.name); + embed.addField("Reply text", embedSet.replyEmbed.description); + } opts.embed = embed; msg = await chan.send("", opts); } @@ -324,6 +331,20 @@ export class DiscordBot { return false; } + public GetDiscordUserOrMember( + userId: Discord.Snowflake, guildId?: Discord.Snowflake, + ): Promise<Discord.User|Discord.GuildMember> { + try { + if (guildId && this.bot.guilds.has(guildId)) { + return this.bot.guilds.get(guildId).fetchMember(userId); + } + return this.bot.fetchUser(userId); + } catch (ex) { + log.warn(`Could not fetch user data for ${userId} (guild: ${guildId})`); + return undefined; + } + } + public GetChannelFromRoomId(roomId: string): Promise<Discord.Channel> { return this.bridge.getRoomStore().getEntriesByMatrixId( roomId, diff --git a/src/matrixeventprocessor.ts b/src/matrixeventprocessor.ts index 1ec23a3074f74e723b7400b3179eb859e9dc8e72..93db39566c3f7a91b6f5039cd1c9fa65fb61330a 100644 --- a/src/matrixeventprocessor.ts +++ b/src/matrixeventprocessor.ts @@ -6,6 +6,7 @@ import * as escapeStringRegexp from "escape-string-regexp"; import {Util} from "./util"; import * as path from "path"; import * as mime from "mime"; +import {MatrixUser} from "matrix-appservice-bridge"; import { Log } from "./log"; const log = new Log("MatrixEventProcessor"); @@ -19,18 +20,26 @@ export class MatrixEventProcessorOpts { constructor( readonly config: DiscordBridgeConfig, readonly bridge: any, + readonly discord: DiscordBot, ) { } } +export interface IMatrixEventProcessorResult { + messageEmbed: Discord.RichEmbed; + replyEmbed?: Discord.RichEmbed; +} + export class MatrixEventProcessor { private config: DiscordBridgeConfig; private bridge: any; + private discord: DiscordBot; constructor (opts: MatrixEventProcessorOpts) { this.config = opts.config; this.bridge = opts.bridge; + this.discord = opts.discord; } public StateEventToMessage(event: any, channel: Discord.TextChannel): string { @@ -71,7 +80,9 @@ export class MatrixEventProcessor { return msg; } - public EventToEmbed(event: any, profile: any|null, channel: Discord.TextChannel): Discord.RichEmbed { + public async EventToEmbed( + event: any, profile: any|null, channel: Discord.TextChannel, + ): Promise<IMatrixEventProcessorResult> { let body = this.config.bridge.disableDiscordMentions ? event.content.body : this.FindMentionsInPlainBody( event.content.body, @@ -116,28 +127,14 @@ export class MatrixEventProcessor { // Handle discord custom emoji body = this.ReplaceDiscordEmoji(body, channel.guild); - let displayName = event.sender; - let avatarUrl = undefined; - if (profile) { - if (profile.displayname && - profile.displayname.length >= MIN_NAME_LENGTH && - profile.displayname.length <= MAX_NAME_LENGTH) { - displayName = profile.displayname; - } - - if (profile.avatar_url) { - const mxClient = this.bridge.getClientFactory().getClientAs(); - avatarUrl = mxClient.mxcUrlToHttp(profile.avatar_url); - } - } - return new Discord.RichEmbed({ - author: { - name: displayName.substr(0, MAX_NAME_LENGTH), - icon_url: avatarUrl, - url: `https://matrix.to/#/${event.sender}`, - }, - description: body, - }); + const messageEmbed = new Discord.RichEmbed(); + const replyEmbedAndBody = await this.GetEmbedForReply(event); + messageEmbed.setDescription(replyEmbedAndBody ? replyEmbedAndBody[1] : body); + await this.SetEmbedAuthor(messageEmbed, event.sender, profile); + return { + messageEmbed, + replyEmbed: replyEmbedAndBody ? replyEmbedAndBody[0] : undefined, + }; } public FindMentionsInPlainBody(body: string, members: Discord.GuildMember[]): string { @@ -212,6 +209,95 @@ export class MatrixEventProcessor { return `[${name}](${url})`; } + public async GetEmbedForReply(event: any): Promise<[Discord.RichEmbed, string]|undefined> { + const relatesTo = event.content["m.relates_to"]; + let eventId = null; + if (relatesTo && relatesTo["m.in_reply_to"]) { + eventId = relatesTo["m.in_reply_to"].event_id; + } else { + return; + } + let reponseText = Util.GetReplyFromReplyBody(event.content.body || ""); + if (reponseText === "") { + reponseText = "Reply with unknown content"; + } + + const intent = this.bridge.getIntent(); + const embed = new Discord.RichEmbed(); + // Try to get the event. + try { + const sourceEvent = await intent.getEvent(event.room_id, eventId); + let replyText = sourceEvent.content.body || "Reply with unknown content"; + // Check if this is also a reply. + if (sourceEvent.content && sourceEvent.content["m.relates_to"] && + sourceEvent.content["m.relates_to"]["m.in_reply_to"]) { + replyText = Util.GetReplyFromReplyBody(sourceEvent.content.body); + } + embed.setDescription(replyText); + await this.SetEmbedAuthor( + embed, + sourceEvent.sender, + ); + } catch (ex) { + log.warn("Failed to handle reply, showing a unknown embed:", ex); + // For some reason we failed to get the event, so using fallback. + embed.setDescription("Reply with unknown content"); + embed.setAuthor("Unknown"); + } + return [embed, reponseText]; + } + + private async SetEmbedAuthor(embed: Discord.RichEmbed, sender: string, profile?: any) { + const intent = this.bridge.getIntent(); + let displayName = sender; + let avatarUrl = undefined; + + // Are they a discord user. + if (this.bridge.getBot().isRemoteUser(sender)) { + const localpart = new MatrixUser(sender.replace("@", "")).localpart; + const userOrMember = await this.discord.GetDiscordUserOrMember(localpart.substring("_discord".length)); + if (userOrMember instanceof Discord.User) { + embed.setAuthor( + userOrMember.username, + userOrMember.avatarURL, + ); + return; + } else if (userOrMember instanceof Discord.GuildMember) { + embed.setAuthor( + userOrMember.displayName, + userOrMember.user.avatarURL, + ); + return; + } + // Let it fall through. + } + if (profile === undefined) { + try { + profile = await intent.getProfileInfo(sender); + } catch (ex) { + log.warn(`Failed to fetch profile for ${sender}`, ex); + } + } + + if (profile) { + if (profile.displayname && + profile.displayname.length >= MIN_NAME_LENGTH && + profile.displayname.length <= MAX_NAME_LENGTH) { + displayName = profile.displayname; + } + + if (profile.avatar_url) { + const mxClient = this.bridge.getClientFactory().getClientAs(); + avatarUrl = mxClient.mxcUrlToHttp(profile.avatar_url); + } + } + embed.setAuthor( + displayName.substr(0, MAX_NAME_LENGTH), + avatarUrl, + `https://matrix.to/#/${sender}`, + ); + } + private GetFilenameForMediaEvent(content: any): string { if (content.body) { if (path.extname(content.body) !== "") { diff --git a/src/matrixroomhandler.ts b/src/matrixroomhandler.ts index e627fc9cc463581996b9045b800496a33554b1e5..4fc96a4d476e63bc4cf6cae8e47c497a378fc88d 100644 --- a/src/matrixroomhandler.ts +++ b/src/matrixroomhandler.ts @@ -109,7 +109,7 @@ export class MatrixRoomHandler { if (event.type === "m.room.member" && event.content.membership === "invite") { return this.HandleInvite(event); } else if (event.type === "m.room.member" && event.content.membership === "join") { - if (this.bridge.getBot()._isRemoteUser(event.state_key)) { + if (this.bridge.getBot().isRemoteUser(event.state_key)) { return this.discord.UserSyncroniser.OnMemberState(event, USERSYNC_STATE_DELAY_MS); } else { return this.discord.ProcessMatrixStateEvent(event); diff --git a/src/util.ts b/src/util.ts index 61edd636899ea5d785d5717a794313566e7f288e..6346ffe5c140b625fff72acb24f751b95f57c6ae 100644 --- a/src/util.ts +++ b/src/util.ts @@ -218,7 +218,7 @@ export class Util { params[param] = await parameters[param].get(args[i]); i++; } - + const retStr = await action.run(params); return retStr; } @@ -237,6 +237,17 @@ export class Util { } return {command, args}; } + + public static GetReplyFromReplyBody(body: string) { + const lines = body.split("\n"); + while (lines[0].startsWith("> ") || lines[0].trim().length === 0) { + lines.splice(0, 1); + if (lines.length === 0) { + return ""; + } + } + return lines.join("\n").trim(); + } } interface IUploadResult { diff --git a/test/test_matrixeventprocessor.ts b/test/test_matrixeventprocessor.ts index 60b803a6352fe8ad47c7a77299ffcb38feea9c21..e70d16950bdd7e4c710576ee279c97b1b6eade71 100644 --- a/test/test_matrixeventprocessor.ts +++ b/test/test_matrixeventprocessor.ts @@ -45,6 +45,11 @@ function createMatrixEventProcessor }, }; }, + getBot: () => { + return { + isRemoteUser: () => false, + }; + }, getIntent: () => { return { getClient: () => { @@ -54,6 +59,47 @@ function createMatrixEventProcessor }, }; }, + getEvent: async (_, eventId: string) => { + if (eventId === "$goodEvent:localhost") { + return { + sender: "@doggo:localhost", + content: { + body: "Hello!", + }, + }; + } else if (eventId === "$reply:localhost") { + return { + sender: "@doggo:localhost", + content: { + "body": `> <@doggo:localhost> This is the original body + +This is the first reply`, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$goodEvent:localhost", + }, + }, + }, + }; + } else if (eventId === "$nontext:localhost") { + return { + sender: "@doggo:localhost", + content: { + something: "not texty", + }, + }; + } + return null; + }, + getProfileInfo: async (userId: string) => { + if (userId !== "@doggo:localhost") { + return null; + } + return { + displayname: "Doggo!", + avatar_url: "mxc://fakeurl.com", + }; + }, }; }, }; @@ -61,19 +107,23 @@ function createMatrixEventProcessor config.bridge.disableDiscordMentions = disableMentions; config.bridge.disableEveryoneMention = disableEveryone; config.bridge.disableHereMention = disableHere; + + const Util = Object.assign(require("../src/util").Util, { + DownloadFile: (name: string) => { + const size = parseInt(name.substring(name.lastIndexOf("/") + 1), undefined); + return Buffer.alloc(size); + }, + }); + return new (Proxyquire("../src/matrixeventprocessor", { "./util": { - Util: { - DownloadFile: (name: string) => { - const size = parseInt(name.substring(name.lastIndexOf("/") + 1), undefined); - return Buffer.alloc(size); - }, - }, + Util, }, })).MatrixEventProcessor( new MatrixEventProcessorOpts( config, bridge, + null, )); } const mockChannel = new MockChannel(); @@ -203,9 +253,9 @@ describe("MatrixEventProcessor", () => { }); }); describe("EventToEmbed", () => { - it("Should contain a profile.", () => { + it("Should contain a profile.", async () => { const processor = createMatrixEventProcessor(); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@test:localhost", content: { body: "testcontent", @@ -214,53 +264,57 @@ describe("MatrixEventProcessor", () => { displayname: "Test User", avatar_url: "mxc://localhost/avatarurl", }, mockChannel as any); - Chai.assert.equal(evt.author.name, "Test User"); - Chai.assert.equal(evt.author.icon_url, "https://localhost/avatarurl"); - Chai.assert.equal(evt.author.url, "https://matrix.to/#/@test:localhost"); + const author = embeds.messageEmbed.author; + Chai.assert.equal(author.name, "Test User"); + Chai.assert.equal(author.icon_url, "https://localhost/avatarurl"); + Chai.assert.equal(author.url, "https://matrix.to/#/@test:localhost"); }); - it("Should contain the users displayname if it exists.", () => { + it("Should contain the users displayname if it exists.", async () => { const processor = createMatrixEventProcessor(); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@test:localhost", content: { body: "testcontent", }, }, { displayname: "Test User"}, mockChannel as any); - Chai.assert.equal(evt.author.name, "Test User"); - Chai.assert.isUndefined(evt.author.icon_url); - Chai.assert.equal(evt.author.url, "https://matrix.to/#/@test:localhost"); + const author = embeds.messageEmbed.author; + Chai.assert.equal(author.name, "Test User"); + Chai.assert.isUndefined(author.icon_url); + Chai.assert.equal(author.url, "https://matrix.to/#/@test:localhost"); }); - it("Should contain the users userid if the displayname is not set", () => { + it("Should contain the users userid if the displayname is not set", async () => { const processor = createMatrixEventProcessor(); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@test:localhost", content: { body: "testcontent", }, }, null, mockChannel as any); - Chai.assert.equal(evt.author.name, "@test:localhost"); - Chai.assert.isUndefined(evt.author.icon_url); - Chai.assert.equal(evt.author.url, "https://matrix.to/#/@test:localhost"); + const author = embeds.messageEmbed.author; + Chai.assert.equal(author.name, "@test:localhost"); + Chai.assert.isUndefined(author.icon_url); + Chai.assert.equal(author.url, "https://matrix.to/#/@test:localhost"); }); - it("Should use the userid when the displayname is too short", () => { + it("Should use the userid when the displayname is too short", async () => { const processor = createMatrixEventProcessor(); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@test:localhost", content: { body: "testcontent", }, }, { displayname: "t"}, mockChannel as any); - Chai.assert.equal(evt.author.name, "@test:localhost"); + const author = embeds.messageEmbed.author; + Chai.assert.equal(author.name, "@test:localhost"); }); - it("Should use the userid when displayname is too long", () => { + it("Should use the userid when displayname is too long", async () => { const processor = createMatrixEventProcessor(); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@test:localhost", content: { body: "testcontent", @@ -268,78 +322,81 @@ describe("MatrixEventProcessor", () => { }, { displayname: "this is a very very long displayname that should be capped", }, mockChannel as any); - Chai.assert.equal(evt.author.name, "@test:localhost"); + const author = embeds.messageEmbed.author; + Chai.assert.equal(author.name, "@test:localhost"); }); - it("Should cap the sender name if it is too long", () => { + it("Should cap the sender name if it is too long", async () => { const processor = createMatrixEventProcessor(); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@testwithalottosayaboutitselfthatwillgoonandonandonandon:localhost", content: { body: "testcontent", }, }, null, mockChannel as any); - Chai.assert.equal(evt.author.name, "@testwithalottosayaboutitselftha"); + const author = embeds.messageEmbed.author; + Chai.assert.equal(author.name, "@testwithalottosayaboutitselftha"); }); - it("Should contain the users avatar if it exists.", () => { + it("Should contain the users avatar if it exists.", async () => { const processor = createMatrixEventProcessor(); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@test:localhost", content: { body: "testcontent", }, }, {avatar_url: "mxc://localhost/test"}, mockChannel as any); - Chai.assert.equal(evt.author.name, "@test:localhost"); - Chai.assert.equal(evt.author.icon_url, "https://localhost/test"); - Chai.assert.equal(evt.author.url, "https://matrix.to/#/@test:localhost"); + const author = embeds.messageEmbed.author; + Chai.assert.equal(author.name, "@test:localhost"); + Chai.assert.equal(author.icon_url, "https://localhost/test"); + Chai.assert.equal(author.url, "https://matrix.to/#/@test:localhost"); }); - it("Should enable mentions if configured.", () => { + it("Should enable mentions if configured.", async () => { const processor = createMatrixEventProcessor(); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@test:localhost", content: { body: "@testuser2 Hello!", }, }, {avatar_url: "test"}, mockChannel as any); - Chai.assert.equal(evt.description, "<@!12345> Hello!"); + Chai.assert.equal(embeds.messageEmbed.description, "<@!12345> Hello!"); }); - it("Should disable mentions if configured.", () => { + it("Should disable mentions if configured.", async () => { const processor = createMatrixEventProcessor(true); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@test:localhost", content: { body: "@testuser2 Hello!", }, }, {avatar_url: "test"}, mockChannel as any); - Chai.assert.equal(evt.description, "@testuser2 Hello!"); + Chai.assert.equal(embeds.messageEmbed.description, "@testuser2 Hello!"); }); - it("Should remove everyone mentions if configured.", () => { + it("Should remove everyone mentions if configured.", async () => { const processor = createMatrixEventProcessor(false, true); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@test:localhost", content: { body: "@everyone Hello!", }, }, {avatar_url: "test"}, mockChannel as any); - Chai.assert.equal(evt.description, "@ everyone Hello!"); + Chai.assert.equal(embeds.messageEmbed.description, "@ everyone Hello!"); }); - it("Should remove here mentions if configured.", () => { + it("Should remove here mentions if configured.", async () => { const processor = createMatrixEventProcessor(false, false, true); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@test:localhost", content: { body: "@here Hello!", }, }, {avatar_url: "test"}, mockChannel as any); - Chai.assert.equal(evt.description, "@ here Hello!"); + Chai.assert.equal(embeds.messageEmbed.description, "@ here Hello!"); }); - it("Should process custom discord emojis.", () => { + it("Should process custom discord emojis.", async () => { const processor = createMatrixEventProcessor(false, false, true); const mockEmoji = new MockEmoji("123", "supercake"); const mockCollectionEmojis = new MockCollection<string, MockEmoji>(); @@ -348,16 +405,19 @@ describe("MatrixEventProcessor", () => { const mockChannelEmojis = new MockChannel("test", { emojis: mockCollectionEmojis, }); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@test:localhost", content: { body: "I like :supercake:", }, }, {avatar_url: "test"}, mockChannelEmojis as any); - Chai.assert.equal(evt.description, "I like <:supercake:123>"); + Chai.assert.equal( + embeds.messageEmbed.description, + "I like <:supercake:123>", + ); }); - it("Should not process invalid custom discord emojis.", () => { + it("Should not process invalid custom discord emojis.", async () => { const processor = createMatrixEventProcessor(false, false, true); const mockEmoji = new MockEmoji("123", "supercake"); const mockCollectionEmojis = new MockCollection<string, MockEmoji>(); @@ -366,17 +426,20 @@ describe("MatrixEventProcessor", () => { const mockChannelEmojis = new MockChannel("test", { emojis: mockCollectionEmojis, }); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@test:localhost", content: { body: "I like :lamecake:", }, }, {avatar_url: "test"}, mockChannelEmojis as any); - Chai.assert.equal(evt.description, "I like :lamecake:"); + Chai.assert.equal( + embeds.messageEmbed.description, + "I like :lamecake:", + ); }); - it("Should replace /me with * displayname, and italicize message", () => { + it("Should replace /me with * displayname, and italicize message", async () => { const processor = createMatrixEventProcessor(); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@test:localhost", content: { body: "likes puppies", @@ -385,11 +448,14 @@ describe("MatrixEventProcessor", () => { }, { displayname: "displayname", }, mockChannel as any); - Chai.assert.equal(evt.description, "*displayname likes puppies*"); + Chai.assert.equal( + embeds.messageEmbed.description, + "*displayname likes puppies*", + ); }); - it("Should handle stickers.", () => { + it("Should handle stickers.", async () => { const processor = createMatrixEventProcessor(); - const evt = processor.EventToEmbed({ + const embeds = await processor.EventToEmbed({ sender: "@test:localhost", type: "m.sticker", content: { @@ -397,7 +463,7 @@ describe("MatrixEventProcessor", () => { url: "mxc://bunny", }, }, {avatar_url: "test"}, mockChannel as any); - Chai.assert.equal(evt.description, ""); + Chai.assert.equal(embeds.messageEmbed.description, ""); }); }); describe("FindMentionsInPlainBody", () => { @@ -603,4 +669,125 @@ describe("MatrixEventProcessor", () => { }); }); }); + describe("GetEmbedForReply", () => { + it("should handle reply-less events", async () => { + const processor = createMatrixEventProcessor(); + const result = await processor.GetEmbedForReply({ + sender: "@test:localhost", + type: "m.room.message", + content: { + body: "Test", + }, + }); + expect(result).to.be.undefined; + }); + it("should handle replies without a fallback", async () => { + const processor = createMatrixEventProcessor(); + const result = await processor.GetEmbedForReply({ + sender: "@test:localhost", + type: "m.room.message", + content: { + "body": "Test", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$goodEvent:localhost", + }, + }, + }, + }); + expect(result[0].description).to.be.equal("Hello!"); + expect(result[0].author.name).to.be.equal("Doggo!"); + expect(result[0].author.icon_url).to.be.equal("https://fakeurl.com"); + expect(result[0].author.url).to.be.equal("https://matrix.to/#/@doggo:localhost"); + expect(result[1]).to.be.equal("Test"); + }); + it("should handle replies with a missing event", async () => { + const processor = createMatrixEventProcessor(); + const result = await processor.GetEmbedForReply({ + sender: "@test:localhost", + type: "m.room.message", + content: { + "body": `> <@doggo:localhost> This is the fake body + +This is where the reply goes`, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$event:thing", + }, + }, + }, + }); + expect(result[0].description).to.be.equal("Reply with unknown content"); + expect(result[0].author.name).to.be.equal("Unknown"); + expect(result[0].author.icon_url).to.be.undefined; + expect(result[0].author.url).to.be.undefined; + expect(result[1]).to.be.equal("This is where the reply goes"); + }); + it("should handle replies with a valid reply event", async () => { + const processor = createMatrixEventProcessor(); + const result = await processor.GetEmbedForReply({ + sender: "@test:localhost", + type: "m.room.message", + content: { + "body": `> <@doggo:localhost> This is the original body + +This is where the reply goes`, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$goodEvent:localhost", + }, + }, + }, + }); + expect(result[0].description).to.be.equal("Hello!"); + expect(result[0].author.name).to.be.equal("Doggo!"); + expect(result[0].author.icon_url).to.be.equal("https://fakeurl.com"); + expect(result[0].author.url).to.be.equal("https://matrix.to/#/@doggo:localhost"); + expect(result[1]).to.be.equal("This is where the reply goes"); + }); + it("should handle replies on top of replies", async () => { + const processor = createMatrixEventProcessor(); + const result = await processor.GetEmbedForReply({ + sender: "@test:localhost", + type: "m.room.message", + content: { + "body": `> <@doggo:localhost> This is the first reply + +This is the second reply`, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$reply:localhost", + }, + }, + }, + }); + expect(result[0].description).to.be.equal("This is the first reply"); + expect(result[0].author.name).to.be.equal("Doggo!"); + expect(result[0].author.icon_url).to.be.equal("https://fakeurl.com"); + expect(result[0].author.url).to.be.equal("https://matrix.to/#/@doggo:localhost"); + expect(result[1]).to.be.equal("This is the second reply"); + }); + it("should handle replies with non text events", async () => { + const processor = createMatrixEventProcessor(); + const result = await processor.GetEmbedForReply({ + sender: "@test:localhost", + type: "m.room.message", + content: { + "body": `> <@doggo:localhost> sent an image. + +This is the reply`, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$nontext:localhost", + }, + }, + }, + }); + expect(result[0].description).to.be.equal("Reply with unknown content"); + expect(result[0].author.name).to.be.equal("Doggo!"); + expect(result[0].author.icon_url).to.be.equal("https://fakeurl.com"); + expect(result[0].author.url).to.be.equal("https://matrix.to/#/@doggo:localhost"); + expect(result[1]).to.be.equal("This is the reply"); + }); + }); }); diff --git a/test/test_matrixroomhandler.ts b/test/test_matrixroomhandler.ts index 0bfefe5e45be2aab7401b65d23c24f451da4f639..aa2b5f641b5c483040e7600d8a9fdd9537dae00c 100644 --- a/test/test_matrixroomhandler.ts +++ b/test/test_matrixroomhandler.ts @@ -66,7 +66,7 @@ function createRH(opts: any = {}) { }, getBot: () => { return { - _isRemoteUser: (id) => { + isRemoteUser: (id) => { return id !== undefined && id.startsWith("@_discord_"); }, }; diff --git a/test/test_util.ts b/test/test_util.ts index ccc810b872048f81b49b5092d0572b2632a38631..dc48346bf48a8c85bbeaf673c49eb804f24590ec 100644 --- a/test/test_util.ts +++ b/test/test_util.ts @@ -116,4 +116,28 @@ describe("Util", () => { return expect(Util.GetMxidFromName(intent, "badboy", ["abc"])).to.eventually.be.rejected; }); }); + describe("GetReplyFromReplyBody", () => { + it("Should get a reply from the body", () => { + const reply = Util.GetReplyFromReplyBody(`> <@alice:example.org> This is the original body + +This is where the reply goes`); + return expect(reply).to.equal("This is where the reply goes"); + }); + it("Should get a multi-line reply from the body", () => { + const reply = Util.GetReplyFromReplyBody(`> <@alice:example.org> This is the original body + +This is where the reply goes and +there are even more lines here.`); + return expect(reply).to.equal("This is where the reply goes and\nthere are even more lines here."); + }); + it("Should get empty string from an empty reply", () => { + const reply = Util.GetReplyFromReplyBody(`> <@alice:example.org> This is the original body +`); + return expect(reply).to.equal(""); + }); + it("Should return body if no reply found", () => { + const reply = Util.GetReplyFromReplyBody("Test\nwith\nhalfy"); + return expect(reply).to.equal("Test\nwith\nhalfy"); + }); + }); });