From d8c3be72d11079d6bb0da7df8af3d6c2b396351c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tadeusz=20So=C5=9Bnierz?= <tadeusz@sosnierz.com>
Date: Mon, 22 Nov 2021 17:32:43 +0100
Subject: [PATCH] Encapsulate bridge's connect-retry loop as DiscordBot.start()

---
 src/bot.ts       | 85 +++++++++++++++++++++++++++++++++++++++++++++++-
 src/discordas.ts | 75 +-----------------------------------------
 2 files changed, 85 insertions(+), 75 deletions(-)

diff --git a/src/bot.ts b/src/bot.ts
index 5870227..73c0345 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -31,7 +31,7 @@ import { Log } from "./log";
 import * as Discord from "better-discord.js";
 import * as mime from "mime";
 import { IMatrixEvent, IMatrixMediaInfo, IMatrixMessage } from "./matrixtypes";
-import { Appservice, Intent } from "matrix-bot-sdk";
+import { Appservice, Intent, MatrixClient } from "matrix-bot-sdk";
 import { DiscordCommandHandler } from "./discordcommandhandler";
 import { MetricPeg } from "./metrics";
 import { Lock } from "./structures/lock";
@@ -44,6 +44,10 @@ const MIN_PRESENCE_UPDATE_DELAY = 250;
 const TYPING_TIMEOUT_MS = 30 * 1000;
 const CACHE_LIFETIME = 90000;
 
