diff --git a/app.js b/app.js index 53ddba339..0a1687590 100644 --- a/app.js +++ b/app.js @@ -5,6 +5,7 @@ const main = require("./lib/main"); const path = require("path"); const REG_PATH = "appservice-registration-irc.yaml"; +let bridge; new Cli({ registrationPath: REG_PATH, enableRegistration: true, @@ -33,7 +34,10 @@ new Cli({ requestTimeoutSeconds: 60 * 5 } } - } + }, + }, + onConfigChanged: function(config) { + bridge.onConfigChanged(config); }, generateRegistration: function(reg, callback) { try { @@ -49,7 +53,9 @@ new Cli({ if (port === -1) { port = null; } - const bridge = main.runBridge(port, config, reg).catch(function(err) { + main.runBridge(port, config, reg).then((resultBridge) => { + bridge = resultBridge; + }).catch(function(err) { log.error("Failed to run bridge."); log.error(err); process.exit(1); @@ -58,7 +64,7 @@ new Cli({ process.on("SIGTERM", async () => { log.info("SIGTERM recieved, killing bridge"); try { - await main.killBridge(await bridge); + await main.killBridge(bridge); } catch (ex) { log.error("Failed to killBridge"); diff --git a/changelog.d/1145.feature b/changelog.d/1145.feature new file mode 100644 index 000000000..b51255751 --- /dev/null +++ b/changelog.d/1145.feature @@ -0,0 +1 @@ +Add support for reconfiguring the bridge at runtime by sending a `SIGHUP` diff --git a/changelog.d/1146.bugfix b/changelog.d/1146.bugfix new file mode 100644 index 000000000..0d0e65273 --- /dev/null +++ b/changelog.d/1146.bugfix @@ -0,0 +1 @@ +Fix more cases of double bridged users diff --git a/changelog.d/1148.misc b/changelog.d/1148.misc new file mode 100644 index 000000000..ce5ded637 --- /dev/null +++ b/changelog.d/1148.misc @@ -0,0 +1 @@ +Add index to client_config for `config->>username` to speed up username lookups diff --git a/changelog.d/1151.bugfix b/changelog.d/1151.bugfix new file mode 100644 index 000000000..e4cb38b16 --- /dev/null +++ b/changelog.d/1151.bugfix @@ -0,0 +1 @@ +Fix a bug where a user leaving with a reason would cause them to join then leave diff --git a/config.sample.yaml b/config.sample.yaml index 184443cb5..6d7c25ca4 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -479,10 +479,6 @@ ircService: eventCacheSize: 4096 ircHandler: - # How many /leave requests can be ongoing at a time. - # This is used to stem the flow of requests in case of a mass quit/leave, which might - # slow down the homeserver. - leaveConcurrency: 10 # Should we attempt to match an IRC side mention (nickaname match) # with the nickname's owner's matrixId, if we are bridging them? # "on" - Defaults to enabled, users can choose to disable. diff --git a/config.schema.yml b/config.schema.yml index 09b4a2c68..0f11b0c91 100644 --- a/config.schema.yml +++ b/config.schema.yml @@ -124,8 +124,6 @@ properties: ircHandler: type: "object" properties: - leaveConcurrency: - type: "integer" mapIrcMentionsToMatrix: type: "string" enum: ["on", "off", "force-off"] diff --git a/package-lock.json b/package-lock.json index d84b635ac..6de100e28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2324,9 +2324,9 @@ } }, "matrix-appservice": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/matrix-appservice/-/matrix-appservice-0.5.0.tgz", - "integrity": "sha512-TzfoXdZCFbwnjHFwkmpxWEDN0SWu4Sgho8Jaa47wTei+Kg8/hLCc/JvRN3AXNtx419+tolsXYX8KO0JJorQSpA==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/matrix-appservice/-/matrix-appservice-0.6.0.tgz", + "integrity": "sha512-MkM4PzBeNBi/AcSDWgUD6zWlGY2ckBSF3dn8IgXWf44uHDx6WZfWyIt4E/KKsx9iy3EO78IfWmK+7Qva4eug5A==", "requires": { "@types/express": "^4.17.8", "body-parser": "^1.19.0", @@ -2364,16 +2364,16 @@ } }, "matrix-appservice-bridge": { - "version": "2.0.0-rc1", - "resolved": "https://registry.npmjs.org/matrix-appservice-bridge/-/matrix-appservice-bridge-2.0.0-rc1.tgz", - "integrity": "sha512-LrqqXGAlEPZ8OV56xsSABnFvljJCAXVyZ0yqCWDrBMEKfHFmSZ+Bdnzq7H17s3PcBfxJTDJaxxErVb/k5zd9yA==", + "version": "2.2.0-rc2", + "resolved": "https://registry.npmjs.org/matrix-appservice-bridge/-/matrix-appservice-bridge-2.2.0-rc2.tgz", + "integrity": "sha512-sQ6QzGqom+Q2Nv3YAinCOp+aLMG/kGm1Q9btT9pnSvQ4ESDqCPA6dAvtFh+vkoS0I9VcX1l4e4r73pdj2u0xRA==", "requires": { "chalk": "^4.1.0", "extend": "^3.0.2", "is-my-json-valid": "^2.20.5", "js-yaml": "^3.14.0", - "matrix-appservice": "^0.5.0", - "matrix-js-sdk": "^8.0.1", + "matrix-appservice": "^0.6.0", + "matrix-js-sdk": "^8.4.1", "nedb": "^1.8.0", "nopt": "^4.0.3", "p-queue": "^6.6.0", @@ -2383,11 +2383,10 @@ }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -2604,19 +2603,26 @@ } }, "matrix-js-sdk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-8.3.0.tgz", - "integrity": "sha512-ndKedUtZt72/4KWjlMevNwNDGfhPTOn/i4U6Iv1ZEfm7uZfbp5u3hVIyr8tyOiVuvMIxmcTajRdwSlRsNtYFkA==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-8.5.0.tgz", + "integrity": "sha512-RJCqK/QkesL+63rX4C4mShEFw/ZR20D1UMw1RKY8pQhv1/7Skz+v5BTv/UCqG45E3rRYMarNOwy13CZ+yq33FA==", "requires": { - "@babel/runtime": "^7.8.3", + "@babel/runtime": "^7.11.2", "another-json": "^0.2.0", "browser-request": "^0.3.3", "bs58": "^4.0.1", - "content-type": "^1.0.2", - "loglevel": "^1.6.4", - "qs": "^6.5.2", - "request": "^2.88.0", - "unhomoglyph": "^1.0.2" + "content-type": "^1.0.4", + "loglevel": "^1.7.0", + "qs": "^6.9.4", + "request": "^2.88.2", + "unhomoglyph": "^1.0.6" + }, + "dependencies": { + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + } } }, "matrix-lastactive": { diff --git a/package.json b/package.json index 79c8fb8c0..54ce327b4 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,8 @@ "irc": "matrix-org/node-irc#9028c2197c216dd8e6fc2cb3cc07ce2d6bf741a7", "js-yaml": "^3.2.7", "logform": "^2.1.2", - "matrix-appservice": "^0.5.0", - "matrix-appservice-bridge": "2.0.0-rc1", + "matrix-appservice": "^0.6.0", + "matrix-appservice-bridge": "2.2.0-rc2", "matrix-lastactive": "^0.1.5", "nedb": "^1.1.2", "nopt": "^3.0.1", diff --git a/spec/integ/invite-rooms.spec.js b/spec/integ/invite-rooms.spec.js index 5ad026234..5da9bb9cf 100644 --- a/spec/integ/invite-rooms.spec.js +++ b/spec/integ/invite-rooms.spec.js @@ -3,44 +3,55 @@ const envBundle = require("../util/env-bundle"); describe("Invite-only rooms", function() { const {env, config, roomMapping, botUserId, test} = envBundle(); - let testUser = { + const testUser = { id: "@flibble:wibble", nick: "flibble" }; - let testIrcUser = { + const testIrcUser = { localpart: roomMapping.server + "_foobar", id: "@" + roomMapping.server + "_foobar:" + config.homeserver.domain, nick: "foobar" }; - beforeEach(test.coroutine(function*() { - yield test.beforeEach(env); + beforeEach(async () => { + await test.beforeEach(env); env.ircMock._autoConnectNetworks( roomMapping.server, roomMapping.botNick, roomMapping.server ); // do the init - yield test.initEnv(env); - })); + await test.initEnv(env); + }); - afterEach(test.coroutine(function*() { - yield test.afterEach(env); - })); + afterEach(async () => { + await test.afterEach(env); + }); - it("should be joined by the bot if the AS does know the room ID", - function(done) { - let adminRoomId = "!adminroom:id"; - let sdk = env.clientMock._client(botUserId); + it("should be joined by the bot if the AS does know the room ID", async() => { + const adminRoomId = "!adminroom:id"; + const sdk = env.clientMock._client(botUserId); let joinRoomCount = 0; - sdk.joinRoom.and.callFake(function(roomId) { + sdk.joinRoom.and.callFake(async (roomId) => { expect(roomId).toEqual(adminRoomId); joinRoomCount += 1; - return Promise.resolve({}); + return {room_id: roomId}; }); - env.mockAppService._trigger("type:m.room.member", { + await env.mockAppService._trigger("type:m.room.member", { + content: { + membership: "invite", + is_direct: true, + }, + state_key: botUserId, + user_id: testUser.id, + room_id: adminRoomId, + type: "m.room.member" + }); + expect(joinRoomCount).toEqual(1, "Failed to join admin room"); + // inviting them AGAIN to an existing known ADMIN room should trigger a join + await env.mockAppService._trigger("type:m.room.member", { content: { membership: "invite", is_direct: true, @@ -49,26 +60,8 @@ describe("Invite-only rooms", function() { user_id: testUser.id, room_id: adminRoomId, type: "m.room.member" - }).then(function() { - expect(joinRoomCount).toEqual(1, "Failed to join admin room"); - // inviting them AGAIN to an existing known ADMIN room should trigger a join - return env.mockAppService._trigger("type:m.room.member", { - content: { - membership: "invite", - is_direct: true, - }, - state_key: botUserId, - user_id: testUser.id, - room_id: adminRoomId, - type: "m.room.member" - }); - }).then(function() { - expect(joinRoomCount).toEqual(2, "Failed to join admin room again"); - done(); - }, function(err) { - expect(true).toBe(false, "Failed to join admin room again: " + err); - done(); }); + expect(joinRoomCount).toEqual(2, "Failed to join admin room again"); }); it("should be joined by a virtual IRC user if the bot invited them, " + diff --git a/src/bridge/IrcBridge.ts b/src/bridge/IrcBridge.ts index 21b7ceb9d..08309c327 100644 --- a/src/bridge/IrcBridge.ts +++ b/src/bridge/IrcBridge.ts @@ -184,7 +184,58 @@ export class IrcBridge { homeserverToken, httpMaxSizeBytes: (this.config.advanced || { }).maxTxnSize || TXN_SIZE_DEFAULT, }); + } + + public async onConfigChanged(newConfig: BridgeConfig) { + log.info(`Bridge config was reloaded, applying changes`); + const oldConfig = this.config; + + if (oldConfig.advanced.maxHttpSockets !== newConfig.advanced.maxHttpSockets) { + const maxSockets = (newConfig.advanced || {maxHttpSockets: 1000}).maxHttpSockets; + require("http").globalAgent.maxSockets = maxSockets; + require("https").globalAgent.maxSockets = maxSockets; + log.info(`Adjusted max sockets to ${maxSockets}`); + } + + // We can't modify the maximum payload size after starting the http listener for the bridge, so + // newConfig.advanced.maxTxnSize is ignored. + + if (oldConfig.homeserver.dropMatrixMessagesAfterSecs !== newConfig.homeserver.dropMatrixMessagesAfterSecs) { + oldConfig.homeserver.dropMatrixMessagesAfterSecs = newConfig.homeserver.dropMatrixMessagesAfterSecs; + log.info(`Adjusted dropMatrixMessagesAfterSecs to ${newConfig.homeserver.dropMatrixMessagesAfterSecs}`); + } + + if (oldConfig.homeserver.media_url !== newConfig.homeserver.media_url) { + oldConfig.homeserver.media_url = newConfig.homeserver.media_url; + log.info(`Adjusted media_url to ${newConfig.homeserver.media_url}`); + } + + this.ircHandler.onConfigChanged(newConfig.ircHandler || {}); + + const hasLoggingChanged = JSON.stringify(oldConfig.ircService.logging) + !== JSON.stringify(newConfig.ircService.logging); + if (hasLoggingChanged) { + Logging.configure(newConfig.ircService.logging); + } + await this.dataStore.removeConfigMappings(); + + // All config mapped channels will be briefly unavailable + await Promise.all(this.ircServers.map(async (server) => { + let newServerConfig = newConfig.ircService.servers[server.domain]; + if (!newServerConfig) { + log.warn(`Server ${server.domain} removed from config. Bridge will need to be restarted`); + return; + } + newServerConfig = extend( + true, {}, IrcServer.DEFAULT_CONFIG, newConfig.ircService.servers[server.domain] + ); + server.reconfigure(newServerConfig, newConfig.homeserver.dropMatrixMessagesAfterSecs); + await this.dataStore.setServerFromConfig(server, newServerConfig); + })); + + await this.fetchJoinedRooms(); + await this.joinMappedMatrixRooms(); } private initialiseMetrics(bindPort: number) { @@ -499,16 +550,12 @@ export class IrcBridge { const allUsers = await this.dataStore.getAllUserIds(); const bot = this.bridge.getBot(); allUsers.filter((u) => bot.isRemoteUser(u)) - .forEach((u) => this.membershipCache.setMemberEntry("", u, "join")); + .forEach((u) => this.membershipCache.setMemberEntry("", u, "join", {})); log.info("Fetching Matrix rooms that are already joined to..."); await this.fetchJoinedRooms(); - for (const roomId of this.joinedRoomList) { - this.membershipCache.setMemberEntry(roomId, this.appServiceUserId, "join"); - } - if (this.config.ircService.bridgeInfoState?.enabled) { this.bridgeStateSyncer = new BridgeStateSyncer(this.dataStore, this.bridge, this); if (this.config.ircService.bridgeInfoState.initial) { diff --git a/src/bridge/IrcHandler.ts b/src/bridge/IrcHandler.ts index 59add6319..0e6a4d5be 100644 --- a/src/bridge/IrcHandler.ts +++ b/src/bridge/IrcHandler.ts @@ -37,7 +37,6 @@ interface TopicQueueItem { export interface IrcHandlerConfig { mapIrcMentionsToMatrix?: "on"|"off"|"force-off"; - leaveConcurrency?: number; } type MetricNames = "join.names"|"join"|"part"|"pm"|"invite"|"topic"|"message"|"kick"|"mode"; @@ -69,7 +68,7 @@ export class IrcHandler { "off" - Defaults to disabled, users can choose to enable. "force-off" - Disabled, cannot be enabled. */ - private readonly mentionMode: "on"|"off"|"force-off"; + private mentionMode: "on"|"off"|"force-off"; public readonly roomAccessSyncer: RoomAccessSyncer; @@ -956,6 +955,10 @@ export class IrcHandler { return metrics; } + public onConfigChanged(config: IrcHandlerConfig) { + this.mentionMode = config.mapIrcMentionsToMatrix || "on"; + } + private invalidateNickUserIdMap(server: IrcServer, channel: string) { this.nickUserIdMapCache.delete(`${server.domain}:${channel}`); } diff --git a/src/bridge/MemberListSyncer.ts b/src/bridge/MemberListSyncer.ts index 807821d25..899b14a60 100644 --- a/src/bridge/MemberListSyncer.ts +++ b/src/bridge/MemberListSyncer.ts @@ -195,7 +195,7 @@ export class MemberListSyncer { // fetch joined members allowing 50 in-flight reqs at a time const pool = new QueuePool(50, async (_roomId) => { const roomId = _roomId as string; - let userMap: Record|undefined; + let userMap: Record|undefined; while (!userMap) { try { userMap = await this.appServiceBot.getJoinedMembers(roomId); diff --git a/src/config/BridgeConfig.ts b/src/config/BridgeConfig.ts index ef0a79f61..8f6112e29 100644 --- a/src/config/BridgeConfig.ts +++ b/src/config/BridgeConfig.ts @@ -6,7 +6,7 @@ export interface BridgeConfig { matrixHandler: { }; - ircHandler: IrcHandlerConfig; + ircHandler?: IrcHandlerConfig; database: { engine: string; connectionString: string; diff --git a/src/datastore/postgres/PgDataStore.ts b/src/datastore/postgres/PgDataStore.ts index 5935df7bc..7860e1f64 100644 --- a/src/datastore/postgres/PgDataStore.ts +++ b/src/datastore/postgres/PgDataStore.ts @@ -33,7 +33,7 @@ const log = getLogger("PgDatastore"); export class PgDataStore implements DataStore { private serverMappings: {[domain: string]: IrcServer} = {}; - public static readonly LATEST_SCHEMA = 4; + public static readonly LATEST_SCHEMA = 5; private pgPool: Pool; private hasEnded = false; private cryptoStore?: StringCrypto; diff --git a/src/datastore/postgres/schema/v5.ts b/src/datastore/postgres/schema/v5.ts new file mode 100644 index 000000000..8f6b92657 --- /dev/null +++ b/src/datastore/postgres/schema/v5.ts @@ -0,0 +1,7 @@ +import { PoolClient } from "pg"; + +export async function runSchema(connection: PoolClient) { + await connection.query(` + CREATE INDEX client_config_domain_username_idx ON client_config (domain, (config->>'username')); + `); +} diff --git a/src/irc/ClientPool.ts b/src/irc/ClientPool.ts index acf0c5c3f..a2e50a296 100644 --- a/src/irc/ClientPool.ts +++ b/src/irc/ClientPool.ts @@ -28,8 +28,11 @@ import { Ipv6Generator } from "./Ipv6Generator"; import { IrcEventBroker } from "./IrcEventBroker"; import { DataStore } from "../datastore/DataStore"; import { Gauge } from "prom-client"; +import QuickLRU from "quick-lru"; const log = getLogger("ClientPool"); +const NICK_CACHE_SIZE = 256; + interface ReconnectionItem { cli: BridgedClient; chanList: string[]; @@ -42,7 +45,7 @@ interface ReconnectionItem { export class ClientPool { private botClients: Map; private virtualClients: { [serverDomain: string]: { - nicks: Map; + nicks: QuickLRU; userIds: Map; pending: Map; };}; @@ -74,7 +77,7 @@ export class ClientPool { return false; } - if (this.getBridgedClientByNick(server, nick)) { + if (this.getBridgedClientByNick(server, nick, true)) { return true; } @@ -85,7 +88,6 @@ export class ClientPool { public killAllClients() { return Bluebird.all(Object.keys(this.virtualClients).map((domain) => [ - ...this.virtualClients[domain].nicks.values(), ...this.virtualClients[domain].userIds.values(), this.botClients.get(domain), ] @@ -279,7 +281,7 @@ export class ClientPool { if (this.virtualClients[server.domain] === undefined) { this.virtualClients[server.domain] = { - nicks: new Map(), + nicks: new QuickLRU({maxSize: NICK_CACHE_SIZE}), userIds: new Map(), pending: new Map(), }; @@ -339,17 +341,28 @@ export class ClientPool { return cli; } - public getBridgedClientByNick(server: IrcServer, nick: string) { + public getBridgedClientByNick(server: IrcServer, nick: string, allowDead = false) { const bot = this.getBot(server); if (bot && bot.nick === nick) { return bot; } - if (!this.virtualClients[server.domain]) { + const serverSet = this.virtualClients[server.domain]; + + if (!serverSet) { return undefined; } - const cli = this.virtualClients[server.domain].nicks.get(nick); - if (cli?.isDead()) { + + let cli = serverSet.nicks.get(nick); + if (!cli) { + cli = [...serverSet.userIds.values()].find(c => c.nick === nick); + if (!cli) { + return undefined; + } + serverSet.nicks.set(cli.nick, cli); + } + + if (!allowDead && cli.isDead()) { return undefined; } return cli; @@ -412,7 +425,7 @@ export class ClientPool { // find the oldest client to kill. let oldest: BridgedClient|null = null; - for (const client of this.virtualClients[server.domain].nicks.values()) { + for (const client of this.virtualClients[server.domain].userIds.values()) { if (!client) { // possible since undefined/null values can be present from culled entries continue; @@ -420,6 +433,9 @@ export class ClientPool { if (client.isBot) { continue; // don't ever kick the bot off. } + if (client.status !== BridgedClientStatus.CONNECTED) { + continue; // Don't kick clients that aren't connected. + } if (oldest === null) { oldest = client; continue; diff --git a/src/irc/IrcServer.ts b/src/irc/IrcServer.ts index 5dca18c35..f8a3cd7b0 100644 --- a/src/irc/IrcServer.ts +++ b/src/irc/IrcServer.ts @@ -43,40 +43,12 @@ export class IrcServer { */ constructor(public domain: string, public config: IrcServerConfig, private homeserverDomain: string, private expiryTimeSeconds: number = 0) { - // This ensures that legacy mappings still work, but we prod the user to update. - const stringMappings = Object.entries(config.mappings || {}).filter(([, data]) => { - return Array.isArray(data); - }) as unknown as [string, string[]][]; + // These are set in reconfigure + this.addresses = []; + this.groupIdValid = false; + this.excludedUsers = []; - if (stringMappings.length) { - log.warn("** The IrcServer.mappings config schema has changed, allowing legacy format for now. **"); - log.warn("See https://github.com/matrix-org/matrix-appservice-irc/blob/master/CHANGELOG.md for details"); - for (const [channelId, roomIds] of stringMappings) { - config.mappings[channelId] = { roomIds: roomIds } - } - } - - this.addresses = config.additionalAddresses || []; - this.addresses.push(domain); - this.excludedUsers = config.excludedUsers.map((excluded) => { - return { - ...excluded, - regex: new RegExp(excluded.regex) - } - }) - - if (this.config.dynamicChannels.groupId !== undefined && - this.config.dynamicChannels.groupId.trim() !== "") { - this.groupIdValid = GROUP_ID_REGEX.test(this.config.dynamicChannels.groupId); - if (!this.groupIdValid) { - log.warn( - `${domain} has an incorrectly configured groupId for dynamicChannels and will not set groups.` - ); - } - } - else { - this.groupIdValid = false; - } + this.reconfigure(config, expiryTimeSeconds); } /** @@ -592,6 +564,46 @@ export class IrcServer { } } + public reconfigure(config: IrcServerConfig, expiryTimeSeconds = 0) { + log.info(`Reconfiguring ${this.domain}`); + this.config = config; + this.expiryTimeSeconds = expiryTimeSeconds; + // This ensures that legacy mappings still work, but we prod the user to update. + const stringMappings = Object.entries(config.mappings || {}).filter(([, data]) => { + return Array.isArray(data); + }) as unknown as [string, string[]][]; + + if (stringMappings.length) { + log.warn("** The IrcServer.mappings config schema has changed, allowing legacy format for now. **"); + log.warn("See https://github.com/matrix-org/matrix-appservice-irc/blob/master/CHANGELOG.md for details"); + for (const [channelId, roomIds] of stringMappings) { + config.mappings[channelId] = { roomIds: roomIds } + } + } + + this.addresses = config.additionalAddresses || []; + this.addresses.push(this.domain); + this.excludedUsers = config.excludedUsers.map((excluded) => { + return { + ...excluded, + regex: new RegExp(excluded.regex) + } + }); + + if (config.dynamicChannels.groupId !== undefined && + config.dynamicChannels.groupId.trim() !== "") { + this.groupIdValid = GROUP_ID_REGEX.test(config.dynamicChannels.groupId); + if (!this.groupIdValid) { + log.warn( + `${this.domain} has an incorrectly configured groupId for dynamicChannels and will not set groups.` + ); + } + } + else { + this.groupIdValid = false; + } + } + private static templateToRegex(template: string, literalVars: {[key: string]: string}, regexVars: {[key: string]: string}, suffix: string) { // The 'template' is a literal string with some special variables which need