Skip to content

Commit a086768

Browse files
authored
Merge pull request #761 from the-draupnir-project/gnuxie/room-takedown
Support for room policies with hashed entity and `org.matrix.msc4204.takedown` recommendation in Synapse Story: the-draupnir-project/planning#41 documentation: https://the-draupnir-project.github.io/draupnir-documentation/protections/room-takedown-protection This PR introduces room takedown functionality into Draupnir. A new `draupnir takedown` command is added similar to the ban command, but marks entities to be taken down. Because the content is illegal or intolerable. To begin with we only allow takedown of rooms. These takedown policies are sharable with policy lists just like normal bans. Draupnir responds to takedown policies on Synapse by calling the [room shutdown](https://element-hq.github.io/synapse/latest/admin_api/rooms.html#version-2-new-version) API with the options `block` and `purge`. The policies that are created by the takedown command are hashed, and this is in order to prevent the room id's being shared directly, and so that we do not create a directory of intolerable content. To be able to use the policies, draupnir therefore needs to be aware of all the rooms that the homeserver is participating in, in order to calculate their hashes and find matching policies, and then takedown the marked rooms. As part of this process, Draupnir has to "discover" the rooms your server is participating in. This is done via the [synapse-http-antispam](https://the-draupnir-project.github.io/draupnir-documentation/bot/synapse-http-antispam) recently added to draupnir. When draupnir discovers rooms, it will prompt the management room with a notification with some details of the title, room description, and creator. This functionality will be toggleable but will be strongly recommended for servers that have public registration
2 parents d6f06c1 + 903a7b6 commit a086768

Some content is hidden

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

48 files changed

+2919
-306
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"@sentry/node": "^7.17.2",
5454
"@sinclair/typebox": "0.34.13",
5555
"@the-draupnir-project/interface-manager": "4.0.2",
56-
"@the-draupnir-project/matrix-basic-types": "1.2.0",
56+
"@the-draupnir-project/matrix-basic-types": "1.3.0",
5757
"better-sqlite3": "^9.4.3",
5858
"body-parser": "^1.20.2",
5959
"config": "^3.3.9",
@@ -63,8 +63,8 @@
6363
"jsdom": "^24.0.0",
6464
"matrix-appservice-bridge": "^10.3.1",
6565
"matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.7.1-element.6",
66-
"matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@2.10.0",
67-
"matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@2.10.1",
66+
"matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@3.0.0",
67+
"matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@3.0.0",
6868
"pg": "^8.8.0",
6969
"yaml": "^2.3.2"
7070
},

src/Draupnir.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import {
8282
makeConfirmationPromptListener,
8383
} from "./commands/interface-manager/MatrixPromptForConfirmation";
8484
import { SynapseHttpAntispam } from "./webapis/SynapseHTTPAntispam/SynapseHttpAntispam";
85+
import { DraupnirStores } from "./backingstore/DraupnirStores";
8586
const log = new Logger("Draupnir");
8687

