From ef7b957f2ea5eacc4b2de65d7c7ec9856035a7d1 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 11 Mar 2023 21:02:22 +0100 Subject: [PATCH 1/8] Add bridge info state event to rooms created by IRC-to-Matrix PMing --- spec/integ/pm.spec.js | 71 ++++++++++++++++++++++++++++------------ src/bridge/IrcHandler.ts | 51 ++++++++++++++++++----------- 2 files changed, 81 insertions(+), 41 deletions(-) diff --git a/spec/integ/pm.spec.js b/spec/integ/pm.spec.js index 48aff4f46..d8fe33c8c 100644 --- a/spec/integ/pm.spec.js +++ b/spec/integ/pm.spec.js @@ -262,33 +262,49 @@ describe("IRC-to-Matrix PMing", () => { afterEach(async () => test.afterEach(env)); - it("should create a 1:1 matrix room and invite the real matrix user when " + - "it receives a PM directed at a virtual user from a real IRC user", async () => { + async function testPmRoomCreation(enableBrigeInfoState) { // mock create room impl + const expectedInitialState = [{ + type: "m.room.power_levels", + state_key: "", + content: { + users: { + "@alice:anotherhomeserver": 10, + "@irc.example_bob:some.home.server": 100 + }, + events: { + "m.room.avatar": 10, + "m.room.name": 10, + "m.room.canonical_alias": 100, + "m.room.history_visibility": 100, + "m.room.power_levels": 100, + "m.room.encryption": 100 + }, + invite: 100 + }, + }]; + if (enableBrigeInfoState) { + expectedInitialState.push({ + type: "uk.half-shot.bridge", + state_key: "org.matrix.appservice-irc:/irc.example/bob", + content: { + bridgebot: '@monkeybot:some.home.server', + protocol: { id: 'irc', displayname: 'IRC' }, + channel: { id: 'bob' }, + network: { + id: 'irc.example', + displayname: '', + avatar_url: undefined, + } + } + }); + } const createRoomPromise = new Promise((resolve) => { sdk.createRoom.and.callFake((opts) => { expect(opts.visibility).toEqual("private"); expect(opts.creation_content["m.federate"]).toEqual(true); expect(opts.preset).not.toBeDefined(); - expect(opts.initial_state).toEqual([{ - type: "m.room.power_levels", - state_key: "", - content: { - users: { - "@alice:anotherhomeserver": 10, - "@irc.example_bob:some.home.server": 100 - }, - events: { - "m.room.avatar": 10, - "m.room.name": 10, - "m.room.canonical_alias": 100, - "m.room.history_visibility": 100, - "m.room.power_levels": 100, - "m.room.encryption": 100 - }, - invite: 100 - }, - }]); + expect(opts.initial_state).toEqual(expectedInitialState); resolve(); return tCreatedRoomId; }); @@ -318,6 +334,19 @@ describe("IRC-to-Matrix PMing", () => { await createRoomPromise; await sentMessagePromise; + } + + it("should create a 1:1 matrix room and invite the real matrix user when " + + "it receives a PM directed at a virtual user from a real IRC user (without bridge info)", async () => { + await testPmRoomCreation(false) + }); + + it("should create a 1:1 matrix room and invite the real matrix user when " + + "it receives a PM directed at a virtual user from a real IRC user (with bridge info)", async () => { + config.ircService.bridgeInfoState = {enabled: true, initial: false}; + await env.ircBridge.onConfigChanged(config); + + await testPmRoomCreation(true) }); it("should not create multiple matrix rooms when several PMs are received in quick succession", async () => { diff --git a/src/bridge/IrcHandler.ts b/src/bridge/IrcHandler.ts index d515b853e..a6ea0a957 100644 --- a/src/bridge/IrcHandler.ts +++ b/src/bridge/IrcHandler.ts @@ -4,7 +4,7 @@ import { RoomAccessSyncer } from "./RoomAccessSyncer"; import { IrcServer, MembershipSyncKind } from "../irc/IrcServer"; import { BridgeRequest, BridgeRequestErr } from "../models/BridgeRequest"; import { BridgedClient } from "../irc/BridgedClient"; -import { MatrixRoom, MatrixUser, MembershipQueue } from "matrix-appservice-bridge"; +import { MatrixRoom, MatrixUser, MembershipQueue, InitialEvent } from "matrix-appservice-bridge"; import { IrcUser } from "../models/IrcUser"; import { IrcAction } from "../models/IrcAction"; import { IrcRoom } from "../models/IrcRoom"; @@ -169,6 +169,35 @@ export class IrcHandler { ): Promise { let remainingReties = PM_ROOM_CREATION_RETRIES; let response; + const initialState: InitialEvent[] = [{ + content: { + users: { + [toUserId]: PM_POWERLEVEL_MATRIXUSER, + [fromUserId]: PM_POWERLEVEL_IRCUSER, + }, + events: { + "m.room.avatar": 10, + "m.room.name": 10, + "m.room.canonical_alias": 100, + "m.room.history_visibility": 100, + "m.room.power_levels": 100, + "m.room.encryption": 100 + }, + invite: 100, + }, + type: "m.room.power_levels", + state_key: "", + }] + + if (this.ircBridge.stateSyncer) { + initialState.push( + await this.ircBridge.stateSyncer.createInitialState("", { + channel: fromUserNick, // TODO: spec this in MSC2346Content + networkId: server.getNetworkId(), + }) + ); + } + do { try { response = await this.ircBridge.getAppServiceBridge().getIntent( @@ -184,25 +213,7 @@ export class IrcHandler { "m.federate": server.shouldFederatePMs() }, is_direct: true, - initial_state: [{ - content: { - users: { - [toUserId]: PM_POWERLEVEL_MATRIXUSER, - [fromUserId]: PM_POWERLEVEL_IRCUSER, - }, - events: { - "m.room.avatar": 10, - "m.room.name": 10, - "m.room.canonical_alias": 100, - "m.room.history_visibility": 100, - "m.room.power_levels": 100, - "m.room.encryption": 100 - }, - invite: 100, - }, - type: "m.room.power_levels", - state_key: "", - }], + initial_state: initialState, } }); } From 413da46abcad8cbb92624d15eff031c9c6221eed Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 11 Mar 2023 21:38:13 +0100 Subject: [PATCH 2/8] Add bridge info state event to Matrix-to-IRC PM rooms we are invited to --- spec/integ/pm.spec.js | 29 ++++++++++++++++++++++++++++- src/bridge/IrcBridge.ts | 21 +++++++++++++++++++++ src/bridge/MatrixHandler.ts | 6 ++++++ src/provisioning/Provisioner.ts | 14 +------------- 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/spec/integ/pm.spec.js b/spec/integ/pm.spec.js index d8fe33c8c..c82332d7f 100644 --- a/spec/integ/pm.spec.js +++ b/spec/integ/pm.spec.js @@ -24,7 +24,7 @@ describe("Matrix-to-IRC PMing", () => { afterEach(async () => test.afterEach(env)); - it("should join 1:1 rooms invited from matrix", async () => { + async function testJoinPmRoom(enableBrigeInfoState) { // get the ball rolling const requestPromise = env.mockAppService._trigger("type:m.room.member", { content: { @@ -64,6 +64,33 @@ describe("Matrix-to-IRC PMing", () => { await joinRoomPromise; await requestPromise; + + if (enableBrigeInfoState) { + expect(intent.underlyingClient.sendStateEvent).toHaveBeenCalledWith( + jasmine.any(String), + "uk.half-shot.bridge", + "org.matrix.appservice-irc:/irc.example/someone", + { + bridgebot: '@monkeybot:some.home.server', + protocol: { id: 'irc', displayname: 'IRC' }, + channel: { id: 'someone' }, + network: { id: 'irc.example', displayname: '', avatar_url: undefined }} + ); + } + else { + expect(intent.underlyingClient.sendStateEvent).not.toHaveBeenCalled(); + } + } + + it("should join 1:1 rooms invited from matrix (without bridge info)", async () => { + await testJoinPmRoom(false); + }); + + it("should join 1:1 rooms invited from matrix (with bridge info)", async () => { + config.ircService.bridgeInfoState = {enabled: true, initial: false}; + await env.ircBridge.onConfigChanged(config); + + await testJoinPmRoom(true); }); it("should join group chat rooms invited from matrix then leave them", async () => { diff --git a/src/bridge/IrcBridge.ts b/src/bridge/IrcBridge.ts index 12a02608e..e994b63e7 100644 --- a/src/bridge/IrcBridge.ts +++ b/src/bridge/IrcBridge.ts @@ -20,6 +20,7 @@ import { Histogram } from "prom-client"; import { Bridge, + Intent, MatrixUser, MatrixRoom, Logger, @@ -820,6 +821,26 @@ export class IrcBridge { this.bridgeState = "running"; } + /* + * Send state events providing information about the state. + * @param intent if given, sends state events from this client instead of the AS bot + */ + public async syncState(ircChannel: string, server: IrcServer, roomId: string, intent?: Intent) { + if (this.stateSyncer) { + intent = intent || this.getAppServiceBridge().getIntent(); + const event = await this.stateSyncer.createInitialState(roomId, { + channel: ircChannel, + networkId: server.getNetworkId(), + }) + const res = await intent.sendStateEvent( + roomId, + event.type, + event.state_key, + event.content as unknown as Record, + ); + } + } + private logMetric(req: Request, outcome: string) { if (!this.timers) { return; // metrics are disabled diff --git a/src/bridge/MatrixHandler.ts b/src/bridge/MatrixHandler.ts index 7660ae971..350695eba 100644 --- a/src/bridge/MatrixHandler.ts +++ b/src/bridge/MatrixHandler.ts @@ -247,6 +247,12 @@ export class MatrixHandler { await this.ircBridge.getStore().setPmRoom( ircRoom, mxRoom, event.sender, event.state_key ); + + // note: it is possible (though unlikely) that the user does not have + // the required power_level to send state events. + // TODO: try with the appservice client if it doesn't? + await this.ircBridge.syncState(invited.nick, invited.server, event.room_id, intent); + return; } req.log.warn(`Room ${event.room_id} is not a 1:1 chat`); diff --git a/src/provisioning/Provisioner.ts b/src/provisioning/Provisioner.ts index 3a49d5747..7e237d8f5 100644 --- a/src/provisioning/Provisioner.ts +++ b/src/provisioning/Provisioner.ts @@ -569,19 +569,7 @@ export class Provisioner extends ProvisioningApi { } await this.updateBridgingState(roomId, userId, 'success', skey); // Send bridge info state event - if (this.ircBridge.stateSyncer) { - const intent = this.ircBridge.getAppServiceBridge().getIntent(); - const infoMapping = await this.ircBridge.stateSyncer.createInitialState(roomId, { - channel: ircChannel, - networkId: server.getNetworkId(), - }) - await intent.sendStateEvent( - roomId, - infoMapping.type, - infoMapping.state_key, - infoMapping.content as unknown as Record, - ); - } + await this.ircBridge.syncState(ircChannel, server, roomId); } private removeRequest (server: IrcServer, opNick: string) { From 92c6cc5b8b538ece2cd156b399371b800ec1d29f Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 11 Mar 2023 23:46:04 +0100 Subject: [PATCH 3/8] Add changelog --- changelog.d/1683.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1683.feature diff --git a/changelog.d/1683.feature b/changelog.d/1683.feature new file mode 100644 index 000000000..d2c29d2e3 --- /dev/null +++ b/changelog.d/1683.feature @@ -0,0 +1 @@ +* Implement MSC2346 (bridge info state event) for PMs From 6dcc95512ff182eb411c45e883b6508ee2b95c3b Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 12 Mar 2023 09:00:50 +0100 Subject: [PATCH 4/8] Use exact match for the room id --- spec/integ/pm.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/integ/pm.spec.js b/spec/integ/pm.spec.js index c82332d7f..dce433f15 100644 --- a/spec/integ/pm.spec.js +++ b/spec/integ/pm.spec.js @@ -67,7 +67,7 @@ describe("Matrix-to-IRC PMing", () => { if (enableBrigeInfoState) { expect(intent.underlyingClient.sendStateEvent).toHaveBeenCalledWith( - jasmine.any(String), + roomMapping.roomId, "uk.half-shot.bridge", "org.matrix.appservice-irc:/irc.example/someone", { From 6ce85b3eef4eab79dfe416a590f1fc9375574f50 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Tue, 2 May 2023 16:50:54 +0200 Subject: [PATCH 5/8] Define power levels to disable calls/reactions/redaction/stickers in PMs initiated from IRC (#1663) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Patch a number of packages * changelog * Ensure tests work for upgrades * Fix tests properly * 0.26.1 * Update node-irc to 1.2.1 and bump version to 0.34.0 * Define power levels to disable calls/reactions/redaction/stickers in PM No one but the sender can see them because we cannot bridge them. Blocking them with PLs allows the clients to hide these features from their UI, so users do not mistakenly believe they will be received. * Block redactions, more call event types, and widgets --------- Co-authored-by: Will Hunt Co-authored-by: Tadeusz Sośnierz Co-authored-by: Half-Shot --- changelog.d/1663.feature | 2 ++ spec/integ/pm.spec.js | 14 ++++++++++++-- src/bridge/IrcHandler.ts | 15 ++++++++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 changelog.d/1663.feature diff --git a/changelog.d/1663.feature b/changelog.d/1663.feature new file mode 100644 index 000000000..7092275bf --- /dev/null +++ b/changelog.d/1663.feature @@ -0,0 +1,2 @@ +- New PM rooms are configured to disable calls, reactions, redactions, and stickers; + as they could not be bridged anyway. diff --git a/spec/integ/pm.spec.js b/spec/integ/pm.spec.js index 48aff4f46..47f35ef0f 100644 --- a/spec/integ/pm.spec.js +++ b/spec/integ/pm.spec.js @@ -284,9 +284,19 @@ describe("IRC-to-Matrix PMing", () => { "m.room.canonical_alias": 100, "m.room.history_visibility": 100, "m.room.power_levels": 100, - "m.room.encryption": 100 + "m.room.encryption": 100, + "org.matrix.msc3401.call": 100, + "org.matrix.msc3401.call.member": 100, + "im.vector.modular.widgets": 100, + "io.element.voice_broadcast_info": 100, + "m.call.invite": 100, + "m.call.candidate": 100, + "m.reaction": 100, + "m.room.redaction": 100, + "m.sticker": 100, }, - invite: 100 + invite: 100, + redact: 100, }, }]); resolve(); diff --git a/src/bridge/IrcHandler.ts b/src/bridge/IrcHandler.ts index d515b853e..7df4e5150 100644 --- a/src/bridge/IrcHandler.ts +++ b/src/bridge/IrcHandler.ts @@ -196,9 +196,22 @@ export class IrcHandler { "m.room.canonical_alias": 100, "m.room.history_visibility": 100, "m.room.power_levels": 100, - "m.room.encryption": 100 + "m.room.encryption": 100, + // Event types that we cannot translate to IRC; + // we might as well block them with PLs so + // Matrix clients can hide them from their UI. + "m.call.invite": 100, + "m.call.candidate": 100, + "org.matrix.msc3401.call": 100, + "org.matrix.msc3401.call.member": 100, + "im.vector.modular.widgets": 100, + "io.element.voice_broadcast_info": 100, + "m.reaction": 100, + "m.room.redaction": 100, + "m.sticker": 100, }, invite: 100, + redact: 100, }, type: "m.room.power_levels", state_key: "", From e14d5abbb6e397edec2b9445ceea9358cfb5568d Mon Sep 17 00:00:00 2001 From: Justin Carlson Date: Wed, 10 May 2023 13:43:38 -0400 Subject: [PATCH 6/8] Fix Widget API origin in setup widget (#1711) * Update matrix-widget-api * Fix widget API origin * Add changelog --- changelog.d/1711.bugfix | 1 + package.json | 2 +- widget/src/ProvisioningApp.tsx | 2 +- yarn.lock | 8 ++++---- 4 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 changelog.d/1711.bugfix diff --git a/changelog.d/1711.bugfix b/changelog.d/1711.bugfix new file mode 100644 index 000000000..3dfd1ae74 --- /dev/null +++ b/changelog.d/1711.bugfix @@ -0,0 +1 @@ +Fix setup widget failing to authenticate. diff --git a/package.json b/package.json index 27383b11b..e492e555e 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "matrix-appservice-bridge": "^9.0.0", "matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.6.6-element.1", "matrix-org-irc": "^2.0.0", - "matrix-widget-api": "^1.1.1", + "matrix-widget-api": "^1.4.0", "nopt": "^6.0.0", "p-queue": "^6.6.2", "pg": "^8.8.0", diff --git a/widget/src/ProvisioningApp.tsx b/widget/src/ProvisioningApp.tsx index b4cb24b72..a08ac8ee7 100644 --- a/widget/src/ProvisioningApp.tsx +++ b/widget/src/ProvisioningApp.tsx @@ -72,7 +72,7 @@ export const ProvisioningApp: React.FC { console.log('Widget API ready'); }); diff --git a/yarn.lock b/yarn.lock index 43a43b13d..7590d8335 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4259,10 +4259,10 @@ matrix-org-irc@^2.0.0: typed-emitter "^2.1.0" utf-8-validate "^6.0.3" -matrix-widget-api@^1.1.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz#e38f404c76bb15c113909505c1c1a5b4d781c2f5" - integrity sha512-+rN6vGvnXm+fn0uq9r2KWSL/aPtehD6ObC50jYmUcEfgo8CUpf9eUurmjbRlwZkWq3XHXFuKQBUCI9UzqWg37Q== +matrix-widget-api@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.4.0.tgz#e426ec16a013897f3a4a9c2bff423f54ab0ba745" + integrity sha512-dw0dRylGQzDUoiaY/g5xx1tBbS7aoov31PRtFMAvG58/4uerYllV9Gfou7w+I1aglwB6hihTREzKltVjARWV6A== dependencies: "@types/events" "^3.0.0" events "^3.2.0" From 3ad4ae751272d38af9d93f32e0ba4fa69ffdd97e Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 12 May 2023 11:15:42 +0100 Subject: [PATCH 7/8] Fix a couple of issues in the RC (#1709) * Provide a domain to the socket * fix auth error * failing to return socket * changelog --- changelog.d/1709.bugfix | 1 + src/irc/ConnectionInstance.ts | 5 +++-- src/pool-service/IrcConnectionPool.ts | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 changelog.d/1709.bugfix diff --git a/changelog.d/1709.bugfix b/changelog.d/1709.bugfix new file mode 100644 index 000000000..b22188042 --- /dev/null +++ b/changelog.d/1709.bugfix @@ -0,0 +1 @@ +Fix the bridge pooling so it supports TLS. \ No newline at end of file diff --git a/src/irc/ConnectionInstance.ts b/src/irc/ConnectionInstance.ts index 54ed4e8a8..95551fe17 100644 --- a/src/irc/ConnectionInstance.ts +++ b/src/irc/ConnectionInstance.ts @@ -428,7 +428,7 @@ export class ConnectionInstance { // Returns: A promise which resolves to a ConnectionInstance const retryConnection = async () => { - + const domain = server.randomDomain(); const redisConn = opts.useRedisPool && await opts.useRedisPool.createOrGetIrcSocket(ident, { ...connectionOpts, clientId: ident, @@ -436,10 +436,11 @@ export class ConnectionInstance { localAddress: connectionOpts.localAddress ?? undefined, localPort: connectionOpts.localPort ?? undefined, family: connectionOpts.family ?? undefined, + host: domain, }); const nodeClient = new Client( - server.randomDomain(), opts.nick, connectionOpts, redisConn?.state, redisConn, + domain, opts.nick, connectionOpts, redisConn?.state, redisConn, ); const inst = new ConnectionInstance( nodeClient, server.domain, opts.nick, { diff --git a/src/pool-service/IrcConnectionPool.ts b/src/pool-service/IrcConnectionPool.ts index f341dfc47..29018dc25 100644 --- a/src/pool-service/IrcConnectionPool.ts +++ b/src/pool-service/IrcConnectionPool.ts @@ -74,7 +74,6 @@ export class IrcConnectionPool { } private async createConnectionForOpts(opts: ConnectionCreateArgs): Promise { - let socket: Socket; if (opts.secure) { let secureOpts: tls.ConnectionOptions = { ...opts, @@ -89,11 +88,12 @@ export class IrcConnectionPool { }; } - socket = await new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { // Taken from https://github.com/matrix-org/node-irc/blob/0764733af7c324ee24f8c2a3c26fe9d1614be344/src/irc.ts#L1231 const sock = tls.connect(secureOpts, () => { if (sock.authorized) { resolve(sock); + return; } let valid = false; const err = sock.authorizationError.toString(); @@ -125,7 +125,7 @@ export class IrcConnectionPool { }); } return new Promise((resolve, reject) => { - socket = createConnection(opts, () => resolve(socket)) as Socket; + const socket = createConnection(opts, () => resolve(socket)) as Socket; socket.once('error', (error) => { reject(error); }); From 536b0c68a3bbef936d80a3bad522eb2c19af4449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dagfinn=20Ilmari=20Manns=C3=A5ker?= Date: Fri, 12 May 2023 11:19:05 +0100 Subject: [PATCH 8/8] Sort by channel name in !listrooms (#1715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sort by channel name in !listrooms It's stored as a set in the client object, which doesn't guarantee anything about the order, so sort it for consistent output. Signed-off-by: Dagfinn Ilmari Mannsåker * Add changelog entry --------- Signed-off-by: Dagfinn Ilmari Mannsåker --- changelog.d/1715.bugfix | 1 + src/bridge/AdminRoomHandler.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/1715.bugfix diff --git a/changelog.d/1715.bugfix b/changelog.d/1715.bugfix new file mode 100644 index 000000000..3cca2e732 --- /dev/null +++ b/changelog.d/1715.bugfix @@ -0,0 +1 @@ +Sort the list of channels in !listrooms output. diff --git a/src/bridge/AdminRoomHandler.ts b/src/bridge/AdminRoomHandler.ts index 1fc9a5c28..2a13898c7 100644 --- a/src/bridge/AdminRoomHandler.ts +++ b/src/bridge/AdminRoomHandler.ts @@ -601,7 +601,7 @@ export class AdminRoomHandler { let chanList = `You are joined to ${client.chanList.size} rooms: \n\n`; let chanListHTML = `

You are joined to ${client.chanList.size} rooms:

    `; - for (const channel of client.chanList) { + for (const channel of [...client.chanList].sort()) { const rooms = await this.ircBridge.getStore().getMatrixRoomsForChannel(server, channel); chanList += `- \`${channel}\` which is bridged to ${rooms.map((r) => r.getId()).join(", ")}`; const roomMentions = rooms