Add MatrixReactionHandler that allows us to create advanced prompts.

These prompts don't have a timeout and don't need a backing store,
so this is ideal for the ban propagation protection.
This commit is contained in:
gnuxie authored and Gnuxie committed 2023-03-24 18:57:25 +00:00
1 parent d390ce8772
commit f565b010a4
3 files changed
+137 -1

No files matched your search

+6
View File
@@ -49,6 +49,7 @@ import { RoomMemberManager } from "./RoomMembers";
import ProtectedRoomsConfig from "./ProtectedRoomsConfig";
import { MatrixEmitter, MatrixSendClient } from "./MatrixEmitter";
import { findCommandTable } from "./commands/interface-manager/InterfaceCommand";
import { MatrixReactionHandler } from "./commands/interface-manager/MatrixReactionHandler";
export const STATE_NOT_STARTED = "not_started";
export const STATE_CHECKING_PERMISSIONS = "checking_permissions";
@@ -96,6 +97,8 @@ export class Mjolnir {
private readonly commandTable = findCommandTable("mjolnir");
public readonly policyListManager: PolicyListManager;
public readonly reactionHandler: MatrixReactionHandler;
/**
* Adds a listener to the client that will automatically accept invitations.
* @param {MatrixSendClient} client
@@ -180,6 +183,7 @@ export class Mjolnir {
) {
this.protectedRoomsConfig = new ProtectedRoomsConfig(client);
this.policyListManager = new PolicyListManager(this);
this.reactionHandler = new MatrixReactionHandler(this.managementRoomId, client);
const mutedModules = (LogService as any).mutedModules;
if (!Array.isArray(mutedModules)) {
@@ -314,6 +318,7 @@ export class Mjolnir {
await this.policyListManager.start();
await this.resyncJoinedRooms(false);
await this.protectionManager.start();
this.reactionHandler.start(this.matrixEmitter);
if (this.config.verifyPermissionsOnStartup) {
await this.managementRoomOutput.logMessage(LogLevel.INFO, "Mjolnir@startup", "Checking permissions...");
@@ -349,6 +354,7 @@ export class Mjolnir {
public stop() {
LogService.info("Mjolnir", "Stopping Mjolnir...");
this.matrixEmitter.stop();
this.reactionHandler.stop(this.matrixEmitter);
this.webapis.stop();
this.reportPoller?.stop();
}
@@ -79,7 +79,7 @@ export async function renderMatrix(node: DocumentNode, cb: SendMatrixEventCB): P
* @param event An event to reply to, if any.
* @param client A MatrixClient to send the events with.
*/
export async function renderMatrixAndSend(node: DocumentNode, roomId: string, event: any|undefined, client: MatrixSendClient): Promise<string[]> {
export async function renderMatrixAndSend(node: DocumentNode, roomId: string, event: any|undefined, client: MatrixSendClient, additionalContent = {}): Promise<string[]> {
const baseContent = (text: string, html: string) => {
return {
msgtype: "m.notice",
@@ -91,6 +91,7 @@ export async function renderMatrixAndSend(node: DocumentNode, roomId: string, ev
const renderInitialReply = async (text: string, html: string) => {
return await client.sendMessage(roomId, {
...baseContent(text, html),
...additionalContent,
...event === undefined
? {} // if they don't supply a reply just send a top level event.
: { "m.relates_to": {
@@ -0,0 +1,129 @@
/**
* Copyright (C) 2023 Gnuxie <Gnuxie@protonmail.com>
* All rights reserved.
*/
import { EventEmitter } from "stream";
import { MatrixEmitter, MatrixSendClient } from "../../MatrixEmitter";
import { LogService } from "matrix-bot-sdk";
const REACTION_ANNOTATION_KEY = 'ge.applied-langua.ge.draupnir.reaction_handler';
type ItemByReactionKey = Map<string/*reaction key*/, any/*serialized presentation*/>;
export type ReactionListener = (key: string, item: any, additionalContext: unknown, reactionMap: ItemByReactionKey) => void;
/**
* A utility that can be associated with an `MatrixEmitter` to listen for
* reactions to Matrix Events. The aim is to simplify reaction UX.
*/
export class MatrixReactionHandler extends EventEmitter {
private listener: MatrixReactionHandler['handleEvent'];
public constructor(
/**
* The room the handler is for. Cannot be enabled for every room as the
* OG event lookup is very slow.
*/
public readonly roomId: string,
/**
* A client to lookup the related events to reactions.
*/
private readonly client: MatrixSendClient
) {
super();
this.listener = this.handleEvent.bind(this);
}
/**
* Handle an event from a `MatrixEmitter` and see if it is a reaction to
* a previously annotated event. If it is a reaction to an annotated event,
* then call its associated listener.
* @param roomId The room the event took place in.
* @param event The Matrix event.
*/
private async handleEvent(roomId: string, event: any): Promise<void> {
if (roomId !== this.roomId) {
return;
}
const relatesTo = event['content']?.['m.relates_to'];
if (relatesTo === undefined) {
return;
}
if (relatesTo['rel_type'] !== 'm.annotation') {
return;
}
const reactionKey = relatesTo['key'];
const relatedEventId = relatesTo['event_id'];
if (!(typeof relatedEventId === 'string' && typeof reactionKey === 'string')) {
return;
}
const annotatedEvent = await this.client.getEvent(roomId, relatedEventId);
const annotation = annotatedEvent.content[REACTION_ANNOTATION_KEY];
if (annotation === undefined) {
return;
}
const reactionMap = annotation['reaction_map'];
if (typeof reactionMap !== 'object' || reactionMap === null) {
LogService.warn('MatrixReactionHandler', `Missing reaction_map for the annotated event ${relatedEventId} in ${roomId}`);
return;
}
const listenerName = annotation['name'];
if (typeof listenerName !== 'string') {
LogService.warn('MatrixReactionHandler', `The event ${relatedEventId} in ${roomId} is missing the name of the annotation`);
return;
}
const association = reactionMap[reactionKey];
if (association === undefined) {
LogService.info('MatrixReactionHandler', `There wasn't a defined key for ${reactionKey} on event ${relatedEventId} in ${roomId}`);
return;
}
this.emit(listenerName, reactionKey, association, annotation['additional_context'], new Map(Object.entries(reactionMap)));
}
/**
* Start listening for reactions to events.
* Called normally by an associated mjolnir instance when it is started.
*/
public start(emitter: MatrixEmitter): void {
emitter.on('room.event', this.listener);
}
/**
* Stop listening for reactions to events.
*/
public stop(emitter: MatrixEmitter): void {
emitter.off('room.event', this.listener);
}
/**
* Create the annotation required to setup a listener for when a reaction is encountered for the list.
* @param listenerName The name of the event to emit when a reaction is encountered for a matrix event that matches a key in the `reactionMap`.
* @param reactionMap A map of reaction keys to items that will be provided to the listener.
* @param additionalContext Any additional context that should be associated with a matrix event for the listener.
* @returns An object that should be deep copied into a the content of a new Matrix event.
*/
public createAnnotation(listenerName: string, reactionMap: ItemByReactionKey, additionalContext: any = undefined): any {
return {
[REACTION_ANNOTATION_KEY]: {
name: listenerName,
reaction_map: Object.fromEntries(reactionMap),
additional_context: additionalContext,
}
}
}
/**
* Use a reaction map to create the initial reactions to an event so that the user has access to quick reactions.
* @param client A client to add the reactions with.
* @param roomId The room id of the event to add the reactions to.
* @param eventId The event id of the event to add reactions to.
* @param reactionMap The reaction map.
*/
public async addReactionsToEvent(client: MatrixSendClient, roomId: string, eventId: string, reactionMap: ItemByReactionKey): Promise<void> {
await [...reactionMap.keys()]
.reduce((acc, key) => acc.then(_ => client.unstableApis.addReactionToEvent(roomId, eventId, key)),
Promise.resolve()
).catch(e => (LogService.error('MatrixReactionHandler', `Could not add reaction to event ${eventId}`, e), Promise.reject(e)));
}
}