mirror of
https://github.com/matrix-org/mjolnir.git
synced 2024-10-01 01:36:06 -04:00
WIP: Computing opinions
This commit is contained in:
parent
81eba65ae3
commit
9425118b60
@ -355,7 +355,7 @@ export class Mjolnir {
|
||||
this.currentState = STATE_SYNCING;
|
||||
if (config.syncOnStartup) {
|
||||
await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Syncing lists...");
|
||||
await this.syncLists(config.verboseLogging);
|
||||
await this.syncPolicyLists(config.verboseLogging);
|
||||
await this.registerProtections();
|
||||
}
|
||||
|
||||
@ -434,7 +434,7 @@ export class Mjolnir {
|
||||
const rooms = (additionalProtectedRooms?.rooms ?? []);
|
||||
rooms.push(roomId);
|
||||
await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, { rooms: rooms });
|
||||
await this.syncLists(config.verboseLogging);
|
||||
await this.syncPolicyLists(config.verboseLogging);
|
||||
}
|
||||
|
||||
public async removeProtectedRoom(roomId: string) {
|
||||
@ -482,7 +482,7 @@ export class Mjolnir {
|
||||
this.applyUnprotectedRooms();
|
||||
|
||||
if (withSync) {
|
||||
await this.syncLists(config.verboseLogging);
|
||||
await this.syncPolicyLists(config.verboseLogging);
|
||||
}
|
||||
}
|
||||
|
||||
@ -882,7 +882,7 @@ export class Mjolnir {
|
||||
* Sync all the rooms with all the watched lists, banning and applying any changed ACLS.
|
||||
* @param verbose Whether to report any errors to the management room.
|
||||
*/
|
||||
public async syncLists(verbose = true) {
|
||||
public async syncPolicyLists(verbose = true) {
|
||||
for (const list of this.policyLists) {
|
||||
const changes = await list.updateList();
|
||||
await this.printBanlistChanges(changes, list, true);
|
||||
|
@ -163,7 +163,7 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol
|
||||
|
||||
if (unbannedSomeone) {
|
||||
await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Syncing lists to ensure no users were accidentally unbanned`);
|
||||
await mjolnir.syncLists(config.verboseLogging);
|
||||
await mjolnir.syncPolicyLists(config.verboseLogging);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { extractRequestError, LogService, MatrixClient, UserID } from "matrix-bot-sdk";
|
||||
import { EventEmitter } from "events";
|
||||
import { ALL_RULE_TYPES, EntityType, PolicyRule, Recommendation, ROOM_RULE_TYPES, RULE_ROOM, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES, USER_RULE_TYPES } from "./PolicyRule";
|
||||
import { ALL_RULE_TYPES, EntityType, PolicyRule, PolicyRuleOpinion, Recommendation, ROOM_RULE_TYPES, RULE_ROOM, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES, USER_RULE_TYPES } from "./PolicyRule";
|
||||
|
||||
export const SHORTCODE_EVENT_TYPE = "org.matrix.mjolnir.shortcode";
|
||||
|
||||
@ -61,8 +61,13 @@ declare interface PolicyList {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* A readonly view of the rules within a policy room.
|
||||
*
|
||||
* The interpretation of these rules is left to the client of this API, so e.g. a `m.ban` within a `PolicyList`
|
||||
* could represent a room ban or a server ban.
|
||||
*
|
||||
* To update events in the policy room, send `m.policy.rule.*` events into the room. This will (eventually) cause
|
||||
* a `/sync`, which will update the `PolicyList`.
|
||||
*/
|
||||
class PolicyList extends EventEmitter {
|
||||
private shortcode: string | null = null;
|
||||
@ -167,24 +172,14 @@ class PolicyList extends EventEmitter {
|
||||
/**
|
||||
* Return all of the rules in this list that will match the provided entity.
|
||||
* If the entity is a user, then we match the domain part against server rules too.
|
||||
* @param ruleKind The type of rule for the entity e.g. `RULE_USER`.
|
||||
* @param ruleKind The type of rule for the entity e.g. `RULE_USER`. If unspecified, extract the type from `entity`.
|
||||
* @param entity The entity to test e.g. the user id, server name or a room id.
|
||||
* @returns All of the rules that match this entity.
|
||||
*/
|
||||
public rulesMatchingEntity(entity: string, recommendation: Recommendation | "*", ruleKind?: EntityType): PolicyRule[] {
|
||||
const ruleTypeOf: (entityPart: string) => EntityType = (entityPart: string) => {
|
||||
if (ruleKind) {
|
||||
return ruleKind;
|
||||
} else if (entityPart.startsWith("#") || entityPart.startsWith("!")) {
|
||||
return EntityType.RULE_ROOM;
|
||||
} else if (entity.startsWith("@")) {
|
||||
return EntityType.RULE_USER;
|
||||
} else {
|
||||
return EntityType.RULE_SERVER;
|
||||
}
|
||||
};
|
||||
ruleKind = ruleKind || extractEntityType(entity);
|
||||
|
||||
if (ruleTypeOf(entity) === RULE_USER) {
|
||||
if (ruleKind === RULE_USER) {
|
||||
// We special case because want to see whether a server ban is preventing this user from participating too.
|
||||
const userId = new UserID(entity);
|
||||
return [
|
||||
@ -192,10 +187,58 @@ class PolicyList extends EventEmitter {
|
||||
...this.getServerRules(recommendation).filter(rule => rule.isMatch(userId.domain))
|
||||
]
|
||||
} else {
|
||||
return this.rulesOfKind(ruleTypeOf(entity), recommendation).filter(rule => rule.isMatch(entity));
|
||||
return this.rulesOfKind(ruleKind, recommendation).filter(rule => rule.isMatch(entity));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the opinion for a given entity.
|
||||
* @returns The opinion specified by this list or `0` if no opinion was defined.
|
||||
*/
|
||||
public opinionForEntity(entity: string, ruleKind?: EntityType): number {
|
||||
ruleKind = ruleKind || extractEntityType(entity);
|
||||
|
||||
// Find all rules for this entity.
|
||||
const allRules = this.rulesMatchingEntity(entity, Recommendation.Opinion, ruleKind);
|
||||
|
||||
// Split across server rules and specialized rules.
|
||||
const byGenericity: PolicyRule[][] = [
|
||||
/* User/room rules without wildcards */ [],
|
||||
/* User/room rules with wildcards */ [],
|
||||
/* Server rules without wildcards */ [],
|
||||
/* Server rules with wildcards */ [],
|
||||
];
|
||||
for (let rule of allRules) {
|
||||
const baseIndex = rule.kind === EntityType.RULE_SERVER ?
|
||||
2 : 0;
|
||||
const finalIndex = rule.isGeneric ?
|
||||
baseIndex + 1 : baseIndex;
|
||||
byGenericity[finalIndex].push(rule);
|
||||
}
|
||||
|
||||
// Find the most specific rule for this entity.
|
||||
// If there is more than one, pick the most recent entry.
|
||||
for (let rules of byGenericity) {
|
||||
let mostRecentRule = null;
|
||||
for (let rule of rules) {
|
||||
if (!mostRecentRule || rule.ts > mostRecentRule.ts) {
|
||||
mostRecentRule = rule;
|
||||
}
|
||||
}
|
||||
if (mostRecentRule) {
|
||||
if (mostRecentRule instanceof PolicyRuleOpinion) {
|
||||
return mostRecentRule.opinion;
|
||||
} else {
|
||||
// This should be impossible.
|
||||
throw new TypeError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// By default, the opinion is 0.
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all rules in the banList for this entity that have the same state key (as when we ban them)
|
||||
* by searching for rules that have legacy state types.
|
||||
@ -404,3 +447,19 @@ class UpdateBatcher {
|
||||
this.checkBatch(eventId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the entity type best matching a given entity.
|
||||
*
|
||||
* @param entity If this entity is a user id, return `RULE_USER`. If it is a room id or alias, return `RULE_ROOM`.
|
||||
* Otherwise, return `RULE_SERVER`.
|
||||
*/
|
||||
function extractEntityType(entity: string): EntityType {
|
||||
if (entity.startsWith("#") || entity.startsWith("!")) {
|
||||
return EntityType.RULE_ROOM;
|
||||
} else if (entity.startsWith("@")) {
|
||||
return EntityType.RULE_ROOM;
|
||||
} else {
|
||||
return EntityType.RULE_SERVER;
|
||||
}
|
||||
}
|
@ -86,6 +86,11 @@ export abstract class PolicyRule {
|
||||
* A glob for `entity`.
|
||||
*/
|
||||
private glob: MatrixGlob;
|
||||
|
||||
/**
|
||||
* `true` if the rule contains at least one wildcard (`?` or `*`).
|
||||
*/
|
||||
public readonly isGeneric: boolean;
|
||||
constructor(
|
||||
/**
|
||||
* The entity covered by this rule, e.g. a glob user ID, a room ID, a server domain.
|
||||
@ -95,6 +100,10 @@ export abstract class PolicyRule {
|
||||
* A human-readable reason for this rule, for audit purposes.
|
||||
*/
|
||||
public readonly reason: string,
|
||||
/**
|
||||
* A Matrix timestamp for the instant this rule was issued.
|
||||
*/
|
||||
public readonly ts: number,
|
||||
/**
|
||||
* The type of entity for this rule, e.g. user, server domain, etc.
|
||||
*/
|
||||
@ -103,8 +112,10 @@ export abstract class PolicyRule {
|
||||
* 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) {
|
||||
public readonly recommendation: Recommendation | null,
|
||||
) {
|
||||
this.glob = new MatrixGlob(entity);
|
||||
this.isGeneric = entity.includes("?") || entity.includes("*");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -120,9 +131,13 @@ export abstract class PolicyRule {
|
||||
* @param event An *untrusted* event.
|
||||
* @returns null if the PolicyRule is invalid or not recognized by Mjölnir.
|
||||
*/
|
||||
public static parse(event: {type: string, content: any}): PolicyRule | null {
|
||||
public static parse(event: {type: string, origin_server_ts: number, content: any}): PolicyRule | null {
|
||||
// Parse common fields.
|
||||
// If a field is ill-formed, discard the rule.
|
||||
const ts = event['origin_server_ts'];
|
||||
if (!ts || typeof ts !== 'number') {
|
||||
return null;
|
||||
}
|
||||
const content = event['content'];
|
||||
if (!content || typeof content !== "object") {
|
||||
return null;
|
||||
@ -155,17 +170,17 @@ export abstract class PolicyRule {
|
||||
|
||||
// From this point, we may need specific fields.
|
||||
if (RECOMMENDATION_BAN_VARIANTS.includes(recommendation)) {
|
||||
return new PolicyRuleBan(entity, reason, kind);
|
||||
return new PolicyRuleBan(entity, reason, ts, kind);
|
||||
} else if (RECOMMENDATION_OPINION_VARIANTS.includes(recommendation)) {
|
||||
let opinion = content['opinion'];
|
||||
if (!Number.isInteger(opinion)) {
|
||||
return null;
|
||||
}
|
||||
return new PolicyRuleOpinion(entity, reason, kind, opinion);
|
||||
return new PolicyRuleOpinion(entity, reason, ts, kind, opinion);
|
||||
} else {
|
||||
// As long as the `recommendation` is defined, we assume
|
||||
// that the rule is correct, just unknown.
|
||||
return new PolicyRuleUnknown(entity, reason, kind, content);
|
||||
return new PolicyRuleUnknown(entity, reason, ts, kind, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -183,12 +198,16 @@ export class PolicyRuleBan extends PolicyRule {
|
||||
* A human-readable reason for this rule, for audit purposes.
|
||||
*/
|
||||
reason: string,
|
||||
/**
|
||||
* A Matrix timestamp for the instant this rule was issued.
|
||||
*/
|
||||
ts: number,
|
||||
/**
|
||||
* The type of entity for this rule, e.g. user, server domain, etc.
|
||||
*/
|
||||
kind: EntityType,
|
||||
) {
|
||||
super(entity, reason, kind, Recommendation.Ban)
|
||||
super(entity, reason, ts, kind, Recommendation.Ban)
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,6 +224,10 @@ export class PolicyRuleOpinion extends PolicyRule {
|
||||
* A human-readable reason for this rule, for audit purposes.
|
||||
*/
|
||||
reason: string,
|
||||
/**
|
||||
* A Matrix timestamp for the instant this rule was issued.
|
||||
*/
|
||||
ts: number,
|
||||
/**
|
||||
* The type of entity for this rule, e.g. user, server domain, etc.
|
||||
*/
|
||||
@ -214,9 +237,9 @@ export class PolicyRuleOpinion extends PolicyRule {
|
||||
* 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
|
||||
public readonly opinion: number,
|
||||
) {
|
||||
super(entity, reason, kind, Recommendation.Opinion);
|
||||
super(entity, reason, ts, kind, Recommendation.Opinion);
|
||||
if (!Number.isInteger(opinion)) {
|
||||
throw new TypeError(`The opinion must be an integer, got ${opinion}`);
|
||||
}
|
||||
@ -240,6 +263,10 @@ export class PolicyRuleUnknown extends PolicyRule {
|
||||
*/
|
||||
reason: string,
|
||||
/**
|
||||
* A Matrix timestamp for the instant this rule was issued.
|
||||
*/
|
||||
ts: number,
|
||||
/**
|
||||
* The type of entity for this rule, e.g. user, server domain, etc.
|
||||
*/
|
||||
kind: EntityType,
|
||||
@ -248,6 +275,6 @@ export class PolicyRuleUnknown extends PolicyRule {
|
||||
*/
|
||||
public readonly content: any,
|
||||
) {
|
||||
super(entity, reason, kind, null);
|
||||
super(entity, reason, ts, kind, null);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user