WIP: First draft

This commit is contained in:
David Teller 2022-09-30 17:31:47 +02:00
parent b0262e9d6c
commit a905a3d077
20 changed files with 153 additions and 155 deletions

View File

@ -40,6 +40,7 @@ import ManagementRoomOutput from "./ManagementRoomOutput";
import { ProtectionManager } from "./protections/ProtectionManager";
import { RoomMemberManager } from "./RoomMembers";
import { CachingClient, WritableCache } from "./CachingClient";
import { DEFAULT_LIST_EVENT_TYPE } from "./commands/SetDefaultBanListCommand";
export const STATE_NOT_STARTED = "not_started";
export const STATE_CHECKING_PERMISSIONS = "checking_permissions";
@ -50,8 +51,6 @@ const PROTECTED_ROOMS_EVENT_TYPE = "org.matrix.mjolnir.protected_rooms";
const WATCHED_LISTS_EVENT_TYPE = "org.matrix.mjolnir.watched_lists";
const WARN_UNPROTECTED_ROOM_EVENT_PREFIX = "org.matrix.mjolnir.unprotected_room_warning.for.";
type WatchedListsEvent = { references?: string[] } | null;
export class Mjolnir {
public readonly client: CachingClient;
private currentState: string = STATE_NOT_STARTED;
@ -97,9 +96,10 @@ export class Mjolnir {
/**
* The list of protected rooms, as specified in the account data.
*/
private accountData: {
public readonly accountData: {
protectedRooms?: WritableCache<any>,
watchedLists?: WritableCache<WatchedListsEvent>,
watchedLists?: WritableCache<any>,
defaultList?: WritableCache<any>,
} = {};
/**
@ -322,6 +322,7 @@ export class Mjolnir {
LogService.warn("Mjolnir", extractRequestError(e));
}
this.accountData.watchedLists = await this.client.accountData(WATCHED_LISTS_EVENT_TYPE);
this.accountData.defaultList = await this.client.accountData(DEFAULT_LIST_EVENT_TYPE);
await this.buildWatchedPolicyLists();
this.applyUnprotectedRooms();
await this.protectionManager.start();
@ -432,7 +433,7 @@ export class Mjolnir {
* @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.uncached);
const list = new PolicyList(roomId, roomRef, this.client);
this.ruleServer?.watch(list);
list.on('PolicyList.batch', (...args) => this.protectedRoomsTracker.syncWithPolicyList(...args));
await list.updateList();

View File

@ -152,7 +152,7 @@ export class ProtectedRooms {
* @returns The list of errors encountered, for reporting to the management room.
*/
public async processRedactionQueue(roomId?: string): Promise<RoomUpdateError[]> {
return await this.eventRedactionQueue.process(this.client.uncached, this.managementRoomOutput, roomId);
return await this.eventRedactionQueue.process(this.client, this.managementRoomOutput, roomId);
}
/**

View File

@ -23,14 +23,14 @@ import { isListSetting } from "../protections/ProtectionSettings";
export async function execEnableProtection(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
try {
await mjolnir.protectionManager.enableProtection(parts[2]);
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
await mjolnir.client.uncached.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
} catch (e) {
LogService.error("ProtectionsCommands", extractRequestError(e));
const message = `Error enabling protection '${parts[0]}' - check the name and try again.`;
const reply = RichReply.createFor(roomId, event, message, message);
reply["msgtype"] = "m.notice";
await mjolnir.client.sendMessage(roomId, reply);
await mjolnir.client.uncached.sendMessage(roomId, reply);
}
}
@ -104,7 +104,7 @@ export async function execConfigSetProtection(roomId: string, event: any, mjolni
const reply = RichReply.createFor(roomId, event, message, message);
reply["msgtype"] = "m.notice";
await mjolnir.client.sendMessage(roomId, reply);
await mjolnir.client.uncached.sendMessage(roomId, reply);
}
/*
@ -117,7 +117,7 @@ export async function execConfigAddProtection(roomId: string, event: any, mjolni
const reply = RichReply.createFor(roomId, event, message, message);
reply["msgtype"] = "m.notice";
await mjolnir.client.sendMessage(roomId, reply);
await mjolnir.client.uncached.sendMessage(roomId, reply);
}
/*
@ -130,7 +130,7 @@ export async function execConfigRemoveProtection(roomId: string, event: any, mjo
const reply = RichReply.createFor(roomId, event, message, message);
reply["msgtype"] = "m.notice";
await mjolnir.client.sendMessage(roomId, reply);
await mjolnir.client.uncached.sendMessage(roomId, reply);
}
/*
@ -151,7 +151,7 @@ export async function execConfigGetProtection(roomId: string, event: any, mjolni
const errMsg = `Unknown protection: ${parts[0]}`;
const errReply = RichReply.createFor(roomId, event, errMsg, errMsg);
errReply["msgtype"] = "m.notice";
await mjolnir.client.sendMessage(roomId, errReply);
await mjolnir.client.uncached.sendMessage(roomId, errReply);
return;
}
pickProtections = [parts[0]];
@ -191,13 +191,13 @@ export async function execConfigGetProtection(roomId: string, event: any, mjolni
const reply = RichReply.createFor(roomId, event, text, html);
reply["msgtype"] = "m.notice";
await mjolnir.client.sendMessage(roomId, reply);
await mjolnir.client.uncached.sendMessage(roomId, reply);
}
// !mjolnir disable <protection>
export async function execDisableProtection(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
await mjolnir.protectionManager.disableProtection(parts[2]);
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
await mjolnir.client.uncached.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
}
// !mjolnir protections
@ -217,5 +217,5 @@ export async function execListProtections(roomId: string, event: any, mjolnir: M
const reply = RichReply.createFor(roomId, event, text, html);
reply["msgtype"] = "m.notice";
await mjolnir.client.sendMessage(roomId, reply);
await mjolnir.client.uncached.sendMessage(roomId, reply);
}

View File

@ -24,7 +24,7 @@ export async function execRedactCommand(roomId: string, event: any, mjolnir: Mjo
let roomAlias: string|null = null;
let limit = Number.parseInt(parts.length > 3 ? parts[3] : "", 10); // default to NaN for later
if (parts.length > 3 && isNaN(limit)) {
roomAlias = await mjolnir.client.resolveRoom(parts[3]);
roomAlias = await mjolnir.client.uncached.resolveRoom(parts[3]);
if (parts.length > 4) {
limit = Number.parseInt(parts[4], 10);
}
@ -33,21 +33,21 @@ export async function execRedactCommand(roomId: string, event: any, mjolnir: Mjo
// Make sure we always have a limit set
if (isNaN(limit)) limit = 1000;
const processingReactionId = await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], 'In Progress');
const processingReactionId = await mjolnir.client.uncached.unstableApis.addReactionToEvent(roomId, event['event_id'], 'In Progress');
if (userId[0] !== '@') {
// Assume it's a permalink
const parsed = Permalinks.parseUrl(parts[2]);
const targetRoomId = await mjolnir.client.resolveRoom(parsed.roomIdOrAlias);
await mjolnir.client.redactEvent(targetRoomId, parsed.eventId);
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
await mjolnir.client.redactEvent(roomId, processingReactionId, 'done processing command');
const targetRoomId = await mjolnir.client.uncached.resolveRoom(parsed.roomIdOrAlias);
await mjolnir.client.uncached.redactEvent(targetRoomId, parsed.eventId);
await mjolnir.client.uncached.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
await mjolnir.client.uncached.redactEvent(roomId, processingReactionId, 'done processing command');
return;
}
const targetRoomIds = roomAlias ? [roomAlias] : mjolnir.protectedRoomsTracker.getProtectedRooms();
await redactUserMessagesIn(mjolnir.client, mjolnir.managementRoomOutput, userId, targetRoomIds, limit);
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
await mjolnir.client.redactEvent(roomId, processingReactionId, 'done processing');
await mjolnir.client.uncached.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
await mjolnir.client.uncached.redactEvent(roomId, processingReactionId, 'done processing');
}

View File

@ -27,10 +27,10 @@ export async function execSetDefaultListCommand(roomId: string, event: any, mjol
const replyText = "No ban list with that shortcode was found.";
const reply = RichReply.createFor(roomId, event, replyText, replyText);
reply["msgtype"] = "m.notice";
mjolnir.client.sendMessage(roomId, reply);
mjolnir.client.uncached.sendMessage(roomId, reply);
return;
}
await mjolnir.client.setAccountData(DEFAULT_LIST_EVENT_TYPE, { shortcode });
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
await mjolnir.accountData.defaultList!.set({ shortcode });
await mjolnir.client.uncached.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
}

View File

@ -23,11 +23,11 @@ export async function execSetPowerLevelCommand(roomId: string, event: any, mjoln
const level = Math.round(Number(parts[3]));
const inRoom = parts[4];
let targetRooms = inRoom ? [await mjolnir.client.resolveRoom(inRoom)] : mjolnir.protectedRoomsTracker.getProtectedRooms();
let targetRooms = inRoom ? [await mjolnir.client.uncached.resolveRoom(inRoom)] : mjolnir.protectedRoomsTracker.getProtectedRooms();
for (const targetRoomId of targetRooms) {
try {
await mjolnir.client.setUserPowerLevel(victim, targetRoomId, level);
await mjolnir.client.uncached.setUserPowerLevel(victim, targetRoomId, level);
} catch (e) {
const message = e.message || (e.body ? e.body.error : '<no message>');
await mjolnir.managementRoomOutput.logMessage(LogLevel.ERROR, "SetPowerLevelCommand", `Failed to set power level of ${victim} to ${level} in ${targetRoomId}: ${message}`, targetRoomId);
@ -35,5 +35,5 @@ export async function execSetPowerLevelCommand(roomId: string, event: any, mjoln
}
}
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
await mjolnir.client.uncached.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
}

View File

@ -27,10 +27,10 @@ export async function execShutdownRoomCommand(roomId: string, event: any, mjolni
const message = "I am not a Synapse administrator, or the endpoint is blocked";
const reply = RichReply.createFor(roomId, event, message, message);
reply['msgtype'] = "m.notice";
mjolnir.client.sendMessage(roomId, reply);
mjolnir.client.uncached.sendMessage(roomId, reply);
return;
}
await mjolnir.shutdownSynapseRoom(await mjolnir.client.resolveRoom(victim), reason);
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
await mjolnir.shutdownSynapseRoom(await mjolnir.client.uncached.resolveRoom(victim), reason);
await mjolnir.client.uncached.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
}

View File

@ -99,14 +99,14 @@ function getTokenAsString(name: string, token: ParseEntry): {error: string}|{ok:
export async function execSinceCommand(destinationRoomId: string, event: any, mjolnir: Mjolnir, tokens: ParseEntry[]) {
let result = await execSinceCommandAux(destinationRoomId, event, mjolnir, tokens);
if ("error" in result) {
mjolnir.client.unstableApis.addReactionToEvent(destinationRoomId, event['event_id'], '❌');
mjolnir.client.uncached.unstableApis.addReactionToEvent(destinationRoomId, event['event_id'], '❌');
mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "SinceCommand", result.error);
const reply = RichReply.createFor(destinationRoomId, event, result.error, htmlEscape(result.error));
reply["msgtype"] = "m.notice";
/* no need to await */ mjolnir.client.sendMessage(destinationRoomId, reply);
/* no need to await */ mjolnir.client.uncached.sendMessage(destinationRoomId, reply);
} else {
// Details have already been printed.
mjolnir.client.unstableApis.addReactionToEvent(destinationRoomId, event['event_id'], '✅');
mjolnir.client.uncached.unstableApis.addReactionToEvent(destinationRoomId, event['event_id'], '✅');
}
}
@ -200,7 +200,7 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni
}
continue;
} else if (maybeRoom.startsWith("#") || maybeRoom.startsWith("!")) {
const roomId = await mjolnir.client.resolveRoom(maybeRoom);
const roomId = await mjolnir.client.uncached.resolveRoom(maybeRoom);
if (!protectedRooms.has(roomId)) {
return mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "SinceCommand", `This room is not protected: ${htmlEscape(roomId)}.`);
}
@ -220,7 +220,7 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni
};
}
const progressEventId = await mjolnir.client.unstableApis.addReactionToEvent(destinationRoomId, event['event_id'], '⏳');
const progressEventId = await mjolnir.client.uncached.unstableApis.addReactionToEvent(destinationRoomId, event['event_id'], '⏳');
const reason: string | undefined = reasonParts?.join(" ");
for (let targetRoomId of rooms) {
@ -235,7 +235,7 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni
case Action.Kick: {
for (let join of recentJoins) {
try {
await mjolnir.client.kickUser(join.userId, targetRoomId, reason);
await mjolnir.client.uncached.kickUser(join.userId, targetRoomId, reason);
results.succeeded.push(join.userId);
} catch (ex) {
LogService.warn("SinceCommand", "Error while attempting to kick user", ex);
@ -248,7 +248,7 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni
case Action.Ban: {
for (let join of recentJoins) {
try {
await mjolnir.client.banUser(join.userId, targetRoomId, reason);
await mjolnir.client.uncached.banUser(join.userId, targetRoomId, reason);
results.succeeded.push(join.userId);
} catch (ex) {
LogService.warn("SinceCommand", "Error while attempting to ban user", ex);
@ -259,13 +259,13 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni
return formatResult("ban", targetRoomId, recentJoins, results);
}
case Action.Mute: {
const powerLevels = await mjolnir.client.getRoomStateEvent(targetRoomId, "m.room.power_levels", "") as {users: Record</* userId */ string, number>};
const powerLevels = await mjolnir.client.uncached.getRoomStateEvent(targetRoomId, "m.room.power_levels", "") as {users: Record</* userId */ string, number>};
for (let join of recentJoins) {
powerLevels.users[join.userId] = -1;
}
try {
await mjolnir.client.sendStateEvent(targetRoomId, "m.room.power_levels", "", powerLevels);
await mjolnir.client.uncached.sendStateEvent(targetRoomId, "m.room.power_levels", "", powerLevels);
for (let join of recentJoins) {
results.succeeded.push(join.userId);
}
@ -279,13 +279,13 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni
return formatResult("mute", targetRoomId, recentJoins, results);
}
case Action.Unmute: {
const powerLevels = await mjolnir.client.getRoomStateEvent(targetRoomId, "m.room.power_levels", "") as {users: Record</* userId */ string, number>, users_default?: number};
const powerLevels = await mjolnir.client.uncached.getRoomStateEvent(targetRoomId, "m.room.power_levels", "") as {users: Record</* userId */ string, number>, users_default?: number};
for (let join of recentJoins) {
// Restore default powerlevel.
delete powerLevels.users[join.userId];
}
try {
await mjolnir.client.sendStateEvent(targetRoomId, "m.room.power_levels", "", powerLevels);
await mjolnir.client.uncached.sendStateEvent(targetRoomId, "m.room.power_levels", "", powerLevels);
for (let join of recentJoins) {
results.succeeded.push(join.userId);
}
@ -303,10 +303,10 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni
const reply = RichReply.createFor(destinationRoomId, event, text, html);
reply["msgtype"] = "m.notice";
/* no need to await */ mjolnir.client.sendMessage(destinationRoomId, reply);
/* no need to await */ mjolnir.client.uncached.sendMessage(destinationRoomId, reply);
}
await mjolnir.client.redactEvent(destinationRoomId, progressEventId);
await mjolnir.client.uncached.redactEvent(destinationRoomId, progressEventId);
return {ok: undefined};
}

View File

@ -86,7 +86,7 @@ async function showMjolnirStatus(roomId: string, event: any, mjolnir: Mjolnir) {
const reply = RichReply.createFor(roomId, event, text, html);
reply["msgtype"] = "m.notice";
return mjolnir.client.sendMessage(roomId, reply);
return mjolnir.client.uncached.sendMessage(roomId, reply);
}
async function showProtectionStatus(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
@ -108,7 +108,7 @@ async function showProtectionStatus(roomId: string, event: any, mjolnir: Mjolnir
}
const reply = RichReply.createFor(roomId, event, text, html);
reply["msgtype"] = "m.notice";
await mjolnir.client.sendMessage(roomId, reply);
await mjolnir.client.uncached.sendMessage(roomId, reply);
}
/**
@ -149,7 +149,7 @@ async function showJoinsStatus(destinationRoomId: string, event: any, mjolnir: M
const maxAgeHumanReadable = HUMANIZER.humanize(maxAgeMS, HUMANIZER_OPTIONS);
let targetRoomId;
try {
targetRoomId = await mjolnir.client.resolveRoom(targetRoomAliasOrId);
targetRoomId = await mjolnir.client.uncached.resolveRoom(targetRoomAliasOrId);
} catch (ex) {
return {
html: `Cannot resolve room ${htmlEscape(targetRoomAliasOrId)}.`,
@ -171,6 +171,6 @@ async function showJoinsStatus(destinationRoomId: string, event: any, mjolnir: M
})();
const reply = RichReply.createFor(destinationRoomId, event, text, html);
reply["msgtype"] = "m.notice";
return mjolnir.client.sendMessage(destinationRoomId, reply);
return mjolnir.client.uncached.sendMessage(destinationRoomId, reply);
}

View File

@ -16,9 +16,8 @@ limitations under the License.
import { Mjolnir } from "../Mjolnir";
import PolicyList from "../models/PolicyList";
import { extractRequestError, LogLevel, LogService, MatrixGlob, RichReply } from "matrix-bot-sdk";
import { LogLevel, MatrixGlob, RichReply } from "matrix-bot-sdk";
import { RULE_ROOM, RULE_SERVER, RULE_USER, USER_RULE_TYPES } from "../models/ListRule";
import { DEFAULT_LIST_EVENT_TYPE } from "./SetDefaultBanListCommand";
interface Arguments {
list: PolicyList | null;
@ -29,16 +28,11 @@ interface Arguments {
// Exported for tests
export async function parseArguments(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]): Promise<Arguments | null> {
let defaultShortcode: string | null = null;
try {
const data: { shortcode: string } = await mjolnir.client.getAccountData(DEFAULT_LIST_EVENT_TYPE);
defaultShortcode = data['shortcode'];
} catch (e) {
LogService.warn("UnbanBanCommand", "Non-fatal error getting default ban list");
LogService.warn("UnbanBanCommand", extractRequestError(e));
// Assume no default.
let data = mjolnir.accountData.defaultList!.get();
if (!data || !("shortcode" in data) || typeof data["shortcode"] !== "string") {
data = { shortcode: null };
}
let defaultShortcode: string | null = data['shortcode'];
let argumentIndex = 2;
let ruleType: string | null = null;
@ -101,7 +95,7 @@ export async function parseArguments(roomId: string, event: any, mjolnir: Mjolni
if (replyMessage) {
const reply = RichReply.createFor(roomId, event, replyMessage, replyMessage);
reply["msgtype"] = "m.notice";
await mjolnir.client.sendMessage(roomId, reply);
await mjolnir.client.uncached.sendMessage(roomId, reply);
return null;
}
@ -119,7 +113,7 @@ export async function execBanCommand(roomId: string, event: any, mjolnir: Mjolni
if (!bits) return; // error already handled
await bits.list!.banEntity(bits.ruleType!, bits.entity, bits.reason);
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
await mjolnir.client.uncached.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
}
// !mjolnir unban <shortcode> <user|server|room> <glob> [apply:t/f]
@ -134,7 +128,7 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol
await mjolnir.managementRoomOutput.logMessage(LogLevel.INFO, "UnbanBanCommand", "Unbanning users that match glob: " + bits.entity);
let unbannedSomeone = false;
for (const protectedRoomId of mjolnir.protectedRoomsTracker.getProtectedRooms()) {
const members = await mjolnir.client.getRoomMembers(protectedRoomId, undefined, ['ban'], undefined);
const members = await mjolnir.client.uncached.getRoomMembers(protectedRoomId, undefined, ['ban'], undefined);
await mjolnir.managementRoomOutput.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Found ${members.length} banned user(s)`);
for (const member of members) {
const victim = member.membershipFor;
@ -143,7 +137,7 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol
await mjolnir.managementRoomOutput.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Unbanning ${victim} in ${protectedRoomId}`, protectedRoomId);
if (!mjolnir.config.noop) {
await mjolnir.client.unbanUser(victim, protectedRoomId);
await mjolnir.client.uncached.unbanUser(victim, protectedRoomId);
} else {
await mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "UnbanBanCommand", `Attempted to unban ${victim} in ${protectedRoomId} but Mjolnir is running in no-op mode`, protectedRoomId);
}
@ -159,5 +153,5 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol
}
}
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
await mjolnir.client.uncached.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
}

View File

@ -24,10 +24,10 @@ export async function execWatchCommand(roomId: string, event: any, mjolnir: Mjol
const replyText = "Cannot watch list due to error - is that a valid room alias?";
const reply = RichReply.createFor(roomId, event, replyText, replyText);
reply["msgtype"] = "m.notice";
mjolnir.client.sendMessage(roomId, reply);
mjolnir.client.uncached.sendMessage(roomId, reply);
return;
}
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
await mjolnir.client.uncached.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
}
// !mjolnir unwatch <room alias or ID>
@ -37,8 +37,8 @@ export async function execUnwatchCommand(roomId: string, event: any, mjolnir: Mj
const replyText = "Cannot unwatch list due to error - is that a valid room alias?";
const reply = RichReply.createFor(roomId, event, replyText, replyText);
reply["msgtype"] = "m.notice";
mjolnir.client.sendMessage(roomId, reply);
mjolnir.client.uncached.sendMessage(roomId, reply);
return;
}
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
await mjolnir.client.uncached.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
}

View File

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { extractRequestError, LogService, MatrixClient, UserID } from "matrix-bot-sdk";
import { extractRequestError, LogService, UserID } from "matrix-bot-sdk";
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 { CachingClient } from "../CachingClient";
export const SHORTCODE_EVENT_TYPE = "org.matrix.mjolnir.shortcode";
@ -92,7 +93,7 @@ class PolicyList extends EventEmitter {
* @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.
*/
constructor(public readonly roomId: string, public readonly roomRef: string, private client: MatrixClient) {
constructor(public readonly roomId: string, public readonly roomRef: string, private client: CachingClient) {
super();
this.batcher = new UpdateBatcher(this);
}
@ -156,7 +157,7 @@ class PolicyList extends EventEmitter {
public set listShortcode(newShortcode: string) {
const currentShortcode = this.shortcode;
this.shortcode = newShortcode;
this.client.sendStateEvent(this.roomId, SHORTCODE_EVENT_TYPE, '', { shortcode: this.shortcode }).catch(err => {
this.client.uncached.sendStateEvent(this.roomId, SHORTCODE_EVENT_TYPE, '', { shortcode: this.shortcode }).catch(err => {
LogService.error("PolicyList", extractRequestError(err));
if (this.shortcode === newShortcode) this.shortcode = currentShortcode;
});
@ -219,7 +220,7 @@ class PolicyList extends EventEmitter {
public async banEntity(ruleType: string, entity: string, reason?: string): Promise<void> {
// '@' at the beginning of state keys is reserved.
const stateKey = ruleType === RULE_USER ? '_' + entity.substring(1) : entity;
const event_id = await this.client.sendStateEvent(this.roomId, ruleType, stateKey, {
const event_id = await this.client.uncached.sendStateEvent(this.roomId, ruleType, stateKey, {
entity,
recommendation: Recommendation.Ban,
reason: reason || '<no reason supplied>',
@ -248,14 +249,14 @@ class PolicyList extends EventEmitter {
break;
}
const sendNullState = async (stateType: string, stateKey: string) => {
const event_id = await this.client.sendStateEvent(this.roomId, stateType, stateKey, {});
const event_id = await this.client.uncached.sendStateEvent(this.roomId, stateType, stateKey, {});
this.updateForEvent(event_id);
}
const removeRule = async (rule: ListRule): Promise<void> => {
const stateKey = rule.sourceEvent.state_key;
// We can't cheat and check our state cache because we normalize the event types to the most recent version.
const typesToRemove = (await Promise.all(
typesToCheck.map(stateType => this.client.getRoomStateEvent(this.roomId, stateType, stateKey)
typesToCheck.map(stateType => this.client.uncached.getRoomStateEvent(this.roomId, stateType, stateKey)
.then(_ => stateType) // We need the state type as getRoomState only returns the content, not the top level.
.catch(e => e.statusCode === 404 ? null : Promise.reject(e))))
).filter(e => e); // remove nulls. I don't know why TS still thinks there can be nulls after this??
@ -277,7 +278,7 @@ class PolicyList extends EventEmitter {
public async updateList(): Promise<ListRuleChange[]> {
let changes: ListRuleChange[] = [];
const state = await this.client.getRoomState(this.roomId);
const state = await this.client.uncached.getRoomState(this.roomId);
for (const event of state) {
if (event['state_key'] === '' && event['type'] === SHORTCODE_EVENT_TYPE) {
this.shortcode = (event['content'] || {})['shortcode'] || null;

View File

@ -64,7 +64,7 @@ export class BasicFlooding extends Protection {
if (messageCount >= this.settings.maxPerMinute.value) {
await mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "BasicFlooding", `Banning ${event['sender']} in ${roomId} for flooding (${messageCount} messages in the last minute)`, roomId);
if (!mjolnir.config.noop) {
await mjolnir.client.banUser(event['sender'], roomId, "spam");
await mjolnir.client.uncached.banUser(event['sender'], roomId, "spam");
} else {
await mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "BasicFlooding", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
}
@ -76,7 +76,7 @@ export class BasicFlooding extends Protection {
// Redact all the things the user said too
if (!mjolnir.config.noop) {
for (const eventId of forUser.map(e => e.eventId)) {
await mjolnir.client.redactEvent(roomId, eventId, "spam");
await mjolnir.client.uncached.redactEvent(roomId, eventId, "spam");
}
} else {
await mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "BasicFlooding", `Tried to redact messages for ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);

View File

@ -58,7 +58,7 @@ export class FirstMessageIsImage extends Protection {
if (isMedia && this.justJoined[roomId].includes(event['sender'])) {
await mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Banning ${event['sender']} for posting an image as the first thing after joining in ${roomId}.`);
if (!mjolnir.config.noop) {
await mjolnir.client.banUser(event['sender'], roomId, "spam");
await mjolnir.client.uncached.banUser(event['sender'], roomId, "spam");
} else {
await mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
}
@ -69,7 +69,7 @@ export class FirstMessageIsImage extends Protection {
// Redact the event
if (!mjolnir.config.noop) {
await mjolnir.client.redactEvent(roomId, event['event_id'], "spam");
await mjolnir.client.uncached.redactEvent(roomId, event['event_id'], "spam");
} else {
await mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
}

View File

@ -89,7 +89,7 @@ export class JoinWaveShortCircuit extends Protection {
await mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Setting ${roomId} to invite-only as more than ${this.settings.maxPer.value} users have joined over the last ${this.settings.timescaleMinutes.value} minutes (since ${this.joinBuckets[roomId].lastBucketStart})`, roomId);
if (!mjolnir.config.noop) {
await mjolnir.client.sendStateEvent(roomId, "m.room.join_rules", "", {"join_rule": "invite"})
await mjolnir.client.uncached.sendStateEvent(roomId, "m.room.join_rules", "", {"join_rule": "invite"})
} else {
await mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Tried to set ${roomId} to invite-only, but Mjolnir is running in no-op mode`, roomId);
}

View File

@ -73,16 +73,16 @@ export class TrustedReporters extends Protection {
}
if (reporters.size === this.settings.redactThreshold.value) {
met.push("redact");
await mjolnir.client.redactEvent(roomId, event.id, "abuse detected");
await mjolnir.client.uncached.redactEvent(roomId, event.id, "abuse detected");
}
if (reporters.size === this.settings.banThreshold.value) {
met.push("ban");
await mjolnir.client.banUser(event.userId, roomId, "abuse detected");
await mjolnir.client.uncached.banUser(event.userId, roomId, "abuse detected");
}
if (met.length > 0) {
await mjolnir.client.sendMessage(mjolnir.config.managementRoom, {
await mjolnir.client.uncached.sendMessage(mjolnir.config.managementRoom, {
msgtype: "m.notice",
body: `message ${event.id} reported by ${[...reporters].join(', ')}. `
+ `actions: ${met.join(', ')}`

View File

@ -13,11 +13,12 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { LogLevel, MatrixClient } from "matrix-bot-sdk"
import { LogLevel } from "matrix-bot-sdk"
import { ERROR_KIND_FATAL } from "../ErrorCache";
import { RoomUpdateError } from "../models/RoomUpdateError";
import { redactUserMessagesIn } from "../utils";
import ManagementRoomOutput from "../ManagementRoomOutput";
import { CachingClient } from "../CachingClient";
export interface QueuedRedaction {
/** The room which the redaction will take place in. */
@ -27,7 +28,7 @@ export interface QueuedRedaction {
* Called by the EventRedactionQueue.
* @param client A MatrixClient to use to carry out the redaction.
*/
redact(client: MatrixClient, managementRoom: ManagementRoomOutput): Promise<void>
redact(client: CachingClient, managementRoom: ManagementRoomOutput): Promise<void>
/**
* Used to test whether the redaction is the equivalent to another redaction.
* @param redaction Another QueuedRedaction to test if this redaction is an equivalent to.
@ -47,7 +48,7 @@ export class RedactUserInRoom implements QueuedRedaction {
this.roomId = roomId;
}
public async redact(client: MatrixClient, managementRoom: ManagementRoomOutput) {
public async redact(client: CachingClient, managementRoom: ManagementRoomOutput) {
await managementRoom.logMessage(LogLevel.DEBUG, "Mjolnir", `Redacting events from ${this.userId} in room ${this.roomId}.`);
await redactUserMessagesIn(client, managementRoom, this.userId, [this.roomId]);
}
@ -107,7 +108,7 @@ export class EventRedactionQueue {
* @param limitToRoomId If the roomId is provided, only redactions for that room will be processed.
* @returns A description of any errors encountered by each QueuedRedaction that was processed.
*/
public async process(client: MatrixClient, managementRoom: ManagementRoomOutput, limitToRoomId?: string): Promise<RoomUpdateError[]> {
public async process(client: CachingClient, managementRoom: ManagementRoomOutput, limitToRoomId?: string): Promise<RoomUpdateError[]> {
const errors: RoomUpdateError[] = [];
const redact = async (currentBatch: QueuedRedaction[]) => {
for (const redaction of currentBatch) {

View File

@ -43,7 +43,7 @@ export class UnlistedUserRedactionQueue {
try {
LogService.info("AutomaticRedactionQueue", `Redacting event because the user is listed as bad: ${permalink}`)
if (!mjolnir.config.noop) {
await mjolnir.client.redactEvent(roomId, event['event_id']);
await mjolnir.client.uncached.redactEvent(roomId, event['event_id']);
} else {
await mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "AutomaticRedactionQueue", `Tried to redact ${permalink} but Mjolnir is running in no-op mode`);
}

View File

@ -17,7 +17,6 @@ limitations under the License.
import {
LogLevel,
LogService,
MatrixClient,
MatrixGlob,
getRequestFn,
setRequestFn,
@ -25,6 +24,7 @@ import {
import { ClientRequest, IncomingMessage } from "http";
import { default as parseDuration } from "parse-duration";
import ManagementRoomOutput from "./ManagementRoomOutput";
import { CachingClient } from "./CachingClient";
// Define a few aliases to simplify parsing durations.
@ -77,7 +77,7 @@ export function isTrueJoinEvent(event: any): boolean {
* @param limit The number of messages to redact from most recent first. If the limit is reached then no further messages will be redacted.
* @param noop Whether to operate in noop mode.
*/
export async function redactUserMessagesIn(client: MatrixClient, managementRoom: ManagementRoomOutput, userIdOrGlob: string, targetRoomIds: string[], limit = 1000, noop = false) {
export async function redactUserMessagesIn(client: CachingClient, managementRoom: ManagementRoomOutput, userIdOrGlob: string, targetRoomIds: string[], limit = 1000, noop = false) {
for (const targetRoomId of targetRoomIds) {
await managementRoom.logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Fetching sent messages for ${userIdOrGlob} in ${targetRoomId} to redact...`, targetRoomId);
@ -85,7 +85,7 @@ export async function redactUserMessagesIn(client: MatrixClient, managementRoom:
for (const victimEvent of eventsToRedact) {
await managementRoom.logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Redacting ${victimEvent['event_id']} in ${targetRoomId}`, targetRoomId);
if (!noop) {
await client.redactEvent(targetRoomId, victimEvent['event_id']);
await client.uncached.redactEvent(targetRoomId, victimEvent['event_id']);
} else {
await managementRoom.logMessage(LogLevel.WARN, "utils#redactUserMessagesIn", `Tried to redact ${victimEvent['event_id']} in ${targetRoomId} but Mjolnir is running in no-op mode`, targetRoomId);
}
@ -110,7 +110,7 @@ export async function redactUserMessagesIn(client: MatrixClient, managementRoom:
* The callback will only be called if there are any relevant events.
* @returns {Promise<void>} Resolves when either: the limit has been reached, no relevant events could be found or there is no more timeline to paginate.
*/
export async function getMessagesByUserIn(client: MatrixClient, sender: string, roomId: string, limit: number, cb: (events: any[]) => void): Promise<void> {
export async function getMessagesByUserIn(client: CachingClient, sender: string, roomId: string, limit: number, cb: (events: any[]) => void): Promise<void> {
const isGlob = sender.includes("*");
const roomEventFilter = {
rooms: [roomId],
@ -154,7 +154,7 @@ export async function getMessagesByUserIn(client: MatrixClient, sender: string,
... from ? { from } : {}
};
LogService.info("utils", "Backfilling with token: " + from);
return client.doRequest("GET", `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/messages`, qs);
return client.uncached.doRequest("GET", `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/messages`, qs);
}
let processed = 0;

View File

@ -7,6 +7,7 @@ import { getFirstReaction } from "./commands/commandUtils";
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";
import { CachingClient } from "../../src/CachingClient";
/**
* Create a policy rule in a policy room.
@ -30,32 +31,32 @@ describe("Test: Updating the PolicyList", function() {
it("Calculates what has changed correctly.", async function() {
this.timeout(10000);
const mjolnir: Mjolnir = this.mjolnir!
const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] });
const moderator: MatrixClient = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
const banListId = await mjolnir.client.uncached.createRoom({ invite: [await moderator.getUserId()] });
const banList = new PolicyList(banListId, banListId, mjolnir.client);
await mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
await mjolnir.client.uncached.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
assert.equal(banList.allRules.length, 0);
// Test adding a new rule
await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@added:localhost:9999', '');
await createPolicyRule(mjolnir.client.uncached, banListId, RULE_USER, '@added:localhost:9999', '');
let changes: ListRuleChange[] = await banList.updateList();
assert.equal(changes.length, 1, 'There should only be one change');
assert.equal(changes[0].changeType, ChangeType.Added);
assert.equal(changes[0].sender, await mjolnir.client.getUserId());
assert.equal(changes[0].sender, await mjolnir.client.uncached.getUserId());
assert.equal(banList.userRules.length, 1);
assert.equal(banList.allRules.length, 1);
// Test modifiying a rule
let originalEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@modified:localhost:9999', '');
let originalEventId = await createPolicyRule(mjolnir.client.uncached, banListId, RULE_USER, '@modified:localhost:9999', '');
await banList.updateList();
let modifyingEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@modified:localhost:9999', 'modified reason');
let modifyingEventId = await createPolicyRule(mjolnir.client.uncached, banListId, RULE_USER, '@modified:localhost:9999', 'modified reason');
changes = await banList.updateList();
assert.equal(changes.length, 1);
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].event['event_id'], modifyingEventId);
let modifyingAgainEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@modified:localhost:9999', 'modified again');
let modifyingAgainEventId = await createPolicyRule(mjolnir.client.uncached, banListId, RULE_USER, '@modified:localhost:9999', 'modified again');
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Modified);
@ -64,10 +65,10 @@ describe("Test: Updating the PolicyList", function() {
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
const redactThis = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@redacted:localhost:9999', '');
const redactThis = await createPolicyRule(mjolnir.client.uncached, banListId, RULE_USER, '@redacted:localhost:9999', '');
await banList.updateList();
assert.equal(banList.userRules.filter(r => r.entity === '@redacted:localhost:9999').length, 1);
await mjolnir.client.redactEvent(banListId, redactThis);
await mjolnir.client.uncached.redactEvent(banListId, redactThis);
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Removed);
@ -79,10 +80,10 @@ describe("Test: Updating the PolicyList", function() {
// Test soft redaction of a rule
const softRedactedEntity = '@softredacted:localhost:9999'
await createPolicyRule(mjolnir.client, banListId, RULE_USER, softRedactedEntity, '');
await createPolicyRule(mjolnir.client.uncached, banListId, RULE_USER, softRedactedEntity, '');
await banList.updateList();
assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 1);
await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {});
await mjolnir.client.uncached.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {});
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Removed);
@ -92,25 +93,25 @@ describe("Test: Updating the PolicyList", function() {
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
await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {});
await mjolnir.client.uncached.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {});
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(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.
originalEventId = await createPolicyRule(mjolnir.client, banListId, 'org.matrix.mjolnir.rule.user', '@old:localhost:9999', '');
originalEventId = await createPolicyRule(mjolnir.client.uncached, banListId, 'org.matrix.mjolnir.rule.user', '@old:localhost:9999', '');
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Added);
assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1);
modifyingEventId = await createPolicyRule(mjolnir.client, banListId, 'm.room.rule.user', '@old:localhost:9999', 'modified reason');
modifyingEventId = await createPolicyRule(mjolnir.client.uncached, banListId, 'm.room.rule.user', '@old:localhost:9999', 'modified reason');
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Modified);
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(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1);
modifyingAgainEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@old:localhost:9999', 'changes again');
modifyingAgainEventId = await createPolicyRule(mjolnir.client.uncached, banListId, RULE_USER, '@old:localhost:9999', 'changes again');
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Modified);
@ -121,18 +122,18 @@ describe("Test: Updating the PolicyList", 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);
const mjolnir: Mjolnir = this.mjolnir!
const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" }} );
const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] });
const moderator: MatrixClient = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" }} );
const banListId = await mjolnir.client.uncached.createRoom({ invite: [await moderator.getUserId()] });
const banList = new PolicyList(banListId, banListId, mjolnir.client);
await mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
await mjolnir.client.uncached.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
const entity = '@old:localhost:9999';
let originalEventId = await createPolicyRule(mjolnir.client, banListId, 'm.room.rule.user', entity, '');
let originalEventId = await createPolicyRule(mjolnir.client.uncached, banListId, 'm.room.rule.user', entity, '');
let changes = await banList.updateList();
assert.equal(changes.length, 1);
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...')
let softRedactingEventId = await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {});
let softRedactingEventId = await mjolnir.client.uncached.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {});
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Removed);
@ -142,18 +143,18 @@ describe("Test: Updating the PolicyList", 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() {
const mjolnir: Mjolnir = this.mjolnir!
const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] });
const moderator: MatrixClient = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
const banListId = await mjolnir.client.uncached.createRoom({ invite: [await moderator.getUserId()] });
const banList = new PolicyList(banListId, banListId, mjolnir.client);
await mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
await mjolnir.client.uncached.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
const entity = '@old:localhost:9999';
let originalEventId = await createPolicyRule(mjolnir.client, banListId, 'm.room.rule.user', entity, '');
let originalEventId = await createPolicyRule(mjolnir.client.uncached, banListId, 'm.room.rule.user', entity, '');
let changes = await banList.updateList();
assert.equal(changes.length, 1);
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...')
let updatedEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, entity, '');
let updatedEventId = await createPolicyRule(mjolnir.client.uncached, banListId, RULE_USER, entity, '');
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
// is the rule type. The actual content is identical.
@ -164,13 +165,13 @@ describe("Test: Updating the PolicyList", function() {
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.
await mjolnir.client.sendStateEvent(banListId, 'm.room.rule.user', `rule:${entity}`, {});
await mjolnir.client.uncached.sendStateEvent(banListId, 'm.room.rule.user', `rule:${entity}`, {});
changes = await banList.updateList();
assert.equal(changes.length, 0);
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.
let softRedactingEventId = await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {});
let softRedactingEventId = await mjolnir.client.uncached.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {});
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Removed);
@ -180,10 +181,10 @@ describe("Test: Updating the PolicyList", function() {
})
it('Test: PolicyList Supports all entity types.', async function () {
const mjolnir: Mjolnir = this.mjolnir!
const banListId = await mjolnir.client.createRoom();
const banListId = await mjolnir.client.uncached.createRoom();
const banList = new PolicyList(banListId, banListId, mjolnir.client);
for (let i = 0; i < ALL_RULE_TYPES.length; i++) {
await createPolicyRule(mjolnir.client, banListId, ALL_RULE_TYPES[i], `*${i}*`, '');
await createPolicyRule(mjolnir.client.uncached, banListId, ALL_RULE_TYPES[i], `*${i}*`, '');
}
let changes: ListRuleChange[] = await banList.updateList();
assert.equal(changes.length, ALL_RULE_TYPES.length);
@ -194,13 +195,13 @@ describe("Test: Updating the PolicyList", 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() {
const mjolnir: Mjolnir = this.mjolnir!
const banListId = await mjolnir.client.createRoom();
const banListId = await mjolnir.client.uncached.createRoom();
const banList = new PolicyList(banListId, banListId, mjolnir.client);
await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, 'exmaple.org', '', { recommendation: 'something that is not m.ban' });
await createPolicyRule(mjolnir.client.uncached, banListId, RULE_SERVER, 'exmaple.org', '', { recommendation: 'something that is not m.ban' });
let changes: ListRuleChange[] = await banList.updateList();
assert.equal(changes.length, 1, 'There should only be one change');
assert.equal(changes[0].changeType, ChangeType.Added);
assert.equal(changes[0].sender, await mjolnir.client.getUserId());
assert.equal(changes[0].sender, await mjolnir.client.uncached.getUserId());
// 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, `We should have an empty serverRules, got ${JSON.stringify(banList.serverRules)}`);
assert.equal(banList.allRules.length, 0, `We should have an empty allRules, got ${JSON.stringify(banList.allRules)}`);
@ -210,12 +211,12 @@ describe('Test: We do not respond to recommendations other than m.ban in the Pol
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() {
const mjolnir: Mjolnir = this.mjolnir
const serverName = new UserID(await mjolnir.client.getUserId()).domain;
const banListId = await mjolnir.client.createRoom();
const serverName = new UserID(await mjolnir.client.uncached.getUserId()).domain;
const banListId = await mjolnir.client.uncached.createRoom();
const banList = new PolicyList(banListId, banListId, mjolnir.client);
await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, serverName, '');
await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, 'evil.com', '');
await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, '*', '');
await createPolicyRule(mjolnir.client.uncached, banListId, RULE_SERVER, serverName, '');
await createPolicyRule(mjolnir.client.uncached, banListId, RULE_SERVER, 'evil.com', '');
await createPolicyRule(mjolnir.client.uncached, banListId, RULE_SERVER, '*', '');
// We should still intern the matching rules rule.
let changes: ListRuleChange[] = await banList.updateList();
assert.equal(banList.serverRules.length, 3);
@ -231,16 +232,16 @@ 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() {
it('Will batch ACL updates if we spam rules into a PolicyList', async function() {
const mjolnir: Mjolnir = this.mjolnir!
const serverName: string = new UserID(await mjolnir.client.getUserId()).domain
const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
const serverName: string = new UserID(await mjolnir.client.uncached.getUserId()).domain
const moderator: MatrixClient = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
await moderator.joinRoom(mjolnir.managementRoomId);
const mjolnirId = await mjolnir.client.getUserId();
const mjolnirId = await mjolnir.client.uncached.getUserId();
// Setup some protected rooms so we can check their ACL state later.
const protectedRooms: string[] = [];
for (let i = 0; i < 5; i++) {
const room = await moderator.createRoom({ invite: [mjolnirId] });
await mjolnir.client.joinRoom(room);
await mjolnir.client.uncached.joinRoom(room);
await moderator.setUserPowerLevel(mjolnirId, room, 100);
await mjolnir.addProtectedRoom(room);
protectedRooms.push(room);
@ -250,13 +251,13 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun
await mjolnir.protectedRoomsTracker.syncLists(mjolnir.config.verboseLogging);
await Promise.all(protectedRooms.map(async room => {
// We're going to need timeline pagination I'm afraid.
const roomAcl = await mjolnir.client.getRoomStateEvent(room, "m.room.server_acl", "");
const roomAcl = await mjolnir.client.uncached.getRoomStateEvent(room, "m.room.server_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.
const banListId = await moderator.createRoom({ invite: [mjolnirId] });
await mjolnir.client.joinRoom(banListId);
await mjolnir.client.uncached.joinRoom(banListId);
await mjolnir.watchList(Permalinks.forRoom(banListId));
const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*");
const evilServerCount = 200;
@ -278,7 +279,7 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun
// 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 => {
const roomAcl = await mjolnir.client.getRoomStateEvent(room, "m.room.server_acl", "");
const roomAcl = await mjolnir.client.uncached.getRoomStateEvent(room, "m.room.server_acl", "");
if (!acl.matches(roomAcl)) {
assert.fail(`Room ${room} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`)
}
@ -299,32 +300,32 @@ describe('Test: unbaning entities via the PolicyList.', function() {
afterEach(function() { this.moderator?.stop(); });
it('Will remove rules that have legacy types', async function() {
const mjolnir: Mjolnir = this.mjolnir!
const serverName: string = new UserID(await mjolnir.client.getUserId()).domain
const serverName: string = new UserID(await mjolnir.client.uncached.getUserId()).domain
const moderator: MatrixClient = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
this.moderator = moderator;
await moderator.joinRoom(mjolnir.managementRoomId);
const mjolnirId = await mjolnir.client.getUserId();
const mjolnirId = await mjolnir.client.uncached.getUserId();
// We'll make 1 protected room to test ACLs in.
const protectedRoom = await moderator.createRoom({ invite: [mjolnirId] });
await mjolnir.client.joinRoom(protectedRoom);
await mjolnir.client.uncached.joinRoom(protectedRoom);
await moderator.setUserPowerLevel(mjolnirId, protectedRoom, 100);
await mjolnir.addProtectedRoom(protectedRoom);
// If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point.
await mjolnir.protectedRoomsTracker.syncLists(mjolnir.config.verboseLogging);
// If this is not present, then it means the room isn't being protected, which is really bad.
const roomAcl = await mjolnir.client.getRoomStateEvent(protectedRoom, "m.room.server_acl", "");
const roomAcl = await mjolnir.client.uncached.getRoomStateEvent(protectedRoom, "m.room.server_acl", "");
assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.');
// Create some legacy rules on a PolicyList.
const banListId = await moderator.createRoom({ invite: [mjolnirId] });
await moderator.setUserPowerLevel(await mjolnir.client.getUserId(), banListId, 100);
await moderator.setUserPowerLevel(await mjolnir.client.uncached.getUserId(), banListId, 100);
await moderator.sendStateEvent(banListId, 'org.matrix.mjolnir.shortcode', '', { shortcode: "unban-test" });
await mjolnir.client.joinRoom(banListId);
await mjolnir.client.uncached.joinRoom(banListId);
await mjolnir.watchList(Permalinks.forRoom(banListId));
// we use this to compare changes.
const banList = new PolicyList(banListId, banListId, moderator);
const banList = new PolicyList(banListId, banListId, new CachingClient(moderator));
// 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)
const olderBadServer = "old.evil.example"
@ -341,7 +342,7 @@ describe('Test: unbaning entities via the PolicyList.', function() {
// Check that we have setup our test properly and therefore evil.example is banned.
const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*").denyServer(olderBadServer).denyServer(newerBadServer);
const protectedAcl = await mjolnir.client.getRoomStateEvent(protectedRoom, "m.room.server_acl", "");
const protectedAcl = await mjolnir.client.uncached.getRoomStateEvent(protectedRoom, "m.room.server_acl", "");
if (!acl.matches(protectedAcl)) {
assert.fail(`Room ${protectedRoom} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`);
}
@ -363,7 +364,7 @@ describe('Test: unbaning entities via the PolicyList.', function() {
// Confirm that the server is unbanned.
await banList.updateList();
assert.equal(banList.allRules.length, 0);
const aclAfter = await mjolnir.client.getRoomStateEvent(protectedRoom, "m.room.server_acl", "");
const aclAfter = await mjolnir.client.uncached.getRoomStateEvent(protectedRoom, "m.room.server_acl", "");
assert.equal(aclAfter.deny.length, 0, 'Should be no servers denied anymore');
})
})
@ -372,16 +373,16 @@ describe('Test: should apply bans to the most recently active rooms first', func
it('Applies bans to the most recently active rooms first', async function() {
this.timeout(180000)
const mjolnir: Mjolnir = this.mjolnir!
const serverName: string = new UserID(await mjolnir.client.getUserId()).domain
const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
const serverName: string = new UserID(await mjolnir.client.uncached.getUserId()).domain
const moderator: MatrixClient = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
await moderator.joinRoom(mjolnir.managementRoomId);
const mjolnirId = await mjolnir.client.getUserId();
const mjolnirId = await mjolnir.client.uncached.getUserId();
// Setup some protected rooms so we can check their ACL state later.
const protectedRooms: string[] = [];
for (let i = 0; i < 10; i++) {
const room = await moderator.createRoom({ invite: [mjolnirId] });
await mjolnir.client.joinRoom(room);
await mjolnir.client.uncached.joinRoom(room);
await moderator.setUserPowerLevel(mjolnirId, room, 100);
await mjolnir.addProtectedRoom(room);
protectedRooms.push(room);
@ -390,13 +391,13 @@ describe('Test: should apply bans to the most recently active rooms first', func
// If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point.
await mjolnir.protectedRoomsTracker.syncLists(mjolnir.config.verboseLogging);
await Promise.all(protectedRooms.map(async room => {
const roomAcl = await mjolnir.client.getRoomStateEvent(room, "m.room.server_acl", "").catch(e => e.statusCode === 404 ? { deny: [] } : Promise.reject(e));
const roomAcl = await mjolnir.client.uncached.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.');
}));
// 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] });
await mjolnir.client.joinRoom(banListId);
await mjolnir.client.uncached.joinRoom(banListId);
await mjolnir.watchList(Permalinks.forRoom(banListId));
await mjolnir.protectedRoomsTracker.syncLists(mjolnir.config.verboseLogging);
@ -423,7 +424,7 @@ describe('Test: should apply bans to the most recently active rooms first', func
// collect all the rooms that received an ACL event.
const aclRooms: any[] = await new Promise(async resolve => {
const rooms: any[] = [];
this.mjolnir.client.on('room.event', (room: string, event: any) => {
this.mjolnir.client.uncached.on('room.event', (room: string, event: any) => {
if (protectedRooms.includes(room)) {
rooms.push(room);
}