8788
// webAPIS should not be included on the Draupnir class.
@@ -145,6 +146,7 @@ export class Draupnir implements Client, MatrixAdaptorContext {
145146
public readonly acceptInvitesFromRoom: MatrixRoomID,
146147
public readonly acceptInvitesFromRoomIssuer: RoomMembershipRevisionIssuer,
147148
public readonly safeModeToggle: SafeModeToggle,
149+
public readonly stores: DraupnirStores,
148150
public readonly synapseAdminClient: SynapseAdminClient | undefined,
149151
public readonly synapseHTTPAntispam: SynapseHttpAntispam | undefined
150152
) {
@@ -212,6 +214,7 @@ export class Draupnir implements Client, MatrixAdaptorContext {
212214
config: IConfig,
213215
loggableConfigTracker: LoggableConfigTracker,
214216
safeModeToggle: SafeModeToggle,
217+
stores: DraupnirStores,
215218
synapseHTTPAntispam: SynapseHttpAntispam | undefined
216219
): Promise<ActionResult<Draupnir>> {
217220
const acceptInvitesFromRoom = await (async () => {
@@ -270,6 +273,7 @@ export class Draupnir implements Client, MatrixAdaptorContext {
270273
acceptInvitesFromRoom.ok,
271274
acceptInvitesFromRoomIssuer.ok,
272275
safeModeToggle,
276+
stores,
273277
new SynapseAdminClient(client, clientUserID),
274278
synapseHTTPAntispam
275279
);
@@ -388,6 +392,7 @@ export class Draupnir implements Client, MatrixAdaptorContext {
388392
this.clientRooms.off("timeline", this.timelineEventListener);
389393
this.reportPoller?.stop();
390394
this.protectedRoomsSet.unregisterListeners();
395+
this.stores.dispose();
391396
}
392397

393398
public createRoomReference(roomID: StringRoomID): MatrixRoomID {

src/DraupnirBotMode.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import {
1212
StandardClientsInRoomMap,
1313
DefaultEventDecoder,
14-
RoomStateBackingStore,
1514
ClientsInRoomMap,
1615
Task,
1716
Logger,
@@ -48,12 +47,16 @@ import { SafeModeDraupnir } from "./safemode/DraupnirSafeMode";
4847
import { ResultError } from "@gnuxie/typescript-result";
4948
import { SafeModeCause, SafeModeReason } from "./safemode/SafeModeCause";
5049
import { SafeModeBootOption } from "./safemode/BootOption";
51-
import { SynapseHttpAntispam } from "./webapis/SynapseHTTPAntispam/SynapseHttpAntispam";
50+
import { TopLevelStores } from "./backingstore/DraupnirStores";
5251

5352
const log = new Logger("DraupnirBotMode");
5453

5554
export function constructWebAPIs(draupnir: Draupnir): WebAPIs {
56-
return new WebAPIs(draupnir.reportManager, draupnir.config);
55+
return new WebAPIs(
56+
draupnir.reportManager,
57+
draupnir.config,
58+
draupnir.synapseHTTPAntispam
59+
);
5760
}
5861

5962
/**
@@ -74,20 +77,13 @@ interface BotModeTogle extends SafeModeToggle {
7477
error: ResultError,
7578
options?: SafeModeToggleOptions
7679
): Promise<Result<SafeModeDraupnir>>;
77-
// The SynapseHTTPAntispam listeners, if available.
78-
// Which they won't be for some bot mode and all application service users.
79-
readonly synapseHTTPAntispam: SynapseHttpAntispam | undefined;
8080
}
8181

8282
export class DraupnirBotModeToggle implements BotModeTogle {
8383
private draupnir: Draupnir | null = null;
8484
private safeModeDraupnir: SafeModeDraupnir | null = null;
8585
private webAPIs: WebAPIs | null = null;
8686

87-
public get synapseHTTPAntispam() {
88-
return this.webAPIs?.synapseHTTPAntispam ?? undefined;
89-
}
90-
9187
private constructor(
9288
private readonly clientUserID: StringUserID,
9389
private readonly managementRoom: MatrixRoomID,
@@ -113,7 +109,7 @@ export class DraupnirBotModeToggle implements BotModeTogle {
113109
client: MatrixSendClient,
114110
matrixEmitter: SafeMatrixEmitter,
115111
config: IConfig,
116-
backingStore?: RoomStateBackingStore
112+
stores: TopLevelStores
117113
): Promise<DraupnirBotModeToggle> {
118114
const clientUserID = await client.getUserId();
119115
if (!isStringUserID(clientUserID)) {
@@ -163,13 +159,15 @@ export class DraupnirBotModeToggle implements BotModeTogle {
163159
clientsInRoomMap,
164160
clientProvider,
165161
DefaultEventDecoder,
166-
backingStore
162+
stores.roomStateBackingStore,
163+
stores.hashStore
167164
);
168165
const draupnirFactory = new DraupnirFactory(
169166
clientsInRoomMap,
170167
clientCapabilityFactory,
171168
clientProvider,
172-
roomStateManagerFactory
169+
roomStateManagerFactory,
170+
stores
173171
);
174172
return new DraupnirBotModeToggle(
175173
clientUserID,

src/appservice/AppService.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import {
3535
ClientsInRoomMap,
3636
DefaultEventDecoder,
3737
EventDecoder,
38-
RoomStateBackingStore,
3938
StandardClientsInRoomMap,
4039
Task,
4140
isError,
@@ -49,6 +48,7 @@ import {
4948
StringUserID,
5049
} from "@the-draupnir-project/matrix-basic-types";
5150
import { SqliteRoomStateBackingStore } from "../backingstore/better-sqlite3/SqliteRoomStateBackingStore";
51+
import { TopLevelStores } from "../backingstore/DraupnirStores";
5252

5353
const log = new Logger("AppService");
5454
/**
@@ -100,7 +100,7 @@ export class MjolnirAppService {
100100
dataStore: DataStore,
101101
eventDecoder: EventDecoder,
102102
registrationFilePath: string,
103-
backingStore?: RoomStateBackingStore
103+
stores: TopLevelStores
104104
) {
105105
const bridge = new Bridge({
106106
homeserverUrl: config.homeserver.url,
@@ -128,7 +128,8 @@ export class MjolnirAppService {
128128
clientsInRoomMap,
129129
clientProvider,
130130
eventDecoder,
131-
backingStore
131+
stores.roomStateBackingStore,
132+
stores.hashStore
132133
);
133134
const clientCapabilityFactory = new ClientCapabilityFactory(
134135
clientsInRoomMap,
@@ -190,6 +191,7 @@ export class MjolnirAppService {
190191
bridge,
191192
accessControl,
192193
roomStateManagerFactory,
194+
stores,
193195
clientCapabilityFactory,
194196
clientProvider,
195197
instanceCountGauge
@@ -239,7 +241,7 @@ export class MjolnirAppService {
239241
dataStore,
240242
DefaultEventDecoder,
241243
registrationFilePath,
242-
backingStore
244+
{ roomStateBackingStore: backingStore } // we don't support any stores in appservice atm except backing store.
243245
);
244246
// The call to `start` MUST happen last. As it needs the datastore, and the mjolnir manager to be initialized before it can process events from the homeserver.
245247
await service.start(port);

src/appservice/AppServiceDraupnirManager.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
userLocalpart,
4646
isStringRoomID,
4747
} from "@the-draupnir-project/matrix-basic-types";
48+
import { TopLevelStores } from "../backingstore/DraupnirStores";
4849

4950
const log = new Logger("AppServiceDraupnirManager");
5051

@@ -63,6 +64,7 @@ export class AppServiceDraupnirManager {
6364
private readonly bridge: Bridge,
6465
private readonly accessControl: AccessControl,
6566
private readonly roomStateManagerFactory: RoomStateManagerFactory,
67+
stores: TopLevelStores,
6668
private readonly clientCapabilityFactory: ClientCapabilityFactory,
6769
clientProvider: ClientForUserID,
6870
private readonly instanceCountGauge: Gauge<"status" | "uuid">
@@ -71,7 +73,8 @@ export class AppServiceDraupnirManager {
7173
this.roomStateManagerFactory.clientsInRoomMap,
7274
this.clientCapabilityFactory,
7375
clientProvider,
74-
this.roomStateManagerFactory
76+
this.roomStateManagerFactory,
77+
stores
7578
);
7679
this.baseManager = new StandardDraupnirManager(draupnirFactory);
7780
}
@@ -93,6 +96,7 @@ export class AppServiceDraupnirManager {
9396
bridge: Bridge,
9497
accessControl: AccessControl,
9598
roomStateManagerFactory: RoomStateManagerFactory,
99+
stores: TopLevelStores,
96100
clientCapabilityFactory: ClientCapabilityFactory,
97101
clientProvider: ClientForUserID,
98102
instanceCountGauge: Gauge<"status" | "uuid">
@@ -103,6 +107,7 @@ export class AppServiceDraupnirManager {
103107
bridge,
104108
accessControl,
105109
roomStateManagerFactory,
110+
stores,
106111
clientCapabilityFactory,
107112
clientProvider,
108113
instanceCountGauge

src/backingstore/DraupnirStores.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// SPDX-FileCopyrightText: 2025 Gnuxie <[email protected]>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
import { EventDecoder, SHA256HashStore } from "matrix-protection-suite";
6+
import { RoomAuditLog } from "../protections/RoomTakedown/RoomAuditLog";
7+
import { SqliteRoomStateBackingStore } from "./better-sqlite3/SqliteRoomStateBackingStore";
8+
import { SqliteHashReversalStore } from "./better-sqlite3/HashStore";
9+
import { SqliteRoomAuditLog } from "../protections/RoomTakedown/SqliteRoomAuditLog";
10+
11+
export type TopLevelStores = {
12+
hashStore?: SHA256HashStore;
13+
roomAuditLog?: RoomAuditLog | undefined;
14+
roomStateBackingStore?: SqliteRoomStateBackingStore | undefined;
15+
};
16+
17+
/**
18+
* These stores will usually be created at the entrypoint of the draupnir
19+
* application or attenuated for each draupnir.
20+
*
21+
* No i don't like it. Some of these stores ARE specific to the draupnir
22+
* such as the hasStore's event emitter... that just can't be mixed.
23+
*
24+
* We could create a wrapper that disposes of only the stores that
25+
* have been attenuated... but i don't know about it.
26+
*/
27+
export type DraupnirStores = {
28+
hashStore?: SHA256HashStore | undefined;
29+
roomAuditLog?: RoomAuditLog | undefined;
30+
/**
31+
* Dispose of stores relevant to a specific draupnir instance.
32+
* For example, the hash store is usually specific to a single draupnir.
33+
*/
34+
dispose(): void;
35+
};
36+
37+
export function createDraupnirStores(
38+
topLevelStores: TopLevelStores
39+
): DraupnirStores {
40+
return Object.freeze({
41+
roomAuditLog: topLevelStores.roomAuditLog,
42+
hashStore: topLevelStores.hashStore,
43+
dispose() {},
44+
} satisfies DraupnirStores);
45+
}
46+
47+
export function makeTopLevelStores(
48+
storagePath: string,
49+
eventDecoder: EventDecoder,
50+
{
51+
isRoomStateBackingStoreEnabled,
52+
}: { isRoomStateBackingStoreEnabled: boolean }
53+
): TopLevelStores {
54+
return Object.freeze({
55+
roomStateBackingStore: isRoomStateBackingStoreEnabled
56+
? SqliteRoomStateBackingStore.create(storagePath, eventDecoder)
57+
: undefined,
58+
hashStore: SqliteHashReversalStore.createToplevel(storagePath),
59+
roomAuditLog: SqliteRoomAuditLog.createToplevel(storagePath),
60+
} satisfies TopLevelStores);
61+
}

0 commit comments

Comments
 (0)