Add !mjolnir rules matching <entity> to search watched lists. (#307)

* Add `!mjolnir rules matching <entity> to search watched lists.

Lists all the rules that will match the entity.
This commit is contained in:
Gnuxie 2022-07-07 13:03:03 +01:00 committed by GitHub
parent b03d81dcc4
commit 941cc32ddd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 91 additions and 3 deletions

View File

@ -17,7 +17,7 @@ limitations under the License.
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { execStatusCommand } from "./StatusCommand"; import { execStatusCommand } from "./StatusCommand";
import { execBanCommand, execUnbanCommand } from "./UnbanBanCommand"; import { execBanCommand, execUnbanCommand } from "./UnbanBanCommand";
import { execDumpRulesCommand } from "./DumpRulesCommand"; import { execDumpRulesCommand, execRulesMatchingCommand } from "./DumpRulesCommand";
import { extractRequestError, LogService, RichReply } from "matrix-bot-sdk"; import { extractRequestError, LogService, RichReply } from "matrix-bot-sdk";
import { htmlEscape } from "../utils"; import { htmlEscape } from "../utils";
import { execSyncCommand } from "./SyncCommand"; 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); return await execBanCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'unban' && parts.length > 2) { } else if (parts[1] === 'unban' && parts.length > 2) {
return await execUnbanCommand(roomId, event, mjolnir, parts); 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') { } else if (parts[1] === 'rules') {
return await execDumpRulesCommand(roomId, event, mjolnir); return await execDumpRulesCommand(roomId, event, mjolnir);
} else if (parts[1] === 'sync') { } else if (parts[1] === 'sync') {
@ -133,6 +135,7 @@ export async function handleCommand(roomId: string, event: { content: { body: st
"!mjolnir redact <event permalink> - Redacts a message by permalink\n" + "!mjolnir redact <event permalink> - Redacts a message by permalink\n" +
"!mjolnir kick <glob> [room alias/ID] [reason] - Kicks a user or all of those matching a glob in a particular room or all protected rooms\n" + "!mjolnir kick <glob> [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 - Lists the rules currently in use by Mjolnir\n" +
"!mjolnir rules matching <user|room|server> - 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 sync - Force updates of all lists and re-apply rules\n" +
"!mjolnir verify - Ensures Mjolnir can moderate all your rooms\n" + "!mjolnir verify - Ensures Mjolnir can moderate all your rooms\n" +
"!mjolnir list create <shortcode> <alias localpart> - Creates a new ban list with the given shortcode and alias\n" + "!mjolnir list create <shortcode> <alias localpart> - Creates a new ban list with the given shortcode and alias\n" +

View File

@ -14,10 +14,63 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Mjolnir } from "../Mjolnir";
import { RichReply } from "matrix-bot-sdk"; import { RichReply } from "matrix-bot-sdk";
import { Mjolnir } from "../Mjolnir";
import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../models/BanList";
import { htmlEscape } from "../utils"; 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 += `<a href="${htmlEscape(list.roomRef)}">${htmlEscape(list.roomId)}</a>${shortcodeInfo} ${matchesInfo}<br/><ul>`;
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 += `<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`;
}
html += "</ul>";
}
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 // !mjolnir rules
export async function execDumpRulesCommand(roomId: string, event: any, mjolnir: Mjolnir) { export async function execDumpRulesCommand(roomId: string, event: any, mjolnir: Mjolnir) {
let html = "<b>Rules currently in use:</b><br/>"; let html = "<b>Rules currently in use:</b><br/>";

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { EventEmitter } from "events";
import { ListRule, RECOMMENDATION_BAN } from "./ListRule"; import { ListRule, RECOMMENDATION_BAN } from "./ListRule";
@ -182,6 +182,38 @@ class BanList extends EventEmitter {
return [...this.serverRules, ...this.userRules, ...this.roomRules]; 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) * 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. * by searching for rules that have legacy state types.