mirror of
https://github.com/matrix-org/mjolnir.git
synced 2024-10-01 01:36:06 -04:00
Support multiple ban lists
This commit is contained in:
parent
56bd7c9500
commit
aecc6e4882
@ -20,7 +20,7 @@ Phase 2:
|
||||
* [ ] Command to import ACLs, etc from rooms
|
||||
* [x] Vet rooms on startup option
|
||||
* [ ] Command to actually unban users (instead of leaving them stuck)
|
||||
* [ ] Support multiple lists
|
||||
* [x] Support multiple lists
|
||||
|
||||
Phase 3:
|
||||
* [ ] Synapse antispam module
|
||||
|
@ -42,12 +42,6 @@ syncOnStartup: true
|
||||
# resets, etc) before Mjolnir is needed.
|
||||
verifyPermissionsOnStartup: true
|
||||
|
||||
# The room ID or alias where the bot's own personal ban list is kept. This is
|
||||
# where the commands to manage a ban list end up being routed to. Note that
|
||||
# this room is NOT automatically added to the banLists list below - you will
|
||||
# need to add it yourself!
|
||||
publishedBanListRoom: "#banlist:example.org"
|
||||
|
||||
# A list of rooms to protect (matrix.to URLs)
|
||||
protectedRooms:
|
||||
- "https://matrix.to/#/#yourroom:example.org"
|
||||
|
@ -36,7 +36,6 @@ export class Mjolnir {
|
||||
constructor(
|
||||
public readonly client: MatrixClient,
|
||||
public readonly managementRoomId: string,
|
||||
public readonly publishedBanListRoomId: string,
|
||||
public readonly protectedRooms: { [roomId: string]: string },
|
||||
public readonly banLists: BanList[],
|
||||
) {
|
||||
|
@ -22,6 +22,7 @@ import { LogService, RichReply } from "matrix-bot-sdk";
|
||||
import * as htmlEscape from "escape-html";
|
||||
import { execSyncCommand } from "./SyncCommand";
|
||||
import { execPermissionCheckCommand } from "./PermissionCheckCommand";
|
||||
import { execCreateListCommand } from "./CreateBanListCommand";
|
||||
|
||||
export const COMMAND_PREFIX = "!mjolnir";
|
||||
|
||||
@ -32,9 +33,9 @@ export function handleCommand(roomId: string, event: any, mjolnir: Mjolnir) {
|
||||
try {
|
||||
if (parts.length === 1 || parts[1] === 'status') {
|
||||
return execStatusCommand(roomId, event, mjolnir);
|
||||
} else if (parts[1] === 'ban' && parts.length > 3) {
|
||||
} else if (parts[1] === 'ban' && parts.length > 4) {
|
||||
return execBanCommand(roomId, event, mjolnir, parts);
|
||||
} else if (parts[1] === 'unban' && parts.length > 3) {
|
||||
} else if (parts[1] === 'unban' && parts.length > 4) {
|
||||
return execUnbanCommand(roomId, event, mjolnir, parts);
|
||||
} else if (parts[1] === 'rules') {
|
||||
return execDumpRulesCommand(roomId, event, mjolnir);
|
||||
@ -42,17 +43,20 @@ export function handleCommand(roomId: string, event: any, mjolnir: Mjolnir) {
|
||||
return execSyncCommand(roomId, event, mjolnir);
|
||||
} else if (parts[1] === 'verify') {
|
||||
return execPermissionCheckCommand(roomId, event, mjolnir);
|
||||
} else if (parts.length >= 5 && parts[1] === 'list' && parts[2] === 'create') {
|
||||
return execCreateListCommand(roomId, event, mjolnir, parts);
|
||||
} else {
|
||||
// Help menu
|
||||
const menu = "" +
|
||||
"!mjolnir - Print status information\n" +
|
||||
"!mjolnir status - Print status information\n" +
|
||||
"!mjolnir ban <user|room|server> <glob> [reason] - Adds an entity to the ban list\n" +
|
||||
"!mjolnir unban <user|room|server> <glob> - Removes an entity from the ban list\n" +
|
||||
"!mjolnir rules - Lists the rules currently in use by Mjolnir\n" +
|
||||
"!mjolnir sync - Force updates of all lists and re-apply rules\n" +
|
||||
"!mjolnir verify - Ensures Mjolnir can moderate all your rooms\n" +
|
||||
"!mjolnir help - This menu\n";
|
||||
"!mjolnir - Print status information\n" +
|
||||
"!mjolnir status - Print status information\n" +
|
||||
"!mjolnir ban <list_shortcode> <user|room|server> <glob> [reason] - Adds an entity to the ban list\n" +
|
||||
"!mjolnir unban <list_shortcode> <user|room|server> <glob> - Removes an entity from the ban list\n" +
|
||||
"!mjolnir rules - Lists the rules currently in use by Mjolnir\n" +
|
||||
"!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 <shortcode> <alias_localpart> - Creates a new ban list with the given shortcode and alias\n" +
|
||||
"!mjolnir help - This menu\n";
|
||||
const html = `<b>Mjolnir help:</b><br><pre><code>${htmlEscape(menu)}</code></pre>`;
|
||||
const text = `Mjolnir help:\n${menu}`;
|
||||
const reply = RichReply.createFor(roomId, event, text, html);
|
||||
|
63
src/commands/CreateBanListCommand.ts
Normal file
63
src/commands/CreateBanListCommand.ts
Normal file
@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
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 { Mjolnir } from "../Mjolnir";
|
||||
import { SHORTCODE_EVENT_TYPE } from "../models/BanList";
|
||||
import { Permalinks, RichReply } from "matrix-bot-sdk";
|
||||
|
||||
// !mjolnir list create <shortcode> <alias localpart>
|
||||
export async function execCreateListCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
|
||||
const shortcode = parts[3];
|
||||
const aliasLocalpart = parts[4];
|
||||
|
||||
const powerLevels = {
|
||||
"ban": 50,
|
||||
"events": {
|
||||
"m.room.name": 100,
|
||||
"m.room.power_levels": 100,
|
||||
},
|
||||
"events_default": 50, // non-default
|
||||
"invite": 0,
|
||||
"kick": 50,
|
||||
"notifications": {
|
||||
"room": 20,
|
||||
},
|
||||
"redact": 50,
|
||||
"state_default": 50,
|
||||
"users": {
|
||||
// populated in a moment
|
||||
},
|
||||
"users_default": 0,
|
||||
};
|
||||
powerLevels['users'][await mjolnir.client.getUserId()] = 100;
|
||||
powerLevels['users'][event['sender']] = 50;
|
||||
|
||||
const listRoomId = await mjolnir.client.createRoom({
|
||||
preset: "public_chat",
|
||||
room_alias_name: aliasLocalpart,
|
||||
invite: [event['sender']],
|
||||
initial_state: [{type: SHORTCODE_EVENT_TYPE, state_key: "", content: {shortcode: shortcode}}],
|
||||
power_level_content_override: powerLevels,
|
||||
});
|
||||
|
||||
const roomRef = Permalinks.forRoom(listRoomId);
|
||||
|
||||
const html = `Created new list (<a href="${roomRef}">${listRoomId}</a>). It is not tracked - you will have to add this to the Mjolnir config to use it.`;
|
||||
const text = `Created new list (${roomRef}). It is not tracked - you will have to add this to the Mjolnir config to use it.`;
|
||||
const reply = RichReply.createFor(roomId, event, text, html);
|
||||
reply["msgtype"] = "m.notice";
|
||||
await mjolnir.client.sendMessage(roomId, reply);
|
||||
}
|
@ -19,10 +19,11 @@ import { RULE_ROOM, RULE_SERVER, RULE_USER, ruleTypeToStable } from "../models/B
|
||||
import { RichReply } from "matrix-bot-sdk";
|
||||
import { RECOMMENDATION_BAN, recommendationToStable } from "../models/ListRule";
|
||||
|
||||
function parseBits(parts: string[]): { entityType: string, ruleType: string, glob: string, reason: string } {
|
||||
const entityType = parts[2].toLowerCase();
|
||||
const glob = parts[3];
|
||||
const reason = parts.slice(4).join(' ') || "<no reason>";
|
||||
function parseBits(parts: string[]): { listShortcode: string, entityType: string, ruleType: string, glob: string, reason: string } {
|
||||
const shortcode = parts[2].toLowerCase();
|
||||
const entityType = parts[3].toLowerCase();
|
||||
const glob = parts[4];
|
||||
const reason = parts.slice(5).join(' ') || "<no reason>";
|
||||
|
||||
let rule = null;
|
||||
if (entityType === "user") {
|
||||
@ -33,11 +34,11 @@ function parseBits(parts: string[]): { entityType: string, ruleType: string, glo
|
||||
rule = RULE_SERVER;
|
||||
}
|
||||
if (!rule) {
|
||||
return {entityType, ruleType: null, glob, reason};
|
||||
return {listShortcode: shortcode, entityType, ruleType: null, glob, reason};
|
||||
}
|
||||
rule = ruleTypeToStable(rule);
|
||||
|
||||
return {entityType, ruleType: rule, glob, reason};
|
||||
return {listShortcode: shortcode, entityType, ruleType: rule, glob, reason};
|
||||
}
|
||||
|
||||
// !mjolnir ban <user|server|room> <glob> [reason]
|
||||
@ -58,7 +59,15 @@ export async function execBanCommand(roomId: string, event: any, mjolnir: Mjolni
|
||||
};
|
||||
const stateKey = `rule:${bits.glob}`;
|
||||
|
||||
await mjolnir.client.sendStateEvent(mjolnir.publishedBanListRoomId, bits.ruleType, stateKey, ruleContent);
|
||||
const list = mjolnir.banLists.find(b => b.listShortcode === bits.listShortcode);
|
||||
if (!list) {
|
||||
const replyText = "No ban list with that shortcode was found.";
|
||||
const reply = RichReply.createFor(roomId, event, replyText, replyText);
|
||||
reply["msgtype"] = "m.notice";
|
||||
return mjolnir.client.sendMessage(roomId, reply);
|
||||
}
|
||||
|
||||
await mjolnir.client.sendStateEvent(list.roomId, bits.ruleType, stateKey, ruleContent);
|
||||
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
|
||||
}
|
||||
|
||||
@ -75,6 +84,14 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol
|
||||
const ruleContent = {}; // empty == clear/unban
|
||||
const stateKey = `rule:${bits.glob}`;
|
||||
|
||||
await mjolnir.client.sendStateEvent(mjolnir.publishedBanListRoomId, bits.ruleType, stateKey, ruleContent);
|
||||
const list = mjolnir.banLists.find(b => b.listShortcode === bits.listShortcode);
|
||||
if (!list) {
|
||||
const replyText = "No ban list with that shortcode was found.";
|
||||
const reply = RichReply.createFor(roomId, event, replyText, replyText);
|
||||
reply["msgtype"] = "m.notice";
|
||||
return mjolnir.client.sendMessage(roomId, reply);
|
||||
}
|
||||
|
||||
await mjolnir.client.sendStateEvent(list.roomId, bits.ruleType, stateKey, ruleContent);
|
||||
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ interface IConfig {
|
||||
verboseLogging: boolean;
|
||||
syncOnStartup: boolean;
|
||||
verifyPermissionsOnStartup: boolean;
|
||||
publishedBanListRoom: string;
|
||||
protectedRooms: string[]; // matrix.to urls
|
||||
banLists: string[]; // matrix.to urls
|
||||
}
|
||||
|
@ -77,17 +77,11 @@ LogService.setLogger(new RichConsoleLogger());
|
||||
protectedRooms[roomId] = roomRef;
|
||||
}
|
||||
|
||||
// Ensure we've joined the ban list we're publishing too
|
||||
let banListRoomId = await client.resolveRoom(config.publishedBanListRoom);
|
||||
if (!joinedRooms.includes(banListRoomId)) {
|
||||
banListRoomId = await client.joinRoom(config.publishedBanListRoom);
|
||||
}
|
||||
|
||||
// Ensure we're also in the management room
|
||||
const managementRoomId = await client.joinRoom(config.managementRoom);
|
||||
await client.sendNotice(managementRoomId, "Mjolnir is starting up. Use !mjolnir to query status.");
|
||||
|
||||
const bot = new Mjolnir(client, managementRoomId, banListRoomId, protectedRooms, banLists);
|
||||
const bot = new Mjolnir(client, managementRoomId, protectedRooms, banLists);
|
||||
await bot.start();
|
||||
|
||||
LogService.info("index", "Bot started!")
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "matrix-bot-sdk";
|
||||
import { LogService, MatrixClient } from "matrix-bot-sdk";
|
||||
import { ListRule } from "./ListRule";
|
||||
|
||||
export const RULE_USER = "m.room.rule.user";
|
||||
@ -26,6 +26,8 @@ export const ROOM_RULE_TYPES = [RULE_ROOM, "org.matrix.mjolnir.rule.room"];
|
||||
export const SERVER_RULE_TYPES = [RULE_SERVER, "org.matrix.mjolnir.rule.server"];
|
||||
export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES];
|
||||
|
||||
export const SHORTCODE_EVENT_TYPE = "org.matrix.mjolnir.shortcode";
|
||||
|
||||
export function ruleTypeToStable(rule: string, unstable = true): string {
|
||||
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;
|
||||
@ -35,10 +37,24 @@ export function ruleTypeToStable(rule: string, unstable = true): string {
|
||||
|
||||
export default class BanList {
|
||||
private rules: ListRule[] = [];
|
||||
private shortcode: string = null;
|
||||
|
||||
constructor(public readonly roomId: string, public readonly roomRef, private client: MatrixClient) {
|
||||
}
|
||||
|
||||
public get listShortcode(): string {
|
||||
return this.shortcode;
|
||||
}
|
||||
|
||||
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", err);
|
||||
if (this.shortcode === newShortcode) this.shortcode = currentShortcode;
|
||||
});
|
||||
}
|
||||
|
||||
public get serverRules(): ListRule[] {
|
||||
return this.rules.filter(r => r.kind === RULE_SERVER);
|
||||
}
|
||||
@ -56,6 +72,11 @@ export default class BanList {
|
||||
|
||||
const state = await this.client.getRoomState(this.roomId);
|
||||
for (const event of state) {
|
||||
if (event['state_key'] === '' && event['type'] === SHORTCODE_EVENT_TYPE) {
|
||||
this.shortcode = (event['content'] || {})['shortcode'] || null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event['state_key'] === '' || !ALL_RULE_TYPES.includes(event['type'])) {
|
||||
continue;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user