diff --git a/src/bot.ts b/src/bot.ts index b0eff028f22b8ade91b144516a9a88af8f6cd2b3..8ddcf134cc2f83e7401d34c98f50a44d73b8fe1d 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -5,7 +5,7 @@ import { DbEmoji } from "./db/dbdataemoji"; import { DbEvent } from "./db/dbdataevent"; import { MatrixUser, RemoteUser, Bridge, Entry } from "matrix-appservice-bridge"; import { Util } from "./util"; -import { MessageProcessor, MessageProcessorOpts } from "./messageprocessor"; +import { MessageProcessor, MessageProcessorOpts, MessageProcessorMatrixResult } from "./messageprocessor"; import { MatrixEventProcessor, MatrixEventProcessorOpts } from "./matrixeventprocessor"; import { PresenceHandler } from "./presencehandler"; import * as Discord from "discord.js"; @@ -80,7 +80,8 @@ export class DiscordBot { client.on("guildMemberAdd", (newMember) => { this.AddGuildMember(newMember); }); client.on("guildMemberRemove", (oldMember) => { this.RemoveGuildMember(oldMember); }); client.on("guildMemberUpdate", (_, newMember) => { this.UpdateGuildMember(newMember); }); - client.on("messageDelete", (msg) => {this.DeleteDiscordMessage(msg); }); + client.on("messageUpdate", (oldMessage, newMessage) => { this.OnMessageUpdate(oldMessage, newMessage); }); + client.on("messageDelete", (msg) => { this.DeleteDiscordMessage(msg); }); client.on("message", (msg) => { Bluebird.delay(MSG_PROCESS_DELAY).then(() => { this.OnMessage(msg); }); @@ -434,6 +435,32 @@ export class DiscordBot { }); } + private async SendMatrixMessage(matrixMsg: MessageProcessorMatrixResult, chan: Discord.Channel, + guild: Discord.Guild, author: Discord.User, + msgID: string): Promise<boolean> { + const rooms = await this.GetRoomIdsFromChannel(chan); + const intent = this.GetIntentFromDiscordMember(author); + + rooms.forEach((room) => { + intent.sendMessage(room, { + body: matrixMsg.body, + msgtype: "m.text", + formatted_body: matrixMsg.formattedBody, + format: "org.matrix.custom.html", + }).then((res) => { + const evt = new DbEvent(); + evt.MatrixId = res.event_id + ";" + room; + evt.DiscordId = msgID; + evt.ChannelId = chan.id; + evt.GuildId = guild.id; + this.store.Insert(evt); + }); + }); + + // Sending was a success + return true; + } + private AddGuildMember(guildMember: Discord.GuildMember) { return this.GetRoomIdsFromGuild(guildMember.guild.id).then((roomIds) => { return this.InitJoinUser(guildMember, roomIds); @@ -586,6 +613,21 @@ export class DiscordBot { }); } + private async OnMessageUpdate(oldMsg: Discord.Message, newMsg: Discord.Message) { + // Check if an edit was actually made + if (oldMsg.content === newMsg.content) { + return; + } + + // Create a new edit message using the old and new message contents + const editedMsg = await this.msgProcessor.FormatEdit(oldMsg, newMsg); + + // Send the message to all bridged matrix rooms + if (!await this.SendMatrixMessage(editedMsg, newMsg.channel, newMsg.guild, newMsg.author, newMsg.id)) { + log.error("DiscordBot", "Unable to announce message edit for msg id:", newMsg.id); + } + } + private async DeleteDiscordMessage(msg: Discord.Message) { log.info("DiscordBot", `Got delete event for ${msg.id}`); const storeEvent = await this.store.Get(DbEvent, {discord_id: msg.id}); diff --git a/src/messageprocessor.ts b/src/messageprocessor.ts index 6dac01a2fc0eb803ce06111fb6144c835c6f1c51..f02893357d4fd442546ed0a3beb85fae02053163 100644 --- a/src/messageprocessor.ts +++ b/src/messageprocessor.ts @@ -71,6 +71,12 @@ export class MessageProcessor { return result; } + public async FormatEdit(oldMsg: Discord.Message, newMsg: Discord.Message): Promise<MessageProcessorMatrixResult> { + // TODO: Produce a nice, colored diff between the old and new message content + oldMsg.content = "*edit:* ~~" + oldMsg.content + "~~ -> " + newMsg.content; + return this.FormatDiscordMessage(oldMsg); + } + 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. diff --git a/test/test_discordbot.ts b/test/test_discordbot.ts index 95855e72dd60790d02f1a5682af28348c05351e9..45f820d8383fda07abc9167208c491d6bfda78ff 100644 --- a/test/test_discordbot.ts +++ b/test/test_discordbot.ts @@ -1,8 +1,13 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; import * as Proxyquire from "proxyquire"; +import * as Discord from "discord.js"; import * as log from "npmlog"; +import { MessageProcessorMatrixResult } from "../src/messageprocessor"; +import { MockGuild } from "./mocks/guild"; +import { MockMember } from "./mocks/member"; + Chai.use(ChaiAsPromised); log.level = "silent"; @@ -76,6 +81,62 @@ describe("DiscordBot", () => { return assert.isFulfilled(discordBot.LookupRoom("123", "321")); }); }); + describe("OnMessageUpdate()", () => { + it("should return on an unchanged message", () => { + discordBot = new modDiscordBot.DiscordBot( + config, + mockBridge, + ); + + const guild: any = new MockGuild("123", []); + guild._mockAddMember(new MockMember("12345", "TestUsername")); + const channel = new Discord.TextChannel(guild, null); + const oldMsg = new Discord.Message(channel, null, null); + const newMsg = new Discord.Message(channel, null, null); + oldMsg.embeds = []; + newMsg.embeds = []; + + // Content updated but not changed + oldMsg.content = "a"; + newMsg.content = "a"; + + // Mock the SendMatrixMessage method to check if it is called + let checkMsgSent = false; + discordBot.SendMatrixMessage = (...args) => checkMsgSent = true; + + discordBot.OnMessageUpdate(oldMsg, newMsg).then(() => { + Chai.assert.equal(checkMsgSent, false); + }); + }); + + it("should send a matrix message on an edited discord message", () => { + discordBot = new modDiscordBot.DiscordBot( + config, + mockBridge, + ); + + const guild: any = new MockGuild("123", []); + guild._mockAddMember(new MockMember("12345", "TestUsername")); + const channel = new Discord.TextChannel(guild, null); + const oldMsg = new Discord.Message(channel, null, null); + const newMsg = new Discord.Message(channel, null, null); + oldMsg.embeds = []; + newMsg.embeds = []; + + // Content updated and edited + oldMsg.content = "a"; + newMsg.content = "b"; + + // Mock the SendMatrixMessage method to check if it is called + let checkMsgSent = false; + discordBot.SendMatrixMessage = (...args) => checkMsgSent = true; + + discordBot.OnMessageUpdate(oldMsg, newMsg).then(() => { + Chai.assert.equal(checkMsgSent, true); + }); + }); + }); + // describe("ProcessMatrixMsgEvent()", () => { // // }); diff --git a/test/test_messageprocessor.ts b/test/test_messageprocessor.ts index 063a29d80d226e4ca9dde2557ebc5ced1c0c87c4..d054b1e5d4f9dac0751dc8a7787a0873d6d12f96 100644 --- a/test/test_messageprocessor.ts +++ b/test/test_messageprocessor.ts @@ -51,6 +51,42 @@ describe("MessageProcessor", () => { Chai.assert.equal(result.formattedBody, "<p>Hello <em>World</em>!</p>\n"); }); }); + describe("FormatEdit", () => { + it("should format basic edits appropriately", async () => { + const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), <DiscordBot> bot); + const oldMsg = new Discord.Message(null, null, null); + const newMsg = new Discord.Message(null, null, null); + oldMsg.embeds = []; + newMsg.embeds = []; + + // Content updated but not changed + oldMsg.content = "a"; + newMsg.content = "b"; + + const result = await processor.FormatEdit(oldMsg, newMsg); + Chai.assert.equal(result.body, "*edit:* ~~a~~ -> b"); + Chai.assert.equal(result.formattedBody, "<p><em>edit:</em> <del>a</del> -> b</p>\n"); + }); + + it("should format markdown heavy edits apropriately", async () => { + const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), <DiscordBot> bot); + const oldMsg = new Discord.Message(null, null, null); + const newMsg = new Discord.Message(null, null, null); + oldMsg.embeds = []; + newMsg.embeds = []; + + // Content updated but not changed + oldMsg.content = "a slice of **cake**"; + newMsg.content = "*a* slice of cake"; + + const result = await processor.FormatEdit(oldMsg, newMsg); + Chai.assert.equal(result.body, "*edit:* ~~a slice of **cake**~~ -> *a* slice of cake"); + Chai.assert.equal(result.formattedBody, "<p><em>edit:</em> <del>a slice of <strong>" + + "cake</strong></del> -> <em>a</em> slice of cake</p>\n"); + }); + + }); + describe("ReplaceMembers", () => { it("processes members missing from the guild correctly", () => { const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), <DiscordBot> bot);