Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.d/1663.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- New PM rooms are configured to disable calls, reactions, redactions, and stickers;
as they could not be bridged anyway.
1 change: 1 addition & 0 deletions changelog.d/1683.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Implement MSC2346 (bridge info state event) for PMs
1 change: 1 addition & 0 deletions changelog.d/1709.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix the bridge pooling so it supports TLS.
1 change: 1 addition & 0 deletions changelog.d/1711.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix setup widget failing to authenticate.
1 change: 1 addition & 0 deletions changelog.d/1715.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sort the list of channels in !listrooms output.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
110 changes: 88 additions & 22 deletions spec/integ/pm.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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;
});
Expand Down Expand Up @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion src/bridge/AdminRoomHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ export class AdminRoomHandler {

let chanList = `You are joined to ${client.chanList.size} rooms: \n\n`;
let chanListHTML = `<p>You are joined to <code>${client.chanList.size}</code> rooms:</p><ul>`;
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
Expand Down
20 changes: 20 additions & 0 deletions src/bridge/IrcBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from "matrix-appservice";
import {
Bridge,
Intent,
MatrixUser,
MatrixRoom,
Logger,
Expand Down Expand Up @@ -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<string, unknown>,
);
}

private async setupStateSyncer(config: BridgeConfig) {
if (!config.ircService.bridgeInfoState?.enabled) {
this.bridgeStateSyncer = undefined;
Expand Down
64 changes: 44 additions & 20 deletions src/bridge/IrcHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -169,6 +169,48 @@ export class IrcHandler {
): Promise<MatrixRoom> {
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(
Expand All @@ -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,
}
});
}
Expand Down
6 changes: 6 additions & 0 deletions src/bridge/MatrixHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand Down
5 changes: 3 additions & 2 deletions src/irc/ConnectionInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,18 +428,19 @@ 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,
port: connectionOpts.port ?? 6667,
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, {
Expand Down
6 changes: 3 additions & 3 deletions src/pool-service/IrcConnectionPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ export class IrcConnectionPool {
}

private async createConnectionForOpts(opts: ConnectionCreateArgs): Promise<Socket> {
let socket: Socket;
if (opts.secure) {
let secureOpts: tls.ConnectionOptions = {
...opts,
Expand All @@ -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();
Expand Down Expand Up @@ -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);
});
Expand Down
14 changes: 1 addition & 13 deletions src/provisioning/Provisioner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>,
);
}
await this.ircBridge.syncState(ircChannel, server, roomId);
}

private removeRequest (server: IrcServer, opNick: string) {
Expand Down
2 changes: 1 addition & 1 deletion widget/src/ProvisioningApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const ProvisioningApp: React.FC<React.PropsWithChildren<{
return;
}

const widgetApi = new WidgetApi(widgetId);
const widgetApi = new WidgetApi(widgetId, '*');
widgetApi.on('ready', () => {
console.log('Widget API ready');
});
Expand Down
Loading