From 941cc32ddd1cfa0f3215df0dea07a54c9ca39953 Mon Sep 17 00:00:00 2001 From: Gnuxie <50846879+Gnuxie@users.noreply.github.com> Date: Thu, 7 Jul 2022 13:03:03 +0100 Subject: [PATCH] Add `!mjolnir rules matching ` to search watched lists. (#307) * Add `!mjolnir rules matching to search watched lists. Lists all the rules that will match the entity. --- src/commands/CommandHandler.ts | 5 ++- src/commands/DumpRulesCommand.ts | 55 +++++++++++++++++++++++++++++++- src/models/BanList.ts | 34 +++++++++++++++++++- 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index ea12167..4d96780 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -17,7 +17,7 @@ limitations under the License. import { Mjolnir } from "../Mjolnir"; import { execStatusCommand } from "./StatusCommand"; import { execBanCommand, execUnbanCommand } from "./UnbanBanCommand"; -import { execDumpRulesCommand } from "./DumpRulesCommand"; +import { execDumpRulesCommand, execRulesMatchingCommand } from "./DumpRulesCommand"; import { extractRequestError, LogService, RichReply } from "matrix-bot-sdk"; import { htmlEscape } from "../utils"; import { execSyncCommand } from "./SyncCommand"; @@ -59,6 +59,8 @@ export async function handleCommand(roomId: string, event: { content: { body: st return await execBanCommand(roomId, event, mjolnir, parts); } else if (parts[1] === 'unban' && parts.length > 2) { return await execUnbanCommand(roomId, event, mjolnir, parts); + } else if (parts[1] === 'rules' && parts.length === 4 && parts[2] === 'matching') { + return await execRulesMatchingCommand(roomId, event, mjolnir, parts[3]) } else if (parts[1] === 'rules') { return await execDumpRulesCommand(roomId, event, mjolnir); } else if (parts[1] === 'sync') { @@ -133,6 +135,7 @@ export async function handleCommand(roomId: string, event: { content: { body: st "!mjolnir redact - Redacts a message by permalink\n" + "!mjolnir kick [room alias/ID] [reason] - Kicks a user or all of those matching a glob in a particular room or all protected rooms\n" + "!mjolnir rules - Lists the rules currently in use by Mjolnir\n" + + "!mjolnir rules matching - Lists the rules in use that will match this entity e.g. `!rules matching @foo:example.com` will show all the user and server rules, including globs, that match this user." + "!mjolnir sync - Force updates of all lists and re-apply rules\n" + "!mjolnir verify - Ensures Mjolnir can moderate all your rooms\n" + "!mjolnir list create - Creates a new ban list with the given shortcode and alias\n" + diff --git a/src/commands/DumpRulesCommand.ts b/src/commands/DumpRulesCommand.ts index a2ee142..2c6c3d7 100644 --- a/src/commands/DumpRulesCommand.ts +++ b/src/commands/DumpRulesCommand.ts @@ -14,10 +14,63 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Mjolnir } from "../Mjolnir"; import { RichReply } from "matrix-bot-sdk"; +import { Mjolnir } from "../Mjolnir"; +import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../models/BanList"; import { htmlEscape } from "../utils"; +/** + * List all of the rules that match a given entity. + * The reason why you want to test against all rules and not just e.g. user or server is because + * there are situations where rules of different types can ban other entities e.g. server ACL can cause users to be banned. + * @param roomId The room the command is from. + * @param event The event containing the command. + * @param mjolnir A mjolnir to fetch the watched lists from. + * @param entity a user, room id or server. + * @returns When a response has been sent to the command. + */ +export async function execRulesMatchingCommand(roomId: string, event: any, mjolnir: Mjolnir, entity: string) { + let html = ""; + let text = ""; + for (const list of mjolnir.lists) { + const matches = list.rulesMatchingEntity(entity) + + if (matches.length === 0) { + continue; + } + + const matchesInfo = `Found ${matches.length} ` + (matches.length === 1 ? 'match:' : 'matches:'); + const shortcodeInfo = list.listShortcode ? ` (shortcode: ${htmlEscape(list.listShortcode)})` : ''; + + html += `${htmlEscape(list.roomId)}${shortcodeInfo} ${matchesInfo}
    `; + text += `${list.roomRef}${shortcodeInfo} ${matchesInfo}:\n`; + + 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'; + } + html += `
  • ${htmlEscape(ruleKind)} (${htmlEscape(rule.recommendation ?? "")}): ${htmlEscape(rule.entity)} (${htmlEscape(rule.reason)})
  • `; + text += `* ${ruleKind} (${rule.recommendation}): ${rule.entity} (${rule.reason})\n`; + } + + html += "
"; + } + + if (text.length === 0) { + html += `No results for ${htmlEscape(entity)}`; + text += `No results for ${entity}`; + } + const reply = RichReply.createFor(roomId, event, text, html); + reply["msgtype"] = "m.notice"; + return mjolnir.client.sendMessage(roomId, reply); +} + // !mjolnir rules export async function execDumpRulesCommand(roomId: string, event: any, mjolnir: Mjolnir) { let html = "Rules currently in use:
"; diff --git a/src/models/BanList.ts b/src/models/BanList.ts index 3b0d0a4..3b56f7f 100644 --- a/src/models/BanList.ts +++ b/src/models/BanList.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { extractRequestError, LogService, MatrixClient } from "matrix-bot-sdk"; +import { extractRequestError, LogService, MatrixClient, UserID } from "matrix-bot-sdk"; import { EventEmitter } from "events"; import { ListRule, RECOMMENDATION_BAN } from "./ListRule"; @@ -182,6 +182,38 @@ class BanList extends EventEmitter { return [...this.serverRules, ...this.userRules, ...this.roomRules]; } + /** + * 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 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, ruleKind?: string): ListRule[] { + const ruleTypeOf: (entityPart: string) => string = (entityPart: string) => { + if (ruleKind) { + return ruleKind; + } else if (entityPart.startsWith("#") || entityPart.startsWith("#")) { + return RULE_ROOM; + } else if (entity.startsWith("@")) { + return RULE_USER; + } else { + return RULE_SERVER; + } + }; + + if (ruleTypeOf(entity) === 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 [ + ...this.userRules.filter(rule => rule.isMatch(entity)), + ...this.serverRules.filter(rule => rule.isMatch(userId.domain)) + ] + } else { + return this.rulesOfKind(ruleTypeOf(entity)).filter(rule => rule.isMatch(entity)); + } + } + /** * 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.