mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-05-15 11:55:41 +00:00
431 lines
14 KiB
TypeScript
431 lines
14 KiB
TypeScript
// Copyright 2022 - 2023 Gnuxie <Gnuxie@protonmail.com>
|
|
// Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
|
//
|
|
// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0
|
|
//
|
|
// SPDX-FileAttributionText: <text>
|
|
// This modified file incorporates work from mjolnir
|
|
// https://github.com/matrix-org/mjolnir
|
|
// </text>
|
|
|
|
import {
|
|
ActionResult,
|
|
Client,
|
|
ClientPlatform,
|
|
ClientRooms,
|
|
EventReport,
|
|
LoggableConfigTracker,
|
|
Logger,
|
|
Membership,
|
|
MembershipEvent,
|
|
Ok,
|
|
PolicyRoomManager,
|
|
ProtectedRoomsSet,
|
|
RoomEvent,
|
|
RoomMembershipManager,
|
|
RoomMembershipRevisionIssuer,
|
|
RoomStateManager,
|
|
Task,
|
|
Value,
|
|
isError,
|
|
} from "matrix-protection-suite";
|
|
import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue";
|
|
import { ThrottlingQueue } from "./queues/ThrottlingQueue";
|
|
import ManagementRoomOutput from "./managementroom/ManagementRoomOutput";
|
|
import { ReportPoller } from "./report/ReportPoller";
|
|
import { StandardReportManager } from "./report/ReportManager";
|
|
import { MatrixReactionHandler } from "./commands/interface-manager/MatrixReactionHandler";
|
|
import {
|
|
MatrixSendClient,
|
|
SynapseAdminClient,
|
|
} from "matrix-protection-suite-for-matrix-bot-sdk";
|
|
import { IConfig } from "./config";
|
|
import { LogLevel } from "matrix-bot-sdk";
|
|
import {
|
|
ARGUMENT_PROMPT_LISTENER,
|
|
DEFAUILT_ARGUMENT_PROMPT_LISTENER,
|
|
makeListenerForArgumentPrompt as makeListenerForArgumentPrompt,
|
|
makeListenerForPromptDefault,
|
|
} from "./commands/interface-manager/MatrixPromptForAccept";
|
|
import { RendererMessageCollector } from "./capabilities/RendererMessageCollector";
|
|
import { DraupnirRendererMessageCollector } from "./capabilities/DraupnirRendererMessageCollector";
|
|
import { renderProtectionFailedToStart } from "./protections/ProtectedRoomsSetRenderers";
|
|
import { draupnirStatusInfo, renderStatusInfo } from "./commands/StatusCommand";
|
|
import {
|
|
StringRoomID,
|
|
StringUserID,
|
|
MatrixRoomID,
|
|
isStringRoomID,
|
|
isStringRoomAlias,
|
|
MatrixRoomReference,
|
|
userServerName,
|
|
} from "@the-draupnir-project/matrix-basic-types";
|
|
import {
|
|
MatrixAdaptorContext,
|
|
sendMatrixEventsFromDeadDocument,
|
|
} from "./commands/interface-manager/MPSMatrixInterfaceAdaptor";
|
|
import {
|
|
makeDraupnirCommandDispatcher,
|
|
makeDraupnirJSCommandDispatcher,
|
|
} from "./commands/DraupnirCommandDispatcher";
|
|
import { SafeModeToggle } from "./safemode/SafeModeToggle";
|
|
import { makeCommandDispatcherTimelineListener } from "./safemode/ManagementRoom";
|
|
import {
|
|
BasicInvocationInformation,
|
|
JSInterfaceCommandDispatcher,
|
|
Presentation,
|
|
StandardPresentationArgumentStream,
|
|
} from "@the-draupnir-project/interface-manager";
|
|
import { ManagementRoomDetail } from "./managementroom/ManagementRoomDetail";
|
|
import {
|
|
COMMAND_CONFIRMATION_LISTENER,
|
|
makeConfirmationPromptListener,
|
|
} from "./commands/interface-manager/MatrixPromptForConfirmation";
|
|
import { SynapseHttpAntispam } from "./webapis/SynapseHTTPAntispam/SynapseHttpAntispam";
|
|
const log = new Logger("Draupnir");
|
|
|
|
// webAPIS should not be included on the Draupnir class.
|
|
// That should be managed elsewhere.
|
|
// It's not actually relevant to the Draupnir instance and it only was connected
|
|
// to Draupnir because it needs to be started after Draupnir started and not before.
|
|
// And giving it to the class was a dumb easy way of doing that.
|
|
|
|
export class Draupnir implements Client, MatrixAdaptorContext {
|
|
/**
|
|
* This is for users who are not listed on a watchlist,
|
|
* but have been flagged by the automatic spam detection as suispicous
|
|
*/
|
|
public unlistedUserRedactionQueue = new UnlistedUserRedactionQueue();
|
|
|
|
private readonly commandDispatcher = makeDraupnirCommandDispatcher(this);
|
|
public taskQueue: ThrottlingQueue;
|
|
/**
|
|
* Reporting back to the management room.
|
|
*/
|
|
public readonly managementRoomOutput: ManagementRoomOutput;
|
|
/*
|
|
* Config-enabled polling of reports in Synapse, so Draupnir can react to reports
|
|
*/
|
|
private reportPoller?: ReportPoller;
|
|
/**
|
|
* Handle user reports from the homeserver.
|
|
* FIXME: ReportManager should be a protection.
|
|
*/
|
|
public readonly reportManager: StandardReportManager;
|
|
|
|
public readonly reactionHandler: MatrixReactionHandler;
|
|
|
|
private readonly timelineEventListener = this.handleTimelineEvent.bind(this);
|
|
|
|
public readonly capabilityMessageRenderer: RendererMessageCollector;
|
|
|
|
private readonly commandDispatcherTimelineListener =
|
|
makeCommandDispatcherTimelineListener(
|
|
this.managementRoom,
|
|
this.client,
|
|
this.commandDispatcher
|
|
);
|
|
|
|
private readonly JSInterfaceDispatcher: JSInterfaceCommandDispatcher<BasicInvocationInformation> =
|
|
makeDraupnirJSCommandDispatcher(this);
|
|
private constructor(
|
|
public readonly client: MatrixSendClient,
|
|
public readonly clientUserID: StringUserID,
|
|
public clientDisplayName: string,
|
|
public readonly clientPlatform: ClientPlatform,
|
|
public readonly managementRoomDetail: ManagementRoomDetail,
|
|
public readonly clientRooms: ClientRooms,
|
|
public readonly config: IConfig,
|
|
public readonly protectedRoomsSet: ProtectedRoomsSet,
|
|
public readonly roomStateManager: RoomStateManager,
|
|
public readonly policyRoomManager: PolicyRoomManager,
|
|
public readonly roomMembershipManager: RoomMembershipManager,
|
|
public readonly loggableConfigTracker: LoggableConfigTracker,
|
|
/** Draupnir has a feature where you can choose to accept invitations from a space and not just the management room. */
|
|
public readonly acceptInvitesFromRoom: MatrixRoomID,
|
|
public readonly acceptInvitesFromRoomIssuer: RoomMembershipRevisionIssuer,
|
|
public readonly safeModeToggle: SafeModeToggle,
|
|
public readonly synapseAdminClient: SynapseAdminClient | undefined,
|
|
public readonly synapseHTTPAntispam: SynapseHttpAntispam | undefined
|
|
) {
|
|
this.managementRoomOutput = new ManagementRoomOutput(
|
|
this.managementRoomDetail,
|
|
this.clientUserID,
|
|
this.client,
|
|
this.config
|
|
);
|
|
this.taskQueue = new ThrottlingQueue(
|
|
this.managementRoomOutput,
|
|
config.backgroundDelayMS
|
|
);
|
|
this.reactionHandler = new MatrixReactionHandler(
|
|
this.managementRoom.toRoomIDOrAlias(),
|
|
client,
|
|
clientUserID,
|
|
clientPlatform
|
|
);
|
|
this.reportManager = new StandardReportManager(this);
|
|
if (config.pollReports) {
|
|
this.reportPoller = new ReportPoller(this, this.reportManager);
|
|
}
|
|
this.reactionHandler.on(
|
|
ARGUMENT_PROMPT_LISTENER,
|
|
makeListenerForArgumentPrompt(
|
|
this.commandRoomID,
|
|
this.commandDispatcher,
|
|
this.reactionHandler
|
|
)
|
|
);
|
|
this.reactionHandler.on(
|
|
DEFAUILT_ARGUMENT_PROMPT_LISTENER,
|
|
makeListenerForPromptDefault(
|
|
this.commandRoomID,
|
|
this.commandDispatcher,
|
|
this.reactionHandler
|
|
)
|
|
);
|
|
this.reactionHandler.on(
|
|
COMMAND_CONFIRMATION_LISTENER,
|
|
makeConfirmationPromptListener(
|
|
this.commandRoomID,
|
|
this.commandDispatcher,
|
|
this.reactionHandler
|
|
)
|
|
);
|
|
this.capabilityMessageRenderer = new DraupnirRendererMessageCollector(
|
|
this.clientPlatform.toRoomMessageSender(),
|
|
this.managementRoomID
|
|
);
|
|
}
|
|
|
|
public static async makeDraupnirBot(
|
|
client: MatrixSendClient,
|
|
clientUserID: StringUserID,
|
|
clientDisplayName: string,
|
|
clientPlatform: ClientPlatform,
|
|
managementRoomDetail: ManagementRoomDetail,
|
|
clientRooms: ClientRooms,
|
|
protectedRoomsSet: ProtectedRoomsSet,
|
|
roomStateManager: RoomStateManager,
|
|
policyRoomManager: PolicyRoomManager,
|
|
roomMembershipManager: RoomMembershipManager,
|
|
config: IConfig,
|
|
loggableConfigTracker: LoggableConfigTracker,
|
|
safeModeToggle: SafeModeToggle,
|
|
synapseHTTPAntispam: SynapseHttpAntispam | undefined
|
|
): Promise<ActionResult<Draupnir>> {
|
|
const acceptInvitesFromRoom = await (async () => {
|
|
if (config.autojoinOnlyIfManager) {
|
|
return Ok(managementRoomDetail.managementRoom);
|
|
} else {
|
|
if (config.acceptInvitesFromSpace === undefined) {
|
|
throw new TypeError(
|
|
`You cannot leave config.acceptInvitesFromSpace undefined if you have disabled config.autojoinOnlyIfManager`
|
|
);
|
|
}
|
|
const room = (() => {
|
|
if (
|
|
isStringRoomID(config.acceptInvitesFromSpace) ||
|
|
isStringRoomAlias(config.acceptInvitesFromSpace)
|
|
) {
|
|
return config.acceptInvitesFromSpace;
|
|
} else {
|
|
const parseResult = MatrixRoomReference.fromPermalink(
|
|
config.acceptInvitesFromSpace
|
|
);
|
|
if (isError(parseResult)) {
|
|
throw new TypeError(
|
|
`config.acceptInvitesFromSpace: ${config.acceptInvitesFromSpace} needs to be a room id, alias or permalink`
|
|
);
|
|
}
|
|
return parseResult.ok;
|
|
}
|
|
})();
|
|
return await clientPlatform.toRoomJoiner().joinRoom(room);
|
|
}
|
|
})();
|
|
if (isError(acceptInvitesFromRoom)) {
|
|
return acceptInvitesFromRoom;
|
|
}
|
|
const acceptInvitesFromRoomIssuer =
|
|
await roomMembershipManager.getRoomMembershipRevisionIssuer(
|
|
acceptInvitesFromRoom.ok
|
|
);
|
|
if (isError(acceptInvitesFromRoomIssuer)) {
|
|
return acceptInvitesFromRoomIssuer;
|
|
}
|
|
const draupnir = new Draupnir(
|
|
client,
|
|
clientUserID,
|
|
clientDisplayName,
|
|
clientPlatform,
|
|
managementRoomDetail,
|
|
clientRooms,
|
|
config,
|
|
protectedRoomsSet,
|
|
roomStateManager,
|
|
policyRoomManager,
|
|
roomMembershipManager,
|
|
loggableConfigTracker,
|
|
acceptInvitesFromRoom.ok,
|
|
acceptInvitesFromRoomIssuer.ok,
|
|
safeModeToggle,
|
|
new SynapseAdminClient(client, clientUserID),
|
|
synapseHTTPAntispam
|
|
);
|
|
const loadResult = await protectedRoomsSet.protections.loadProtections(
|
|
protectedRoomsSet,
|
|
draupnir,
|
|
(error, protectionName, description) =>
|
|
renderProtectionFailedToStart(
|
|
clientPlatform.toRoomMessageSender(),
|
|
managementRoomDetail.managementRoomID,
|
|
error,
|
|
protectionName,
|
|
description
|
|
)
|
|
);
|
|
if (isError(loadResult)) {
|
|
return loadResult;
|
|
}
|
|
// we need to make sure that we are protecting the management room so we
|
|
// have immediate access to its membership (for accepting invitations).
|
|
const managementRoomProtectResult =
|
|
await draupnir.protectedRoomsSet.protectedRoomsManager.addRoom(
|
|
managementRoomDetail.managementRoom
|
|
);
|
|
if (isError(managementRoomProtectResult)) {
|
|
return managementRoomProtectResult;
|
|
}
|
|
return Ok(draupnir);
|
|
}
|
|
|
|
public get managementRoomID(): StringRoomID {
|
|
return this.managementRoomDetail.managementRoomID;
|
|
}
|
|
|
|
public get managementRoom(): MatrixRoomID {
|
|
return this.managementRoomDetail.managementRoom;
|
|
}
|
|
|
|
/**
|
|
* Note: This is only public due to having to first start the syncloop before sending events
|
|
* when we use encryption.
|
|
* This means this is only used in the index.ts.
|
|
*/
|
|
public async startupComplete(): Promise<void> {
|
|
try {
|
|
await this.managementRoomOutput.logMessage(
|
|
LogLevel.INFO,
|
|
"Draupnir@startup",
|
|
"Startup complete. Now monitoring rooms."
|
|
);
|
|
const statusInfo = draupnirStatusInfo(this);
|
|
await sendMatrixEventsFromDeadDocument(
|
|
this.clientPlatform.toRoomMessageSender(),
|
|
this.managementRoomID,
|
|
renderStatusInfo(statusInfo),
|
|
{}
|
|
);
|
|
} catch (ex) {
|
|
log.error(`Caught an error when trying to show status at startup`, ex);
|
|
}
|
|
}
|
|
|
|
public handleTimelineEvent(roomID: StringRoomID, event: RoomEvent): void {
|
|
if (
|
|
Value.Check(MembershipEvent, event) &&
|
|
event.state_key === this.clientUserID &&
|
|
// if the membership is join, make sure that we filter out protected rooms.
|
|
(event.content.membership === Membership.Join
|
|
? !this.protectedRoomsSet.isProtectedRoom(roomID)
|
|
: true)
|
|
) {
|
|
this.protectedRoomsSet.handleExternalMembership(roomID, event);
|
|
}
|
|
this.managementRoomMessageListener(roomID, event);
|
|
void Task(
|
|
(async () => {
|
|
await this.reactionHandler.handleEvent(roomID, event);
|
|
})()
|
|
);
|
|
if (this.protectedRoomsSet.isProtectedRoom(roomID)) {
|
|
this.protectedRoomsSet.handleTimelineEvent(roomID, event);
|
|
}
|
|
}
|
|
|
|
private managementRoomMessageListener(
|
|
roomID: StringRoomID,
|
|
event: RoomEvent
|
|
): void {
|
|
if (roomID !== this.managementRoomID) {
|
|
return;
|
|
}
|
|
this.commandDispatcherTimelineListener(roomID, event);
|
|
this.reportManager.handleTimelineEvent(roomID, event);
|
|
}
|
|
|
|
/**
|
|
* Start responding to events.
|
|
* This will not start the appservice from listening and responding
|
|
* to events. Nor will it start any syncing client.
|
|
*/
|
|
public start(): void {
|
|
// to avoid handlers getting out of sync on clientRooms and leaking
|
|
// when draupnir keeps being started and restarted, we can basically
|
|
// clear all listeners each time and add the factory listener back.
|
|
this.clientRooms.on("timeline", this.timelineEventListener);
|
|
if (this.reportPoller) {
|
|
// allow this to crash draupnir if it fails, since we need to know.
|
|
void this.reportPoller.startFromStoredSetting(
|
|
this.client,
|
|
this.managementRoomOutput
|
|
);
|
|
}
|
|
}
|
|
|
|
public stop(): void {
|
|
this.clientRooms.off("timeline", this.timelineEventListener);
|
|
this.reportPoller?.stop();
|
|
this.protectedRoomsSet.unregisterListeners();
|
|
}
|
|
|
|
public createRoomReference(roomID: StringRoomID): MatrixRoomID {
|
|
return new MatrixRoomID(roomID, [userServerName(this.clientUserID)]);
|
|
}
|
|
public handleEventReport(report: EventReport): void {
|
|
this.protectedRoomsSet.handleEventReport(report);
|
|
}
|
|
|
|
/**
|
|
* This is needed to implement the MatrixInterfaceAdaptor interface.
|
|
*/
|
|
public get commandRoomID() {
|
|
return this.managementRoomID;
|
|
}
|
|
|
|
/**
|
|
* API for integration tests to be able to test commands, mostly to ensure
|
|
* functionality of the appservice bots.
|
|
*/
|
|
public async sendPresentationCommand<CommandReturn>(
|
|
sender: StringUserID,
|
|
...items: Presentation[]
|
|
): Promise<ActionResult<CommandReturn>> {
|
|
return await this.JSInterfaceDispatcher.invokeCommandFromPresentationStream(
|
|
{ commandSender: sender },
|
|
new StandardPresentationArgumentStream(items)
|
|
);
|
|
}
|
|
|
|
public async sendTextCommand<CommandReturn>(
|
|
sender: StringUserID,
|
|
command: string
|
|
): Promise<ActionResult<CommandReturn>> {
|
|
return await this.JSInterfaceDispatcher.invokeCommandFromBody(
|
|
{ commandSender: sender },
|
|
command
|
|
);
|
|
}
|
|
}
|