Skip to content

Commit 941cc32

Browse files
authored
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.
1 parent b03d81d commit 941cc32

File tree

3 files changed

+91
-3
lines changed

3 files changed

+91
-3
lines changed

src/commands/CommandHandler.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ limitations under the License.
1717
import { Mjolnir } from "../Mjolnir";
1818
import { execStatusCommand } from "./StatusCommand";
1919
import { execBanCommand, execUnbanCommand } from "./UnbanBanCommand";
20-
import { execDumpRulesCommand } from "./DumpRulesCommand";
20+
import { execDumpRulesCommand, execRulesMatchingCommand } from "./DumpRulesCommand";
2121
import { extractRequestError, LogService, RichReply } from "matrix-bot-sdk";
2222
import { htmlEscape } from "../utils";
2323
import { execSyncCommand } from "./SyncCommand";
@@ -59,6 +59,8 @@ export async function handleCommand(roomId: string, event: { content: { body: st
5959
return await execBanCommand(roomId, event, mjolnir, parts);
6060
} else if (parts[1] === 'unban' && parts.length > 2) {
6161
return await execUnbanCommand(roomId, event, mjolnir, parts);
62+
} else if (parts[1] === 'rules' && parts.length === 4 && parts[2] === 'matching') {
63+
return await execRulesMatchingCommand(roomId, event, mjolnir, parts[3])
6264
} else if (parts[1] === 'rules') {
6365
return await execDumpRulesCommand(roomId, event, mjolnir);
6466
} else if (parts[1] === 'sync') {
@@ -133,6 +135,7 @@ export async function handleCommand(roomId: string, event: { content: { body: st
133135
"!mjolnir redact <event permalink> - Redacts a message by permalink\n" +
134136
"!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" +
135137
"!mjolnir rules - Lists the rules currently in use by Mjolnir\n" +
138+
"!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." +
136139
"!mjolnir sync - Force updates of all lists and re-apply rules\n" +
137140
"!mjolnir verify - Ensures Mjolnir can moderate all your rooms\n" +
138141
"!mjolnir list create <shortcode> <alias localpart> - Creates a new ban list with the given shortcode and alias\n" +

src/commands/DumpRulesCommand.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,63 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { Mjolnir } from "../Mjolnir";
1817
import { RichReply } from "matrix-bot-sdk";
18+
import { Mjolnir } from "../Mjolnir";
19+
import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../models/BanList";
1920
import { htmlEscape } from "../utils";
2021

22+
/**
23+
* List all of the rules that match a given entity.
24+
* The reason why you want to test against all rules and not just e.g. user or server is because
25+
* there are situations where rules of different types can ban other entities e.g. server ACL can cause users to be banned.
26+
* @param roomId The room the command is from.
27+
* @param event The event containing the command.
28+
* @param mjolnir A mjolnir to fetch the watched lists from.
29+
* @param entity a user, room id or server.
30+
* @returns When a response has been sent to the command.
31+
*/
32+
export async function execRulesMatchingCommand(roomId: string, event: any, mjolnir: Mjolnir, entity: string) {
33+
let html = "";
34+
let text = "";
35+
for (const list of mjolnir.lists) {
36+
const matches = list.rulesMatchingEntity(entity)
37+
38+
if (matches.length === 0) {
39+
continue;
40+
}
41+
42+
const matchesInfo = `Found ${matches.length} ` + (matches.length === 1 ? 'match:' : 'matches:');
43+
const shortcodeInfo = list.listShortcode ? ` (shortcode: ${htmlEscape(list.listShortcode)})` : '';
44+
45+
html += `<a href="${htmlEscape(list.roomRef)}">${htmlEscape(list.roomId)}</a>${shortcodeInfo} ${matchesInfo}<br/><ul>`;
46+
text += `${list.roomRef}${shortcodeInfo} ${matchesInfo}:\n`;
47+
48+
for (const rule of matches) {
49+
// If we know the rule kind, we will give it a readable name, otherwise just use its name.
50+
let ruleKind: string = rule.kind;
51+
if (ruleKind === RULE_USER) {
52+
ruleKind = 'user';
53+
} else if (ruleKind === RULE_SERVER) {
54+
ruleKind = 'server';
55+
} else if (ruleKind === RULE_ROOM) {
56+
ruleKind = 'room';
57+
}
58+
html += `<li>${htmlEscape(ruleKind)} (<code>${htmlEscape(rule.recommendation ?? "")}</code>): <code>${htmlEscape(rule.entity)}</code> (${htmlEscape(rule.reason)})</li>`;
59+
text += `* ${ruleKind} (${rule.recommendation}): ${rule.entity} (${rule.reason})\n`;
60+
}
61+
62+
html += "</ul>";
63+
}
64+
65+
if (text.length === 0) {
66+
html += `No results for ${htmlEscape(entity)}`;
67+
text += `No results for ${entity}`;
68+
}
69+
const reply = RichReply.createFor(roomId, event, text, html);
70+
reply["msgtype"] = "m.notice";
71+
return mjolnir.client.sendMessage(roomId, reply);
72+
}
73+
2174
// !mjolnir rules
2275
export async function execDumpRulesCommand(roomId: string, event: any, mjolnir: Mjolnir) {
2376
let html = "<b>Rules currently in use:</b><br/>";

src/models/BanList.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { extractRequestError, LogService, MatrixClient } from "matrix-bot-sdk";
17+
import { extractRequestError, LogService, MatrixClient, UserID } from "matrix-bot-sdk";
1818
import { EventEmitter } from "events";
1919
import { ListRule, RECOMMENDATION_BAN } from "./ListRule";
2020

@@ -182,6 +182,38 @@ class BanList extends EventEmitter {
182182
return [...this.serverRules, ...this.userRules, ...this.roomRules];
183183
}
184184

185+
/**
186+
* Return all of the rules in this list that will match the provided entity.
187+
* If the entity is a user, then we match the domain part against server rules too.
188+
* @param ruleKind The type of rule for the entity e.g. `RULE_USER`.
189+
* @param entity The entity to test e.g. the user id, server name or a room id.
190+
* @returns All of the rules that match this entity.
191+
*/
192+
public rulesMatchingEntity(entity: string, ruleKind?: string): ListRule[] {
193+
const ruleTypeOf: (entityPart: string) => string = (entityPart: string) => {
194+
if (ruleKind) {
195+
return ruleKind;
196+
} else if (entityPart.startsWith("#") || entityPart.startsWith("#")) {
197+
return RULE_ROOM;
198+
} else if (entity.startsWith("@")) {
199+
return RULE_USER;
200+
} else {
201+
return RULE_SERVER;
202+
}
203+
};
204+
205+
if (ruleTypeOf(entity) === RULE_USER) {
206+
// We special case because want to see whether a server ban is preventing this user from participating too.
207+
const userId = new UserID(entity);
208+
return [
209+
...this.userRules.filter(rule => rule.isMatch(entity)),
210+
...this.serverRules.filter(rule => rule.isMatch(userId.domain))
211+
]
212+
} else {
213+
return this.rulesOfKind(ruleTypeOf(entity)).filter(rule => rule.isMatch(entity));
214+
}
215+
}
216+
185217
/**
186218
* Remove all rules in the banList for this entity that have the same state key (as when we ban them)
187219
* by searching for rules that have legacy state types.

0 commit comments

Comments
 (0)