Skip to content

Commit f0f80bb

Browse files
authored
Allow manual configuration of connections from the config (#1102)
* Cleanup config a bit. * Add support for defining static connections * Allow generic hooks to be static. * Add test for static configs * Lots of config import cleanup * Fixup hooks * remove console statement * Passing tests * Add changelog * Add default config * Fix test * Add documentation for static configs * Validate configuration too * Update 1102.feature Signed-off-by: Will Hunt <[email protected]> --------- Signed-off-by: Will Hunt <[email protected]>
1 parent 781c98d commit f0f80bb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+799
-412
lines changed

changelog.d/1102.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Static connections may now be configured in the config file.

config.sample.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ listeners:
108108
# # (Optional) Support for generic webhook events.
109109
# #'allowJsTransformationFunctions' will allow users to write short transformation snippets in code, and thus is unsafe in untrusted environments
110110

111-
# enabled: false
111+
# enabled: true
112112
# outbound: false
113113
# urlPrefix: https://example.com/webhook/
114114
# userIdPrefix: _webhooks_
@@ -200,5 +200,13 @@ listeners:
200200
# dsn: https://[email protected]/0
201201
# environment: production
202202

203+
#connections:
204+
# # (Optional) Static connections that may be configured by an admin
205+
# - connectionType: uk.half-shot.matrix-hookshot.generic.hook
206+
# stateKey: any-unique-id
207+
# roomId: "!any-room-id:example.org"
208+
# state:
209+
# name: My static hook
210+
203211

204212

docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- [OpenProject](./setup/openproject.md)
1414
- [👤 Usage](./usage.md)
1515
- [Dynamic Rooms](./usage/dynamic_rooms.md)
16+
- [Static Connections](./usage/static_connections.md)
1617
- [Authenticating](./usage/auth.md)
1718
- [Room Configuration](./usage/room_configuration.md)
1819
- [GitHub Repo](./usage/room_configuration/github_repo.md)

docs/setup/webhooks.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ generic:
1919
# userIdPrefix: webhook_
2020
```
2121

22+
Inbound webhooks may also be specified [statically](../usage/static_connections.html#generic-hook-ukhalf-shotmatrix-hookshotgenerichook)
23+
2224
## Inbound Webhooks
2325

2426
Hookshot supports generic webhook support so that services can send messages into Matrix rooms without being aware of the Matrix protocol. This works

docs/usage/static_connections.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Static Connections
2+
3+
Hookshot can also now be configured with "static connections". These allow system administrators to
4+
configure Hookshot with pre-specified set of connections which cannot be altered at runtime, but will
5+
have predictable configuration without any interactions with Matrix.
6+
7+
Not all connection types are currently suitable for static configuration, the supported types are listed below.
8+
9+
### Generic Hook `uk.half-shot.matrix-hookshot.generic.hook`
10+
11+
Generic (inbound) webhooks can be configured, an example configuration is below:
12+
13+
```yaml
14+
connections:
15+
- connectionType: uk.half-shot.matrix-hookshot.generic.hook
16+
stateKey: id-used-by-webhook
17+
roomId: "!any-room-id:example.org"
18+
state:
19+
name: My static hook
20+
waitForComplete: true
21+
includeHookBody: true
22+
expirationDate: 2025-11-03T16:44:59.533Z
23+
# All below are optional
24+
transformationFunction: |
25+
result = {
26+
plain: "*Everything is fine*",
27+
version: "v2",
28+
};
29+
}
30+
```
31+
32+
You may then send requests to `http(s)://example.org/webhooks/id-used-by-webhook` to activate the webhook.
33+
34+
See [the webhook documentation](../setup/webhooks) for more help on
35+
configuring hooks.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { E2ESetupTestTimeout, E2ETestEnv } from "./util/e2e-test";
2+
import {
3+
describe,
4+
test,
5+
beforeAll,
6+
afterAll,
7+
afterEach,
8+
expect,
9+
vitest,
10+
} from "vitest";
11+
import {
12+
GenericHookConnection,
13+
GenericHookConnectionState,
14+
} from "../src/Connections";
15+
16+
describe("Statically configured connection", () => {
17+
let testEnv: E2ETestEnv;
18+
19+
beforeAll(async () => {
20+
const webhooksPort = 9500 + E2ETestEnv.workerId;
21+
testEnv = await E2ETestEnv.createTestEnv({
22+
matrixLocalparts: ["user"],
23+
staticConnectionRooms: {
24+
"my-room": { members: ["user"] },
25+
},
26+
config: {
27+
generic: {
28+
enabled: true,
29+
// Prefer to wait for complete as it reduces the concurrency of the test.
30+
waitForComplete: true,
31+
urlPrefix: `http://localhost:${webhooksPort}`,
32+
payloadSizeLimit: "10mb",
33+
},
34+
connections: [
35+
{
36+
roomId: "my-room",
37+
stateKey: "foo",
38+
connectionType: GenericHookConnection.CanonicalEventType,
39+
state: {
40+
name: "My hook",
41+
} satisfies GenericHookConnectionState,
42+
},
43+
],
44+
listeners: [
45+
{
46+
port: webhooksPort,
47+
bindAddress: "0.0.0.0",
48+
// Bind to the SAME listener to ensure we don't have conflicts.
49+
resources: ["webhooks"],
50+
},
51+
],
52+
},
53+
});
54+
await testEnv.setUp();
55+
}, E2ESetupTestTimeout);
56+
57+
afterAll(() => {
58+
return testEnv?.tearDown();
59+
});
60+
61+
afterEach(() => {
62+
vitest.useRealTimers();
63+
});
64+
65+
test("should be able to handle an incoming request.", async () => {
66+
const user = testEnv.getUser("user");
67+
const roomId = testEnv.connectionRooms["my-room"];
68+
await user.joinRoom(roomId);
69+
const url = new URL(
70+
testEnv.opts.config?.generic?.urlPrefix! + "/webhook/foo",
71+
);
72+
73+
const expectedMsg = user.waitForRoomEvent({
74+
eventType: "m.room.message",
75+
sender: testEnv.botMxid,
76+
roomId,
77+
});
78+
79+
const req = await fetch(url, {
80+
method: "PUT",
81+
body: "Hello world",
82+
});
83+
expect(req.status).toEqual(200);
84+
expect(await req.json()).toEqual({ ok: true });
85+
expect((await expectedMsg).data.content).toEqual({
86+
msgtype: "m.notice",
87+
body: "Received webhook data: Hello world",
88+
formatted_body: "<p>Received webhook data: Hello world</p>",
89+
format: "org.matrix.custom.html",
90+
"uk.half-shot.hookshot.webhook_data": "Hello world",
91+
});
92+
});
93+
});

