UNPKG

@vector-im/matrix-bot-sdk

Version:

TypeScript/JavaScript SDK for Matrix bots and appservices

160 lines (140 loc) 5.93 kB
import { MatrixClient } from "../MatrixClient"; import { IPreprocessor } from "./IPreprocessor"; import { EventKind, extractRequestError, LogService } from ".."; /** * Metadata for a rich reply. Usually stored under the "mx_richreply" * field of an event (at the top level). * @category Preprocessors * @see RichRepliesPreprocessor */ export interface IRichReplyMetadata { /** * If true, the preprocessor found some inconsistencies in the reply * information that does not match the specification. For example, * this may indicate that a reply was sent without an HTML component. */ wasLenient: boolean; /** * The event ID the event references. May be an empty string if * wasLenient is true. */ parentEventId: string; /** * The fallback plain text the preprocessor found. May be an empty * string if wasLenient is true. The prefix characters to indicate * this is a fallback will have already been removed. */ fallbackPlainBody: string; /** * The fallback HTML the processor found. May be an empty string if * wasLenient is true. The fallback structure will have already been * removed, leaving just the original assumed HTML. */ fallbackHtmlBody: string; /** * The user ID that sent the parent event, as determined by the fallback * text. This should not be relied upon for anything serious, and instead * the preprocessor should be configured to fetch the real event to * populate the realEvent property. May be an empty string if wasLenient * is true. */ fallbackSender: string; /** * If the preprocessor is configured to fetch event content, this field * will contain the event as reported by the homeserver. May be null if * wasLenient is true. */ realEvent: any; } /** * Processes rich replies found in events. This automatically strips * the fallback representation from events, providing the information * as a top level "mx_richreply" key. The "mx_richreply" property may * be cast to the type IRichReplyMetadata. * @category Preprocessors */ export class RichRepliesPreprocessor implements IPreprocessor { /** * Creates a new rich replies preprocessor. * @param fetchRealEventContents If enabled, this preprocessor will * attempt to get the real event contents and append them to the event * information. */ public constructor(private fetchRealEventContents = false) { } public getSupportedEventTypes(): string[] { return ["m.room.message"]; } public async processEvent(event: any, client: MatrixClient, kind?: EventKind): Promise<any> { if (kind && kind !== EventKind.RoomEvent) return; if (!event["content"]) return; if (!event["content"]["m.relates_to"]) return; if (!event["content"]["m.relates_to"]["m.in_reply_to"]) return; const parentEventId = event["content"]["m.relates_to"]["m.in_reply_to"]["event_id"]; if (!parentEventId) return; let fallbackHtml = ""; let fallbackText = ""; let fallbackSender = ""; let realHtml = event["content"]["formatted_body"]; let realText = event["content"]["body"]; let lenient = false; if (event["content"]["format"] !== "org.matrix.custom.html" || !event["content"]["formatted_body"]) { lenient = true; // Not safe to parse: probably not HTML } else { const formattedBody = event["content"]["formatted_body"]; if (!formattedBody.startsWith("<mx-reply>") || formattedBody.indexOf("</mx-reply>") === -1) { lenient = true; // Doesn't look like a reply } else { const parts = formattedBody.split("</mx-reply>"); const fbHtml = parts[0]; realHtml = parts[1]; const results = fbHtml.match(/<br[ ]*[/]{0,2}>(.*)<\/blockquote>\s*$/i); if (!results) { lenient = true; } else { fallbackHtml = results[1]; } } } let processedFallback = false; const body = event["content"]["body"] || ""; for (const line of body.split("\n")) { if (line.startsWith("> ") && !processedFallback) { fallbackText += line.substring(2) + "\n"; } else if (!processedFallback) { realText = ""; processedFallback = true; } else { realText += line + "\n"; } } const firstFallbackLine = fallbackText.split("\n")[0]; const matches = firstFallbackLine.match(/<(@.*:.*)>/); if (!matches) { lenient = true; } else { fallbackSender = matches[1]; } const metadata: IRichReplyMetadata = { wasLenient: lenient, fallbackHtmlBody: fallbackHtml ? fallbackHtml.trim() : "", fallbackPlainBody: fallbackText ? fallbackText.trim() : "", fallbackSender: fallbackSender ? fallbackSender.trim() : "", parentEventId: parentEventId ? parentEventId.trim() : "", realEvent: null, }; if (this.fetchRealEventContents) { try { metadata.realEvent = await client.getEvent(event["room_id"], parentEventId); } catch (e) { LogService.error("RichRepliesPreprocessor", "Failed to fetch real event:"); LogService.error("RichRepliesPreprocessor", extractRequestError(e)); metadata.wasLenient = true; // failed to fetch event } } event["mx_richreply"] = metadata; event["content"]["body"] = realText.trim(); if (realHtml) event["content"]["formatted_body"] = realHtml.trim(); return event; } }