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/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
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/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/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/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/spec/integ/pm.spec.js b/spec/integ/pm.spec.js
index 48aff4f46..b5a7e7c85 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(
+ roomMapping.roomId,
+ "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 () => {
@@ -262,33 +289,59 @@ 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,
+ "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,
+ redact: 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 +371,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/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
diff --git a/src/bridge/IrcBridge.ts b/src/bridge/IrcBridge.ts
index 4032af4c0..279f51cea 100644
--- a/src/bridge/IrcBridge.ts
+++ b/src/bridge/IrcBridge.ts
@@ -24,6 +24,7 @@ import {
} from "matrix-appservice";
import {
Bridge,
+ Intent,
MatrixUser,
MatrixRoom,
Logger,
@@ -830,6 +831,25 @@ 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 async setupStateSyncer(config: BridgeConfig) {
if (!config.ircService.bridgeInfoState?.enabled) {
this.bridgeStateSyncer = undefined;
diff --git a/src/bridge/IrcHandler.ts b/src/bridge/IrcHandler.ts
index d515b853e..76b698780 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,48 @@ 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,
+ // 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: "",
+ }]
+
+ 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 +226,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,
}
});
}
diff --git a/src/bridge/MatrixHandler.ts b/src/bridge/MatrixHandler.ts
index a2fd8e41a..b0907db94 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/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);
});
diff --git a/src/provisioning/Provisioner.ts b/src/provisioning/Provisioner.ts
index 23ad27464..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 initialEvent = await this.ircBridge.stateSyncer.createInitialState(roomId, {
- channel: ircChannel,
- networkId: server.getNetworkId(),
- })
- await intent.sendStateEvent(
- roomId,
- initialEvent.type,
- initialEvent.state_key,
- initialEvent.content as unknown as Record,
- );
- }
+ await this.ircBridge.syncState(ircChannel, server, roomId);
}
private removeRequest (server: IrcServer, opNick: string) {
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"