mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-06-04 14:51:21 +00:00
fffd8563e3
Add logging to tracing Improve logging Fix order of imports Add missing pieces of tracing code Add more logging Try nested spans Expand traces using an decorator Improve quality of spans Add missing tracing decorations Filter metrics and healthz Add more traces Fix return type error
143 lines
5.9 KiB
TypeScript
143 lines
5.9 KiB
TypeScript
/**
|
|
* 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";
|
|
import { trace, traceSync } from "../../utils";
|
|
|
|
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,
|
|
/**
|
|
* The user id of the client. Ignores reactions from this user
|
|
*/
|
|
private readonly clientUserId: string
|
|
) {
|
|
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.
|
|
*/
|
|
@trace('MatrixReactionHandler.handleEvent')
|
|
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;
|
|
}
|
|
if (event['sender'] === this.clientUserId) {
|
|
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.
|
|
*/
|
|
@traceSync('MatrixReactionHandler.start')
|
|
public start(emitter: MatrixEmitter): void {
|
|
emitter.on('room.event', this.listener);
|
|
}
|
|
|
|
/**
|
|
* Stop listening for reactions to events.
|
|
*/
|
|
@traceSync('MatrixReactionHandler.stop')
|
|
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.
|
|
*/
|
|
@traceSync('MatrixReactionHandler.createAnnotation')
|
|
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.
|
|
*/
|
|
@trace('MatrixReactionHandler.addReactionsToEvent')
|
|
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)));
|
|
}
|
|
}
|