From 73fcb06037055bca0bcca5085e66a0f3b0135479 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 13 Jan 2023 15:26:20 -0700 Subject: [PATCH 1/5] Implement new event parsing interface from events-sdk --- src/models/event.ts | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/models/event.ts b/src/models/event.ts index e4ac9691666..ea8d0857186 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -19,7 +19,7 @@ limitations under the License. * the public classes. */ -import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk"; +import { EventParser, Optional, RoomEvent } from "matrix-events-sdk"; import type { IEventDecryptionResult } from "../@types/crypto"; import { logger } from "../logger"; @@ -212,6 +212,12 @@ export type MatrixEventHandlerMap = { } & Pick; export class MatrixEvent extends TypedEventEmitter { + /** + * The default shared event parser for Extensible Events. To register custom event types or content blocks + * with the parser, do so here before constructing a new MatrixEvent object (implicitly or explicitly). + */ + public static readonly defaultExtensibleEventParser = new EventParser(); + private pushActions: IActionsObject | null = null; private _replacingEvent: MatrixEvent | null = null; private _localRedactionEvent: MatrixEvent | null = null; @@ -228,7 +234,7 @@ export class MatrixEvent extends TypedEventEmitter = undefined; + private _cachedExtEv: Optional = undefined; /* curve25519 key which we believe belongs to the sender of the event. See * getSenderKey() @@ -334,8 +340,15 @@ export class MatrixEvent extends TypedEventEmitter directly unless you absolutely have to. Prefer the getter * methods defined on this class. Using the getter methods shields your app * from changes to event JSON between Matrix versions. - */ - public constructor(public event: Partial = {}) { + * @param extensibleEventParser - The event parser to use when attempting to + * handle this event as an extensible event. This field is experimental + * and unstable - it may be removed without warning at any time. Defaults + * to the static parser from MatrixEvent. + */ + public constructor( + public event: Partial = {}, + private extensibleEventParser: EventParser = MatrixEvent.defaultExtensibleEventParser, + ) { super(); // intern the values of matrix events to force share strings and reduce the @@ -371,9 +384,15 @@ export class MatrixEvent extends TypedEventEmitter { + public get unstableExtensibleEvent(): Optional { if (!this._hasCachedExtEv) { - this._cachedExtEv = ExtensibleEvents.parse(this.getEffectiveEvent()); + const effectiveEvent = this.getEffectiveEvent(); + this._cachedExtEv = this.extensibleEventParser.parse({ + ...effectiveEvent, + // XXX: This case should never happen, but MatrixEvent in the js-sdk + // covers room-less events, unlike the events-sdk. + room_id: effectiveEvent.room_id ?? "!unknown:localhost", + }); } return this._cachedExtEv; } From 9ef619430a562df01e549eeda2c4afb2452adee8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 13 Jan 2023 15:26:45 -0700 Subject: [PATCH 2/5] Intercept message events in MSC1767 rooms --- src/client.ts | 61 +++++++++++++++++++++++++++++++++++++++++++--- src/models/room.ts | 12 +++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index 91cc21be0b0..69863fa93b5 100644 --- a/src/client.ts +++ b/src/client.ts @@ -18,7 +18,7 @@ limitations under the License. * This is an internal module. See {@link MatrixClient} for the public class. */ -import { Optional } from "matrix-events-sdk"; +import { EmoteEvent, MarkupBlock, Optional, WireMessageEvent, MessageEvent } from "matrix-events-sdk"; import type { IMegolmSessionData } from "./@types/crypto"; import { ISyncStateData, SyncApi, SyncApiOptions, SyncState } from "./sync"; @@ -4545,8 +4545,63 @@ export class MatrixClient extends TypedEventEmitter { + let newContent: IContent | undefined = undefined; + let newEventType: string | undefined = undefined; + + // Dev note: We exclude notices from this for now, at least while we figure out what to do with + // them at the spec level. + const textTypes: (string | undefined)[] = [MsgType.Text, MsgType.Emote]; + if (textTypes.includes(content.msgtype)) { + newContent = { + [MarkupBlock.type.name]: [{ body: content.body }], + } satisfies WireMessageEvent.ContentValue; + if (content.format === "org.matrix.custom.html" && content.formatted_body) { + newContent[MarkupBlock.type.name].push({ body: content.formatted_body, mimetype: "text/html" }); + } + newEventType = content.msgtype === MsgType.Emote ? EmoteEvent.type.name : MessageEvent.type.name; + + // Remove the fields we processed, so we don't clone them in a later stage. + delete content.body; + delete content.format; + delete content.formatted_body; + delete content.msgtype; + } + + // Deal with edits too: just a bit of recursion. + if (newContent && !!content["m.new_content"] && recurse) { + const editContent = makeContentExtensible(content["m.new_content"], false); + if (editContent) { + newContent["m.new_content"] = editContent; + } + } + + if (newContent) { + // copy over all other fields we don't know about (for safety) + for (const [k, v] of Object.entries(content)) { + if (!newContent.hasOwnProperty(k)) { + newContent[k] = v; + } + } + } + + return [newEventType, newContent]; + }; + + const [newEventType, newContent] = makeContentExtensible(sendContent); + if (newEventType !== undefined) eventType = newEventType; + if (newContent !== undefined) sendContent = newContent; + } return this.sendEvent(roomId, threadId as string | null, eventType, sendContent, txnId); } diff --git a/src/models/room.ts b/src/models/room.ts index a363ef0dfa3..c44ceead6de 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -547,6 +547,18 @@ export class Room extends ReadReceipt { return createEvent?.getContent()["creator"] ?? null; } + /** + * Unstable/experimental function to determine if the room requires MSC1767-style + * events or not. + * + * @deprecated Use stable functions where possible. This function may be removed or + * changed without notice. + */ + public unstableRequiresExtensibleEvents(): boolean { + // TODO(TR): We need a better check for this that doesn't involve hardcoding supported room versions + return this.getVersion().startsWith("org.matrix.msc1767."); + } + /** * Gets the version of the room * @returns The version of the room, or null if it could not be determined From 145ba1a0e1f6437a8d1e0c3fb0b889bfa9174b10 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 13 Jan 2023 16:21:02 -0700 Subject: [PATCH 3/5] Upgrade matrix-events-sdk --- package.json | 2 +- yarn.lock | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 89387fe75da..5318d5605f7 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "bs58": "^5.0.0", "content-type": "^1.0.4", "loglevel": "^1.7.1", - "matrix-events-sdk": "0.0.1", + "matrix-events-sdk": "^2.0.0", "matrix-widget-api": "^1.0.0", "p-retry": "4", "sdp-transform": "^2.14.1", diff --git a/yarn.lock b/yarn.lock index a9776dac67d..38c6946c0db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1944,6 +1944,11 @@ agent-base@6: dependencies: debug "4" +ajv-errors@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-3.0.0.tgz#e54f299f3a3d30fe144161e5f0d8d51196c527bc" + integrity sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ== + ajv@^6.10.0, ajv@^6.12.4, ajv@~6.12.6: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -1954,6 +1959,16 @@ ajv@^6.10.0, ajv@^6.12.4, ajv@~6.12.6: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^8.11.2: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + align-text@^0.1.1, align-text@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" @@ -4929,6 +4944,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -5150,10 +5170,13 @@ marked@^4.2.5: resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.5.tgz#979813dfc1252cc123a79b71b095759a32f42a5d" integrity sha512-jPueVhumq7idETHkb203WDD4fMA3yV9emQ5vLwop58lu8bTclMghBWcYAavlDqIEMaisADinV1TooIFCfqOsYQ== -matrix-events-sdk@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" - integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== +matrix-events-sdk@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-2.0.0.tgz#f5f8dafbe4eae07fdbb628627f430ca5b1fd8c7a" + integrity sha512-UZbifYIO2o9+sNn4YuGjhMof/88TG68PyecKnH/pt8V3MFq0RZsbBUe+3/k5ZeVcEtr0pQLmcKB7d8aQVsVO/w== + dependencies: + ajv "^8.11.2" + ajv-errors "^3.0.0" matrix-mock-request@^2.5.0: version "2.6.0" @@ -6121,6 +6144,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" From 5234bbe22e7c776b5865419dde4d3a40506b5d71 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 13 Jan 2023 16:58:00 -0700 Subject: [PATCH 4/5] Add tests --- spec/unit/matrix-client.spec.ts | 67 ++++++++++++++++++++++++++++++++- spec/unit/room.spec.ts | 16 ++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index b692e10ce22..3d127ef9486 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -32,7 +32,7 @@ import { } from "../../src/@types/event"; import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib"; import { Crypto } from "../../src/crypto"; -import { EventStatus, MatrixEvent } from "../../src/models/event"; +import { EventStatus, IContent, MatrixEvent } from "../../src/models/event"; import { Preset } from "../../src/@types/partials"; import { ReceiptType } from "../../src/@types/read_receipts"; import * as testUtils from "../test-utils/test-utils"; @@ -2134,6 +2134,71 @@ describe("MatrixClient", function () { }); }); + describe("sendMessage", () => { + describe("roomId,content,txnId signature", () => { + it("should translate these values to the correct request", async () => { + const roomId = "!room:example.org"; + const content: IContent = {hello: "world"}; + const txnId = "m.1234"; + const response = {event_id: "$example"}; + + const sendSpy = jest.spyOn(client, "sendEvent").mockResolvedValue(response); + + const result = await client.sendMessage(roomId, content, txnId); + expect(result).toEqual(response); + expect(sendSpy).toHaveBeenCalledWith(roomId, null, "m.room.message", content, txnId); + }); + }); + + describe("roomId,threadId,content,txnId signature", () => { + it("should translate these values to the correct request", async () => { + const roomId = "!room:example.org"; + const threadId = "$thread"; + const content: IContent = {hello: "world"}; + const txnId = "m.1234"; + const response = {event_id: "$example"}; + + const sendSpy = jest.spyOn(client, "sendEvent").mockResolvedValue(response); + + const result = await client.sendMessage(roomId, threadId, content, txnId); + expect(result).toEqual(response); + expect(sendSpy).toHaveBeenCalledWith(roomId, threadId, "m.room.message", content, txnId); + }); + }); + + it.each(["m.text", "m.emote"])("should override %s msgtype events", async (msgtype) => { + const roomId = "!room:example.org"; + const content: IContent = { + msgtype: msgtype, + body: "**test**", + format: "org.matrix.custom.html", + formatted_body: "test", + }; + const expectedContent: IContent = { + "org.matrix.msc1767.markup": [ + {body: "**test**"}, + {body: "test", mimetype: "text/html"}, + ], + }; + const expectedEventType = msgtype === "m.emote" ? "org.matrix.msc1767.emote" : "org.matrix.msc1767.message"; + const txnId = "m.1234"; + const response = {event_id: "$example"}; + + client.getRoom = (rid) => { + if (rid === roomId) { + // XXX: This is a really bad mock. + return {unstableRequiresExtensibleEvents: () => true} as unknown as Room; + } + return null; + } + const sendSpy = jest.spyOn(client, "sendEvent").mockResolvedValue(response); + + const result = await client.sendMessage(roomId, content, txnId); + expect(result).toEqual(response); + expect(sendSpy).toHaveBeenCalledWith(roomId, null, expectedEventType, expectedContent, txnId); + }); + }); + describe("delete account data", () => { afterEach(() => { jest.spyOn(featureUtils, "buildFeatureSupportMap").mockRestore(); diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 38fc2cdc42a..c890a0f42dc 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -193,6 +193,22 @@ describe("Room", function () { }); }); + describe("unstableRequiresExtensibleEvents", () => { + it("should return true for MSC1767 room version prefixes", () => { + room.getVersion = () => "org.matrix.msc1767.10"; + expect(room.unstableRequiresExtensibleEvents()).toBe(true); + + room.getVersion = () => "org.matrix.msc1767.9"; + expect(room.unstableRequiresExtensibleEvents()).toBe(true); + + room.getVersion = () => "not.extensible.10"; + expect(room.unstableRequiresExtensibleEvents()).toBe(false); + + room.getVersion = () => "10"; + expect(room.unstableRequiresExtensibleEvents()).toBe(false); + }); + }); + describe("getAvatarUrl", function () { const hsUrl = "https://my.home.server"; From 11c9a29fef5d2c60639758f69d31abc0ffba4c17 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 13 Jan 2023 17:00:51 -0700 Subject: [PATCH 5/5] =?UTF-8?q?=E2=9C=A8=20prettier=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/unit/matrix-client.spec.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 3d127ef9486..4337845e88c 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -2138,9 +2138,9 @@ describe("MatrixClient", function () { describe("roomId,content,txnId signature", () => { it("should translate these values to the correct request", async () => { const roomId = "!room:example.org"; - const content: IContent = {hello: "world"}; + const content: IContent = { hello: "world" }; const txnId = "m.1234"; - const response = {event_id: "$example"}; + const response = { event_id: "$example" }; const sendSpy = jest.spyOn(client, "sendEvent").mockResolvedValue(response); @@ -2154,9 +2154,9 @@ describe("MatrixClient", function () { it("should translate these values to the correct request", async () => { const roomId = "!room:example.org"; const threadId = "$thread"; - const content: IContent = {hello: "world"}; + const content: IContent = { hello: "world" }; const txnId = "m.1234"; - const response = {event_id: "$example"}; + const response = { event_id: "$example" }; const sendSpy = jest.spyOn(client, "sendEvent").mockResolvedValue(response); @@ -2175,22 +2175,19 @@ describe("MatrixClient", function () { formatted_body: "test", }; const expectedContent: IContent = { - "org.matrix.msc1767.markup": [ - {body: "**test**"}, - {body: "test", mimetype: "text/html"}, - ], + "org.matrix.msc1767.markup": [{ body: "**test**" }, { body: "test", mimetype: "text/html" }], }; const expectedEventType = msgtype === "m.emote" ? "org.matrix.msc1767.emote" : "org.matrix.msc1767.message"; const txnId = "m.1234"; - const response = {event_id: "$example"}; + const response = { event_id: "$example" }; client.getRoom = (rid) => { if (rid === roomId) { // XXX: This is a really bad mock. - return {unstableRequiresExtensibleEvents: () => true} as unknown as Room; + return { unstableRequiresExtensibleEvents: () => true } as unknown as Room; } return null; - } + }; const sendSpy = jest.spyOn(client, "sendEvent").mockResolvedValue(response); const result = await client.sendMessage(roomId, content, txnId);