mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-10-23 09:22:24 +00:00
Start cleaning up Mjolnir into Draupnir, starting with listeners.
This commit is contained in:
1 parent
2929266c37
commit
67adb530bc
2 files changed
+177
-27
No files matched your search
+112
@@ -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)));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
Reference in new issue
Block a user