diff --git a/src/bot.ts b/src/bot.ts index deed4d7e4a38bd2d9b5a68eb405405d8e67935b9..83e1dfa7bb7cc7d1b10f0a04a281cb136cf9dd10 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -46,6 +46,9 @@ export class DiscordBot { private channelSync: ChannelSyncroniser; private roomHandler: MatrixRoomHandler; + /* Handles messages queued up to be sent to discord. */ + private discordMessageQueue: { [channelId: string]: Promise<any> }; + constructor(config: DiscordBridgeConfig, store: DiscordStore, private provisioner: Provisioner) { this.config = config; this.store = store; @@ -55,6 +58,7 @@ export class DiscordBot { new MessageProcessorOpts(this.config.bridge.domain, this), ); this.presenceHandler = new PresenceHandler(this); + this.discordMessageQueue = {}; } public setBridge(bridge: Bridge) { @@ -103,11 +107,24 @@ export class DiscordBot { client.on("guildUpdate", (_, newGuild) => { this.channelSync.OnGuildUpdate(newGuild); }); client.on("guildDelete", (guild) => { this.channelSync.OnGuildDelete(guild); }); - client.on("messageDelete", (msg) => { this.DeleteDiscordMessage(msg); }); - client.on("messageUpdate", (oldMessage, newMessage) => { this.OnMessageUpdate(oldMessage, newMessage); }); - client.on("message", (msg) => { Bluebird.delay(MSG_PROCESS_DELAY).then(() => { - this.OnMessage(msg); - }); + client.on("messageDelete", (msg: Discord.Message) => { + this.discordMessageQueue[msg.channel.id] = Promise.all([ + this.discordMessageQueue[msg.channel.id] || Promise.resolve(), + Bluebird.delay(this.config.limits.discordSendDelay), + ]).then(() => this.DeleteDiscordMessage(msg)); + }); + client.on("messageUpdate", (oldMessage: Discord.Message, newMessage: Discord.Message) => { + this.discordMessageQueue[newMessage.channel.id] = Promise.all([ + this.discordMessageQueue[newMessage.channel.id] || Promise.resolve(), + Bluebird.delay(this.config.limits.discordSendDelay), + ]).then(() => this.OnMessageUpdate(oldMessage, newMessage)); + }); + client.on("message", (msg: Discord.Message) => { + this.discordMessageQueue[msg.channel.id] = (async () => { + await (this.discordMessageQueue[msg.channel.id] || Promise.resolve()); + await Bluebird.delay(this.config.limits.discordSendDelay); + await this.OnMessage(msg); + })(); }); const jsLog = new Log("discord.js"); @@ -486,7 +503,7 @@ export class DiscordBot { } // Update presence because sometimes discord misses people. - this.userSync.OnUpdateUser(msg.author).then(() => { + return this.userSync.OnUpdateUser(msg.author).then(() => { return this.channelSync.GetRoomIdsFromChannel(msg.channel).catch((err) => { log.verbose("No bridged rooms to send message to. Oh well."); return null; @@ -576,18 +593,18 @@ export class DiscordBot { } } - private async DeleteDiscordMessage(msg: Discord.Message) { - log.info(`Got delete event for ${msg.id}`); - const storeEvent = await this.store.Get(DbEvent, {discord_id: msg.id}); - if (!storeEvent.Result) { - log.warn(`Could not redact because the event was not in the store.`); - return; - } - while (storeEvent.Next()) { - log.info(`Deleting discord msg ${storeEvent.DiscordId}`); - const intent = this.GetIntentFromDiscordMember(msg.author); - const matrixIds = storeEvent.MatrixId.split(";"); - await intent.getClient().redactEvent(matrixIds[1], matrixIds[0]); - } + private async DeleteDiscordMessage(msg: Discord.Message) { + log.info(`Got delete event for ${msg.id}`); + const storeEvent = await this.store.Get(DbEvent, {discord_id: msg.id}); + if (!storeEvent.Result) { + log.warn(`Could not redact because the event was not in the store.`); + return; } + while (storeEvent.Next()) { + log.info(`Deleting discord msg ${storeEvent.DiscordId}`); + const intent = this.GetIntentFromDiscordMember(msg.author); + const matrixIds = storeEvent.MatrixId.split(";"); + await intent.getClient().redactEvent(matrixIds[1], matrixIds[0]); + } + } } diff --git a/src/config.ts b/src/config.ts index bb35047882be464802f00288eeeb7d8d87ee0b5b..ca04a5ebe9a4158267aede7f8c73bc23915c032f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -78,6 +78,7 @@ class DiscordBridgeConfigChannelDeleteOptions { class DiscordBridgeConfigLimits { public roomGhostJoinDelay: number = 6000; + public discordSendDelay: number = 750; } export class LoggingFile { diff --git a/test/mocks/discordclient.ts b/test/mocks/discordclient.ts index d431ebf1dd0b0a768d691aeeac08560148ef8a24..20da7ba2e72ef8d2dcbb5b801e2b5acfcce10b44 100644 --- a/test/mocks/discordclient.ts +++ b/test/mocks/discordclient.ts @@ -1,12 +1,13 @@ import {MockCollection} from "./collection"; import {MockGuild} from "./guild"; import {MockUser} from "./user"; +import { EventEmitter } from "events"; export class MockDiscordClient { public guilds = new MockCollection<string, MockGuild>(); public user: MockUser; private testLoggedIn: boolean = false; - private testCallbacks: Map<string, () => void> = new Map(); + private testCallbacks: Map<string, (...data: any[]) => void> = new Map(); constructor() { const channels = [ @@ -30,10 +31,14 @@ export class MockDiscordClient { this.user = new MockUser("12345"); } - public on(event: string, callback: () => void) { + public on(event: string, callback: (...data: any[]) => void) { this.testCallbacks.set(event, callback); } + public emit(event: string, ...data: any[]) { + return this.testCallbacks.get(event).apply(this, data); + } + public async login(token: string): Promise<void> { if (token !== "passme") { throw new Error("Mock Discord Client only logins with the token 'passme'"); diff --git a/test/mocks/discordclientfactory.ts b/test/mocks/discordclientfactory.ts index 99bb5b2e47e6fd4ffc5becc86a9e5f1a6082cff6..5248acbffdf67cdb359bf65c9ce9fafc48561fa1 100644 --- a/test/mocks/discordclientfactory.ts +++ b/test/mocks/discordclientfactory.ts @@ -1,6 +1,7 @@ import {MockDiscordClient} from "./discordclient"; export class DiscordClientFactory { + private botClient: MockDiscordClient = null; constructor(config: any, store: any) { ; } @@ -10,6 +11,9 @@ export class DiscordClientFactory { } public getClient(userId?: string): Promise<MockDiscordClient> { - return Promise.resolve(new MockDiscordClient()); + if (userId == null && !this.botClient){ + this.botClient = new MockDiscordClient(); + } + return Promise.resolve(this.botClient); } } diff --git a/test/test_discordbot.ts b/test/test_discordbot.ts index 1233fb299e4437b5b30d6de931c14851c4bf703a..aa21e3a19b6f4bba231572dccd0d8384b2cda594 100644 --- a/test/test_discordbot.ts +++ b/test/test_discordbot.ts @@ -7,6 +7,8 @@ import { Log } from "../src/log"; import { MessageProcessorMatrixResult } from "../src/messageprocessor"; import { MockGuild } from "./mocks/guild"; import { MockMember } from "./mocks/member"; +import { DiscordBot } from "../src/bot"; +import { MockDiscordClient } from "./mocks/discordclient"; Chai.use(ChaiAsPromised); @@ -52,6 +54,9 @@ describe("DiscordBot", () => { bridge: { domain: "localhost", }, + limits: { + discordSendDelay: 50, + } }; describe("run()", () => { it("should resolve when ready.", () => { @@ -140,6 +145,28 @@ describe("DiscordBot", () => { }); }); }); + describe("event:message", () => { + it("should delay messages so they arrive in order", async () => { + discordBot = new modDiscordBot.DiscordBot( + config, + mockBridge, + ); + let expected = 0; + discordBot.OnMessage = (msg: any) => { + assert.equal(msg.n, expected); + expected++; + return Promise.resolve() + }; + const client: MockDiscordClient = (await discordBot.ClientFactory.getClient()) as MockDiscordClient; + discordBot.setBridge(mockBridge); + await discordBot.run(); + // Send delay of 50ms, 2 seconds / 50ms - 5 for safety. + for (let i = 0; i < (2000 / 50) - 5; i++) { + client.emit("message", { n: i, channel: { id: 123} }); + } + await discordBot.discordMessageQueue[123]; + }); + }); // describe("ProcessMatrixMsgEvent()", () => { //