Merge pull request #794 from the-draupnir-project/gnuxie/user-suspension

Add commands to suspend/unsuspend users via the synapse admin API

- I have no idea how this interacts with MAS
- There is no locking available in the synapse admin API.
This commit is contained in:
Gnuxie authored and GitHub committed 2025-03-29 20:20:55 +00:00
commit 5014489cc8
8 files changed
+182 -21

No files matched your search

+1 -1
View File
@@ -64,7 +64,7 @@
"matrix-appservice-bridge": "^10.3.1",
"matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.7.1-element.6",
"matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@3.1.0",
"matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@3.1.0",
"matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@3.1.1",
"pg": "^8.8.0",
"yaml": "^2.3.2"
},
+11 -5
View File
@@ -15,9 +15,9 @@ import {
} from "./AliasCommands";
import { DraupnirBanCommand } from "./Ban";
import { DraupnirListCreateCommand } from "./CreateBanListCommand";
import { SynapseAdminDeactivateCommand } from "./DeactivateCommand";
import { SynapseAdminDeactivateCommand } from "./server-admin/DeactivateCommand";
import { DraupnirHelpCommand } from "./Help";
import { SynapseAdminHijackRoomCommand } from "./HijackRoomCommand";
import { SynapseAdminHijackRoomCommand } from "./server-admin/HijackRoomCommand";
import { DraupnirImportCommand } from "./ImportCommand";
import { DraupnirKickCommand } from "./KickCommand";
import {
@@ -43,7 +43,7 @@ import {
} from "./Rules";
import { DraupnirDisplaynameCommand } from "./SetDisplayNameCommand";
import { DraupnirSetPowerLevelCommand } from "./SetPowerLevelCommand";
import { SynapseAdminShutdownRoomCommand } from "./ShutdownRoomCommand";
import { SynapseAdminShutdownRoomCommand } from "./server-admin/ShutdownRoomCommand";
import { DraupnirStatusCommand } from "./StatusCommand";
import { DraupnirUnbanCommand } from "./unban/Unban";
import {
@@ -55,14 +55,20 @@ import { DraupnirSafeModeCommand } from "./SafeModeCommand";
import { DraupnirProtectionsShowCommand } from "./ProtectionsShowCommand";
import { DraupnirProtectionsCapabilityCommand } from "./ProtectionsCapabilitiesCommand";
import { JoinWaveCommandTable } from "../protections/JoinWaveShortCircuit";
import { DraupnirTakedownCommand } from "./Takedown";
import { DraupnirTakedownCommand } from "./server-admin/Takedown";
import {
SynapseAdminSuspendUserCommand,
SynapseAdminUnsuspendUserCommand,
} from "./server-admin/SuspendCommand";
// TODO: These commands should all be moved to subdirectories tbh and this
// should be split like an index file for each subdirectory.
export const SynapseAdminCommands = new StandardCommandTable("synapse admin")
.internCommand(SynapseAdminDeactivateCommand, ["deactivate"])
.internCommand(SynapseAdminHijackRoomCommand, ["hijack", "room"])
.internCommand(SynapseAdminShutdownRoomCommand, ["shutdown", "room"]);
.internCommand(SynapseAdminShutdownRoomCommand, ["shutdown", "room"])
.internCommand(SynapseAdminSuspendUserCommand, ["suspend"])
.internCommand(SynapseAdminUnsuspendUserCommand, ["unsuspend"]);
const DraupnirCommands = new StandardCommandTable("draupnir")
.internCommand(DraupnirAliasMoveCommand, ["alias", "move"])
@@ -15,8 +15,8 @@ import {
tuple,
} from "@the-draupnir-project/interface-manager";
import { ActionError } from "matrix-protection-suite";
import { Draupnir } from "../Draupnir";
import { DraupnirInterfaceAdaptor } from "./DraupnirCommandPrerequisites";
import { Draupnir } from "../../Draupnir";
import { DraupnirInterfaceAdaptor } from "../DraupnirCommandPrerequisites";
export const SynapseAdminDeactivateCommand = describeCommand({
summary: "Deactivate a user on the homeserver.",
@@ -21,8 +21,8 @@ import {
describeCommand,
tuple,
} from "@the-draupnir-project/interface-manager";
import { Draupnir } from "../Draupnir";
import { DraupnirInterfaceAdaptor } from "./DraupnirCommandPrerequisites";
import { Draupnir } from "../../Draupnir";
import { DraupnirInterfaceAdaptor } from "../DraupnirCommandPrerequisites";
export const SynapseAdminHijackRoomCommand = describeCommand({
summary:
@@ -15,9 +15,9 @@ import {
describeCommand,
tuple,
} from "@the-draupnir-project/interface-manager";
import { Draupnir } from "../Draupnir";
import { Draupnir } from "../../Draupnir";
import { ActionError } from "matrix-protection-suite";
import { DraupnirInterfaceAdaptor } from "./DraupnirCommandPrerequisites";
import { DraupnirInterfaceAdaptor } from "../DraupnirCommandPrerequisites";
export const SynapseAdminShutdownRoomCommand = describeCommand({
summary:
@@ -0,0 +1,155 @@
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
//
// SPDX-License-Identifier: AFL-3.0
import {
DeadDocumentJSX,
describeCommand,
MatrixUserIDPresentationType,
tuple,
} from "@the-draupnir-project/interface-manager";
import { Draupnir } from "../../Draupnir";
import { isError, Ok, Result, ResultError } from "@gnuxie/typescript-result";
import { StringUserID } from "@the-draupnir-project/matrix-basic-types";
import { DraupnirInterfaceAdaptor } from "../DraupnirCommandPrerequisites";
import { renderMentionPill } from "../interface-manager/MatrixHelpRenderer";
type SuspensionPreview = {
userID: StringUserID;
};
export const SynapseAdminSuspendUserCommand = describeCommand({
summary:
"Suspend a user on the homeserver, this allows them to login but not interact with anything",
parameters: tuple({
name: "user",
description: "The user to suspend",
acceptor: MatrixUserIDPresentationType,
}),
keywords: {
keywordDescriptions: {
"no-confirm": {
isFlag: true,
description:
"Runs the command without the preview of the unban and the confirmation prompt.",
},
},
},
async executor(
draupnir: Draupnir,
_info,
keywords,
_rest,
targetUser
): Promise<Result<SuspensionPreview>> {
const isAdmin = await draupnir.synapseAdminClient?.isSynapseAdmin();
if (
draupnir.synapseAdminClient === undefined ||
isAdmin === undefined ||
isError(isAdmin) ||
!isAdmin.ok
) {
return ResultError.Result(
"I am not a Synapse administrator, or the endpoint to deactivate a user is blocked"
);
}
const preview = {
userID: targetUser.toString(),
};
const isNoConfirm = keywords.getKeywordValue<boolean>("no-confirm", false);
if (!isNoConfirm) {
return Ok(preview);
}
const suspensionResult = await draupnir.synapseAdminClient.suspendUser(
targetUser.toString()
);
if (isError(suspensionResult)) {
return suspensionResult;
} else {
return Ok(preview);
}
},
});
DraupnirInterfaceAdaptor.describeRenderer(SynapseAdminSuspendUserCommand, {
isAlwaysSupposedToUseDefaultRenderer: true,
confirmationPromptJSXRenderer(commandResult) {
if (isError(commandResult)) {
return Ok(undefined);
}
return Ok(
<root>
You are about to suspend the user{" "}
{renderMentionPill(commandResult.ok.userID, commandResult.ok.userID)}{" "}
<code>{commandResult.ok.userID}</code>. Doing so will prevent further
activity from their account. However, they will still be able to login,
manage their account, and redact messages. See
https://spec.matrix.org/v1.14/client-server-api/#account-suspension for
a complete explanation.
</root>
);
},
JSXRenderer(commandResult) {
if (isError(commandResult)) {
return Ok(undefined);
}
return Ok(
<root>
The user <code>{commandResult.ok.userID}</code> has been suspended.
</root>
);
},
});
export const SynapseAdminUnsuspendUserCommand = describeCommand({
summary: "Unsuspend a user on the homeserver that was previously suspended",
parameters: tuple({
name: "user",
description: "The user to unsuspend",
acceptor: MatrixUserIDPresentationType,
}),
async executor(
draupnir: Draupnir,
_info,
_keywords,
_rest,
targetUser
): Promise<Result<SuspensionPreview>> {
const isAdmin = await draupnir.synapseAdminClient?.isSynapseAdmin();
if (
draupnir.synapseAdminClient === undefined ||
isAdmin === undefined ||
isError(isAdmin) ||
!isAdmin.ok
) {
return ResultError.Result(
"I am not a Synapse administrator, or the endpoint to deactivate a user is blocked"
);
}
const preview = {
userID: targetUser.toString(),
};
const suspensionResult = await draupnir.synapseAdminClient.unsuspendUser(
targetUser.toString()
);
if (isError(suspensionResult)) {
return suspensionResult;
} else {
return Ok(preview);
}
},
});
DraupnirInterfaceAdaptor.describeRenderer(SynapseAdminUnsuspendUserCommand, {
isAlwaysSupposedToUseDefaultRenderer: true,
JSXRenderer(commandResult) {
if (isError(commandResult)) {
return Ok(undefined);
}
return Ok(
<root>
The user <code>{commandResult.ok.userID}</code> has been unsuspended.
</root>
);
},
});
@@ -14,7 +14,7 @@ import {
tuple,
union,
} from "@the-draupnir-project/interface-manager";
import { findPolicyRoomEditorFromRoomReference } from "./Ban";
import { findPolicyRoomEditorFromRoomReference } from "../Ban";
import {
isError,
Logger,
@@ -38,10 +38,10 @@ import {
import {
DraupnirContextToCommandContextTranslator,
DraupnirInterfaceAdaptor,
} from "./DraupnirCommandPrerequisites";
import { renderRoomPill } from "./interface-manager/MatrixHelpRenderer";
import { RoomDetailsProvider } from "../capabilities/RoomTakedownCapability";
import { SynapseAdminRoomDetailsProvider } from "../capabilities/SynapseAdminRoomTakedown/SynapseAdminRoomTakedown";
} from "../DraupnirCommandPrerequisites";
import { renderRoomPill } from "../interface-manager/MatrixHelpRenderer";
import { RoomDetailsProvider } from "../../capabilities/RoomTakedownCapability";
import { SynapseAdminRoomDetailsProvider } from "../../capabilities/SynapseAdminRoomTakedown/SynapseAdminRoomTakedown";
const log = new Logger("DraupnirTakedownCommand");
+4 -4
View File
@@ -2593,10 +2593,10 @@ matrix-appservice@^2.0.0:
request-promise "^4.2.6"
sanitize-html "^2.11.0"
"matrix-protection-suite-for-matrix-bot-sdk@npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite-for-matrix-bot-sdk/-/matrix-protection-suite-for-matrix-bot-sdk-3.1.0.tgz#36ed640f6425f568e85c880b6e3da87019e06db5"
integrity sha512-dgagHqAupTj7L/MAdXxwXFxNNceGALuMUr05WfnfBUfkbeI0xiC2rPtwKzh3xO2crjSYx52UE+59V73koHzDpQ==
"matrix-protection-suite-for-matrix-bot-sdk@npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite-for-matrix-bot-sdk/-/matrix-protection-suite-for-matrix-bot-sdk-3.1.1.tgz#1bdfadd16a3e69677a6dd694d6288fd967764a2d"
integrity sha512-jzOFhKt24FiSmK5Ej0khaPfBder607yFX1vg5dvDSv6q/GrHrkFB5poq4TGtu+x+gRIDNAe2zfEOriK1p/qyoQ==
dependencies:
"@gnuxie/typescript-result" "^1.0.0"
await-lock "^2.2.2"