mirror of
https://github.com/matrix-org/mjolnir.git
synced 2024-10-01 01:36:06 -04:00
Add commands to suspend/unsuspend user (#506)
This commit is contained in:
parent
033d84cc70
commit
2f6120eca9
@ -35,6 +35,7 @@ modules:
|
|||||||
|
|
||||||
|
|
||||||
homeserver:
|
homeserver:
|
||||||
|
docker: element-hq/synapse:latest
|
||||||
# Basic configuration.
|
# Basic configuration.
|
||||||
server_name: localhost:9999
|
server_name: localhost:9999
|
||||||
public_baseurl: http://localhost:9999
|
public_baseurl: http://localhost:9999
|
||||||
@ -76,6 +77,10 @@ homeserver:
|
|||||||
per_second: 10000
|
per_second: 10000
|
||||||
burst_count: 10000
|
burst_count: 10000
|
||||||
|
|
||||||
|
experimental_features:
|
||||||
|
msc3823_account_suspension: true
|
||||||
|
|
||||||
|
|
||||||
# Creating a few users simplifies testing.
|
# Creating a few users simplifies testing.
|
||||||
users:
|
users:
|
||||||
- localname: admin
|
- localname: admin
|
||||||
|
@ -479,6 +479,19 @@ export class Mjolnir {
|
|||||||
return await this.client.doRequest("POST", endpoint);
|
return await this.client.doRequest("POST", endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async suspendSynapseUser(userId: string): Promise<any> {
|
||||||
|
const endpoint = `/_synapse/admin/v1/suspend/${userId}`;
|
||||||
|
const body = {"suspend": true}
|
||||||
|
return await this.client.doRequest("PUT", endpoint, null, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async unsuspendSynapseUser(userId: string): Promise<any> {
|
||||||
|
const endpoint = `/_synapse/admin/v1/suspend/${userId}`;
|
||||||
|
const body = {"suspend": false}
|
||||||
|
return await this.client.doRequest("PUT", endpoint, null, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async shutdownSynapseRoom(roomId: string, message?: string): Promise<any> {
|
public async shutdownSynapseRoom(roomId: string, message?: string): Promise<any> {
|
||||||
const endpoint = `/_synapse/admin/v1/rooms/${roomId}`;
|
const endpoint = `/_synapse/admin/v1/rooms/${roomId}`;
|
||||||
return await this.client.doRequest("DELETE", endpoint, null, {
|
return await this.client.doRequest("DELETE", endpoint, null, {
|
||||||
|
@ -43,6 +43,8 @@ import { execMakeRoomAdminCommand } from "./MakeRoomAdminCommand";
|
|||||||
import { parse as tokenize } from "shell-quote";
|
import { parse as tokenize } from "shell-quote";
|
||||||
import { execSinceCommand } from "./SinceCommand";
|
import { execSinceCommand } from "./SinceCommand";
|
||||||
import { execSetupProtectedRoom } from "./SetupDecentralizedReportingCommand";
|
import { execSetupProtectedRoom } from "./SetupDecentralizedReportingCommand";
|
||||||
|
import {execSuspendCommand} from "./SuspendCommand";
|
||||||
|
import {execUnsuspendCommand} from "./UnsuspendCommand";
|
||||||
|
|
||||||
|
|
||||||
export const COMMAND_PREFIX = "!mjolnir";
|
export const COMMAND_PREFIX = "!mjolnir";
|
||||||
@ -128,6 +130,10 @@ export async function handleCommand(roomId: string, event: { content: { body: st
|
|||||||
return await execKickCommand(roomId, event, mjolnir, parts);
|
return await execKickCommand(roomId, event, mjolnir, parts);
|
||||||
} else if (parts[1] === 'make' && parts[2] === 'admin' && parts.length > 3) {
|
} else if (parts[1] === 'make' && parts[2] === 'admin' && parts.length > 3) {
|
||||||
return await execMakeRoomAdminCommand(roomId, event, mjolnir, parts);
|
return await execMakeRoomAdminCommand(roomId, event, mjolnir, parts);
|
||||||
|
} else if (parts[1] === 'suspend' && parts.length > 2) {
|
||||||
|
return await execSuspendCommand(roomId, event, mjolnir, parts);
|
||||||
|
} else if (parts[1] === 'unsuspend' && parts.length > 2) {
|
||||||
|
return await execUnsuspendCommand(roomId, event, mjolnir, parts)
|
||||||
} else {
|
} else {
|
||||||
// Help menu
|
// Help menu
|
||||||
const menu = "" +
|
const menu = "" +
|
||||||
@ -170,7 +176,9 @@ export async function handleCommand(roomId: string, event: { content: { body: st
|
|||||||
"!mjolnir shutdown room <room alias/ID> [message] - Uses the bot's account to shut down a room, preventing access to the room on this server\n" +
|
"!mjolnir shutdown room <room alias/ID> [message] - Uses the bot's account to shut down a room, preventing access to the room on this server\n" +
|
||||||
"!mjolnir powerlevel <user ID> <power level> [room alias/ID] - Sets the power level of the user in the specified room (or all protected rooms)\n" +
|
"!mjolnir powerlevel <user ID> <power level> [room alias/ID] - Sets the power level of the user in the specified room (or all protected rooms)\n" +
|
||||||
"!mjolnir make admin <room alias> [user alias/ID] - Make the specified user or the bot itself admin of the room\n" +
|
"!mjolnir make admin <room alias> [user alias/ID] - Make the specified user or the bot itself admin of the room\n" +
|
||||||
"!mjolnir help - This menu\n";
|
"!mjolnir suspend <user ID> - Suspend the specified user" +
|
||||||
|
"!mjolnir unsuspend <user ID> - Unsuspend the specified user" +
|
||||||
|
"!mjolnir help - This menu\n"
|
||||||
const html = `<b>Mjolnir help:</b><br><pre><code>${htmlEscape(menu)}</code></pre>`;
|
const html = `<b>Mjolnir help:</b><br><pre><code>${htmlEscape(menu)}</code></pre>`;
|
||||||
const text = `Mjolnir help:\n${menu}`;
|
const text = `Mjolnir help:\n${menu}`;
|
||||||
const reply = RichReply.createFor(roomId, event, text, html);
|
const reply = RichReply.createFor(roomId, event, text, html);
|
||||||
|
38
src/commands/SuspendCommand.ts
Normal file
38
src/commands/SuspendCommand.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Mjolnir} from "../Mjolnir";
|
||||||
|
import {RichReply} from "matrix-bot-sdk";
|
||||||
|
|
||||||
|
export async function execSuspendCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||||
|
const target = parts[2];
|
||||||
|
|
||||||
|
const isAdmin = await mjolnir.isSynapseAdmin();
|
||||||
|
if (!isAdmin) {
|
||||||
|
const message = "I am not a Synapse administrator, or the endpoint is blocked";
|
||||||
|
const reply = RichReply.createFor(roomId, event, message, message);
|
||||||
|
reply['msgtype'] = "m.notice";
|
||||||
|
await mjolnir.client.sendMessage(roomId, reply);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mjolnir.suspendSynapseUser(target);
|
||||||
|
const msg = `User ${target} has been suspended.`
|
||||||
|
const confirmation = RichReply.createFor(roomId, event, msg, msg);
|
||||||
|
confirmation['msgtype'] = "m.notice";
|
||||||
|
await mjolnir.client.sendMessage(roomId, confirmation)
|
||||||
|
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
|
||||||
|
}
|
38
src/commands/UnsuspendCommand.ts
Normal file
38
src/commands/UnsuspendCommand.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Mjolnir} from "../Mjolnir";
|
||||||
|
import {RichReply} from "matrix-bot-sdk";
|
||||||
|
|
||||||
|
export async function execUnsuspendCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||||
|
const target = parts[2];
|
||||||
|
|
||||||
|
const isAdmin = await mjolnir.isSynapseAdmin();
|
||||||
|
if (!isAdmin) {
|
||||||
|
const message = "I am not a Synapse administrator, or the endpoint is blocked";
|
||||||
|
const reply = RichReply.createFor(roomId, event, message, message);
|
||||||
|
reply['msgtype'] = "m.notice";
|
||||||
|
mjolnir.client.sendMessage(roomId, reply);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mjolnir.unsuspendSynapseUser(target);
|
||||||
|
const msg = `User ${target}'s suspension has been reversed.`
|
||||||
|
const confirmation = RichReply.createFor(roomId, event, msg, msg);
|
||||||
|
confirmation['msgtype'] = "m.notice";
|
||||||
|
mjolnir.client.sendMessage(roomId, confirmation)
|
||||||
|
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
|
||||||
|
}
|
91
test/integration/commands/suspendCommandTest.ts
Normal file
91
test/integration/commands/suspendCommandTest.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {newTestUser} from "../clientHelper";
|
||||||
|
import {strict as assert} from "assert";
|
||||||
|
import { MatrixClient, RoomCreateOptions } from "matrix-bot-sdk";
|
||||||
|
import { read as configRead } from "../../../src/config";
|
||||||
|
|
||||||
|
describe("Test: suspend/unsuspend command", function () {
|
||||||
|
let admin: MatrixClient;
|
||||||
|
let badUser: MatrixClient;
|
||||||
|
const config = configRead()
|
||||||
|
this.beforeEach(async () => {
|
||||||
|
admin = await newTestUser(config.homeserverUrl, { name: { contains: "suspend-command" }});
|
||||||
|
await admin.start();
|
||||||
|
badUser = await newTestUser(config.homeserverUrl, {name: { contains: "bad-user"}})
|
||||||
|
await badUser.start();
|
||||||
|
})
|
||||||
|
this.afterEach(async function () {
|
||||||
|
admin.stop();
|
||||||
|
badUser.stop();
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Mjolnir asks synapse to suspend and unsuspend a user", async function() {
|
||||||
|
this.timeout(20000);
|
||||||
|
await admin.joinRoom(this.mjolnir.managementRoomId);
|
||||||
|
const roomOption: RoomCreateOptions = {preset: "public_chat"}
|
||||||
|
const room = await admin.createRoom(roomOption);
|
||||||
|
await badUser.joinRoom(room)
|
||||||
|
await admin.joinRoom(room)
|
||||||
|
const badUserID = await badUser.getUserId()
|
||||||
|
|
||||||
|
let reply = new Promise(async (resolve, reject) => {
|
||||||
|
await admin.sendMessage(this.mjolnir.managementRoomId, {msgtype: "m.text", body: `!mjolnir suspend ${badUserID}`});
|
||||||
|
admin.on('room.event', (roomId, event) => {
|
||||||
|
if (
|
||||||
|
roomId === this.mjolnir.managementRoomId
|
||||||
|
&& event?.type === "m.room.message"
|
||||||
|
&& event.sender === this.mjolnir.client.userId
|
||||||
|
&& event.content?.body.endsWith(`User ${badUserID} has been suspended.`)
|
||||||
|
) {
|
||||||
|
resolve(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await reply
|
||||||
|
try {
|
||||||
|
await badUser.sendMessage(room, {msgtype: "m.text", body: `testing`})
|
||||||
|
assert.fail("Bad user successfully sent message.")
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
assert.match(error.message, /ORG.MATRIX.MSC3823.USER_ACCOUNT_SUSPENDED/i, )
|
||||||
|
}
|
||||||
|
|
||||||
|
let reply2 = new Promise(async (resolve, reject) => {
|
||||||
|
await admin.sendMessage(this.mjolnir.managementRoomId, {msgtype: "m.text", body: `!mjolnir unsuspend ${badUserID}`});
|
||||||
|
admin.on('room.event', (roomId, event) => {
|
||||||
|
if (
|
||||||
|
roomId === this.mjolnir.managementRoomId
|
||||||
|
&& event?.type === "m.room.message"
|
||||||
|
&& event.sender === this.mjolnir.client.userId
|
||||||
|
&& event.content?.body.endsWith(`User ${badUserID}'s suspension has been reversed.`)
|
||||||
|
) {
|
||||||
|
resolve(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await reply2
|
||||||
|
|
||||||
|
try {
|
||||||
|
await badUser.sendMessage(room, {msgtype: "m.text", body: `testing`});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
assert.fail("Unable to send message, account not successfully unsuspended.")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user