Start cleaning up Mjolnir into Draupnir, starting with listeners.

This commit is contained in:
gnuxie committed 2024-04-06 20:03:32 +01:00
1 parent 2929266c37
commit 67adb530bc
2 files changed
+177 -27

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
/**
* Copyright (C) 2022-2023 Gnuxie <Gnuxie@protonmail.com>
* All rights reserved.
*
* This file is modified and is NOT licensed under the Apache License.
* This modified file incorperates work from mjolnir
* https://github.com/matrix-org/mjolnir
* which included the following license notice:
Copyright 2019-2021 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.
*
* However, this file is modified and the modifications in this file
* are NOT distributed, contributed, committed, or licensed under the Apache License.
*/
import { Logger, Ok, ProtectedRoomsSet, Task, TextMessageContent, Value } from "matrix-protection-suite";
import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue";
import { findCommandTable } from "./commands/interface-manager/InterfaceCommand";
import { WebAPIs } from "./webapis/WebAPIs";
import { ThrottlingQueue } from "./queues/ThrottlingQueue";
import ManagementRoomOutput from "./ManagementRoomOutput";
import { ReportPoller } from "./report/ReportPoller";
import { ProtectionManager } from "./protections/ProtectionManager";
import { ReportManager } from "./report/ReportManager";
import { MatrixReactionHandler } from "./commands/interface-manager/MatrixReactionHandler";
import { MatrixSendClient, SafeMatrixEmitter } from "matrix-protection-suite-for-matrix-bot-sdk";
import { IConfig } from "./config";
import { COMMAND_PREFIX, extractCommandFromMessageBody, handleCommand } from "./commands/CommandHandler";
const log = new Logger('Draupnir');
export class Draupnir {
private displayName: string;
private localpart: string;
/**
* This is for users who are not listed on a watchlist,
* but have been flagged by the automatic spam detection as suispicous
*/
private unlistedUserRedactionQueue = new UnlistedUserRedactionQueue();
private webapis: WebAPIs;
private readonly commandTable = findCommandTable("mjolnir");
public taskQueue: ThrottlingQueue;
/**
* Reporting back to the management room.
*/
public readonly managementRoomOutput: ManagementRoomOutput;
/*
* Config-enabled polling of reports in Synapse, so Mjolnir can react to reports
*/
private reportPoller?: ReportPoller;
/**
* Store the protections being used by Mjolnir.
*/
public readonly legacyProtectionManager: ProtectionManager;
/**
* Handle user reports from the homeserver.
*/
public readonly reportManager: ReportManager;
public readonly reactionHandler: MatrixReactionHandler;
private constructor(
public readonly client: MatrixSendClient,
private readonly clientUserId: string,
public readonly matrixEmitter: SafeMatrixEmitter,
public readonly managementRoomId: string,
public readonly config: IConfig,
public readonly protectedRoomsSet: ProtectedRoomsSet
) {
this.reactionHandler = new MatrixReactionHandler(this.managementRoomId, client, clientUserId);
this.setupMatrixEmitterListeners();
}
private setupMatrixEmitterListeners(): void {
this.matrixEmitter.on("room.message", (roomID, event) => {
if (roomID !== this.managementRoomId) {
return;
}
if (Value.Check(TextMessageContent, event.content)) {
const commandBeingRun = extractCommandFromMessageBody(
event.content.body,
{
prefix: COMMAND_PREFIX,
localpart: this.localpart,
displayName: this.displayName,
userId: this.clientUserId,
additionalPrefixes: this.config.commands.additionalPrefixes,
allowNoPrefix: this.config.commands.allowNoPrefix,
}
);
if (commandBeingRun === undefined) {
return;
}
log.info(`Command being run by ${event.sender}: ${commandBeingRun}`);
Task(this.client.sendReadReceipt(roomID, event.event_id).then((_) => Ok(undefined)))
Task(handleCommand(roomID, event, commandBeingRun, this, this.commandTable).then((_) => Ok(undefined)));
}
});
}
}
+65 -27
View File
@@ -73,75 +73,113 @@ import "./Rules";
import "./WatchUnwatchCommand";
import "./Help";
import "./SetDisplayNameCommand";
import { RoomMessage, StringRoomID } from "matrix-protection-suite";
export const COMMAND_PREFIX = "!mjolnir";
export async function handleCommand(roomId: string, event: { content: { body: string } }, mjolnir: Mjolnir, commandTable: CommandTable<BaseFunction>) {
const cmd = event['content']['body'];
const parts = cmd.trim().split(' ').filter(p => p.trim().length > 0);
export async function handleCommand(
roomID: StringRoomID,
event: RoomMessage,
normalisedCommand: string,
mjolnir: Mjolnir,
commandTable: CommandTable<BaseFunction>
) {
const parts = normalisedCommand.trim().split(' ').filter(p => p.trim().length > 0);
// A shell-style parser that can parse `"a b c"` (with quotes) as a single argument.
// We do **not** want to parse `#` as a comment start, though.
const tokens = tokenize(cmd.replace("#", "\\#")).slice(/* get rid of ["!mjolnir", command] */ 2);
const tokens = tokenize(normalisedCommand.replace("#", "\\#")).slice(/* get rid of ["!mjolnir", command] */ 2);
try {
if (parts[1] === 'joins') {
return await showJoinsStatus(roomId, event, mjolnir, parts.slice(/* ["joins"] */ 2));
return await showJoinsStatus(roomID, event, mjolnir, parts.slice(/* ["joins"] */ 2));
} else if (parts[1] === 'sync') {
return await execSyncCommand(roomId, event, mjolnir);
return await execSyncCommand(roomID, event, mjolnir);
} else if (parts[1] === 'verify') {
return await execPermissionCheckCommand(roomId, event, mjolnir);
return await execPermissionCheckCommand(roomID, event, mjolnir);
} else if (parts.length >= 5 && parts[1] === 'list' && parts[2] === 'create') {
return await execCreateListCommand(roomId, event, mjolnir, parts);
return await execCreateListCommand(roomID, event, mjolnir, parts);
} else if (parts[1] === 'redact' && parts.length > 1) {
return await execRedactCommand(roomId, event, mjolnir, parts);
return await execRedactCommand(roomID, event, mjolnir, parts);
} else if (parts[1] === 'import' && parts.length > 2) {
return await execImportCommand(roomId, event, mjolnir, parts);
return await execImportCommand(roomID, event, mjolnir, parts);
} else if (parts[1] === 'default' && parts.length > 2) {
return await execSetDefaultListCommand(roomId, event, mjolnir, parts);
return await execSetDefaultListCommand(roomID, event, mjolnir, parts);
} else if (parts[1] === 'protections') {
return await execListProtections(roomId, event, mjolnir, parts);
return await execListProtections(roomID, event, mjolnir, parts);
} else if (parts[1] === 'enable' && parts.length > 1) {
return await execEnableProtection(roomId, event, mjolnir, parts);
return await execEnableProtection(roomID, event, mjolnir, parts);
} else if (parts[1] === 'disable' && parts.length > 1) {
return await execDisableProtection(roomId, event, mjolnir, parts);
return await execDisableProtection(roomID, event, mjolnir, parts);
} else if (parts[1] === 'config' && parts[2] === 'set' && parts.length > 3) {
return await execConfigSetProtection(roomId, event, mjolnir, parts.slice(3))
return await execConfigSetProtection(roomID, event, mjolnir, parts.slice(3))
} else if (parts[1] === 'config' && parts[2] === 'add' && parts.length > 3) {
return await execConfigAddProtection(roomId, event, mjolnir, parts.slice(3))
return await execConfigAddProtection(roomID, event, mjolnir, parts.slice(3))
} else if (parts[1] === 'config' && parts[2] === 'remove' && parts.length > 3) {
return await execConfigRemoveProtection(roomId, event, mjolnir, parts.slice(3))
return await execConfigRemoveProtection(roomID, event, mjolnir, parts.slice(3))
} else if (parts[1] === 'config' && parts[2] === 'get') {
return await execConfigGetProtection(roomId, event, mjolnir, parts.slice(3))
return await execConfigGetProtection(roomID, event, mjolnir, parts.slice(3))
} else if (parts[1] === 'resolve' && parts.length > 2) {
return await execResolveCommand(roomId, event, mjolnir, parts);
return await execResolveCommand(roomID, event, mjolnir, parts);
} else if (parts[1] === 'powerlevel' && parts.length > 3) {
return await execSetPowerLevelCommand(roomId, event, mjolnir, parts);
return await execSetPowerLevelCommand(roomID, event, mjolnir, parts);
} else if (parts[1] === 'since') {
return await execSinceCommand(roomId, event, mjolnir, tokens);
return await execSinceCommand(roomID, event, mjolnir, tokens);
} else if (parts[1] === 'kick' && parts.length > 2) {
return await execKickCommand(roomId, event, mjolnir, parts);
return await execKickCommand(roomID, event, mjolnir, parts);
} else {
const readItems = readCommand(cmd).slice(1); // remove "!mjolnir"
const readItems = readCommand(normalisedCommand).slice(1); // remove "!mjolnir"
const stream = new ArgumentStream(readItems);
const command = commandTable.findAMatchingCommand(stream)
?? findTableCommand("mjolnir", "help");
const adaptor = findMatrixInterfaceAdaptor(command);
const mjolnirContext: MjolnirContext = {
mjolnir, roomId, event, client: mjolnir.client, emitter: mjolnir.matrixEmitter,
mjolnir, roomId: roomID, event, client: mjolnir.client, emitter: mjolnir.matrixEmitter,
};
try {
return await adaptor.invoke(mjolnirContext, mjolnirContext, ...stream.rest());
} catch (e) {
const commandError = new CommandException(CommandExceptionKind.Unknown, e, 'Unknown Unexpected Error');
await tickCrossRenderer.call(mjolnirContext, mjolnir.client, roomId, event, CommandResult.Err(commandError));
await tickCrossRenderer.call(mjolnirContext, mjolnir.client, roomID, event, CommandResult.Err(commandError));
}
}
} catch (e) {
LogService.error("CommandHandler", e);
const text = "There was an error processing your command - see console/log for details";
const reply = RichReply.createFor(roomId, event, text, text);
const reply = RichReply.createFor(roomID, event, text, text);
reply["msgtype"] = "m.notice";
return await mjolnir.client.sendMessage(roomId, reply);
return await mjolnir.client.sendMessage(roomID, reply);
}
}
export function extractCommandFromMessageBody(
body: string,
{ prefix,
localpart,
displayName,
userId,
additionalPrefixes,
allowNoPrefix
}: {
prefix: string,
localpart: string,
displayName: string,
userId: string,
additionalPrefixes: string[],
allowNoPrefix: boolean
}): string | undefined {
const plainPrefixes = [prefix, localpart, displayName, userId, ...additionalPrefixes];
const allPossiblePrefixes = [
...plainPrefixes.map(p => `!${p}`),
...plainPrefixes.map(p => `${p}:`),
...plainPrefixes,
...allowNoPrefix ? ['!'] : [],
];
const usedPrefixInMessage = allPossiblePrefixes.find(p => body.toLowerCase().startsWith(p.toLowerCase()));
if (usedPrefixInMessage === undefined) {
return;
}
// normalise the event body to make the prefix uniform (in case the bot has spaces in its display name)
const restOfBody = body.substring(usedPrefixInMessage.length);
return prefix + restOfBody.startsWith(' ') ? restOfBody : ` ${restOfBody}`;
}