diff --git a/src/bot.ts b/src/bot.ts index 58702274186002d05688938437b2c48fa895c904..73c0345ad87f8b733fe848901127159b6a7baed2 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 abc0a68a43246c94d69d935552f84727ae71cf57..e8e30c383794d1616ef3a4fafd97e40894d48ecc 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);