This should hopefully fix some startup woes (#462)

Splitting PolicyListManager from Mjolnir, making it more resilient to startup errors
This commit is contained in:
David Teller 2022-12-21 19:32:27 +01:00 committed by GitHub
parent 7534fbc73c
commit cff9b43207
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 249 additions and 167 deletions

View File

@ -19,7 +19,6 @@ import {
LogLevel, LogLevel,
LogService, LogService,
MembershipEvent, MembershipEvent,
Permalinks,
} from "matrix-bot-sdk"; } from "matrix-bot-sdk";
import { ALL_RULE_TYPES as ALL_BAN_LIST_RULE_TYPES } from "./models/ListRule"; import { ALL_RULE_TYPES as ALL_BAN_LIST_RULE_TYPES } from "./models/ListRule";
@ -32,7 +31,7 @@ import { WebAPIs } from "./webapis/WebAPIs";
import RuleServer from "./models/RuleServer"; import RuleServer from "./models/RuleServer";
import { ThrottlingQueue } from "./queues/ThrottlingQueue"; import { ThrottlingQueue } from "./queues/ThrottlingQueue";
import { getDefaultConfig, IConfig } from "./config"; import { getDefaultConfig, IConfig } from "./config";
import PolicyList from "./models/PolicyList"; import { PolicyListManager } from "./models/PolicyList";
import { ProtectedRoomsSet } from "./ProtectedRoomsSet"; import { ProtectedRoomsSet } from "./ProtectedRoomsSet";
import ManagementRoomOutput from "./ManagementRoomOutput"; import ManagementRoomOutput from "./ManagementRoomOutput";
import { ProtectionManager } from "./protections/ProtectionManager"; import { ProtectionManager } from "./protections/ProtectionManager";
@ -45,9 +44,6 @@ export const STATE_CHECKING_PERMISSIONS = "checking_permissions";
export const STATE_SYNCING = "syncing"; export const STATE_SYNCING = "syncing";
export const STATE_RUNNING = "running"; export const STATE_RUNNING = "running";
const WATCHED_LISTS_EVENT_TYPE = "org.matrix.mjolnir.watched_lists";
const WARN_UNPROTECTED_ROOM_EVENT_PREFIX = "org.matrix.mjolnir.unprotected_room_warning.for.";
/** /**
* Synapse will tell us where we last got to on polling reports, so we need * Synapse will tell us where we last got to on polling reports, so we need
* to store that for pagination on further polls * to store that for pagination on further polls
@ -86,6 +82,8 @@ export class Mjolnir {
*/ */
public readonly reportManager: ReportManager; public readonly reportManager: ReportManager;
public readonly policyListManager: PolicyListManager;
/** /**
* Adds a listener to the client that will automatically accept invitations. * Adds a listener to the client that will automatically accept invitations.
* @param {MatrixSendClient} client * @param {MatrixSendClient} client
@ -143,7 +141,6 @@ export class Mjolnir {
if (!config.autojoinOnlyIfManager && config.acceptInvitesFromSpace === getDefaultConfig().acceptInvitesFromSpace) { if (!config.autojoinOnlyIfManager && config.acceptInvitesFromSpace === getDefaultConfig().acceptInvitesFromSpace) {
throw new TypeError("`autojoinOnlyIfManager` has been disabled, yet no space has been provided for `acceptInvitesFromSpace`."); throw new TypeError("`autojoinOnlyIfManager` has been disabled, yet no space has been provided for `acceptInvitesFromSpace`.");
} }
const policyLists: PolicyList[] = [];
const joinedRooms = await client.getJoinedRooms(); const joinedRooms = await client.getJoinedRooms();
// Ensure we're also in the management room // Ensure we're also in the management room
@ -154,7 +151,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, await client.getUserId(), matrixEmitter, managementRoomId, config, policyLists, ruleServer); const mjolnir = new Mjolnir(client, await client.getUserId(), matrixEmitter, managementRoomId, config, ruleServer);
await mjolnir.managementRoomOutput.logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status."); await mjolnir.managementRoomOutput.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;
@ -166,11 +163,11 @@ export class Mjolnir {
public readonly matrixEmitter: MatrixEmitter, public readonly matrixEmitter: MatrixEmitter,
public readonly managementRoomId: string, public readonly managementRoomId: string,
public readonly config: IConfig, public readonly config: IConfig,
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.protectedRoomsConfig = new ProtectedRoomsConfig(client); this.protectedRoomsConfig = new ProtectedRoomsConfig(client);
this.policyListManager = new PolicyListManager(this);
// Setup bot. // Setup bot.
@ -247,10 +244,6 @@ export class Mjolnir {
this.protectedRoomsTracker = new ProtectedRoomsSet(client, clientUserId, managementRoomId, this.managementRoomOutput, protections, config); this.protectedRoomsTracker = new ProtectedRoomsSet(client, clientUserId, managementRoomId, this.managementRoomOutput, protections, config);
} }
public get lists(): PolicyList[] {
return this.policyLists;
}
public get state(): string { public get state(): string {
return this.currentState; return this.currentState;
} }
@ -296,7 +289,7 @@ export class Mjolnir {
this.protectedRoomsConfig.getExplicitlyProtectedRooms().forEach(this.protectRoom, this); this.protectedRoomsConfig.getExplicitlyProtectedRooms().forEach(this.protectRoom, this);
// We have to build the policy lists before calling `resyncJoinedRooms` otherwise mjolnir will try to protect // We have to build the policy lists before calling `resyncJoinedRooms` otherwise mjolnir will try to protect
// every policy list we are already joined to, as mjolnir will not be able to distinguish them from normal rooms. // every policy list we are already joined to, as mjolnir will not be able to distinguish them from normal rooms.
await this.buildWatchedPolicyLists(); await this.policyListManager.init();
await this.resyncJoinedRooms(false); await this.resyncJoinedRooms(false);
await this.protectionManager.start(); await this.protectionManager.start();
@ -399,7 +392,7 @@ export class Mjolnir {
// We filter out all policy rooms so that we only protect ones that are // We filter out all policy rooms so that we only protect ones that are
// explicitly protected, so that we don't try to protect lists that we are just watching. // explicitly protected, so that we don't try to protect lists that we are just watching.
const filterOutManagementAndPolicyRooms = (roomId: string) => { const filterOutManagementAndPolicyRooms = (roomId: string) => {
const policyListIds = this.policyLists.map(list => list.roomId); const policyListIds = this.policyListManager.lists.map(list => list.roomId);
return roomId !== this.managementRoomId && !policyListIds.includes(roomId); return roomId !== this.managementRoomId && !policyListIds.includes(roomId);
}; };
@ -430,109 +423,6 @@ export class Mjolnir {
} }
} }
/**
* Helper for constructing `PolicyList`s and making sure they have the right listeners set up.
* @param roomId The room id for the `PolicyList`.
* @param roomRef A reference (matrix.to URL) for the `PolicyList`.
*/
private async addPolicyList(roomId: string, roomRef: string): Promise<PolicyList> {
const list = new PolicyList(roomId, roomRef, this.client);
this.ruleServer?.watch(list);
await list.updateList();
this.policyLists.push(list);
this.protectedRoomsTracker.watchList(list);
return list;
}
public async watchList(roomRef: string): Promise<PolicyList | null> {
const joinedRooms = await this.client.getJoinedRooms();
const permalink = Permalinks.parseUrl(roomRef);
if (!permalink.roomIdOrAlias) return null;
const roomId = await this.client.resolveRoom(permalink.roomIdOrAlias);
if (!joinedRooms.includes(roomId)) {
await this.client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
}
if (this.policyLists.find(b => b.roomId === roomId)) return null;
const list = await this.addPolicyList(roomId, roomRef);
await this.client.setAccountData(WATCHED_LISTS_EVENT_TYPE, {
references: this.policyLists.map(b => b.roomRef),
});
await this.warnAboutUnprotectedPolicyListRoom(roomId);
return list;
}
public async unwatchList(roomRef: string): Promise<PolicyList | null> {
const permalink = Permalinks.parseUrl(roomRef);
if (!permalink.roomIdOrAlias) return null;
const roomId = await this.client.resolveRoom(permalink.roomIdOrAlias);
const list = this.policyLists.find(b => b.roomId === roomId) || null;
if (list) {
this.policyLists.splice(this.policyLists.indexOf(list), 1);
this.ruleServer?.unwatch(list);
this.protectedRoomsTracker.unwatchList(list);
}
await this.client.setAccountData(WATCHED_LISTS_EVENT_TYPE, {
references: this.policyLists.map(b => b.roomRef),
});
return list;
}
public async warnAboutUnprotectedPolicyListRoom(roomId: string) {
if (!this.config.protectAllJoinedRooms) return; // doesn't matter
if (this.protectedRoomsConfig.getExplicitlyProtectedRooms().includes(roomId)) return; // explicitly protected
try {
const accountData: { warned: boolean } | null = await this.client.getAccountData(WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId);
if (accountData && accountData.warned) return; // already warned
} catch (e) {
// Ignore - probably haven't warned about it yet
}
await this.managementRoomOutput.logMessage(LogLevel.WARN, "Mjolnir", `Not protecting ${roomId} - it is a ban list that this bot did not create. Add the room as protected if it is supposed to be protected. This warning will not appear again.`, roomId);
await this.client.setAccountData(WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId, { warned: true });
}
/**
* Load the watched policy lists from account data, only used when Mjolnir is initialized.
*/
private async buildWatchedPolicyLists() {
this.policyLists = [];
const joinedRooms = await this.client.getJoinedRooms();
let watchedListsEvent: { references?: string[] } | null = null;
try {
watchedListsEvent = await this.client.getAccountData(WATCHED_LISTS_EVENT_TYPE);
} catch (e) {
if (e.statusCode === 404) {
LogService.warn('Mjolnir', "Couldn't find account data for Mjolnir's watched lists, assuming first start.", extractRequestError(e));
} else {
throw e;
}
}
for (const roomRef of (watchedListsEvent?.references || [])) {
const permalink = Permalinks.parseUrl(roomRef);
if (!permalink.roomIdOrAlias) continue;
const roomId = await this.client.resolveRoom(permalink.roomIdOrAlias);
if (!joinedRooms.includes(roomId)) {
await this.client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
}
await this.warnAboutUnprotectedPolicyListRoom(roomId);
await this.addPolicyList(roomId, roomRef);
}
}
private async handleEvent(roomId: string, event: any) { private async handleEvent(roomId: string, event: any) {
// Check for UISI errors // Check for UISI errors
if (roomId === this.managementRoomId) { if (roomId === this.managementRoomId) {
@ -548,7 +438,7 @@ 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 policyList = this.policyLists.find(list => list.roomId === roomId); const policyList = this.policyListManager.lists.find(list => list.roomId === roomId);
if (policyList !== 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') {
policyList.updateForEvent(event.event_id) policyList.updateForEvent(event.event_id)

View File

@ -206,7 +206,7 @@ export class ManagedMjolnir {
); );
const roomRef = Permalinks.forRoom(listRoomId); const roomRef = Permalinks.forRoom(listRoomId);
await this.mjolnir.addProtectedRoom(listRoomId); await this.mjolnir.addProtectedRoom(listRoomId);
return await this.mjolnir.watchList(roomRef); return await this.mjolnir.policyListManager.watchList(roomRef);
} }
public get managementRoomId(): string { public get managementRoomId(): string {

View File

@ -31,7 +31,7 @@ export async function execCreateListCommand(roomId: string, event: any, mjolnir:
); );
const roomRef = Permalinks.forRoom(listRoomId); const roomRef = Permalinks.forRoom(listRoomId);
await mjolnir.watchList(roomRef); await mjolnir.policyListManager.watchList(roomRef);
await mjolnir.addProtectedRoom(listRoomId); await mjolnir.addProtectedRoom(listRoomId);
const html = `Created new list (<a href="${roomRef}">${listRoomId}</a>). This list is now being watched.`; const html = `Created new list (<a href="${roomRef}">${listRoomId}</a>). This list is now being watched.`;

View File

@ -32,7 +32,7 @@ import { htmlEscape } from "../utils";
export async function execRulesMatchingCommand(roomId: string, event: any, mjolnir: Mjolnir, entity: string) { export async function execRulesMatchingCommand(roomId: string, event: any, mjolnir: Mjolnir, entity: string) {
let html = ""; let html = "";
let text = ""; let text = "";
for (const list of mjolnir.lists) { for (const list of mjolnir.policyListManager.lists) {
const matches = list.rulesMatchingEntity(entity) const matches = list.rulesMatchingEntity(entity)
if (matches.length === 0) { if (matches.length === 0) {
@ -81,7 +81,7 @@ export async function execDumpRulesCommand(roomId: string, event: any, mjolnir:
let text = "Rules currently in use:\n"; let text = "Rules currently in use:\n";
let hasLists = false; let hasLists = false;
for (const list of mjolnir.lists) { for (const list of mjolnir.policyListManager.lists) {
hasLists = true; hasLists = true;
let hasRules = false; let hasRules = false;

View File

@ -22,7 +22,7 @@ import PolicyList from "../models/PolicyList";
// !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[]) {
const importRoomId = await mjolnir.client.resolveRoom(parts[2]); const importRoomId = await mjolnir.client.resolveRoom(parts[2]);
const list = mjolnir.lists.find(b => b.listShortcode === parts[3]) as PolicyList; const list = mjolnir.policyListManager.lists.find(b => b.listShortcode === parts[3]) as PolicyList;
if (!list) { if (!list) {
const errMessage = "Unable to find list - check your shortcode."; const errMessage = "Unable to find list - check your shortcode.";
const errReply = RichReply.createFor(roomId, event, errMessage, errMessage); const errReply = RichReply.createFor(roomId, event, errMessage, errMessage);

View File

@ -22,7 +22,7 @@ export const DEFAULT_LIST_EVENT_TYPE = "org.matrix.mjolnir.default_list";
// !mjolnir default <shortcode> // !mjolnir default <shortcode>
export async function execSetDefaultListCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { export async function execSetDefaultListCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
const shortcode = parts[2]; const shortcode = parts[2];
const list = mjolnir.lists.find(b => b.listShortcode === shortcode); const list = mjolnir.policyListManager.lists.find(b => b.listShortcode === shortcode);
if (!list) { if (!list) {
const replyText = "No ban list with that shortcode was found."; const replyText = "No ban list with that shortcode was found.";
const reply = RichReply.createFor(roomId, event, replyText, replyText); const reply = RichReply.createFor(roomId, event, replyText, replyText);

View File

@ -86,9 +86,9 @@ async function showMjolnirStatus(roomId: string, event: any, mjolnir: Mjolnir) {
} }
html += "</ul>"; html += "</ul>";
} }
const subscribedLists = mjolnir.lists.filter(list => !mjolnir.explicitlyProtectedRooms.includes(list.roomId)); const subscribedLists = mjolnir.policyListManager.lists.filter(list => !mjolnir.explicitlyProtectedRooms.includes(list.roomId));
renderPolicyLists("Subscribed policy lists", subscribedLists); renderPolicyLists("Subscribed policy lists", subscribedLists);
const subscribedAndProtectedLists = mjolnir.lists.filter(list => mjolnir.explicitlyProtectedRooms.includes(list.roomId)); const subscribedAndProtectedLists = mjolnir.policyListManager.lists.filter(list => mjolnir.explicitlyProtectedRooms.includes(list.roomId));
renderPolicyLists("Subscribed and protected policy lists", subscribedAndProtectedLists); renderPolicyLists("Subscribed and protected policy lists", subscribedAndProtectedLists);
const reply = RichReply.createFor(roomId, event, text, html); const reply = RichReply.createFor(roomId, event, text, html);

View File

@ -59,7 +59,7 @@ export async function parseArguments(roomId: string, event: any, mjolnir: Mjolni
else if (arg.startsWith("!") && !ruleType) ruleType = RULE_ROOM; else if (arg.startsWith("!") && !ruleType) ruleType = RULE_ROOM;
else if (!ruleType) ruleType = RULE_SERVER; else if (!ruleType) ruleType = RULE_SERVER;
} else if (!list) { } else if (!list) {
const foundList = mjolnir.lists.find(b => b.listShortcode.toLowerCase() === arg.toLowerCase()); const foundList = mjolnir.policyListManager.lists.find(b => b.listShortcode.toLowerCase() === arg.toLowerCase());
if (foundList !== undefined) { if (foundList !== undefined) {
list = foundList; list = foundList;
} }
@ -86,7 +86,7 @@ export async function parseArguments(roomId: string, event: any, mjolnir: Mjolni
} }
if (!list) { if (!list) {
list = mjolnir.lists.find(b => b.listShortcode.toLowerCase() === defaultShortcode) || null; list = mjolnir.policyListManager.lists.find(b => b.listShortcode.toLowerCase() === defaultShortcode) || null;
} }
let replyMessage: string | null = null; let replyMessage: string | null = null;

View File

@ -19,7 +19,7 @@ import { Permalinks, RichReply } from "matrix-bot-sdk";
// !mjolnir watch <room alias or ID> // !mjolnir watch <room alias or ID>
export async function execWatchCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { export async function execWatchCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
const list = await mjolnir.watchList(Permalinks.forRoom(parts[2])); const list = await mjolnir.policyListManager.watchList(Permalinks.forRoom(parts[2]));
if (!list) { if (!list) {
const replyText = "Cannot watch list due to error - is that a valid room alias?"; const replyText = "Cannot watch list due to error - is that a valid room alias?";
const reply = RichReply.createFor(roomId, event, replyText, replyText); const reply = RichReply.createFor(roomId, event, replyText, replyText);
@ -32,7 +32,7 @@ export async function execWatchCommand(roomId: string, event: any, mjolnir: Mjol
// !mjolnir unwatch <room alias or ID> // !mjolnir unwatch <room alias or ID>
export async function execUnwatchCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { export async function execUnwatchCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
const list = await mjolnir.unwatchList(Permalinks.forRoom(parts[2])); const list = await mjolnir.policyListManager.unwatchList(Permalinks.forRoom(parts[2]));
if (!list) { if (!list) {
const replyText = "Cannot unwatch list due to error - is that a valid room alias?"; const replyText = "Cannot unwatch list due to error - is that a valid room alias?";
const reply = RichReply.createFor(roomId, event, replyText, replyText); const reply = RichReply.createFor(roomId, event, replyText, replyText);

View File

@ -14,13 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { extractRequestError, LogService, RoomCreateOptions, UserID } from "matrix-bot-sdk"; import { extractRequestError, LogLevel, LogService, Permalinks, RoomCreateOptions, UserID } from "matrix-bot-sdk";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { ALL_RULE_TYPES, EntityType, ListRule, Recommendation, ROOM_RULE_TYPES, RULE_ROOM, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES, USER_RULE_TYPES } 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";
import { MatrixSendClient } from "../MatrixEmitter"; import { MatrixSendClient } from "../MatrixEmitter";
import AwaitLock from "await-lock"; import AwaitLock from "await-lock";
import { monotonicFactory } from "ulidx"; import { monotonicFactory } from "ulidx";
import { Mjolnir } from "../Mjolnir";
/**
* Account data event type used to store the permalinks to each of the policylists.
*
* Content:
* ```jsonc
* {
* references: string[], // Each entry is a `matrix.to` permalink.
* }
* ```
*/
export const WATCHED_LISTS_EVENT_TYPE = "org.matrix.mjolnir.watched_lists";
/**
* A prefix used to record that we have already warned at least once that a PolicyList room is unprotected.
*/
const WARN_UNPROTECTED_ROOM_EVENT_PREFIX = "org.matrix.mjolnir.unprotected_room_warning.for.";
export const SHORTCODE_EVENT_TYPE = "org.matrix.mjolnir.shortcode"; export const SHORTCODE_EVENT_TYPE = "org.matrix.mjolnir.shortcode";
export enum ChangeType { export enum ChangeType {
@ -596,3 +613,176 @@ export class Revision {
return this.ulid > revision.ulid; return this.ulid > revision.ulid;
} }
} }
/**
* A manager for all the policy lists for this Mjölnir
*/
export class PolicyListManager {
private policyLists: PolicyList[];
/**
* A list of references (matrix.to URLs) to policy lists that
* we could not resolve during startup. We store them to make
* sure that they're written back whenever we rewrite the references
* to account data.
*/
private readonly failedStartupWatchListRefs: Set<string> = new Set();
constructor(private readonly mjolnir: Mjolnir) {
// Nothing to do.
}
public get lists(): PolicyList[] {
return this.policyLists;
}
/**
* Helper for constructing `PolicyList`s and making sure they have the right listeners set up.
* @param roomId The room id for the `PolicyList`.
* @param roomRef A reference (matrix.to URL) for the `PolicyList`.
*/
private async addPolicyList(roomId: string, roomRef: string): Promise<PolicyList> {
const list = new PolicyList(roomId, roomRef, this.mjolnir.client);
this.mjolnir.ruleServer?.watch(list);
await list.updateList();
this.policyLists.push(list);
this.mjolnir.protectedRoomsTracker.watchList(list);
// If we have succeeded, let's remove this from the list of failed policy rooms.
this.failedStartupWatchListRefs.delete(roomRef);
return list;
}
public async watchList(roomRef: string): Promise<PolicyList | null> {
const joinedRooms = await this.mjolnir.client.getJoinedRooms();
const permalink = Permalinks.parseUrl(roomRef);
if (!permalink.roomIdOrAlias) return null;
const roomId = await this.mjolnir.client.resolveRoom(permalink.roomIdOrAlias);
if (!joinedRooms.includes(roomId)) {
await this.mjolnir.client.joinRoom(roomId, permalink.viaServers);
}
if (this.policyLists.find(b => b.roomId === roomId)) {
// This room was already in our list of policy rooms, nothing else to do.
// Note that we bailout *after* the call to `joinRoom`, in case a user
// calls `watchList` in an attempt to repair something that was broken,
// e.g. a Mjölnir who could not join the room because of alias resolution
// or server being down, etc.
return null;
}
const list = await this.addPolicyList(roomId, roomRef);
await this.storeWatchedPolicyLists();
await this.warnAboutUnprotectedPolicyListRoom(roomId);
return list;
}
public async unwatchList(roomRef: string): Promise<PolicyList | null> {
const permalink = Permalinks.parseUrl(roomRef);
if (!permalink.roomIdOrAlias) return null;
const roomId = await this.mjolnir.client.resolveRoom(permalink.roomIdOrAlias);
const list = this.policyLists.find(b => b.roomId === roomId) || null;
if (list) {
this.policyLists.splice(this.policyLists.indexOf(list), 1);
this.mjolnir.ruleServer?.unwatch(list);
this.mjolnir.protectedRoomsTracker.unwatchList(list);
}
await this.storeWatchedPolicyLists();
return list;
}
/**
* Load the watched policy lists from account data, only used when Mjolnir is initialized.
*/
public async init() {
this.policyLists = [];
const joinedRooms = await this.mjolnir.client.getJoinedRooms();
let watchedListsEvent: { references?: string[] } | null = null;
try {
watchedListsEvent = await this.mjolnir.client.getAccountData(WATCHED_LISTS_EVENT_TYPE);
} catch (e) {
if (e.statusCode === 404) {
LogService.warn('Mjolnir', "Couldn't find account data for Mjolnir's watched lists, assuming first start.", extractRequestError(e));
} else {
throw e;
}
}
for (const roomRef of (watchedListsEvent?.references || [])) {
const permalink = Permalinks.parseUrl(roomRef);
if (!permalink.roomIdOrAlias) continue;
let roomId;
try {
roomId = await this.mjolnir.client.resolveRoom(permalink.roomIdOrAlias);
} catch (ex) {
// Let's not fail startup because of a problem resolving a room id or an alias.
LogService.warn('Mjolnir', 'Could not resolve policy list room, skipping for this run', permalink.roomIdOrAlias)
await this.mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "Mjolnir", `Room ${permalink.roomIdOrAlias} could **not** be resolved, perhaps a server is down? Skipping this room. If this is a recurring problem, please consider removing this room.`);
this.failedStartupWatchListRefs.add(roomRef);
continue;
}
if (!joinedRooms.includes(roomId)) {
await this.mjolnir.client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
}
await this.warnAboutUnprotectedPolicyListRoom(roomId);
await this.addPolicyList(roomId, roomRef);
}
}
/**
* Store to account the list of policy rooms.
*
* We store both rooms that we are currently monitoring and rooms for which
* we could not setup monitoring, assuming that the setup is a transient issue
* that the user (or someone else) will eventually resolve.
*/
private async storeWatchedPolicyLists() {
let list = this.policyLists.map(b => b.roomRef);
for (let entry of this.failedStartupWatchListRefs) {
list.push(entry);
}
await this.mjolnir.client.setAccountData(WATCHED_LISTS_EVENT_TYPE, {
references: list,
});
}
/**
* Check whether a policy list room is protected. If not, display
* a user-readable warning.
*
* We store as account data the list of room ids for which we have
* already displayed the warning, to avoid bothering users at every
* single startup.
*
* @param roomId The id of the room to check/warn.
*/
private async warnAboutUnprotectedPolicyListRoom(roomId: string) {
if (!this.mjolnir.config.protectAllJoinedRooms) {
return; // doesn't matter
}
if (this.mjolnir.explicitlyProtectedRooms.includes(roomId)) {
return; // explicitly protected
}
try {
const accountData: { warned: boolean } | null = await this.mjolnir.client.getAccountData(WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId);
if (accountData && accountData.warned) {
return; // already warned
}
} catch (e) {
// Expect that we haven't warned yet.
}
await this.mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "Mjolnir", `Not protecting ${roomId} - it is a ban list that this bot did not create. Add the room as protected if it is supposed to be protected. This warning will not appear again.`, roomId);
await this.mjolnir.client.setAccountData(WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId, { warned: true });
}
}

View File

@ -35,6 +35,7 @@ function createTestMjolnir(defaultShortcode: string|null = null): Mjolnir {
return <Mjolnir>{ return <Mjolnir>{
client, client,
config, config,
policyListManager: {}
}; };
} }
@ -53,7 +54,7 @@ describe("UnbanBanCommand", () => {
describe("parseArguments", () => { describe("parseArguments", () => {
it("should be able to detect servers", async () => { it("should be able to detect servers", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -70,7 +71,7 @@ describe("UnbanBanCommand", () => {
it("should be able to detect servers with ban reasons", async () => { it("should be able to detect servers with ban reasons", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -87,7 +88,7 @@ describe("UnbanBanCommand", () => {
it("should be able to detect servers with globs", async () => { it("should be able to detect servers with globs", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -104,7 +105,7 @@ describe("UnbanBanCommand", () => {
it("should be able to detect servers with the type specified", async () => { it("should be able to detect servers with the type specified", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -121,7 +122,7 @@ describe("UnbanBanCommand", () => {
it("should be able to detect room IDs", async () => { it("should be able to detect room IDs", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -138,7 +139,7 @@ describe("UnbanBanCommand", () => {
it("should be able to detect room IDs with ban reasons", async () => { it("should be able to detect room IDs with ban reasons", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -155,7 +156,7 @@ describe("UnbanBanCommand", () => {
it("should be able to detect room IDs with globs", async () => { it("should be able to detect room IDs with globs", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -172,7 +173,7 @@ describe("UnbanBanCommand", () => {
it("should be able to detect room aliases", async () => { it("should be able to detect room aliases", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -189,7 +190,7 @@ describe("UnbanBanCommand", () => {
it("should be able to detect room aliases with ban reasons", async () => { it("should be able to detect room aliases with ban reasons", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -206,7 +207,7 @@ describe("UnbanBanCommand", () => {
it("should be able to detect room aliases with globs", async () => { it("should be able to detect room aliases with globs", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -223,7 +224,7 @@ describe("UnbanBanCommand", () => {
it("should be able to detect rooms with the type specified", async () => { it("should be able to detect rooms with the type specified", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -240,7 +241,7 @@ describe("UnbanBanCommand", () => {
it("should be able to detect user IDs", async () => { it("should be able to detect user IDs", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -257,7 +258,7 @@ describe("UnbanBanCommand", () => {
it("should be able to detect user IDs with ban reasons", async () => { it("should be able to detect user IDs with ban reasons", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -274,7 +275,7 @@ describe("UnbanBanCommand", () => {
it("should be able to detect user IDs with globs", async () => { it("should be able to detect user IDs with globs", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -291,7 +292,7 @@ describe("UnbanBanCommand", () => {
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 () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -308,7 +309,7 @@ describe("UnbanBanCommand", () => {
it("should error if wildcards used without --force", async () => { it("should error if wildcards used without --force", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
expect(content).toBeDefined(); expect(content).toBeDefined();
expect(content['body']).toContain("Wildcard bans require an additional `--force` argument to confirm"); expect(content['body']).toContain("Wildcard bans require an additional `--force` argument to confirm");
@ -322,7 +323,7 @@ describe("UnbanBanCommand", () => {
it("should have correct ban reason with --force after", async () => { it("should have correct ban reason with --force after", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -340,7 +341,7 @@ describe("UnbanBanCommand", () => {
describe("[without default list]", () => { describe("[without default list]", () => {
it("should error if no list (with type) is specified", async () => { it("should error if no list (with type) is specified", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
expect(content).toBeDefined(); expect(content).toBeDefined();
expect(content['body']).toContain("No ban list matching that shortcode was found"); expect(content['body']).toContain("No ban list matching that shortcode was found");
@ -354,7 +355,7 @@ describe("UnbanBanCommand", () => {
it("should error if no list (without type) is specified", async () => { it("should error if no list (without type) is specified", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
expect(content).toBeDefined(); expect(content).toBeDefined();
expect(content['body']).toContain("No ban list matching that shortcode was found"); expect(content['body']).toContain("No ban list matching that shortcode was found");
@ -368,7 +369,7 @@ describe("UnbanBanCommand", () => {
it("should not error if a list (with type) is specified", async () => { it("should not error if a list (with type) is specified", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -385,7 +386,7 @@ describe("UnbanBanCommand", () => {
it("should not error if a list (without type) is specified", async () => { it("should not error if a list (without type) is specified", async () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -402,7 +403,7 @@ describe("UnbanBanCommand", () => {
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 () => {
const mjolnir = createTestMjolnir(); const mjolnir = createTestMjolnir();
(<any>mjolnir).lists = [{listShortcode: "test"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -421,7 +422,7 @@ describe("UnbanBanCommand", () => {
describe("[with default list]", () => { describe("[with default list]", () => {
it("should use the default list if no list (with type) is specified", async () => { it("should use the default list if no list (with type) is specified", async () => {
const mjolnir = createTestMjolnir("test"); const mjolnir = createTestMjolnir("test");
(<any>mjolnir).lists = [{listShortcode: "test"}, {listShortcode: "other"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}, {listShortcode: "other"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -438,7 +439,7 @@ describe("UnbanBanCommand", () => {
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 () => {
const mjolnir = createTestMjolnir("test"); const mjolnir = createTestMjolnir("test");
(<any>mjolnir).lists = [{listShortcode: "test"}, {listShortcode: "other"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}, {listShortcode: "other"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -455,7 +456,7 @@ describe("UnbanBanCommand", () => {
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 () => {
const mjolnir = createTestMjolnir("test"); const mjolnir = createTestMjolnir("test");
(<any>mjolnir).lists = [{listShortcode: "test"}, {listShortcode: "other"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}, {listShortcode: "other"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -472,7 +473,7 @@ describe("UnbanBanCommand", () => {
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 () => {
const mjolnir = createTestMjolnir("test"); const mjolnir = createTestMjolnir("test");
(<any>mjolnir).lists = [{listShortcode: "test"}, {listShortcode: "other"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}, {listShortcode: "other"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };
@ -489,7 +490,7 @@ describe("UnbanBanCommand", () => {
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 () => {
const mjolnir = createTestMjolnir("test"); const mjolnir = createTestMjolnir("test");
(<any>mjolnir).lists = [{listShortcode: "test"}, {listShortcode: "other"}]; (<any>mjolnir).policyListManager.lists = [{listShortcode: "test"}, {listShortcode: "other"}];
mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => { mjolnir.client.sendMessage = (roomId: string, content: any): Promise<string> => {
throw new Error("sendMessage should not have been called: " + JSON.stringify(content)); throw new Error("sendMessage should not have been called: " + JSON.stringify(content));
}; };

View File

@ -262,7 +262,7 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun
// 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] });
await mjolnir.client.joinRoom(banListId); await mjolnir.client.joinRoom(banListId);
await mjolnir.watchList(Permalinks.forRoom(banListId)); await mjolnir.policyListManager.watchList(Permalinks.forRoom(banListId));
const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*"); const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*");
const evilServerCount = 200; const evilServerCount = 200;
for (let i = 0; i < evilServerCount; i++) { for (let i = 0; i < evilServerCount; i++) {
@ -278,7 +278,7 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun
// At this point we check that the state within Mjolnir is internally consistent, this is just because debugging the following // At this point we check that the state within Mjolnir is internally consistent, this is just because debugging the following
// is a pita. // is a pita.
const list: PolicyList = this.mjolnir.policyLists[0]!; const list: PolicyList = this.mjolnir.policyListManager.lists[0]!;
assert.equal(list.serverRules.length, evilServerCount, `There should be ${evilServerCount} rules in here`); 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.
@ -327,7 +327,7 @@ describe('Test: unbaning entities via the PolicyList.', function() {
await moderator.setUserPowerLevel(await mjolnir.client.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.client.joinRoom(banListId); await mjolnir.client.joinRoom(banListId);
await mjolnir.watchList(Permalinks.forRoom(banListId)); await mjolnir.policyListManager.watchList(Permalinks.forRoom(banListId));
// we use this to compare changes. // we use this to compare changes.
const banList = new PolicyList(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
@ -402,7 +402,7 @@ describe('Test: should apply bans to the most recently active rooms first', func
// 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] });
await mjolnir.client.joinRoom(banListId); await mjolnir.client.joinRoom(banListId);
await mjolnir.watchList(Permalinks.forRoom(banListId)); await mjolnir.policyListManager.watchList(Permalinks.forRoom(banListId));
await mjolnir.protectedRoomsTracker.syncLists(); await mjolnir.protectedRoomsTracker.syncLists();
@ -506,7 +506,7 @@ describe('Test: AccessControlUnit interaction with policy lists.', function() {
assertAccess(Access.Allowed, aclUnit.getAccessForUser('@someone:matrix.org', "CHECK_SERVER")); assertAccess(Access.Allowed, aclUnit.getAccessForUser('@someone:matrix.org', "CHECK_SERVER"));
// protect a room and check that only bad.example.com, *.ddns.example.com are in the deny ACL and not matrix.org // protect a room and check that only bad.example.com, *.ddns.example.com are in the deny ACL and not matrix.org
await mjolnir.watchList(policyList.roomRef); await mjolnir.policyListManager.watchList(policyList.roomRef);
const protectedRoom = await mjolnir.client.createRoom(); const protectedRoom = await mjolnir.client.createRoom();
await mjolnir.protectedRoomsTracker.addProtectedRoom(protectedRoom); await mjolnir.protectedRoomsTracker.addProtectedRoom(protectedRoom);
await mjolnir.protectedRoomsTracker.syncLists(); await mjolnir.protectedRoomsTracker.syncLists();

View File

@ -125,7 +125,7 @@ export async function getFirstReaction(matrix: MatrixEmitter, targetRoom: string
* @param client A client that isn't mjolnir to send the message with, as you will be invited to the room. * @param client A client that isn't mjolnir to send the message with, as you will be invited to the room.
* @returns The shortcode for the list that can be used to refer to the list in future commands. * @returns The shortcode for the list that can be used to refer to the list in future commands.
*/ */
export async function createBanList(managementRoom: string, mjolnir: MatrixClient, client: MatrixClient): Promise<string> { export async function createBanList(managementRoom: string, mjolnir: MatrixEmitter, client: MatrixClient): Promise<string> {
const listName = crypto.randomUUID(); const listName = crypto.randomUUID();
const listCreationResponse = await getFirstReply(mjolnir, managementRoom, async () => { const listCreationResponse = await getFirstReply(mjolnir, managementRoom, async () => {
return await client.sendMessage(managementRoom, { msgtype: 'm.text', body: `!mjolnir list create ${listName} ${listName}`}); return await client.sendMessage(managementRoom, { msgtype: 'm.text', body: `!mjolnir list create ${listName} ${listName}`});

View File

@ -1,6 +1,7 @@
import { strict as assert } from "assert"; import { strict as assert } from "assert";
import { MatrixClient, Permalinks, UserID } from "matrix-bot-sdk"; import { MatrixClient, Permalinks, UserID } from "matrix-bot-sdk";
import { MatrixSendClient } from "../../src/MatrixEmitter";
import { Mjolnir } from "../../src/Mjolnir"; import { Mjolnir } from "../../src/Mjolnir";
import PolicyList from "../../src/models/PolicyList"; import PolicyList from "../../src/models/PolicyList";
import { newTestUser } from "./clientHelper"; import { newTestUser } from "./clientHelper";
@ -12,7 +13,7 @@ async function createPolicyList(client: MatrixClient): Promise<PolicyList> {
return new PolicyList(policyListId, Permalinks.forRoom(policyListId), client); return new PolicyList(policyListId, Permalinks.forRoom(policyListId), client);
} }
async function getProtectedRoomsFromAccountData(client: MatrixClient): Promise<string[]> { async function getProtectedRoomsFromAccountData(client: MatrixSendClient): Promise<string[]> {
const rooms: { rooms?: string[] } = await client.getAccountData("org.matrix.mjolnir.protected_rooms"); const rooms: { rooms?: string[] } = await client.getAccountData("org.matrix.mjolnir.protected_rooms");
return rooms.rooms!; return rooms.rooms!;
} }
@ -42,9 +43,9 @@ describe('Test: config.protectAllJoinedRooms behaves correctly.', function() {
.forEach(roomId => assert.equal(implicitlyProtectedRooms.includes(roomId), true)); .forEach(roomId => assert.equal(implicitlyProtectedRooms.includes(roomId), true));
// We create one policy list with Mjolnir, and we watch another that is maintained by someone else. // We create one policy list with Mjolnir, and we watch another that is maintained by someone else.
const policyListShortcode = await createBanList(mjolnir.managementRoomId, mjolnir.client, moderator); const policyListShortcode = await createBanList(mjolnir.managementRoomId, mjolnir.matrixEmitter, moderator);
const unprotectedWatchedList = await createPolicyList(moderator); const unprotectedWatchedList = await createPolicyList(moderator);
await mjolnir.watchList(unprotectedWatchedList.roomRef); await mjolnir.policyListManager.watchList(unprotectedWatchedList.roomRef);
await mjolnir.protectedRoomsTracker.syncLists(); await mjolnir.protectedRoomsTracker.syncLists();
// We expect that the watched list will not be protected, despite config.protectAllJoinedRooms being true // We expect that the watched list will not be protected, despite config.protectAllJoinedRooms being true