import { IrcBridgeConfiguration } from "../integrations/Bridge"; import { Cache, CACHE_IRC_BRIDGE } from "../MemoryCache"; import IrcBridgeRecord from "../db/models/IrcBridgeRecord"; import Upstream from "../db/models/Upstream"; import UserScalarToken from "../db/models/UserScalarToken"; import { LogService } from "matrix-js-snippets"; import * as request from "request"; import { ListLinksResponseItem, ListOpsResponse, QueryNetworksResponse } from "./models/irc"; import IrcBridgeNetwork from "../db/models/IrcBridgeNetwork"; import { ModularIrcResponse } from "../models/ModularResponses"; interface CachedNetwork { ircBridgeId: number; bridgeNetworkId: string; bridgeUserId: string; displayName: string; domain: string; isEnabled: boolean; } export interface AvailableNetworks { [networkId: string]: { name: string; domain: string; bridgeUserId: string; isEnabled: boolean; }; } export interface LinkedChannels { [networkId: string]: { channelName: string; }[]; } export class IrcBridge { private static getNetworkId(network: CachedNetwork): string { return network.ircBridgeId + "-" + network.bridgeNetworkId; } public static parseNetworkId(networkId: string): { bridgeId: string, bridgeNetworkId: string } { const parts = networkId.split("-"); const bridgeId = parts.splice(0, 1)[0]; const bridgeNetworkId = parts.join("-"); return {bridgeId, bridgeNetworkId}; } constructor(private requestingUserId: string) { } public async isBridgingEnabled(): Promise { const bridges = await IrcBridgeRecord.findAll({where: {isEnabled: true}}); return !!bridges && bridges.length > 0; } public async hasNetworks(): Promise { const allNetworks = (await this.getAllNetworks()).filter(n => n.isEnabled); return allNetworks.length > 0; } public async getNetworks(bridge?: IrcBridgeRecord, enabledOnly?: boolean): Promise { let networks = await this.getAllNetworks(); if (bridge) networks = networks.filter(n => n.ircBridgeId === bridge.id); if (enabledOnly) networks = networks.filter(n => n.isEnabled); const available: AvailableNetworks = {}; networks.forEach(n => available[IrcBridge.getNetworkId(n)] = { name: n.displayName, domain: n.domain, bridgeUserId: n.bridgeUserId, isEnabled: n.isEnabled, }); return available; } public async getRoomConfiguration(inRoomId: string): Promise { const availableNetworks = await this.getNetworks(null, true); const bridges = await IrcBridgeRecord.findAll({where: {isEnabled: true}}); const linkedChannels: LinkedChannels = {}; for (const bridge of bridges) { const links = await this.fetchLinks(bridge, inRoomId); for (const key of Object.keys(links)) { linkedChannels[key] = links[key]; } } return {availableNetworks: availableNetworks, links: linkedChannels}; } public async getOperators(bridge: IrcBridgeRecord, networkId: string, channel: string): Promise { const network = (await this.getAllNetworks()).find(n => n.isEnabled && n.ircBridgeId === bridge.id && n.bridgeNetworkId === networkId); if (!network) throw new Error("Network not found"); const requestBody = {remote_room_server: network.domain, remote_room_channel: channel}; let responses: ListOpsResponse[] = []; if (bridge.upstreamId) { const result = await this.doUpstreamRequest>(bridge, "POST", "/bridges/irc/_matrix/provision/querylink", null, requestBody); if (result && result.replies) responses = result.replies.map(r => r.response); } else { const result = await this.doProvisionRequest(bridge, "POST", "/_matrix/provision/querylink", null, requestBody); if (result) responses = [result]; } const ops: string[] = []; for (const response of responses) { if (!response || !response.operators) continue; response.operators.forEach(i => ops.push(i)); } return ops; } public async requestLink(bridge: IrcBridgeRecord, networkId: string, channel: string, op: string, inRoomId: string): Promise { const network = (await this.getAllNetworks()).find(n => n.isEnabled && n.ircBridgeId === bridge.id && n.bridgeNetworkId === networkId); if (!network) throw new Error("Network not found"); const requestBody = { remote_room_server: network.domain, remote_room_channel: channel, matrix_room_id: inRoomId, op_nick: op, user_id: this.requestingUserId, }; if (bridge.upstreamId) { delete requestBody["user_id"]; await this.doUpstreamRequest(bridge, "POST", "/bridges/irc/_matrix/provision/link", null, requestBody); } else { await this.doProvisionRequest(bridge, "POST", "/_matrix/provision/link", null, requestBody); } } public async removeLink(bridge: IrcBridgeRecord, networkId: string, channel: string, inRoomId: string): Promise { const network = (await this.getAllNetworks()).find(n => n.isEnabled && n.ircBridgeId === bridge.id && n.bridgeNetworkId === networkId); if (!network) throw new Error("Network not found"); const requestBody = { remote_room_server: network.domain, remote_room_channel: channel, matrix_room_id: inRoomId, user_id: this.requestingUserId, }; if (bridge.upstreamId) { delete requestBody["user_id"]; await this.doUpstreamRequest(bridge, "POST", "/bridges/irc/_matrix/provision/unlink", null, requestBody); } else { await this.doProvisionRequest(bridge, "POST", "/_matrix/provision/unlink", null, requestBody); } } private async getAllNetworks(): Promise { const cached = Cache.for(CACHE_IRC_BRIDGE).get("networks"); if (cached) return cached; const bridges = await IrcBridgeRecord.findAll(); if (!bridges) return []; const networks: CachedNetwork[] = []; for (const bridge of bridges) { const bridgeNetworks = await this.fetchNetworks(bridge); bridgeNetworks.forEach(n => networks.push(n)); } Cache.for(CACHE_IRC_BRIDGE).put("networks", networks, 60 * 60 * 1000); // 1 hour return networks; } private async fetchLinks(bridge: IrcBridgeRecord, inRoomId: string): Promise { const availableNetworks = await this.getNetworks(bridge, true); const networksByDomain: { [domain: string]: { id: string, name: string, bridgeUserId: string } } = {}; for (const key of Object.keys(availableNetworks)) { const network = availableNetworks[key]; networksByDomain[network.domain] = { id: key, name: network.name, bridgeUserId: network.bridgeUserId, }; } let responses: ListLinksResponseItem[] = []; if (bridge.upstreamId) { const result = await this.doUpstreamRequest>(bridge, "GET", "/bridges/irc/_matrix/provision/listlinks/" + inRoomId); if (result && result.replies) { const replies = result.replies.map(r => r.response); for (const reply of replies) reply.forEach(r => responses.push(r)); } } else { const result = await this.doProvisionRequest(bridge, "GET", "/_matrix/provision/listlinks/" + inRoomId); if (result) responses = result; } const linked: LinkedChannels = {}; for (const response of responses) { if (!response || !response.remote_room_server) continue; const network = networksByDomain[response.remote_room_server]; if (!network) continue; if (!linked[network.id]) linked[network.id] = []; linked[network.id].push({ channelName: response.remote_room_channel, }); } return linked; } private async fetchNetworks(bridge: IrcBridgeRecord): Promise { let responses: QueryNetworksResponse[] = []; if (bridge.upstreamId) { const result = await this.doUpstreamRequest>(bridge, "GET", "/bridges/irc/_matrix/provision/querynetworks"); if (result && result.replies) responses = result.replies.map(r => r.response); } else { const result = await this.doProvisionRequest(bridge, "GET", "/_matrix/provision/querynetworks"); if (result) responses = [result]; } const networks: CachedNetwork[] = []; for (const response of responses) { if (!response || !response.servers) continue; for (const server of response.servers) { if (!server) continue; let existingNetwork = await IrcBridgeNetwork.findOne({ where: { bridgeId: bridge.id, bridgeNetworkId: server.network_id, }, }); if (!existingNetwork) { LogService.info("IrcBridge", "Discovered new network for bridge " + bridge.id + ": " + server.network_id); existingNetwork = await IrcBridgeNetwork.create({ bridgeId: bridge.id, isEnabled: false, bridgeNetworkId: server.network_id, bridgeUserId: server.bot_user_id, displayName: server.desc, domain: server.fields.domain, }); } else { existingNetwork.displayName = server.desc; existingNetwork.bridgeUserId = server.bot_user_id; existingNetwork.domain = server.fields.domain; await existingNetwork.save(); } networks.push({ ircBridgeId: bridge.id, bridgeNetworkId: existingNetwork.bridgeNetworkId, bridgeUserId: existingNetwork.bridgeUserId, displayName: existingNetwork.displayName, domain: existingNetwork.domain, isEnabled: existingNetwork.isEnabled, }); } } return networks; } private async doUpstreamRequest(bridge: IrcBridgeRecord, method: string, endpoint: string, qs?: any, body?: any): Promise { const upstream = await Upstream.findByPk(bridge.upstreamId); const token = await UserScalarToken.findOne({ where: { upstreamId: upstream.id, isDimensionToken: false, userId: this.requestingUserId, }, }); if (!qs) qs = {}; qs["scalar_token"] = token.scalarToken; const apiUrl = upstream.apiUrl.endsWith("/") ? upstream.apiUrl.substring(0, upstream.apiUrl.length - 1) : upstream.apiUrl; const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint); LogService.info("IrcBridge", "Doing upstream IRC Bridge request: " + url); return new Promise((resolve, reject) => { request({ method: method, url: url, qs: qs, json: body, }, (err, res, _body) => { try { if (err) { LogService.error("IrcBridge", "Error calling " + url); LogService.error("IrcBridge", err); reject(err); } else if (!res) { LogService.error("IrcBridge", "There is no response for " + url); reject(new Error("No response provided - is the service online?")); } else if (res.statusCode !== 200) { LogService.error("IrcBridge", "Got status code " + res.statusCode + " when calling " + url); LogService.error("IrcBridge", res.body); reject(new Error("Request failed")); } else { if (typeof (res.body) === "string") res.body = JSON.parse(res.body); resolve(res.body); } } catch (e) { LogService.error("IrcBridge", e); reject(e); } }); }); } private async doProvisionRequest(bridge: IrcBridgeRecord, method: string, endpoint: string, qs?: any, body?: any): Promise { const provisionUrl = bridge.provisionUrl; const apiUrl = provisionUrl.endsWith("/") ? provisionUrl.substring(0, provisionUrl.length - 1) : provisionUrl; const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint); LogService.info("IrcBridge", "Doing provision IRC Bridge request: " + url); return new Promise((resolve, reject) => { request({ method: method, url: url, qs: qs, json: body, }, (err, res, _body) => { try { if (err) { LogService.error("IrcBridge", "Error calling" + url); LogService.error("IrcBridge", err); reject(err); } else if (!res) { LogService.error("IrcBridge", "There is no response for " + url); reject(new Error("No response provided - is the service online?")); } else if (res.statusCode !== 200) { LogService.error("IrcBridge", "Got status code " + res.statusCode + " when calling " + url); LogService.error("IrcBridge", res.body); reject(new Error("Request failed")); } else { if (typeof (res.body) === "string") res.body = JSON.parse(res.body); resolve(res.body); } } catch (e) { LogService.error("IrcBridge", e); reject(e); } }); }); } }