This commit is contained in:
David Teller 2022-01-14 17:51:08 +01:00
parent 77784a88b1
commit df63b608e7
6 changed files with 151 additions and 8 deletions

View File

@ -192,3 +192,42 @@ web:
abuseReporting:
# Whether to enable this feature.
enabled: false
# Uncomment to enable support for undoable actions.
#
# As of this writing, the only undoable action is `softredact`.
#
#trashcan:
# # The room in which we store undoable actions for review.
# # Mjölnir will not create it. This can be the same as the
# # management room but it doesn't have to.
# roomAliasOrId: "#trashcan:localhost"
#
# # Support for soft redactions, i.e. using Mjölnir to mark
# # messages as hidden pending moderation.
# #
# # As of this writing, this feature only works with develop
# # versions of Element Web, with feature
# # `feature_msc3531_hide_messages_pending_moderation`
# # enabled.
# #
# # Comment this out if you wish to disable soft redactions.
# softRedact:
# # After `goodAfterMs` milliseconds, any message hidden
# # pending moderation by Mjölnir will revert to visible
# # and vanish from the trashcan.
# # Ignored if `goodAfterMs` is >= `badAfterMs` or `retainMs`.
# goodAfterMs: .inf
#
# # After `badAfterMs` milliseconds, any message hidden
# # pending moderation by Mjölnir will be irreversibly
# # deleted from homeservers and up-to-date clients and
# # vanish from the trashcan.
# # Ignored if `badAfterMs` is > `goodAfterMs` or `retainMs`.
# badAfterMs: .inf
#
# # After `retainMs` milliseconds, any message hidden
# # pending moderation will vanish from the trashcan, without
# # further action.
# # Ignored if `retainMs` is > `goodAfterMs` or `badAfterMs`.
# retainMs: 604800000 # Approximately one week.

View File

