Skip to content

Commit d9d8d18

Browse files
Add support for room upgrades (matrix-org#1079)
* Add a test for room upgrades * Add support for following room upgrades. * Add room upgrade support to GenericHook * Add support for upgrades on feed connections * Add support for upgrades on figma file connections * Add support for room upgrades across all connection types. * Fix test flakiness * Cleanup * Lint all the things. * changelog * fixup setup connection * Use LegacyEventType on fallback * Combine room upgrade methods --------- Co-authored-by: Andrew Ferrazzutti <[email protected]>
1 parent 9d59510 commit d9d8d18

24 files changed

+744
-304
lines changed

changelog.d/1079.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for following room upgrades. Hookshot will now carry across all connections from the predecessor room to the next room.

spec/generic-hooks.spec.ts

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
E2ESetupTestTimeout,
3-
E2ETestEnv,
4-
E2ETestMatrixClient,
5-
} from "./util/e2e-test";
1+
import { E2ESetupTestTimeout, E2ETestEnv } from "./util/e2e-test";
62
import {
73
describe,
84
test,
@@ -13,47 +9,8 @@ import {
139
vitest,
1410
} from "vitest";
1511
import { GenericHookConnection } from "../src/Connections";
16-
import { TextualMessageEventContent } from "matrix-bot-sdk";
1712
import { add } from "date-fns/add";
18-
19-
async function createInboundConnection(
20-
user: E2ETestMatrixClient,
21-
botMxid: string,
22-
roomId: string,
23-
duration?: string,
24-
) {
25-
const join = user.waitForRoomJoin({ sender: botMxid, roomId });
26-
const connectionEvent = user.waitForRoomEvent({
27-
eventType: GenericHookConnection.CanonicalEventType,
28-
stateKey: "test",
29-
sender: botMxid,
30-
});
31-
await user.inviteUser(botMxid, roomId);
32-
await user.setUserPowerLevel(botMxid, roomId, 50);
33-
await join;
34-
35-
// Note: Here we create the DM proactively so this works across multiple
36-
// tests.
37-
// Get the DM room so we can get the token.
38-
const dmRoomId = await user.dms.getOrCreateDm(botMxid);
39-
40-
await user.sendText(
41-
roomId,
42-
"!hookshot webhook test" + (duration ? ` ${duration}` : ""),
43-
);
44-
// Test the contents of this.
45-
await connectionEvent;
46-
47-
const msgPromise = user.waitForRoomEvent({
48-
sender: botMxid,
49-
eventType: "m.room.message",
50-
roomId: dmRoomId,
51-
});
52-
const { data: msgData } = await msgPromise;
53-
const msgContent = msgData.content as unknown as TextualMessageEventContent;
54-
const [_unused1, _unused2, url] = msgContent.body.split("\n");
55-
return url;
56-
}
13+
import { createInboundConnection } from "./util/helpers";
5714

5815
describe("Inbound (Generic) Webhooks", () => {
5916
let testEnv: E2ETestEnv;

spec/room-upgrades.spec.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { E2ESetupTestTimeout, E2ETestEnv } from "./util/e2e-test";
2+
import { describe, test, beforeAll, afterAll, expect } from "vitest";
3+
import { createInboundConnection, waitFor } from "./util/helpers";
4+
import { getBridgeApi } from "./util/bridge-api";
5+
6+
describe("Room Upgrades", () => {
7+
let testEnv: E2ETestEnv;
8+
9+
beforeAll(async () => {
10+
const webhooksPort = 9500 + E2ETestEnv.workerId;
11+
testEnv = await E2ETestEnv.createTestEnv({
12+
matrixLocalparts: ["user"],
13+
config: {
14+
generic: {
15+
enabled: true,
16+
// Prefer to wait for complete as it reduces the concurrency of the test.
17+
waitForComplete: true,
18+
urlPrefix: `http://localhost:${webhooksPort}`,
19+
},
20+
widgets: {
21+
publicUrl: `http://localhost:${webhooksPort}`,
22+
},
23+
listeners: [
24+
{
25+
port: webhooksPort,
26+
bindAddress: "0.0.0.0",
27+
resources: ["webhooks", "widgets"],
28+
},
29+
],
30+
},
31+
});
32+
await testEnv.setUp();
33+
}, E2ESetupTestTimeout);
34+
35+
afterAll(() => {
36+
return testEnv?.tearDown();
37+
});
38+
39+
test(
40+
"should be able to create a new generic webhook, upgrade the room, and have the state carry over.",
41+
{ timeout: 25000 },
42+
async () => {
43+
const user = testEnv.getUser("user");
44+
const bridgeApi = await getBridgeApi(
45+
testEnv.opts.config?.widgets?.publicUrl!,
46+
user,
47+
);
48+
const previousRoomId = await user.createRoom({
49+
name: "My Test Webhooks room",
50+
});
51+
const okMsg = user.waitForRoomEvent({
52+
eventType: "m.room.message",
53+
sender: testEnv.botMxid,
54+
roomId: previousRoomId,
55+
});
56+
const url = await createInboundConnection(
57+
user,
58+
testEnv.botMxid,
59+
previousRoomId,
60+
);
61+
expect((await okMsg).data.content.body).toEqual(
62+
"Room configured to bridge webhooks. See admin room for secret url.",
63+
);
64+
const newRoomId = await user.upgradeRoom(previousRoomId, "10");
65+
// NOTE: The room upgrade endpoint does NOT do invites, so we need to do this.
66+
await user.inviteUser(testEnv.botMxid, newRoomId);
67+
68+
await user.waitForRoomJoin({
69+
sender: testEnv.botMxid,
70+
roomId: newRoomId,
71+
});
72+
73+
// Wait for the state to carry over.
74+
await user.waitForRoomEvent({
75+
eventType: "uk.half-shot.matrix-hookshot.generic.hook",
76+
roomId: newRoomId,
77+
sender: testEnv.botMxid,
78+
});
79+
80+
// Wait for hookshot to accept the new state.
81+
await waitFor(
82+
async () =>
83+
(await bridgeApi.getConnectionsForRoom(newRoomId)).length === 1,
84+
);
85+
86+
const expectedMsg = user.waitForRoomEvent({
87+
eventType: "m.room.message",
88+
sender: testEnv.botMxid,
89+
roomId: newRoomId,
90+
});
91+
const req = await fetch(url, {
92+
method: "PUT",
93+
body: "Hello world",
94+
});
95+
expect(req.status).toEqual(200);
96+
expect(await req.json()).toEqual({ ok: true });
97+
expect((await expectedMsg).data.content).toEqual({
98+
msgtype: "m.notice",
99+
body: "Received webhook data: Hello world",
100+
formatted_body: "<p>Received webhook data: Hello world</p>",
101+
format: "org.matrix.custom.html",
102+
"uk.half-shot.hookshot.webhook_data": "Hello world",
103+
});
104+
105+
// And finally ensure that the old room is no longer configured
106+
expect(
107+
await user.getRoomStateEventContent(
108+
previousRoomId,
109+
"uk.half-shot.matrix-hookshot.generic.hook",
110+
"test",
111+
),
112+
).toMatchObject({ disabled: true });
113+
},
114+
);
115+
});

spec/util/helpers.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { TextualMessageEventContent } from "matrix-bot-sdk";
2+
import { GenericHookConnection } from "../../src/Connections";
3+
import { E2ETestMatrixClient } from "./e2e-test";
4+
15
export async function waitFor(
26
condition: () => Promise<boolean>,
37
delay = 100,
@@ -11,3 +15,42 @@ export async function waitFor(
1115
throw Error("Hit retry limit");
1216
}
1317
}
18+
19+
export async function createInboundConnection(
20+
user: E2ETestMatrixClient,
21+
botMxid: string,
22+
roomId: string,
23+
duration?: string,
24+
) {
25+
const join = user.waitForRoomJoin({ sender: botMxid, roomId });
26+
const connectionEvent = user.waitForRoomEvent({
27+
eventType: GenericHookConnection.CanonicalEventType,
28+
stateKey: "test",
29+
sender: botMxid,
30+
});
31+
await user.inviteUser(botMxid, roomId);
32+
await user.setUserPowerLevel(botMxid, roomId, 50);
33+
await join;
34+
35+
// Note: Here we create the DM proactively so this works across multiple
36+
// tests.
37+
// Get the DM room so we can get the token.
38+
const dmRoomId = await user.dms.getOrCreateDm(botMxid);
39+
40+
await user.sendText(
41+
roomId,
42+
"!hookshot webhook test" + (duration ? ` ${duration}` : ""),
43+
);
44+
// Test the contents of this.
45+
await connectionEvent;
46+
47+
const msgPromise = user.waitForRoomEvent({
48+
sender: botMxid,
49+
eventType: "m.room.message",
50+
roomId: dmRoomId,
51+
});
52+
const { data: msgData } = await msgPromise;
53+
const msgContent = msgData.content as unknown as TextualMessageEventContent;
54+
const [_unused1, _unused2, url] = msgContent.body.split("\n");
55+
return url;
56+
}

src/Bridge.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1526,6 +1526,9 @@ export class Bridge {
15261526
roomId: string,
15271527
matrixEvent: MatrixEvent<MatrixMemberContent>,
15281528
) {
1529+
if (!this.connectionManager) {
1530+
return;
1531+
}
15291532
const userId = matrixEvent.state_key;
15301533
if (!userId) {
15311534
return;
@@ -1537,6 +1540,7 @@ export class Bridge {
15371540
return;
15381541
}
15391542
this.botUsersManager.onRoomJoin(botUser, roomId);
1543+
this.connectionManager.checkAndMigrateIfPendingUpgrade(roomId, "join");
15401544

15411545
if (this.config.encryption) {
15421546
// Ensure crypto is aware of all members of this room before posting any messages,
@@ -1627,6 +1631,26 @@ export class Bridge {
16271631
}
16281632
return;
16291633
}
1634+
1635+
if (event.type === "m.room.tombstone" && event.state_key === "") {
1636+
const replacementRoomId = event.content.replacement_room;
1637+
if (typeof replacementRoomId !== "string") {
1638+
// This event is invalid, ignore it.
1639+
return;
1640+
}
1641+
await this.connectionManager.onTombstoneEvent(
1642+
roomId,
1643+
replacementRoomId,
1644+
);
1645+
}
1646+
1647+
if (event.type === "m.room.power_levels" && event.state_key === "") {
1648+
this.connectionManager.checkAndMigrateIfPendingUpgrade(
1649+
roomId,
1650+
"power level",
1651+
);
1652+
}
1653+
16301654
// A state update, hurrah!
16311655
const existingConnections =
16321656
this.connectionManager.getInterestedForRoomState(

0 commit comments

Comments
 (0)