Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use admin api for redactions if possible #538

Merged
merged 8 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Mjolnir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ export class Mjolnir {

this.currentState = STATE_RUNNING;
await this.managementRoomOutput.logMessage(LogLevel.INFO, "Mjolnir@startup", "Startup complete. Now monitoring rooms.");
// update protected rooms set
this.protectedRoomsTracker.isAdmin = await this.isSynapseAdmin()
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
} catch (err) {
try {
LogService.error("Mjolnir", "Error during startup:");
Expand Down
7 changes: 6 additions & 1 deletion src/ProtectedRoomsSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ export class ProtectedRoomsSet {
*/
private readonly listRevisions = new Map<PolicyList, /** The last revision we used to sync protected rooms. */ Revision>();

/**
* whether the mjolnir instance is server admin
*/
public isAdmin = false
H-Shay marked this conversation as resolved.
Show resolved Hide resolved

constructor(
private readonly client: MatrixSendClient,
private readonly clientUserId: string,
Expand All @@ -124,7 +129,7 @@ export class ProtectedRoomsSet {
* @param roomId The room we want to redact them in.
*/
public redactUser(userId: string, roomId: string) {
this.eventRedactionQueue.add(new RedactUserInRoom(userId, roomId));
this.eventRedactionQueue.add(new RedactUserInRoom(userId, roomId, this.isAdmin));
}

/**
Expand Down
9 changes: 5 additions & 4 deletions src/commands/RedactCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ import { Permalinks } from "@vector-im/matrix-bot-sdk";
// !mjolnir redact <user ID> [room alias] [limit]
export async function execRedactCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
const userId = parts[2];
let roomAlias: string|null = null;
let targetRoom: string|null = null;
let limit = Number.parseInt(parts.length > 3 ? parts[3] : "", 10); // default to NaN for later
if (parts.length > 3 && isNaN(limit)) {
roomAlias = await mjolnir.client.resolveRoom(parts[3]);
targetRoom = await mjolnir.client.resolveRoom(parts[3]);
if (parts.length > 4) {
limit = Number.parseInt(parts[4], 10);
}
Expand All @@ -45,8 +45,9 @@ export async function execRedactCommand(roomId: string, event: any, mjolnir: Mjo
return;
}

const targetRoomIds = roomAlias ? [roomAlias] : mjolnir.protectedRoomsTracker.getProtectedRooms();
await redactUserMessagesIn(mjolnir.client, mjolnir.managementRoomOutput, userId, targetRoomIds, limit);
const targetRoomIds = targetRoom ? [targetRoom] : mjolnir.protectedRoomsTracker.getProtectedRooms();
const isAdmin = await mjolnir.isSynapseAdmin();
await redactUserMessagesIn(mjolnir.client, mjolnir.managementRoomOutput, userId, targetRoomIds, isAdmin, limit);

await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
await mjolnir.client.redactEvent(roomId, processingReactionId, 'done processing');
Expand Down
6 changes: 4 additions & 2 deletions src/queues/EventRedactionQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,17 @@ export interface QueuedRedaction {
export class RedactUserInRoom implements QueuedRedaction {
userId: string;
roomId: string;
isAdmin: boolean;

constructor(userId: string, roomId: string) {
constructor(userId: string, roomId: string, isAdmin: boolean) {
this.userId = userId;
this.roomId = roomId;
this.isAdmin = isAdmin;
}

public async redact(client: MatrixClient, managementRoom: ManagementRoomOutput) {
await managementRoom.logMessage(LogLevel.DEBUG, "Mjolnir", `Redacting events from ${this.userId} in room ${this.roomId}.`);
await redactUserMessagesIn(client, managementRoom, this.userId, [this.roomId]);
await redactUserMessagesIn(client, managementRoom, this.userId, [this.roomId], this.isAdmin);
}

public redactionEqual(redaction: QueuedRedaction): boolean {
Expand Down
62 changes: 45 additions & 17 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ import {
LogService,
MatrixGlob,
getRequestFn,
setRequestFn,
setRequestFn, extractRequestError,
} from "@vector-im/matrix-bot-sdk";
import { ClientRequest, IncomingMessage } from "http";
import { default as parseDuration } from "parse-duration";
import * as Sentry from '@sentry/node';
import * as _ from '@sentry/tracing'; // Performing the import activates tracing.
import { collectDefaultMetrics, Counter, Histogram, register } from "prom-client";

import ManagementRoomOutput from "./ManagementRoomOutput";
import { IHealthConfig } from "./config";
import { MatrixSendClient } from "./MatrixEmitter";
import ManagementRoomOutput from "./ManagementRoomOutput";

// Define a few aliases to simplify parsing durations.

Expand Down Expand Up @@ -79,27 +79,55 @@ export function isTrueJoinEvent(event: any): boolean {
* @param userIdOrGlob A mxid or a glob which is applied to the whole sender field of events in the room, which will be redacted if they match.
* See `MatrixGlob` in matrix-bot-sdk.
* @param targetRoomIds Rooms to redact the messages from.
* @param isAdmin whether the bot is server admin
* @param limit The number of messages to redact from most recent first. If the limit is reached then no further messages will be redacted.
* @param noop Whether to operate in noop mode.
*/
export async function redactUserMessagesIn(client: MatrixSendClient, managementRoom: ManagementRoomOutput, userIdOrGlob: string, targetRoomIds: string[], limit = 1000, noop = false) {
for (const targetRoomId of targetRoomIds) {
await managementRoom.logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Fetching sent messages for ${userIdOrGlob} in ${targetRoomId} to redact...`, targetRoomId);
export async function redactUserMessagesIn(client: MatrixSendClient, managementRoom: ManagementRoomOutput, userIdOrGlob: string, targetRoomIds: string[], isAdmin: boolean, limit = 1000, noop = false) {
const usingGlob = userIdOrGlob.includes("*");

async function adminRedaction(c: MatrixSendClient, mr: ManagementRoomOutput, userId: string, targetRooms: string[], lim = 1000) {
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
let redactID: string;
const body = {"limit": lim, "rooms": targetRooms};
const redactEndpoint = `/_synapse/admin/v1/user/${userId}/redact`;
const response = await c.doRequest("GET", redactEndpoint, null, body);
redactID = response["redact_id"];
await mr.logMessage(LogLevel.INFO, "utils#redactUserMessagesIn", `Successfully requested redaction, ID for task is ${redactID}`);
}

try {
await getMessagesByUserIn(client, userIdOrGlob, targetRoomId, limit, async (eventsToRedact) => {
for (const targetEvent of eventsToRedact) {
await managementRoom.logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Redacting ${targetEvent['event_id']} in ${targetRoomId}`, targetRoomId);
if (!noop) {
await client.redactEvent(targetRoomId, targetEvent['event_id']);
} else {
await managementRoom.logMessage(LogLevel.WARN, "utils#redactUserMessagesIn", `Tried to redact ${targetEvent['event_id']} in ${targetRoomId} but Mjolnir is running in no-op mode`, targetRoomId);
async function redaction(c: MatrixSendClient, mr: ManagementRoomOutput, uIdOrGlob: string, targetRooms: string [], lim = 1000) {
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
for (const targetRoomId of targetRooms) {
await mr.logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Fetching sent messages for ${uIdOrGlob} in ${targetRoomId} to redact...`, targetRoomId);

try {
await getMessagesByUserIn(c, uIdOrGlob, targetRoomId, lim, async (eventsToRedact) => {
for (const targetEvent of eventsToRedact) {
await mr.logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Redacting ${targetEvent['event_id']} in ${targetRoomId}`, targetRoomId);
if (!noop) {
await c.redactEvent(targetRoomId, targetEvent['event_id']);
} else {
await mr.logMessage(LogLevel.WARN, "utils#redactUserMessagesIn", `Tried to redact ${targetEvent['event_id']} in ${targetRoomId} but Mjolnir is running in no-op mode`, targetRoomId);
}
}
}
});
} catch (error) {
await managementRoom.logMessage(LogLevel.ERROR, "utils#redactUserMessagesIn", `Caught an error while trying to redact messages for ${userIdOrGlob} in ${targetRoomId}: ${error}`, targetRoomId);
});
} catch (error) {
await mr.logMessage(LogLevel.ERROR, "utils#redactUserMessagesIn", `Caught an error while trying to redact messages for ${userIdOrGlob} in ${targetRoomId}: ${error}`, targetRoomId);
}
}
}

// if admin use the Admin API, but admin endpoint does not support globs
if (isAdmin && !usingGlob) {
try {
await adminRedaction(client, managementRoom, userIdOrGlob, targetRoomIds, limit)
} catch (e) {
LogService.error("utils#redactUserMessagesIn", `Error using admin API to redact messages: ${extractRequestError(e)}`);
await managementRoom.logMessage(LogLevel.ERROR, "utils#redactUserMessagesIn", `Error using admin API to redact messages for user ${userIdOrGlob}, please check logs for more info - falling
back to non-admin redaction process.`);
await redaction(client, managementRoom, userIdOrGlob, targetRoomIds, limit);
}
} else {
await redaction(client, managementRoom, userIdOrGlob, targetRoomIds, limit);
}
}

Expand Down
Loading
Loading