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:
* [ ] Ban users
* [ ] ACL servers
* [ ] Update lists with new bans/ACLs
* [x] ACL servers
* [x] Update lists with new bans/ACLs
* [ ] "Ban on sight" mode (rather than proactive)
Phase 2:

View File

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

View File

@ -23,7 +23,8 @@ import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler";
export class Mjolnir {
constructor(
public readonly client: MatrixClient,
private managementRoomId: string,
public readonly managementRoomId: string,
public readonly publishedBanListRoomId: string,
public readonly protectedRooms: { [roomId: string]: string },
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 (ALL_RULE_TYPES.includes(event['type'])) {
let updated = false;
for (const list of this.banLists) {
if (list.roomId !== roomId) continue;
await list.updateList();
updated = true;
}
if (!updated) return;
const errors = await applyServerAcls(this.banLists, Object.keys(this.protectedRooms), this.client);
return this.printActionResult(errors);

View File

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

View File

@ -16,6 +16,7 @@ limitations under the License.
import { Mjolnir } from "../Mjolnir";
// !mjolnir
export async function execStatusCommand(roomId: string, event: any, mjolnir: Mjolnir) {
let html = "";
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;
autojoin: boolean;
managementRoom: string;
publishedBanListRoom: string;
protectedRooms: string[]; // matrix.to urls
banLists: string[]; // matrix.to urls
}

View File

@ -24,9 +24,7 @@ import {
SimpleFsStorageProvider
} from "matrix-bot-sdk";
import config from "./config";
import BanList, { ALL_RULE_TYPES } from "./models/BanList";
import { applyServerAcls } from "./actions/ApplyAcl";
import { RoomUpdateError } from "./models/RoomUpdateError";
import BanList from "./models/BanList";
import { Mjolnir } from "./Mjolnir";
LogService.setLogger(new RichConsoleLogger());
@ -40,7 +38,7 @@ if (config.autojoin) {
(async function () {
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
const joinedRooms = await client.getJoinedRooms();
@ -71,11 +69,17 @@ if (config.autojoin) {
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, protectedRooms, banLists);
const bot = new Mjolnir(client, managementRoomId, banListRoomId, protectedRooms, banLists);
await bot.start();
// 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 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 {
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_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 {
private glob: MatrixGlob;