Skip to content

Commit fe516fc

Browse files
committed
Add initial SynapseAdmin room takedown support.
We need to know add a paginator of somekind for polling the rooms list API.
1 parent 0caae26 commit fe516fc

File tree

4 files changed

+208
-1
lines changed

4 files changed

+208
-1
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// SPDX-FileCopyrightText: 2025 Gnuxie <[email protected]>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
import { Type } from '@sinclair/typebox';
6+
import { EDStatic, StringUserIDSchema } from 'matrix-protection-suite';
7+
8+
export type BlockStatusResponse = EDStatic<typeof BlockStatusResponse>;
9+
export const BlockStatusResponse = Type.Object({
10+
block: Type.Boolean({
11+
description: 'True if the room is blocked, otherwise false.',
12+
}),
13+
user_id: Type.Optional(
14+
Type.Union([StringUserIDSchema], {
15+
description:
16+
"User ID of the person who added the room to the blocking list. Only present if 'block' is true.",
17+
})
18+
),
19+
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// SPDX-FileCopyrightText: 2025 Gnuxie <[email protected]>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
import { Type } from '@sinclair/typebox';
6+
import {
7+
EDStatic,
8+
StringRoomAliasSchema,
9+
StringRoomIDSchema,
10+
StringUserIDSchema,
11+
} from 'matrix-protection-suite';
12+
13+
export type RoomDetailsResponse = EDStatic<typeof RoomDetailsResponse>;
14+
export const RoomDetailsResponse = Type.Object({
15+
room_id: Type.Union(
16+
[StringRoomIDSchema],
17+
Type.String({ description: 'The ID of the room.' })
18+
),
19+
name: Type.Optional(
20+
Type.Union([Type.String(), Type.Null()], {
21+
description: 'The name of the room.',
22+
})
23+
),
24+
topic: Type.Optional(
25+
Type.Union([Type.String(), Type.Null()], {
26+
description: 'The topic of the room.',
27+
})
28+
),
29+
avatar: Type.Optional(
30+
Type.Union([Type.String(), Type.Null()], {
31+
description: 'The mxc URI to the avatar of the room.',
32+
})
33+
),
34+
canonical_alias: Type.Optional(
35+
Type.Union([StringRoomAliasSchema, Type.String(), Type.Null()], {
36+
description: 'The canonical (main) alias address of the room.',
37+
})
38+
),
39+
joined_members: Type.Number({
40+
description: 'How many users are currently in the room.',
41+
}),
42+
joined_local_members: Type.Number({
43+
description: 'How many local users are currently in the room.',
44+
}),
45+
joined_local_devices: Type.Number({
46+
description: 'How many local devices are currently in the room.',
47+
}),
48+
version: Type.String({ description: 'The version of the room as a string.' }),
49+
creator: Type.Union([StringUserIDSchema], {
50+
description: 'The user_id of the room creator.',
51+
}),
52+
encryption: Type.Union([Type.String(), Type.Null()], {
53+
description:
54+
'Algorithm of end-to-end encryption of messages. Null if encryption is not active.',
55+
}),
56+
federatable: Type.Boolean({
57+
description: 'Whether users on other servers can join this room.',
58+
}),
59+
public: Type.Boolean({
60+
description: 'Whether the room is visible in the room directory.',
61+
}),
62+
join_rules: Type.Union(
63+
[
64+
Type.Literal('public'),
65+
Type.Literal('knock'),
66+
Type.Literal('invite'),
67+
Type.Literal('private'),
68+
],
69+
{
70+
description:
71+
'The type of rules used for users wishing to join this room.',
72+
}
73+
),
74+
guest_access: Type.Union(
75+
[Type.Literal('can_join'), Type.Literal('forbidden'), Type.Null()],
76+
{ description: 'Whether guests can join the room.' }
77+
),
78+
history_visibility: Type.Union(
79+
[
80+
Type.Literal('invited'),
81+
Type.Literal('joined'),
82+
Type.Literal('shared'),
83+
Type.Literal('world_readable'),
84+
],
85+
{ description: 'Who can see the room history.' }
86+
),
87+
state_events: Type.Number({
88+
description:
89+
'Total number of state events in the room. Represents the complexity of the room.',
90+
}),
91+
room_type: Type.Union([Type.String(), Type.Null()], {
92+
description:
93+
"The type of the room from the room's creation event, e.g., 'm.space'. Null if not defined.",
94+
}),
95+
forgotten: Type.Boolean({
96+
description: 'Whether all local users have forgotten the room.',
97+
}),
98+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-FileCopyrightText: 2025 Gnuxie <[email protected]>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
import { Type } from '@sinclair/typebox';
6+
import { EDStatic } from 'matrix-protection-suite';
7+
8+
export type SynapseRoomShutdownV2RequestBody = EDStatic<
9+
typeof SynapseRoomShutdownV2RequestBody
10+
>;
11+
export const SynapseRoomShutdownV2RequestBody = Type.Object(
12+
{
13+
new_room_user_id: Type.Optional(
14+
Type.String({
15+
description:
16+
'User ID of the creator/admin for the new room. Must be local but not necessarily registered.',
17+
})
18+
),
19+
room_name: Type.Optional(
20+
Type.String({
21+
description:
22+
"Name of the new room. Defaults to 'Content Violation Notification'.",
23+
})
24+
),
25+
message: Type.Optional(
26+
Type.String({
27+
description:
28+
"First message in the new room. Defaults to 'Sharing illegal content on this server is not permitted and rooms in violation will be blocked.'",
29+
})
30+
),
31+
block: Type.Optional(
32+
Type.Boolean({
33+
description:
34+
'If true, prevents future attempts to join the room. Defaults to false.',
35+
})
36+
),
37+
purge: Type.Optional(
38+
Type.Boolean({
39+
description:
40+
'If true, removes all traces of the room from the database. Defaults to true.',
41+
})
42+
),
43+
force_purge: Type.Optional(
44+
Type.Boolean({
45+
description:
46+
"If true, forces purge even if local users are still in the room. Only applies if 'purge' is true.",
47+
})
48+
),
49+
},
50+
{ minProperties: 1, description: 'Request body must not be empty.' }
51+
);

src/SynapseAdmin/SynapseAdminClient.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ import {
4444
StringUserID,
4545
} from '@the-draupnir-project/matrix-basic-types';
4646
import { Type } from '@sinclair/typebox';
47-
import { resultifyBotSDKRequestError } from '../Client/BotSDKBaseClient';
47+
import {
48+
resultifyBotSDKRequestError,
49+
resultifyBotSDKRequestErrorWith404AsUndefined,
50+
} from '../Client/BotSDKBaseClient';
51+
import { SynapseRoomShutdownV2RequestBody } from './ShutdownV2Endpoint';
52+
import { BlockStatusResponse } from './BlockStatusEndpoint';
53+
import { RoomDetailsResponse } from './RoomDetailsEndpoint';
4854

4955
const ReportPollResponse = Type.Object({
5056
event_reports: Type.Array(SynapseReport),
@@ -177,4 +183,37 @@ export class SynapseAdminClient {
177183
}
178184
return Value.Decode(ReportPollResponse, response.ok);
179185
}
186+
187+
public async shutdownRoomV2(
188+
roomID: StringRoomID,
189+
options: SynapseRoomShutdownV2RequestBody
190+
): Promise<ActionResult<void>> {
191+
const endpoint = `/_synapse/admin/v2/rooms/${encodeURIComponent(roomID)}`;
192+
return await this.client
193+
.doRequest('DELETE', endpoint, null, options)
194+
.then(() => Ok(undefined), resultifyBotSDKRequestError);
195+
}
196+
197+
public async getBlockStatus(
198+
roomID: StringRoomID
199+
): Promise<ActionResult<BlockStatusResponse>> {
200+
const endpoint = `/_synapse/admin/v1/rooms/${encodeURIComponent(
201+
roomID
202+
)}/block`;
203+
return await this.client
204+
.doRequest('GET', endpoint)
205+
.then(
206+
(value) => Value.Decode(BlockStatusResponse, value),
207+
resultifyBotSDKRequestError
208+
);
209+
}
210+
211+
public async getRoomDetails(
212+
roomID: StringRoomID
213+
): Promise<ActionResult<RoomDetailsResponse | undefined>> {
214+
const endpoint = `/_synapse/admin/v1/rooms/${encodeURIComponent(roomID)}`;
215+
return await this.client.doRequest('GET', endpoint).then((value) => {
216+
return Value.Decode(RoomDetailsResponse, value);
217+
}, resultifyBotSDKRequestErrorWith404AsUndefined);
218+
}
180219
}

0 commit comments

Comments
 (0)