diff --git a/config/config.sample.yaml b/config/config.sample.yaml index edb0b2e6994a21f6a89afdd6996173cfd7512a19..1baa13ea20cf4b8bc09e46122d94388af0719be8 100644 --- a/config/config.sample.yaml +++ b/config/config.sample.yaml @@ -38,6 +38,10 @@ bridge: disableInviteNotifications: false # Auto-determine the language of code blocks (this can be CPU-intensive) determineCodeLanguage: false + # MXID of an admin user that will be PMd if the bridge experiences problems. Optional + adminMxid: '@admin:localhost' + # The message to send to the bridge admin if the Discord token is not valid + invalidTokenMessage: 'Your Discord bot token seems to be invalid, and the bridge cannot function. Please update it in your bridge settings and restart the bridge' # Authentication configuration for the discord bot. auth: # This MUST be a string (wrapped in quotes) @@ -113,4 +117,4 @@ ghosts: metrics: enable: false port: 9001 - host: "127.0.0.1" \ No newline at end of file + host: "127.0.0.1" 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/config.ts b/src/config.ts index 216d531eaa23879c7c74a0d2bfe4e3376624f2af..96ebf97a003f4219e33822cee0dc4771c347967b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -100,6 +100,8 @@ class DiscordBridgeConfigBridge { public determineCodeLanguage: boolean = false; public activityTracker: UserActivityTrackerConfig = UserActivityTrackerConfig.DEFAULT; public userLimit: number|null = null; + public adminMxid: string|null = null; + public invalidTokenMessage: string = 'Your Discord token is invalid'; } export class DiscordBridgeConfigDatabase { diff --git a/src/discordas.ts b/src/discordas.ts index c863c3e1e1605ce693e97f8649fdba973971b4c0..951b4fb9db8de3a5535143ba33eca5ce9182ca8a 100644 --- a/src/discordas.ts +++ b/src/discordas.ts @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { Appservice, IAppserviceRegistration, LogService } from "matrix-bot-sdk"; +import { Appservice, IAppserviceRegistration, LogService, MatrixClient } from "matrix-bot-sdk"; import * as yaml from "js-yaml"; import * as fs from "fs"; import { DiscordBridgeConfig } from "./config"; @@ -214,18 +214,18 @@ async function run(): Promise<void> { roomhandler.bindThirdparty(); - await appservice.begin(); - log.info(`Started listening on port ${port}`); - try { - await discordbot.init(); - await discordbot.run(); + await discordbot.start(); log.info("Discordbot started successfully"); } catch (err) { log.error(err); log.error("Failure during startup. Exiting"); process.exit(1); } + + await appservice.begin(); + log.info(`Started listening on port ${port}`); + } run().catch((err) => {