mirror of
https://github.com/matrix-org/mjolnir.git
synced 2024-10-01 01:36:06 -04:00
Merge branch 'main' into gnuxie/replace-groups
This commit is contained in:
commit
2042d9ba4c
130
src/Mjolnir.ts
130
src/Mjolnir.ts
@ -27,19 +27,17 @@ import {
|
|||||||
TextualMessageEventContent
|
TextualMessageEventContent
|
||||||
} from "matrix-bot-sdk";
|
} from "matrix-bot-sdk";
|
||||||
|
|
||||||
import BanList, { ALL_RULE_TYPES as ALL_BAN_LIST_RULE_TYPES, ListRuleChange, RULE_ROOM, RULE_SERVER, RULE_USER } from "./models/BanList";
|
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 { applyServerAcls } from "./actions/ApplyAcl";
|
||||||
import { RoomUpdateError } from "./models/RoomUpdateError";
|
import { RoomUpdateError } from "./models/RoomUpdateError";
|
||||||
import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler";
|
import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler";
|
||||||
import { applyUserBans } from "./actions/ApplyBan";
|
import { applyUserBans } from "./actions/ApplyBan";
|
||||||
import config from "./config";
|
|
||||||
import ErrorCache, { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "./ErrorCache";
|
import ErrorCache, { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "./ErrorCache";
|
||||||
import { Protection } from "./protections/IProtection";
|
import { Protection } from "./protections/IProtection";
|
||||||
import { PROTECTIONS } from "./protections/protections";
|
import { PROTECTIONS } from "./protections/protections";
|
||||||
import { ConsequenceType, Consequence } from "./protections/consequence";
|
import { ConsequenceType, Consequence } from "./protections/consequence";
|
||||||
import { ProtectionSettingValidationError } from "./protections/ProtectionSettings";
|
import { ProtectionSettingValidationError } from "./protections/ProtectionSettings";
|
||||||
import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue";
|
import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue";
|
||||||
import { Healthz } from "./health/healthz";
|
|
||||||
import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue";
|
import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue";
|
||||||
import { htmlEscape } from "./utils";
|
import { htmlEscape } from "./utils";
|
||||||
import { ReportManager } from "./report/ReportManager";
|
import { ReportManager } from "./report/ReportManager";
|
||||||
@ -50,6 +48,8 @@ import RuleServer from "./models/RuleServer";
|
|||||||
import { RoomMemberManager } from "./RoomMembers";
|
import { RoomMemberManager } from "./RoomMembers";
|
||||||
import { ProtectedRoomActivityTracker } from "./queues/ProtectedRoomActivityTracker";
|
import { ProtectedRoomActivityTracker } from "./queues/ProtectedRoomActivityTracker";
|
||||||
import { ThrottlingQueue } from "./queues/ThrottlingQueue";
|
import { ThrottlingQueue } from "./queues/ThrottlingQueue";
|
||||||
|
import { IConfig } from "./config";
|
||||||
|
import PolicyList, { ListRuleChange } from "./models/PolicyList";
|
||||||
|
|
||||||
const levelToFn = {
|
const levelToFn = {
|
||||||
[LogLevel.DEBUG.toString()]: LogService.debug,
|
[LogLevel.DEBUG.toString()]: LogService.debug,
|
||||||
@ -103,6 +103,15 @@ export class Mjolnir {
|
|||||||
private webapis: WebAPIs;
|
private webapis: WebAPIs;
|
||||||
private protectedRoomActivityTracker: ProtectedRoomActivityTracker;
|
private protectedRoomActivityTracker: ProtectedRoomActivityTracker;
|
||||||
public taskQueue: ThrottlingQueue;
|
public taskQueue: ThrottlingQueue;
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* could race & give the room an inconsistent state. An example is if we add multiple m.policy.rule.server rules,
|
||||||
|
* which would cause several requests to a room to send a new m.room.server_acl event.
|
||||||
|
* These requests could finish in any order, which has left rooms with an inconsistent server_acl event
|
||||||
|
* until Mjolnir synchronises the room with its policy lists again, which can be in the region of hours.
|
||||||
|
*/
|
||||||
|
public aclChain: Promise<void> = Promise.resolve();
|
||||||
/*
|
/*
|
||||||
* Config-enabled polling of reports in Synapse, so Mjolnir can react to reports
|
* Config-enabled polling of reports in Synapse, so Mjolnir can react to reports
|
||||||
*/
|
*/
|
||||||
@ -161,8 +170,8 @@ export class Mjolnir {
|
|||||||
* @param {MatrixClient} client The client for Mjolnir to use.
|
* @param {MatrixClient} client The client for Mjolnir to use.
|
||||||
* @returns A new Mjolnir instance that can be started without further setup.
|
* @returns A new Mjolnir instance that can be started without further setup.
|
||||||
*/
|
*/
|
||||||
static async setupMjolnirFromConfig(client: MatrixClient): Promise<Mjolnir> {
|
static async setupMjolnirFromConfig(client: MatrixClient, config: IConfig): Promise<Mjolnir> {
|
||||||
const banLists: BanList[] = [];
|
const policyLists: PolicyList[] = [];
|
||||||
const protectedRooms: { [roomId: string]: string } = {};
|
const protectedRooms: { [roomId: string]: string } = {};
|
||||||
const joinedRooms = await client.getJoinedRooms();
|
const joinedRooms = await client.getJoinedRooms();
|
||||||
// Ensure we're also joined to the rooms we're protecting
|
// Ensure we're also joined to the rooms we're protecting
|
||||||
@ -187,7 +196,7 @@ export class Mjolnir {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ruleServer = config.web.ruleServer ? new RuleServer() : null;
|
const ruleServer = config.web.ruleServer ? new RuleServer() : null;
|
||||||
const mjolnir = new Mjolnir(client, managementRoomId, protectedRooms, banLists, ruleServer);
|
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.logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status.");
|
||||||
Mjolnir.addJoinOnInviteListener(mjolnir, client, config);
|
Mjolnir.addJoinOnInviteListener(mjolnir, client, config);
|
||||||
return mjolnir;
|
return mjolnir;
|
||||||
@ -196,18 +205,19 @@ export class Mjolnir {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly client: MatrixClient,
|
public readonly client: MatrixClient,
|
||||||
public readonly managementRoomId: string,
|
public readonly managementRoomId: string,
|
||||||
|
public readonly config: IConfig,
|
||||||
/*
|
/*
|
||||||
* All the rooms that Mjolnir is protecting and their permalinks.
|
* 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).
|
* 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 },
|
public readonly protectedRooms: { [roomId: string]: string },
|
||||||
private banLists: BanList[],
|
private policyLists: PolicyList[],
|
||||||
// Combines the rules from ban lists so they can be served to a homeserver module or another consumer.
|
// Combines the rules from ban lists so they can be served to a homeserver module or another consumer.
|
||||||
public readonly ruleServer: RuleServer | null,
|
public readonly ruleServer: RuleServer | null,
|
||||||
) {
|
) {
|
||||||
this.explicitlyProtectedRoomIds = Object.keys(this.protectedRooms);
|
this.explicitlyProtectedRoomIds = Object.keys(this.protectedRooms);
|
||||||
|
|
||||||
for (const reason of config.automaticallyRedactForReasons) {
|
for (const reason of this.config.automaticallyRedactForReasons) {
|
||||||
this.automaticRedactionReasons.push(new MatrixGlob(reason.toLowerCase()));
|
this.automaticRedactionReasons.push(new MatrixGlob(reason.toLowerCase()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,7 +285,7 @@ export class Mjolnir {
|
|||||||
console.log("Creating Web APIs");
|
console.log("Creating Web APIs");
|
||||||
const reportManager = new ReportManager(this);
|
const reportManager = new ReportManager(this);
|
||||||
reportManager.on("report.new", this.handleReport.bind(this));
|
reportManager.on("report.new", this.handleReport.bind(this));
|
||||||
this.webapis = new WebAPIs(reportManager, this.ruleServer);
|
this.webapis = new WebAPIs(reportManager, this.config, this.ruleServer);
|
||||||
if (config.pollReports) {
|
if (config.pollReports) {
|
||||||
this.reportPoller = new ReportPoller(this, reportManager);
|
this.reportPoller = new ReportPoller(this, reportManager);
|
||||||
}
|
}
|
||||||
@ -284,8 +294,8 @@ export class Mjolnir {
|
|||||||
this.taskQueue = new ThrottlingQueue(this, config.backgroundDelayMS);
|
this.taskQueue = new ThrottlingQueue(this, config.backgroundDelayMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get lists(): BanList[] {
|
public get lists(): PolicyList[] {
|
||||||
return this.banLists;
|
return this.policyLists;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get state(): string {
|
public get state(): string {
|
||||||
@ -352,23 +362,22 @@ export class Mjolnir {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
LogService.warn("Mjolnir", extractRequestError(e));
|
LogService.warn("Mjolnir", extractRequestError(e));
|
||||||
}
|
}
|
||||||
await this.buildWatchedBanLists();
|
await this.buildWatchedPolicyLists();
|
||||||
this.applyUnprotectedRooms();
|
this.applyUnprotectedRooms();
|
||||||
|
|
||||||
if (config.verifyPermissionsOnStartup) {
|
if (this.config.verifyPermissionsOnStartup) {
|
||||||
await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Checking permissions...");
|
await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Checking permissions...");
|
||||||
await this.verifyPermissions(config.verboseLogging);
|
await this.verifyPermissions(this.config.verboseLogging);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentState = STATE_SYNCING;
|
this.currentState = STATE_SYNCING;
|
||||||
if (config.syncOnStartup) {
|
if (this.config.syncOnStartup) {
|
||||||
await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Syncing lists...");
|
await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Syncing lists...");
|
||||||
await this.syncLists(config.verboseLogging);
|
await this.syncLists(this.config.verboseLogging);
|
||||||
await this.registerProtections();
|
await this.registerProtections();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentState = STATE_RUNNING;
|
this.currentState = STATE_RUNNING;
|
||||||
Healthz.isHealthy = true;
|
|
||||||
await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Startup complete. Now monitoring rooms.");
|
await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Startup complete. Now monitoring rooms.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
try {
|
try {
|
||||||
@ -398,14 +407,13 @@ export class Mjolnir {
|
|||||||
if (!additionalRoomIds) additionalRoomIds = [];
|
if (!additionalRoomIds) additionalRoomIds = [];
|
||||||
if (!Array.isArray(additionalRoomIds)) additionalRoomIds = [additionalRoomIds];
|
if (!Array.isArray(additionalRoomIds)) additionalRoomIds = [additionalRoomIds];
|
||||||
|
|
||||||
if (config.RUNTIME.client && (config.verboseLogging || LogLevel.INFO.includes(level))) {
|
if (this.config.verboseLogging || LogLevel.INFO.includes(level)) {
|
||||||
let clientMessage = message;
|
let clientMessage = message;
|
||||||
if (level === LogLevel.WARN) clientMessage = `⚠ | ${message}`;
|
if (level === LogLevel.WARN) clientMessage = `⚠ | ${message}`;
|
||||||
if (level === LogLevel.ERROR) clientMessage = `‼ | ${message}`;
|
if (level === LogLevel.ERROR) clientMessage = `‼ | ${message}`;
|
||||||
|
|
||||||
const client = config.RUNTIME.client;
|
const client = this.client;
|
||||||
const managementRoomId = await client.resolveRoom(config.managementRoom);
|
const roomIds = [this.managementRoomId, ...additionalRoomIds];
|
||||||
const roomIds = [managementRoomId, ...additionalRoomIds];
|
|
||||||
|
|
||||||
let evContent: TextualMessageEventContent = {
|
let evContent: TextualMessageEventContent = {
|
||||||
body: message,
|
body: message,
|
||||||
@ -417,7 +425,7 @@ export class Mjolnir {
|
|||||||
evContent = await replaceRoomIdsWithPills(this, clientMessage, new Set(roomIds), "m.notice");
|
evContent = await replaceRoomIdsWithPills(this, clientMessage, new Set(roomIds), "m.notice");
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.sendMessage(managementRoomId, evContent);
|
await client.sendMessage(this.managementRoomId, evContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
levelToFn[level.toString()](module, message);
|
levelToFn[level.toString()](module, message);
|
||||||
@ -442,7 +450,7 @@ export class Mjolnir {
|
|||||||
const rooms = (additionalProtectedRooms?.rooms ?? []);
|
const rooms = (additionalProtectedRooms?.rooms ?? []);
|
||||||
rooms.push(roomId);
|
rooms.push(roomId);
|
||||||
await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, { rooms: rooms });
|
await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, { rooms: rooms });
|
||||||
await this.syncLists(config.verboseLogging);
|
await this.syncLists(this.config.verboseLogging);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async removeProtectedRoom(roomId: string) {
|
public async removeProtectedRoom(roomId: string) {
|
||||||
@ -464,7 +472,7 @@ export class Mjolnir {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async resyncJoinedRooms(withSync = true) {
|
private async resyncJoinedRooms(withSync = true) {
|
||||||
if (!config.protectAllJoinedRooms) return;
|
if (!this.config.protectAllJoinedRooms) return;
|
||||||
|
|
||||||
const joinedRoomIds = (await this.client.getJoinedRooms()).filter(r => r !== this.managementRoomId);
|
const joinedRoomIds = (await this.client.getJoinedRooms()).filter(r => r !== this.managementRoomId);
|
||||||
const oldRoomIdsSet = new Set(this.protectedJoinedRoomIds);
|
const oldRoomIdsSet = new Set(this.protectedJoinedRoomIds);
|
||||||
@ -490,7 +498,7 @@ export class Mjolnir {
|
|||||||
this.applyUnprotectedRooms();
|
this.applyUnprotectedRooms();
|
||||||
|
|
||||||
if (withSync) {
|
if (withSync) {
|
||||||
await this.syncLists(config.verboseLogging);
|
await this.syncLists(this.config.verboseLogging);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -653,16 +661,16 @@ export class Mjolnir {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for constructing `BanList`s and making sure they have the right listeners set up.
|
* Helper for constructing `PolicyList`s and making sure they have the right listeners set up.
|
||||||
* @param roomId The room id for the `BanList`.
|
* @param roomId The room id for the `PolicyList`.
|
||||||
* @param roomRef A reference (matrix.to URL) for the `BanList`.
|
* @param roomRef A reference (matrix.to URL) for the `PolicyList`.
|
||||||
*/
|
*/
|
||||||
private async addBanList(roomId: string, roomRef: string): Promise<BanList> {
|
private async addPolicyList(roomId: string, roomRef: string): Promise<PolicyList> {
|
||||||
const list = new BanList(roomId, roomRef, this.client);
|
const list = new PolicyList(roomId, roomRef, this.client);
|
||||||
this.ruleServer?.watch(list);
|
this.ruleServer?.watch(list);
|
||||||
list.on('BanList.batch', this.syncWithBanList.bind(this));
|
list.on('PolicyList.batch', this.syncWithPolicyList.bind(this));
|
||||||
await list.updateList();
|
await list.updateList();
|
||||||
this.banLists.push(list);
|
this.policyLists.push(list);
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -676,7 +684,7 @@ export class Mjolnir {
|
|||||||
return this.protections.get(protectionName) ?? null;
|
return this.protections.get(protectionName) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async watchList(roomRef: string): Promise<BanList | null> {
|
public async watchList(roomRef: string): Promise<PolicyList | null> {
|
||||||
const joinedRooms = await this.client.getJoinedRooms();
|
const joinedRooms = await this.client.getJoinedRooms();
|
||||||
const permalink = Permalinks.parseUrl(roomRef);
|
const permalink = Permalinks.parseUrl(roomRef);
|
||||||
if (!permalink.roomIdOrAlias) return null;
|
if (!permalink.roomIdOrAlias) return null;
|
||||||
@ -686,38 +694,38 @@ export class Mjolnir {
|
|||||||
await this.client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
|
await this.client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.banLists.find(b => b.roomId === roomId)) return null;
|
if (this.policyLists.find(b => b.roomId === roomId)) return null;
|
||||||
|
|
||||||
const list = await this.addBanList(roomId, roomRef);
|
const list = await this.addPolicyList(roomId, roomRef);
|
||||||
|
|
||||||
await this.client.setAccountData(WATCHED_LISTS_EVENT_TYPE, {
|
await this.client.setAccountData(WATCHED_LISTS_EVENT_TYPE, {
|
||||||
references: this.banLists.map(b => b.roomRef),
|
references: this.policyLists.map(b => b.roomRef),
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.warnAboutUnprotectedBanListRoom(roomId);
|
await this.warnAboutUnprotectedPolicyListRoom(roomId);
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async unwatchList(roomRef: string): Promise<BanList | null> {
|
public async unwatchList(roomRef: string): Promise<PolicyList | null> {
|
||||||
const permalink = Permalinks.parseUrl(roomRef);
|
const permalink = Permalinks.parseUrl(roomRef);
|
||||||
if (!permalink.roomIdOrAlias) return null;
|
if (!permalink.roomIdOrAlias) return null;
|
||||||
|
|
||||||
const roomId = await this.client.resolveRoom(permalink.roomIdOrAlias);
|
const roomId = await this.client.resolveRoom(permalink.roomIdOrAlias);
|
||||||
const list = this.banLists.find(b => b.roomId === roomId) || null;
|
const list = this.policyLists.find(b => b.roomId === roomId) || null;
|
||||||
if (list) {
|
if (list) {
|
||||||
this.banLists.splice(this.banLists.indexOf(list), 1);
|
this.policyLists.splice(this.policyLists.indexOf(list), 1);
|
||||||
this.ruleServer?.unwatch(list);
|
this.ruleServer?.unwatch(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.client.setAccountData(WATCHED_LISTS_EVENT_TYPE, {
|
await this.client.setAccountData(WATCHED_LISTS_EVENT_TYPE, {
|
||||||
references: this.banLists.map(b => b.roomRef),
|
references: this.policyLists.map(b => b.roomRef),
|
||||||
});
|
});
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async warnAboutUnprotectedBanListRoom(roomId: string) {
|
public async warnAboutUnprotectedPolicyListRoom(roomId: string) {
|
||||||
if (!config.protectAllJoinedRooms) return; // doesn't matter
|
if (!this.config.protectAllJoinedRooms) return; // doesn't matter
|
||||||
if (this.explicitlyProtectedRoomIds.includes(roomId)) return; // explicitly protected
|
if (this.explicitlyProtectedRoomIds.includes(roomId)) return; // explicitly protected
|
||||||
|
|
||||||
const createEvent = new CreateEvent(await this.client.getRoomStateEvent(roomId, "m.room.create", ""));
|
const createEvent = new CreateEvent(await this.client.getRoomStateEvent(roomId, "m.room.create", ""));
|
||||||
@ -744,8 +752,8 @@ export class Mjolnir {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildWatchedBanLists() {
|
private async buildWatchedPolicyLists() {
|
||||||
this.banLists = [];
|
this.policyLists = [];
|
||||||
const joinedRooms = await this.client.getJoinedRooms();
|
const joinedRooms = await this.client.getJoinedRooms();
|
||||||
|
|
||||||
let watchedListsEvent: { references?: string[] } | null = null;
|
let watchedListsEvent: { references?: string[] } | null = null;
|
||||||
@ -764,8 +772,8 @@ export class Mjolnir {
|
|||||||
await this.client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
|
await this.client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.warnAboutUnprotectedBanListRoom(roomId);
|
await this.warnAboutUnprotectedPolicyListRoom(roomId);
|
||||||
await this.addBanList(roomId, roomRef);
|
await this.addPolicyList(roomId, roomRef);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -891,15 +899,15 @@ export class Mjolnir {
|
|||||||
* @param verbose Whether to report any errors to the management room.
|
* @param verbose Whether to report any errors to the management room.
|
||||||
*/
|
*/
|
||||||
public async syncLists(verbose = true) {
|
public async syncLists(verbose = true) {
|
||||||
for (const list of this.banLists) {
|
for (const list of this.policyLists) {
|
||||||
const changes = await list.updateList();
|
const changes = await list.updateList();
|
||||||
await this.printBanlistChanges(changes, list, true);
|
await this.printBanlistChanges(changes, list, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hadErrors = false;
|
let hadErrors = false;
|
||||||
const [aclErrors, banErrors] = await Promise.all([
|
const [aclErrors, banErrors] = await Promise.all([
|
||||||
applyServerAcls(this.banLists, this.protectedRoomsByActivity(), this),
|
applyServerAcls(this.policyLists, this.protectedRoomsByActivity(), this),
|
||||||
applyUserBans(this.banLists, this.protectedRoomsByActivity(), this)
|
applyUserBans(this.policyLists, this.protectedRoomsByActivity(), this)
|
||||||
]);
|
]);
|
||||||
const redactionErrors = await this.processRedactionQueue();
|
const redactionErrors = await this.processRedactionQueue();
|
||||||
hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:");
|
hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:");
|
||||||
@ -921,16 +929,16 @@ export class Mjolnir {
|
|||||||
/**
|
/**
|
||||||
* Pulls any changes to the rules that are in a policy room and updates all protected rooms
|
* Pulls any changes to the rules that are in a policy room and updates all protected rooms
|
||||||
* with those changes. Does not fail if there are errors updating the room, these are reported to the management room.
|
* with those changes. Does not fail if there are errors updating the room, these are reported to the management room.
|
||||||
* @param banList The `BanList` which we will check for changes and apply them to all protected rooms.
|
* @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.
|
* @returns When all of the protected rooms have been updated.
|
||||||
*/
|
*/
|
||||||
private async syncWithBanList(banList: BanList): Promise<void> {
|
private async syncWithPolicyList(policyList: PolicyList): Promise<void> {
|
||||||
const changes = await banList.updateList();
|
const changes = await policyList.updateList();
|
||||||
|
|
||||||
let hadErrors = false;
|
let hadErrors = false;
|
||||||
const [aclErrors, banErrors] = await Promise.all([
|
const [aclErrors, banErrors] = await Promise.all([
|
||||||
applyServerAcls(this.banLists, this.protectedRoomsByActivity(), this),
|
applyServerAcls(this.policyLists, this.protectedRoomsByActivity(), this),
|
||||||
applyUserBans(this.banLists, this.protectedRoomsByActivity(), this)
|
applyUserBans(this.policyLists, this.protectedRoomsByActivity(), this)
|
||||||
]);
|
]);
|
||||||
const redactionErrors = await this.processRedactionQueue();
|
const redactionErrors = await this.processRedactionQueue();
|
||||||
hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:");
|
hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:");
|
||||||
@ -948,7 +956,7 @@ export class Mjolnir {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
// This can fail if the change is very large and it is much less important than applying bans, so do it last.
|
// This can fail if the change is very large and it is much less important than applying bans, so do it last.
|
||||||
await this.printBanlistChanges(changes, banList, true);
|
await this.printBanlistChanges(changes, policyList, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleConsequence(protection: Protection, roomId: string, eventId: string, sender: string, consequence: Consequence) {
|
private async handleConsequence(protection: Protection, roomId: string, eventId: string, sender: string, consequence: Consequence) {
|
||||||
@ -998,10 +1006,10 @@ export class Mjolnir {
|
|||||||
|
|
||||||
// Check for updated ban lists before checking protected rooms - the ban lists might be protected
|
// Check for updated ban lists before checking protected rooms - the ban lists might be protected
|
||||||
// themselves.
|
// themselves.
|
||||||
const banList = this.banLists.find(list => list.roomId === roomId);
|
const policyList = this.policyLists.find(list => list.roomId === roomId);
|
||||||
if (banList !== undefined) {
|
if (policyList !== undefined) {
|
||||||
if (ALL_BAN_LIST_RULE_TYPES.includes(event['type']) || event['type'] === 'm.room.redaction') {
|
if (ALL_BAN_LIST_RULE_TYPES.includes(event['type']) || event['type'] === 'm.room.redaction') {
|
||||||
banList.updateForEvent(event)
|
policyList.updateForEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1046,7 +1054,7 @@ export class Mjolnir {
|
|||||||
// we cannot eagerly ban users (that is to ban them when they have never been a member)
|
// we cannot eagerly ban users (that is to ban them when they have never been a member)
|
||||||
// as they can be force joined to a room they might not have known existed.
|
// as they can be force joined to a room they might not have known existed.
|
||||||
// Only apply bans and then redactions in the room we are currently looking at.
|
// Only apply bans and then redactions in the room we are currently looking at.
|
||||||
const banErrors = await applyUserBans(this.banLists, [roomId], this);
|
const banErrors = await applyUserBans(this.policyLists, [roomId], this);
|
||||||
const redactionErrors = await this.processRedactionQueue(roomId);
|
const redactionErrors = await this.processRedactionQueue(roomId);
|
||||||
await this.printActionResult(banErrors);
|
await this.printActionResult(banErrors);
|
||||||
await this.printActionResult(redactionErrors);
|
await this.printActionResult(redactionErrors);
|
||||||
@ -1060,7 +1068,7 @@ export class Mjolnir {
|
|||||||
* @param ignoreSelf Whether to exclude changes that have been made by Mjolnir.
|
* @param ignoreSelf Whether to exclude changes that have been made by Mjolnir.
|
||||||
* @returns true if the message was sent, false if it wasn't (because there there were no changes to report).
|
* @returns true if the message was sent, false if it wasn't (because there there were no changes to report).
|
||||||
*/
|
*/
|
||||||
private async printBanlistChanges(changes: ListRuleChange[], list: BanList, ignoreSelf = false): Promise<boolean> {
|
private async printBanlistChanges(changes: ListRuleChange[], list: PolicyList, ignoreSelf = false): Promise<boolean> {
|
||||||
if (ignoreSelf) {
|
if (ignoreSelf) {
|
||||||
const sender = await this.client.getUserId();
|
const sender = await this.client.getUserId();
|
||||||
changes = changes.filter(change => change.sender !== sender);
|
changes = changes.filter(change => change.sender !== sender);
|
||||||
|
@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import BanList from "../models/BanList";
|
import PolicyList from "../models/PolicyList";
|
||||||
import { ServerAcl } from "../models/ServerAcl";
|
import { ServerAcl } from "../models/ServerAcl";
|
||||||
import { RoomUpdateError } from "../models/RoomUpdateError";
|
import { RoomUpdateError } from "../models/RoomUpdateError";
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
import config from "../config";
|
|
||||||
import { LogLevel, UserID } from "matrix-bot-sdk";
|
import { LogLevel, UserID } from "matrix-bot-sdk";
|
||||||
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
|
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
|
||||||
|
|
||||||
@ -26,11 +25,21 @@ import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
|
|||||||
* Applies the server ACLs represented by the ban lists to the provided rooms, returning the
|
* Applies the server ACLs represented by the ban lists to the provided rooms, returning the
|
||||||
* room IDs that could not be updated and their error.
|
* room IDs that could not be updated and their error.
|
||||||
* Does not update the banLists before taking their rules to build the server ACL.
|
* Does not update the banLists before taking their rules to build the server ACL.
|
||||||
* @param {BanList[]} lists The lists to construct ACLs from.
|
* @param {PolicyList[]} lists The lists to construct ACLs from.
|
||||||
* @param {string[]} roomIds The room IDs to apply the ACLs in.
|
* @param {string[]} roomIds The room IDs to apply the ACLs in.
|
||||||
* @param {Mjolnir} mjolnir The Mjolnir client to apply the ACLs with.
|
* @param {Mjolnir} mjolnir The Mjolnir client to apply the ACLs with.
|
||||||
*/
|
*/
|
||||||
export async function applyServerAcls(lists: BanList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
|
export async function applyServerAcls(lists: PolicyList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
|
||||||
|
// we need to provide mutual exclusion so that we do not have requests updating the m.room.server_acl event
|
||||||
|
// finish out of order and therefore leave the room out of sync with the policy lists.
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
mjolnir.aclChain = mjolnir.aclChain
|
||||||
|
.then(() => _applyServerAcls(lists, roomIds, mjolnir))
|
||||||
|
.then(resolve, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _applyServerAcls(lists: PolicyList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
|
||||||
const serverName: string = new UserID(await mjolnir.client.getUserId()).domain;
|
const serverName: string = new UserID(await mjolnir.client.getUserId()).domain;
|
||||||
|
|
||||||
// Construct a server ACL first
|
// Construct a server ACL first
|
||||||
@ -47,7 +56,7 @@ export async function applyServerAcls(lists: BanList[], roomIds: string[], mjoln
|
|||||||
mjolnir.logMessage(LogLevel.WARN, "ApplyAcl", `Mjölnir has detected and removed an ACL that would exclude itself. Please check the ACL lists.`);
|
mjolnir.logMessage(LogLevel.WARN, "ApplyAcl", `Mjölnir has detected and removed an ACL that would exclude itself. Please check the ACL lists.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.verboseLogging) {
|
if (mjolnir.config.verboseLogging) {
|
||||||
// We specifically use sendNotice to avoid having to escape HTML
|
// We specifically use sendNotice to avoid having to escape HTML
|
||||||
await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Constructed server ACL:\n${JSON.stringify(finalAcl, null, 2)}`);
|
await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Constructed server ACL:\n${JSON.stringify(finalAcl, null, 2)}`);
|
||||||
}
|
}
|
||||||
@ -70,7 +79,7 @@ export async function applyServerAcls(lists: BanList[], roomIds: string[], mjoln
|
|||||||
// We specifically use sendNotice to avoid having to escape HTML
|
// We specifically use sendNotice to avoid having to escape HTML
|
||||||
await mjolnir.logMessage(LogLevel.DEBUG, "ApplyAcl", `Applying ACL in ${roomId}`, roomId);
|
await mjolnir.logMessage(LogLevel.DEBUG, "ApplyAcl", `Applying ACL in ${roomId}`, roomId);
|
||||||
|
|
||||||
if (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
await mjolnir.client.sendStateEvent(roomId, "m.room.server_acl", "", finalAcl);
|
await mjolnir.client.sendStateEvent(roomId, "m.room.server_acl", "", finalAcl);
|
||||||
} else {
|
} else {
|
||||||
await mjolnir.logMessage(LogLevel.WARN, "ApplyAcl", `Tried to apply ACL in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
await mjolnir.logMessage(LogLevel.WARN, "ApplyAcl", `Tried to apply ACL in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||||
|
@ -14,21 +14,20 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import BanList from "../models/BanList";
|
import PolicyList from "../models/PolicyList";
|
||||||
import { RoomUpdateError } from "../models/RoomUpdateError";
|
import { RoomUpdateError } from "../models/RoomUpdateError";
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
import config from "../config";
|
|
||||||
import { LogLevel } from "matrix-bot-sdk";
|
import { LogLevel } from "matrix-bot-sdk";
|
||||||
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
|
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the member bans represented by the ban lists to the provided rooms, returning the
|
* Applies the member bans represented by the ban lists to the provided rooms, returning the
|
||||||
* room IDs that could not be updated and their error.
|
* room IDs that could not be updated and their error.
|
||||||
* @param {BanList[]} lists The lists to determine bans from.
|
* @param {PolicyList[]} lists The lists to determine bans from.
|
||||||
* @param {string[]} roomIds The room IDs to apply the bans in.
|
* @param {string[]} roomIds The room IDs to apply the bans in.
|
||||||
* @param {Mjolnir} mjolnir The Mjolnir client to apply the bans with.
|
* @param {Mjolnir} mjolnir The Mjolnir client to apply the bans with.
|
||||||
*/
|
*/
|
||||||
export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
|
export async function applyUserBans(lists: PolicyList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
|
||||||
// We can only ban people who are not already banned, and who match the rules.
|
// We can only ban people who are not already banned, and who match the rules.
|
||||||
const errors: RoomUpdateError[] = [];
|
const errors: RoomUpdateError[] = [];
|
||||||
for (const roomId of roomIds) {
|
for (const roomId of roomIds) {
|
||||||
@ -38,7 +37,7 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir
|
|||||||
|
|
||||||
let members: { userId: string, membership: string }[];
|
let members: { userId: string, membership: string }[];
|
||||||
|
|
||||||
if (config.fasterMembershipChecks) {
|
if (mjolnir.config.fasterMembershipChecks) {
|
||||||
const memberIds = await mjolnir.client.getJoinedRoomMembers(roomId);
|
const memberIds = await mjolnir.client.getJoinedRoomMembers(roomId);
|
||||||
members = memberIds.map(u => {
|
members = memberIds.map(u => {
|
||||||
return { userId: u, membership: "join" };
|
return { userId: u, membership: "join" };
|
||||||
@ -64,7 +63,7 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir
|
|||||||
// We specifically use sendNotice to avoid having to escape HTML
|
// We specifically use sendNotice to avoid having to escape HTML
|
||||||
await mjolnir.logMessage(LogLevel.INFO, "ApplyBan", `Banning ${member.userId} in ${roomId} for: ${userRule.reason}`, roomId);
|
await mjolnir.logMessage(LogLevel.INFO, "ApplyBan", `Banning ${member.userId} in ${roomId} for: ${userRule.reason}`, roomId);
|
||||||
|
|
||||||
if (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
await mjolnir.client.banUser(member.userId, roomId, userRule.reason);
|
await mjolnir.client.banUser(member.userId, roomId, userRule.reason);
|
||||||
if (mjolnir.automaticRedactGlobs.find(g => g.test(userRule.reason.toLowerCase()))) {
|
if (mjolnir.automaticRedactGlobs.find(g => g.test(userRule.reason.toLowerCase()))) {
|
||||||
mjolnir.queueRedactUserMessagesIn(member.userId, roomId);
|
mjolnir.queueRedactUserMessagesIn(member.userId, roomId);
|
||||||
|
@ -28,8 +28,10 @@ import { execRedactCommand } from "./RedactCommand";
|
|||||||
import { execImportCommand } from "./ImportCommand";
|
import { execImportCommand } from "./ImportCommand";
|
||||||
import { execSetDefaultListCommand } from "./SetDefaultBanListCommand";
|
import { execSetDefaultListCommand } from "./SetDefaultBanListCommand";
|
||||||
import { execDeactivateCommand } from "./DeactivateCommand";
|
import { execDeactivateCommand } from "./DeactivateCommand";
|
||||||
import { execDisableProtection, execEnableProtection, execListProtections, execConfigGetProtection,
|
import {
|
||||||
execConfigSetProtection, execConfigAddProtection, execConfigRemoveProtection } from "./ProtectionsCommands";
|
execDisableProtection, execEnableProtection, execListProtections, execConfigGetProtection,
|
||||||
|
execConfigSetProtection, execConfigAddProtection, execConfigRemoveProtection
|
||||||
|
} from "./ProtectionsCommands";
|
||||||
import { execListProtectedRooms } from "./ListProtectedRoomsCommand";
|
import { execListProtectedRooms } from "./ListProtectedRoomsCommand";
|
||||||
import { execAddProtectedRoom, execRemoveProtectedRoom } from "./AddRemoveProtectedRoomsCommand";
|
import { execAddProtectedRoom, execRemoveProtectedRoom } from "./AddRemoveProtectedRoomsCommand";
|
||||||
import { execAddRoomToDirectoryCommand, execRemoveRoomFromDirectoryCommand } from "./AddRemoveRoomFromDirectoryCommand";
|
import { execAddRoomToDirectoryCommand, execRemoveRoomFromDirectoryCommand } from "./AddRemoveRoomFromDirectoryCommand";
|
||||||
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
import { SHORTCODE_EVENT_TYPE } from "../models/BanList";
|
import { SHORTCODE_EVENT_TYPE } from "../models/PolicyList";
|
||||||
import { Permalinks, RichReply } from "matrix-bot-sdk";
|
import { Permalinks, RichReply } from "matrix-bot-sdk";
|
||||||
|
|
||||||
// !mjolnir list create <shortcode> <alias localpart>
|
// !mjolnir list create <shortcode> <alias localpart>
|
||||||
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import { RichReply } from "matrix-bot-sdk";
|
import { RichReply } from "matrix-bot-sdk";
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../models/BanList";
|
import { EntityType } from "../models/ListRule";
|
||||||
import { htmlEscape } from "../utils";
|
import { htmlEscape } from "../utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,12 +48,16 @@ export async function execRulesMatchingCommand(roomId: string, event: any, mjoln
|
|||||||
for (const rule of matches) {
|
for (const rule of matches) {
|
||||||
// If we know the rule kind, we will give it a readable name, otherwise just use its name.
|
// If we know the rule kind, we will give it a readable name, otherwise just use its name.
|
||||||
let ruleKind: string = rule.kind;
|
let ruleKind: string = rule.kind;
|
||||||
if (ruleKind === RULE_USER) {
|
switch (ruleKind) {
|
||||||
|
case EntityType.RULE_USER:
|
||||||
ruleKind = 'user';
|
ruleKind = 'user';
|
||||||
} else if (ruleKind === RULE_SERVER) {
|
break;
|
||||||
|
case EntityType.RULE_SERVER:
|
||||||
ruleKind = 'server';
|
ruleKind = 'server';
|
||||||
} else if (ruleKind === RULE_ROOM) {
|
break;
|
||||||
|
case EntityType.RULE_ROOM:
|
||||||
ruleKind = 'room';
|
ruleKind = 'room';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
html += `<li>${htmlEscape(ruleKind)} (<code>${htmlEscape(rule.recommendation ?? "")}</code>): <code>${htmlEscape(rule.entity)}</code> (${htmlEscape(rule.reason)})</li>`;
|
html += `<li>${htmlEscape(ruleKind)} (<code>${htmlEscape(rule.recommendation ?? "")}</code>): <code>${htmlEscape(rule.entity)}</code> (${htmlEscape(rule.reason)})</li>`;
|
||||||
text += `* ${ruleKind} (${rule.recommendation}): ${rule.entity} (${rule.reason})\n`;
|
text += `* ${ruleKind} (${rule.recommendation}): ${rule.entity} (${rule.reason})\n`;
|
||||||
|
@ -16,8 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
import { RichReply } from "matrix-bot-sdk";
|
import { RichReply } from "matrix-bot-sdk";
|
||||||
import { RECOMMENDATION_BAN, recommendationToStable } from "../models/ListRule";
|
import { EntityType, Recommendation } from "../models/ListRule";
|
||||||
import { RULE_SERVER, RULE_USER, ruleTypeToStable } from "../models/BanList";
|
|
||||||
|
|
||||||
// !mjolnir import <room ID> <shortcode>
|
// !mjolnir import <room ID> <shortcode>
|
||||||
export async function execImportCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
export async function execImportCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||||
@ -45,14 +44,13 @@ export async function execImportCommand(roomId: string, event: any, mjolnir: Mjo
|
|||||||
|
|
||||||
await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Adding user ${stateEvent['state_key']} to ban list`);
|
await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Adding user ${stateEvent['state_key']} to ban list`);
|
||||||
|
|
||||||
const recommendation = recommendationToStable(RECOMMENDATION_BAN);
|
|
||||||
const ruleContent = {
|
const ruleContent = {
|
||||||
entity: stateEvent['state_key'],
|
entity: stateEvent['state_key'],
|
||||||
recommendation,
|
recommendation: Recommendation.Ban,
|
||||||
reason: reason,
|
reason: reason,
|
||||||
};
|
};
|
||||||
const stateKey = `rule:${ruleContent.entity}`;
|
const stateKey = `rule:${ruleContent.entity}`;
|
||||||
let stableRule = ruleTypeToStable(RULE_USER);
|
let stableRule = EntityType.RULE_USER;
|
||||||
if (stableRule) {
|
if (stableRule) {
|
||||||
await mjolnir.client.sendStateEvent(list.roomId, stableRule, stateKey, ruleContent);
|
await mjolnir.client.sendStateEvent(list.roomId, stableRule, stateKey, ruleContent);
|
||||||
}
|
}
|
||||||
@ -66,14 +64,13 @@ export async function execImportCommand(roomId: string, event: any, mjolnir: Mjo
|
|||||||
|
|
||||||
await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Adding server ${server} to ban list`);
|
await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Adding server ${server} to ban list`);
|
||||||
|
|
||||||
const recommendation = recommendationToStable(RECOMMENDATION_BAN);
|
|
||||||
const ruleContent = {
|
const ruleContent = {
|
||||||
entity: server,
|
entity: server,
|
||||||
recommendation,
|
recommendation: Recommendation.Ban,
|
||||||
reason: reason,
|
reason: reason,
|
||||||
};
|
};
|
||||||
const stateKey = `rule:${ruleContent.entity}`;
|
const stateKey = `rule:${ruleContent.entity}`;
|
||||||
let stableRule = ruleTypeToStable(RULE_SERVER);
|
let stableRule = EntityType.RULE_SERVER;
|
||||||
if (stableRule) {
|
if (stableRule) {
|
||||||
await mjolnir.client.sendStateEvent(list.roomId, stableRule, stateKey, ruleContent);
|
await mjolnir.client.sendStateEvent(list.roomId, stableRule, stateKey, ruleContent);
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||||||
|
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
import { LogLevel, MatrixGlob, RichReply } from "matrix-bot-sdk";
|
import { LogLevel, MatrixGlob, RichReply } from "matrix-bot-sdk";
|
||||||
import config from "../config";
|
|
||||||
|
|
||||||
// !mjolnir kick <user|filter> [room] [reason]
|
// !mjolnir kick <user|filter> [room] [reason]
|
||||||
export async function execKickCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
export async function execKickCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||||
@ -30,7 +29,7 @@ export async function execKickCommand(roomId: string, event: any, mjolnir: Mjoln
|
|||||||
parts.pop();
|
parts.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.commands.confirmWildcardBan && /[*?]/.test(glob) && !force) {
|
if (mjolnir.config.commands.confirmWildcardBan && /[*?]/.test(glob) && !force) {
|
||||||
let replyMessage = "Wildcard bans require an addition `--force` argument to confirm";
|
let replyMessage = "Wildcard bans require an addition `--force` argument to confirm";
|
||||||
const reply = RichReply.createFor(roomId, event, replyMessage, replyMessage);
|
const reply = RichReply.createFor(roomId, event, replyMessage, replyMessage);
|
||||||
reply["msgtype"] = "m.notice";
|
reply["msgtype"] = "m.notice";
|
||||||
@ -60,7 +59,7 @@ export async function execKickCommand(roomId: string, event: any, mjolnir: Mjoln
|
|||||||
if (kickRule.test(victim)) {
|
if (kickRule.test(victim)) {
|
||||||
await mjolnir.logMessage(LogLevel.DEBUG, "KickCommand", `Removing ${victim} in ${protectedRoomId}`, protectedRoomId);
|
await mjolnir.logMessage(LogLevel.DEBUG, "KickCommand", `Removing ${victim} in ${protectedRoomId}`, protectedRoomId);
|
||||||
|
|
||||||
if (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
try {
|
try {
|
||||||
await mjolnir.taskQueue.push(async () => {
|
await mjolnir.taskQueue.push(async () => {
|
||||||
return mjolnir.client.kickUser(victim, protectedRoomId, reason);
|
return mjolnir.client.kickUser(victim, protectedRoomId, reason);
|
||||||
|
@ -14,14 +14,13 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import config from "../config";
|
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
import { RichReply } from "matrix-bot-sdk";
|
import { RichReply } from "matrix-bot-sdk";
|
||||||
|
|
||||||
// !mjolnir make admin <room> [<user ID>]
|
// !mjolnir make admin <room> [<user ID>]
|
||||||
export async function execMakeRoomAdminCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
export async function execMakeRoomAdminCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||||
const isAdmin = await mjolnir.isSynapseAdmin();
|
const isAdmin = await mjolnir.isSynapseAdmin();
|
||||||
if (!config.admin?.enableMakeRoomAdminCommand || !isAdmin) {
|
if (!mjolnir.config.admin?.enableMakeRoomAdminCommand || !isAdmin) {
|
||||||
const message = "Either the command is disabled or I am not running as homeserver administrator.";
|
const message = "Either the command is disabled or I am not running as homeserver administrator.";
|
||||||
const reply = RichReply.createFor(roomId, event, message, message);
|
const reply = RichReply.createFor(roomId, event, message, message);
|
||||||
reply['msgtype'] = "m.notice";
|
reply['msgtype'] = "m.notice";
|
||||||
|
@ -15,14 +15,13 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
import BanList, { RULE_ROOM, RULE_SERVER, RULE_USER, USER_RULE_TYPES } from "../models/BanList";
|
import PolicyList from "../models/PolicyList";
|
||||||
import { extractRequestError, LogLevel, LogService, MatrixGlob, RichReply } from "matrix-bot-sdk";
|
import { extractRequestError, LogLevel, LogService, MatrixGlob, RichReply } from "matrix-bot-sdk";
|
||||||
import { RECOMMENDATION_BAN, recommendationToStable } from "../models/ListRule";
|
import { Recommendation, RULE_ROOM, RULE_SERVER, RULE_USER, USER_RULE_TYPES } from "../models/ListRule";
|
||||||
import config from "../config";
|
|
||||||
import { DEFAULT_LIST_EVENT_TYPE } from "./SetDefaultBanListCommand";
|
import { DEFAULT_LIST_EVENT_TYPE } from "./SetDefaultBanListCommand";
|
||||||
|
|
||||||
interface Arguments {
|
interface Arguments {
|
||||||
list: BanList | null;
|
list: PolicyList | null;
|
||||||
entity: string;
|
entity: string;
|
||||||
ruleType: string | null;
|
ruleType: string | null;
|
||||||
reason: string;
|
reason: string;
|
||||||
@ -44,7 +43,7 @@ export async function parseArguments(roomId: string, event: any, mjolnir: Mjolni
|
|||||||
let argumentIndex = 2;
|
let argumentIndex = 2;
|
||||||
let ruleType: string | null = null;
|
let ruleType: string | null = null;
|
||||||
let entity: string | null = null;
|
let entity: string | null = null;
|
||||||
let list: BanList | null = null;
|
let list: PolicyList | null = null;
|
||||||
let force = false;
|
let force = false;
|
||||||
while (argumentIndex < 7 && argumentIndex < parts.length) {
|
while (argumentIndex < 7 && argumentIndex < parts.length) {
|
||||||
const arg = parts[argumentIndex++];
|
const arg = parts[argumentIndex++];
|
||||||
@ -95,7 +94,7 @@ export async function parseArguments(roomId: string, event: any, mjolnir: Mjolni
|
|||||||
else if (!ruleType) replyMessage = "Please specify the type as either 'user', 'room', or 'server'";
|
else if (!ruleType) replyMessage = "Please specify the type as either 'user', 'room', or 'server'";
|
||||||
else if (!entity) replyMessage = "No entity found";
|
else if (!entity) replyMessage = "No entity found";
|
||||||
|
|
||||||
if (config.commands.confirmWildcardBan && /[*?]/.test(entity) && !force) {
|
if (mjolnir.config.commands.confirmWildcardBan && /[*?]/.test(entity) && !force) {
|
||||||
replyMessage = "Wildcard bans require an additional `--force` argument to confirm";
|
replyMessage = "Wildcard bans require an additional `--force` argument to confirm";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,10 +118,9 @@ export async function execBanCommand(roomId: string, event: any, mjolnir: Mjolni
|
|||||||
const bits = await parseArguments(roomId, event, mjolnir, parts);
|
const bits = await parseArguments(roomId, event, mjolnir, parts);
|
||||||
if (!bits) return; // error already handled
|
if (!bits) return; // error already handled
|
||||||
|
|
||||||
const recommendation = recommendationToStable(RECOMMENDATION_BAN);
|
|
||||||
const ruleContent = {
|
const ruleContent = {
|
||||||
entity: bits.entity,
|
entity: bits.entity,
|
||||||
recommendation,
|
recommendation: Recommendation.Ban,
|
||||||
reason: bits.reason || '<no reason supplied>',
|
reason: bits.reason || '<no reason supplied>',
|
||||||
};
|
};
|
||||||
const stateKey = `rule:${bits.entity}`;
|
const stateKey = `rule:${bits.entity}`;
|
||||||
@ -151,7 +149,7 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol
|
|||||||
if (rule.test(victim)) {
|
if (rule.test(victim)) {
|
||||||
await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Unbanning ${victim} in ${protectedRoomId}`, protectedRoomId);
|
await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Unbanning ${victim} in ${protectedRoomId}`, protectedRoomId);
|
||||||
|
|
||||||
if (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
await mjolnir.client.unbanUser(victim, protectedRoomId);
|
await mjolnir.client.unbanUser(victim, protectedRoomId);
|
||||||
} else {
|
} else {
|
||||||
await mjolnir.logMessage(LogLevel.WARN, "UnbanBanCommand", `Attempted to unban ${victim} in ${protectedRoomId} but Mjolnir is running in no-op mode`, protectedRoomId);
|
await mjolnir.logMessage(LogLevel.WARN, "UnbanBanCommand", `Attempted to unban ${victim} in ${protectedRoomId} but Mjolnir is running in no-op mode`, protectedRoomId);
|
||||||
@ -164,7 +162,7 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol
|
|||||||
|
|
||||||
if (unbannedSomeone) {
|
if (unbannedSomeone) {
|
||||||
await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Syncing lists to ensure no users were accidentally unbanned`);
|
await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Syncing lists to ensure no users were accidentally unbanned`);
|
||||||
await mjolnir.syncLists(config.verboseLogging);
|
await mjolnir.syncLists(mjolnir.config.verboseLogging);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import { MatrixClient } from "matrix-bot-sdk";
|
|||||||
// The object is magically generated by external lib `config`
|
// The object is magically generated by external lib `config`
|
||||||
// from the file specified by `NODE_ENV`, e.g. production.yaml
|
// from the file specified by `NODE_ENV`, e.g. production.yaml
|
||||||
// or harness.yaml.
|
// or harness.yaml.
|
||||||
interface IConfig {
|
export interface IConfig {
|
||||||
homeserverUrl: string;
|
homeserverUrl: string;
|
||||||
rawHomeserverUrl: string;
|
rawHomeserverUrl: string;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import config from "../config";
|
|
||||||
import * as http from "http";
|
import * as http from "http";
|
||||||
import { LogService } from "matrix-bot-sdk";
|
import { LogService } from "matrix-bot-sdk";
|
||||||
|
// allowed to use the global configuration since this is only intended to be used by `src/index.ts`.
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
export class Healthz {
|
export class Healthz {
|
||||||
private static healthCode: number;
|
private static healthCode: number;
|
||||||
|
@ -56,13 +56,14 @@ if (config.health.healthz.enabled) {
|
|||||||
patchMatrixClient();
|
patchMatrixClient();
|
||||||
config.RUNTIME.client = client;
|
config.RUNTIME.client = client;
|
||||||
|
|
||||||
bot = await Mjolnir.setupMjolnirFromConfig(client);
|
bot = await Mjolnir.setupMjolnirFromConfig(client, config);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to setup mjolnir from the config ${config.dataPath}: ${err}`);
|
console.error(`Failed to setup mjolnir from the config ${config.dataPath}: ${err}`);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await bot.start();
|
await bot.start();
|
||||||
|
Healthz.isHealthy = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Mjolnir failed to start: ${err}`);
|
console.error(`Mjolnir failed to start: ${err}`);
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -16,32 +16,238 @@ limitations under the License.
|
|||||||
|
|
||||||
import { MatrixGlob } from "matrix-bot-sdk";
|
import { MatrixGlob } from "matrix-bot-sdk";
|
||||||
|
|
||||||
export const RECOMMENDATION_BAN = "m.ban";
|
export enum EntityType {
|
||||||
export const RECOMMENDATION_BAN_TYPES = [RECOMMENDATION_BAN, "org.matrix.mjolnir.ban"];
|
/// `entity` is to be parsed as a glob of users IDs
|
||||||
|
RULE_USER = "m.policy.rule.user",
|
||||||
|
|
||||||
export function recommendationToStable(recommendation: string, unstable = false): string|null {
|
/// `entity` is to be parsed as a glob of room IDs/aliases
|
||||||
if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN;
|
RULE_ROOM = "m.policy.rule.room",
|
||||||
return null;
|
|
||||||
|
/// `entity` is to be parsed as a glob of server names
|
||||||
|
RULE_SERVER = "m.policy.rule.server",
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ListRule {
|
export const RULE_USER = EntityType.RULE_USER;
|
||||||
|
export const RULE_ROOM = EntityType.RULE_ROOM;
|
||||||
|
export const RULE_SERVER = EntityType.RULE_SERVER;
|
||||||
|
|
||||||
|
// README! The order here matters for determining whether a type is obsolete, most recent should be first.
|
||||||
|
// These are the current and historical types for each type of rule which were used while MSC2313 was being developed
|
||||||
|
// and were left as an artifact for some time afterwards.
|
||||||
|
// Most rules (as of writing) will have the prefix `m.room.rule.*` as this has been in use for roughly 2 years.
|
||||||
|
export const USER_RULE_TYPES = [RULE_USER, "m.room.rule.user", "org.matrix.mjolnir.rule.user"];
|
||||||
|
export const ROOM_RULE_TYPES = [RULE_ROOM, "m.room.rule.room", "org.matrix.mjolnir.rule.room"];
|
||||||
|
export const SERVER_RULE_TYPES = [RULE_SERVER, "m.room.rule.server", "org.matrix.mjolnir.rule.server"];
|
||||||
|
export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES];
|
||||||
|
|
||||||
|
export enum Recommendation {
|
||||||
|
/// The rule recommends a "ban".
|
||||||
|
///
|
||||||
|
/// The actual semantics for this "ban" may vary, e.g. room ban,
|
||||||
|
/// server ban, ignore user, etc. To determine the semantics for
|
||||||
|
/// this "ban", clients need to take into account the context for
|
||||||
|
/// the list, e.g. how the rule was imported.
|
||||||
|
Ban = "m.ban",
|
||||||
|
|
||||||
|
/// The rule specifies an "opinion", as a number in [-100, +100],
|
||||||
|
/// where -100 represents a user who is considered absolutely toxic
|
||||||
|
/// by whoever issued this ListRule and +100 represents a user who
|
||||||
|
/// is considered absolutely absolutely perfect by whoever issued
|
||||||
|
/// this ListRule.
|
||||||
|
Opinion = "org.matrix.msc3845.opinion",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All variants of recommendation `m.ban`
|
||||||
|
*/
|
||||||
|
const RECOMMENDATION_BAN_VARIANTS = [
|
||||||
|
// Stable
|
||||||
|
Recommendation.Ban,
|
||||||
|
// Unstable prefix, for compatibility.
|
||||||
|
"org.matrix.mjolnir.ban"
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All variants of recommendation `m.opinion`
|
||||||
|
*/
|
||||||
|
const RECOMMENDATION_OPINION_VARIANTS: string[] = [
|
||||||
|
// Unstable
|
||||||
|
Recommendation.Opinion
|
||||||
|
];
|
||||||
|
|
||||||
|
export const OPINION_MIN = -100;
|
||||||
|
export const OPINION_MAX = +100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a rule within a Policy List.
|
||||||
|
*/
|
||||||
|
export abstract class ListRule {
|
||||||
|
/**
|
||||||
|
* A glob for `entity`.
|
||||||
|
*/
|
||||||
private glob: MatrixGlob;
|
private glob: MatrixGlob;
|
||||||
|
constructor(
|
||||||
constructor(public readonly entity: string, private action: string, public readonly reason: string, public readonly kind: string) {
|
/**
|
||||||
|
* The entity covered by this rule, e.g. a glob user ID, a room ID, a server domain.
|
||||||
|
*/
|
||||||
|
public readonly entity: string,
|
||||||
|
/**
|
||||||
|
* A human-readable reason for this rule, for audit purposes.
|
||||||
|
*/
|
||||||
|
public readonly reason: string,
|
||||||
|
/**
|
||||||
|
* The type of entity for this rule, e.g. user, server domain, etc.
|
||||||
|
*/
|
||||||
|
public readonly kind: EntityType,
|
||||||
|
/**
|
||||||
|
* The recommendation for this rule, e.g. "ban" or "opinion", or `null`
|
||||||
|
* if the recommendation is one that Mjölnir doesn't understand.
|
||||||
|
*/
|
||||||
|
public readonly recommendation: Recommendation | null) {
|
||||||
this.glob = new MatrixGlob(entity);
|
this.glob = new MatrixGlob(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The recommendation for this rule, or `null` if there is no recommendation or the recommendation is invalid.
|
* Determine whether this rule should apply to a given entity.
|
||||||
* Recommendations are normalised to their stable types.
|
|
||||||
*/
|
*/
|
||||||
public get recommendation(): string|null {
|
|
||||||
if (RECOMMENDATION_BAN_TYPES.includes(this.action)) return RECOMMENDATION_BAN;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMatch(entity: string): boolean {
|
public isMatch(entity: string): boolean {
|
||||||
return this.glob.test(entity);
|
return this.glob.test(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and parse an event into a ListRule.
|
||||||
|
*
|
||||||
|
* @param event An *untrusted* event.
|
||||||
|
* @returns null if the ListRule is invalid or not recognized by Mjölnir.
|
||||||
|
*/
|
||||||
|
public static parse(event: {type: string, content: any}): ListRule | null {
|
||||||
|
// Parse common fields.
|
||||||
|
// If a field is ill-formed, discard the rule.
|
||||||
|
const content = event['content'];
|
||||||
|
if (!content || typeof content !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const entity = content['entity'];
|
||||||
|
if (!entity || typeof entity !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const recommendation = content['recommendation'];
|
||||||
|
if (!recommendation || typeof recommendation !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reason = content['reason'] || '<no reason>';
|
||||||
|
if (typeof reason !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let type = event['type'];
|
||||||
|
let kind;
|
||||||
|
if (USER_RULE_TYPES.includes(type)) {
|
||||||
|
kind = EntityType.RULE_USER;
|
||||||
|
} else if (ROOM_RULE_TYPES.includes(type)) {
|
||||||
|
kind = EntityType.RULE_ROOM;
|
||||||
|
} else if (SERVER_RULE_TYPES.includes(type)) {
|
||||||
|
kind = EntityType.RULE_SERVER;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From this point, we may need specific fields.
|
||||||
|
if (RECOMMENDATION_BAN_VARIANTS.includes(recommendation)) {
|
||||||
|
return new ListRuleBan(entity, reason, kind);
|
||||||
|
} else if (RECOMMENDATION_OPINION_VARIANTS.includes(recommendation)) {
|
||||||
|
let opinion = content['opinion'];
|
||||||
|
if (!Number.isInteger(opinion)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new ListRuleOpinion(entity, reason, kind, opinion);
|
||||||
|
} else {
|
||||||
|
// As long as the `recommendation` is defined, we assume
|
||||||
|
// that the rule is correct, just unknown.
|
||||||
|
return new ListRuleUnknown(entity, reason, kind, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A rule representing a "ban".
|
||||||
|
*/
|
||||||
|
export class ListRuleBan extends ListRule {
|
||||||
|
constructor(
|
||||||
|
/**
|
||||||
|
* The entity covered by this rule, e.g. a glob user ID, a room ID, a server domain.
|
||||||
|
*/
|
||||||
|
entity: string,
|
||||||
|
/**
|
||||||
|
* A human-readable reason for this rule, for audit purposes.
|
||||||
|
*/
|
||||||
|
reason: string,
|
||||||
|
/**
|
||||||
|
* The type of entity for this rule, e.g. user, server domain, etc.
|
||||||
|
*/
|
||||||
|
kind: EntityType,
|
||||||
|
) {
|
||||||
|
super(entity, reason, kind, Recommendation.Ban)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A rule representing an "opinion"
|
||||||
|
*/
|
||||||
|
export class ListRuleOpinion extends ListRule {
|
||||||
|
constructor(
|
||||||
|
/**
|
||||||
|
* The entity covered by this rule, e.g. a glob user ID, a room ID, a server domain.
|
||||||
|
*/
|
||||||
|
entity: string,
|
||||||
|
/**
|
||||||
|
* A human-readable reason for this rule, for audit purposes.
|
||||||
|
*/
|
||||||
|
reason: string,
|
||||||
|
/**
|
||||||
|
* The type of entity for this rule, e.g. user, server domain, etc.
|
||||||
|
*/
|
||||||
|
kind: EntityType,
|
||||||
|
/**
|
||||||
|
* A number in [-100, +100] where -100 represents the worst possible opinion
|
||||||
|
* on the entity (e.g. toxic user or community) and +100 represents the best
|
||||||
|
* possible opinion on the entity (e.g. pillar of the community).
|
||||||
|
*/
|
||||||
|
public readonly opinion: number
|
||||||
|
) {
|
||||||
|
super(entity, reason, kind, Recommendation.Opinion);
|
||||||
|
if (!Number.isInteger(opinion)) {
|
||||||
|
throw new TypeError(`The opinion must be an integer, got ${opinion}`);
|
||||||
|
}
|
||||||
|
if (opinion < OPINION_MIN || opinion > OPINION_MAX) {
|
||||||
|
throw new TypeError(`The opinion must be within [-100, +100], got ${opinion}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any list rule that we do not understand.
|
||||||
|
*/
|
||||||
|
export class ListRuleUnknown extends ListRule {
|
||||||
|
constructor(
|
||||||
|
/**
|
||||||
|
* The entity covered by this rule, e.g. a glob user ID, a room ID, a server domain.
|
||||||
|
*/
|
||||||
|
entity: string,
|
||||||
|
/**
|
||||||
|
* A human-readable reason for this rule, for audit purposes.
|
||||||
|
*/
|
||||||
|
reason: string,
|
||||||
|
/**
|
||||||
|
* The type of entity for this rule, e.g. user, server domain, etc.
|
||||||
|
*/
|
||||||
|
kind: EntityType,
|
||||||
|
/**
|
||||||
|
* The event used to create the rule.
|
||||||
|
*/
|
||||||
|
public readonly content: any,
|
||||||
|
) {
|
||||||
|
super(entity, reason, kind, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,30 +16,10 @@ limitations under the License.
|
|||||||
|
|
||||||
import { extractRequestError, LogService, MatrixClient, UserID } from "matrix-bot-sdk";
|
import { extractRequestError, LogService, MatrixClient, UserID } from "matrix-bot-sdk";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { ListRule, RECOMMENDATION_BAN } from "./ListRule";
|
import { ALL_RULE_TYPES, EntityType, ListRule, Recommendation, ROOM_RULE_TYPES, RULE_ROOM, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES, USER_RULE_TYPES } from "./ListRule";
|
||||||
|
|
||||||
export const RULE_USER = "m.policy.rule.user";
|
|
||||||
export const RULE_ROOM = "m.policy.rule.room";
|
|
||||||
export const RULE_SERVER = "m.policy.rule.server";
|
|
||||||
|
|
||||||
// README! The order here matters for determining whether a type is obsolete, most recent should be first.
|
|
||||||
// These are the current and historical types for each type of rule which were used while MSC2313 was being developed
|
|
||||||
// and were left as an artifact for some time afterwards.
|
|
||||||
// Most rules (as of writing) will have the prefix `m.room.rule.*` as this has been in use for roughly 2 years.
|
|
||||||
export const USER_RULE_TYPES = [RULE_USER, "m.room.rule.user", "org.matrix.mjolnir.rule.user"];
|
|
||||||
export const ROOM_RULE_TYPES = [RULE_ROOM, "m.room.rule.room", "org.matrix.mjolnir.rule.room"];
|
|
||||||
export const SERVER_RULE_TYPES = [RULE_SERVER, "m.room.rule.server", "org.matrix.mjolnir.rule.server"];
|
|
||||||
export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES];
|
|
||||||
|
|
||||||
export const SHORTCODE_EVENT_TYPE = "org.matrix.mjolnir.shortcode";
|
export const SHORTCODE_EVENT_TYPE = "org.matrix.mjolnir.shortcode";
|
||||||
|
|
||||||
export function ruleTypeToStable(rule: string, unstable = true): string|null {
|
|
||||||
if (USER_RULE_TYPES.includes(rule)) return unstable ? USER_RULE_TYPES[USER_RULE_TYPES.length - 1] : RULE_USER;
|
|
||||||
if (ROOM_RULE_TYPES.includes(rule)) return unstable ? ROOM_RULE_TYPES[ROOM_RULE_TYPES.length - 1] : RULE_ROOM;
|
|
||||||
if (SERVER_RULE_TYPES.includes(rule)) return unstable ? SERVER_RULE_TYPES[SERVER_RULE_TYPES.length - 1] : RULE_SERVER;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ChangeType {
|
export enum ChangeType {
|
||||||
Added = "ADDED",
|
Added = "ADDED",
|
||||||
Removed = "REMOVED",
|
Removed = "REMOVED",
|
||||||
@ -71,20 +51,20 @@ export interface ListRuleChange {
|
|||||||
readonly previousState?: any,
|
readonly previousState?: any,
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface BanList {
|
declare interface PolicyList {
|
||||||
// BanList.update is emitted when the BanList has pulled new rules from Matrix and informs listeners of any changes.
|
// PolicyList.update is emitted when the PolicyList has pulled new rules from Matrix and informs listeners of any changes.
|
||||||
on(event: 'BanList.update', listener: (list: BanList, changes: ListRuleChange[]) => void): this
|
on(event: 'PolicyList.update', listener: (list: PolicyList, changes: ListRuleChange[]) => void): this
|
||||||
emit(event: 'BanList.update', list: BanList, changes: ListRuleChange[]): boolean
|
emit(event: 'PolicyList.update', list: PolicyList, changes: ListRuleChange[]): boolean
|
||||||
// BanList.batch is emitted when the BanList has created a batch from the events provided by `updateForEvent`.
|
// PolicyList.batch is emitted when the PolicyList has created a batch from the events provided by `updateForEvent`.
|
||||||
on(event: 'BanList.batch', listener: (list: BanList) => void): this
|
on(event: 'PolicyList.batch', listener: (list: PolicyList) => void): this
|
||||||
emit(event: 'BanList.batch', list: BanList): boolean
|
emit(event: 'PolicyList.batch', list: PolicyList): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The BanList caches all of the rules that are active in a policy room so Mjolnir can refer to when applying bans etc.
|
* The PolicyList caches all of the rules that are active in a policy room so Mjolnir can refer to when applying bans etc.
|
||||||
* This cannot be used to update events in the modeled room, it is a readonly model of the policy room.
|
* This cannot be used to update events in the modeled room, it is a readonly model of the policy room.
|
||||||
*/
|
*/
|
||||||
class BanList extends EventEmitter {
|
class PolicyList extends EventEmitter {
|
||||||
private shortcode: string | null = null;
|
private shortcode: string | null = null;
|
||||||
// A map of state events indexed first by state type and then state keys.
|
// A map of state events indexed first by state type and then state keys.
|
||||||
private state: Map<string, Map<string, any>> = new Map();
|
private state: Map<string, Map<string, any>> = new Map();
|
||||||
@ -92,7 +72,7 @@ class BanList extends EventEmitter {
|
|||||||
private readonly batcher: UpdateBatcher;
|
private readonly batcher: UpdateBatcher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a BanList, does not synchronize with the room.
|
* Construct a PolicyList, does not synchronize with the room.
|
||||||
* @param roomId The id of the policy room, i.e. a room containing MSC2313 policies.
|
* @param roomId The id of the policy room, i.e. a room containing MSC2313 policies.
|
||||||
* @param roomRef A sharable/clickable matrix URL that refers to the room.
|
* @param roomRef A sharable/clickable matrix URL that refers to the room.
|
||||||
* @param client A matrix client that is used to read the state of the room when `updateList` is called.
|
* @param client A matrix client that is used to read the state of the room when `updateList` is called.
|
||||||
@ -120,7 +100,7 @@ class BanList extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store this state event as part of the active room state for this BanList (used to cache rules).
|
* Store this state event as part of the active room state for this PolicyList (used to cache rules).
|
||||||
* The state type should be normalised if it is obsolete e.g. m.room.rule.user should be stored as m.policy.rule.user.
|
* The state type should be normalised if it is obsolete e.g. m.room.rule.user should be stored as m.policy.rule.user.
|
||||||
* @param stateType The event type e.g. m.room.policy.user.
|
* @param stateType The event type e.g. m.room.policy.user.
|
||||||
* @param stateKey The state key e.g. rule:@bad:matrix.org
|
* @param stateKey The state key e.g. rule:@bad:matrix.org
|
||||||
@ -137,7 +117,7 @@ class BanList extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return all the active rules of a given kind.
|
* Return all the active rules of a given kind.
|
||||||
* @param kind e.g. RULE_SERVER (m.policy.rule.server). Rule types are always normalised when they are interned into the BanList.
|
* @param kind e.g. RULE_SERVER (m.policy.rule.server). Rule types are always normalised when they are interned into the PolicyList.
|
||||||
* @returns The active ListRules for the ban list of that kind.
|
* @returns The active ListRules for the ban list of that kind.
|
||||||
*/
|
*/
|
||||||
private rulesOfKind(kind: string): ListRule[] {
|
private rulesOfKind(kind: string): ListRule[] {
|
||||||
@ -149,7 +129,7 @@ class BanList extends EventEmitter {
|
|||||||
// README! If you are refactoring this and/or introducing a mechanism to return the list of rules,
|
// README! If you are refactoring this and/or introducing a mechanism to return the list of rules,
|
||||||
// please make sure that you *only* return rules with `m.ban` or create a different method
|
// please make sure that you *only* return rules with `m.ban` or create a different method
|
||||||
// (we don't want to accidentally ban entities).
|
// (we don't want to accidentally ban entities).
|
||||||
if (rule && rule.kind === kind && rule.recommendation === RECOMMENDATION_BAN) {
|
if (rule && rule.kind === kind && rule.recommendation === Recommendation.Ban) {
|
||||||
rules.push(rule);
|
rules.push(rule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,7 +141,7 @@ class BanList extends EventEmitter {
|
|||||||
const currentShortcode = this.shortcode;
|
const currentShortcode = this.shortcode;
|
||||||
this.shortcode = newShortcode;
|
this.shortcode = newShortcode;
|
||||||
this.client.sendStateEvent(this.roomId, SHORTCODE_EVENT_TYPE, '', { shortcode: this.shortcode }).catch(err => {
|
this.client.sendStateEvent(this.roomId, SHORTCODE_EVENT_TYPE, '', { shortcode: this.shortcode }).catch(err => {
|
||||||
LogService.error("BanList", extractRequestError(err));
|
LogService.error("PolicyList", extractRequestError(err));
|
||||||
if (this.shortcode === newShortcode) this.shortcode = currentShortcode;
|
if (this.shortcode === newShortcode) this.shortcode = currentShortcode;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -267,7 +247,7 @@ class BanList extends EventEmitter {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let kind: string|null = null;
|
let kind: EntityType | null = null;
|
||||||
if (USER_RULE_TYPES.includes(event['type'])) {
|
if (USER_RULE_TYPES.includes(event['type'])) {
|
||||||
kind = RULE_USER;
|
kind = RULE_USER;
|
||||||
} else if (ROOM_RULE_TYPES.includes(event['type'])) {
|
} else if (ROOM_RULE_TYPES.includes(event['type'])) {
|
||||||
@ -286,7 +266,7 @@ class BanList extends EventEmitter {
|
|||||||
// as it may be someone deleting the older versions of the rules.
|
// as it may be someone deleting the older versions of the rules.
|
||||||
if (previousState) {
|
if (previousState) {
|
||||||
const logObsoleteRule = () => {
|
const logObsoleteRule = () => {
|
||||||
LogService.info('BanList', `In BanList ${this.roomRef}, conflict between rules ${event['event_id']} (with obsolete type ${event['type']}) ` +
|
LogService.info('PolicyList', `In PolicyList ${this.roomRef}, conflict between rules ${event['event_id']} (with obsolete type ${event['type']}) ` +
|
||||||
`and ${previousState['event_id']} (with standard type ${previousState['type']}). Ignoring rule with obsolete type.`);
|
`and ${previousState['event_id']} (with standard type ${previousState['type']}). Ignoring rule with obsolete type.`);
|
||||||
}
|
}
|
||||||
if (kind === RULE_USER && USER_RULE_TYPES.indexOf(event['type']) > USER_RULE_TYPES.indexOf(previousState['type'])) {
|
if (kind === RULE_USER && USER_RULE_TYPES.indexOf(event['type']) > USER_RULE_TYPES.indexOf(previousState['type'])) {
|
||||||
@ -329,45 +309,41 @@ class BanList extends EventEmitter {
|
|||||||
// and so will not have been used. Removing a rule like this therefore results in no change.
|
// and so will not have been used. Removing a rule like this therefore results in no change.
|
||||||
if (changeType === ChangeType.Removed && previousState?.unsigned?.rule) {
|
if (changeType === ChangeType.Removed && previousState?.unsigned?.rule) {
|
||||||
const sender = event.unsigned['redacted_because'] ? event.unsigned['redacted_because']['sender'] : event.sender;
|
const sender = event.unsigned['redacted_because'] ? event.unsigned['redacted_because']['sender'] : event.sender;
|
||||||
changes.push({changeType, event, sender, rule: previousState.unsigned.rule,
|
changes.push({
|
||||||
... previousState ? {previousState} : {} });
|
changeType, event, sender, rule: previousState.unsigned.rule,
|
||||||
|
...previousState ? { previousState } : {}
|
||||||
|
});
|
||||||
// Event has no content and cannot be parsed as a ListRule.
|
// Event has no content and cannot be parsed as a ListRule.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// It's a rule - parse it
|
// It's a rule - parse it
|
||||||
const content = event['content'];
|
const rule = ListRule.parse(event);
|
||||||
if (!content) continue;
|
if (!rule) {
|
||||||
|
// Invalid/unknown rule, just skip it.
|
||||||
const entity = content['entity'];
|
|
||||||
const recommendation = content['recommendation'];
|
|
||||||
const reason = content['reason'] || '<no reason>';
|
|
||||||
|
|
||||||
if (!entity || !recommendation) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const rule = new ListRule(entity, recommendation, reason, kind);
|
|
||||||
event.unsigned.rule = rule;
|
event.unsigned.rule = rule;
|
||||||
if (changeType) {
|
if (changeType) {
|
||||||
changes.push({ rule, changeType, event, sender: event.sender, ...previousState ? { previousState } : {} });
|
changes.push({ rule, changeType, event, sender: event.sender, ...previousState ? { previousState } : {} });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.emit('BanList.update', this, changes);
|
this.emit('PolicyList.update', this, changes);
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inform the `BanList` about a new event from the room it is modelling.
|
* Inform the `PolicyList` about a new event from the room it is modelling.
|
||||||
* @param event An event from the room the `BanList` models to inform an instance about.
|
* @param event An event from the room the `PolicyList` models to inform an instance about.
|
||||||
*/
|
*/
|
||||||
public updateForEvent(event: { event_id: string }): void {
|
public updateForEvent(event: { event_id: string }): void {
|
||||||
this.batcher.addToBatch(event.event_id)
|
this.batcher.addToBatch(event.event_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BanList;
|
export default PolicyList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper class that emits a batch event on a `BanList` when it has made a batch
|
* Helper class that emits a batch event on a `PolicyList` when it has made a batch
|
||||||
* out of the events given to `addToBatch`.
|
* out of the events given to `addToBatch`.
|
||||||
*/
|
*/
|
||||||
class UpdateBatcher {
|
class UpdateBatcher {
|
||||||
@ -378,7 +354,7 @@ class UpdateBatcher {
|
|||||||
private readonly waitPeriodMS = 200; // 200ms seems good enough.
|
private readonly waitPeriodMS = 200; // 200ms seems good enough.
|
||||||
private readonly maxWaitMS = 3000; // 3s is long enough to wait while batching.
|
private readonly maxWaitMS = 3000; // 3s is long enough to wait while batching.
|
||||||
|
|
||||||
constructor(private readonly banList: BanList) {
|
constructor(private readonly banList: PolicyList) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,7 +378,7 @@ class UpdateBatcher {
|
|||||||
await new Promise(resolve => setTimeout(resolve, this.waitPeriodMS));
|
await new Promise(resolve => setTimeout(resolve, this.waitPeriodMS));
|
||||||
} while ((Date.now() - start) < this.maxWaitMS && this.latestEventId !== eventId)
|
} while ((Date.now() - start) < this.maxWaitMS && this.latestEventId !== eventId)
|
||||||
this.reset();
|
this.reset();
|
||||||
this.banList.emit('BanList.batch', this.banList);
|
this.banList.emit('PolicyList.batch', this.banList);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
@ -13,10 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import BanList, { ChangeType, ListRuleChange, RULE_ROOM, RULE_SERVER, RULE_USER } from "./BanList"
|
import BanList, { ChangeType, ListRuleChange } from "./PolicyList"
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import { LogService } from "matrix-bot-sdk";
|
import { LogService } from "matrix-bot-sdk";
|
||||||
import { ListRule } from "./ListRule";
|
import { EntityType, ListRule } from "./ListRule";
|
||||||
|
import PolicyList from "./PolicyList";
|
||||||
|
|
||||||
export const USER_MAY_INVITE = 'user_may_invite';
|
export const USER_MAY_INVITE = 'user_may_invite';
|
||||||
export const CHECK_EVENT_FOR_SPAM = 'check_event_for_spam';
|
export const CHECK_EVENT_FOR_SPAM = 'check_event_for_spam';
|
||||||
@ -184,16 +185,16 @@ export default class RuleServer {
|
|||||||
* as we won't be able to serve rules that have already been interned in the BanList.
|
* as we won't be able to serve rules that have already been interned in the BanList.
|
||||||
* @param banList a BanList to watch for rule changes with.
|
* @param banList a BanList to watch for rule changes with.
|
||||||
*/
|
*/
|
||||||
public watch(banList: BanList): void {
|
public watch(banList: PolicyList): void {
|
||||||
banList.on('BanList.update', this.banListUpdateListener);
|
banList.on('PolicyList.update', this.banListUpdateListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove all of the rules that have been created from the policies in this banList.
|
* Remove all of the rules that have been created from the policies in this banList.
|
||||||
* @param banList The BanList to unwatch.
|
* @param banList The BanList to unwatch.
|
||||||
*/
|
*/
|
||||||
public unwatch(banList: BanList): void {
|
public unwatch(banList: PolicyList): void {
|
||||||
banList.removeListener('BanList.update', this.banListUpdateListener);
|
banList.removeListener('PolicyList.update', this.banListUpdateListener);
|
||||||
const listRules = this.rulesByEvent.get(banList.roomId);
|
const listRules = this.rulesByEvent.get(banList.roomId);
|
||||||
this.nextToken();
|
this.nextToken();
|
||||||
if (listRules) {
|
if (listRules) {
|
||||||
@ -280,7 +281,7 @@ function toRuleServerFormat(policyRule: ListRule): RuleServerRule[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (policyRule.kind === RULE_USER) {
|
if (policyRule.kind === EntityType.RULE_USER) {
|
||||||
// Block any messages or invites from being sent by a matching local user
|
// Block any messages or invites from being sent by a matching local user
|
||||||
// Block any messages or invitations from being received that were sent by a matching remote user.
|
// Block any messages or invitations from being received that were sent by a matching remote user.
|
||||||
return [{
|
return [{
|
||||||
@ -291,7 +292,7 @@ function toRuleServerFormat(policyRule: ListRule): RuleServerRule[] {
|
|||||||
property: CHECK_EVENT_FOR_SPAM,
|
property: CHECK_EVENT_FOR_SPAM,
|
||||||
sender: [makeGlob(policyRule.entity)]
|
sender: [makeGlob(policyRule.entity)]
|
||||||
}].map(makeRule)
|
}].map(makeRule)
|
||||||
} else if (policyRule.kind === RULE_ROOM) {
|
} else if (policyRule.kind === EntityType.RULE_ROOM) {
|
||||||
// Block any messages being sent or received in the room, stop invitations being sent to the room and
|
// Block any messages being sent or received in the room, stop invitations being sent to the room and
|
||||||
// stop anyone receiving invitations from the room.
|
// stop anyone receiving invitations from the room.
|
||||||
return [{
|
return [{
|
||||||
@ -302,7 +303,7 @@ function toRuleServerFormat(policyRule: ListRule): RuleServerRule[] {
|
|||||||
property: CHECK_EVENT_FOR_SPAM,
|
property: CHECK_EVENT_FOR_SPAM,
|
||||||
'room_id': [makeLiteral(policyRule.entity)]
|
'room_id': [makeLiteral(policyRule.entity)]
|
||||||
}].map(makeRule)
|
}].map(makeRule)
|
||||||
} else if (policyRule.kind === RULE_SERVER) {
|
} else if (policyRule.kind === EntityType.RULE_SERVER) {
|
||||||
// Block any invitations from the server or any new messages from the server.
|
// Block any invitations from the server or any new messages from the server.
|
||||||
return [{
|
return [{
|
||||||
property: USER_MAY_INVITE,
|
property: USER_MAY_INVITE,
|
||||||
|
@ -18,7 +18,6 @@ import { Protection } from "./IProtection";
|
|||||||
import { NumberProtectionSetting } from "./ProtectionSettings";
|
import { NumberProtectionSetting } from "./ProtectionSettings";
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
import { LogLevel, LogService } from "matrix-bot-sdk";
|
import { LogLevel, LogService } from "matrix-bot-sdk";
|
||||||
import config from "../config";
|
|
||||||
|
|
||||||
// if this is exceeded, we'll ban the user for spam and redact their messages
|
// if this is exceeded, we'll ban the user for spam and redact their messages
|
||||||
export const DEFAULT_MAX_PER_MINUTE = 10;
|
export const DEFAULT_MAX_PER_MINUTE = 10;
|
||||||
@ -64,7 +63,7 @@ export class BasicFlooding extends Protection {
|
|||||||
|
|
||||||
if (messageCount >= this.settings.maxPerMinute.value) {
|
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.logMessage(LogLevel.WARN, "BasicFlooding", `Banning ${event['sender']} in ${roomId} for flooding (${messageCount} messages in the last minute)`, roomId);
|
||||||
if (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
await mjolnir.client.banUser(event['sender'], roomId, "spam");
|
await mjolnir.client.banUser(event['sender'], roomId, "spam");
|
||||||
} else {
|
} 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.logMessage(LogLevel.WARN, "BasicFlooding", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||||
@ -75,7 +74,7 @@ export class BasicFlooding extends Protection {
|
|||||||
this.recentlyBanned.push(event['sender']); // flag to reduce spam
|
this.recentlyBanned.push(event['sender']); // flag to reduce spam
|
||||||
|
|
||||||
// Redact all the things the user said too
|
// Redact all the things the user said too
|
||||||
if (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
for (const eventId of forUser.map(e => e.eventId)) {
|
for (const eventId of forUser.map(e => e.eventId)) {
|
||||||
await mjolnir.client.redactEvent(roomId, eventId, "spam");
|
await mjolnir.client.redactEvent(roomId, eventId, "spam");
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||||||
import { Protection } from "./IProtection";
|
import { Protection } from "./IProtection";
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
import { LogLevel, LogService } from "matrix-bot-sdk";
|
import { LogLevel, LogService } from "matrix-bot-sdk";
|
||||||
import config from "../config";
|
|
||||||
import { isTrueJoinEvent } from "../utils";
|
import { isTrueJoinEvent } from "../utils";
|
||||||
|
|
||||||
export class FirstMessageIsImage extends Protection {
|
export class FirstMessageIsImage extends Protection {
|
||||||
@ -58,7 +57,7 @@ export class FirstMessageIsImage extends Protection {
|
|||||||
const isMedia = msgtype === 'm.image' || msgtype === 'm.video' || formattedBody.toLowerCase().includes('<img');
|
const isMedia = msgtype === 'm.image' || msgtype === 'm.video' || formattedBody.toLowerCase().includes('<img');
|
||||||
if (isMedia && this.justJoined[roomId].includes(event['sender'])) {
|
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.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Banning ${event['sender']} for posting an image as the first thing after joining in ${roomId}.`);
|
||||||
if (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
await mjolnir.client.banUser(event['sender'], roomId, "spam");
|
await mjolnir.client.banUser(event['sender'], roomId, "spam");
|
||||||
} else {
|
} 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.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||||
@ -69,7 +68,7 @@ export class FirstMessageIsImage extends Protection {
|
|||||||
this.recentlyBanned.push(event['sender']); // flag to reduce spam
|
this.recentlyBanned.push(event['sender']); // flag to reduce spam
|
||||||
|
|
||||||
// Redact the event
|
// Redact the event
|
||||||
if (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
await mjolnir.client.redactEvent(roomId, event['event_id'], "spam");
|
await mjolnir.client.redactEvent(roomId, event['event_id'], "spam");
|
||||||
} else {
|
} 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.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||||
|
@ -18,7 +18,6 @@ import {Protection} from "./IProtection";
|
|||||||
import {Mjolnir} from "../Mjolnir";
|
import {Mjolnir} from "../Mjolnir";
|
||||||
import {NumberProtectionSetting} from "./ProtectionSettings";
|
import {NumberProtectionSetting} from "./ProtectionSettings";
|
||||||
import {LogLevel} from "matrix-bot-sdk";
|
import {LogLevel} from "matrix-bot-sdk";
|
||||||
import config from "../config";
|
|
||||||
|
|
||||||
const DEFAULT_MAX_PER_TIMESCALE = 50;
|
const DEFAULT_MAX_PER_TIMESCALE = 50;
|
||||||
const DEFAULT_TIMESCALE_MINUTES = 60;
|
const DEFAULT_TIMESCALE_MINUTES = 60;
|
||||||
@ -89,7 +88,7 @@ export class JoinWaveShortCircuit extends Protection {
|
|||||||
if (++this.joinBuckets[roomId].numberOfJoins >= this.settings.maxPer.value) {
|
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.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 (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
await mjolnir.client.sendStateEvent(roomId, "m.room.join_rules", "", {"join_rule": "invite"})
|
await mjolnir.client.sendStateEvent(roomId, "m.room.join_rules", "", {"join_rule": "invite"})
|
||||||
} else {
|
} 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.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Tried to set ${roomId} to invite-only, but Mjolnir is running in no-op mode`, roomId);
|
||||||
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||||||
import { Protection } from "./IProtection";
|
import { Protection } from "./IProtection";
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk";
|
import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk";
|
||||||
import config from "../config";
|
|
||||||
|
|
||||||
export class MessageIsMedia extends Protection {
|
export class MessageIsMedia extends Protection {
|
||||||
|
|
||||||
@ -43,7 +42,7 @@ export class MessageIsMedia extends Protection {
|
|||||||
if (isMedia) {
|
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.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
|
// Redact the event
|
||||||
if (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
await mjolnir.client.redactEvent(roomId, event['event_id'], "Images/videos are not permitted here");
|
await mjolnir.client.redactEvent(roomId, event['event_id'], "Images/videos are not permitted here");
|
||||||
} else {
|
} 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.logMessage(LogLevel.WARN, "MessageIsMedia", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||||
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||||||
import { Protection } from "./IProtection";
|
import { Protection } from "./IProtection";
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk";
|
import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk";
|
||||||
import config from "../config";
|
|
||||||
|
|
||||||
export class MessageIsVoice extends Protection {
|
export class MessageIsVoice extends Protection {
|
||||||
|
|
||||||
@ -40,7 +39,7 @@ export class MessageIsVoice extends Protection {
|
|||||||
if (event['content']['org.matrix.msc3245.voice'] === undefined) 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.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
|
// Redact the event
|
||||||
if (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
await mjolnir.client.redactEvent(roomId, event['event_id'], "Voice messages are not permitted here");
|
await mjolnir.client.redactEvent(roomId, event['event_id'], "Voice messages are not permitted here");
|
||||||
} else {
|
} 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.logMessage(LogLevel.WARN, "MessageIsVoice", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||||
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import config from "../config";
|
|
||||||
import { Protection } from "./IProtection";
|
import { Protection } from "./IProtection";
|
||||||
import { MXIDListProtectionSetting, NumberProtectionSetting } from "./ProtectionSettings";
|
import { MXIDListProtectionSetting, NumberProtectionSetting } from "./ProtectionSettings";
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
@ -83,7 +82,7 @@ export class TrustedReporters extends Protection {
|
|||||||
|
|
||||||
|
|
||||||
if (met.length > 0) {
|
if (met.length > 0) {
|
||||||
await mjolnir.client.sendMessage(config.managementRoom, {
|
await mjolnir.client.sendMessage(mjolnir.config.managementRoom, {
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: `message ${event.id} reported by ${[...reporters].join(', ')}. `
|
body: `message ${event.id} reported by ${[...reporters].join(', ')}. `
|
||||||
+ `actions: ${met.join(', ')}`
|
+ `actions: ${met.join(', ')}`
|
||||||
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||||||
import { Protection } from "./IProtection";
|
import { Protection } from "./IProtection";
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
import { LogLevel, LogService } from "matrix-bot-sdk";
|
import { LogLevel, LogService } from "matrix-bot-sdk";
|
||||||
import config from "../config";
|
|
||||||
import { isTrueJoinEvent } from "../utils";
|
import { isTrueJoinEvent } from "../utils";
|
||||||
|
|
||||||
export class WordList extends Protection {
|
export class WordList extends Protection {
|
||||||
@ -25,15 +24,10 @@ export class WordList extends Protection {
|
|||||||
settings = {};
|
settings = {};
|
||||||
|
|
||||||
private justJoined: { [roomId: string]: { [username: string]: Date} } = {};
|
private justJoined: { [roomId: string]: { [username: string]: Date} } = {};
|
||||||
private badWords: RegExp;
|
private badWords?: RegExp;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
// Create a mega-regex from all the tiny baby regexs
|
|
||||||
this.badWords = new RegExp(
|
|
||||||
"(" + config.protections.wordlist.words.join(")|(") + ")",
|
|
||||||
"i"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get name(): string {
|
public get name(): string {
|
||||||
@ -47,7 +41,7 @@ export class WordList extends Protection {
|
|||||||
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
|
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
|
||||||
|
|
||||||
const content = event['content'] || {};
|
const content = event['content'] || {};
|
||||||
const minsBeforeTrusting = config.protections.wordlist.minutesBeforeTrusting;
|
const minsBeforeTrusting = mjolnir.config.protections.wordlist.minutesBeforeTrusting;
|
||||||
|
|
||||||
if (minsBeforeTrusting > 0) {
|
if (minsBeforeTrusting > 0) {
|
||||||
if (!this.justJoined[roomId]) this.justJoined[roomId] = {};
|
if (!this.justJoined[roomId]) this.justJoined[roomId] = {};
|
||||||
@ -89,19 +83,29 @@ export class WordList extends Protection {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.badWords === null) {
|
||||||
|
// Create a mega-regex from all the tiny baby regexs
|
||||||
|
try {
|
||||||
|
this.badWords = new RegExp(
|
||||||
|
"(" + mjolnir.config.protections.wordlist.words.join(")|(") + ")",
|
||||||
|
"i"
|
||||||
|
);
|
||||||
|
} catch (ex) {
|
||||||
|
await mjolnir.logMessage(LogLevel.ERROR, "WordList", `Could not produce a regex from the word list:\n${ex}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Perform the test
|
// Perform the test
|
||||||
if (message && this.badWords.test(message)) {
|
if (message && this.badWords!.test(message)) {
|
||||||
await mjolnir.logMessage(LogLevel.WARN, "WordList", `Banning ${event['sender']} for word list violation in ${roomId}.`);
|
await mjolnir.logMessage(LogLevel.WARN, "WordList", `Banning ${event['sender']} for word list violation in ${roomId}.`);
|
||||||
if (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
await mjolnir.client.banUser(event['sender'], roomId, "Word list violation");
|
await mjolnir.client.banUser(event['sender'], roomId, "Word list violation");
|
||||||
} else {
|
} 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.logMessage(LogLevel.WARN, "WordList", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redact the event
|
// Redact the event
|
||||||
if (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
await mjolnir.client.redactEvent(roomId, event['event_id'], "spam");
|
await mjolnir.client.redactEvent(roomId, event['event_id'], "spam");
|
||||||
} else {
|
} 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.logMessage(LogLevel.WARN, "WordList", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
|
||||||
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { extractRequestError, LogLevel, LogService, Permalinks } from "matrix-bot-sdk";
|
import { extractRequestError, LogLevel, LogService, Permalinks } from "matrix-bot-sdk";
|
||||||
import config from "../config";
|
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,7 +42,7 @@ export class UnlistedUserRedactionQueue {
|
|||||||
const permalink = Permalinks.forEvent(roomId, event['event_id']);
|
const permalink = Permalinks.forEvent(roomId, event['event_id']);
|
||||||
try {
|
try {
|
||||||
LogService.info("AutomaticRedactionQueue", `Redacting event because the user is listed as bad: ${permalink}`)
|
LogService.info("AutomaticRedactionQueue", `Redacting event because the user is listed as bad: ${permalink}`)
|
||||||
if (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
await mjolnir.client.redactEvent(roomId, event['event_id']);
|
await mjolnir.client.redactEvent(roomId, event['event_id']);
|
||||||
} else {
|
} else {
|
||||||
await mjolnir.logMessage(LogLevel.WARN, "AutomaticRedactionQueue", `Tried to redact ${permalink} but Mjolnir is running in no-op mode`);
|
await mjolnir.logMessage(LogLevel.WARN, "AutomaticRedactionQueue", `Tried to redact ${permalink} but Mjolnir is running in no-op mode`);
|
||||||
|
@ -20,8 +20,6 @@ import { htmlToText } from "html-to-text";
|
|||||||
import { htmlEscape } from "../utils";
|
import { htmlEscape } from "../utils";
|
||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
import config from "../config";
|
|
||||||
import { Mjolnir } from "../Mjolnir";
|
import { Mjolnir } from "../Mjolnir";
|
||||||
|
|
||||||
/// Regexp, used to extract the action label from an action reaction
|
/// Regexp, used to extract the action label from an action reaction
|
||||||
@ -115,7 +113,7 @@ export class ReportManager extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
public async handleServerAbuseReport({ roomId, reporterId, event, reason }: { roomId: string, reporterId: string, event: any, reason?: string }) {
|
public async handleServerAbuseReport({ roomId, reporterId, event, reason }: { roomId: string, reporterId: string, event: any, reason?: string }) {
|
||||||
this.emit("report.new", { roomId: roomId, reporterId: reporterId, event: event, reason: reason });
|
this.emit("report.new", { roomId: roomId, reporterId: reporterId, event: event, reason: reason });
|
||||||
if (config.displayReports) {
|
if (this.mjolnir.config.displayReports) {
|
||||||
return this.displayManager.displayReportAndUI({ kind: Kind.SERVER_ABUSE_REPORT, event, reporterId, reason, moderationRoomId: this.mjolnir.managementRoomId });
|
return this.displayManager.displayReportAndUI({ kind: Kind.SERVER_ABUSE_REPORT, event, reporterId, reason, moderationRoomId: this.mjolnir.managementRoomId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,6 @@ import {
|
|||||||
setRequestFn,
|
setRequestFn,
|
||||||
} from "matrix-bot-sdk";
|
} from "matrix-bot-sdk";
|
||||||
import { Mjolnir } from "./Mjolnir";
|
import { Mjolnir } from "./Mjolnir";
|
||||||
import config from "./config";
|
|
||||||
import { ClientRequest, IncomingMessage } from "http";
|
import { ClientRequest, IncomingMessage } from "http";
|
||||||
import { default as parseDuration } from "parse-duration";
|
import { default as parseDuration } from "parse-duration";
|
||||||
|
|
||||||
@ -78,7 +77,7 @@ export async function redactUserMessagesIn(mjolnir: Mjolnir, userIdOrGlob: strin
|
|||||||
await getMessagesByUserIn(mjolnir.client, userIdOrGlob, targetRoomId, limit, async (eventsToRedact) => {
|
await getMessagesByUserIn(mjolnir.client, userIdOrGlob, targetRoomId, limit, async (eventsToRedact) => {
|
||||||
for (const victimEvent of eventsToRedact) {
|
for (const victimEvent of eventsToRedact) {
|
||||||
await mjolnir.logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Redacting ${victimEvent['event_id']} in ${targetRoomId}`, targetRoomId);
|
await mjolnir.logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Redacting ${victimEvent['event_id']} in ${targetRoomId}`, targetRoomId);
|
||||||
if (!config.noop) {
|
if (!mjolnir.config.noop) {
|
||||||
await mjolnir.client.redactEvent(targetRoomId, victimEvent['event_id']);
|
await mjolnir.client.redactEvent(targetRoomId, victimEvent['event_id']);
|
||||||
} else {
|
} else {
|
||||||
await mjolnir.logMessage(LogLevel.WARN, "utils#redactUserMessagesIn", `Tried to redact ${victimEvent['event_id']} in ${targetRoomId} but Mjolnir is running in no-op mode`, targetRoomId);
|
await mjolnir.logMessage(LogLevel.WARN, "utils#redactUserMessagesIn", `Tried to redact ${victimEvent['event_id']} in ${targetRoomId} but Mjolnir is running in no-op mode`, targetRoomId);
|
||||||
|
@ -15,13 +15,11 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Server } from "http";
|
import { Server } from "http";
|
||||||
|
|
||||||
import * as express from "express";
|
import * as express from "express";
|
||||||
import { LogService, MatrixClient } from "matrix-bot-sdk";
|
import { LogService, MatrixClient } from "matrix-bot-sdk";
|
||||||
|
|
||||||
import config from "../config";
|
|
||||||
import RuleServer from "../models/RuleServer";
|
import RuleServer from "../models/RuleServer";
|
||||||
import { ReportManager } from "../report/ReportManager";
|
import { ReportManager } from "../report/ReportManager";
|
||||||
|
import { IConfig } from "../config";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,7 +33,7 @@ export class WebAPIs {
|
|||||||
private webController: express.Express = express();
|
private webController: express.Express = express();
|
||||||
private httpServer?: Server;
|
private httpServer?: Server;
|
||||||
|
|
||||||
constructor(private reportManager: ReportManager, private readonly ruleServer: RuleServer|null) {
|
constructor(private reportManager: ReportManager, private readonly config: IConfig, private readonly ruleServer: RuleServer|null) {
|
||||||
// Setup JSON parsing.
|
// Setup JSON parsing.
|
||||||
this.webController.use(express.json());
|
this.webController.use(express.json());
|
||||||
}
|
}
|
||||||
@ -44,14 +42,14 @@ export class WebAPIs {
|
|||||||
* Start accepting requests to the Web API.
|
* Start accepting requests to the Web API.
|
||||||
*/
|
*/
|
||||||
public async start() {
|
public async start() {
|
||||||
if (!config.web.enabled) {
|
if (!this.config.web.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.httpServer = this.webController.listen(config.web.port, config.web.address);
|
this.httpServer = this.webController.listen(this.config.web.port, this.config.web.address);
|
||||||
|
|
||||||
// Configure /report API.
|
// configure /report API.
|
||||||
if (config.web.abuseReporting.enabled) {
|
if (this.config.web.abuseReporting.enabled) {
|
||||||
console.log(`Configuring ${API_PREFIX}/report/:room_id/:event_id...`);
|
console.log(`configuring ${API_PREFIX}/report/:room_id/:event_id...`);
|
||||||
this.webController.options(`${API_PREFIX}/report/:room_id/:event_id`, async (request, response) => {
|
this.webController.options(`${API_PREFIX}/report/:room_id/:event_id`, async (request, response) => {
|
||||||
// reply with CORS options
|
// reply with CORS options
|
||||||
response.header("Access-Control-Allow-Origin", "*");
|
response.header("Access-Control-Allow-Origin", "*");
|
||||||
@ -68,15 +66,15 @@ export class WebAPIs {
|
|||||||
response.header("Access-Control-Allow-Methods", "POST, OPTIONS");
|
response.header("Access-Control-Allow-Methods", "POST, OPTIONS");
|
||||||
await this.handleReport({ request, response, roomId: request.params.room_id, eventId: request.params.event_id })
|
await this.handleReport({ request, response, roomId: request.params.room_id, eventId: request.params.event_id })
|
||||||
});
|
});
|
||||||
console.log(`Configuring ${API_PREFIX}/report/:room_id/:event_id... DONE`);
|
console.log(`configuring ${API_PREFIX}/report/:room_id/:event_id... DONE`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure ruleServer API.
|
// configure ruleServer API.
|
||||||
// FIXME: Doesn't this need some kind of access control?
|
// FIXME: Doesn't this need some kind of access control?
|
||||||
// See https://github.com/matrix-org/mjolnir/issues/139#issuecomment-1012221479.
|
// See https://github.com/matrix-org/mjolnir/issues/139#issuecomment-1012221479.
|
||||||
if (config.web.ruleServer?.enabled) {
|
if (this.config.web.ruleServer?.enabled) {
|
||||||
const updatesUrl = `${API_PREFIX}/ruleserver/updates`;
|
const updatesUrl = `${API_PREFIX}/ruleserver/updates`;
|
||||||
LogService.info("WebAPIs", `Configuring ${updatesUrl}...`);
|
LogService.info("WebAPIs", `configuring ${updatesUrl}...`);
|
||||||
if (!this.ruleServer) {
|
if (!this.ruleServer) {
|
||||||
throw new Error("The rule server to use has not been configured for the WebAPIs.");
|
throw new Error("The rule server to use has not been configured for the WebAPIs.");
|
||||||
}
|
}
|
||||||
@ -84,7 +82,7 @@ export class WebAPIs {
|
|||||||
this.webController.get(updatesUrl, async (request, response) => {
|
this.webController.get(updatesUrl, async (request, response) => {
|
||||||
await this.handleRuleServerUpdate(ruleServer, { request, response, since: request.query.since as string});
|
await this.handleRuleServerUpdate(ruleServer, { request, response, since: request.query.since as string});
|
||||||
});
|
});
|
||||||
LogService.info("WebAPIs", `Configuring ${updatesUrl}... DONE`);
|
LogService.info("WebAPIs", `configuring ${updatesUrl}... DONE`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +161,7 @@ export class WebAPIs {
|
|||||||
// so we are not extending the abilities of Mjölnir
|
// so we are not extending the abilities of Mjölnir
|
||||||
// 3. We are avoiding the use of the Synapse Admin API to ensure that
|
// 3. We are avoiding the use of the Synapse Admin API to ensure that
|
||||||
// this feature can work with all homeservers, not just Synapse.
|
// this feature can work with all homeservers, not just Synapse.
|
||||||
let reporterClient = new MatrixClient(config.rawHomeserverUrl, accessToken);
|
let reporterClient = new MatrixClient(this.config.rawHomeserverUrl, accessToken);
|
||||||
reporterClient.start = () => {
|
reporterClient.start = () => {
|
||||||
throw new Error("We MUST NEVER call start on the reporter client");
|
throw new Error("We MUST NEVER call start on the reporter client");
|
||||||
};
|
};
|
||||||
|
@ -18,18 +18,23 @@ import * as expect from "expect";
|
|||||||
import { Mjolnir } from "../../src/Mjolnir";
|
import { Mjolnir } from "../../src/Mjolnir";
|
||||||
import { DEFAULT_LIST_EVENT_TYPE } from "../../src/commands/SetDefaultBanListCommand";
|
import { DEFAULT_LIST_EVENT_TYPE } from "../../src/commands/SetDefaultBanListCommand";
|
||||||
import { parseArguments } from "../../src/commands/UnbanBanCommand";
|
import { parseArguments } from "../../src/commands/UnbanBanCommand";
|
||||||
import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../../src/models/BanList";
|
import config from "../../src/config";
|
||||||
|
import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../../src/models/ListRule";
|
||||||
|
|
||||||
function createTestMjolnir(defaultShortcode: string = null): Mjolnir {
|
function createTestMjolnir(defaultShortcode: string|null = null): Mjolnir {
|
||||||
const client = {
|
const client = {
|
||||||
|
// Mock `MatrixClient.getAccountData` .
|
||||||
getAccountData: (eventType: string): Promise<any> => {
|
getAccountData: (eventType: string): Promise<any> => {
|
||||||
if (eventType === DEFAULT_LIST_EVENT_TYPE && defaultShortcode) {
|
if (eventType === DEFAULT_LIST_EVENT_TYPE || defaultShortcode) {
|
||||||
return Promise.resolve({shortcode: defaultShortcode});
|
return Promise.resolve({shortcode: defaultShortcode});
|
||||||
}
|
}
|
||||||
throw new Error("Unknown event type");
|
throw new Error(`Unknown event type ${eventType}, expected ${DEFAULT_LIST_EVENT_TYPE}`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return <Mjolnir>{client};
|
return <Mjolnir>{
|
||||||
|
client,
|
||||||
|
config,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFakeEvent(command: string): any {
|
function createFakeEvent(command: string): any {
|
||||||
@ -55,11 +60,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test example.org";
|
const command = "!mjolnir ban test example.org";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_SERVER);
|
expect(bits!.ruleType).toBe(RULE_SERVER);
|
||||||
expect(bits.entity).toBe("example.org");
|
expect(bits!.entity).toBe("example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to detect servers with ban reasons", async () => {
|
it("should be able to detect servers with ban reasons", async () => {
|
||||||
@ -72,11 +77,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test example.org reason here";
|
const command = "!mjolnir ban test example.org reason here";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBe("reason here");
|
expect(bits!.reason).toBe("reason here");
|
||||||
expect(bits.ruleType).toBe(RULE_SERVER);
|
expect(bits!.ruleType).toBe(RULE_SERVER);
|
||||||
expect(bits.entity).toBe("example.org");
|
expect(bits!.entity).toBe("example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to detect servers with globs", async () => {
|
it("should be able to detect servers with globs", async () => {
|
||||||
@ -89,11 +94,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test *.example.org --force";
|
const command = "!mjolnir ban test *.example.org --force";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_SERVER);
|
expect(bits!.ruleType).toBe(RULE_SERVER);
|
||||||
expect(bits.entity).toBe("*.example.org");
|
expect(bits!.entity).toBe("*.example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to detect servers with the type specified", async () => {
|
it("should be able to detect servers with the type specified", async () => {
|
||||||
@ -106,11 +111,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test server @*.example.org --force";
|
const command = "!mjolnir ban test server @*.example.org --force";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_SERVER);
|
expect(bits!.ruleType).toBe(RULE_SERVER);
|
||||||
expect(bits.entity).toBe("@*.example.org");
|
expect(bits!.entity).toBe("@*.example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to detect room IDs", async () => {
|
it("should be able to detect room IDs", async () => {
|
||||||
@ -123,11 +128,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test !example.org";
|
const command = "!mjolnir ban test !example.org";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_ROOM);
|
expect(bits!.ruleType).toBe(RULE_ROOM);
|
||||||
expect(bits.entity).toBe("!example.org");
|
expect(bits!.entity).toBe("!example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to detect room IDs with ban reasons", async () => {
|
it("should be able to detect room IDs with ban reasons", async () => {
|
||||||
@ -140,11 +145,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test !example.org reason here";
|
const command = "!mjolnir ban test !example.org reason here";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBe("reason here");
|
expect(bits!.reason).toBe("reason here");
|
||||||
expect(bits.ruleType).toBe(RULE_ROOM);
|
expect(bits!.ruleType).toBe(RULE_ROOM);
|
||||||
expect(bits.entity).toBe("!example.org");
|
expect(bits!.entity).toBe("!example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to detect room IDs with globs", async () => {
|
it("should be able to detect room IDs with globs", async () => {
|
||||||
@ -157,11 +162,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test !*.example.org --force";
|
const command = "!mjolnir ban test !*.example.org --force";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_ROOM);
|
expect(bits!.ruleType).toBe(RULE_ROOM);
|
||||||
expect(bits.entity).toBe("!*.example.org");
|
expect(bits!.entity).toBe("!*.example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to detect room aliases", async () => {
|
it("should be able to detect room aliases", async () => {
|
||||||
@ -174,11 +179,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test #example.org";
|
const command = "!mjolnir ban test #example.org";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_ROOM);
|
expect(bits!.ruleType).toBe(RULE_ROOM);
|
||||||
expect(bits.entity).toBe("#example.org");
|
expect(bits!.entity).toBe("#example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to detect room aliases with ban reasons", async () => {
|
it("should be able to detect room aliases with ban reasons", async () => {
|
||||||
@ -191,11 +196,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test #example.org reason here";
|
const command = "!mjolnir ban test #example.org reason here";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBe("reason here");
|
expect(bits!.reason).toBe("reason here");
|
||||||
expect(bits.ruleType).toBe(RULE_ROOM);
|
expect(bits!.ruleType).toBe(RULE_ROOM);
|
||||||
expect(bits.entity).toBe("#example.org");
|
expect(bits!.entity).toBe("#example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to detect room aliases with globs", async () => {
|
it("should be able to detect room aliases with globs", async () => {
|
||||||
@ -208,11 +213,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test #*.example.org --force";
|
const command = "!mjolnir ban test #*.example.org --force";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_ROOM);
|
expect(bits!.ruleType).toBe(RULE_ROOM);
|
||||||
expect(bits.entity).toBe("#*.example.org");
|
expect(bits!.entity).toBe("#*.example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to detect rooms with the type specified", async () => {
|
it("should be able to detect rooms with the type specified", async () => {
|
||||||
@ -225,11 +230,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test room @*.example.org --force";
|
const command = "!mjolnir ban test room @*.example.org --force";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_ROOM);
|
expect(bits!.ruleType).toBe(RULE_ROOM);
|
||||||
expect(bits.entity).toBe("@*.example.org");
|
expect(bits!.entity).toBe("@*.example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to detect user IDs", async () => {
|
it("should be able to detect user IDs", async () => {
|
||||||
@ -242,11 +247,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test @example.org";
|
const command = "!mjolnir ban test @example.org";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_USER);
|
expect(bits!.ruleType).toBe(RULE_USER);
|
||||||
expect(bits.entity).toBe("@example.org");
|
expect(bits!.entity).toBe("@example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to detect user IDs with ban reasons", async () => {
|
it("should be able to detect user IDs with ban reasons", async () => {
|
||||||
@ -259,11 +264,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test @example.org reason here";
|
const command = "!mjolnir ban test @example.org reason here";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBe("reason here");
|
expect(bits!.reason).toBe("reason here");
|
||||||
expect(bits.ruleType).toBe(RULE_USER);
|
expect(bits!.ruleType).toBe(RULE_USER);
|
||||||
expect(bits.entity).toBe("@example.org");
|
expect(bits!.entity).toBe("@example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to detect user IDs with globs", async () => {
|
it("should be able to detect user IDs with globs", async () => {
|
||||||
@ -276,11 +281,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test @*.example.org --force";
|
const command = "!mjolnir ban test @*.example.org --force";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_USER);
|
expect(bits!.ruleType).toBe(RULE_USER);
|
||||||
expect(bits.entity).toBe("@*.example.org");
|
expect(bits!.entity).toBe("@*.example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to detect user IDs with the type specified", async () => {
|
it("should be able to detect user IDs with the type specified", async () => {
|
||||||
@ -293,11 +298,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test user #*.example.org --force";
|
const command = "!mjolnir ban test user #*.example.org --force";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_USER);
|
expect(bits!.ruleType).toBe(RULE_USER);
|
||||||
expect(bits.entity).toBe("#*.example.org");
|
expect(bits!.entity).toBe("#*.example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should error if wildcards used without --force", async () => {
|
it("should error if wildcards used without --force", async () => {
|
||||||
@ -324,11 +329,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test user #*.example.org reason here --force";
|
const command = "!mjolnir ban test user #*.example.org reason here --force";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBe("reason here");
|
expect(bits!.reason).toBe("reason here");
|
||||||
expect(bits.ruleType).toBe(RULE_USER);
|
expect(bits!.ruleType).toBe(RULE_USER);
|
||||||
expect(bits.entity).toBe("#*.example.org");
|
expect(bits!.entity).toBe("#*.example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("[without default list]", () => {
|
describe("[without default list]", () => {
|
||||||
@ -370,11 +375,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban user test @example:example.org";
|
const command = "!mjolnir ban user test @example:example.org";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_USER);
|
expect(bits!.ruleType).toBe(RULE_USER);
|
||||||
expect(bits.entity).toBe("@example:example.org");
|
expect(bits!.entity).toBe("@example:example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not error if a list (without type) is specified", async () => {
|
it("should not error if a list (without type) is specified", async () => {
|
||||||
@ -387,11 +392,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test @example:example.org";
|
const command = "!mjolnir ban test @example:example.org";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_USER);
|
expect(bits!.ruleType).toBe(RULE_USER);
|
||||||
expect(bits.entity).toBe("@example:example.org");
|
expect(bits!.entity).toBe("@example:example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not error if a list (with type reversed) is specified", async () => {
|
it("should not error if a list (with type reversed) is specified", async () => {
|
||||||
@ -404,11 +409,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban test user @example:example.org";
|
const command = "!mjolnir ban test user @example:example.org";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_USER);
|
expect(bits!.ruleType).toBe(RULE_USER);
|
||||||
expect(bits.entity).toBe("@example:example.org");
|
expect(bits!.entity).toBe("@example:example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -423,11 +428,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban user @example:example.org";
|
const command = "!mjolnir ban user @example:example.org";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_USER);
|
expect(bits!.ruleType).toBe(RULE_USER);
|
||||||
expect(bits.entity).toBe("@example:example.org");
|
expect(bits!.entity).toBe("@example:example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should use the default list if no list (without type) is specified", async () => {
|
it("should use the default list if no list (without type) is specified", async () => {
|
||||||
@ -440,11 +445,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban @example:example.org";
|
const command = "!mjolnir ban @example:example.org";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_USER);
|
expect(bits!.ruleType).toBe(RULE_USER);
|
||||||
expect(bits.entity).toBe("@example:example.org");
|
expect(bits!.entity).toBe("@example:example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("test");
|
expect(bits!.list!.listShortcode).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should use the specified list if a list (with type) is specified", async () => {
|
it("should use the specified list if a list (with type) is specified", async () => {
|
||||||
@ -457,11 +462,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban user other @example:example.org";
|
const command = "!mjolnir ban user other @example:example.org";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_USER);
|
expect(bits!.ruleType).toBe(RULE_USER);
|
||||||
expect(bits.entity).toBe("@example:example.org");
|
expect(bits!.entity).toBe("@example:example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("other");
|
expect(bits!.list!.listShortcode).toBe("other");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should use the specified list if a list (without type) is specified", async () => {
|
it("should use the specified list if a list (without type) is specified", async () => {
|
||||||
@ -474,11 +479,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban other @example:example.org";
|
const command = "!mjolnir ban other @example:example.org";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_USER);
|
expect(bits!.ruleType).toBe(RULE_USER);
|
||||||
expect(bits.entity).toBe("@example:example.org");
|
expect(bits!.entity).toBe("@example:example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("other");
|
expect(bits!.list!.listShortcode).toBe("other");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not error if a list (with type reversed) is specified", async () => {
|
it("should not error if a list (with type reversed) is specified", async () => {
|
||||||
@ -491,11 +496,11 @@ describe("UnbanBanCommand", () => {
|
|||||||
const command = "!mjolnir ban other user @example:example.org";
|
const command = "!mjolnir ban other user @example:example.org";
|
||||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||||
expect(bits).toBeTruthy();
|
expect(bits).toBeTruthy();
|
||||||
expect(bits.reason).toBeFalsy();
|
expect(bits!.reason).toBeFalsy();
|
||||||
expect(bits.ruleType).toBe(RULE_USER);
|
expect(bits!.ruleType).toBe(RULE_USER);
|
||||||
expect(bits.entity).toBe("@example:example.org");
|
expect(bits!.entity).toBe("@example:example.org");
|
||||||
expect(bits.list).toBeDefined();
|
expect(bits!.list).toBeDefined();
|
||||||
expect(bits.list.listShortcode).toBe("other");
|
expect(bits!.list!.listShortcode).toBe("other");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { strict as assert } from "assert";
|
import { strict as assert } from "assert";
|
||||||
|
|
||||||
import config from "../../src/config";
|
|
||||||
import { newTestUser } from "./clientHelper";
|
import { newTestUser } from "./clientHelper";
|
||||||
import { LogService, MatrixClient, Permalinks, UserID } from "matrix-bot-sdk";
|
import { LogService, MatrixClient, Permalinks, UserID } from "matrix-bot-sdk";
|
||||||
import BanList, { ALL_RULE_TYPES, ChangeType, ListRuleChange, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES } from "../../src/models/BanList";
|
import PolicyList, { ChangeType, ListRuleChange } from "../../src/models/PolicyList";
|
||||||
import { ServerAcl } from "../../src/models/ServerAcl";
|
import { ServerAcl } from "../../src/models/ServerAcl";
|
||||||
import { getFirstReaction } from "./commands/commandUtils";
|
import { getFirstReaction } from "./commands/commandUtils";
|
||||||
import { getMessagesByUserIn } from "../../src/utils";
|
import { getMessagesByUserIn } from "../../src/utils";
|
||||||
|
import { Mjolnir } from "../../src/Mjolnir";
|
||||||
|
import { ALL_RULE_TYPES, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES } from "../../src/models/ListRule";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a policy rule in a policy room.
|
* Create a policy rule in a policy room.
|
||||||
@ -26,36 +26,36 @@ async function createPolicyRule(client: MatrixClient, policyRoomId: string, poli
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Test: Updating the BanList", function () {
|
describe("Test: Updating the PolicyList", function() {
|
||||||
it("Calculates what has changed correctly.", async function() {
|
it("Calculates what has changed correctly.", async function() {
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir: Mjolnir = this.mjolnir!
|
||||||
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||||
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
|
const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] });
|
||||||
const banList = new BanList(banListId, banListId, mjolnir);
|
const banList = new PolicyList(banListId, banListId, mjolnir.client);
|
||||||
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
||||||
|
|
||||||
assert.equal(banList.allRules.length, 0);
|
assert.equal(banList.allRules.length, 0);
|
||||||
|
|
||||||
// Test adding a new rule
|
// Test adding a new rule
|
||||||
await createPolicyRule(mjolnir, banListId, RULE_USER, '@added:localhost:9999', '');
|
await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@added:localhost:9999', '');
|
||||||
let changes: ListRuleChange[] = await banList.updateList();
|
let changes: ListRuleChange[] = await banList.updateList();
|
||||||
assert.equal(changes.length, 1, 'There should only be one change');
|
assert.equal(changes.length, 1, 'There should only be one change');
|
||||||
assert.equal(changes[0].changeType, ChangeType.Added);
|
assert.equal(changes[0].changeType, ChangeType.Added);
|
||||||
assert.equal(changes[0].sender, await mjolnir.getUserId());
|
assert.equal(changes[0].sender, await mjolnir.client.getUserId());
|
||||||
assert.equal(banList.userRules.length, 1);
|
assert.equal(banList.userRules.length, 1);
|
||||||
assert.equal(banList.allRules.length, 1);
|
assert.equal(banList.allRules.length, 1);
|
||||||
|
|
||||||
// Test modifiying a rule
|
// Test modifiying a rule
|
||||||
let originalEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@modified:localhost:9999', '');
|
let originalEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@modified:localhost:9999', '');
|
||||||
await banList.updateList();
|
await banList.updateList();
|
||||||
let modifyingEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@modified:localhost:9999', 'modified reason');
|
let modifyingEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@modified:localhost:9999', 'modified reason');
|
||||||
changes = await banList.updateList();
|
changes = await banList.updateList();
|
||||||
assert.equal(changes.length, 1);
|
assert.equal(changes.length, 1);
|
||||||
assert.equal(changes[0].changeType, ChangeType.Modified);
|
assert.equal(changes[0].changeType, ChangeType.Modified);
|
||||||
assert.equal(changes[0].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule');
|
assert.equal(changes[0].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule');
|
||||||
assert.equal(changes[0].event['event_id'], modifyingEventId);
|
assert.equal(changes[0].event['event_id'], modifyingEventId);
|
||||||
let modifyingAgainEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@modified:localhost:9999', 'modified again');
|
let modifyingAgainEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@modified:localhost:9999', 'modified again');
|
||||||
changes = await banList.updateList();
|
changes = await banList.updateList();
|
||||||
assert.equal(changes.length, 1);
|
assert.equal(changes.length, 1);
|
||||||
assert.equal(changes[0].changeType, ChangeType.Modified);
|
assert.equal(changes[0].changeType, ChangeType.Modified);
|
||||||
@ -64,10 +64,10 @@ describe("Test: Updating the BanList", function () {
|
|||||||
assert.equal(banList.userRules.length, 2, 'There should be two rules, one for @modified:localhost:9999 and one for @added:localhost:9999');
|
assert.equal(banList.userRules.length, 2, 'There should be two rules, one for @modified:localhost:9999 and one for @added:localhost:9999');
|
||||||
|
|
||||||
// Test redacting a rule
|
// Test redacting a rule
|
||||||
const redactThis = await createPolicyRule(mjolnir, banListId, RULE_USER, '@redacted:localhost:9999', '');
|
const redactThis = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@redacted:localhost:9999', '');
|
||||||
await banList.updateList();
|
await banList.updateList();
|
||||||
assert.equal(banList.userRules.filter(r => r.entity === '@redacted:localhost:9999').length, 1);
|
assert.equal(banList.userRules.filter(r => r.entity === '@redacted:localhost:9999').length, 1);
|
||||||
await mjolnir.redactEvent(banListId, redactThis);
|
await mjolnir.client.redactEvent(banListId, redactThis);
|
||||||
changes = await banList.updateList();
|
changes = await banList.updateList();
|
||||||
assert.equal(changes.length, 1);
|
assert.equal(changes.length, 1);
|
||||||
assert.equal(changes[0].changeType, ChangeType.Removed);
|
assert.equal(changes[0].changeType, ChangeType.Removed);
|
||||||
@ -79,10 +79,10 @@ describe("Test: Updating the BanList", function () {
|
|||||||
|
|
||||||
// Test soft redaction of a rule
|
// Test soft redaction of a rule
|
||||||
const softRedactedEntity = '@softredacted:localhost:9999'
|
const softRedactedEntity = '@softredacted:localhost:9999'
|
||||||
await createPolicyRule(mjolnir, banListId, RULE_USER, softRedactedEntity, '');
|
await createPolicyRule(mjolnir.client, banListId, RULE_USER, softRedactedEntity, '');
|
||||||
await banList.updateList();
|
await banList.updateList();
|
||||||
assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 1);
|
assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 1);
|
||||||
await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {});
|
await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {});
|
||||||
changes = await banList.updateList();
|
changes = await banList.updateList();
|
||||||
assert.equal(changes.length, 1);
|
assert.equal(changes.length, 1);
|
||||||
assert.equal(changes[0].changeType, ChangeType.Removed);
|
assert.equal(changes[0].changeType, ChangeType.Removed);
|
||||||
@ -92,25 +92,25 @@ describe("Test: Updating the BanList", function () {
|
|||||||
assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 0, 'The rule should have been removed');
|
assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 0, 'The rule should have been removed');
|
||||||
|
|
||||||
// Now test a double soft redaction just to make sure stuff doesn't explode
|
// Now test a double soft redaction just to make sure stuff doesn't explode
|
||||||
await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {});
|
await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {});
|
||||||
changes = await banList.updateList();
|
changes = await banList.updateList();
|
||||||
assert.equal(changes.length, 0, "It shouldn't detect a double soft redaction as a change, it should be seen as adding an invalid rule.");
|
assert.equal(changes.length, 0, "It shouldn't detect a double soft redaction as a change, it should be seen as adding an invalid rule.");
|
||||||
assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 0, 'The rule should have been removed');
|
assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 0, 'The rule should have been removed');
|
||||||
|
|
||||||
// Test that different (old) rule types will be modelled as the latest event type.
|
// Test that different (old) rule types will be modelled as the latest event type.
|
||||||
originalEventId = await createPolicyRule(mjolnir, banListId, 'org.matrix.mjolnir.rule.user', '@old:localhost:9999', '');
|
originalEventId = await createPolicyRule(mjolnir.client, banListId, 'org.matrix.mjolnir.rule.user', '@old:localhost:9999', '');
|
||||||
changes = await banList.updateList();
|
changes = await banList.updateList();
|
||||||
assert.equal(changes.length, 1);
|
assert.equal(changes.length, 1);
|
||||||
assert.equal(changes[0].changeType, ChangeType.Added);
|
assert.equal(changes[0].changeType, ChangeType.Added);
|
||||||
assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1);
|
assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1);
|
||||||
modifyingEventId = await createPolicyRule(mjolnir, banListId, 'm.room.rule.user', '@old:localhost:9999', 'modified reason');
|
modifyingEventId = await createPolicyRule(mjolnir.client, banListId, 'm.room.rule.user', '@old:localhost:9999', 'modified reason');
|
||||||
changes = await banList.updateList();
|
changes = await banList.updateList();
|
||||||
assert.equal(changes.length, 1);
|
assert.equal(changes.length, 1);
|
||||||
assert.equal(changes[0].changeType, ChangeType.Modified);
|
assert.equal(changes[0].changeType, ChangeType.Modified);
|
||||||
assert.equal(changes[0].event['event_id'], modifyingEventId);
|
assert.equal(changes[0].event['event_id'], modifyingEventId);
|
||||||
assert.equal(changes[0].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule');
|
assert.equal(changes[0].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule');
|
||||||
assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1);
|
assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1);
|
||||||
modifyingAgainEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@old:localhost:9999', 'changes again');
|
modifyingAgainEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@old:localhost:9999', 'changes again');
|
||||||
changes = await banList.updateList();
|
changes = await banList.updateList();
|
||||||
assert.equal(changes.length, 1);
|
assert.equal(changes.length, 1);
|
||||||
assert.equal(changes[0].changeType, ChangeType.Modified);
|
assert.equal(changes[0].changeType, ChangeType.Modified);
|
||||||
@ -120,19 +120,19 @@ describe("Test: Updating the BanList", function () {
|
|||||||
})
|
})
|
||||||
it("Will remove rules with old types when they are 'soft redacted' with a different but more recent event type.", async function() {
|
it("Will remove rules with old types when they are 'soft redacted' with a different but more recent event type.", async function() {
|
||||||
this.timeout(3000);
|
this.timeout(3000);
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir: Mjolnir = this.mjolnir!
|
||||||
const moderator = await newTestUser({ name: { contains: "moderator" }} );
|
const moderator = await newTestUser({ name: { contains: "moderator" }} );
|
||||||
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
|
const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] });
|
||||||
const banList = new BanList(banListId, banListId, mjolnir);
|
const banList = new PolicyList(banListId, banListId, mjolnir.client);
|
||||||
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
||||||
|
|
||||||
const entity = '@old:localhost:9999';
|
const entity = '@old:localhost:9999';
|
||||||
let originalEventId = await createPolicyRule(mjolnir, banListId, 'm.room.rule.user', entity, '');
|
let originalEventId = await createPolicyRule(mjolnir.client, banListId, 'm.room.rule.user', entity, '');
|
||||||
let changes = await banList.updateList();
|
let changes = await banList.updateList();
|
||||||
assert.equal(changes.length, 1);
|
assert.equal(changes.length, 1);
|
||||||
assert.equal(changes[0].changeType, ChangeType.Added);
|
assert.equal(changes[0].changeType, ChangeType.Added);
|
||||||
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'There should be a rule stored that we just added...')
|
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'There should be a rule stored that we just added...')
|
||||||
let softRedactingEventId = await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {});
|
let softRedactingEventId = await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {});
|
||||||
changes = await banList.updateList();
|
changes = await banList.updateList();
|
||||||
assert.equal(changes.length, 1);
|
assert.equal(changes.length, 1);
|
||||||
assert.equal(changes[0].changeType, ChangeType.Removed);
|
assert.equal(changes[0].changeType, ChangeType.Removed);
|
||||||
@ -142,19 +142,19 @@ describe("Test: Updating the BanList", function () {
|
|||||||
})
|
})
|
||||||
it("A rule of the most recent type won't be deleted when an old rule is deleted for the same entity.", async function() {
|
it("A rule of the most recent type won't be deleted when an old rule is deleted for the same entity.", async function() {
|
||||||
this.timeout(3000);
|
this.timeout(3000);
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir: Mjolnir = this.mjolnir!
|
||||||
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||||
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
|
const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] });
|
||||||
const banList = new BanList(banListId, banListId, mjolnir);
|
const banList = new PolicyList(banListId, banListId, mjolnir.client);
|
||||||
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
||||||
|
|
||||||
const entity = '@old:localhost:9999';
|
const entity = '@old:localhost:9999';
|
||||||
let originalEventId = await createPolicyRule(mjolnir, banListId, 'm.room.rule.user', entity, '');
|
let originalEventId = await createPolicyRule(mjolnir.client, banListId, 'm.room.rule.user', entity, '');
|
||||||
let changes = await banList.updateList();
|
let changes = await banList.updateList();
|
||||||
assert.equal(changes.length, 1);
|
assert.equal(changes.length, 1);
|
||||||
assert.equal(changes[0].changeType, ChangeType.Added);
|
assert.equal(changes[0].changeType, ChangeType.Added);
|
||||||
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'There should be a rule stored that we just added...')
|
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'There should be a rule stored that we just added...')
|
||||||
let updatedEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, entity, '');
|
let updatedEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, entity, '');
|
||||||
changes = await banList.updateList();
|
changes = await banList.updateList();
|
||||||
// If in the future you change this and it fails, it's really subjective whether this constitutes a modification, since the only thing that has changed
|
// If in the future you change this and it fails, it's really subjective whether this constitutes a modification, since the only thing that has changed
|
||||||
// is the rule type. The actual content is identical.
|
// is the rule type. The actual content is identical.
|
||||||
@ -165,13 +165,13 @@ describe("Test: Updating the BanList", function () {
|
|||||||
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'Only the latest version of the rule gets returned.');
|
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'Only the latest version of the rule gets returned.');
|
||||||
|
|
||||||
// Now we delete the old version of the rule without consequence.
|
// Now we delete the old version of the rule without consequence.
|
||||||
await mjolnir.sendStateEvent(banListId, 'm.room.rule.user', `rule:${entity}`, {});
|
await mjolnir.client.sendStateEvent(banListId, 'm.room.rule.user', `rule:${entity}`, {});
|
||||||
changes = await banList.updateList();
|
changes = await banList.updateList();
|
||||||
assert.equal(changes.length, 0);
|
assert.equal(changes.length, 0);
|
||||||
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'The rule should still be active.');
|
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'The rule should still be active.');
|
||||||
|
|
||||||
// And we can still delete the new version of the rule.
|
// And we can still delete the new version of the rule.
|
||||||
let softRedactingEventId = await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {});
|
let softRedactingEventId = await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {});
|
||||||
changes = await banList.updateList();
|
changes = await banList.updateList();
|
||||||
assert.equal(changes.length, 1);
|
assert.equal(changes.length, 1);
|
||||||
assert.equal(changes[0].changeType, ChangeType.Removed);
|
assert.equal(changes[0].changeType, ChangeType.Removed);
|
||||||
@ -179,12 +179,12 @@ describe("Test: Updating the BanList", function () {
|
|||||||
assert.equal(changes[0].previousState['event_id'], updatedEventId, 'There should be a previous state event for a modified rule');
|
assert.equal(changes[0].previousState['event_id'], updatedEventId, 'There should be a previous state event for a modified rule');
|
||||||
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 0, 'The rule should no longer be stored.');
|
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 0, 'The rule should no longer be stored.');
|
||||||
})
|
})
|
||||||
it('Test: BanList Supports all entity types.', async function () {
|
it('Test: PolicyList Supports all entity types.', async function () {
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir: Mjolnir = this.mjolnir!
|
||||||
const banListId = await mjolnir.createRoom();
|
const banListId = await mjolnir.client.createRoom();
|
||||||
const banList = new BanList(banListId, banListId, mjolnir);
|
const banList = new PolicyList(banListId, banListId, mjolnir.client);
|
||||||
for (let i = 0; i < ALL_RULE_TYPES.length; i++) {
|
for (let i = 0; i < ALL_RULE_TYPES.length; i++) {
|
||||||
await createPolicyRule(mjolnir, banListId, ALL_RULE_TYPES[i], `*${i}*`, '');
|
await createPolicyRule(mjolnir.client, banListId, ALL_RULE_TYPES[i], `*${i}*`, '');
|
||||||
}
|
}
|
||||||
let changes: ListRuleChange[] = await banList.updateList();
|
let changes: ListRuleChange[] = await banList.updateList();
|
||||||
assert.equal(changes.length, ALL_RULE_TYPES.length);
|
assert.equal(changes.length, ALL_RULE_TYPES.length);
|
||||||
@ -192,31 +192,31 @@ describe("Test: Updating the BanList", function () {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Test: We do not respond to recommendations other than m.ban in the banlist', function () {
|
describe('Test: We do not respond to recommendations other than m.ban in the PolicyList', function() {
|
||||||
it('Will not respond to a rule that has a different recommendation to m.ban (or the unstable equivalent).', async function() {
|
it('Will not respond to a rule that has a different recommendation to m.ban (or the unstable equivalent).', async function() {
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir: Mjolnir = this.mjolnir!
|
||||||
const banListId = await mjolnir.createRoom();
|
const banListId = await mjolnir.client.createRoom();
|
||||||
const banList = new BanList(banListId, banListId, mjolnir);
|
const banList = new PolicyList(banListId, banListId, mjolnir.client);
|
||||||
await createPolicyRule(mjolnir, banListId, RULE_SERVER, 'exmaple.org', '', {recommendation: 'something that is not m.ban'});
|
await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, 'exmaple.org', '', { recommendation: 'something that is not m.ban' });
|
||||||
let changes: ListRuleChange[] = await banList.updateList();
|
let changes: ListRuleChange[] = await banList.updateList();
|
||||||
assert.equal(changes.length, 1, 'There should only be one change');
|
assert.equal(changes.length, 1, 'There should only be one change');
|
||||||
assert.equal(changes[0].changeType, ChangeType.Added);
|
assert.equal(changes[0].changeType, ChangeType.Added);
|
||||||
assert.equal(changes[0].sender, await mjolnir.getUserId());
|
assert.equal(changes[0].sender, await mjolnir.client.getUserId());
|
||||||
// We really don't want things that aren't m.ban to end up being accessible in these APIs.
|
// We really don't want things that aren't m.ban to end up being accessible in these APIs.
|
||||||
assert.equal(banList.serverRules.length, 0);
|
assert.equal(banList.serverRules.length, 0, `We should have an empty serverRules, got ${JSON.stringify(banList.serverRules)}`);
|
||||||
assert.equal(banList.allRules.length, 0);
|
assert.equal(banList.allRules.length, 0, `We should have an empty allRules, got ${JSON.stringify(banList.allRules)}`);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Test: We will not be able to ban ourselves via ACL.', function() {
|
describe('Test: We will not be able to ban ourselves via ACL.', function() {
|
||||||
it('We do not ban ourselves when we put ourselves into the policy list.', async function() {
|
it('We do not ban ourselves when we put ourselves into the policy list.', async function() {
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir: Mjolnir = this.mjolnir
|
||||||
const serverName = new UserID(await mjolnir.getUserId()).domain;
|
const serverName = new UserID(await mjolnir.client.getUserId()).domain;
|
||||||
const banListId = await mjolnir.createRoom();
|
const banListId = await mjolnir.client.createRoom();
|
||||||
const banList = new BanList(banListId, banListId, mjolnir);
|
const banList = new PolicyList(banListId, banListId, mjolnir.client);
|
||||||
await createPolicyRule(mjolnir, banListId, RULE_SERVER, serverName, '');
|
await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, serverName, '');
|
||||||
await createPolicyRule(mjolnir, banListId, RULE_SERVER, 'evil.com', '');
|
await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, 'evil.com', '');
|
||||||
await createPolicyRule(mjolnir, banListId, RULE_SERVER, '*', '');
|
await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, '*', '');
|
||||||
// We should still intern the matching rules rule.
|
// We should still intern the matching rules rule.
|
||||||
let changes: ListRuleChange[] = await banList.updateList();
|
let changes: ListRuleChange[] = await banList.updateList();
|
||||||
assert.equal(banList.serverRules.length, 3);
|
assert.equal(banList.serverRules.length, 3);
|
||||||
@ -230,61 +230,64 @@ describe('Test: We will not be able to ban ourselves via ACL.', function () {
|
|||||||
|
|
||||||
|
|
||||||
describe('Test: ACL updates will batch when rules are added in succession.', function() {
|
describe('Test: ACL updates will batch when rules are added in succession.', function() {
|
||||||
it('Will batch ACL updates if we spam rules into a BanList', async function () {
|
it('Will batch ACL updates if we spam rules into a PolicyList', async function() {
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir: Mjolnir = this.mjolnir!
|
||||||
const serverName: string = new UserID(await mjolnir.getUserId()).domain
|
const serverName: string = new UserID(await mjolnir.client.getUserId()).domain
|
||||||
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||||
moderator.joinRoom(this.mjolnir.managementRoomId);
|
moderator.joinRoom(this.mjolnir.client.managementRoomId);
|
||||||
const mjolnirId = await mjolnir.getUserId();
|
const mjolnirId = await mjolnir.client.getUserId();
|
||||||
|
|
||||||
// Setup some protected rooms so we can check their ACL state later.
|
// Setup some protected rooms so we can check their ACL state later.
|
||||||
const protectedRooms: string[] = [];
|
const protectedRooms: string[] = [];
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const room = await moderator.createRoom({ invite: [mjolnirId] });
|
const room = await moderator.createRoom({ invite: [mjolnirId] });
|
||||||
await mjolnir.joinRoom(room);
|
await mjolnir.client.joinRoom(room);
|
||||||
await moderator.setUserPowerLevel(mjolnirId, room, 100);
|
await moderator.setUserPowerLevel(mjolnirId, room, 100);
|
||||||
await this.mjolnir!.addProtectedRoom(room);
|
await mjolnir.addProtectedRoom(room);
|
||||||
protectedRooms.push(room);
|
protectedRooms.push(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point.
|
// If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point.
|
||||||
await this.mjolnir!.syncLists();
|
await mjolnir.syncLists();
|
||||||
await Promise.all(protectedRooms.map(async room => {
|
await Promise.all(protectedRooms.map(async room => {
|
||||||
// We're going to need timeline pagination I'm afraid.
|
// We're going to need timeline pagination I'm afraid.
|
||||||
const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", "");
|
const roomAcl = await mjolnir.client.getRoomStateEvent(room, "m.room.server_acl", "");
|
||||||
assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.');
|
assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Flood the watched list with banned servers, which should prompt Mjolnir to update server ACL in protected rooms.
|
// Flood the watched list with banned servers, which should prompt Mjolnir to update server ACL in protected rooms.
|
||||||
const banListId = await moderator.createRoom({ invite: [mjolnirId] });
|
const banListId = await moderator.createRoom({ invite: [mjolnirId] });
|
||||||
mjolnir.joinRoom(banListId);
|
mjolnir.client.joinRoom(banListId);
|
||||||
this.mjolnir!.watchList(Permalinks.forRoom(banListId));
|
mjolnir.watchList(Permalinks.forRoom(banListId));
|
||||||
const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*");
|
const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*");
|
||||||
for (let i = 0; i < 200; i++) {
|
const evilServerCount = 200;
|
||||||
|
for (let i = 0; i < evilServerCount; i++) {
|
||||||
const badServer = `${i}.evil.com`;
|
const badServer = `${i}.evil.com`;
|
||||||
acl.denyServer(badServer);
|
acl.denyServer(badServer);
|
||||||
await createPolicyRule(moderator, banListId, RULE_SERVER, badServer, `Rule #${i}`);
|
await createPolicyRule(moderator, banListId, RULE_SERVER, badServer, `Rule #${i}`);
|
||||||
// Give them a bit of a spread over time.
|
// Give them a bit of a spread over time.
|
||||||
await new Promise(resolve => setTimeout(resolve, 5));
|
await new Promise(resolve => setTimeout(resolve, 5));
|
||||||
}
|
}
|
||||||
// give the events a chance to appear in the response to `/state`, since this is a problem.
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
// We do this because it should force us to wait until all the ACL events have been applied.
|
// 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...
|
// Even if that does mean the last few events will not go through batching...
|
||||||
await this.mjolnir!.syncLists();
|
await mjolnir.syncLists();
|
||||||
|
|
||||||
|
// At this point we check that the state within Mjolnir is internally consistent, this is just because debugging the following
|
||||||
|
// is a pita.
|
||||||
|
const list: PolicyList = this.mjolnir.policyLists[0]!;
|
||||||
|
assert.equal(list.serverRules.length, evilServerCount, `There should be ${evilServerCount} rules in here`);
|
||||||
|
|
||||||
// Check each of the protected rooms for ACL events and make sure they were batched and are correct.
|
// Check each of the protected rooms for ACL events and make sure they were batched and are correct.
|
||||||
await Promise.all(protectedRooms.map(async room => {
|
await Promise.all(protectedRooms.map(async room => {
|
||||||
const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", "");
|
const roomAcl = await mjolnir.client.getRoomStateEvent(room, "m.room.server_acl", "");
|
||||||
if (!acl.matches(roomAcl)) {
|
if (!acl.matches(roomAcl)) {
|
||||||
assert.fail(`Room ${room} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`)
|
assert.fail(`Room ${room} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`)
|
||||||
}
|
}
|
||||||
let aclEventCount = 0;
|
let aclEventCount = 0;
|
||||||
await getMessagesByUserIn(mjolnir, mjolnirId, room, 100, events => {
|
await getMessagesByUserIn(mjolnir.client, mjolnirId, room, 100, events => {
|
||||||
events.forEach(event => event.type === 'm.room.server_acl' ? aclEventCount += 1 : null);
|
events.forEach(event => event.type === 'm.room.server_acl' ? aclEventCount += 1 : null);
|
||||||
});
|
});
|
||||||
LogService.debug('BanListTest', `aclEventCount: ${aclEventCount}`);
|
LogService.debug('PolicyListTest', `aclEventCount: ${aclEventCount}`);
|
||||||
// If there's less than two then it means the ACL was updated by this test calling `this.mjolnir!.syncLists()`
|
// If there's less than two then it means the ACL was updated by this test calling `this.mjolnir!.syncLists()`
|
||||||
// and not the listener that detects changes to ban lists (that we want to test!).
|
// and not the listener that detects changes to ban lists (that we want to test!).
|
||||||
// It used to be 10, but it was too low, 30 seems better for CI.
|
// It used to be 10, but it was too low, 30 seems better for CI.
|
||||||
@ -293,36 +296,36 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Test: unbaning entities via the BanList.', function () {
|
describe('Test: unbaning entities via the PolicyList.', function() {
|
||||||
afterEach(function() { this.moderator?.stop(); });
|
afterEach(function() { this.moderator?.stop(); });
|
||||||
it('Will remove rules that have legacy types', async function() {
|
it('Will remove rules that have legacy types', async function() {
|
||||||
this.timeout(20000)
|
this.timeout(20000)
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir: Mjolnir = this.mjolnir!
|
||||||
const serverName: string = new UserID(await mjolnir.getUserId()).domain
|
const serverName: string = new UserID(await mjolnir.client.getUserId()).domain
|
||||||
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||||
this.moderator = moderator;
|
this.moderator = moderator;
|
||||||
moderator.joinRoom(this.mjolnir.managementRoomId);
|
moderator.joinRoom(mjolnir.managementRoomId);
|
||||||
const mjolnirId = await mjolnir.getUserId();
|
const mjolnirId = await mjolnir.client.getUserId();
|
||||||
|
|
||||||
// We'll make 1 protected room to test ACLs in.
|
// We'll make 1 protected room to test ACLs in.
|
||||||
const protectedRoom = await moderator.createRoom({ invite: [mjolnirId] });
|
const protectedRoom = await moderator.createRoom({ invite: [mjolnirId] });
|
||||||
await mjolnir.joinRoom(protectedRoom);
|
await mjolnir.client.joinRoom(protectedRoom);
|
||||||
await moderator.setUserPowerLevel(mjolnirId, protectedRoom, 100);
|
await moderator.setUserPowerLevel(mjolnirId, protectedRoom, 100);
|
||||||
await this.mjolnir!.addProtectedRoom(protectedRoom);
|
await mjolnir.addProtectedRoom(protectedRoom);
|
||||||
|
|
||||||
// If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point.
|
// If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point.
|
||||||
await this.mjolnir!.syncLists();
|
await mjolnir.syncLists();
|
||||||
const roomAcl = await mjolnir.getRoomStateEvent(protectedRoom, "m.room.server_acl", "");
|
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.');
|
assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.');
|
||||||
|
|
||||||
// Create some legacy rules on a BanList.
|
// Create some legacy rules on a PolicyList.
|
||||||
const banListId = await moderator.createRoom({ invite: [mjolnirId] });
|
const banListId = await moderator.createRoom({ invite: [mjolnirId] });
|
||||||
await moderator.setUserPowerLevel(await mjolnir.getUserId(), banListId, 100);
|
await moderator.setUserPowerLevel(await mjolnir.client.getUserId(), banListId, 100);
|
||||||
await moderator.sendStateEvent(banListId, 'org.matrix.mjolnir.shortcode', '', { shortcode: "unban-test" });
|
await moderator.sendStateEvent(banListId, 'org.matrix.mjolnir.shortcode', '', { shortcode: "unban-test" });
|
||||||
await mjolnir.joinRoom(banListId);
|
await mjolnir.client.joinRoom(banListId);
|
||||||
this.mjolnir!.watchList(Permalinks.forRoom(banListId));
|
this.mjolnir!.watchList(Permalinks.forRoom(banListId));
|
||||||
// we use this to compare changes.
|
// we use this to compare changes.
|
||||||
const banList = new BanList(banListId, banListId, moderator);
|
const banList = new PolicyList(banListId, banListId, moderator);
|
||||||
// we need two because we need to test the case where an entity has all rule types in the list
|
// we need two because we need to test the case where an entity has all rule types in the list
|
||||||
// and another one that only has one (so that we would hit 404 while looking up state)
|
// and another one that only has one (so that we would hit 404 while looking up state)
|
||||||
const olderBadServer = "old.evil.com"
|
const olderBadServer = "old.evil.com"
|
||||||
@ -338,7 +341,7 @@ describe('Test: unbaning entities via the BanList.', function () {
|
|||||||
|
|
||||||
// Check that we have setup our test properly and therefore evil.com is banned.
|
// Check that we have setup our test properly and therefore evil.com is banned.
|
||||||
const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*").denyServer(olderBadServer).denyServer(newerBadServer);
|
const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*").denyServer(olderBadServer).denyServer(newerBadServer);
|
||||||
const protectedAcl = await mjolnir.getRoomStateEvent(protectedRoom, "m.room.server_acl", "");
|
const protectedAcl = await mjolnir.client.getRoomStateEvent(protectedRoom, "m.room.server_acl", "");
|
||||||
if (!acl.matches(protectedAcl)) {
|
if (!acl.matches(protectedAcl)) {
|
||||||
assert.fail(`Room ${protectedRoom} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`);
|
assert.fail(`Room ${protectedRoom} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`);
|
||||||
}
|
}
|
||||||
@ -360,7 +363,7 @@ describe('Test: unbaning entities via the BanList.', function () {
|
|||||||
// Confirm that the server is unbanned.
|
// Confirm that the server is unbanned.
|
||||||
await banList.updateList();
|
await banList.updateList();
|
||||||
assert.equal(banList.allRules.length, 0);
|
assert.equal(banList.allRules.length, 0);
|
||||||
const aclAfter = await mjolnir.getRoomStateEvent(protectedRoom, "m.room.server_acl", "");
|
const aclAfter = await mjolnir.client.getRoomStateEvent(protectedRoom, "m.room.server_acl", "");
|
||||||
assert.equal(aclAfter.deny.length, 0, 'Should be no servers denied anymore');
|
assert.equal(aclAfter.deny.length, 0, 'Should be no servers denied anymore');
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -368,35 +371,35 @@ describe('Test: unbaning entities via the BanList.', function () {
|
|||||||
describe('Test: should apply bans to the most recently active rooms first', function() {
|
describe('Test: should apply bans to the most recently active rooms first', function() {
|
||||||
it('Applies bans to the most recently active rooms first', async function() {
|
it('Applies bans to the most recently active rooms first', async function() {
|
||||||
this.timeout(180000)
|
this.timeout(180000)
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir: Mjolnir = this.mjolnir!
|
||||||
const serverName: string = new UserID(await mjolnir.getUserId()).domain
|
const serverName: string = new UserID(await mjolnir.client.getUserId()).domain
|
||||||
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||||
moderator.joinRoom(this.mjolnir.managementRoomId);
|
moderator.joinRoom(mjolnir.managementRoomId);
|
||||||
const mjolnirId = await mjolnir.getUserId();
|
const mjolnirId = await mjolnir.client.getUserId();
|
||||||
|
|
||||||
// Setup some protected rooms so we can check their ACL state later.
|
// Setup some protected rooms so we can check their ACL state later.
|
||||||
const protectedRooms: string[] = [];
|
const protectedRooms: string[] = [];
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const room = await moderator.createRoom({ invite: [mjolnirId] });
|
const room = await moderator.createRoom({ invite: [mjolnirId] });
|
||||||
await mjolnir.joinRoom(room);
|
await mjolnir.client.joinRoom(room);
|
||||||
await moderator.setUserPowerLevel(mjolnirId, room, 100);
|
await moderator.setUserPowerLevel(mjolnirId, room, 100);
|
||||||
await this.mjolnir!.addProtectedRoom(room);
|
await mjolnir.addProtectedRoom(room);
|
||||||
protectedRooms.push(room);
|
protectedRooms.push(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point.
|
// If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point.
|
||||||
await this.mjolnir!.syncLists();
|
await mjolnir.syncLists();
|
||||||
await Promise.all(protectedRooms.map(async room => {
|
await Promise.all(protectedRooms.map(async room => {
|
||||||
const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", "").catch(e => e.statusCode === 404 ? {deny: []} : Promise.reject(e));
|
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.');
|
assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Flood the watched list with banned servers, which should prompt Mjolnir to update server ACL in protected rooms.
|
// Flood the watched list with banned servers, which should prompt Mjolnir to update server ACL in protected rooms.
|
||||||
const banListId = await moderator.createRoom({ invite: [mjolnirId] });
|
const banListId = await moderator.createRoom({ invite: [mjolnirId] });
|
||||||
mjolnir.joinRoom(banListId);
|
mjolnir.client.joinRoom(banListId);
|
||||||
this.mjolnir!.watchList(Permalinks.forRoom(banListId));
|
mjolnir.watchList(Permalinks.forRoom(banListId));
|
||||||
|
|
||||||
await this.mjolnir!.syncLists();
|
await mjolnir.syncLists();
|
||||||
|
|
||||||
// shuffle protected rooms https://stackoverflow.com/a/12646864, we do this so we can create activity "randomly" in them.
|
// 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--) {
|
for (let i = protectedRooms.length - 1; i > 0; i--) {
|
||||||
@ -405,13 +408,13 @@ describe('Test: should apply bans to the most recently active rooms first', func
|
|||||||
}
|
}
|
||||||
// create some activity in the same order.
|
// create some activity in the same order.
|
||||||
for (const roomId of protectedRooms.slice().reverse()) {
|
for (const roomId of protectedRooms.slice().reverse()) {
|
||||||
await mjolnir.sendMessage(roomId, {body: `activity`, msgtype: 'm.text'});
|
await mjolnir.client.sendMessage(roomId, { body: `activity`, msgtype: 'm.text' });
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
// check the rooms are in the expected order
|
// check the rooms are in the expected order
|
||||||
for (let i = 0; i < protectedRooms.length; i++) {
|
for (let i = 0; i < protectedRooms.length; i++) {
|
||||||
assert.equal(this.mjolnir!.protectedRoomsByActivity()[i], protectedRooms[i]);
|
assert.equal(mjolnir.protectedRoomsByActivity()[i], protectedRooms[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const badServer = `evil.com`;
|
const badServer = `evil.com`;
|
||||||
@ -420,10 +423,10 @@ describe('Test: should apply bans to the most recently active rooms first', func
|
|||||||
await createPolicyRule(moderator, banListId, RULE_SERVER, badServer, `Rule ${badServer}`);
|
await createPolicyRule(moderator, banListId, RULE_SERVER, badServer, `Rule ${badServer}`);
|
||||||
|
|
||||||
// Wait until all the ACL events have been applied.
|
// Wait until all the ACL events have been applied.
|
||||||
await this.mjolnir!.syncLists();
|
await mjolnir.syncLists();
|
||||||
|
|
||||||
for (let i = 0; i < protectedRooms.length; i++) {
|
for (let i = 0; i < protectedRooms.length; i++) {
|
||||||
assert.equal(this.mjolnir!.protectedRoomsByActivity()[i], protectedRooms.at(-i - 1));
|
assert.equal(mjolnir.protectedRoomsByActivity()[i], protectedRooms.at(-i - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the most recently active rooms got the ACL update first.
|
// Check that the most recently active rooms got the ACL update first.
|
||||||
@ -431,7 +434,7 @@ describe('Test: should apply bans to the most recently active rooms first', func
|
|||||||
for (const roomId of protectedRooms) {
|
for (const roomId of protectedRooms) {
|
||||||
let roomAclEvent: null | any;
|
let roomAclEvent: null | any;
|
||||||
// Can't be the best way to get the whole event, but ok.
|
// Can't be the best way to get the whole event, but ok.
|
||||||
await getMessagesByUserIn(mjolnir, mjolnirId, roomId, 1, events => roomAclEvent = events[0]);
|
await getMessagesByUserIn(mjolnir.client, mjolnirId, roomId, 1, events => roomAclEvent = events[0]);
|
||||||
const roomAcl = roomAclEvent!.content;
|
const roomAcl = roomAclEvent!.content;
|
||||||
if (!acl.matches(roomAcl)) {
|
if (!acl.matches(roomAcl)) {
|
||||||
assert.fail(`Room ${roomId} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`)
|
assert.fail(`Room ${roomId} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { HmacSHA1 } from "crypto-js";
|
import { HmacSHA1 } from "crypto-js";
|
||||||
import { getRequestFn, LogService, MatrixClient, MemoryStorageProvider, PantalaimonClient } from "matrix-bot-sdk";
|
import { getRequestFn, LogService, MatrixClient, MemoryStorageProvider, PantalaimonClient } from "matrix-bot-sdk";
|
||||||
import config from "../../src/config";
|
import config from '../../src/config';
|
||||||
|
|
||||||
const REGISTRATION_ATTEMPTS = 10;
|
const REGISTRATION_ATTEMPTS = 10;
|
||||||
const REGISTRATION_RETRY_BASE_DELAY_MS = 100;
|
const REGISTRATION_RETRY_BASE_DELAY_MS = 100;
|
||||||
|
@ -14,7 +14,7 @@ export const mochaHooks = {
|
|||||||
// Sometimes it takes a little longer to register users.
|
// Sometimes it takes a little longer to register users.
|
||||||
this.timeout(10000)
|
this.timeout(10000)
|
||||||
this.managementRoomAlias = config.managementRoom;
|
this.managementRoomAlias = config.managementRoom;
|
||||||
this.mjolnir = await makeMjolnir();
|
this.mjolnir = await makeMjolnir(config);
|
||||||
config.RUNTIME.client = this.mjolnir.client;
|
config.RUNTIME.client = this.mjolnir.client;
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.mjolnir.client.setAccountData('org.matrix.mjolnir.protected_rooms', { rooms: [] }),
|
this.mjolnir.client.setAccountData('org.matrix.mjolnir.protected_rooms', { rooms: [] }),
|
||||||
|
@ -3,8 +3,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { makeMjolnir } from "./mjolnirSetupUtils";
|
import { makeMjolnir } from "./mjolnirSetupUtils";
|
||||||
|
import config from '../../src/config';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
let mjolnir = await makeMjolnir();
|
let mjolnir = await makeMjolnir(config);
|
||||||
await mjolnir.start();
|
await mjolnir.start();
|
||||||
})();
|
})();
|
||||||
|
@ -22,9 +22,9 @@ import {
|
|||||||
RichConsoleLogger
|
RichConsoleLogger
|
||||||
} from "matrix-bot-sdk";
|
} from "matrix-bot-sdk";
|
||||||
import { Mjolnir} from '../../src/Mjolnir';
|
import { Mjolnir} from '../../src/Mjolnir';
|
||||||
import config from "../../src/config";
|
|
||||||
import { overrideRatelimitForUser, registerUser } from "./clientHelper";
|
import { overrideRatelimitForUser, registerUser } from "./clientHelper";
|
||||||
import { patchMatrixClient } from "../../src/utils";
|
import { patchMatrixClient } from "../../src/utils";
|
||||||
|
import { IConfig } from "../../src/config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures that a room exists with the alias, if it does not exist we create it.
|
* Ensures that a room exists with the alias, if it does not exist we create it.
|
||||||
@ -48,7 +48,7 @@ export async function ensureAliasedRoomExists(client: MatrixClient, alias: strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function configureMjolnir() {
|
async function configureMjolnir(config: IConfig) {
|
||||||
try {
|
try {
|
||||||
await registerUser(config.pantalaimon.username, config.pantalaimon.username, config.pantalaimon.password, true)
|
await registerUser(config.pantalaimon.username, config.pantalaimon.username, config.pantalaimon.password, true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -72,8 +72,8 @@ let globalMjolnir: Mjolnir | null;
|
|||||||
/**
|
/**
|
||||||
* Return a test instance of Mjolnir.
|
* Return a test instance of Mjolnir.
|
||||||
*/
|
*/
|
||||||
export async function makeMjolnir(): Promise<Mjolnir> {
|
export async function makeMjolnir(config: IConfig): Promise<Mjolnir> {
|
||||||
await configureMjolnir();
|
await configureMjolnir(config);
|
||||||
LogService.setLogger(new RichConsoleLogger());
|
LogService.setLogger(new RichConsoleLogger());
|
||||||
LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG));
|
LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG));
|
||||||
LogService.info("test/mjolnirSetupUtils", "Starting bot...");
|
LogService.info("test/mjolnirSetupUtils", "Starting bot...");
|
||||||
@ -82,7 +82,7 @@ export async function makeMjolnir(): Promise<Mjolnir> {
|
|||||||
await overrideRatelimitForUser(await client.getUserId());
|
await overrideRatelimitForUser(await client.getUserId());
|
||||||
patchMatrixClient();
|
patchMatrixClient();
|
||||||
await ensureAliasedRoomExists(client, config.managementRoom);
|
await ensureAliasedRoomExists(client, config.managementRoom);
|
||||||
let mj = await Mjolnir.setupMjolnirFromConfig(client);
|
let mj = await Mjolnir.setupMjolnirFromConfig(client, config);
|
||||||
globalClient = client;
|
globalClient = client;
|
||||||
globalMjolnir = mj;
|
globalMjolnir = mj;
|
||||||
return mj;
|
return mj;
|
||||||
|
Loading…
Reference in New Issue
Block a user