+// how often do we retry to connect on startup
+const INITIAL_FALLOFF_SECONDS = 5;
+const MAX_FALLOFF_SECONDS = 5 * 60; // 5 minutes
+
 // TODO: This is bad. We should be serving the icon from the own homeserver.
 const MATRIX_ICON_URL = "https://matrix.org/_matrix/media/r0/download/matrix.org/mlxoESwIsTbJrfXyAAogrNxA";
 class ChannelLookupResult {
@@ -128,6 +132,7 @@ export class DiscordBot {
         private config: DiscordBridgeConfig,
         private bridge: Appservice,
         private store: DiscordStore,
+        private adminNotifier?: AdminNotifier,
     ) {
 
         // create handlers
@@ -146,6 +151,12 @@ export class DiscordBot {
         this.discordMessageQueue = {};
         this.channelLock = new Lock(this.config.limits.discordSendDelay);
         this.lastEventIds = {};
+
+        if (!this.adminNotifier && config.bridge.adminMxid) {
+            this.adminNotifier = new AdminNotifier(
+                this.bridge.botClient, config.bridge.adminMxid
+            );
+        }
     }
 
     get ClientFactory(): DiscordClientFactory {
@@ -387,6 +398,31 @@ export class DiscordBot {
         }
     }
 
+    public async start(): Promise<void> {
+        return this._start(INITIAL_FALLOFF_SECONDS);
+    }
+
+    private async _start(falloffSeconds: number, isRetry = false): Promise<void> {
+        try {
+            await this.init();
+            await this.run();
+        } catch (err) {
+            if (err.code === 'TOKEN_INVALID' && !isRetry) {
+                await this.adminNotifier?.notify(this.config.bridge.invalidTokenMessage);
+            }
+
+            // no more than 5 minutes
+            const newFalloffSeconds = Math.min(falloffSeconds * 2, MAX_FALLOFF_SECONDS);
+            log.error(`Failed do start Discordbot: ${err.code}. Will try again in ${newFalloffSeconds} seconds`);
+            await new Promise((r, _) => setTimeout(r, newFalloffSeconds * 1000));
+            return this._start(newFalloffSeconds, true);
+        }
+
+        if (isRetry) {
+            await this.adminNotifier?.notify(`The token situation is now resolved and the bridge is running correctly`);
+        }
+    }
+
     public async stop(): Promise<void> {
         this._bot = undefined;
     }
@@ -1203,3 +1239,50 @@ export class DiscordBot {
         MetricPeg.get.setRemoteMonthlyActiveUsers(state.activeUsers);
     }
 }
+
+class AdminNotifier {
+    constructor(
+        private client:    MatrixClient,
+        private adminMxid: string,
+    ) {}
+
+    public async notify(message: string) {
+        const roomId = await this.ensureDMRoom(this.adminMxid);
+        await this.client.sendText(roomId, message)
+    }
+
+    private async findDMRoom(targetMxid: string): Promise<string|undefined> {
+        const rooms = await this.client.getJoinedRooms();
+        const roomsWithMembers = await Promise.all(rooms.map(async (id) => {
+            return {
+                id,
+                memberships: await this.client.getRoomMembers(id, undefined, ['join', 'invite']),
+            }
+        }));
+
+        return roomsWithMembers.find(
+            room => room.memberships.length == 2
+                 && !!room.memberships.find(member => member.stateKey === targetMxid)
+        )?.id;
+    }
+
+    private async ensureDMRoom(mxid: string): Promise<string> {
+        const existing = await this.findDMRoom(mxid);
+        if (existing) {
+            log.verbose(`Found existing DM room with ${mxid}: ${existing}`);
+            return existing;
+        }
+
+        const roomId = await this.client.createRoom();
+        try {
+            await this.client.inviteUser(mxid, roomId);
+        } catch (err) {
+            log.verbose(`Failed to invite ${mxid} to ${roomId}, cleaning up`);
+            this.client.leaveRoom(roomId); // no point awaiting it, nothing we can do if we fail
+            throw err;
+        }
+
+        log.verbose(`Created ${roomId} to DM with ${mxid}`);
+        return roomId;
+    }
+}
diff --git a/src/discordas.ts b/src/discordas.ts
index abc0a68..e8e30c3 100644
--- a/src/discordas.ts
+++ b/src/discordas.ts
@@ -215,7 +215,7 @@ async function run(): Promise<void> {
     roomhandler.bindThirdparty();
 
     try {
-        await startDiscordBot(discordbot, appservice.botClient, config);
+        await discordbot.start();
         log.info("Discordbot started successfully");
     } catch (err) {
         log.error(err);
@@ -228,79 +228,6 @@ async function run(): Promise<void> {
 
 }
 
-async function findDMRoom(client: MatrixClient, targetMxid: string): Promise<string|undefined> {
-    const rooms = await client.getJoinedRooms();
-    const roomsWithMembers = await Promise.all(rooms.map(async (id) => {
-        return {
-            id,
-            memberships: await client.getRoomMembers(id, undefined, ['join', 'invite']),
-        }
-    }));
-
-    return roomsWithMembers.find(
-        room => room.memberships.length == 2
-             && !!room.memberships.find(member => member.stateKey === targetMxid)
-    )?.id;
-}
-
-async function ensureDMRoom(client: MatrixClient, mxid: string): Promise<string> {
-    const existing = await findDMRoom(client, mxid);
-    if (existing) {
-        log.verbose(`Found existing DM room with ${mxid}: ${existing}`);
-        return existing;
-    }
-
-    const roomId = await client.createRoom();
-    try {
-        await client.inviteUser(mxid, roomId);
-    } catch (err) {
-        log.verbose(`Failed to invite ${mxid} to ${roomId}, cleaning up`);
-        client.leaveRoom(roomId); // no point awaiting it, nothing we can do if we fail
-        throw err;
-    }
-
-    log.verbose(`Created ${roomId} to DM with ${mxid}`);
-    return roomId;
-}
-
-async function notifyBridgeAdmin(client: MatrixClient, adminMxid: string, message: string) {
-    const roomId = await ensureDMRoom(client, adminMxid);
-    await client.sendText(roomId, message)
-}
-
-let adminNotified = false;
-
-async function startDiscordBot(
-    discordbot: DiscordBot,
-    client: MatrixClient,
-    config: DiscordBridgeConfig,
-    falloffSeconds = 5
-) {
-    const adminMxid = config.bridge.adminMxid;
-
-    try {
-        await discordbot.init();
-        await discordbot.run();
-    } catch (err) {
-        if (err.code === 'TOKEN_INVALID' && adminMxid && !adminNotified) {
-            await notifyBridgeAdmin(client, adminMxid, config.bridge.invalidTokenMessage);
-            adminNotified = true;
-        }
-
-        // no more than 5 minutes
-        const newFalloffSeconds = Math.min(falloffSeconds * 2, 5 * 60);
-        log.error(`Failed do start Discordbot: ${err.code}. Will try again in ${newFalloffSeconds} seconds`);
-        await new Promise((r, _) => setTimeout(r, newFalloffSeconds * 1000));
-        return startDiscordBot(discordbot, client, config, newFalloffSeconds);
-    }
-
-    if (adminMxid && adminNotified) {
-        await notifyBridgeAdmin(client, adminMxid, `The token situation is now resolved and the bridge is running correctly`);
-        adminNotified = false;
-    }
-}
-
-
 run().catch((err) => {
     log.error("A fatal error occurred during startup:", err);
     process.exit(1);
-- 
GitLab