mirror of
https://github.com/matrix-org/mjolnir.git
synced 2024-10-01 01:36:06 -04:00
Very much a WIP
This is a rough attempt to take the previous changeset and make it pass type-check. There are no good reasons to believe that it will work just yet.
This commit is contained in:
parent
927f2bd70f
commit
42969f602c
@ -16,8 +16,6 @@ limitations under the License.
|
||||
|
||||
import { extractRequestError, LogLevel, LogService, MatrixClient, MessageType, Permalinks, TextualMessageEventContent, UserID } from "matrix-bot-sdk";
|
||||
import { IConfig } from "./config";
|
||||
import ErrorCache from "./ErrorCache";
|
||||
import { RoomUpdateError } from "./models/RoomUpdateError";
|
||||
import { htmlEscape } from "./utils";
|
||||
|
||||
const levelToFn = {
|
||||
@ -102,7 +100,7 @@ export default class ManagementRoomOutput {
|
||||
format: "org.matrix.custom.html",
|
||||
};
|
||||
if (!isRecursive) {
|
||||
evContent = await this.replaceRoomIdsWithPills(this, clientMessage, new Set(roomIds), "m.notice");
|
||||
evContent = await this.replaceRoomIdsWithPills(clientMessage, new Set(roomIds), "m.notice");
|
||||
}
|
||||
|
||||
await client.sendMessage(this.managementRoomId, evContent);
|
||||
|
385
src/Mjolnir.ts
385
src/Mjolnir.ts
@ -20,37 +20,24 @@ import {
|
||||
LogLevel,
|
||||
LogService,
|
||||
MatrixClient,
|
||||
MatrixGlob,
|
||||
MembershipEvent,
|
||||
Permalinks,
|
||||
UserID,
|
||||
TextualMessageEventContent
|
||||
} from "matrix-bot-sdk";
|
||||
|
||||
import { ALL_RULE_TYPES as ALL_BAN_LIST_RULE_TYPES, RULE_ROOM, RULE_SERVER, RULE_USER } from "./models/ListRule";
|
||||
import { applyServerAcls } from "./actions/ApplyAcl";
|
||||
import { RoomUpdateError } from "./models/RoomUpdateError";
|
||||
import { ALL_RULE_TYPES as ALL_BAN_LIST_RULE_TYPES } from "./models/ListRule";
|
||||
import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler";
|
||||
import { applyUserBans } from "./actions/ApplyBan";
|
||||
import ErrorCache, { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "./ErrorCache";
|
||||
import { Protection } from "./protections/IProtection";
|
||||
import { PROTECTIONS } from "./protections/protections";
|
||||
import { ConsequenceType, Consequence } from "./protections/consequence";
|
||||
import { ProtectionSettingValidationError } from "./protections/ProtectionSettings";
|
||||
import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue";
|
||||
import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue";
|
||||
import { htmlEscape } from "./utils";
|
||||
import { ReportManager } from "./report/ReportManager";
|
||||
import { ReportPoller } from "./report/ReportPoller";
|
||||
import { WebAPIs } from "./webapis/WebAPIs";
|
||||
import RuleServer from "./models/RuleServer";
|
||||
import { RoomMemberManager } from "./RoomMembers";
|
||||
import { ProtectedRoomActivityTracker } from "./queues/ProtectedRoomActivityTracker";
|
||||
import { ThrottlingQueue } from "./queues/ThrottlingQueue";
|
||||
import { IConfig } from "./config";
|
||||
import PolicyList, { ListRuleChange } from "./models/PolicyList";
|
||||
import PolicyList from "./models/PolicyList";
|
||||
import { ProtectedRooms } from "./ProtectedRooms";
|
||||
import ManagementRoomOutput from "./ManagementRoom";
|
||||
import { ProtectionManager } from "./protections/protections";
|
||||
|
||||
export const STATE_NOT_STARTED = "not_started";
|
||||
export const STATE_CHECKING_PERMISSIONS = "checking_permissions";
|
||||
@ -58,10 +45,8 @@ export const STATE_SYNCING = "syncing";
|
||||
export const STATE_RUNNING = "running";
|
||||
|
||||
const WATCHED_LISTS_EVENT_TYPE = "org.matrix.mjolnir.watched_lists";
|
||||
const ENABLED_PROTECTIONS_EVENT_TYPE = "org.matrix.mjolnir.enabled_protections";
|
||||
const PROTECTED_ROOMS_EVENT_TYPE = "org.matrix.mjolnir.protected_rooms";
|
||||
const WARN_UNPROTECTED_ROOM_EVENT_PREFIX = "org.matrix.mjolnir.unprotected_room_warning.for.";
|
||||
const CONSEQUENCE_EVENT_DATA = "org.matrix.mjolnir.consequence";
|
||||
|
||||
/**
|
||||
* Synapse will tell us where we last got to on polling reports, so we need
|
||||
* to store that for pagination on further polls
|
||||
@ -72,8 +57,6 @@ export class Mjolnir {
|
||||
private displayName: string;
|
||||
private localpart: string;
|
||||
private currentState: string = STATE_NOT_STARTED;
|
||||
public readonly roomJoins: RoomMemberManager;
|
||||
public protections = new Map<string /* protection name */, Protection>();
|
||||
/**
|
||||
* This is for users who are not listed on a watchlist,
|
||||
* but have been flagged by the automatic spam detection as suispicous
|
||||
@ -83,19 +66,18 @@ export class Mjolnir {
|
||||
* Every room that we are joined to except the management room. Used to implement `config.protectAllJoinedRooms`.
|
||||
*/
|
||||
private protectedJoinedRoomIds: string[] = [];
|
||||
private protectedRoomsTracker: ProtectedRooms;
|
||||
/**
|
||||
* These are rooms that were explicitly said to be protected either in the config, or by what is present in the account data for `org.matrix.mjolnir.protected_rooms`.
|
||||
*/
|
||||
private explicitlyProtectedRoomIds: string[] = [];
|
||||
private unprotectedWatchedListRooms: string[] = [];
|
||||
public readonly protectedRoomsTracker: ProtectedRooms; // FIXME: Construct
|
||||
private webapis: WebAPIs;
|
||||
public taskQueue: ThrottlingQueue;
|
||||
private managementRoom: ManagementRoomOutput;
|
||||
public readonly managementRoom: ManagementRoomOutput;
|
||||
/*
|
||||
* Config-enabled polling of reports in Synapse, so Mjolnir can react to reports
|
||||
*/
|
||||
private reportPoller?: ReportPoller;
|
||||
|
||||
public readonly protectionManager: ProtectionManager;
|
||||
public readonly reportManager: ReportManager;
|
||||
|
||||
/**
|
||||
* Adds a listener to the client that will automatically accept invitations.
|
||||
* @param {MatrixClient} client
|
||||
@ -131,7 +113,7 @@ export class Mjolnir {
|
||||
const spaceUserIds = await client.getJoinedRoomMembers(spaceId)
|
||||
.catch(async e => {
|
||||
if (e.body?.errcode === "M_FORBIDDEN") {
|
||||
await mjolnir.logMessage(LogLevel.ERROR, 'Mjolnir', `Mjolnir is not in the space configured for acceptInvitesFromSpace, did you invite it?`);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.ERROR, 'Mjolnir', `Mjolnir is not in the space configured for acceptInvitesFromSpace, did you invite it?`);
|
||||
await client.joinRoom(spaceId);
|
||||
return await client.getJoinedRoomMembers(spaceId);
|
||||
} else {
|
||||
@ -177,7 +159,7 @@ export class Mjolnir {
|
||||
|
||||
const ruleServer = config.web.ruleServer ? new RuleServer() : null;
|
||||
const mjolnir = new Mjolnir(client, managementRoomId, config, protectedRooms, policyLists, ruleServer);
|
||||
await mjolnir.logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status.");
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status.");
|
||||
Mjolnir.addJoinOnInviteListener(mjolnir, client, config);
|
||||
return mjolnir;
|
||||
}
|
||||
@ -190,17 +172,11 @@ export class Mjolnir {
|
||||
* All the rooms that Mjolnir is protecting and their permalinks.
|
||||
* If `config.protectAllJoinedRooms` is specified, then `protectedRooms` will be all joined rooms except watched banlists that we can't protect (because they aren't curated by us).
|
||||
*/
|
||||
public readonly protectedRooms: { [roomId: string]: string },
|
||||
protectedRooms: { [roomId: string]: string },
|
||||
private policyLists: PolicyList[],
|
||||
// Combines the rules from ban lists so they can be served to a homeserver module or another consumer.
|
||||
public readonly ruleServer: RuleServer | null,
|
||||
) {
|
||||
this.explicitlyProtectedRoomIds = Object.keys(this.protectedRooms);
|
||||
|
||||
for (const reason of this.config.automaticallyRedactForReasons) {
|
||||
this.automaticRedactionReasons.push(new MatrixGlob(reason.toLowerCase()));
|
||||
}
|
||||
|
||||
// Setup bot.
|
||||
|
||||
client.on("room.event", this.handleEvent.bind(this));
|
||||
@ -260,15 +236,14 @@ export class Mjolnir {
|
||||
|
||||
// Setup Web APIs
|
||||
console.log("Creating Web APIs");
|
||||
const reportManager = new ReportManager(this);
|
||||
reportManager.on("report.new", this.handleReport.bind(this));
|
||||
this.webapis = new WebAPIs(reportManager, this.config, this.ruleServer);
|
||||
this.reportManager = new ReportManager(this);
|
||||
this.webapis = new WebAPIs(this.reportManager, this.config, this.ruleServer);
|
||||
if (config.pollReports) {
|
||||
this.reportPoller = new ReportPoller(this, reportManager);
|
||||
this.reportPoller = new ReportPoller(this, this.reportManager);
|
||||
}
|
||||
// Setup join/leave listener
|
||||
this.roomJoins = new RoomMemberManager(this.client);
|
||||
this.taskQueue = new ThrottlingQueue(this, config.backgroundDelayMS);
|
||||
|
||||
this.protectionManager = new ProtectionManager(this);
|
||||
}
|
||||
|
||||
public get lists(): PolicyList[] {
|
||||
@ -279,10 +254,6 @@ export class Mjolnir {
|
||||
return this.currentState;
|
||||
}
|
||||
|
||||
public get enabledProtections(): Protection[] {
|
||||
return [...this.protections.values()].filter(p => p.enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the handler to flag a user for redaction, removing any future messages that they send.
|
||||
* Typically this is used by the flooding or image protection on users that have not been banned from a list yet.
|
||||
@ -323,31 +294,19 @@ export class Mjolnir {
|
||||
|
||||
await this.managementRoom.logMessage(LogLevel.DEBUG, "Mjolnir@startup", "Loading protected rooms...");
|
||||
await this.resyncJoinedRooms(false);
|
||||
try {
|
||||
const data: { rooms?: string[] } | null = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE);
|
||||
if (data && data['rooms']) {
|
||||
for (const roomId of data['rooms']) {
|
||||
this.protectedRooms[roomId] = Permalinks.forRoom(roomId);
|
||||
this.explicitlyProtectedRoomIds.push(roomId);
|
||||
this.protectedRoomActivityTracker.addProtectedRoom(roomId);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
LogService.warn("Mjolnir", extractRequestError(e));
|
||||
}
|
||||
await this.protectedRoomsTracker.start();
|
||||
await this.buildWatchedPolicyLists();
|
||||
this.applyUnprotectedRooms();
|
||||
|
||||
if (this.config.verifyPermissionsOnStartup) {
|
||||
await this.managementRoom.logMessage(LogLevel.INFO, "Mjolnir@startup", "Checking permissions...");
|
||||
await this.verifyPermissions(this.config.verboseLogging);
|
||||
await this.protectedRoomsTracker.verifyPermissions(this.config.verboseLogging);
|
||||
}
|
||||
|
||||
this.currentState = STATE_SYNCING;
|
||||
if (this.config.syncOnStartup) {
|
||||
await this.managementRoom.logMessage(LogLevel.INFO, "Mjolnir@startup", "Syncing lists...");
|
||||
await this.syncLists(this.config.verboseLogging);
|
||||
await this.registerProtections();
|
||||
await this.protectedRoomsTracker.syncLists(this.config.verboseLogging);
|
||||
await this.protectionManager.start();
|
||||
}
|
||||
|
||||
this.currentState = STATE_RUNNING;
|
||||
@ -376,42 +335,6 @@ export class Mjolnir {
|
||||
this.reportPoller?.stop();
|
||||
}
|
||||
|
||||
public async addProtectedRoom(roomId: string) {
|
||||
this.protectedRooms[roomId] = Permalinks.forRoom(roomId);
|
||||
this.roomJoins.addRoom(roomId);
|
||||
|
||||
const unprotectedIdx = this.unprotectedWatchedListRooms.indexOf(roomId);
|
||||
if (unprotectedIdx >= 0) this.unprotectedWatchedListRooms.splice(unprotectedIdx, 1);
|
||||
this.explicitlyProtectedRoomIds.push(roomId);
|
||||
|
||||
let additionalProtectedRooms: { rooms?: string[] } | null = null;
|
||||
try {
|
||||
additionalProtectedRooms = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE);
|
||||
} catch (e) {
|
||||
LogService.warn("Mjolnir", extractRequestError(e));
|
||||
}
|
||||
const rooms = (additionalProtectedRooms?.rooms ?? []);
|
||||
rooms.push(roomId);
|
||||
await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, { rooms: rooms });
|
||||
}
|
||||
|
||||
public async removeProtectedRoom(roomId: string) {
|
||||
delete this.protectedRooms[roomId];
|
||||
this.roomJoins.removeRoom(roomId);
|
||||
|
||||
const idx = this.explicitlyProtectedRoomIds.indexOf(roomId);
|
||||
if (idx >= 0) this.explicitlyProtectedRoomIds.splice(idx, 1);
|
||||
|
||||
let additionalProtectedRooms: { rooms?: string[] } | null = null;
|
||||
try {
|
||||
additionalProtectedRooms = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE);
|
||||
} catch (e) {
|
||||
LogService.warn("Mjolnir", extractRequestError(e));
|
||||
}
|
||||
additionalProtectedRooms = { rooms: additionalProtectedRooms?.rooms?.filter(r => r !== roomId) ?? [] };
|
||||
await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, additionalProtectedRooms);
|
||||
}
|
||||
|
||||
// need to brewritten to add/remove from a ProtectedRooms instance.
|
||||
private async resyncJoinedRooms(withSync = true) {
|
||||
// this is really terrible!
|
||||
@ -420,192 +343,29 @@ export class Mjolnir {
|
||||
if (!this.config.protectAllJoinedRooms) return;
|
||||
|
||||
const joinedRoomIds = (await this.client.getJoinedRooms())
|
||||
.filter(r => r !== this.managementRoomId && !this.unprotectedWatchedListRooms.includes(r));
|
||||
.filter(r => r !== this.managementRoomId && !this.protectedRoomsTracker.unprotectedWatchedListRooms.includes(r));
|
||||
const oldRoomIdsSet = new Set(this.protectedJoinedRoomIds);
|
||||
const joinedRoomIdsSet = new Set(joinedRoomIds);
|
||||
// find every room that we have left (since last time)
|
||||
for (const roomId of oldRoomIdsSet.keys()) {
|
||||
if (!joinedRoomIdsSet.has(roomId)) {
|
||||
// Then we have left this room.
|
||||
delete this.protectedRooms[roomId];
|
||||
this.protectedRoomsTracker.removeProtectedRoom(roomId);
|
||||
this.roomJoins.removeRoom(roomId);
|
||||
}
|
||||
}
|
||||
// find every room that we have joined (since last time).
|
||||
for (const roomId of joinedRoomIdsSet.keys()) {
|
||||
if (!oldRoomIdsSet.has(roomId)) {
|
||||
// Then we have joined this room
|
||||
this.roomJoins.addRoom(roomId);
|
||||
this.protectedRooms[roomId] = Permalinks.forRoom(roomId);
|
||||
await this.protectedRoomsTracker.addProtectedRoom(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
this.applyUnprotectedRooms();
|
||||
|
||||
if (withSync) {
|
||||
await this.syncLists(this.config.verboseLogging);
|
||||
await this.protectedRoomsTracker.syncLists(this.config.verboseLogging);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Take all the builtin protections, register them to set their enabled (or not) state and
|
||||
* update their settings with any saved non-default values
|
||||
*/
|
||||
private async registerProtections() {
|
||||
for (const protection of PROTECTIONS) {
|
||||
try {
|
||||
await this.registerProtection(protection);
|
||||
} catch (e) {
|
||||
LogService.warn("Mjolnir", extractRequestError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Make a list of the names of enabled protections and save them in a state event
|
||||
*/
|
||||
private async saveEnabledProtections() {
|
||||
const protections = this.enabledProtections.map(p => p.name);
|
||||
await this.client.setAccountData(ENABLED_PROTECTIONS_EVENT_TYPE, { enabled: protections });
|
||||
}
|
||||
/*
|
||||
* Enable a protection by name and persist its enable state in to a state event
|
||||
*
|
||||
* @param name The name of the protection whose settings we're enabling
|
||||
*/
|
||||
public async enableProtection(name: string) {
|
||||
const protection = this.protections.get(name);
|
||||
if (protection !== undefined) {
|
||||
protection.enabled = true;
|
||||
await this.saveEnabledProtections();
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Disable a protection by name and remove it from the persistent list of enabled protections
|
||||
*
|
||||
* @param name The name of the protection whose settings we're disabling
|
||||
*/
|
||||
public async disableProtection(name: string) {
|
||||
const protection = this.protections.get(name);
|
||||
if (protection !== undefined) {
|
||||
protection.enabled = false;
|
||||
await this.saveEnabledProtections();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Read org.matrix.mjolnir.setting state event, find any saved settings for
|
||||
* the requested protectionName, then iterate and validate against their parser
|
||||
* counterparts in Protection.settings and return those which validate
|
||||
*
|
||||
* @param protectionName The name of the protection whose settings we're reading
|
||||
* @returns Every saved setting for this protectionName that has a valid value
|
||||
*/
|
||||
public async getProtectionSettings(protectionName: string): Promise<{ [setting: string]: any }> {
|
||||
let savedSettings: { [setting: string]: any } = {}
|
||||
try {
|
||||
savedSettings = await this.client.getRoomStateEvent(
|
||||
this.managementRoomId, 'org.matrix.mjolnir.setting', protectionName
|
||||
);
|
||||
} catch {
|
||||
// setting does not exist, return empty object
|
||||
return {};
|
||||
}
|
||||
|
||||
const settingDefinitions = this.protections.get(protectionName)?.settings ?? {};
|
||||
const validatedSettings: { [setting: string]: any } = {}
|
||||
for (let [key, value] of Object.entries(savedSettings)) {
|
||||
if (
|
||||
// is this a setting name with a known parser?
|
||||
key in settingDefinitions
|
||||
// is the datatype of this setting's value what we expect?
|
||||
&& typeof (settingDefinitions[key].value) === typeof (value)
|
||||
// is this setting's value valid for the setting?
|
||||
&& settingDefinitions[key].validate(value)
|
||||
) {
|
||||
validatedSettings[key] = value;
|
||||
} else {
|
||||
await this.managementRoom.logMessage(
|
||||
LogLevel.WARN,
|
||||
"getProtectionSetting",
|
||||
`Tried to read ${protectionName}.${key} and got invalid value ${value}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return validatedSettings;
|
||||
}
|
||||
|
||||
/*
|
||||
* Takes an object of settings we want to change and what their values should be,
|
||||
* check that their values are valid, combine them with current saved settings,
|
||||
* then save the amalgamation to a state event
|
||||
*
|
||||
* @param protectionName Which protection these settings belong to
|
||||
* @param changedSettings The settings to change and their values
|
||||
*/
|
||||
public async setProtectionSettings(protectionName: string, changedSettings: { [setting: string]: any }): Promise<any> {
|
||||
const protection = this.protections.get(protectionName);
|
||||
if (protection === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedSettings: { [setting: string]: any } = await this.getProtectionSettings(protectionName);
|
||||
|
||||
for (let [key, value] of Object.entries(changedSettings)) {
|
||||
if (!(key in protection.settings)) {
|
||||
throw new ProtectionSettingValidationError(`Failed to find protection setting by name: ${key}`);
|
||||
}
|
||||
if (typeof (protection.settings[key].value) !== typeof (value)) {
|
||||
throw new ProtectionSettingValidationError(`Invalid type for protection setting: ${key} (${typeof (value)})`);
|
||||
}
|
||||
if (!protection.settings[key].validate(value)) {
|
||||
throw new ProtectionSettingValidationError(`Invalid value for protection setting: ${key} (${value})`);
|
||||
}
|
||||
validatedSettings[key] = value;
|
||||
}
|
||||
|
||||
await this.client.sendStateEvent(
|
||||
this.managementRoomId, 'org.matrix.mjolnir.setting', protectionName, validatedSettings
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Given a protection object; add it to our list of protections, set whether it is enabled
|
||||
* and update its settings with any saved non-default values.
|
||||
*
|
||||
* @param protection The protection object we want to register
|
||||
*/
|
||||
public async registerProtection(protection: Protection) {
|
||||
this.protections.set(protection.name, protection)
|
||||
|
||||
let enabledProtections: { enabled: string[] } | null = null;
|
||||
try {
|
||||
enabledProtections = await this.client.getAccountData(ENABLED_PROTECTIONS_EVENT_TYPE);
|
||||
} catch {
|
||||
// this setting either doesn't exist, or we failed to read it (bad network?)
|
||||
// TODO: retry on certain failures?
|
||||
}
|
||||
protection.enabled = enabledProtections?.enabled.includes(protection.name) ?? false;
|
||||
|
||||
const savedSettings = await this.getProtectionSettings(protection.name);
|
||||
for (let [key, value] of Object.entries(savedSettings)) {
|
||||
// this.getProtectionSettings() validates this data for us, so we don't need to
|
||||
protection.settings[key].setValue(value);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Given a protection object; remove it from our list of protections.
|
||||
*
|
||||
* @param protection The protection object we want to unregister
|
||||
*/
|
||||
public unregisterProtection(protectionName: string) {
|
||||
if (!(protectionName in this.protections)) {
|
||||
throw new Error("Failed to find protection by name: " + protectionName);
|
||||
}
|
||||
this.protections.delete(protectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for constructing `PolicyList`s and making sure they have the right listeners set up.
|
||||
@ -615,22 +375,12 @@ export class Mjolnir {
|
||||
private async addPolicyList(roomId: string, roomRef: string): Promise<PolicyList> {
|
||||
const list = new PolicyList(roomId, roomRef, this.client);
|
||||
this.ruleServer?.watch(list);
|
||||
list.on('PolicyList.batch', this.syncWithPolicyList.bind(this));
|
||||
list.on('PolicyList.batch', (...args) => this.protectedRoomsTracker.syncWithPolicyList(...args));
|
||||
await list.updateList();
|
||||
this.policyLists.push(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a protection by name.
|
||||
*
|
||||
* @return If there is a protection with this name *and* it is enabled,
|
||||
* return the protection.
|
||||
*/
|
||||
public getProtection(protectionName: string): Protection | null {
|
||||
return this.protections.get(protectionName) ?? null;
|
||||
}
|
||||
|
||||
public async watchList(roomRef: string): Promise<PolicyList | null> {
|
||||
const joinedRooms = await this.client.getJoinedRooms();
|
||||
const permalink = Permalinks.parseUrl(roomRef);
|
||||
@ -673,13 +423,12 @@ export class Mjolnir {
|
||||
|
||||
public async warnAboutUnprotectedPolicyListRoom(roomId: string) {
|
||||
if (!this.config.protectAllJoinedRooms) return; // doesn't matter
|
||||
if (this.explicitlyProtectedRoomIds.includes(roomId)) return; // explicitly protected
|
||||
if (this.protectedRoomsTracker.explicitlyProtectedRoomIds.includes(roomId)) return; // explicitly protected
|
||||
|
||||
const createEvent = new CreateEvent(await this.client.getRoomStateEvent(roomId, "m.room.create", ""));
|
||||
if (createEvent.creator === await this.client.getUserId()) return; // we created it
|
||||
|
||||
if (!this.unprotectedWatchedListRooms.includes(roomId)) this.unprotectedWatchedListRooms.push(roomId);
|
||||
this.applyUnprotectedRooms();
|
||||
this.protectedRoomsTracker.addUnprotectedWatchedListRoom(roomId);
|
||||
|
||||
try {
|
||||
const accountData: { warned: boolean } | null = await this.client.getAccountData(WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId);
|
||||
@ -692,13 +441,6 @@ export class Mjolnir {
|
||||
await this.client.setAccountData(WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId, { warned: true });
|
||||
}
|
||||
|
||||
private applyUnprotectedRooms() {
|
||||
for (const roomId of this.unprotectedWatchedListRooms) {
|
||||
delete this.protectedRooms[roomId];
|
||||
this.protectedRoomActivityTracker.removeProtectedRoom(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
private async buildWatchedPolicyLists() {
|
||||
this.policyLists = [];
|
||||
const joinedRooms = await this.client.getJoinedRooms();
|
||||
@ -724,44 +466,6 @@ export class Mjolnir {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private requiredProtectionPermissions(): Set<string> {
|
||||
return new Set(this.enabledProtections.map((p) => p.requiredStatePermissions).flat())
|
||||
}
|
||||
|
||||
private async handleConsequence(protection: Protection, roomId: string, eventId: string, sender: string, consequence: Consequence) {
|
||||
switch (consequence.type) {
|
||||
case ConsequenceType.alert:
|
||||
break;
|
||||
case ConsequenceType.redact:
|
||||
await this.client.redactEvent(roomId, eventId, "abuse detected");
|
||||
break;
|
||||
case ConsequenceType.ban:
|
||||
await this.client.banUser(sender, roomId, "abuse detected");
|
||||
break;
|
||||
}
|
||||
|
||||
let message = `protection ${protection.name} enacting ${ConsequenceType[consequence.type]}`
|
||||
+ ` against ${htmlEscape(sender)}`
|
||||
+ ` in ${htmlEscape(roomId)}`;
|
||||
if (consequence.reason !== undefined) {
|
||||
// even though internally-sourced, there's no promise that `consequence.reason`
|
||||
// will never have user-supplied information, so escape it
|
||||
message += ` (reason: ${htmlEscape(consequence.reason)})`;
|
||||
}
|
||||
|
||||
await this.client.sendMessage(this.managementRoomId, {
|
||||
msgtype: "m.notice",
|
||||
body: message,
|
||||
[CONSEQUENCE_EVENT_DATA]: {
|
||||
who: sender,
|
||||
room: roomId,
|
||||
type: ConsequenceType[consequence.type]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleEvent(roomId: string, event: any) {
|
||||
// Check for UISI errors
|
||||
if (roomId === this.managementRoomId) {
|
||||
@ -783,35 +487,6 @@ export class Mjolnir {
|
||||
policyList.updateForEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
if (roomId in this.protectedRooms) {
|
||||
if (event['sender'] === await this.client.getUserId()) return; // Ignore ourselves
|
||||
|
||||
// Iterate all the enabled protections
|
||||
for (const protection of this.enabledProtections) {
|
||||
let consequence: Consequence | undefined = undefined;
|
||||
try {
|
||||
consequence = await protection.handleEvent(this, roomId, event);
|
||||
} catch (e) {
|
||||
const eventPermalink = Permalinks.forEvent(roomId, event['event_id']);
|
||||
LogService.error("Mjolnir", "Error handling protection: " + protection.name);
|
||||
LogService.error("Mjolnir", "Failed event: " + eventPermalink);
|
||||
LogService.error("Mjolnir", extractRequestError(e));
|
||||
await this.client.sendNotice(this.managementRoomId, "There was an error processing an event through a protection - see log for details. Event: " + eventPermalink);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (consequence !== undefined) {
|
||||
await this.handleConsequence(protection, roomId, event["event_id"], event["sender"], consequence);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the event handlers - we always run this after protections so that the protections
|
||||
// can flag the event for redaction.
|
||||
await this.unlistedUserRedactionHandler.handleEvent(roomId, event, this);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public async isSynapseAdmin(): Promise<boolean> {
|
||||
@ -856,10 +531,4 @@ export class Mjolnir {
|
||||
return extractRequestError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleReport({ roomId, reporterId, event, reason }: { roomId: string, reporterId: string, event: any, reason?: string }) {
|
||||
for (const protection of this.enabledProtections) {
|
||||
await protection.handleReport(this, roomId, reporterId, event, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,10 +22,14 @@ import { RULE_ROOM, RULE_SERVER, RULE_USER } from "./models/ListRule";
|
||||
import PolicyList, { ListRuleChange } from "./models/PolicyList";
|
||||
import { RoomUpdateError } from "./models/RoomUpdateError";
|
||||
import { ServerAcl } from "./models/ServerAcl";
|
||||
import { ProtectionManager } from "./protections/protections";
|
||||
import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue";
|
||||
import { ProtectedRoomActivityTracker } from "./queues/ProtectedRoomActivityTracker";
|
||||
import { RoomMemberManager } from "./RoomMembers";
|
||||
import { htmlEscape } from "./utils";
|
||||
|
||||
const PROTECTED_ROOMS_EVENT_TYPE = "org.matrix.mjolnir.protected_rooms";
|
||||
|
||||
/**
|
||||
* When you consider spaces https://github.com/matrix-org/mjolnir/issues/283
|
||||
* rather than indexing rooms via some collection, you instead have rooms
|
||||
@ -37,9 +41,6 @@ import { htmlEscape } from "./utils";
|
||||
* as in future we might want to borrow this class to represent a space.
|
||||
*/
|
||||
export class ProtectedRooms {
|
||||
|
||||
private protectedRooms = new Set</* room id */string>();
|
||||
|
||||
private policyLists: PolicyList[];
|
||||
|
||||
private protectedRoomActivityTracker: ProtectedRoomActivityTracker;
|
||||
@ -54,6 +55,31 @@ export class ProtectedRooms {
|
||||
|
||||
private automaticRedactionReasons: MatrixGlob[] = [];
|
||||
|
||||
/**
|
||||
* A list of rooms that we watch and protect.
|
||||
*/
|
||||
private readonly _protectedRooms = new Map</* room id */string, /* perma url */ string>();
|
||||
public get protectedRooms(): Iterable<string> {
|
||||
return this._protectedRooms.keys();
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of rooms that we watch but do not protect.
|
||||
*/
|
||||
private _unprotectedWatchedListRooms: string[] = [];
|
||||
public get unprotectedWatchedListRooms(): ReadonlyArray<string> {
|
||||
return this._unprotectedWatchedListRooms;
|
||||
}
|
||||
|
||||
/**
|
||||
* These are rooms that were explicitly said to be protected either in the config, or by what is present in the account data for `org.matrix.mjolnir.protected_rooms`.
|
||||
*/
|
||||
private _explicitlyProtectedRoomIds: string[] = [];
|
||||
public get explicitlyProtectedRoomIds(): ReadonlyArray<string> {
|
||||
return this._explicitlyProtectedRoomIds;
|
||||
}
|
||||
public readonly roomJoins: RoomMemberManager;
|
||||
|
||||
/**
|
||||
* Used to provide mutual exclusion when synchronizing rooms with the state of a policy list.
|
||||
* This is because requests operating with rules from an older version of the list that are slow
|
||||
@ -69,10 +95,37 @@ export class ProtectedRooms {
|
||||
private readonly clientUserId: string,
|
||||
private readonly managementRoomId: string,
|
||||
private readonly managementRoom: ManagementRoomOutput,
|
||||
private readonly protections: ProtectionManager,
|
||||
private readonly config: IConfig,
|
||||
protectedRooms: string[],
|
||||
) {
|
||||
this._explicitlyProtectedRoomIds = protectedRooms;
|
||||
|
||||
for (const reason of this.config.automaticallyRedactForReasons) {
|
||||
this.automaticRedactionReasons.push(new MatrixGlob(reason.toLowerCase()));
|
||||
}
|
||||
|
||||
// Setup room activity watcher
|
||||
this.protectedRoomActivityTracker = new ProtectedRoomActivityTracker(client);
|
||||
|
||||
// Setup join/leave listener
|
||||
this.roomJoins = new RoomMemberManager(this.client);
|
||||
}
|
||||
|
||||
public async start() {
|
||||
try {
|
||||
const data: { rooms?: string[] } | null = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE);
|
||||
if (data && data['rooms']) {
|
||||
for (const roomId of data['rooms']) {
|
||||
this.addProtectedRoom(roomId);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
LogService.warn("ProtectedRooms", extractRequestError(e));
|
||||
}
|
||||
for (const roomId of this._unprotectedWatchedListRooms) {
|
||||
this.removeProtectedRoom(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
public queueRedactUserMessagesIn(userId: string, roomId: string) {
|
||||
@ -110,7 +163,7 @@ export class ProtectedRooms {
|
||||
// power levels were updated - recheck permissions
|
||||
this.errorCache.resetError(roomId, ERROR_KIND_PERMISSION);
|
||||
await this.managementRoom.logMessage(LogLevel.DEBUG, "Mjolnir", `Power levels changed in ${roomId} - checking permissions...`, roomId);
|
||||
const errors = await this.verifyPermissionsIn(roomId);
|
||||
const errors = await this.protections.verifyPermissionsIn(roomId);
|
||||
const hadErrors = await this.printActionResult(errors);
|
||||
if (!hadErrors) {
|
||||
await this.managementRoom.logMessage(LogLevel.DEBUG, "Mjolnir", `All permissions look OK.`);
|
||||
@ -161,19 +214,53 @@ export class ProtectedRooms {
|
||||
}
|
||||
|
||||
public async addProtectedRoom(roomId: string): Promise<void> {
|
||||
if (this.protectedRooms.has(roomId)) {
|
||||
if (this._protectedRooms.has(roomId)) {
|
||||
// we need to protect ourselves form syncing all the lists unnecessarily
|
||||
// as Mjolnir does call this method repeatedly.
|
||||
return;
|
||||
}
|
||||
this.protectedRooms.add(roomId);
|
||||
this._protectedRooms.set(roomId, Permalinks.forRoom(roomId));
|
||||
this._explicitlyProtectedRoomIds.push(roomId);
|
||||
this.protectedRoomActivityTracker.addProtectedRoom(roomId);
|
||||
this.roomJoins.addRoom(roomId);
|
||||
|
||||
const unprotectedIdx = this._unprotectedWatchedListRooms.indexOf(roomId);
|
||||
if (unprotectedIdx >= 0) this._unprotectedWatchedListRooms.splice(unprotectedIdx, 1);
|
||||
this._explicitlyProtectedRoomIds.push(roomId);
|
||||
|
||||
let additionalProtectedRooms: { rooms?: string[] } | null = null;
|
||||
try {
|
||||
additionalProtectedRooms = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE);
|
||||
} catch (e) {
|
||||
LogService.warn("Mjolnir", extractRequestError(e));
|
||||
}
|
||||
const rooms = (additionalProtectedRooms?.rooms ?? []);
|
||||
rooms.push(roomId);
|
||||
await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, { rooms: rooms });
|
||||
|
||||
await this.syncLists(this.config.verboseLogging);
|
||||
}
|
||||
|
||||
public removeProtectedRoom(roomId: string): void {
|
||||
public async removeProtectedRoom(roomId: string) {
|
||||
this.protectedRoomActivityTracker.removeProtectedRoom(roomId);
|
||||
this.protectedRooms.delete(roomId);
|
||||
this._protectedRooms.delete(roomId);
|
||||
this.roomJoins.removeRoom(roomId);
|
||||
|
||||
const idx = this._explicitlyProtectedRoomIds.indexOf(roomId);
|
||||
if (idx >= 0) this._explicitlyProtectedRoomIds.splice(idx, 1);
|
||||
|
||||
let additionalProtectedRooms: { rooms?: string[] } | null = null;
|
||||
try {
|
||||
additionalProtectedRooms = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE);
|
||||
} catch (e) {
|
||||
LogService.warn("Mjolnir", extractRequestError(e));
|
||||
}
|
||||
additionalProtectedRooms = { rooms: additionalProtectedRooms?.rooms?.filter(r => r !== roomId) ?? [] };
|
||||
await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, additionalProtectedRooms);
|
||||
}
|
||||
|
||||
public isProtectedRoom(roomId: string): boolean {
|
||||
return this._protectedRooms.has(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -182,7 +269,7 @@ export class ProtectedRooms {
|
||||
* @param policyList The `PolicyList` which we will check for changes and apply them to all protected rooms.
|
||||
* @returns When all of the protected rooms have been updated.
|
||||
*/
|
||||
private async syncWithPolicyList(policyList: PolicyList): Promise<void> {
|
||||
public async syncWithPolicyList(policyList: PolicyList): Promise<void> {
|
||||
// this bit can move away into a listener.
|
||||
const changes = await policyList.updateList();
|
||||
|
||||
@ -445,8 +532,8 @@ export class ProtectedRooms {
|
||||
|
||||
public async verifyPermissions(verbose = true, printRegardless = false) {
|
||||
const errors: RoomUpdateError[] = [];
|
||||
for (const roomId of Object.keys(this.protectedRooms)) {
|
||||
errors.push(...(await this.verifyPermissionsIn(roomId)));
|
||||
for (const roomId of this._protectedRooms.keys()) {
|
||||
errors.push(...(await this.protections.verifyPermissionsIn(roomId)));
|
||||
}
|
||||
|
||||
const hadErrors = await this.printActionResult(errors, "Permission errors in protected rooms:", printRegardless);
|
||||
@ -462,90 +549,11 @@ export class ProtectedRooms {
|
||||
}
|
||||
}
|
||||
|
||||
private async verifyPermissionsIn(roomId: string): Promise<RoomUpdateError[]> {
|
||||
const errors: RoomUpdateError[] = [];
|
||||
const additionalPermissions = this.requiredProtectionPermissions();
|
||||
|
||||
try {
|
||||
const ownUserId = await this.client.getUserId();
|
||||
|
||||
const powerLevels = await this.client.getRoomStateEvent(roomId, "m.room.power_levels", "");
|
||||
if (!powerLevels) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Missing power levels state event");
|
||||
}
|
||||
|
||||
function plDefault(val: number | undefined | null, def: number): number {
|
||||
if (!val && val !== 0) return def;
|
||||
return val;
|
||||
}
|
||||
|
||||
const users = powerLevels['users'] || {};
|
||||
const events = powerLevels['events'] || {};
|
||||
const usersDefault = plDefault(powerLevels['users_default'], 0);
|
||||
const stateDefault = plDefault(powerLevels['state_default'], 50);
|
||||
const ban = plDefault(powerLevels['ban'], 50);
|
||||
const kick = plDefault(powerLevels['kick'], 50);
|
||||
const redact = plDefault(powerLevels['redact'], 50);
|
||||
|
||||
const userLevel = plDefault(users[ownUserId], usersDefault);
|
||||
const aclLevel = plDefault(events["m.room.server_acl"], stateDefault);
|
||||
|
||||
// Wants: ban, kick, redact, m.room.server_acl
|
||||
|
||||
if (userLevel < ban) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for bans: ${userLevel} < ${ban}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
}
|
||||
if (userLevel < kick) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for kicks: ${userLevel} < ${kick}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
}
|
||||
if (userLevel < redact) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for redactions: ${userLevel} < ${redact}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
}
|
||||
if (userLevel < aclLevel) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for server ACLs: ${userLevel} < ${aclLevel}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
}
|
||||
|
||||
// Wants: Additional permissions
|
||||
|
||||
for (const additionalPermission of additionalPermissions) {
|
||||
const permLevel = plDefault(events[additionalPermission], stateDefault);
|
||||
|
||||
if (userLevel < permLevel) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for "${additionalPermission}" state events: ${userLevel} < ${permLevel}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise OK
|
||||
} catch (e) {
|
||||
LogService.error("Mjolnir", extractRequestError(e));
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: e.message || (e.body ? e.body.error : '<no message>'),
|
||||
errorKind: ERROR_KIND_FATAL,
|
||||
});
|
||||
public addUnprotectedWatchedListRoom(roomId: string) {
|
||||
if (this._unprotectedWatchedListRooms.includes(roomId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return errors;
|
||||
this._unprotectedWatchedListRooms.push(roomId);
|
||||
this.removeProtectedRoom(roomId);
|
||||
}
|
||||
}
|
@ -20,19 +20,19 @@ import { extractRequestError, LogLevel, LogService } from "matrix-bot-sdk";
|
||||
// !mjolnir rooms add <room alias/ID>
|
||||
export async function execAddProtectedRoom(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||
const protectedRoomId = await mjolnir.client.joinRoom(parts[3]);
|
||||
await mjolnir.addProtectedRoom(protectedRoomId);
|
||||
await mjolnir.protectedRoomsTracker.addProtectedRoom(protectedRoomId);
|
||||
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
|
||||
}
|
||||
|
||||
// !mjolnir rooms remove <room alias/ID>
|
||||
export async function execRemoveProtectedRoom(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||
const protectedRoomId = await mjolnir.client.resolveRoom(parts[3]);
|
||||
await mjolnir.removeProtectedRoom(protectedRoomId);
|
||||
await mjolnir.protectedRoomsTracker.removeProtectedRoom(protectedRoomId);
|
||||
try {
|
||||
await mjolnir.client.leaveRoom(protectedRoomId);
|
||||
} catch (e) {
|
||||
LogService.warn("AddRemoveProtectedRoomsCommand", extractRequestError(e));
|
||||
await mjolnir.logMessage(LogLevel.WARN, "AddRemoveProtectedRoomsCommand", `Failed to leave ${protectedRoomId} - the room is no longer being protected, but the bot could not leave`, protectedRoomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "AddRemoveProtectedRoomsCommand", `Failed to leave ${protectedRoomId} - the room is no longer being protected, but the bot could not leave`, protectedRoomId);
|
||||
}
|
||||
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ export async function execKickCommand(roomId: string, event: any, mjolnir: Mjoln
|
||||
let force = false;
|
||||
|
||||
const glob = parts[2];
|
||||
let rooms = [...Object.keys(mjolnir.protectedRooms)];
|
||||
let rooms = [...Object.keys(mjolnir.protectedRoomsTracker.protectedRooms)];
|
||||
|
||||
if (parts[parts.length - 1] === "--force") {
|
||||
force = true;
|
||||
@ -57,7 +57,7 @@ export async function execKickCommand(roomId: string, event: any, mjolnir: Mjoln
|
||||
const victim = member.membershipFor;
|
||||
|
||||
if (kickRule.test(victim)) {
|
||||
await mjolnir.logMessage(LogLevel.DEBUG, "KickCommand", `Removing ${victim} in ${protectedRoomId}`, protectedRoomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.DEBUG, "KickCommand", `Removing ${victim} in ${protectedRoomId}`, protectedRoomId);
|
||||
|
||||
if (!mjolnir.config.noop) {
|
||||
try {
|
||||
@ -65,10 +65,10 @@ export async function execKickCommand(roomId: string, event: any, mjolnir: Mjoln
|
||||
return mjolnir.client.kickUser(victim, protectedRoomId, reason);
|
||||
});
|
||||
} catch (e) {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "KickCommand", `An error happened while trying to kick ${victim}: ${e}`);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "KickCommand", `An error happened while trying to kick ${victim}: ${e}`);
|
||||
}
|
||||
} else {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "KickCommand", `Tried to kick ${victim} in ${protectedRoomId} but the bot is running in no-op mode.`, protectedRoomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "KickCommand", `Tried to kick ${victim} in ${protectedRoomId} but the bot is running in no-op mode.`, protectedRoomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,18 +15,19 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { Mjolnir } from "../Mjolnir";
|
||||
import { RichReply } from "matrix-bot-sdk";
|
||||
import { Permalinks, RichReply } from "matrix-bot-sdk";
|
||||
|
||||
// !mjolnir rooms
|
||||
export async function execListProtectedRooms(roomId: string, event: any, mjolnir: Mjolnir) {
|
||||
let html = `<b>Protected rooms (${Object.keys(mjolnir.protectedRooms).length}):</b><br/><ul>`;
|
||||
let text = `Protected rooms (${Object.keys(mjolnir.protectedRooms).length}):\n`;
|
||||
const rooms = [...mjolnir.protectedRoomsTracker.protectedRooms];
|
||||
let html = `<b>Protected rooms (${rooms.length}):</b><br/><ul>`;
|
||||
let text = `Protected rooms (${rooms.length}):\n`;
|
||||
|
||||
let hasRooms = false;
|
||||
for (const protectedRoomId in mjolnir.protectedRooms) {
|
||||
for (const protectedRoomId in rooms) {
|
||||
hasRooms = true;
|
||||
|
||||
const roomUrl = mjolnir.protectedRooms[protectedRoomId];
|
||||
const roomUrl = Permalinks.forRoom(protectedRoomId);
|
||||
html += `<li><a href="${roomUrl}">${protectedRoomId}</a></li>`;
|
||||
text += `* ${roomUrl}\n`;
|
||||
}
|
||||
|
@ -18,5 +18,5 @@ import { Mjolnir } from "../Mjolnir";
|
||||
|
||||
// !mjolnir verify
|
||||
export async function execPermissionCheckCommand(roomId: string, event: any, mjolnir: Mjolnir) {
|
||||
return mjolnir.verifyPermissions(true, true);
|
||||
return mjolnir.protectedRoomsTracker.verifyPermissions(true, true);
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ import { isListSetting } from "../protections/ProtectionSettings";
|
||||
// !mjolnir enable <protection>
|
||||
export async function execEnableProtection(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||
try {
|
||||
await mjolnir.enableProtection(parts[2]);
|
||||
await mjolnir.protectionManager.enableProtection(parts[2]);
|
||||
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
|
||||
} catch (e) {
|
||||
LogService.error("ProtectionsCommands", extractRequestError(e));
|
||||
@ -50,8 +50,8 @@ enum ConfigAction {
|
||||
*/
|
||||
async function _execConfigChangeProtection(mjolnir: Mjolnir, parts: string[], action: ConfigAction): Promise<string> {
|
||||
const [protectionName, ...settingParts] = parts[0].split(".");
|
||||
const protection = mjolnir.protections.get(protectionName);
|
||||
if (protection === undefined) {
|
||||
const protection = mjolnir.protectionManager.getProtection(protectionName);
|
||||
if (!protection) {
|
||||
return `Unknown protection ${protectionName}`;
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ async function _execConfigChangeProtection(mjolnir: Mjolnir, parts: string[], ac
|
||||
}
|
||||
|
||||
try {
|
||||
await mjolnir.setProtectionSettings(protectionName, { [settingName]: value });
|
||||
await mjolnir.protectionManager.setProtectionSettings(protectionName, { [settingName]: value });
|
||||
} catch (e) {
|
||||
return `Failed to set setting: ${e.message}`;
|
||||
}
|
||||
@ -139,7 +139,7 @@ export async function execConfigRemoveProtection(roomId: string, event: any, mjo
|
||||
* !mjolnir get [protection name]
|
||||
*/
|
||||
export async function execConfigGetProtection(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||
let pickProtections = Object.keys(mjolnir.protections);
|
||||
let pickProtections = Object.keys(mjolnir.protectionManager.protections);
|
||||
|
||||
if (parts.length < 3) {
|
||||
// no specific protectionName provided, show all of them.
|
||||
@ -163,7 +163,7 @@ export async function execConfigGetProtection(roomId: string, event: any, mjolni
|
||||
let anySettings = false;
|
||||
|
||||
for (const protectionName of pickProtections) {
|
||||
const protectionSettings = mjolnir.protections.get(protectionName)?.settings ?? {};
|
||||
const protectionSettings = mjolnir.protectionManager.getProtection(protectionName)?.settings ?? {};
|
||||
|
||||
if (Object.keys(protectionSettings).length === 0) {
|
||||
continue;
|
||||
@ -196,18 +196,18 @@ export async function execConfigGetProtection(roomId: string, event: any, mjolni
|
||||
|
||||
// !mjolnir disable <protection>
|
||||
export async function execDisableProtection(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||
await mjolnir.disableProtection(parts[2]);
|
||||
await mjolnir.protectionManager.disableProtection(parts[2]);
|
||||
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
|
||||
}
|
||||
|
||||
// !mjolnir protections
|
||||
export async function execListProtections(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||
const enabledProtections = mjolnir.enabledProtections.map(p => p.name);
|
||||
const enabledProtections = mjolnir.protectionManager.enabledProtections.map(p => p.name);
|
||||
|
||||
let html = "Available protections:<ul>";
|
||||
let text = "Available protections:\n";
|
||||
|
||||
for (const [protectionName, protection] of mjolnir.protections) {
|
||||
for (const [protectionName, protection] of mjolnir.protectionManager.protections) {
|
||||
const emoji = enabledProtections.includes(protectionName) ? '🟢 (enabled)' : '🔴 (disabled)';
|
||||
html += `<li>${emoji} <code>${protectionName}</code> - ${protection.description}</li>`;
|
||||
text += `* ${emoji} ${protectionName} - ${protection.description}\n`;
|
||||
|
@ -45,8 +45,8 @@ export async function execRedactCommand(roomId: string, event: any, mjolnir: Mjo
|
||||
return;
|
||||
}
|
||||
|
||||
const targetRoomIds = roomAlias ? [roomAlias] : Object.keys(mjolnir.protectedRooms);
|
||||
await redactUserMessagesIn(mjolnir, userId, targetRoomIds, limit);
|
||||
const targetRoomIds = roomAlias ? [roomAlias] : [...mjolnir.protectedRoomsTracker.protectedRooms];
|
||||
await redactUserMessagesIn(mjolnir.client, mjolnir.managementRoom, userId, targetRoomIds, limit);
|
||||
|
||||
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
|
||||
await mjolnir.client.redactEvent(roomId, processingReactionId, 'done processing');
|
||||
|
@ -23,14 +23,14 @@ export async function execSetPowerLevelCommand(roomId: string, event: any, mjoln
|
||||
const level = Math.round(Number(parts[3]));
|
||||
const inRoom = parts[4];
|
||||
|
||||
let targetRooms = inRoom ? [await mjolnir.client.resolveRoom(inRoom)] : Object.keys(mjolnir.protectedRooms);
|
||||
let targetRooms = inRoom ? [await mjolnir.client.resolveRoom(inRoom)] : [...mjolnir.protectedRoomsTracker.protectedRooms];
|
||||
|
||||
for (const targetRoomId of targetRooms) {
|
||||
try {
|
||||
await mjolnir.client.setUserPowerLevel(victim, targetRoomId, level);
|
||||
} catch (e) {
|
||||
const message = e.message || (e.body ? e.body.error : '<no message>');
|
||||
await mjolnir.logMessage(LogLevel.ERROR, "SetPowerLevelCommand", `Failed to set power level of ${victim} to ${level} in ${targetRoomId}: ${message}`, targetRoomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.ERROR, "SetPowerLevelCommand", `Failed to set power level of ${victim} to ${level} in ${targetRoomId}: ${message}`, targetRoomId);
|
||||
LogService.error("SetPowerLevelCommand", extractRequestError(e));
|
||||
}
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ export async function execSinceCommand(destinationRoomId: string, event: any, mj
|
||||
let result = await execSinceCommandAux(destinationRoomId, event, mjolnir, tokens);
|
||||
if ("error" in result) {
|
||||
mjolnir.client.unstableApis.addReactionToEvent(destinationRoomId, event['event_id'], '❌');
|
||||
mjolnir.logMessage(LogLevel.WARN, "SinceCommand", result.error);
|
||||
mjolnir.managementRoom.logMessage(LogLevel.WARN, "SinceCommand", result.error);
|
||||
const reply = RichReply.createFor(destinationRoomId, event, result.error, htmlEscape(result.error));
|
||||
reply["msgtype"] = "m.notice";
|
||||
/* no need to await */ mjolnir.client.sendMessage(destinationRoomId, reply);
|
||||
@ -185,6 +185,7 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni
|
||||
// Now list affected rooms.
|
||||
const rooms: Set</* room id */string> = new Set();
|
||||
let reasonParts: string[] | undefined;
|
||||
const protectedRooms = new Set(mjolnir.protectedRoomsTracker.protectedRooms);
|
||||
for (let token of optionalTokens) {
|
||||
const maybeArg = getTokenAsString(reasonParts ? "[reason]" : "[room]", token);
|
||||
if ("error" in maybeArg) {
|
||||
@ -194,14 +195,14 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni
|
||||
if (!reasonParts) {
|
||||
// If we haven't reached the reason yet, attempt to use `maybeRoom` as a room.
|
||||
if (maybeRoom === "*") {
|
||||
for (let roomId of Object.keys(mjolnir.protectedRooms)) {
|
||||
for (let roomId of mjolnir.protectedRoomsTracker.protectedRooms) {
|
||||
rooms.add(roomId);
|
||||
}
|
||||
continue;
|
||||
} else if (maybeRoom.startsWith("#") || maybeRoom.startsWith("!")) {
|
||||
const roomId = await mjolnir.client.resolveRoom(maybeRoom);
|
||||
if (!(roomId in mjolnir.protectedRooms)) {
|
||||
return mjolnir.logMessage(LogLevel.WARN, "SinceCommand", `This room is not protected: ${htmlEscape(roomId)}.`);
|
||||
if (!protectedRooms.has(roomId)) {
|
||||
return mjolnir.managementRoom.logMessage(LogLevel.WARN, "SinceCommand", `This room is not protected: ${htmlEscape(roomId)}.`);
|
||||
}
|
||||
rooms.add(roomId);
|
||||
continue;
|
||||
@ -225,7 +226,7 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni
|
||||
for (let targetRoomId of rooms) {
|
||||
let {html, text} = await (async () => {
|
||||
let results: Summary = { succeeded: [], failed: []};
|
||||
const recentJoins = mjolnir.roomJoins.getUsersInRoom(targetRoomId, minDate, maxEntries);
|
||||
const recentJoins = mjolnir.protectedRoomsTracker.roomJoins.getUsersInRoom(targetRoomId, minDate, maxEntries);
|
||||
|
||||
switch (action) {
|
||||
case Action.Show: {
|
||||
|
@ -67,8 +67,9 @@ async function showMjolnirStatus(roomId: string, event: any, mjolnir: Mjolnir) {
|
||||
break;
|
||||
}
|
||||
|
||||
html += `<b>Protected rooms: </b> ${Object.keys(mjolnir.protectedRooms).length}<br/>`;
|
||||
text += `Protected rooms: ${Object.keys(mjolnir.protectedRooms).length}\n`;
|
||||
const protectedRooms = [...mjolnir.protectedRoomsTracker.protectedRooms];
|
||||
html += `<b>Protected rooms: </b> ${protectedRooms.length}<br/>`;
|
||||
text += `Protected rooms: ${protectedRooms.length}\n`;
|
||||
|
||||
// Append list information
|
||||
html += "<b>Subscribed ban lists:</b><br><ul>";
|
||||
@ -91,7 +92,7 @@ async function showMjolnirStatus(roomId: string, event: any, mjolnir: Mjolnir) {
|
||||
|
||||
async function showProtectionStatus(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||
const protectionName = parts[0];
|
||||
const protection = mjolnir.getProtection(protectionName);
|
||||
const protection = mjolnir.protectionManager.getProtection(protectionName);
|
||||
let text;
|
||||
let html;
|
||||
if (!protection) {
|
||||
@ -156,7 +157,7 @@ async function showJoinsStatus(destinationRoomId: string, event: any, mjolnir: M
|
||||
text: `Cannot resolve room \`${targetRoomAliasOrId}\`.`
|
||||
}
|
||||
}
|
||||
const joins = mjolnir.roomJoins.getUsersInRoom(targetRoomId, minDate, maxEntries);
|
||||
const joins = mjolnir.protectedRoomsTracker.roomJoins.getUsersInRoom(targetRoomId, minDate, maxEntries);
|
||||
const htmlFragments = [];
|
||||
const textFragments = [];
|
||||
for (let join of joins) {
|
||||
|
@ -18,5 +18,5 @@ import { Mjolnir } from "../Mjolnir";
|
||||
|
||||
// !mjolnir sync
|
||||
export async function execSyncCommand(roomId: string, event: any, mjolnir: Mjolnir) {
|
||||
return mjolnir.syncLists();
|
||||
return mjolnir.protectedRoomsTracker.syncLists(mjolnir.config.verboseLogging);
|
||||
}
|
||||
|
@ -138,21 +138,21 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol
|
||||
|
||||
if (USER_RULE_TYPES.includes(bits.ruleType!) && bits.reason === 'true') {
|
||||
const rule = new MatrixGlob(bits.entity);
|
||||
await mjolnir.logMessage(LogLevel.INFO, "UnbanBanCommand", "Unbanning users that match glob: " + bits.entity);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.INFO, "UnbanBanCommand", "Unbanning users that match glob: " + bits.entity);
|
||||
let unbannedSomeone = false;
|
||||
for (const protectedRoomId of Object.keys(mjolnir.protectedRooms)) {
|
||||
for (const protectedRoomId of mjolnir.protectedRoomsTracker.protectedRooms) {
|
||||
const members = await mjolnir.client.getRoomMembers(protectedRoomId, undefined, ['ban'], undefined);
|
||||
await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Found ${members.length} banned user(s)`);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Found ${members.length} banned user(s)`);
|
||||
for (const member of members) {
|
||||
const victim = member.membershipFor;
|
||||
if (member.membership !== 'ban') continue;
|
||||
if (rule.test(victim)) {
|
||||
await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Unbanning ${victim} in ${protectedRoomId}`, protectedRoomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Unbanning ${victim} in ${protectedRoomId}`, protectedRoomId);
|
||||
|
||||
if (!mjolnir.config.noop) {
|
||||
await mjolnir.client.unbanUser(victim, protectedRoomId);
|
||||
} else {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "UnbanBanCommand", `Attempted to unban ${victim} in ${protectedRoomId} but Mjolnir is running in no-op mode`, protectedRoomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "UnbanBanCommand", `Attempted to unban ${victim} in ${protectedRoomId} but Mjolnir is running in no-op mode`, protectedRoomId);
|
||||
}
|
||||
|
||||
unbannedSomeone = true;
|
||||
@ -161,8 +161,8 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol
|
||||
}
|
||||
|
||||
if (unbannedSomeone) {
|
||||
await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Syncing lists to ensure no users were accidentally unbanned`);
|
||||
await mjolnir.syncLists(mjolnir.config.verboseLogging);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Syncing lists to ensure no users were accidentally unbanned`);
|
||||
await mjolnir.protectedRoomsTracker.syncLists(mjolnir.config.verboseLogging);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,11 +62,11 @@ export class BasicFlooding extends Protection {
|
||||
}
|
||||
|
||||
if (messageCount >= this.settings.maxPerMinute.value) {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "BasicFlooding", `Banning ${event['sender']} in ${roomId} for flooding (${messageCount} messages in the last minute)`, roomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "BasicFlooding", `Banning ${event['sender']} in ${roomId} for flooding (${messageCount} messages in the last minute)`, roomId);
|
||||
if (!mjolnir.config.noop) {
|
||||
await mjolnir.client.banUser(event['sender'], roomId, "spam");
|
||||
} else {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "BasicFlooding", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "BasicFlooding", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
}
|
||||
|
||||
if (this.recentlyBanned.includes(event['sender'])) return; // already handled (will be redacted)
|
||||
@ -79,7 +79,7 @@ export class BasicFlooding extends Protection {
|
||||
await mjolnir.client.redactEvent(roomId, eventId, "spam");
|
||||
}
|
||||
} else {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "BasicFlooding", `Tried to redact messages for ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "BasicFlooding", `Tried to redact messages for ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
}
|
||||
|
||||
// Free up some memory now that we're ready to handle it elsewhere
|
||||
|
@ -627,7 +627,7 @@ export class DetectFederationLag extends Protection {
|
||||
roomInfo.latestAlertStart = now;
|
||||
// Background-send message.
|
||||
const stats = roomInfo.globalStats();
|
||||
/* do not await */ mjolnir.logMessage(LogLevel.WARN, "FederationLag",
|
||||
/* do not await */ mjolnir.managementRoom.logMessage(LogLevel.WARN, "FederationLag",
|
||||
`Room ${roomId} is experiencing ${isLocalDomainOnAlert ? "LOCAL" : "federated"} lag since ${roomInfo.latestAlertStart}.\n${roomInfo.alerts} homeservers are lagging: ${[...roomInfo.serversOnAlert()].sort()} .\nRoom lag statistics: ${JSON.stringify(stats, null, 2)}.`);
|
||||
// Drop a state event, for the use of potential other bots.
|
||||
const warnStateEventId = await mjolnir.client.sendStateEvent(mjolnir.managementRoomId, LAG_STATE_EVENT, roomId, {
|
||||
@ -642,7 +642,7 @@ export class DetectFederationLag extends Protection {
|
||||
} else if (roomInfo.alerts < this.settings.numberOfLaggingFederatedHomeserversExitWarningZone.value
|
||||
|| !isLocalDomainOnAlert) {
|
||||
// Stop the alarm!
|
||||
/* do not await */ mjolnir.logMessage(LogLevel.INFO, "FederationLag",
|
||||
/* do not await */ mjolnir.managementRoom.logMessage(LogLevel.INFO, "FederationLag",
|
||||
`Room ${roomId} lag has decreased to an acceptable level. Currently, ${roomInfo.alerts} homeservers are still lagging`
|
||||
);
|
||||
if (roomInfo.warnStateEventId) {
|
||||
|
@ -56,11 +56,11 @@ export class FirstMessageIsImage extends Protection {
|
||||
const formattedBody = content['formatted_body'] || '';
|
||||
const isMedia = msgtype === 'm.image' || msgtype === 'm.video' || formattedBody.toLowerCase().includes('<img');
|
||||
if (isMedia && this.justJoined[roomId].includes(event['sender'])) {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Banning ${event['sender']} for posting an image as the first thing after joining in ${roomId}.`);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Banning ${event['sender']} for posting an image as the first thing after joining in ${roomId}.`);
|
||||
if (!mjolnir.config.noop) {
|
||||
await mjolnir.client.banUser(event['sender'], roomId, "spam");
|
||||
} else {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
}
|
||||
|
||||
if (this.recentlyBanned.includes(event['sender'])) return; // already handled (will be redacted)
|
||||
@ -71,7 +71,7 @@ export class FirstMessageIsImage extends Protection {
|
||||
if (!mjolnir.config.noop) {
|
||||
await mjolnir.client.redactEvent(roomId, event['event_id'], "spam");
|
||||
} else {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ export class JoinWaveShortCircuit extends Protection {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(roomId in mjolnir.protectedRooms)) {
|
||||
if (!mjolnir.protectedRoomsTracker.isProtectedRoom(roomId)) {
|
||||
// Not a room we are watching.
|
||||
return;
|
||||
}
|
||||
@ -86,12 +86,12 @@ export class JoinWaveShortCircuit extends Protection {
|
||||
}
|
||||
|
||||
if (++this.joinBuckets[roomId].numberOfJoins >= this.settings.maxPer.value) {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Setting ${roomId} to invite-only as more than ${this.settings.maxPer.value} users have joined over the last ${this.settings.timescaleMinutes.value} minutes (since ${this.joinBuckets[roomId].lastBucketStart})`, roomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Setting ${roomId} to invite-only as more than ${this.settings.maxPer.value} users have joined over the last ${this.settings.timescaleMinutes.value} minutes (since ${this.joinBuckets[roomId].lastBucketStart})`, roomId);
|
||||
|
||||
if (!mjolnir.config.noop) {
|
||||
await mjolnir.client.sendStateEvent(roomId, "m.room.join_rules", "", {"join_rule": "invite"})
|
||||
} else {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Tried to set ${roomId} to invite-only, but Mjolnir is running in no-op mode`, roomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Tried to set ${roomId} to invite-only, but Mjolnir is running in no-op mode`, roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,12 +40,12 @@ export class MessageIsMedia extends Protection {
|
||||
const formattedBody = content['formatted_body'] || '';
|
||||
const isMedia = msgtype === 'm.image' || msgtype === 'm.video' || formattedBody.toLowerCase().includes('<img');
|
||||
if (isMedia) {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "MessageIsMedia", `Redacting event from ${event['sender']} for posting an image/video. ${Permalinks.forEvent(roomId, event['event_id'], [new UserID(await mjolnir.client.getUserId()).domain])}`);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "MessageIsMedia", `Redacting event from ${event['sender']} for posting an image/video. ${Permalinks.forEvent(roomId, event['event_id'], [new UserID(await mjolnir.client.getUserId()).domain])}`);
|
||||
// Redact the event
|
||||
if (!mjolnir.config.noop) {
|
||||
await mjolnir.client.redactEvent(roomId, event['event_id'], "Images/videos are not permitted here");
|
||||
} else {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "MessageIsMedia", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "MessageIsMedia", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,12 +37,12 @@ export class MessageIsVoice extends Protection {
|
||||
if (event['type'] === 'm.room.message' && event['content']) {
|
||||
if (event['content']['msgtype'] !== 'm.audio') return;
|
||||
if (event['content']['org.matrix.msc3245.voice'] === undefined) return;
|
||||
await mjolnir.logMessage(LogLevel.INFO, "MessageIsVoice", `Redacting event from ${event['sender']} for posting a voice message. ${Permalinks.forEvent(roomId, event['event_id'], [new UserID(await mjolnir.client.getUserId()).domain])}`);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.INFO, "MessageIsVoice", `Redacting event from ${event['sender']} for posting a voice message. ${Permalinks.forEvent(roomId, event['event_id'], [new UserID(await mjolnir.client.getUserId()).domain])}`);
|
||||
// Redact the event
|
||||
if (!mjolnir.config.noop) {
|
||||
await mjolnir.client.redactEvent(roomId, event['event_id'], "Voice messages are not permitted here");
|
||||
} else {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "MessageIsVoice", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "MessageIsVoice", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,24 +91,24 @@ export class WordList extends Protection {
|
||||
"i"
|
||||
);
|
||||
} catch (ex) {
|
||||
await mjolnir.logMessage(LogLevel.ERROR, "WordList", `Could not produce a regex from the word list:\n${ex}.`)
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.ERROR, "WordList", `Could not produce a regex from the word list:\n${ex}.`)
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the test
|
||||
if (message && this.badWords!.test(message)) {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "WordList", `Banning ${event['sender']} for word list violation in ${roomId}.`);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "WordList", `Banning ${event['sender']} for word list violation in ${roomId}.`);
|
||||
if (!mjolnir.config.noop) {
|
||||
await mjolnir.client.banUser(event['sender'], roomId, "Word list violation");
|
||||
} else {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "WordList", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "WordList", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
}
|
||||
|
||||
// Redact the event
|
||||
if (!mjolnir.config.noop) {
|
||||
await mjolnir.client.redactEvent(roomId, event['event_id'], "spam");
|
||||
} else {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "WordList", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "WordList", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,8 +23,15 @@ import { MessageIsVoice } from "./MessageIsVoice";
|
||||
import { MessageIsMedia } from "./MessageIsMedia";
|
||||
import { TrustedReporters } from "./TrustedReporters";
|
||||
import { JoinWaveShortCircuit } from "./JoinWaveShortCircuit";
|
||||
import { Mjolnir } from "../Mjolnir";
|
||||
import { extractRequestError, LogLevel, LogService, Permalinks } from "matrix-bot-sdk";
|
||||
import { ProtectionSettingValidationError } from "./ProtectionSettings";
|
||||
import { Consequence, ConsequenceType } from "./consequence";
|
||||
import { htmlEscape } from "../utils";
|
||||
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
|
||||
import { RoomUpdateError } from "../models/RoomUpdateError";
|
||||
|
||||
export const PROTECTIONS: Protection[] = [
|
||||
const PROTECTIONS: Protection[] = [
|
||||
new FirstMessageIsImage(),
|
||||
new BasicFlooding(),
|
||||
new WordList(),
|
||||
@ -34,3 +41,353 @@ export const PROTECTIONS: Protection[] = [
|
||||
new DetectFederationLag(),
|
||||
new JoinWaveShortCircuit(),
|
||||
];
|
||||
|
||||
const ENABLED_PROTECTIONS_EVENT_TYPE = "org.matrix.mjolnir.enabled_protections";
|
||||
const CONSEQUENCE_EVENT_DATA = "org.matrix.mjolnir.consequence";
|
||||
|
||||
export class ProtectionManager {
|
||||
private _protections = new Map<string /* protection name */, Protection>();
|
||||
get protections(): Readonly<Map<string /* protection name */, Protection>> {
|
||||
return this._protections;
|
||||
}
|
||||
|
||||
|
||||
constructor(private readonly mjolnir: Mjolnir) {
|
||||
}
|
||||
|
||||
/*
|
||||
* Take all the builtin protections, register them to set their enabled (or not) state and
|
||||
* update their settings with any saved non-default values
|
||||
*/
|
||||
public async start() {
|
||||
this.mjolnir.reportManager.on("report.new", this.handleReport.bind(this));
|
||||
this.mjolnir.client.on("room.event", this.handleEvent.bind(this));
|
||||
for (const protection of PROTECTIONS) {
|
||||
try {
|
||||
await this.registerProtection(protection);
|
||||
} catch (e) {
|
||||
this.mjolnir.managementRoom.logMessage(LogLevel.WARN, "ProtectionManager", extractRequestError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Given a protection object; add it to our list of protections, set whether it is enabled
|
||||
* and update its settings with any saved non-default values.
|
||||
*
|
||||
* @param protection The protection object we want to register
|
||||
*/
|
||||
public async registerProtection(protection: Protection) {
|
||||
this._protections.set(protection.name, protection)
|
||||
|
||||
let enabledProtections: { enabled: string[] } | null = null;
|
||||
try {
|
||||
enabledProtections = await this.mjolnir.client.getAccountData(ENABLED_PROTECTIONS_EVENT_TYPE);
|
||||
} catch {
|
||||
// this setting either doesn't exist, or we failed to read it (bad network?)
|
||||
// TODO: retry on certain failures?
|
||||
}
|
||||
protection.enabled = enabledProtections?.enabled.includes(protection.name) ?? false;
|
||||
|
||||
const savedSettings = await this.getProtectionSettings(protection.name);
|
||||
for (let [key, value] of Object.entries(savedSettings)) {
|
||||
// this.getProtectionSettings() validates this data for us, so we don't need to
|
||||
protection.settings[key].setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Given a protection object; remove it from our list of protections.
|
||||
*
|
||||
* @param protection The protection object we want to unregister
|
||||
*/
|
||||
public unregisterProtection(protectionName: string) {
|
||||
if (!(protectionName in this._protections)) {
|
||||
throw new Error("Failed to find protection by name: " + protectionName);
|
||||
}
|
||||
this._protections.delete(protectionName);
|
||||
}
|
||||
|
||||
/*
|
||||
* Takes an object of settings we want to change and what their values should be,
|
||||
* check that their values are valid, combine them with current saved settings,
|
||||
* then save the amalgamation to a state event
|
||||
*
|
||||
* @param protectionName Which protection these settings belong to
|
||||
* @param changedSettings The settings to change and their values
|
||||
*/
|
||||
public async setProtectionSettings(protectionName: string, changedSettings: { [setting: string]: any }): Promise<any> {
|
||||
const protection = this._protections.get(protectionName);
|
||||
if (protection === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedSettings: { [setting: string]: any } = await this.getProtectionSettings(protectionName);
|
||||
|
||||
for (let [key, value] of Object.entries(changedSettings)) {
|
||||
if (!(key in protection.settings)) {
|
||||
throw new ProtectionSettingValidationError(`Failed to find protection setting by name: ${key}`);
|
||||
}
|
||||
if (typeof (protection.settings[key].value) !== typeof (value)) {
|
||||
throw new ProtectionSettingValidationError(`Invalid type for protection setting: ${key} (${typeof (value)})`);
|
||||
}
|
||||
if (!protection.settings[key].validate(value)) {
|
||||
throw new ProtectionSettingValidationError(`Invalid value for protection setting: ${key} (${value})`);
|
||||
}
|
||||
validatedSettings[key] = value;
|
||||
}
|
||||
|
||||
await this.mjolnir.client.sendStateEvent(
|
||||
this.mjolnir.managementRoomId, 'org.matrix.mjolnir.setting', protectionName, validatedSettings
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Make a list of the names of enabled protections and save them in a state event
|
||||
*/
|
||||
private async saveEnabledProtections() {
|
||||
const protections = this.enabledProtections.map(p => p.name);
|
||||
await this.mjolnir.client.setAccountData(ENABLED_PROTECTIONS_EVENT_TYPE, { enabled: protections });
|
||||
}
|
||||
/*
|
||||
* Enable a protection by name and persist its enable state in to a state event
|
||||
*
|
||||
* @param name The name of the protection whose settings we're enabling
|
||||
*/
|
||||
public async enableProtection(name: string) {
|
||||
const protection = this._protections.get(name);
|
||||
if (protection !== undefined) {
|
||||
protection.enabled = true;
|
||||
await this.saveEnabledProtections();
|
||||
}
|
||||
}
|
||||
|
||||
public get enabledProtections(): Protection[] {
|
||||
return [...this._protections.values()].filter(p => p.enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a protection by name.
|
||||
*
|
||||
* @return If there is a protection with this name *and* it is enabled,
|
||||
* return the protection.
|
||||
*/
|
||||
public getProtection(protectionName: string): Protection | null {
|
||||
return this._protections.get(protectionName) ?? null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Disable a protection by name and remove it from the persistent list of enabled protections
|
||||
*
|
||||
* @param name The name of the protection whose settings we're disabling
|
||||
*/
|
||||
public async disableProtection(name: string) {
|
||||
const protection = this._protections.get(name);
|
||||
if (protection !== undefined) {
|
||||
protection.enabled = false;
|
||||
await this.saveEnabledProtections();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Read org.matrix.mjolnir.setting state event, find any saved settings for
|
||||
* the requested protectionName, then iterate and validate against their parser
|
||||
* counterparts in Protection.settings and return those which validate
|
||||
*
|
||||
* @param protectionName The name of the protection whose settings we're reading
|
||||
* @returns Every saved setting for this protectionName that has a valid value
|
||||
*/
|
||||
public async getProtectionSettings(protectionName: string): Promise<{ [setting: string]: any }> {
|
||||
let savedSettings: { [setting: string]: any } = {}
|
||||
try {
|
||||
savedSettings = await this.mjolnir.client.getRoomStateEvent(
|
||||
this.mjolnir.managementRoomId, 'org.matrix.mjolnir.setting', protectionName
|
||||
);
|
||||
} catch {
|
||||
// setting does not exist, return empty object
|
||||
return {};
|
||||
}
|
||||
|
||||
const settingDefinitions = this._protections.get(protectionName)?.settings ?? {};
|
||||
const validatedSettings: { [setting: string]: any } = {}
|
||||
for (let [key, value] of Object.entries(savedSettings)) {
|
||||
if (
|
||||
// is this a setting name with a known parser?
|
||||
key in settingDefinitions
|
||||
// is the datatype of this setting's value what we expect?
|
||||
&& typeof (settingDefinitions[key].value) === typeof (value)
|
||||
// is this setting's value valid for the setting?
|
||||
&& settingDefinitions[key].validate(value)
|
||||
) {
|
||||
validatedSettings[key] = value;
|
||||
} else {
|
||||
await this.mjolnir.managementRoom.logMessage(
|
||||
LogLevel.WARN,
|
||||
"getProtectionSetting",
|
||||
`Tried to read ${protectionName}.${key} and got invalid value ${value}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return validatedSettings;
|
||||
}
|
||||
|
||||
private async handleConsequence(protection: Protection, roomId: string, eventId: string, sender: string, consequence: Consequence) {
|
||||
switch (consequence.type) {
|
||||
case ConsequenceType.alert:
|
||||
break;
|
||||
case ConsequenceType.redact:
|
||||
await this.mjolnir.client.redactEvent(roomId, eventId, "abuse detected");
|
||||
break;
|
||||
case ConsequenceType.ban:
|
||||
await this.mjolnir.client.banUser(sender, roomId, "abuse detected");
|
||||
break;
|
||||
}
|
||||
|
||||
let message = `protection ${protection.name} enacting ${ConsequenceType[consequence.type]}`
|
||||
+ ` against ${htmlEscape(sender)}`
|
||||
+ ` in ${htmlEscape(roomId)}`;
|
||||
if (consequence.reason !== undefined) {
|
||||
// even though internally-sourced, there's no promise that `consequence.reason`
|
||||
// will never have user-supplied information, so escape it
|
||||
message += ` (reason: ${htmlEscape(consequence.reason)})`;
|
||||
}
|
||||
|
||||
await this.mjolnir.client.sendMessage(this.mjolnir.managementRoomId, {
|
||||
msgtype: "m.notice",
|
||||
body: message,
|
||||
[CONSEQUENCE_EVENT_DATA]: {
|
||||
who: sender,
|
||||
room: roomId,
|
||||
type: ConsequenceType[consequence.type]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleEvent(roomId: string, event: any) {
|
||||
if (roomId in this.mjolnir.protectedRoomsTracker.protectedRooms) {
|
||||
if (event['sender'] === await this.mjolnir.client.getUserId()) return; // Ignore ourselves
|
||||
|
||||
// Iterate all the enabled protections
|
||||
for (const protection of this.enabledProtections) {
|
||||
let consequence: Consequence | undefined = undefined;
|
||||
try {
|
||||
consequence = await protection.handleEvent(this.mjolnir, roomId, event);
|
||||
} catch (e) {
|
||||
const eventPermalink = Permalinks.forEvent(roomId, event['event_id']);
|
||||
LogService.error("ProtectionManager", "Error handling protection: " + protection.name);
|
||||
LogService.error("ProtectionManager", "Failed event: " + eventPermalink);
|
||||
LogService.error("ProtectionManager", extractRequestError(e));
|
||||
await this.mjolnir.client.sendNotice(this.mjolnir.managementRoomId, "There was an error processing an event through a protection - see log for details. Event: " + eventPermalink);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (consequence !== undefined) {
|
||||
await this.handleConsequence(protection, roomId, event["event_id"], event["sender"], consequence);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the event handlers - we always run this after protections so that the protections
|
||||
// can flag the event for redaction.
|
||||
await this.mjolnir.unlistedUserRedactionHandler.handleEvent(roomId, event, this.mjolnir); // FIXME: That's rather spaghetti
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private requiredProtectionPermissions(): Set<string> {
|
||||
return new Set(this.enabledProtections.map((p) => p.requiredStatePermissions).flat())
|
||||
}
|
||||
|
||||
public async verifyPermissionsIn(roomId: string): Promise<RoomUpdateError[]> {
|
||||
const errors: RoomUpdateError[] = [];
|
||||
const additionalPermissions = this.requiredProtectionPermissions();
|
||||
|
||||
try {
|
||||
const ownUserId = await this.mjolnir.client.getUserId();
|
||||
|
||||
const powerLevels = await this.mjolnir.client.getRoomStateEvent(roomId, "m.room.power_levels", "");
|
||||
if (!powerLevels) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Missing power levels state event");
|
||||
}
|
||||
|
||||
function plDefault(val: number | undefined | null, def: number): number {
|
||||
if (!val && val !== 0) return def;
|
||||
return val;
|
||||
}
|
||||
|
||||
const users = powerLevels['users'] || {};
|
||||
const events = powerLevels['events'] || {};
|
||||
const usersDefault = plDefault(powerLevels['users_default'], 0);
|
||||
const stateDefault = plDefault(powerLevels['state_default'], 50);
|
||||
const ban = plDefault(powerLevels['ban'], 50);
|
||||
const kick = plDefault(powerLevels['kick'], 50);
|
||||
const redact = plDefault(powerLevels['redact'], 50);
|
||||
|
||||
const userLevel = plDefault(users[ownUserId], usersDefault);
|
||||
const aclLevel = plDefault(events["m.room.server_acl"], stateDefault);
|
||||
|
||||
// Wants: ban, kick, redact, m.room.server_acl
|
||||
|
||||
if (userLevel < ban) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for bans: ${userLevel} < ${ban}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
}
|
||||
if (userLevel < kick) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for kicks: ${userLevel} < ${kick}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
}
|
||||
if (userLevel < redact) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for redactions: ${userLevel} < ${redact}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
}
|
||||
if (userLevel < aclLevel) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for server ACLs: ${userLevel} < ${aclLevel}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
}
|
||||
|
||||
// Wants: Additional permissions
|
||||
|
||||
for (const additionalPermission of additionalPermissions) {
|
||||
const permLevel = plDefault(events[additionalPermission], stateDefault);
|
||||
|
||||
if (userLevel < permLevel) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for "${additionalPermission}" state events: ${userLevel} < ${permLevel}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise OK
|
||||
} catch (e) {
|
||||
LogService.error("Mjolnir", extractRequestError(e));
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: e.message || (e.body ? e.body.error : '<no message>'),
|
||||
errorKind: ERROR_KIND_FATAL,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// FIXME: Hook up
|
||||
private async handleReport({ roomId, reporterId, event, reason }: { roomId: string, reporterId: string, event: any, reason?: string }) {
|
||||
for (const protection of this.enabledProtections) {
|
||||
await protection.handleReport(this.mjolnir, roomId, reporterId, event, reason);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,6 @@ import { LogLevel, MatrixClient } from "matrix-bot-sdk"
|
||||
import { ERROR_KIND_FATAL } from "../ErrorCache";
|
||||
import { RoomUpdateError } from "../models/RoomUpdateError";
|
||||
import { redactUserMessagesIn } from "../utils";
|
||||
import { Mjolnir } from "../Mjolnir";
|
||||
import ManagementRoomOutput from "../ManagementRoom";
|
||||
|
||||
export interface QueuedRedaction {
|
||||
|
@ -178,7 +178,7 @@ export class ThrottlingQueue {
|
||||
try {
|
||||
await task();
|
||||
} catch (ex) {
|
||||
await this.mjolnir.logMessage(
|
||||
await this.mjolnir.managementRoom.logMessage(
|
||||
LogLevel.WARN,
|
||||
'Error while executing task',
|
||||
extractRequestError(ex)
|
||||
|
@ -45,10 +45,10 @@ export class UnlistedUserRedactionQueue {
|
||||
if (!mjolnir.config.noop) {
|
||||
await mjolnir.client.redactEvent(roomId, event['event_id']);
|
||||
} else {
|
||||
await mjolnir.logMessage(LogLevel.WARN, "AutomaticRedactionQueue", `Tried to redact ${permalink} but Mjolnir is running in no-op mode`);
|
||||
await mjolnir.managementRoom.logMessage(LogLevel.WARN, "AutomaticRedactionQueue", `Tried to redact ${permalink} but Mjolnir is running in no-op mode`);
|
||||
}
|
||||
} catch (e) {
|
||||
mjolnir.logMessage(LogLevel.WARN, "AutomaticRedactionQueue", `Unable to redact message: ${permalink}`);
|
||||
mjolnir.managementRoom.logMessage(LogLevel.WARN, "AutomaticRedactionQueue", `Unable to redact message: ${permalink}`);
|
||||
LogService.warn("AutomaticRedactionQueue", extractRequestError(e));
|
||||
}
|
||||
}
|
||||
|
@ -75,13 +75,13 @@ export class ReportPoller {
|
||||
}
|
||||
);
|
||||
} catch (ex) {
|
||||
await this.mjolnir.logMessage(LogLevel.ERROR, "getAbuseReports", `failed to poll events: ${ex}`);
|
||||
await this.mjolnir.managementRoom.logMessage(LogLevel.ERROR, "getAbuseReports", `failed to poll events: ${ex}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = response_!;
|
||||
for (let report of response.event_reports) {
|
||||
if (!(report.room_id in this.mjolnir.protectedRooms)) {
|
||||
if (!this.mjolnir.protectedRoomsTracker.isProtectedRoom(report.room_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -92,7 +92,7 @@ export class ReportPoller {
|
||||
`/_synapse/admin/v1/rooms/${report.room_id}/context/${report.event_id}?limit=1`
|
||||
)).event;
|
||||
} catch (ex) {
|
||||
this.mjolnir.logMessage(LogLevel.ERROR, "getAbuseReports", `failed to get context: ${ex}`);
|
||||
this.mjolnir.managementRoom.logMessage(LogLevel.ERROR, "getAbuseReports", `failed to get context: ${ex}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -114,7 +114,7 @@ export class ReportPoller {
|
||||
try {
|
||||
await this.mjolnir.client.setAccountData(REPORT_POLL_EVENT_TYPE, { from: response.next_token });
|
||||
} catch (ex) {
|
||||
await this.mjolnir.logMessage(LogLevel.ERROR, "getAbuseReports", `failed to update progress: ${ex}`);
|
||||
await this.mjolnir.managementRoom.logMessage(LogLevel.ERROR, "getAbuseReports", `failed to update progress: ${ex}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -125,7 +125,7 @@ export class ReportPoller {
|
||||
try {
|
||||
await this.getAbuseReports()
|
||||
} catch (ex) {
|
||||
await this.mjolnir.logMessage(LogLevel.ERROR, "tryGetAbuseReports", `failed to get abuse reports: ${ex}`);
|
||||
await this.mjolnir.managementRoom.logMessage(LogLevel.ERROR, "tryGetAbuseReports", `failed to get abuse reports: ${ex}`);
|
||||
}
|
||||
|
||||
this.schedulePoll();
|
||||
|
@ -15,19 +15,13 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
extractRequestError,
|
||||
LogLevel,
|
||||
LogService,
|
||||
MatrixClient,
|
||||
MatrixGlob,
|
||||
MessageType,
|
||||
Permalinks,
|
||||
TextualMessageEventContent,
|
||||
UserID,
|
||||
getRequestFn,
|
||||
setRequestFn,
|
||||
} from "matrix-bot-sdk";
|
||||
import { Mjolnir } from "./Mjolnir";
|
||||
import { ClientRequest, IncomingMessage } from "http";
|
||||
import { default as parseDuration } from "parse-duration";
|
||||
import ManagementRoomOutput from "./ManagementRoom";
|
||||
|
@ -24,8 +24,8 @@ describe("Test: Reporting abuse", async () => {
|
||||
this.timeout(60000);
|
||||
|
||||
// Listen for any notices that show up.
|
||||
let notices = [];
|
||||
matrixClient().on("room.event", (roomId, event) => {
|
||||
let notices: any[] = [];
|
||||
matrixClient()!.on("room.event", (roomId, event) => {
|
||||
if (roomId = this.mjolnir.managementRoomId) {
|
||||
notices.push(event);
|
||||
}
|
||||
|
@ -242,12 +242,12 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun
|
||||
const room = await moderator.createRoom({ invite: [mjolnirId] });
|
||||
await mjolnir.client.joinRoom(room);
|
||||
await moderator.setUserPowerLevel(mjolnirId, room, 100);
|
||||
await mjolnir.addProtectedRoom(room);
|
||||
await mjolnir.protectedRoomsTracker.addProtectedRoom(room);
|
||||
protectedRooms.push(room);
|
||||
}
|
||||
|
||||
// If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point.
|
||||
await mjolnir.syncLists();
|
||||
await mjolnir.protectedRoomsTracker.syncLists(mjolnir.config.verboseLogging);
|
||||
await Promise.all(protectedRooms.map(async room => {
|
||||
// We're going to need timeline pagination I'm afraid.
|
||||
const roomAcl = await mjolnir.client.getRoomStateEvent(room, "m.room.server_acl", "");
|
||||
@ -269,7 +269,7 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun
|
||||
}
|
||||
// We do this because it should force us to wait until all the ACL events have been applied.
|
||||
// Even if that does mean the last few events will not go through batching...
|
||||
await mjolnir.syncLists();
|
||||
await mjolnir.protectedRoomsTracker.syncLists(mjolnir.config.verboseLogging);
|
||||
|
||||
// At this point we check that the state within Mjolnir is internally consistent, this is just because debugging the following
|
||||
// is a pita.
|
||||
@ -300,7 +300,7 @@ describe('Test: unbaning entities via the PolicyList.', function() {
|
||||
it('Will remove rules that have legacy types', async function() {
|
||||
const mjolnir: Mjolnir = this.mjolnir!
|
||||
const serverName: string = new UserID(await mjolnir.client.getUserId()).domain
|
||||
const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
|
||||
const moderator: MatrixClient = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
|
||||
this.moderator = moderator;
|
||||
await moderator.joinRoom(mjolnir.managementRoomId);
|
||||
const mjolnirId = await mjolnir.client.getUserId();
|
||||
@ -309,10 +309,10 @@ describe('Test: unbaning entities via the PolicyList.', function() {
|
||||
const protectedRoom = await moderator.createRoom({ invite: [mjolnirId] });
|
||||
await mjolnir.client.joinRoom(protectedRoom);
|
||||
await moderator.setUserPowerLevel(mjolnirId, protectedRoom, 100);
|
||||
await mjolnir.addProtectedRoom(protectedRoom);
|
||||
await mjolnir.protectedRoomsTracker.addProtectedRoom(protectedRoom);
|
||||
|
||||
// If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point.
|
||||
await mjolnir.syncLists();
|
||||
await mjolnir.protectedRoomsTracker.syncLists(mjolnir.config.verboseLogging);
|
||||
// If this is not present, then it means the room isn't being protected, which is really bad.
|
||||
const roomAcl = await mjolnir.client.getRoomStateEvent(protectedRoom, "m.room.server_acl", "");
|
||||
assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.');
|
||||
@ -358,7 +358,7 @@ describe('Test: unbaning entities via the PolicyList.', function() {
|
||||
}
|
||||
|
||||
// Wait for mjolnir to sync protected rooms to update ACL.
|
||||
await mjolnir.syncLists();
|
||||
await mjolnir.protectedRoomsTracker.syncLists(mjolnir.config.verboseLogging);
|
||||
// Confirm that the server is unbanned.
|
||||
await banList.updateList();
|
||||
assert.equal(banList.allRules.length, 0);
|
||||
@ -382,12 +382,12 @@ describe('Test: should apply bans to the most recently active rooms first', func
|
||||
const room = await moderator.createRoom({ invite: [mjolnirId] });
|
||||
await mjolnir.client.joinRoom(room);
|
||||
await moderator.setUserPowerLevel(mjolnirId, room, 100);
|
||||
await mjolnir.addProtectedRoom(room);
|
||||
await mjolnir.protectedRoomsTracker.addProtectedRoom(room);
|
||||
protectedRooms.push(room);
|
||||
}
|
||||
|
||||
// If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point.
|
||||
await mjolnir.syncLists();
|
||||
await mjolnir.protectedRoomsTracker.syncLists(mjolnir.config.verboseLogging);
|
||||
await Promise.all(protectedRooms.map(async room => {
|
||||
const roomAcl = await mjolnir.client.getRoomStateEvent(room, "m.room.server_acl", "").catch(e => e.statusCode === 404 ? { deny: [] } : Promise.reject(e));
|
||||
assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.');
|
||||
@ -398,7 +398,7 @@ describe('Test: should apply bans to the most recently active rooms first', func
|
||||
await mjolnir.client.joinRoom(banListId);
|
||||
await mjolnir.watchList(Permalinks.forRoom(banListId));
|
||||
|
||||
await mjolnir.syncLists();
|
||||
await mjolnir.protectedRoomsTracker.syncLists(mjolnir.config.verboseLogging);
|
||||
|
||||
// shuffle protected rooms https://stackoverflow.com/a/12646864, we do this so we can create activity "randomly" in them.
|
||||
for (let i = protectedRooms.length - 1; i > 0; i--) {
|
||||
@ -413,7 +413,7 @@ describe('Test: should apply bans to the most recently active rooms first', func
|
||||
|
||||
// check the rooms are in the expected order
|
||||
for (let i = 0; i < protectedRooms.length; i++) {
|
||||
assert.equal(mjolnir.protectedRoomsByActivity()[i], protectedRooms[i]);
|
||||
assert.equal(mjolnir.protectedRoomsTracker.protectedRoomsByActivity()[i], protectedRooms[i]);
|
||||
}
|
||||
|
||||
const badServer = `evil.com`;
|
||||
@ -422,10 +422,10 @@ describe('Test: should apply bans to the most recently active rooms first', func
|
||||
await createPolicyRule(moderator, banListId, RULE_SERVER, badServer, `Rule ${badServer}`);
|
||||
|
||||
// Wait until all the ACL events have been applied.
|
||||
await mjolnir.syncLists();
|
||||
await mjolnir.protectedRoomsTracker.syncLists(mjolnir.config.verboseLogging);
|
||||
|
||||
for (let i = 0; i < protectedRooms.length; i++) {
|
||||
assert.equal(mjolnir.protectedRoomsByActivity()[i], protectedRooms.at(-i - 1));
|
||||
assert.equal(mjolnir.protectedRoomsTracker.protectedRoomsByActivity()[i], protectedRooms.at(-i - 1));
|
||||
}
|
||||
|
||||
// Check that the most recently active rooms got the ACL update first.
|
||||
|
Loading…
Reference in New Issue
Block a user