spec/util/e2e-test.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { TestHomeServer, createHS, destroyHS } from "./homerunner";
22
import {
3-
Appservice,
43
IAppserviceRegistration,
54
MatrixClient,
65
Membership,
@@ -12,7 +11,6 @@ import { BridgeConfig, BridgeConfigRoot } from "../../src/config/Config";
1211
import { start } from "../../src/App/BridgeApp";
1312
import { RSAKeyPairOptions, generateKeyPair } from "node:crypto";
1413
import path from "node:path";
15-
import Redis from "ioredis";
1614
import {
1715
BridgeConfigActorPermission,
1816
BridgeConfigServicePermission,
@@ -21,11 +19,15 @@ import { TestContainers } from "testcontainers";
2119

2220
const WAIT_EVENT_TIMEOUT = 20000;
2321
export const E2ESetupTestTimeout = 60000;
24-
const REDIS_DATABASE_URI =
25-
process.env.HOOKSHOT_E2E_REDIS_DB_URI ?? "redis://localhost:6379";
2622

2723
interface Opts<ML extends string> {
2824
matrixLocalparts?: ML[];
25+
staticConnectionRooms?: Record<
26+
string,
27+
{
28+
members: string[];
29+
}
30+
>;
2931
permissionsRoom?: {
3032
members: string[];
3133
permissions: Array<BridgeConfigServicePermission>;
@@ -378,6 +380,7 @@ export class E2ETestEnv<ML extends string = string> {
378380

379381
const registration: IAppserviceRegistration = {
380382
id: "hookshot",
383+
url: null,
381384
as_token: homeserver.asToken,
382385
hs_token: homeserver.hsToken,
383386
sender_localpart: "hookshot",
@@ -394,9 +397,22 @@ export class E2ETestEnv<ML extends string = string> {
394397
"de.sorunome.msc2409.push_ephemeral": true,
395398
};
396399

400+
const connectionRooms: Record<string, string> = {};
401+
402+
const botClient = new MatrixClient(homeserver.url, homeserver.asToken);
403+
for (const [roomIdMapper, roomOpts] of Object.entries(
404+
opts.staticConnectionRooms ?? {},
405+
)) {
406+
connectionRooms[roomIdMapper] = await botClient.createRoom({
407+
name: `${roomIdMapper}`,
408+
invite: roomOpts.members.map(
409+
(localpart) => `@${localpart}:${homeserver.domain}`,
410+
),
411+
});
412+
}
413+
397414
let permissions: BridgeConfigActorPermission[] = [];
398415
if (opts.permissionsRoom) {
399-
const botClient = new MatrixClient(homeserver.url, homeserver.asToken);
400416
const permsRoom = await botClient.createRoom({
401417
name: "Permissions room",
402418
invite: opts.permissionsRoom.members.map(
@@ -448,11 +464,15 @@ export class E2ETestEnv<ML extends string = string> {
448464
cache: cacheConfig,
449465
permissions,
450466
...providedConfig,
467+
connections: opts.config?.connections?.map((c) => ({
468+
...c,
469+
roomId: connectionRooms[c.roomId],
470+
})),
451471
});
452472
const app = await start(config, registration);
453473
app.listener.finaliseListeners();
454474

455-
return new E2ETestEnv(homeserver, app, opts, config, dir);
475+
return new E2ETestEnv(homeserver, app, opts, config, dir, connectionRooms);
456476
}
457477

458478
private constructor(
@@ -461,6 +481,7 @@ export class E2ETestEnv<ML extends string = string> {
461481
public readonly opts: Opts<ML>,
462482
private readonly config: BridgeConfig,
463483
private readonly dir: string,
484+
public readonly connectionRooms: Record<string, string>,
464485
) {
465486
const appService = app.appservice;
466487

src/Bridge.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ import {
1616
PLManager,
1717
} from "matrix-bot-sdk";
1818
import BotUsersManager from "./managers/BotUsersManager";
19-
import {
20-
BridgeConfig,
21-
BridgePermissionLevel,
22-
GitLabInstance,
23-
} from "./config/Config";
19+
import { BridgeConfig, BridgePermissionLevel } from "./config/Config";
2420
import { BridgeWidgetApi } from "./widgets/BridgeWidgetApi";
2521
import { CommentProcessor } from "./CommentProcessor";
2622
import { ConnectionManager } from "./ConnectionManager";
@@ -111,6 +107,7 @@ import { OpenProjectWebhookPayloadWorkPackage } from "./openproject/Types";
111107
import { OpenProjectConnection } from "./Connections/OpenProjectConnection";
112108
import { OAuthRequest, OAuthRequestResult } from "./tokens/Oauth";
113109
import { IJsonType } from "matrix-bot-sdk/lib/helpers/Types";
110+
import { GitLabInstance } from "./config/sections";
114111

115112
const log = new Logger("Bridge");
116113

0 commit comments

Comments
 (0)