@ -42,6 +42,7 @@ import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQu
import * as htmlEscape from "escape-html";
import { ReportManager } from "./report/ReportManager";
import { WebAPIs } from "./webapis/WebAPIs";
import { TrashManager } from "./trashcan/TrashManager";
export const STATE_NOT_STARTED = "not_started";
export const STATE_CHECKING_PERMISSIONS = "checking_permissions";
@ -73,6 +74,8 @@ export class Mjolnir {
private explicitlyProtectedRoomIds: string[] = [];
private knownUnprotectedRooms: string[] = [];
private webapis: WebAPIs;
public trashcanManager?: TrashManager;
/**
* Adds a listener to the client that will automatically accept invitations.
* @param {MatrixClient} client
@ -146,15 +149,25 @@ export class Mjolnir {
} else {
config.managementRoom = managementRoomId;
}
await logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status.");
return new Mjolnir(client, protectedRooms, banLists);
let trashcanRoomId;
if (config.trashcan) {
trashcanRoomId = await client.resolveRoom(config.trashcan.roomAliasOrId);
const joinedRooms = await client.getJoinedRooms();
if (!joinedRooms.includes(trashcanRoomId)) {
await client.joinRoom(trashcanRoomId);
}
}
await logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status.");
return new Mjolnir(client, protectedRooms, banLists, trashcanRoomId);
}
constructor(
public readonly client: MatrixClient,
public readonly protectedRooms: { [roomId: string]: string },
private banLists: BanList[],
trashcanRoomId?: string,
) {
this.explicitlyProtectedRoomIds = Object.keys(this.protectedRooms);
@ -222,6 +235,12 @@ export class Mjolnir {
// Setup Web APIs
console.log("Creating Web APIs");
this.webapis = new WebAPIs(new ReportManager(this));
// Setup trashcan.
console.log("Creating Trashcan Manager");
if (trashcanRoomId) {
this.trashcanManager = new TrashManager(this, trashcanRoomId);
}
}
public get lists(): BanList[] {

View File

@ -1,5 +1,5 @@
/*
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Copyright 2019-2022 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.
@ -24,7 +24,8 @@ import { execSyncCommand } from "./SyncCommand";
import { execPermissionCheckCommand } from "./PermissionCheckCommand";
import { execCreateListCommand } from "./CreateBanListCommand";
import { execUnwatchCommand, execWatchCommand } from "./WatchUnwatchCommand";
import { execRedactCommand } from "./RedactCommand";
import { execHardRedactCommand } from "./HardRedactCommand";
import { execSoftRedactCommand } from "./SoftRedactCommand";
import { execImportCommand } from "./ImportCommand";
import { execSetDefaultListCommand } from "./SetDefaultBanListCommand";
import { execDeactivateCommand } from "./DeactivateCommand";
@ -63,7 +64,9 @@ export async function handleCommand(roomId: string, event: any, mjolnir: Mjolnir
} else if (parts[1] === 'unwatch' && parts.length > 1) {
return await execUnwatchCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'redact' && parts.length > 1) {
return await execRedactCommand(roomId, event, mjolnir, parts);
return await execHardRedactCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'hide' && parts.length > 1) {
return await execSoftRedactCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'import' && parts.length > 2) {
return await execImportCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'default' && parts.length > 2) {

View File

@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019-2022 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.
@ -18,8 +18,16 @@ import { Mjolnir } from "../Mjolnir";
import { redactUserMessagesIn } from "../utils";
import { Permalinks } from "matrix-bot-sdk";
/*!
* "Hard redaction", cannot be undone.
*
* Instruct all participant homeservers to permanently erase the
* contents of the message from the timeline and to request the
* same from all clients.
*/
// !mjolnir redact <user ID> [room alias] [limit]
export async function execRedactCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
export async function execHardRedactCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
const userId = parts[2];
let roomAlias: string|null = null;
let limit = Number.parseInt(parts.length > 3 ? parts[3] : "", 10); // default to NaN for later

View File

@ -0,0 +1,66 @@
/*
Copyright 2022 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 { redactUserMessagesIn } from "../utils";
import { Permalinks } from "matrix-bot-sdk";
/*!
* "Soft redaction", aka "undoable redaction".
*
* 1. Instruct all clients to mark the message as hidden
* pending moderation. Only moderators and the author
* of the message may still see it.
* 2. Copy the message to the trashcan room.
* 3. In the trashcan room, moderators have the ability
* to either restore the message or trash (i.e. redact)
* it permanently.
*/
// !mjolnir hide <user ID> [room alias] [limit]
export async function execSoftRedactCommand(roomId: string, instructionEvent: any, mjolnir: Mjolnir, parts: string[]) {
// This could either be an eventId or a userId.
const targetId = parts[2];
let roomAlias: 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]);
if (parts.length > 4) {
limit = Number.parseInt(parts[4], 10);
}
}
// Make sure we always have a limit set
if (isNaN(limit)) limit = 1000;
const processingReactionId = await mjolnir.client.unstableApis.addReactionToEvent(roomId, instructionEvent['event_id'], 'In Progress');
if (userId[0] !== '@') {
// Assume it's a permalink
const parsed = Permalinks.parseUrl(parts[2]);
const targetRoomId = await mjolnir.client.resolveRoom(parsed.roomIdOrAlias);
await mjolnir.client.redactEvent(targetRoomId, parsed.eventId);
await mjolnir.client.unstableApis.addReactionToEvent(roomId, instructionEvent['event_id'], '✅');
await mjolnir.client.redactEvent(roomId, processingReactionId, 'done processing command');
return;
}
const targetRoomIds = roomAlias ? [roomAlias] : Object.keys(mjolnir.protectedRooms);
await redactUserMessagesIn(mjolnir.client, userId, targetRoomIds, limit);
await mjolnir.client.unstableApis.addReactionToEvent(roomId, instructionEvent['event_id'], '✅');
await mjolnir.client.redactEvent(roomId, processingReactionId, 'done processing');
}

View File

@ -76,7 +76,15 @@ interface IConfig {
abuseReporting: {
enabled: boolean;
}
}
};
trashcan?: {
roomAliasOrId: string;
softRedact?: {
goodAfterMs: number;
badAfterMs: number;
retainMs: number;
}
};
/**
* Config options only set at runtime. Try to avoid using the objects