Skip to content

Commit

Permalink
Fail early when PolicyManager cannot resolve alias.
Browse files Browse the repository at this point in the history
  • Loading branch information
Gnuxie committed Jan 26, 2023
1 parent 7dbd9eb commit 011814c
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 56 deletions.
2 changes: 1 addition & 1 deletion src/Mjolnir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ export class Mjolnir {
this.protectedRoomsConfig.getExplicitlyProtectedRooms().forEach(this.protectRoom, this);
// 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.
await this.policyListManager.init();
await this.policyListManager.start();
await this.resyncJoinedRooms(false);
await this.protectionManager.start();

Expand Down
55 changes: 55 additions & 0 deletions src/models/MatrixDataManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Copyright (C) 2023 Gnuxie <Gnuxie@protonmail.com>
*/

export const SCHEMA_VERSION_KEY = 'ge.applied-langua.ge.draupnir.schema_version';

export type RawSchemedData = object & Record<typeof SCHEMA_VERSION_KEY, unknown>;
export type SchemaMigration = (input: RawSchemedData) => Promise<RawSchemedData>;

export abstract class MatrixDataManager<Format extends RawSchemedData = RawSchemedData> {

protected abstract schema: SchemaMigration[];
protected abstract isAllowedToInferNoVersionAsZero: boolean;
protected abstract requestMatrixData(): Promise<unknown>
protected abstract storeMatixData(data: Format): Promise<void>;
protected abstract createFirstData(): Promise<Format>;

protected async migrateData(rawData: RawSchemedData): Promise<RawSchemedData> {
const startingVersion = rawData[SCHEMA_VERSION_KEY] as number;
// Rememeber, version 0 has no migrations
if (this.schema.length < startingVersion) {
throw new TypeError(`Encountered a version that we do not have migrations for ${startingVersion}`);
} else if (this.schema.length === startingVersion) {
return rawData;
} else {
const applicableSchema = this.schema.slice(startingVersion);
const migratedData = await applicableSchema.reduce(
async (previousData: Promise<RawSchemedData>, schema: SchemaMigration) => {
return await schema(await previousData)
}, Promise.resolve(rawData)
);
return migratedData;
}
}

protected async loadData(): Promise<Format> {
const rawData = await this.requestMatrixData();
if (rawData === undefined) {
return await this.createFirstData();
} else if (typeof rawData !== 'object' || rawData === null) {
throw new TypeError("The data has been corrupted.");
}

if (!(SCHEMA_VERSION_KEY in rawData) && this.isAllowedToInferNoVersionAsZero) {
(rawData as RawSchemedData)[SCHEMA_VERSION_KEY] = 0;
}
if (SCHEMA_VERSION_KEY in rawData
&& Number.isInteger(rawData[SCHEMA_VERSION_KEY])
) {
// what if the schema migration is somehow incorrect and we are casting as Format?
return await this.migrateData(rawData) as Format;
}
throw new TypeError("The schema version or data has been corrupted")
}
}
97 changes: 42 additions & 55 deletions src/models/PolicyList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { MatrixSendClient } from "../MatrixEmitter";
import AwaitLock from "await-lock";
import { monotonicFactory } from "ulidx";
import { Mjolnir } from "../Mjolnir";
import { MatrixDataManager, RawSchemedData, SCHEMA_VERSION_KEY } from "./MatrixDataManager";
import { MatrixRoomReference } from "../commands/interface-manager/MatrixRoomReference";

/**
* Account data event type used to store the permalinks to each of the policylists.
Expand Down Expand Up @@ -625,26 +627,23 @@ export class Revision {
}
}

type WatchedListsEvent = RawSchemedData & { references?: string[] };

/**
* A manager for all the policy lists for this Mjölnir
* Manages the policy lists that a Mjolnir watches
*/
export class PolicyListManager {
export class PolicyListManager extends MatrixDataManager<WatchedListsEvent> {
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();
protected schema = [];
protected isAllowedToInferNoVersionAsZero = true;

constructor(private readonly mjolnir: Mjolnir) {
// Nothing to do.
super();
}

public get lists(): PolicyList[] {
return this.policyLists;
return [...this.policyLists];
}

/**
Expand All @@ -659,8 +658,6 @@ export class PolicyListManager {
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;
}

Expand All @@ -685,8 +682,7 @@ export class PolicyListManager {

const list = await this.addPolicyList(roomId, roomRef);

await this.storeWatchedPolicyLists();

await this.storeMatixData();
await this.warnAboutUnprotectedPolicyListRoom(roomId);

return list;
Expand All @@ -697,71 +693,62 @@ export class PolicyListManager {
if (!permalink.roomIdOrAlias) return null;

const roomId = await this.mjolnir.client.resolveRoom(permalink.roomIdOrAlias);
this.failedStartupWatchListRefs.delete(roomRef);
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();
await this.storeMatixData();
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();
protected async createFirstData(): Promise<RawSchemedData> {
return { [SCHEMA_VERSION_KEY]: 0 };
}

let watchedListsEvent: { references?: string[] } | null = null;
protected async requestMatrixData(): Promise<unknown> {
try {
watchedListsEvent = await this.mjolnir.client.getAccountData(WATCHED_LISTS_EVENT_TYPE);
return 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));
return this.createFirstData();
} 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);
}
/**
* Load the watched policy lists from account data, only used when Mjolnir is initialized.
*/
public async start() {
this.policyLists = [];
const watchedListsEvent = await super.loadData();

await Promise.all(
(watchedListsEvent?.references || []).map(async (roomRef: string) => {
const roomReference = await MatrixRoomReference.fromPermalink(roomRef).resolve(this.mjolnir.client)
.catch(ex => {
LogService.error("PolicyListManager", "Failed to load watched lists for this mjolnir", ex);
return Promise.reject(ex);
}
);
await roomReference.joinClient(this.mjolnir.client);
await this.warnAboutUnprotectedPolicyListRoom(roomReference.toRoomIdOrAlias());
// TODO, FIXME: fix this so that it stores room references and not this utter junk.
await this.addPolicyList(roomReference.toRoomIdOrAlias(), roomReference.toPermalink());
})
);
}

/**
* 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() {
protected async storeMatixData() {
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,
});
Expand Down Expand Up @@ -797,4 +784,4 @@ export class PolicyListManager {
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 });
}
}
}

0 comments on commit 011814c

Please sign in to comment.