Add commands for managing a personal ban list

This commit is contained in:
Travis Ralston 2019-09-27 15:44:28 -06:00
parent 41cc36e4c6
commit 39b59dbee1
10 changed files with 122 additions and 9 deletions

View File

@ -8,8 +8,8 @@ TODO: Describe what all this means.
Phase 1: Phase 1:
* [ ] Ban users * [ ] Ban users
* [ ] ACL servers * [x] ACL servers
* [ ] Update lists with new bans/ACLs * [x] Update lists with new bans/ACLs
* [ ] "Ban on sight" mode (rather than proactive) * [ ] "Ban on sight" mode (rather than proactive)
Phase 2: Phase 2:

View File

@ -15,6 +15,12 @@ autojoin: true
# This should be a room alias or room ID - not a matrix.to URL. # This should be a room alias or room ID - not a matrix.to URL.
managementRoom: "#moderators:example.org" managementRoom: "#moderators:example.org"
# 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) # A list of rooms to protect (matrix.to URLs)
protectedRooms: protectedRooms:
- "https://matrix.to/#/#yourroom:example.org" - "https://matrix.to/#/#yourroom:example.org"

View File

@ -23,7 +23,8 @@ import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler";
export class Mjolnir { export class Mjolnir {
constructor( constructor(
public readonly client: MatrixClient, public readonly client: MatrixClient,
private managementRoomId: string, public readonly managementRoomId: string,
public readonly publishedBanListRoomId: string,
public readonly protectedRooms: { [roomId: string]: string }, public readonly protectedRooms: { [roomId: string]: string },
public readonly banLists: BanList[], public readonly banLists: BanList[],
) { ) {
@ -49,10 +50,13 @@ export class Mjolnir {
if (!event['state_key']) return; // we also don't do anything with state events that have no state key if (!event['state_key']) return; // we also don't do anything with state events that have no state key
if (ALL_RULE_TYPES.includes(event['type'])) { if (ALL_RULE_TYPES.includes(event['type'])) {
let updated = false;
for (const list of this.banLists) { for (const list of this.banLists) {
if (list.roomId !== roomId) continue; if (list.roomId !== roomId) continue;
await list.updateList(); await list.updateList();
updated = true;
} }
if (!updated) return;
const errors = await applyServerAcls(this.banLists, Object.keys(this.protectedRooms), this.client); const errors = await applyServerAcls(this.banLists, Object.keys(this.protectedRooms), this.client);
return this.printActionResult(errors); return this.printActionResult(errors);

View File

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixClient } from "matrix-bot-sdk";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { execStatusCommand } from "./StatusCommand"; import { execStatusCommand } from "./StatusCommand";
import { execBanCommand, execUnbanCommand } from "./UnbanBanCommand";
export const COMMAND_PREFIX = "!mjolnir"; export const COMMAND_PREFIX = "!mjolnir";
@ -26,6 +26,10 @@ export function handleCommand(roomId: string, event: any, mjolnir: Mjolnir) {
if (parts.length === 1) { if (parts.length === 1) {
return execStatusCommand(roomId, event, mjolnir); return execStatusCommand(roomId, event, mjolnir);
} else if (parts[1] === 'ban' && parts.length > 3) {
return execBanCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'unban' && parts.length > 3) {
return execUnbanCommand(roomId, event, mjolnir, parts);
} else { } else {
// TODO: Help menu // TODO: Help menu
} }

View File

@ -16,6 +16,7 @@ limitations under the License.
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
// !mjolnir
export async function execStatusCommand(roomId: string, event: any, mjolnir: Mjolnir) { export async function execStatusCommand(roomId: string, event: any, mjolnir: Mjolnir) {
let html = ""; let html = "";
let text = ""; let text = "";

View File

@ -0,0 +1,81 @@
/*
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 { RULE_ROOM, RULE_SERVER, RULE_USER, ruleTypeToStable } from "../models/BanList";
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>";
let rule = null;
if (entityType === "user") {
rule = RULE_USER;
} else if (entityType === "room") {
rule = RULE_ROOM;
} else if (entityType === "server") {
rule = RULE_SERVER;
}
if (!rule) {
return {entityType, ruleType: null, glob, reason};
}
rule = ruleTypeToStable(rule);
return {entityType, ruleType: rule, glob, reason};
}
// !mjolnir ban <user|server|room> <glob> [reason]
export async function execBanCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
const bits = parseBits(parts);
if (!bits.ruleType) {
const replyText = "Unknown entity type '" + bits.entityType + "' - try one of user, room, or server";
const reply = RichReply.createFor(roomId, event, replyText, replyText);
reply["msgtype"] = "m.notice";
return mjolnir.client.sendMessage(roomId, reply);
}
const recommendation = recommendationToStable(RECOMMENDATION_BAN);
const ruleContent = {
entity: bits.glob,
recommendation,
reason: bits.reason,
};
const stateKey = `rule:${bits.glob}`;
await mjolnir.client.sendStateEvent(mjolnir.publishedBanListRoomId, bits.ruleType, stateKey, ruleContent);
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
}
// !mjolnir unban <user|server|room> <glob>
export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
const bits = parseBits(parts);
if (!bits.ruleType) {
const replyText = "Unknown entity type '" + bits.entityType + "' - try one of user, room, or server";
const reply = RichReply.createFor(roomId, event, replyText, replyText);
reply["msgtype"] = "m.notice";
return mjolnir.client.sendMessage(roomId, reply);
}
const ruleContent = {}; // empty == clear/unban
const stateKey = `rule:${bits.glob}`;
await mjolnir.client.sendStateEvent(mjolnir.publishedBanListRoomId, bits.ruleType, stateKey, ruleContent);
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
}

View File

@ -22,6 +22,7 @@ interface IConfig {
dataPath: string; dataPath: string;
autojoin: boolean; autojoin: boolean;
managementRoom: string; managementRoom: string;
publishedBanListRoom: string;
protectedRooms: string[]; // matrix.to urls protectedRooms: string[]; // matrix.to urls
banLists: string[]; // matrix.to urls banLists: string[]; // matrix.to urls
} }

View File

@ -24,9 +24,7 @@ import {
SimpleFsStorageProvider SimpleFsStorageProvider
} from "matrix-bot-sdk"; } from "matrix-bot-sdk";
import config from "./config"; import config from "./config";
import BanList, { ALL_RULE_TYPES } from "./models/BanList"; import BanList from "./models/BanList";
import { applyServerAcls } from "./actions/ApplyAcl";
import { RoomUpdateError } from "./models/RoomUpdateError";
import { Mjolnir } from "./Mjolnir"; import { Mjolnir } from "./Mjolnir";
LogService.setLogger(new RichConsoleLogger()); LogService.setLogger(new RichConsoleLogger());
@ -40,7 +38,7 @@ if (config.autojoin) {
(async function () { (async function () {
const banLists: BanList[] = []; const banLists: BanList[] = [];
const protectedRooms:{[roomId: string]: string} = {}; const protectedRooms: { [roomId: string]: string } = {};
// Ensure we're in all the rooms we expect to be in // Ensure we're in all the rooms we expect to be in
const joinedRooms = await client.getJoinedRooms(); const joinedRooms = await client.getJoinedRooms();
@ -71,11 +69,17 @@ if (config.autojoin) {
protectedRooms[roomId] = roomRef; 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 // Ensure we're also in the management room
const managementRoomId = await client.joinRoom(config.managementRoom); const managementRoomId = await client.joinRoom(config.managementRoom);
await client.sendNotice(managementRoomId, "Mjolnir is starting up. Use !mjolnir to query status."); await client.sendNotice(managementRoomId, "Mjolnir is starting up. Use !mjolnir to query status.");
const bot = new Mjolnir(client, managementRoomId, protectedRooms, banLists); const bot = new Mjolnir(client, managementRoomId, banListRoomId, protectedRooms, banLists);
await bot.start(); await bot.start();
// TODO: Check permissions for mjolnir in protected rooms // TODO: Check permissions for mjolnir in protected rooms

View File

@ -26,6 +26,13 @@ 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 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 ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES];
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;
if (SERVER_RULE_TYPES.includes(rule)) return unstable ? SERVER_RULE_TYPES[SERVER_RULE_TYPES.length - 1] : RULE_SERVER;
return null;
}
export default class BanList { export default class BanList {
private rules: ListRule[] = []; private rules: ListRule[] = [];

View File

@ -19,6 +19,11 @@ import { MatrixGlob } from "matrix-bot-sdk/lib/MatrixGlob";
export const RECOMMENDATION_BAN = "m.ban"; export const RECOMMENDATION_BAN = "m.ban";
export const RECOMMENDATION_BAN_TYPES = [RECOMMENDATION_BAN, "org.matrix.mjolnir.ban"]; export const RECOMMENDATION_BAN_TYPES = [RECOMMENDATION_BAN, "org.matrix.mjolnir.ban"];
export function recommendationToStable(recommendation: string, unstable = true): string {
if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN;
return null;
}
export class ListRule { export class ListRule {
private glob: MatrixGlob; private glob: MatrixGlob;