mirror of
https://github.com/matrix-org/mjolnir.git
synced 2024-10-01 01:36:06 -04:00
Towards opinions in PolicyLists. (#336)
Towards opinions in PolicyLists. This changeset is part of an ongoing effort to implement "opinions" within policy lists, as per MSC3847. For the time being: - we rename BanList into PolicyList; - we cleanup a little dead code; - we replace a few `string`s with `enum`; - `ListRule` becomes an abstract class with two concrete subclasses `ListRuleBan` and `ListRuleOpinion`.
This commit is contained in:
parent
4aad5c455d
commit
829e1bd0aa
103
src/Mjolnir.ts
103
src/Mjolnir.ts
@ -27,7 +27,7 @@ import {
|
||||
TextualMessageEventContent
|
||||
} from "matrix-bot-sdk";
|
||||
|
||||
import BanList, { ALL_RULE_TYPES as ALL_BAN_LIST_RULE_TYPES, ListRuleChange, RULE_ROOM, RULE_SERVER, RULE_USER } from "./models/BanList";
|
||||
import { ALL_RULE_TYPES as ALL_BAN_LIST_RULE_TYPES, RULE_ROOM, RULE_SERVER, RULE_USER } from "./models/ListRule";
|
||||
import { applyServerAcls } from "./actions/ApplyAcl";
|
||||
import { RoomUpdateError } from "./models/RoomUpdateError";
|
||||
import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler";
|
||||
@ -50,6 +50,7 @@ import RuleServer from "./models/RuleServer";
|
||||
import { RoomMemberManager } from "./RoomMembers";
|
||||
import { ProtectedRoomActivityTracker } from "./queues/ProtectedRoomActivityTracker";
|
||||
import { ThrottlingQueue } from "./queues/ThrottlingQueue";
|
||||
import PolicyList, { ListRuleChange } from "./models/PolicyList";
|
||||
|
||||
const levelToFn = {
|
||||
[LogLevel.DEBUG.toString()]: LogService.debug,
|
||||
@ -153,7 +154,7 @@ export class Mjolnir {
|
||||
* @returns A new Mjolnir instance that can be started without further setup.
|
||||
*/
|
||||
static async setupMjolnirFromConfig(client: MatrixClient): Promise<Mjolnir> {
|
||||
const banLists: BanList[] = [];
|
||||
const policyLists: PolicyList[] = [];
|
||||
const protectedRooms: { [roomId: string]: string } = {};
|
||||
const joinedRooms = await client.getJoinedRooms();
|
||||
// Ensure we're also joined to the rooms we're protecting
|
||||
@ -178,7 +179,7 @@ export class Mjolnir {
|
||||
}
|
||||
|
||||
const ruleServer = config.web.ruleServer ? new RuleServer() : null;
|
||||
const mjolnir = new Mjolnir(client, managementRoomId, protectedRooms, banLists, ruleServer);
|
||||
const mjolnir = new Mjolnir(client, managementRoomId, protectedRooms, policyLists, ruleServer);
|
||||
await mjolnir.logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status.");
|
||||
Mjolnir.addJoinOnInviteListener(mjolnir, client, config);
|
||||
return mjolnir;
|
||||
@ -192,9 +193,9 @@ export class Mjolnir {
|
||||
* If `config.protectAllJoinedRooms` is specified, then `protectedRooms` will be all joined rooms except watched banlists that we can't protect (because they aren't curated by us).
|
||||
*/
|
||||
public readonly protectedRooms: { [roomId: string]: string },
|
||||
private banLists: BanList[],
|
||||
private policyLists: PolicyList[],
|
||||
// Combines the rules from ban lists so they can be served to a homeserver module or another consumer.
|
||||
public readonly ruleServer: RuleServer|null,
|
||||
public readonly ruleServer: RuleServer | null,
|
||||
) {
|
||||
this.explicitlyProtectedRoomIds = Object.keys(this.protectedRooms);
|
||||
|
||||
@ -275,8 +276,8 @@ export class Mjolnir {
|
||||
this.taskQueue = new ThrottlingQueue(this, config.backgroundDelayMS);
|
||||
}
|
||||
|
||||
public get lists(): BanList[] {
|
||||
return this.banLists;
|
||||
public get lists(): PolicyList[] {
|
||||
return this.policyLists;
|
||||
}
|
||||
|
||||
public get state(): string {
|
||||
@ -343,7 +344,7 @@ export class Mjolnir {
|
||||
} catch (e) {
|
||||
LogService.warn("Mjolnir", extractRequestError(e));
|
||||
}
|
||||
await this.buildWatchedBanLists();
|
||||
await this.buildWatchedPolicyLists();
|
||||
this.applyUnprotectedRooms();
|
||||
|
||||
if (config.verifyPermissionsOnStartup) {
|
||||
@ -554,12 +555,12 @@ export class Mjolnir {
|
||||
const validatedSettings: { [setting: string]: any } = {}
|
||||
for (let [key, value] of Object.entries(savedSettings)) {
|
||||
if (
|
||||
// is this a setting name with a known parser?
|
||||
key in settingDefinitions
|
||||
// is the datatype of this setting's value what we expect?
|
||||
&& typeof(settingDefinitions[key].value) === typeof(value)
|
||||
// is this setting's value valid for the setting?
|
||||
&& settingDefinitions[key].validate(value)
|
||||
// is this a setting name with a known parser?
|
||||
key in settingDefinitions
|
||||
// is the datatype of this setting's value what we expect?
|
||||
&& typeof (settingDefinitions[key].value) === typeof (value)
|
||||
// is this setting's value valid for the setting?
|
||||
&& settingDefinitions[key].validate(value)
|
||||
) {
|
||||
validatedSettings[key] = value;
|
||||
} else {
|
||||
@ -593,8 +594,8 @@ export class Mjolnir {
|
||||
if (!(key in protection.settings)) {
|
||||
throw new ProtectionSettingValidationError(`Failed to find protection setting by name: ${key}`);
|
||||
}
|
||||
if (typeof(protection.settings[key].value) !== typeof(value)) {
|
||||
throw new ProtectionSettingValidationError(`Invalid type for protection setting: ${key} (${typeof(value)})`);
|
||||
if (typeof (protection.settings[key].value) !== typeof (value)) {
|
||||
throw new ProtectionSettingValidationError(`Invalid type for protection setting: ${key} (${typeof (value)})`);
|
||||
}
|
||||
if (!protection.settings[key].validate(value)) {
|
||||
throw new ProtectionSettingValidationError(`Invalid value for protection setting: ${key} (${value})`);
|
||||
@ -644,16 +645,16 @@ export class Mjolnir {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for constructing `BanList`s and making sure they have the right listeners set up.
|
||||
* @param roomId The room id for the `BanList`.
|
||||
* @param roomRef A reference (matrix.to URL) for the `BanList`.
|
||||
* 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 addBanList(roomId: string, roomRef: string): Promise<BanList> {
|
||||
const list = new BanList(roomId, roomRef, this.client);
|
||||
private async addPolicyList(roomId: string, roomRef: string): Promise<PolicyList> {
|
||||
const list = new PolicyList(roomId, roomRef, this.client);
|
||||
this.ruleServer?.watch(list);
|
||||
list.on('BanList.batch', this.syncWithBanList.bind(this));
|
||||
list.on('PolicyList.batch', this.syncWithPolicyList.bind(this));
|
||||
await list.updateList();
|
||||
this.banLists.push(list);
|
||||
this.policyLists.push(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
@ -667,7 +668,7 @@ export class Mjolnir {
|
||||
return this.protections.get(protectionName) ?? null;
|
||||
}
|
||||
|
||||
public async watchList(roomRef: string): Promise<BanList | null> {
|
||||
public async watchList(roomRef: string): Promise<PolicyList | null> {
|
||||
const joinedRooms = await this.client.getJoinedRooms();
|
||||
const permalink = Permalinks.parseUrl(roomRef);
|
||||
if (!permalink.roomIdOrAlias) return null;
|
||||
@ -677,37 +678,37 @@ export class Mjolnir {
|
||||
await this.client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
|
||||
}
|
||||
|
||||
if (this.banLists.find(b => b.roomId === roomId)) return null;
|
||||
if (this.policyLists.find(b => b.roomId === roomId)) return null;
|
||||
|
||||
const list = await this.addBanList(roomId, roomRef);
|
||||
const list = await this.addPolicyList(roomId, roomRef);
|
||||
|
||||
await this.client.setAccountData(WATCHED_LISTS_EVENT_TYPE, {
|
||||
references: this.banLists.map(b => b.roomRef),
|
||||
references: this.policyLists.map(b => b.roomRef),
|
||||
});
|
||||
|
||||
await this.warnAboutUnprotectedBanListRoom(roomId);
|
||||
await this.warnAboutUnprotectedPolicyListRoom(roomId);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public async unwatchList(roomRef: string): Promise<BanList | null> {
|
||||
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.banLists.find(b => b.roomId === roomId) || null;
|
||||
const list = this.policyLists.find(b => b.roomId === roomId) || null;
|
||||
if (list) {
|
||||
this.banLists.splice(this.banLists.indexOf(list), 1);
|
||||
this.policyLists.splice(this.policyLists.indexOf(list), 1);
|
||||
this.ruleServer?.unwatch(list);
|
||||
}
|
||||
|
||||
await this.client.setAccountData(WATCHED_LISTS_EVENT_TYPE, {
|
||||
references: this.banLists.map(b => b.roomRef),
|
||||
references: this.policyLists.map(b => b.roomRef),
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
public async warnAboutUnprotectedBanListRoom(roomId: string) {
|
||||
public async warnAboutUnprotectedPolicyListRoom(roomId: string) {
|
||||
if (!config.protectAllJoinedRooms) return; // doesn't matter
|
||||
if (this.explicitlyProtectedRoomIds.includes(roomId)) return; // explicitly protected
|
||||
|
||||
@ -735,8 +736,8 @@ export class Mjolnir {
|
||||
}
|
||||
}
|
||||
|
||||
private async buildWatchedBanLists() {
|
||||
this.banLists = [];
|
||||
private async buildWatchedPolicyLists() {
|
||||
this.policyLists = [];
|
||||
const joinedRooms = await this.client.getJoinedRooms();
|
||||
|
||||
let watchedListsEvent: { references?: string[] } | null = null;
|
||||
@ -755,8 +756,8 @@ export class Mjolnir {
|
||||
await this.client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
|
||||
}
|
||||
|
||||
await this.warnAboutUnprotectedBanListRoom(roomId);
|
||||
await this.addBanList(roomId, roomRef);
|
||||
await this.warnAboutUnprotectedPolicyListRoom(roomId);
|
||||
await this.addPolicyList(roomId, roomRef);
|
||||
}
|
||||
}
|
||||
|
||||
@ -882,15 +883,15 @@ export class Mjolnir {
|
||||
* @param verbose Whether to report any errors to the management room.
|
||||
*/
|
||||
public async syncLists(verbose = true) {
|
||||
for (const list of this.banLists) {
|
||||
for (const list of this.policyLists) {
|
||||
const changes = await list.updateList();
|
||||
await this.printBanlistChanges(changes, list, true);
|
||||
}
|
||||
|
||||
let hadErrors = false;
|
||||
const [aclErrors, banErrors] = await Promise.all([
|
||||
applyServerAcls(this.banLists, this.protectedRoomsByActivity(), this),
|
||||
applyUserBans(this.banLists, this.protectedRoomsByActivity(), this)
|
||||
applyServerAcls(this.policyLists, this.protectedRoomsByActivity(), this),
|
||||
applyUserBans(this.policyLists, this.protectedRoomsByActivity(), this)
|
||||
]);
|
||||
const redactionErrors = await this.processRedactionQueue();
|
||||
hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:");
|
||||
@ -912,16 +913,16 @@ export class Mjolnir {
|
||||
/**
|
||||
* Pulls any changes to the rules that are in a policy room and updates all protected rooms
|
||||
* with those changes. Does not fail if there are errors updating the room, these are reported to the management room.
|
||||
* @param banList The `BanList` which we will check for changes and apply them to all protected rooms.
|
||||
* @param policyList The `PolicyList` which we will check for changes and apply them to all protected rooms.
|
||||
* @returns When all of the protected rooms have been updated.
|
||||
*/
|
||||
private async syncWithBanList(banList: BanList): Promise<void> {
|
||||
const changes = await banList.updateList();
|
||||
private async syncWithPolicyList(policyList: PolicyList): Promise<void> {
|
||||
const changes = await policyList.updateList();
|
||||
|
||||
let hadErrors = false;
|
||||
const [aclErrors, banErrors] = await Promise.all([
|
||||
applyServerAcls(this.banLists, this.protectedRoomsByActivity(), this),
|
||||
applyUserBans(this.banLists, this.protectedRoomsByActivity(), this)
|
||||
applyServerAcls(this.policyLists, this.protectedRoomsByActivity(), this),
|
||||
applyUserBans(this.policyLists, this.protectedRoomsByActivity(), this)
|
||||
]);
|
||||
const redactionErrors = await this.processRedactionQueue();
|
||||
hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:");
|
||||
@ -939,7 +940,7 @@ export class Mjolnir {
|
||||
});
|
||||
}
|
||||
// This can fail if the change is very large and it is much less important than applying bans, so do it last.
|
||||
await this.printBanlistChanges(changes, banList, true);
|
||||
await this.printBanlistChanges(changes, policyList, true);
|
||||
}
|
||||
|
||||
private async handleConsequence(protection: Protection, roomId: string, eventId: string, sender: string, consequence: Consequence) {
|
||||
@ -989,10 +990,10 @@ export class Mjolnir {
|
||||
|
||||
// Check for updated ban lists before checking protected rooms - the ban lists might be protected
|
||||
// themselves.
|
||||
const banList = this.banLists.find(list => list.roomId === roomId);
|
||||
if (banList !== undefined) {
|
||||
const policyList = this.policyLists.find(list => list.roomId === roomId);
|
||||
if (policyList !== undefined) {
|
||||
if (ALL_BAN_LIST_RULE_TYPES.includes(event['type']) || event['type'] === 'm.room.redaction') {
|
||||
banList.updateForEvent(event)
|
||||
policyList.updateForEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1037,7 +1038,7 @@ export class Mjolnir {
|
||||
// we cannot eagerly ban users (that is to ban them when they have never been a member)
|
||||
// as they can be force joined to a room they might not have known existed.
|
||||
// Only apply bans and then redactions in the room we are currently looking at.
|
||||
const banErrors = await applyUserBans(this.banLists, [roomId], this);
|
||||
const banErrors = await applyUserBans(this.policyLists, [roomId], this);
|
||||
const redactionErrors = await this.processRedactionQueue(roomId);
|
||||
await this.printActionResult(banErrors);
|
||||
await this.printActionResult(redactionErrors);
|
||||
@ -1051,7 +1052,7 @@ export class Mjolnir {
|
||||
* @param ignoreSelf Whether to exclude changes that have been made by Mjolnir.
|
||||
* @returns true if the message was sent, false if it wasn't (because there there were no changes to report).
|
||||
*/
|
||||
private async printBanlistChanges(changes: ListRuleChange[], list: BanList, ignoreSelf = false): Promise<boolean> {
|
||||
private async printBanlistChanges(changes: ListRuleChange[], list: PolicyList, ignoreSelf = false): Promise<boolean> {
|
||||
if (ignoreSelf) {
|
||||
const sender = await this.client.getUserId();
|
||||
changes = changes.filter(change => change.sender !== sender);
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import BanList from "../models/BanList";
|
||||
import PolicyList from "../models/PolicyList";
|
||||
import { ServerAcl } from "../models/ServerAcl";
|
||||
import { RoomUpdateError } from "../models/RoomUpdateError";
|
||||
import { Mjolnir } from "../Mjolnir";
|
||||
@ -26,11 +26,11 @@ import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
|
||||
* Applies the server ACLs represented by the ban lists to the provided rooms, returning the
|
||||
* room IDs that could not be updated and their error.
|
||||
* Does not update the banLists before taking their rules to build the server ACL.
|
||||
* @param {BanList[]} lists The lists to construct ACLs from.
|
||||
* @param {PolicyList[]} lists The lists to construct ACLs from.
|
||||
* @param {string[]} roomIds The room IDs to apply the ACLs in.
|
||||
* @param {Mjolnir} mjolnir The Mjolnir client to apply the ACLs with.
|
||||
*/
|
||||
export async function applyServerAcls(lists: BanList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
|
||||
export async function applyServerAcls(lists: PolicyList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
|
||||
const serverName: string = new UserID(await mjolnir.client.getUserId()).domain;
|
||||
|
||||
// Construct a server ACL first
|
||||
@ -78,7 +78,7 @@ export async function applyServerAcls(lists: BanList[], roomIds: string[], mjoln
|
||||
} catch (e) {
|
||||
const message = e.message || (e.body ? e.body.error : '<no message>');
|
||||
const kind = message && message.includes("You don't have permission to post that to the room") ? ERROR_KIND_PERMISSION : ERROR_KIND_FATAL;
|
||||
errors.push({roomId, errorMessage: message, errorKind: kind});
|
||||
errors.push({ roomId, errorMessage: message, errorKind: kind });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import BanList from "../models/BanList";
|
||||
import PolicyList from "../models/PolicyList";
|
||||
import { RoomUpdateError } from "../models/RoomUpdateError";
|
||||
import { Mjolnir } from "../Mjolnir";
|
||||
import config from "../config";
|
||||
@ -24,11 +24,11 @@ import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
|
||||
/**
|
||||
* Applies the member bans represented by the ban lists to the provided rooms, returning the
|
||||
* room IDs that could not be updated and their error.
|
||||
* @param {BanList[]} lists The lists to determine bans from.
|
||||
* @param {PolicyList[]} lists The lists to determine bans from.
|
||||
* @param {string[]} roomIds The room IDs to apply the bans in.
|
||||
* @param {Mjolnir} mjolnir The Mjolnir client to apply the bans with.
|
||||
*/
|
||||
export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
|
||||
export async function applyUserBans(lists: PolicyList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
|
||||
// We can only ban people who are not already banned, and who match the rules.
|
||||
const errors: RoomUpdateError[] = [];
|
||||
for (const roomId of roomIds) {
|
||||
@ -41,12 +41,12 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir
|
||||
if (config.fasterMembershipChecks) {
|
||||
const memberIds = await mjolnir.client.getJoinedRoomMembers(roomId);
|
||||
members = memberIds.map(u => {
|
||||
return {userId: u, membership: "join"};
|
||||
return { userId: u, membership: "join" };
|
||||
});
|
||||
} else {
|
||||
const state = await mjolnir.client.getRoomState(roomId);
|
||||
members = state.filter(s => s['type'] === 'm.room.member' && !!s['state_key']).map(s => {
|
||||
return {userId: s['state_key'], membership: s['content'] ? s['content']['membership'] : 'leave'};
|
||||
return { userId: s['state_key'], membership: s['content'] ? s['content']['membership'] : 'leave' };
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -28,8 +28,10 @@ import { execRedactCommand } from "./RedactCommand";
|
||||
import { execImportCommand } from "./ImportCommand";
|
||||
import { execSetDefaultListCommand } from "./SetDefaultBanListCommand";
|
||||
import { execDeactivateCommand } from "./DeactivateCommand";
|
||||
import { execDisableProtection, execEnableProtection, execListProtections, execConfigGetProtection,
|
||||
execConfigSetProtection, execConfigAddProtection, execConfigRemoveProtection } from "./ProtectionsCommands";
|
||||
import {
|
||||
execDisableProtection, execEnableProtection, execListProtections, execConfigGetProtection,
|
||||
execConfigSetProtection, execConfigAddProtection, execConfigRemoveProtection
|
||||
} from "./ProtectionsCommands";
|
||||
import { execListProtectedRooms } from "./ListProtectedRoomsCommand";
|
||||
import { execAddProtectedRoom, execRemoveProtectedRoom } from "./AddRemoveProtectedRoomsCommand";
|
||||
import { execAddRoomToDirectoryCommand, execRemoveRoomFromDirectoryCommand } from "./AddRemoveRoomFromDirectoryCommand";
|
||||
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { Mjolnir } from "../Mjolnir";
|
||||
import { SHORTCODE_EVENT_TYPE } from "../models/BanList";
|
||||
import { SHORTCODE_EVENT_TYPE } from "../models/PolicyList";
|
||||
import { Permalinks, RichReply } from "matrix-bot-sdk";
|
||||
|
||||
// !mjolnir list create <shortcode> <alias localpart>
|
||||
@ -48,7 +48,7 @@ export async function execCreateListCommand(roomId: string, event: any, mjolnir:
|
||||
preset: "public_chat",
|
||||
room_alias_name: aliasLocalpart,
|
||||
invite: [event['sender']],
|
||||
initial_state: [{type: SHORTCODE_EVENT_TYPE, state_key: "", content: {shortcode: shortcode}}],
|
||||
initial_state: [{ type: SHORTCODE_EVENT_TYPE, state_key: "", content: { shortcode: shortcode } }],
|
||||
power_level_content_override: powerLevels,
|
||||
});
|
||||
|
||||
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { RichReply } from "matrix-bot-sdk";
|
||||
import { Mjolnir } from "../Mjolnir";
|
||||
import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../models/BanList";
|
||||
import { EntityType } from "../models/ListRule";
|
||||
import { htmlEscape } from "../utils";
|
||||
|
||||
/**
|
||||
@ -33,7 +33,7 @@ export async function execRulesMatchingCommand(roomId: string, event: any, mjoln
|
||||
let html = "";
|
||||
let text = "";
|
||||
for (const list of mjolnir.lists) {
|
||||
const matches = list.rulesMatchingEntity(entity)
|
||||
const matches = list.rulesMatchingEntity(entity)
|
||||
|
||||
if (matches.length === 0) {
|
||||
continue;
|
||||
@ -48,12 +48,16 @@ export async function execRulesMatchingCommand(roomId: string, event: any, mjoln
|
||||
for (const rule of matches) {
|
||||
// If we know the rule kind, we will give it a readable name, otherwise just use its name.
|
||||
let ruleKind: string = rule.kind;
|
||||
if (ruleKind === RULE_USER) {
|
||||
ruleKind = 'user';
|
||||
} else if (ruleKind === RULE_SERVER) {
|
||||
ruleKind = 'server';
|
||||
} else if (ruleKind === RULE_ROOM) {
|
||||
ruleKind = 'room';
|
||||
switch (ruleKind) {
|
||||
case EntityType.RULE_USER:
|
||||
ruleKind = 'user';
|
||||
break;
|
||||
case EntityType.RULE_SERVER:
|
||||
ruleKind = 'server';
|
||||
break;
|
||||
case EntityType.RULE_ROOM:
|
||||
ruleKind = 'room';
|
||||
break;
|
||||
}
|
||||
html += `<li>${htmlEscape(ruleKind)} (<code>${htmlEscape(rule.recommendation ?? "")}</code>): <code>${htmlEscape(rule.entity)}</code> (${htmlEscape(rule.reason)})</li>`;
|
||||
text += `* ${ruleKind} (${rule.recommendation}): ${rule.entity} (${rule.reason})\n`;
|
||||
|
@ -16,8 +16,7 @@ limitations under the License.
|
||||
|
||||
import { Mjolnir } from "../Mjolnir";
|
||||
import { RichReply } from "matrix-bot-sdk";
|
||||
import { RECOMMENDATION_BAN, recommendationToStable } from "../models/ListRule";
|
||||
import { RULE_SERVER, RULE_USER, ruleTypeToStable } from "../models/BanList";
|
||||
import { EntityType, Recommendation } from "../models/ListRule";
|
||||
|
||||
// !mjolnir import <room ID> <shortcode>
|
||||
export async function execImportCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||
@ -45,14 +44,13 @@ export async function execImportCommand(roomId: string, event: any, mjolnir: Mjo
|
||||
|
||||
await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Adding user ${stateEvent['state_key']} to ban list`);
|
||||
|
||||
const recommendation = recommendationToStable(RECOMMENDATION_BAN);
|
||||
const ruleContent = {
|
||||
entity: stateEvent['state_key'],
|
||||
recommendation,
|
||||
recommendation: Recommendation.Ban,
|
||||
reason: reason,
|
||||
};
|
||||
const stateKey = `rule:${ruleContent.entity}`;
|
||||
let stableRule = ruleTypeToStable(RULE_USER);
|
||||
let stableRule = EntityType.RULE_USER;
|
||||
if (stableRule) {
|
||||
await mjolnir.client.sendStateEvent(list.roomId, stableRule, stateKey, ruleContent);
|
||||
}
|
||||
@ -66,14 +64,13 @@ export async function execImportCommand(roomId: string, event: any, mjolnir: Mjo
|
||||
|
||||
await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Adding server ${server} to ban list`);
|
||||
|
||||
const recommendation = recommendationToStable(RECOMMENDATION_BAN);
|
||||
const ruleContent = {
|
||||
entity: server,
|
||||
recommendation,
|
||||
recommendation: Recommendation.Ban,
|
||||
reason: reason,
|
||||
};
|
||||
const stateKey = `rule:${ruleContent.entity}`;
|
||||
let stableRule = ruleTypeToStable(RULE_SERVER);
|
||||
let stableRule = EntityType.RULE_SERVER;
|
||||
if (stableRule) {
|
||||
await mjolnir.client.sendStateEvent(list.roomId, stableRule, stateKey, ruleContent);
|
||||
}
|
||||
|
@ -31,6 +31,6 @@ export async function execSetDefaultListCommand(roomId: string, event: any, mjol
|
||||
return;
|
||||
}
|
||||
|
||||
await mjolnir.client.setAccountData(DEFAULT_LIST_EVENT_TYPE, {shortcode});
|
||||
await mjolnir.client.setAccountData(DEFAULT_LIST_EVENT_TYPE, { shortcode });
|
||||
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
|
||||
}
|
||||
|
@ -15,21 +15,21 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { Mjolnir } from "../Mjolnir";
|
||||
import BanList, { RULE_ROOM, RULE_SERVER, RULE_USER, USER_RULE_TYPES } from "../models/BanList";
|
||||
import PolicyList from "../models/PolicyList";
|
||||
import { extractRequestError, LogLevel, LogService, MatrixGlob, RichReply } from "matrix-bot-sdk";
|
||||
import { RECOMMENDATION_BAN, recommendationToStable } from "../models/ListRule";
|
||||
import { Recommendation, RULE_ROOM, RULE_SERVER, RULE_USER, USER_RULE_TYPES } from "../models/ListRule";
|
||||
import config from "../config";
|
||||
import { DEFAULT_LIST_EVENT_TYPE } from "./SetDefaultBanListCommand";
|
||||
|
||||
interface Arguments {
|
||||
list: BanList | null;
|
||||
list: PolicyList | null;
|
||||
entity: string;
|
||||
ruleType: string | null;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
// Exported for tests
|
||||
export async function parseArguments(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]): Promise<Arguments|null> {
|
||||
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);
|
||||
@ -44,7 +44,7 @@ export async function parseArguments(roomId: string, event: any, mjolnir: Mjolni
|
||||
let argumentIndex = 2;
|
||||
let ruleType: string | null = null;
|
||||
let entity: string | null = null;
|
||||
let list: BanList | null = null;
|
||||
let list: PolicyList | null = null;
|
||||
let force = false;
|
||||
while (argumentIndex < 7 && argumentIndex < parts.length) {
|
||||
const arg = parts[argumentIndex++];
|
||||
@ -119,10 +119,9 @@ export async function execBanCommand(roomId: string, event: any, mjolnir: Mjolni
|
||||
const bits = await parseArguments(roomId, event, mjolnir, parts);
|
||||
if (!bits) return; // error already handled
|
||||
|
||||
const recommendation = recommendationToStable(RECOMMENDATION_BAN);
|
||||
const ruleContent = {
|
||||
entity: bits.entity,
|
||||
recommendation,
|
||||
recommendation: Recommendation.Ban,
|
||||
reason: bits.reason || '<no reason supplied>',
|
||||
};
|
||||
const stateKey = `rule:${bits.entity}`;
|
||||
|
@ -16,32 +16,238 @@ limitations under the License.
|
||||
|
||||
import { MatrixGlob } from "matrix-bot-sdk";
|
||||
|
||||
export const RECOMMENDATION_BAN = "m.ban";
|
||||
export const RECOMMENDATION_BAN_TYPES = [RECOMMENDATION_BAN, "org.matrix.mjolnir.ban"];
|
||||
export enum EntityType {
|
||||
/// `entity` is to be parsed as a glob of users IDs
|
||||
RULE_USER = "m.policy.rule.user",
|
||||
|
||||
export function recommendationToStable(recommendation: string, unstable = false): string|null {
|
||||
if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN;
|
||||
return null;
|
||||
/// `entity` is to be parsed as a glob of room IDs/aliases
|
||||
RULE_ROOM = "m.policy.rule.room",
|
||||
|
||||
/// `entity` is to be parsed as a glob of server names
|
||||
RULE_SERVER = "m.policy.rule.server",
|
||||
}
|
||||
|
||||
export class ListRule {
|
||||
export const RULE_USER = EntityType.RULE_USER;
|
||||
export const RULE_ROOM = EntityType.RULE_ROOM;
|
||||
export const RULE_SERVER = EntityType.RULE_SERVER;
|
||||
|
||||
// README! The order here matters for determining whether a type is obsolete, most recent should be first.
|
||||
// These are the current and historical types for each type of rule which were used while MSC2313 was being developed
|
||||
// and were left as an artifact for some time afterwards.
|
||||
// Most rules (as of writing) will have the prefix `m.room.rule.*` as this has been in use for roughly 2 years.
|
||||
export const USER_RULE_TYPES = [RULE_USER, "m.room.rule.user", "org.matrix.mjolnir.rule.user"];
|
||||
export const ROOM_RULE_TYPES = [RULE_ROOM, "m.room.rule.room", "org.matrix.mjolnir.rule.room"];
|
||||
export const SERVER_RULE_TYPES = [RULE_SERVER, "m.room.rule.server", "org.matrix.mjolnir.rule.server"];
|
||||
export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES];
|
||||
|
||||
export enum Recommendation {
|
||||
/// The rule recommends a "ban".
|
||||
///
|
||||
/// The actual semantics for this "ban" may vary, e.g. room ban,
|
||||
/// server ban, ignore user, etc. To determine the semantics for
|
||||
/// this "ban", clients need to take into account the context for
|
||||
/// the list, e.g. how the rule was imported.
|
||||
Ban = "m.ban",
|
||||
|
||||
/// The rule specifies an "opinion", as a number in [-100, +100],
|
||||
/// where -100 represents a user who is considered absolutely toxic
|
||||
/// by whoever issued this ListRule and +100 represents a user who
|
||||
/// is considered absolutely absolutely perfect by whoever issued
|
||||
/// this ListRule.
|
||||
Opinion = "org.matrix.msc3845.opinion",
|
||||
}
|
||||
|
||||
/**
|
||||
* All variants of recommendation `m.ban`
|
||||
*/
|
||||
const RECOMMENDATION_BAN_VARIANTS = [
|
||||
// Stable
|
||||
Recommendation.Ban,
|
||||
// Unstable prefix, for compatibility.
|
||||
"org.matrix.mjolnir.ban"
|
||||
];
|
||||
|
||||
/**
|
||||
* All variants of recommendation `m.opinion`
|
||||
*/
|
||||
const RECOMMENDATION_OPINION_VARIANTS: string[] = [
|
||||
// Unstable
|
||||
Recommendation.Opinion
|
||||
];
|
||||
|
||||
export const OPINION_MIN = -100;
|
||||
export const OPINION_MAX = +100;
|
||||
|
||||
/**
|
||||
* Representation of a rule within a Policy List.
|
||||
*/
|
||||
export abstract class ListRule {
|
||||
/**
|
||||
* A glob for `entity`.
|
||||
*/
|
||||
private glob: MatrixGlob;
|
||||
|
||||
constructor(public readonly entity: string, private action: string, public readonly reason: string, public readonly kind: string) {
|
||||
constructor(
|
||||
/**
|
||||
* The entity covered by this rule, e.g. a glob user ID, a room ID, a server domain.
|
||||
*/
|
||||
public readonly entity: string,
|
||||
/**
|
||||
* A human-readable reason for this rule, for audit purposes.
|
||||
*/
|
||||
public readonly reason: string,
|
||||
/**
|
||||
* The type of entity for this rule, e.g. user, server domain, etc.
|
||||
*/
|
||||
public readonly kind: EntityType,
|
||||
/**
|
||||
* The recommendation for this rule, e.g. "ban" or "opinion", or `null`
|
||||
* if the recommendation is one that Mjölnir doesn't understand.
|
||||
*/
|
||||
public readonly recommendation: Recommendation | null) {
|
||||
this.glob = new MatrixGlob(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* The recommendation for this rule, or `null` if there is no recommendation or the recommendation is invalid.
|
||||
* Recommendations are normalised to their stable types.
|
||||
* Determine whether this rule should apply to a given entity.
|
||||
*/
|
||||
public get recommendation(): string|null {
|
||||
if (RECOMMENDATION_BAN_TYPES.includes(this.action)) return RECOMMENDATION_BAN;
|
||||
return null;
|
||||
}
|
||||
|
||||
public isMatch(entity: string): boolean {
|
||||
return this.glob.test(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and parse an event into a ListRule.
|
||||
*
|
||||
* @param event An *untrusted* event.
|
||||
* @returns null if the ListRule is invalid or not recognized by Mjölnir.
|
||||
*/
|
||||
public static parse(event: {type: string, content: any}): ListRule | null {
|
||||
// Parse common fields.
|
||||
// If a field is ill-formed, discard the rule.
|
||||
const content = event['content'];
|
||||
if (!content || typeof content !== "object") {
|
||||
return null;
|
||||
}
|
||||
const entity = content['entity'];
|
||||
if (!entity || typeof entity !== "string") {
|
||||
return null;
|
||||
}
|
||||
const recommendation = content['recommendation'];
|
||||
if (!recommendation || typeof recommendation !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const reason = content['reason'] || '<no reason>';
|
||||
if (typeof reason !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let type = event['type'];
|
||||
let kind;
|
||||
if (USER_RULE_TYPES.includes(type)) {
|
||||
kind = EntityType.RULE_USER;
|
||||
} else if (ROOM_RULE_TYPES.includes(type)) {
|
||||
kind = EntityType.RULE_ROOM;
|
||||
} else if (SERVER_RULE_TYPES.includes(type)) {
|
||||
kind = EntityType.RULE_SERVER;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// From this point, we may need specific fields.
|
||||
if (RECOMMENDATION_BAN_VARIANTS.includes(recommendation)) {
|
||||
return new ListRuleBan(entity, reason, kind);
|
||||
} else if (RECOMMENDATION_OPINION_VARIANTS.includes(recommendation)) {
|
||||
let opinion = content['opinion'];
|
||||
if (!Number.isInteger(opinion)) {
|
||||
return null;
|
||||
}
|
||||
return new ListRuleOpinion(entity, reason, kind, opinion);
|
||||
} else {
|
||||
// As long as the `recommendation` is defined, we assume
|
||||
// that the rule is correct, just unknown.
|
||||
return new ListRuleUnknown(entity, reason, kind, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A rule representing a "ban".
|
||||
*/
|
||||
export class ListRuleBan extends ListRule {
|
||||
constructor(
|
||||
/**
|
||||
* The entity covered by this rule, e.g. a glob user ID, a room ID, a server domain.
|
||||
*/
|
||||
entity: string,
|
||||
/**
|
||||
* A human-readable reason for this rule, for audit purposes.
|
||||
*/
|
||||
reason: string,
|
||||
/**
|
||||
* The type of entity for this rule, e.g. user, server domain, etc.
|
||||
*/
|
||||
kind: EntityType,
|
||||
) {
|
||||
super(entity, reason, kind, Recommendation.Ban)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A rule representing an "opinion"
|
||||
*/
|
||||
export class ListRuleOpinion extends ListRule {
|
||||
constructor(
|
||||
/**
|
||||
* The entity covered by this rule, e.g. a glob user ID, a room ID, a server domain.
|
||||
*/
|
||||
entity: string,
|
||||
/**
|
||||
* A human-readable reason for this rule, for audit purposes.
|
||||
*/
|
||||
reason: string,
|
||||
/**
|
||||
* The type of entity for this rule, e.g. user, server domain, etc.
|
||||
*/
|
||||
kind: EntityType,
|
||||
/**
|
||||
* A number in [-100, +100] where -100 represents the worst possible opinion
|
||||
* on the entity (e.g. toxic user or community) and +100 represents the best
|
||||
* possible opinion on the entity (e.g. pillar of the community).
|
||||
*/
|
||||
public readonly opinion: number
|
||||
) {
|
||||
super(entity, reason, kind, Recommendation.Opinion);
|
||||
if (!Number.isInteger(opinion)) {
|
||||
throw new TypeError(`The opinion must be an integer, got ${opinion}`);
|
||||
}
|
||||
if (opinion < OPINION_MIN || opinion > OPINION_MAX) {
|
||||
throw new TypeError(`The opinion must be within [-100, +100], got ${opinion}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Any list rule that we do not understand.
|
||||
*/
|
||||
export class ListRuleUnknown extends ListRule {
|
||||
constructor(
|
||||
/**
|
||||
* The entity covered by this rule, e.g. a glob user ID, a room ID, a server domain.
|
||||
*/
|
||||
entity: string,
|
||||
/**
|
||||
* A human-readable reason for this rule, for audit purposes.
|
||||
*/
|
||||
reason: string,
|
||||
/**
|
||||
* The type of entity for this rule, e.g. user, server domain, etc.
|
||||
*/
|
||||
kind: EntityType,
|
||||
/**
|
||||
* The event used to create the rule.
|
||||
*/
|
||||
public readonly content: any,
|
||||
) {
|
||||
super(entity, reason, kind, null);
|
||||
}
|
||||
}
|
||||
|
@ -16,33 +16,13 @@ limitations under the License.
|
||||
|
||||
import { extractRequestError, LogService, MatrixClient, UserID } from "matrix-bot-sdk";
|
||||
import { EventEmitter } from "events";
|
||||
import { ListRule, RECOMMENDATION_BAN } from "./ListRule";
|
||||
|
||||
export const RULE_USER = "m.policy.rule.user";
|
||||
export const RULE_ROOM = "m.policy.rule.room";
|
||||
export const RULE_SERVER = "m.policy.rule.server";
|
||||
|
||||
// README! The order here matters for determining whether a type is obsolete, most recent should be first.
|
||||
// These are the current and historical types for each type of rule which were used while MSC2313 was being developed
|
||||
// and were left as an artifact for some time afterwards.
|
||||
// Most rules (as of writing) will have the prefix `m.room.rule.*` as this has been in use for roughly 2 years.
|
||||
export const USER_RULE_TYPES = [RULE_USER, "m.room.rule.user", "org.matrix.mjolnir.rule.user"];
|
||||
export const ROOM_RULE_TYPES = [RULE_ROOM, "m.room.rule.room", "org.matrix.mjolnir.rule.room"];
|
||||
export const SERVER_RULE_TYPES = [RULE_SERVER, "m.room.rule.server", "org.matrix.mjolnir.rule.server"];
|
||||
export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES];
|
||||
import { ALL_RULE_TYPES, EntityType, ListRule, Recommendation, ROOM_RULE_TYPES, RULE_ROOM, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES, USER_RULE_TYPES } from "./ListRule";
|
||||
|
||||
export const SHORTCODE_EVENT_TYPE = "org.matrix.mjolnir.shortcode";
|
||||
|
||||
export function ruleTypeToStable(rule: string, unstable = true): string|null {
|
||||
if (USER_RULE_TYPES.includes(rule)) return unstable ? USER_RULE_TYPES[USER_RULE_TYPES.length - 1] : RULE_USER;
|
||||
if (ROOM_RULE_TYPES.includes(rule)) return unstable ? ROOM_RULE_TYPES[ROOM_RULE_TYPES.length - 1] : RULE_ROOM;
|
||||
if (SERVER_RULE_TYPES.includes(rule)) return unstable ? SERVER_RULE_TYPES[SERVER_RULE_TYPES.length - 1] : RULE_SERVER;
|
||||
return null;
|
||||
}
|
||||
|
||||
export enum ChangeType {
|
||||
Added = "ADDED",
|
||||
Removed = "REMOVED",
|
||||
Added = "ADDED",
|
||||
Removed = "REMOVED",
|
||||
Modified = "MODIFIED"
|
||||
}
|
||||
|
||||
@ -71,28 +51,28 @@ export interface ListRuleChange {
|
||||
readonly previousState?: any,
|
||||
}
|
||||
|
||||
declare interface BanList {
|
||||
// BanList.update is emitted when the BanList has pulled new rules from Matrix and informs listeners of any changes.
|
||||
on(event: 'BanList.update', listener: (list: BanList, changes: ListRuleChange[]) => void): this
|
||||
emit(event: 'BanList.update', list: BanList, changes: ListRuleChange[]): boolean
|
||||
// BanList.batch is emitted when the BanList has created a batch from the events provided by `updateForEvent`.
|
||||
on(event: 'BanList.batch', listener: (list: BanList) => void): this
|
||||
emit(event: 'BanList.batch', list: BanList): boolean
|
||||
declare interface PolicyList {
|
||||
// PolicyList.update is emitted when the PolicyList has pulled new rules from Matrix and informs listeners of any changes.
|
||||
on(event: 'PolicyList.update', listener: (list: PolicyList, changes: ListRuleChange[]) => void): this
|
||||
emit(event: 'PolicyList.update', list: PolicyList, changes: ListRuleChange[]): boolean
|
||||
// PolicyList.batch is emitted when the PolicyList has created a batch from the events provided by `updateForEvent`.
|
||||
on(event: 'PolicyList.batch', listener: (list: PolicyList) => void): this
|
||||
emit(event: 'PolicyList.batch', list: PolicyList): boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The BanList caches all of the rules that are active in a policy room so Mjolnir can refer to when applying bans etc.
|
||||
* The PolicyList caches all of the rules that are active in a policy room so Mjolnir can refer to when applying bans etc.
|
||||
* This cannot be used to update events in the modeled room, it is a readonly model of the policy room.
|
||||
*/
|
||||
class BanList extends EventEmitter {
|
||||
private shortcode: string|null = null;
|
||||
class PolicyList extends EventEmitter {
|
||||
private shortcode: string | null = null;
|
||||
// A map of state events indexed first by state type and then state keys.
|
||||
private state: Map<string, Map<string, any>> = new Map();
|
||||
// Batches new events from sync together before starting the process to update the list.
|
||||
private readonly batcher: UpdateBatcher;
|
||||
|
||||
/**
|
||||
* Construct a BanList, does not synchronize with the room.
|
||||
* Construct a PolicyList, does not synchronize with the room.
|
||||
* @param roomId The id of the policy room, i.e. a room containing MSC2313 policies.
|
||||
* @param 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.
|
||||
@ -120,7 +100,7 @@ class BanList extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Store this state event as part of the active room state for this BanList (used to cache rules).
|
||||
* Store this state event as part of the active room state for this PolicyList (used to cache rules).
|
||||
* The state type should be normalised if it is obsolete e.g. m.room.rule.user should be stored as m.policy.rule.user.
|
||||
* @param stateType The event type e.g. m.room.policy.user.
|
||||
* @param stateKey The state key e.g. rule:@bad:matrix.org
|
||||
@ -137,7 +117,7 @@ class BanList extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Return all the active rules of a given kind.
|
||||
* @param kind e.g. RULE_SERVER (m.policy.rule.server). Rule types are always normalised when they are interned into the BanList.
|
||||
* @param kind e.g. RULE_SERVER (m.policy.rule.server). Rule types are always normalised when they are interned into the PolicyList.
|
||||
* @returns The active ListRules for the ban list of that kind.
|
||||
*/
|
||||
private rulesOfKind(kind: string): ListRule[] {
|
||||
@ -149,7 +129,7 @@ class BanList extends EventEmitter {
|
||||
// README! If you are refactoring this and/or introducing a mechanism to return the list of rules,
|
||||
// please make sure that you *only* return rules with `m.ban` or create a different method
|
||||
// (we don't want to accidentally ban entities).
|
||||
if (rule && rule.kind === kind && rule.recommendation === RECOMMENDATION_BAN) {
|
||||
if (rule && rule.kind === kind && rule.recommendation === Recommendation.Ban) {
|
||||
rules.push(rule);
|
||||
}
|
||||
}
|
||||
@ -160,8 +140,8 @@ class BanList 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 => {
|
||||
LogService.error("BanList", extractRequestError(err));
|
||||
this.client.sendStateEvent(this.roomId, SHORTCODE_EVENT_TYPE, '', { shortcode: this.shortcode }).catch(err => {
|
||||
LogService.error("PolicyList", extractRequestError(err));
|
||||
if (this.shortcode === newShortcode) this.shortcode = currentShortcode;
|
||||
});
|
||||
}
|
||||
@ -267,7 +247,7 @@ class BanList extends EventEmitter {
|
||||
continue;
|
||||
}
|
||||
|
||||
let kind: string|null = null;
|
||||
let kind: EntityType | null = null;
|
||||
if (USER_RULE_TYPES.includes(event['type'])) {
|
||||
kind = RULE_USER;
|
||||
} else if (ROOM_RULE_TYPES.includes(event['type'])) {
|
||||
@ -286,7 +266,7 @@ class BanList extends EventEmitter {
|
||||
// as it may be someone deleting the older versions of the rules.
|
||||
if (previousState) {
|
||||
const logObsoleteRule = () => {
|
||||
LogService.info('BanList', `In BanList ${this.roomRef}, conflict between rules ${event['event_id']} (with obsolete type ${event['type']}) ` +
|
||||
LogService.info('PolicyList', `In PolicyList ${this.roomRef}, conflict between rules ${event['event_id']} (with obsolete type ${event['type']}) ` +
|
||||
`and ${previousState['event_id']} (with standard type ${previousState['type']}). Ignoring rule with obsolete type.`);
|
||||
}
|
||||
if (kind === RULE_USER && USER_RULE_TYPES.indexOf(event['type']) > USER_RULE_TYPES.indexOf(previousState['type'])) {
|
||||
@ -305,7 +285,7 @@ class BanList extends EventEmitter {
|
||||
// in order to mark a rule as deleted.
|
||||
// We always set state with the normalised state type via `kind` to de-duplicate rules.
|
||||
this.setState(kind, event['state_key'], event);
|
||||
const changeType: null|ChangeType = (() => {
|
||||
const changeType: null | ChangeType = (() => {
|
||||
if (!previousState) {
|
||||
return ChangeType.Added;
|
||||
} else if (previousState['event_id'] === event['event_id']) {
|
||||
@ -329,56 +309,52 @@ class BanList extends EventEmitter {
|
||||
// and so will not have been used. Removing a rule like this therefore results in no change.
|
||||
if (changeType === ChangeType.Removed && previousState?.unsigned?.rule) {
|
||||
const sender = event.unsigned['redacted_because'] ? event.unsigned['redacted_because']['sender'] : event.sender;
|
||||
changes.push({changeType, event, sender, rule: previousState.unsigned.rule,
|
||||
... previousState ? {previousState} : {} });
|
||||
changes.push({
|
||||
changeType, event, sender, rule: previousState.unsigned.rule,
|
||||
...previousState ? { previousState } : {}
|
||||
});
|
||||
// Event has no content and cannot be parsed as a ListRule.
|
||||
continue;
|
||||
}
|
||||
// It's a rule - parse it
|
||||
const content = event['content'];
|
||||
if (!content) continue;
|
||||
|
||||
const entity = content['entity'];
|
||||
const recommendation = content['recommendation'];
|
||||
const reason = content['reason'] || '<no reason>';
|
||||
|
||||
if (!entity || !recommendation) {
|
||||
const rule = ListRule.parse(event);
|
||||
if (!rule) {
|
||||
// Invalid/unknown rule, just skip it.
|
||||
continue;
|
||||
}
|
||||
const rule = new ListRule(entity, recommendation, reason, kind);
|
||||
event.unsigned.rule = rule;
|
||||
if (changeType) {
|
||||
changes.push({rule, changeType, event, sender: event.sender, ... previousState ? {previousState} : {} });
|
||||
changes.push({ rule, changeType, event, sender: event.sender, ...previousState ? { previousState } : {} });
|
||||
}
|
||||
}
|
||||
this.emit('BanList.update', this, changes);
|
||||
this.emit('PolicyList.update', this, changes);
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inform the `BanList` about a new event from the room it is modelling.
|
||||
* @param event An event from the room the `BanList` models to inform an instance about.
|
||||
* Inform the `PolicyList` about a new event from the room it is modelling.
|
||||
* @param event An event from the room the `PolicyList` models to inform an instance about.
|
||||
*/
|
||||
public updateForEvent(event: { event_id: string }): void {
|
||||
this.batcher.addToBatch(event.event_id)
|
||||
}
|
||||
}
|
||||
|
||||
export default BanList;
|
||||
export default PolicyList;
|
||||
|
||||
/**
|
||||
* Helper class that emits a batch event on a `BanList` when it has made a batch
|
||||
* Helper class that emits a batch event on a `PolicyList` when it has made a batch
|
||||
* out of the events given to `addToBatch`.
|
||||
*/
|
||||
class UpdateBatcher {
|
||||
// Whether we are waiting for more events to form a batch.
|
||||
private isWaiting = false;
|
||||
// The latest (or most recent) event we have received.
|
||||
private latestEventId: string|null = null;
|
||||
private latestEventId: string | null = null;
|
||||
private readonly waitPeriodMS = 200; // 200ms seems good enough.
|
||||
private readonly maxWaitMS = 3000; // 3s is long enough to wait while batching.
|
||||
|
||||
constructor(private readonly banList: BanList) {
|
||||
constructor(private readonly banList: PolicyList) {
|
||||
|
||||
}
|
||||
|
||||
@ -402,7 +378,7 @@ class UpdateBatcher {
|
||||
await new Promise(resolve => setTimeout(resolve, this.waitPeriodMS));
|
||||
} while ((Date.now() - start) < this.maxWaitMS && this.latestEventId !== eventId)
|
||||
this.reset();
|
||||
this.banList.emit('BanList.batch', this.banList);
|
||||
this.banList.emit('PolicyList.batch', this.banList);
|
||||
}
|
||||
|
||||
/**
|
@ -13,10 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import BanList, { ChangeType, ListRuleChange, RULE_ROOM, RULE_SERVER, RULE_USER } from "./BanList"
|
||||
import BanList, { ChangeType, ListRuleChange } from "./PolicyList"
|
||||
import * as crypto from "crypto";
|
||||
import { LogService } from "matrix-bot-sdk";
|
||||
import { ListRule } from "./ListRule";
|
||||
import { EntityType, ListRule } from "./ListRule";
|
||||
import PolicyList from "./PolicyList";
|
||||
|
||||
export const USER_MAY_INVITE = 'user_may_invite';
|
||||
export const CHECK_EVENT_FOR_SPAM = 'check_event_for_spam';
|
||||
@ -25,7 +26,7 @@ export const CHECK_EVENT_FOR_SPAM = 'check_event_for_spam';
|
||||
* Rules in the RuleServer format that have been produced from a single event.
|
||||
*/
|
||||
class EventRules {
|
||||
constructor (
|
||||
constructor(
|
||||
readonly eventId: string,
|
||||
readonly roomId: string,
|
||||
readonly ruleServerRules: RuleServerRule[],
|
||||
@ -108,7 +109,7 @@ export default class RuleServer {
|
||||
* @returns The `EventRules` object describing which rules have been created based on the policy the event represents
|
||||
* or `undefined` if there are no `EventRules` associated with the event.
|
||||
*/
|
||||
private getEventRules(roomId: string, eventId: string): EventRules|undefined {
|
||||
private getEventRules(roomId: string, eventId: string): EventRules | undefined {
|
||||
return this.rulesByEvent.get(roomId)?.get(eventId);
|
||||
}
|
||||
|
||||
@ -118,7 +119,7 @@ export default class RuleServer {
|
||||
* @throws If there are already rules associated with the event specified in `eventRules.eventId`.
|
||||
*/
|
||||
private addEventRules(eventRules: EventRules): void {
|
||||
const {roomId, eventId, token} = eventRules;
|
||||
const { roomId, eventId, token } = eventRules;
|
||||
if (this.rulesByEvent.get(roomId)?.has(eventId)) {
|
||||
throw new TypeError(`There is already an entry in the RuleServer for rules created from the event ${eventId}.`);
|
||||
}
|
||||
@ -136,7 +137,7 @@ export default class RuleServer {
|
||||
* @param eventRules The EventRules to stop serving from the rule server.
|
||||
*/
|
||||
private stopEventRules(eventRules: EventRules): void {
|
||||
const {eventId, roomId, token} = eventRules;
|
||||
const { eventId, roomId, token } = eventRules;
|
||||
this.rulesByEvent.get(roomId)?.delete(eventId);
|
||||
// We expect that each row of `rulesByEvent` list of eventRules (represented by 1 row in `rulesByEvent`) to be relatively small (1-5)
|
||||
// as it can only contain eventRules added during the instant of time represented by one token.
|
||||
@ -156,7 +157,7 @@ export default class RuleServer {
|
||||
const eventRules = new EventRules(change.event.event_id, change.event.room_id, toRuleServerFormat(change.rule), this.currentToken);
|
||||
this.addEventRules(eventRules);
|
||||
} else if (change.changeType === ChangeType.Modified) {
|
||||
const entry: EventRules|undefined = this.getEventRules(change.event.roomId, change.previousState.event_id);
|
||||
const entry: EventRules | undefined = this.getEventRules(change.event.roomId, change.previousState.event_id);
|
||||
if (entry === undefined) {
|
||||
LogService.error('RuleServer', `Could not find the rules for the previous modified state ${change.event['state_type']} ${change.event['state_key']} ${change.previousState?.event_id}`);
|
||||
return;
|
||||
@ -169,7 +170,7 @@ export default class RuleServer {
|
||||
// 2) When an event has been "soft redacted" (ie we have a new event with the same state type and state_key with no content),
|
||||
// the events in the `previousState` and `event` slots of `change` will be distinct events.
|
||||
// In either case (of redaction or "soft redaction") we can use `previousState` to get the right event id to stop.
|
||||
const entry: EventRules|undefined = this.getEventRules(change.event.room_id, change.previousState.event_id);
|
||||
const entry: EventRules | undefined = this.getEventRules(change.event.room_id, change.previousState.event_id);
|
||||
if (entry === undefined) {
|
||||
LogService.error('RuleServer', `Could not find the rules for the previous modified state ${change.event['state_type']} ${change.event['state_key']} ${change.previousState?.event_id}`);
|
||||
return;
|
||||
@ -184,16 +185,16 @@ export default class RuleServer {
|
||||
* as we won't be able to serve rules that have already been interned in the BanList.
|
||||
* @param banList a BanList to watch for rule changes with.
|
||||
*/
|
||||
public watch(banList: BanList): void {
|
||||
banList.on('BanList.update', this.banListUpdateListener);
|
||||
public watch(banList: PolicyList): void {
|
||||
banList.on('PolicyList.update', this.banListUpdateListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all of the rules that have been created from the policies in this banList.
|
||||
* @param banList The BanList to unwatch.
|
||||
*/
|
||||
public unwatch(banList: BanList): void {
|
||||
banList.removeListener('BanList.update', this.banListUpdateListener);
|
||||
public unwatch(banList: PolicyList): void {
|
||||
banList.removeListener('PolicyList.update', this.banListUpdateListener);
|
||||
const listRules = this.rulesByEvent.get(banList.roomId);
|
||||
this.nextToken();
|
||||
if (listRules) {
|
||||
@ -221,8 +222,8 @@ export default class RuleServer {
|
||||
* @param sinceToken A token that has previously been issued by this server.
|
||||
* @returns An object with the rules that have been started and stopped since the token and a new token to poll for more rules with.
|
||||
*/
|
||||
public getUpdates(sinceToken: string | null): {start: RuleServerRule[], stop: string[], reset?: boolean, since: string} {
|
||||
const updatesSince = <T = EventRules|string>(token: number | null, policyStore: T[][]): T[] => {
|
||||
public getUpdates(sinceToken: string | null): { start: RuleServerRule[], stop: string[], reset?: boolean, since: string } {
|
||||
const updatesSince = <T = EventRules | string>(token: number | null, policyStore: T[][]): T[] => {
|
||||
if (token === null) {
|
||||
// The client is requesting for the first time, we will give them everything.
|
||||
return policyStore.flat();
|
||||
@ -234,7 +235,7 @@ export default class RuleServer {
|
||||
}
|
||||
}
|
||||
const [serverId, since] = sinceToken ? sinceToken.split('::') : [null, null];
|
||||
const parsedSince: number | null = since ? parseInt(since, 10) : null;
|
||||
const parsedSince: number | null = since ? parseInt(since, 10) : null;
|
||||
if (serverId && serverId !== this.serverId) {
|
||||
// The server has restarted, but the client has not and still has rules we can no longer account for.
|
||||
// So we have to resend them everything.
|
||||
@ -261,59 +262,59 @@ export default class RuleServer {
|
||||
* @returns An array of rules that can be served from the rule server.
|
||||
*/
|
||||
function toRuleServerFormat(policyRule: ListRule): RuleServerRule[] {
|
||||
function makeLiteral(literal: string) {
|
||||
return {literal}
|
||||
}
|
||||
function makeLiteral(literal: string) {
|
||||
return { literal }
|
||||
}
|
||||
|
||||
function makeGlob(glob: string) {
|
||||
return {glob}
|
||||
}
|
||||
function makeGlob(glob: string) {
|
||||
return { glob }
|
||||
}
|
||||
|
||||
function makeServerGlob(server: string) {
|
||||
return {glob: `:${server}`}
|
||||
}
|
||||
function makeServerGlob(server: string) {
|
||||
return { glob: `:${server}` }
|
||||
}
|
||||
|
||||
function makeRule(checks: Checks) {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
checks: checks
|
||||
}
|
||||
}
|
||||
function makeRule(checks: Checks) {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
checks: checks
|
||||
}
|
||||
}
|
||||
|
||||
if (policyRule.kind === RULE_USER) {
|
||||
// Block any messages or invites from being sent by a matching local user
|
||||
// Block any messages or invitations from being received that were sent by a matching remote user.
|
||||
return [{
|
||||
property: USER_MAY_INVITE,
|
||||
user_id: [makeGlob(policyRule.entity)]
|
||||
},
|
||||
{
|
||||
property: CHECK_EVENT_FOR_SPAM,
|
||||
sender: [makeGlob(policyRule.entity)]
|
||||
}].map(makeRule)
|
||||
} else if (policyRule.kind === RULE_ROOM) {
|
||||
// Block any messages being sent or received in the room, stop invitations being sent to the room and
|
||||
// stop anyone receiving invitations from the room.
|
||||
return [{
|
||||
property: USER_MAY_INVITE,
|
||||
'room_id': [makeLiteral(policyRule.entity)]
|
||||
},
|
||||
{
|
||||
property: CHECK_EVENT_FOR_SPAM,
|
||||
'room_id': [makeLiteral(policyRule.entity)]
|
||||
}].map(makeRule)
|
||||
} else if (policyRule.kind === RULE_SERVER) {
|
||||
// Block any invitations from the server or any new messages from the server.
|
||||
return [{
|
||||
property: USER_MAY_INVITE,
|
||||
user_id: [makeServerGlob(policyRule.entity)]
|
||||
},
|
||||
{
|
||||
property: CHECK_EVENT_FOR_SPAM,
|
||||
sender: [makeServerGlob(policyRule.entity)]
|
||||
}].map(makeRule)
|
||||
} else {
|
||||
LogService.info('RuleServer', `Ignoring unsupported policy rule type ${policyRule.kind}`);
|
||||
return []
|
||||
}
|
||||
if (policyRule.kind === EntityType.RULE_USER) {
|
||||
// Block any messages or invites from being sent by a matching local user
|
||||
// Block any messages or invitations from being received that were sent by a matching remote user.
|
||||
return [{
|
||||
property: USER_MAY_INVITE,
|
||||
user_id: [makeGlob(policyRule.entity)]
|
||||
},
|
||||
{
|
||||
property: CHECK_EVENT_FOR_SPAM,
|
||||
sender: [makeGlob(policyRule.entity)]
|
||||
}].map(makeRule)
|
||||
} else if (policyRule.kind === EntityType.RULE_ROOM) {
|
||||
// Block any messages being sent or received in the room, stop invitations being sent to the room and
|
||||
// stop anyone receiving invitations from the room.
|
||||
return [{
|
||||
property: USER_MAY_INVITE,
|
||||
'room_id': [makeLiteral(policyRule.entity)]
|
||||
},
|
||||
{
|
||||
property: CHECK_EVENT_FOR_SPAM,
|
||||
'room_id': [makeLiteral(policyRule.entity)]
|
||||
}].map(makeRule)
|
||||
} else if (policyRule.kind === EntityType.RULE_SERVER) {
|
||||
// Block any invitations from the server or any new messages from the server.
|
||||
return [{
|
||||
property: USER_MAY_INVITE,
|
||||
user_id: [makeServerGlob(policyRule.entity)]
|
||||
},
|
||||
{
|
||||
property: CHECK_EVENT_FOR_SPAM,
|
||||
sender: [makeServerGlob(policyRule.entity)]
|
||||
}].map(makeRule)
|
||||
} else {
|
||||
LogService.info('RuleServer', `Ignoring unsupported policy rule type ${policyRule.kind}`);
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import { Mjolnir, REPORT_POLL_EVENT_TYPE } from "../Mjolnir";
|
||||
import { ReportManager } from './ReportManager';
|
||||
import { LogLevel } from "matrix-bot-sdk";
|
||||
|
||||
class InvalidStateError extends Error {}
|
||||
class InvalidStateError extends Error { }
|
||||
|
||||
/**
|
||||
* A class to poll synapse's report endpoint, so we can act on new reports
|
||||
@ -108,7 +108,7 @@ export class ReportPoller {
|
||||
if (response.next_token !== undefined) {
|
||||
this.from = response.next_token;
|
||||
try {
|
||||
await this.mjolnir.client.setAccountData(REPORT_POLL_EVENT_TYPE, { from: response.next_token });
|
||||
await this.mjolnir.client.setAccountData(REPORT_POLL_EVENT_TYPE, { from: response.next_token });
|
||||
} catch (ex) {
|
||||
await this.mjolnir.logMessage(LogLevel.ERROR, "getAbuseReports", `failed to update progress: ${ex}`);
|
||||
}
|
||||
|
@ -18,15 +18,16 @@ import * as expect from "expect";
|
||||
import { Mjolnir } from "../../src/Mjolnir";
|
||||
import { DEFAULT_LIST_EVENT_TYPE } from "../../src/commands/SetDefaultBanListCommand";
|
||||
import { parseArguments } from "../../src/commands/UnbanBanCommand";
|
||||
import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../../src/models/BanList";
|
||||
import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../../src/models/ListRule";
|
||||
|
||||
function createTestMjolnir(defaultShortcode: string = null): Mjolnir {
|
||||
function createTestMjolnir(defaultShortcode: string|null = null): Mjolnir {
|
||||
const client = {
|
||||
// Mock `MatrixClient.getAccountData` .
|
||||
getAccountData: (eventType: string): Promise<any> => {
|
||||
if (eventType === DEFAULT_LIST_EVENT_TYPE && defaultShortcode) {
|
||||
if (eventType === DEFAULT_LIST_EVENT_TYPE || defaultShortcode) {
|
||||
return Promise.resolve({shortcode: defaultShortcode});
|
||||
}
|
||||
throw new Error("Unknown event type");
|
||||
throw new Error(`Unknown event type ${eventType}, expected ${DEFAULT_LIST_EVENT_TYPE}`);
|
||||
},
|
||||
};
|
||||
return <Mjolnir>{client};
|
||||
@ -55,11 +56,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test example.org";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_SERVER);
|
||||
expect(bits.entity).toBe("example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_SERVER);
|
||||
expect(bits!.entity).toBe("example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should be able to detect servers with ban reasons", async () => {
|
||||
@ -72,11 +73,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test example.org reason here";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBe("reason here");
|
||||
expect(bits.ruleType).toBe(RULE_SERVER);
|
||||
expect(bits.entity).toBe("example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBe("reason here");
|
||||
expect(bits!.ruleType).toBe(RULE_SERVER);
|
||||
expect(bits!.entity).toBe("example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should be able to detect servers with globs", async () => {
|
||||
@ -89,11 +90,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test *.example.org --force";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_SERVER);
|
||||
expect(bits.entity).toBe("*.example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_SERVER);
|
||||
expect(bits!.entity).toBe("*.example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should be able to detect servers with the type specified", async () => {
|
||||
@ -106,11 +107,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test server @*.example.org --force";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_SERVER);
|
||||
expect(bits.entity).toBe("@*.example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_SERVER);
|
||||
expect(bits!.entity).toBe("@*.example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should be able to detect room IDs", async () => {
|
||||
@ -123,11 +124,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test !example.org";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_ROOM);
|
||||
expect(bits.entity).toBe("!example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_ROOM);
|
||||
expect(bits!.entity).toBe("!example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should be able to detect room IDs with ban reasons", async () => {
|
||||
@ -140,11 +141,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test !example.org reason here";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBe("reason here");
|
||||
expect(bits.ruleType).toBe(RULE_ROOM);
|
||||
expect(bits.entity).toBe("!example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBe("reason here");
|
||||
expect(bits!.ruleType).toBe(RULE_ROOM);
|
||||
expect(bits!.entity).toBe("!example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should be able to detect room IDs with globs", async () => {
|
||||
@ -157,11 +158,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test !*.example.org --force";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_ROOM);
|
||||
expect(bits.entity).toBe("!*.example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_ROOM);
|
||||
expect(bits!.entity).toBe("!*.example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should be able to detect room aliases", async () => {
|
||||
@ -174,11 +175,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test #example.org";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_ROOM);
|
||||
expect(bits.entity).toBe("#example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_ROOM);
|
||||
expect(bits!.entity).toBe("#example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should be able to detect room aliases with ban reasons", async () => {
|
||||
@ -191,11 +192,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test #example.org reason here";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBe("reason here");
|
||||
expect(bits.ruleType).toBe(RULE_ROOM);
|
||||
expect(bits.entity).toBe("#example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBe("reason here");
|
||||
expect(bits!.ruleType).toBe(RULE_ROOM);
|
||||
expect(bits!.entity).toBe("#example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should be able to detect room aliases with globs", async () => {
|
||||
@ -208,11 +209,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test #*.example.org --force";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_ROOM);
|
||||
expect(bits.entity).toBe("#*.example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_ROOM);
|
||||
expect(bits!.entity).toBe("#*.example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should be able to detect rooms with the type specified", async () => {
|
||||
@ -225,11 +226,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test room @*.example.org --force";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_ROOM);
|
||||
expect(bits.entity).toBe("@*.example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_ROOM);
|
||||
expect(bits!.entity).toBe("@*.example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should be able to detect user IDs", async () => {
|
||||
@ -242,11 +243,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test @example.org";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_USER);
|
||||
expect(bits.entity).toBe("@example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_USER);
|
||||
expect(bits!.entity).toBe("@example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should be able to detect user IDs with ban reasons", async () => {
|
||||
@ -259,11 +260,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test @example.org reason here";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBe("reason here");
|
||||
expect(bits.ruleType).toBe(RULE_USER);
|
||||
expect(bits.entity).toBe("@example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBe("reason here");
|
||||
expect(bits!.ruleType).toBe(RULE_USER);
|
||||
expect(bits!.entity).toBe("@example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should be able to detect user IDs with globs", async () => {
|
||||
@ -276,11 +277,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test @*.example.org --force";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_USER);
|
||||
expect(bits.entity).toBe("@*.example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_USER);
|
||||
expect(bits!.entity).toBe("@*.example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should be able to detect user IDs with the type specified", async () => {
|
||||
@ -293,11 +294,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test user #*.example.org --force";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_USER);
|
||||
expect(bits.entity).toBe("#*.example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_USER);
|
||||
expect(bits!.entity).toBe("#*.example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should error if wildcards used without --force", async () => {
|
||||
@ -324,11 +325,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test user #*.example.org reason here --force";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBe("reason here");
|
||||
expect(bits.ruleType).toBe(RULE_USER);
|
||||
expect(bits.entity).toBe("#*.example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBe("reason here");
|
||||
expect(bits!.ruleType).toBe(RULE_USER);
|
||||
expect(bits!.entity).toBe("#*.example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
describe("[without default list]", () => {
|
||||
@ -370,11 +371,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban user test @example:example.org";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_USER);
|
||||
expect(bits.entity).toBe("@example:example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_USER);
|
||||
expect(bits!.entity).toBe("@example:example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should not error if a list (without type) is specified", async () => {
|
||||
@ -387,11 +388,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test @example:example.org";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_USER);
|
||||
expect(bits.entity).toBe("@example:example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_USER);
|
||||
expect(bits!.entity).toBe("@example:example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should not error if a list (with type reversed) is specified", async () => {
|
||||
@ -404,11 +405,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban test user @example:example.org";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_USER);
|
||||
expect(bits.entity).toBe("@example:example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_USER);
|
||||
expect(bits!.entity).toBe("@example:example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
});
|
||||
|
||||
@ -423,11 +424,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban user @example:example.org";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_USER);
|
||||
expect(bits.entity).toBe("@example:example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_USER);
|
||||
expect(bits!.entity).toBe("@example:example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should use the default list if no list (without type) is specified", async () => {
|
||||
@ -440,11 +441,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban @example:example.org";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_USER);
|
||||
expect(bits.entity).toBe("@example:example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("test");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_USER);
|
||||
expect(bits!.entity).toBe("@example:example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("test");
|
||||
});
|
||||
|
||||
it("should use the specified list if a list (with type) is specified", async () => {
|
||||
@ -457,11 +458,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban user other @example:example.org";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_USER);
|
||||
expect(bits.entity).toBe("@example:example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("other");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_USER);
|
||||
expect(bits!.entity).toBe("@example:example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("other");
|
||||
});
|
||||
|
||||
it("should use the specified list if a list (without type) is specified", async () => {
|
||||
@ -474,11 +475,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban other @example:example.org";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_USER);
|
||||
expect(bits.entity).toBe("@example:example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("other");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_USER);
|
||||
expect(bits!.entity).toBe("@example:example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("other");
|
||||
});
|
||||
|
||||
it("should not error if a list (with type reversed) is specified", async () => {
|
||||
@ -491,11 +492,11 @@ describe("UnbanBanCommand", () => {
|
||||
const command = "!mjolnir ban other user @example:example.org";
|
||||
const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' '));
|
||||
expect(bits).toBeTruthy();
|
||||
expect(bits.reason).toBeFalsy();
|
||||
expect(bits.ruleType).toBe(RULE_USER);
|
||||
expect(bits.entity).toBe("@example:example.org");
|
||||
expect(bits.list).toBeDefined();
|
||||
expect(bits.list.listShortcode).toBe("other");
|
||||
expect(bits!.reason).toBeFalsy();
|
||||
expect(bits!.ruleType).toBe(RULE_USER);
|
||||
expect(bits!.entity).toBe("@example:example.org");
|
||||
expect(bits!.list).toBeDefined();
|
||||
expect(bits!.list!.listShortcode).toBe("other");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,10 +3,11 @@ import { strict as assert } from "assert";
|
||||
import config from "../../src/config";
|
||||
import { newTestUser } from "./clientHelper";
|
||||
import { LogService, MatrixClient, Permalinks, UserID } from "matrix-bot-sdk";
|
||||
import BanList, { ALL_RULE_TYPES, ChangeType, ListRuleChange, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES } from "../../src/models/BanList";
|
||||
import PolicyList, { ChangeType, ListRuleChange } from "../../src/models/PolicyList";
|
||||
import { ServerAcl } from "../../src/models/ServerAcl";
|
||||
import { getFirstReaction } from "./commands/commandUtils";
|
||||
import { getMessagesByUserIn } from "../../src/utils";
|
||||
import { ALL_RULE_TYPES, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES } from "../../src/models/ListRule";
|
||||
|
||||
/**
|
||||
* Create a policy rule in a policy room.
|
||||
@ -18,7 +19,7 @@ import { getMessagesByUserIn } from "../../src/utils";
|
||||
* @param template The template to use for the policy rule event.
|
||||
* @returns The event id of the newly created policy rule.
|
||||
*/
|
||||
async function createPolicyRule(client: MatrixClient, policyRoomId: string, policyType: string, entity: string, reason: string, template = {recommendation: 'm.ban'}) {
|
||||
async function createPolicyRule(client: MatrixClient, policyRoomId: string, policyType: string, entity: string, reason: string, template = { recommendation: 'm.ban' }) {
|
||||
return await client.sendStateEvent(policyRoomId, policyType, `rule:${entity}`, {
|
||||
entity,
|
||||
reason,
|
||||
@ -26,13 +27,13 @@ async function createPolicyRule(client: MatrixClient, policyRoomId: string, poli
|
||||
});
|
||||
}
|
||||
|
||||
describe("Test: Updating the BanList", function () {
|
||||
it("Calculates what has changed correctly.", async function () {
|
||||
describe("Test: Updating the PolicyList", function() {
|
||||
it("Calculates what has changed correctly.", async function() {
|
||||
this.timeout(10000);
|
||||
const mjolnir = config.RUNTIME.client!
|
||||
const moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
|
||||
const banList = new BanList(banListId, banListId, mjolnir);
|
||||
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()] });
|
||||
const banList = new PolicyList(banListId, banListId, mjolnir);
|
||||
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
||||
|
||||
assert.equal(banList.allRules.length, 0);
|
||||
@ -118,12 +119,12 @@ describe("Test: Updating the BanList", function () {
|
||||
assert.equal(changes[0].previousState['event_id'], modifyingEventId, 'There should be a previous state event for a modified rule');
|
||||
assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1);
|
||||
})
|
||||
it("Will remove rules with old types when they are 'soft redacted' with a different but more recent event type.", async function () {
|
||||
it("Will remove rules with old types when they are 'soft redacted' with a different but more recent event type.", async function() {
|
||||
this.timeout(3000);
|
||||
const mjolnir = config.RUNTIME.client!
|
||||
const moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
|
||||
const banList = new BanList(banListId, banListId, mjolnir);
|
||||
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()] });
|
||||
const banList = new PolicyList(banListId, banListId, mjolnir);
|
||||
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
||||
|
||||
const entity = '@old:localhost:9999';
|
||||
@ -140,12 +141,12 @@ describe("Test: Updating the BanList", function () {
|
||||
assert.equal(changes[0].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule');
|
||||
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 0, 'The rule should no longer be stored.');
|
||||
})
|
||||
it("A rule of the most recent type won't be deleted when an old rule is deleted for the same entity.", async function () {
|
||||
it("A rule of the most recent type won't be deleted when an old rule is deleted for the same entity.", async function() {
|
||||
this.timeout(3000);
|
||||
const mjolnir = config.RUNTIME.client!
|
||||
const moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
|
||||
const banList = new BanList(banListId, banListId, mjolnir);
|
||||
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()] });
|
||||
const banList = new PolicyList(banListId, banListId, mjolnir);
|
||||
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
|
||||
|
||||
const entity = '@old:localhost:9999';
|
||||
@ -179,10 +180,10 @@ describe("Test: Updating the BanList", function () {
|
||||
assert.equal(changes[0].previousState['event_id'], updatedEventId, 'There should be a previous state event for a modified rule');
|
||||
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 0, 'The rule should no longer be stored.');
|
||||
})
|
||||
it('Test: BanList Supports all entity types.', async function () {
|
||||
it('Test: PolicyList Supports all entity types.', async function() {
|
||||
const mjolnir = config.RUNTIME.client!
|
||||
const banListId = await mjolnir.createRoom();
|
||||
const banList = new BanList(banListId, banListId, mjolnir);
|
||||
const banList = new PolicyList(banListId, banListId, mjolnir);
|
||||
for (let i = 0; i < ALL_RULE_TYPES.length; i++) {
|
||||
await createPolicyRule(mjolnir, banListId, ALL_RULE_TYPES[i], `*${i}*`, '');
|
||||
}
|
||||
@ -192,28 +193,28 @@ describe("Test: Updating the BanList", function () {
|
||||
})
|
||||
});
|
||||
|
||||
describe('Test: We do not respond to recommendations other than m.ban in the banlist', function () {
|
||||
it('Will not respond to a rule that has a different recommendation to m.ban (or the unstable equivalent).', async function () {
|
||||
describe('Test: We do not respond to recommendations other than m.ban in the banlist', function() {
|
||||
it('Will not respond to a rule that has a different recommendation to m.ban (or the unstable equivalent).', async function() {
|
||||
const mjolnir = config.RUNTIME.client!
|
||||
const banListId = await mjolnir.createRoom();
|
||||
const banList = new BanList(banListId, banListId, mjolnir);
|
||||
await createPolicyRule(mjolnir, banListId, RULE_SERVER, 'exmaple.org', '', {recommendation: 'something that is not m.ban'});
|
||||
const banList = new PolicyList(banListId, banListId, mjolnir);
|
||||
await createPolicyRule(mjolnir, 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.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);
|
||||
assert.equal(banList.allRules.length, 0);
|
||||
assert.equal(banList.serverRules.length, 0, `We should have an empty serverRules, got ${JSON.stringify(banList.serverRules)}`);
|
||||
assert.equal(banList.allRules.length, 0, `We should have an empty allRules, got ${JSON.stringify(banList.allRules)}`);
|
||||
})
|
||||
})
|
||||
|
||||
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 () {
|
||||
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 = config.RUNTIME.client!
|
||||
const serverName = new UserID(await mjolnir.getUserId()).domain;
|
||||
const banListId = await mjolnir.createRoom();
|
||||
const banList = new BanList(banListId, banListId, mjolnir);
|
||||
const banList = new PolicyList(banListId, banListId, mjolnir);
|
||||
await createPolicyRule(mjolnir, banListId, RULE_SERVER, serverName, '');
|
||||
await createPolicyRule(mjolnir, banListId, RULE_SERVER, 'evil.com', '');
|
||||
await createPolicyRule(mjolnir, banListId, RULE_SERVER, '*', '');
|
||||
@ -229,18 +230,18 @@ 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 BanList', async 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 = config.RUNTIME.client!
|
||||
const serverName: string = new UserID(await mjolnir.getUserId()).domain
|
||||
const moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||
moderator.joinRoom(this.mjolnir.managementRoomId);
|
||||
const mjolnirId = await mjolnir.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]});
|
||||
const room = await moderator.createRoom({ invite: [mjolnirId] });
|
||||
await mjolnir.joinRoom(room);
|
||||
await moderator.setUserPowerLevel(mjolnirId, room, 100);
|
||||
await this.mjolnir!.addProtectedRoom(room);
|
||||
@ -284,7 +285,7 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun
|
||||
await getMessagesByUserIn(mjolnir, mjolnirId, room, 100, events => {
|
||||
events.forEach(event => event.type === 'm.room.server_acl' ? aclEventCount += 1 : null);
|
||||
});
|
||||
LogService.debug('BanListTest', `aclEventCount: ${aclEventCount}`);
|
||||
LogService.debug('PolicyListTest', `aclEventCount: ${aclEventCount}`);
|
||||
// If there's less than two then it means the ACL was updated by this test calling `this.mjolnir!.syncLists()`
|
||||
// and not the listener that detects changes to ban lists (that we want to test!).
|
||||
// It used to be 10, but it was too low, 30 seems better for CI.
|
||||
@ -293,19 +294,19 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun
|
||||
})
|
||||
})
|
||||
|
||||
describe('Test: unbaning entities via the BanList.', function () {
|
||||
describe('Test: unbaning entities via the PolicyList.', function() {
|
||||
afterEach(function() { this.moderator?.stop(); });
|
||||
it('Will remove rules that have legacy types', async function () {
|
||||
it('Will remove rules that have legacy types', async function() {
|
||||
this.timeout(20000)
|
||||
const mjolnir = config.RUNTIME.client!
|
||||
const serverName: string = new UserID(await mjolnir.getUserId()).domain
|
||||
const moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||
this.moderator = moderator;
|
||||
moderator.joinRoom(this.mjolnir.managementRoomId);
|
||||
const mjolnirId = await mjolnir.getUserId();
|
||||
|
||||
// We'll make 1 protected room to test ACLs in.
|
||||
const protectedRoom = await moderator.createRoom({ invite: [mjolnirId]});
|
||||
const protectedRoom = await moderator.createRoom({ invite: [mjolnirId] });
|
||||
await mjolnir.joinRoom(protectedRoom);
|
||||
await moderator.setUserPowerLevel(mjolnirId, protectedRoom, 100);
|
||||
await this.mjolnir!.addProtectedRoom(protectedRoom);
|
||||
@ -315,14 +316,14 @@ describe('Test: unbaning entities via the BanList.', function () {
|
||||
const roomAcl = await mjolnir.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 BanList.
|
||||
// Create some legacy rules on a PolicyList.
|
||||
const banListId = await moderator.createRoom({ invite: [mjolnirId] });
|
||||
await moderator.setUserPowerLevel(await mjolnir.getUserId(), banListId, 100);
|
||||
await moderator.sendStateEvent(banListId, 'org.matrix.mjolnir.shortcode', '', { shortcode: "unban-test"});
|
||||
await moderator.sendStateEvent(banListId, 'org.matrix.mjolnir.shortcode', '', { shortcode: "unban-test" });
|
||||
await mjolnir.joinRoom(banListId);
|
||||
this.mjolnir!.watchList(Permalinks.forRoom(banListId));
|
||||
// we use this to compare changes.
|
||||
const banList = new BanList(banListId, banListId, moderator);
|
||||
const banList = new PolicyList(banListId, banListId, moderator);
|
||||
// we need two because we need to test the case where an entity has all rule types in the list
|
||||
// and another one that only has one (so that we would hit 404 while looking up state)
|
||||
const olderBadServer = "old.evil.com"
|
||||
@ -348,7 +349,7 @@ describe('Test: unbaning entities via the BanList.', function () {
|
||||
await moderator.start();
|
||||
for (const server of [olderBadServer, newerBadServer]) {
|
||||
await getFirstReaction(moderator, this.mjolnir.managementRoomId, '✅', async () => {
|
||||
return await moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text', body: `!mjolnir unban unban-test server ${server}`});
|
||||
return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir unban unban-test server ${server}` });
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
@ -365,19 +366,19 @@ describe('Test: unbaning entities via the BanList.', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Test: should apply bans to the most recently active rooms first', function () {
|
||||
it('Applies bans to the most recently active rooms first', async function () {
|
||||
describe('Test: should apply bans to the most recently active rooms first', function() {
|
||||
it('Applies bans to the most recently active rooms first', async function() {
|
||||
this.timeout(180000)
|
||||
const mjolnir = config.RUNTIME.client!
|
||||
const serverName: string = new UserID(await mjolnir.getUserId()).domain
|
||||
const moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||
const moderator = await newTestUser({ name: { contains: "moderator" } });
|
||||
moderator.joinRoom(this.mjolnir.managementRoomId);
|
||||
const mjolnirId = await mjolnir.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]});
|
||||
const room = await moderator.createRoom({ invite: [mjolnirId] });
|
||||
await mjolnir.joinRoom(room);
|
||||
await moderator.setUserPowerLevel(mjolnirId, room, 100);
|
||||
await this.mjolnir!.addProtectedRoom(room);
|
||||
@ -387,7 +388,7 @@ 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 this.mjolnir!.syncLists();
|
||||
await Promise.all(protectedRooms.map(async room => {
|
||||
const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", "").catch(e => e.statusCode === 404 ? {deny: []} : Promise.reject(e));
|
||||
const roomAcl = await mjolnir.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.');
|
||||
}));
|
||||
|
||||
@ -405,7 +406,7 @@ describe('Test: should apply bans to the most recently active rooms first', func
|
||||
}
|
||||
// create some activity in the same order.
|
||||
for (const roomId of protectedRooms.slice().reverse()) {
|
||||
await mjolnir.sendMessage(roomId, {body: `activity`, msgtype: 'm.text'});
|
||||
await mjolnir.sendMessage(roomId, { body: `activity`, msgtype: 'm.text' });
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
@ -429,7 +430,7 @@ describe('Test: should apply bans to the most recently active rooms first', func
|
||||
// Check that the most recently active rooms got the ACL update first.
|
||||
let last_event_ts = 0;
|
||||
for (const roomId of protectedRooms) {
|
||||
let roomAclEvent: null|any;
|
||||
let roomAclEvent: null | any;
|
||||
// Can't be the best way to get the whole event, but ok.
|
||||
await getMessagesByUserIn(mjolnir, mjolnirId, roomId, 1, events => roomAclEvent = events[0]);
|
||||
const roomAcl = roomAclEvent!.content;
|
||||
|
Loading…
Reference in New Issue
Block a user