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

Incorperate Mjolnir's new report-to-moderator features #8

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions mx-tester.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,19 @@ homeserver:
remote:
per_second: 10000
burst_count: 10000

# Creating a few users simplifies testing.
users:
- localname: admin
admin: true
rooms:
- public: true
name: "List of users"
alias: access-control-list
members:
- admin
- user_in_mjolnir_for_all
# This user can use Mjölnir-for-all
- localname: user_in_mjolnir_for_all
# This user cannot
- localname: user_regular
Comment on lines +79 to +93
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably shouldn't be part of the PR, it's about testing Mjolnir for all

13 changes: 9 additions & 4 deletions src/Mjolnir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export class Mjolnir {
if (options.autojoinOnlyIfManager) {
const managers = await client.getJoinedRoomMembers(mjolnir.managementRoomId);
if (!managers.includes(membershipEvent.sender)) return reportInvite(); // ignore invite
} else {
} else if (options.acceptInvitesFromSpace) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check why this was changed. doesn't look good

const spaceId = await client.resolveRoom(options.acceptInvitesFromSpace);
const spaceUserIds = await client.getJoinedRoomMembers(spaceId)
.catch(async e => {
Expand All @@ -154,7 +154,7 @@ export class Mjolnir {
*/
static async setupMjolnirFromConfig(client: MatrixSendClient, matrixEmitter: MatrixEmitter, config: IConfig): Promise<Mjolnir> {
if (!config.autojoinOnlyIfManager && config.acceptInvitesFromSpace === getDefaultConfig().acceptInvitesFromSpace) {
throw new TypeError("`autojoinOnlyIfManager` has been disabled, yet no space has been provided for `acceptInvitesFromSpace`.");
throw new TypeError("`autojoinOnlyIfManager` has been disabled but you have not set `acceptInvitesFromSpace`. Please make it empty to accept invites from everywhere or give it a namespace alias or room id.");
}
const policyLists: PolicyList[] = [];
const joinedRooms = await client.getJoinedRooms();
Expand Down Expand Up @@ -257,8 +257,13 @@ export class Mjolnir {
this.protectionManager = new ProtectionManager(this);

this.managementRoomOutput = new ManagementRoomOutput(managementRoomId, client, config);
const protections = new ProtectionManager(this);
this.protectedRoomsTracker = new ProtectedRoomsSet(client, clientUserId, managementRoomId, this.managementRoomOutput, protections, config);
this.protectedRoomsTracker = new ProtectedRoomsSet(
client,
clientUserId,
managementRoomId,
this.managementRoomOutput,
this.protectionManager,
config);
}

public get lists(): PolicyList[] {
Expand Down
8 changes: 2 additions & 6 deletions src/ProtectedRoomsSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,6 @@ export class ProtectedRoomsSet {
private readonly clientUserId: string,
private readonly managementRoomId: string,
private readonly managementRoomOutput: ManagementRoomOutput,
/**
* The protection manager is only used to verify the permissions
* that the protection manager requires are correct for this set of rooms.
* The protection manager is not really compatible with this abstraction yet
* because of a direct dependency on the protection manager in Mjolnir commands.
*/
Comment on lines -118 to -123
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this deleted, is the comment still true?

private readonly protectionManager: ProtectionManager,
private readonly config: IConfig,
) {
Expand Down Expand Up @@ -275,11 +269,13 @@ export class ProtectedRoomsSet {
}
this.protectedRooms.add(roomId);
this.protectedRoomActivityTracker.addProtectedRoom(roomId);
this.protectionManager.addProtectedRoom(roomId);
}

public removeProtectedRoom(roomId: string): void {
this.protectedRoomActivityTracker.removeProtectedRoom(roomId);
this.protectedRooms.delete(roomId);
this.protectionManager.removeProtectedRoom(roomId);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/commands/CommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { BaseFunction, CommandTable, defineCommandTable } from "./interface-mana
import { findMatrixInterfaceAdaptor, MatrixContext } from "./interface-manager/MatrixInterfaceAdaptor";
import { ArgumentStream } from "./interface-manager/ParamaterParsing";
import { execBanCommand, execUnbanCommand } from "./UnbanBanCommand";
import { execSetupProtectedRoom } from "./SetupDecentralizedReportingCommand";

export interface MjolnirContext extends MatrixContext {
mjolnir: Mjolnir,
Expand Down Expand Up @@ -122,6 +123,8 @@ export async function handleCommand(roomId: string, event: { content: { body: st
return await execAddProtectedRoom(roomId, event, mjolnir, parts);
} else if (parts[1] === 'rooms' && parts.length > 3 && parts[2] === 'remove') {
return await execRemoveProtectedRoom(roomId, event, mjolnir, parts);
} else if (parts[1] === 'rooms' && parts.length > 3 && parts[2] === 'setup') {
return await execSetupProtectedRoom(roomId, event, mjolnir, parts);
} else if (parts[1] === 'rooms' && parts.length === 2) {
return await execListProtectedRooms(roomId, event, mjolnir);
} else if (parts[1] === 'move' && parts.length > 3) {
Expand Down Expand Up @@ -188,6 +191,7 @@ export async function handleCommand(roomId: string, event: { content: { body: st
"!mjolnir rooms - Lists all the protected rooms\n" +
"!mjolnir rooms add <room alias/ID> - Adds a protected room (may cause high server load)\n" +
"!mjolnir rooms remove <room alias/ID> - Removes a protected room\n" +
"!mjolnir rooms setup <room alias/ID> reporting - Setup decentralized reporting in a room\n" +
"!mjolnir move <room alias> <room alias/ID> - Moves a <room alias> to a new <room ID>\n" +
"!mjolnir directory add <room alias/ID> - Publishes a room in the server's room directory\n" +
"!mjolnir directory remove <room alias/ID> - Removes a room from the server's room directory\n" +
Expand Down
61 changes: 61 additions & 0 deletions src/commands/SetupDecentralizedReportingCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Mjolnir } from "../Mjolnir";
import { LogLevel } from "matrix-bot-sdk";

const EVENT_MODERATED_BY = "org.matrix.msc3215.room.moderation.moderated_by";
const EVENT_MODERATOR_OF = "org.matrix.msc3215.room.moderation.moderator_of";

// !mjolnir rooms setup <room alias/ID> reporting
export async function execSetupProtectedRoom(commandRoomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upgrade this to a new style command

// For the moment, we only accept a subcommand `reporting`.
if (parts[4] !== 'reporting') {
await mjolnir.client.sendNotice(commandRoomId, "Invalid subcommand for `rooms setup <room alias/ID> subcommand`, expected one of \"reporting\"");
await mjolnir.client.unstableApis.addReactionToEvent(commandRoomId, event['event_id'], '❌');
return;
}
const protectedRoomId = await mjolnir.client.joinRoom(parts[3]);

try {
const userId = await mjolnir.client.getUserId();

// A backup of the previous state in case we need to rollback.
let previousState: /* previous content */ any | /* there was no previous content */ null;
try {
previousState = await mjolnir.client.getRoomStateEvent(protectedRoomId, EVENT_MODERATED_BY, EVENT_MODERATED_BY);
} catch (ex) {
previousState = null;
}

// Setup protected room -> moderation room link.
// We do this before the other one to be able to fail early if we do not have a sufficient
// powerlevel.
let eventId = await mjolnir.client.sendStateEvent(protectedRoomId, EVENT_MODERATED_BY, EVENT_MODERATED_BY, {
room_id: commandRoomId,
user_id: userId,
});

try {
// Setup moderation room -> protected room.
await mjolnir.client.sendStateEvent(commandRoomId, EVENT_MODERATOR_OF, protectedRoomId, {
user_id: userId,
});
} catch (ex) {
// If the second `sendStateEvent` fails, we could end up with a room half setup, which
// is bad. Attempt to rollback.
try {
if (previousState) {
await mjolnir.client.sendStateEvent(protectedRoomId, EVENT_MODERATED_BY, EVENT_MODERATED_BY, previousState);
} else {
await mjolnir.client.redactEvent(protectedRoomId, eventId, "Rolling back incomplete MSC3215 setup");
}
} finally {
// Ignore second exception
throw ex;
}
}

} catch (ex) {
mjolnir.managementRoomOutput.logMessage(LogLevel.ERROR, "execSetupProtectedRoom", ex.message);
await mjolnir.client.unstableApis.addReactionToEvent(commandRoomId, event['event_id'], '❌');
}
await mjolnir.client.unstableApis.addReactionToEvent(commandRoomId, event['event_id'], '✅');
}
16 changes: 16 additions & 0 deletions src/protections/IProtection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,34 @@ export abstract class Protection {
readonly requiredStatePermissions: string[] = [];
abstract settings: { [setting: string]: AbstractProtectionSetting<any, any> };

/**
* A new room has been added to the list of rooms to protect with this protection.
*/
async startProtectingRoom(mjolnir: Mjolnir, roomId: string) {
// By default, do nothing.
}

/**
* A room has been removed from the list of rooms to protect with this protection.
*/
async stopProtectingRoom(mjolnir: Mjolnir, roomId: string) {
// By default, do nothing.
}

/*
* Handle a single event from a protected room, to decide if we need to
* respond to it
*/
async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<Consequence[] | any> {
// By default, do nothing.
}

/*
* Handle a single reported event from a protecte room, to decide if we
* need to respond to it
*/
async handleReport(mjolnir: Mjolnir, roomId: string, reporterId: string, event: any, reason?: string): Promise<any> {
// By default, do nothing.
}

/**
Expand Down
98 changes: 98 additions & 0 deletions src/protections/LocalAbuseReports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
Copyright 2023 Element.

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 { LogLevel } from "matrix-bot-sdk";
import { Mjolnir } from "../Mjolnir";
import { Protection } from "./IProtection";

/*
An implementation of per decentralized abuse reports, as per
https://github.com/Yoric/matrix-doc/blob/aristotle/proposals/3215-towards-decentralized-moderation.md
*/

const EVENT_MODERATED_BY = "org.matrix.msc3215.room.moderation.moderated_by";
const EVENT_MODERATOR_OF = "org.matrix.msc3215.room.moderation.moderator_of";

/**
* Setup decentralized abuse reports in protected rooms.
*/
export class LocalAbuseReports extends Protection {
settings: { };
public readonly name = "LocalAbuseReports";
public readonly description = "Enables MSC3215-compliant web clients to send abuse reports to the moderator instead of the homeserver admin";
readonly requiredStatePermissions = [EVENT_MODERATED_BY];

/**
* A new room has been added to the list of rooms to protect with this protection.
*/
async startProtectingRoom(mjolnir: Mjolnir, protectedRoomId: string) {
try {
const userId = await mjolnir.client.getUserId();

// Fetch the previous state of the room, to avoid overwriting any existing setup.
let previousState: /* previous content */ any | /* there was no previous content */ null;
try {
previousState = await mjolnir.client.getRoomStateEvent(protectedRoomId, EVENT_MODERATED_BY, EVENT_MODERATED_BY);
} catch (ex) {
previousState = null;
}
if (previousState && previousState["room_id"] && previousState["user_id"]) {
if (previousState["room_id"] === mjolnir.managementRoomId && previousState["user_id"] === userId) {
// The room is already setup, do nothing.
return;
} else {
// There is a setup already, but it's not for us. Don't overwrite it.
let protectedRoomAliasOrId = await mjolnir.client.getPublishedAlias(protectedRoomId) || protectedRoomId;
mjolnir.managementRoomOutput.logMessage(LogLevel.INFO, "LocalAbuseReports", `Room ${protectedRoomAliasOrId} is already setup for decentralized abuse reports with bot ${previousState["user_id"]} and room ${previousState["room_id"]}, not overwriting automatically. To overwrite, use command \`!mjolnir rooms setup ${protectedRoomId} reporting\``);
return;
}
}

// Setup protected room -> moderation room link.
// We do this before the other one to be able to fail early if we do not have a sufficient
// powerlevel.
let eventId;
try {
eventId = await mjolnir.client.sendStateEvent(protectedRoomId, EVENT_MODERATED_BY, EVENT_MODERATED_BY, {
room_id: mjolnir.managementRoomId,
user_id: userId,
});
} catch (ex) {
mjolnir.managementRoomOutput.logMessage(LogLevel.ERROR, "LocalAbuseReports", `Could not autoset protected room -> moderation room link: ${ex.message}. To set it manually, use command \`!mjolnir rooms setup ${protectedRoomId} reporting\``);
return;
}

try {
// Setup moderation room -> protected room.
await mjolnir.client.sendStateEvent(mjolnir.managementRoomId, EVENT_MODERATOR_OF, protectedRoomId, {
user_id: userId,
});
} catch (ex) {
// If the second `sendStateEvent` fails, we could end up with a room half setup, which
// is bad. Attempt to rollback.
mjolnir.managementRoomOutput.logMessage(LogLevel.ERROR, "LocalAbuseReports", `Could not autoset moderation room -> protected room link: ${ex.message}. To set it manually, use command \`!mjolnir rooms setup ${protectedRoomId} reporting\``);
try {
await mjolnir.client.redactEvent(protectedRoomId, eventId, "Rolling back incomplete MSC3215 setup");
} finally {
// Ignore second exception, propagate first.
throw ex;
}
}
} catch (ex) {
mjolnir.managementRoomOutput.logMessage(LogLevel.ERROR, "LocalAbuseReports", ex.message);
}
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newline please

Loading