diff --git a/spec/unit/models/event.spec.ts b/spec/unit/models/event.spec.ts new file mode 100644 index 000000000..cafb318e1 --- /dev/null +++ b/spec/unit/models/event.spec.ts @@ -0,0 +1,60 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixEvent } from "../../../src/models/event"; + +describe('MatrixEvent', () => { + it('should create copies of itself', () => { + const a = new MatrixEvent({ + type: "com.example.test", + content: { + isTest: true, + num: 42, + }, + }); + + const clone = a.getSnapshotCopy(); + expect(clone).toBeDefined(); + expect(clone).not.toBe(a); + expect(clone.event).not.toBe(a.event); + expect(clone.event).toMatchObject(a.event); + + // The other properties we're not super interested in, honestly. + }); + + it('should compare itself to other events using json', () => { + const a = new MatrixEvent({ + type: "com.example.test", + content: { + isTest: true, + num: 42, + }, + }); + const b = new MatrixEvent({ + type: "com.example.test______B", + content: { + isTest: true, + num: 42, + }, + }); + expect(a.isEquivalentTo(b)).toBe(false); + expect(a.isEquivalentTo(a)).toBe(true); + expect(b.isEquivalentTo(a)).toBe(false); + expect(b.isEquivalentTo(b)).toBe(true); + expect(a.getSnapshotCopy().isEquivalentTo(a)).toBe(true); + expect(a.getSnapshotCopy().isEquivalentTo(b)).toBe(false); + }); +}); diff --git a/spec/unit/utils.spec.ts b/spec/unit/utils.spec.ts index 76123d1ca..5867a9fde 100644 --- a/spec/unit/utils.spec.ts +++ b/spec/unit/utils.spec.ts @@ -2,7 +2,7 @@ import * as utils from "../../src/utils"; import { alphabetPad, averageBetweenStrings, - baseToString, + baseToString, deepSortedObjectEntries, DEFAULT_ALPHABET, lexicographicCompare, nextString, @@ -429,4 +429,38 @@ describe("utils", function() { expect(lexicographicCompare('a', 'A') > 0).toBe(true); }); }); + + describe('deepSortedObjectEntries', () => { + it('should auto-return non-objects', () => { + expect(deepSortedObjectEntries(42)).toEqual(42); + expect(deepSortedObjectEntries("not object")).toEqual("not object"); + expect(deepSortedObjectEntries(true)).toEqual(true); + expect(deepSortedObjectEntries([42])).toEqual([42]); + expect(deepSortedObjectEntries(null)).toEqual(null); + expect(deepSortedObjectEntries(undefined)).toEqual(undefined); + }); + + it('should sort objects appropriately', () => { + const input = { + a: 42, + b: { + d: {}, + a: "test", + b: "alpha", + }, + [72]: "test", + }; + const output = [ + ["72", "test"], + ["a", 42], + ["b", [ + ["a", "test"], + ["b", "alpha"], + ["d", []], + ]], + ]; + + expect(deepSortedObjectEntries(input)).toMatchObject(output); + }); + }); }); diff --git a/src/models/event.js b/src/models/event.js index 1b3bec755..838cd1a3d 100644 --- a/src/models/event.js +++ b/src/models/event.js @@ -24,6 +24,7 @@ limitations under the License. import { EventEmitter } from 'events'; import * as utils from '../utils'; import { logger } from '../logger'; +import { deepSortedObjectEntries } from "../utils"; /** * Enum for event statuses. @@ -1143,6 +1144,41 @@ utils.extend(MatrixEvent.prototype, { getTxnId() { return this._txnId; }, + + /** + * Get a copy/snapshot of this event. The returned copy will be loosely linked + * back to this instance, though will have "frozen" event information. Other + * properties may mutate depending on the state of this instance at the time + * of snapshotting. + * + * This is meant to be used to snapshot the event details themselves, not the + * features (such as sender) surrounding the event. + * @returns {MatrixEvent} A snapshot of this event. + */ + getSnapshotCopy() { + const ev = new MatrixEvent(JSON.parse(JSON.stringify(this.event))); + for (const [p, v] of Object.entries(this)) { + if (p !== "event") { // exclude the thing we just cloned + ev[p] = v; + } + } + return ev; + }, + + /** + * Determines if this event is equivalent to the given event. This only checks + * the event object itself, not the other properties of the event. Intended for + * use with getSnapshotCopy() to identify events changing. + * @param {MatrixEvent} otherEvent The other event to check against. + * @returns {boolean} True if the events are the same, false otherwise. + */ + isEquivalentTo(otherEvent) { + if (!otherEvent) return false; + if (otherEvent === this) return true; + const myProps = deepSortedObjectEntries(this.event); + const theirProps = deepSortedObjectEntries(otherEvent.event); + return JSON.stringify(myProps) === JSON.stringify(theirProps); + }, }); /* _REDACT_KEEP_KEY_MAP gives the keys we keep when an event is redacted diff --git a/src/utils.ts b/src/utils.ts index 50c5b9f39..2097e5bb9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -235,6 +235,31 @@ export function deepCompare(x: any, y: any): boolean { return true; } +/** + * Creates an array of object properties/values (entries) then + * sorts the result by key, recursively. The input object must + * ensure it does not have loops. If the input is not an object + * then it will be returned as-is. + * @param {*} obj The object to get entries of + * @returns {[string, *][]} The entries, sorted by key. + */ +export function deepSortedObjectEntries(obj: any): [string, any][] { + if (typeof(obj) !== "object") return obj; + + // Apparently these are object types... + if (obj === null || obj === undefined || Array.isArray(obj)) return obj; + + const pairs: [string, any][] = []; + for (const [k, v] of Object.entries(obj)) { + pairs.push([k, deepSortedObjectEntries(v)]); + } + + // lexicographicCompare is faster than localeCompare, so let's use that. + pairs.sort((a, b) => lexicographicCompare(a[0], b[0])); + + return pairs; +} + /** * Copy properties from one object to another. *