From 66174007cb9c2a54585722f2ba09dbd8a3c0ad88 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 9 Dec 2022 13:22:38 -0700 Subject: [PATCH 01/22] Move old extensible events structure to v1-old holding dir --- src/{ => v1-old}/ExtensibleEvents.ts | 0 src/{ => v1-old}/IPartialEvent.ts | 0 src/{ => v1-old}/InvalidEventError.ts | 0 src/{ => v1-old}/NamespacedMap.ts | 0 src/{ => v1-old}/NamespacedValue.ts | 0 src/{ => v1-old}/events/EmoteEvent.ts | 0 src/{ => v1-old}/events/ExtensibleEvent.ts | 0 src/{ => v1-old}/events/MessageEvent.ts | 0 src/{ => v1-old}/events/NoticeEvent.ts | 0 src/{ => v1-old}/events/PollEndEvent.ts | 0 src/{ => v1-old}/events/PollResponseEvent.ts | 0 src/{ => v1-old}/events/PollStartEvent.ts | 0 src/{ => v1-old}/events/message_types.ts | 0 src/{ => v1-old}/events/poll_types.ts | 0 src/{ => v1-old}/events/relationship_types.ts | 0 src/{ => v1-old}/index.ts | 0 src/{ => v1-old}/interpreters/legacy/MRoomMessage.ts | 0 src/{ => v1-old}/interpreters/modern/MMessage.ts | 0 src/{ => v1-old}/interpreters/modern/MPoll.ts | 0 src/{ => v1-old}/types.ts | 0 src/{ => v1-old}/utility/MessageMatchers.ts | 0 src/{ => v1-old}/utility/events.ts | 0 test/{ => v1-old}/ExtensibleEvents.test.ts | 2 +- test/{ => v1-old}/NamespacedMap.test.ts | 2 +- test/{ => v1-old}/NamespacedValue.test.ts | 2 +- test/{ => v1-old}/events/EmoteEvent.test.ts | 2 +- test/{ => v1-old}/events/ExtensibleEvent.test.ts | 2 +- test/{ => v1-old}/events/MessageEvent.test.ts | 2 +- test/{ => v1-old}/events/NoticeEvent.test.ts | 2 +- test/{ => v1-old}/events/PollEndEvent.test.ts | 2 +- test/{ => v1-old}/events/PollResponseEvent.test.ts | 2 +- test/{ => v1-old}/events/PollStartEvent.test.ts | 2 +- test/{ => v1-old}/interpreters/legacy/MRoomMessage.test.ts | 2 +- test/{ => v1-old}/interpreters/modern/MMessage.test.ts | 2 +- test/{ => v1-old}/interpreters/modern/MPoll.test.ts | 2 +- test/{ => v1-old}/utility/MessageMatchers.test.ts | 2 +- test/{ => v1-old}/utility/events.test.ts | 2 +- 37 files changed, 15 insertions(+), 15 deletions(-) rename src/{ => v1-old}/ExtensibleEvents.ts (100%) rename src/{ => v1-old}/IPartialEvent.ts (100%) rename src/{ => v1-old}/InvalidEventError.ts (100%) rename src/{ => v1-old}/NamespacedMap.ts (100%) rename src/{ => v1-old}/NamespacedValue.ts (100%) rename src/{ => v1-old}/events/EmoteEvent.ts (100%) rename src/{ => v1-old}/events/ExtensibleEvent.ts (100%) rename src/{ => v1-old}/events/MessageEvent.ts (100%) rename src/{ => v1-old}/events/NoticeEvent.ts (100%) rename src/{ => v1-old}/events/PollEndEvent.ts (100%) rename src/{ => v1-old}/events/PollResponseEvent.ts (100%) rename src/{ => v1-old}/events/PollStartEvent.ts (100%) rename src/{ => v1-old}/events/message_types.ts (100%) rename src/{ => v1-old}/events/poll_types.ts (100%) rename src/{ => v1-old}/events/relationship_types.ts (100%) rename src/{ => v1-old}/index.ts (100%) rename src/{ => v1-old}/interpreters/legacy/MRoomMessage.ts (100%) rename src/{ => v1-old}/interpreters/modern/MMessage.ts (100%) rename src/{ => v1-old}/interpreters/modern/MPoll.ts (100%) rename src/{ => v1-old}/types.ts (100%) rename src/{ => v1-old}/utility/MessageMatchers.ts (100%) rename src/{ => v1-old}/utility/events.ts (100%) rename test/{ => v1-old}/ExtensibleEvents.test.ts (99%) rename test/{ => v1-old}/NamespacedMap.test.ts (99%) rename test/{ => v1-old}/NamespacedValue.test.ts (99%) rename test/{ => v1-old}/events/EmoteEvent.test.ts (99%) rename test/{ => v1-old}/events/ExtensibleEvent.test.ts (94%) rename test/{ => v1-old}/events/MessageEvent.test.ts (99%) rename test/{ => v1-old}/events/NoticeEvent.test.ts (99%) rename test/{ => v1-old}/events/PollEndEvent.test.ts (99%) rename test/{ => v1-old}/events/PollResponseEvent.test.ts (99%) rename test/{ => v1-old}/events/PollStartEvent.test.ts (99%) rename test/{ => v1-old}/interpreters/legacy/MRoomMessage.test.ts (99%) rename test/{ => v1-old}/interpreters/modern/MMessage.test.ts (99%) rename test/{ => v1-old}/interpreters/modern/MPoll.test.ts (99%) rename test/{ => v1-old}/utility/MessageMatchers.test.ts (99%) rename test/{ => v1-old}/utility/events.test.ts (98%) diff --git a/src/ExtensibleEvents.ts b/src/v1-old/ExtensibleEvents.ts similarity index 100% rename from src/ExtensibleEvents.ts rename to src/v1-old/ExtensibleEvents.ts diff --git a/src/IPartialEvent.ts b/src/v1-old/IPartialEvent.ts similarity index 100% rename from src/IPartialEvent.ts rename to src/v1-old/IPartialEvent.ts diff --git a/src/InvalidEventError.ts b/src/v1-old/InvalidEventError.ts similarity index 100% rename from src/InvalidEventError.ts rename to src/v1-old/InvalidEventError.ts diff --git a/src/NamespacedMap.ts b/src/v1-old/NamespacedMap.ts similarity index 100% rename from src/NamespacedMap.ts rename to src/v1-old/NamespacedMap.ts diff --git a/src/NamespacedValue.ts b/src/v1-old/NamespacedValue.ts similarity index 100% rename from src/NamespacedValue.ts rename to src/v1-old/NamespacedValue.ts diff --git a/src/events/EmoteEvent.ts b/src/v1-old/events/EmoteEvent.ts similarity index 100% rename from src/events/EmoteEvent.ts rename to src/v1-old/events/EmoteEvent.ts diff --git a/src/events/ExtensibleEvent.ts b/src/v1-old/events/ExtensibleEvent.ts similarity index 100% rename from src/events/ExtensibleEvent.ts rename to src/v1-old/events/ExtensibleEvent.ts diff --git a/src/events/MessageEvent.ts b/src/v1-old/events/MessageEvent.ts similarity index 100% rename from src/events/MessageEvent.ts rename to src/v1-old/events/MessageEvent.ts diff --git a/src/events/NoticeEvent.ts b/src/v1-old/events/NoticeEvent.ts similarity index 100% rename from src/events/NoticeEvent.ts rename to src/v1-old/events/NoticeEvent.ts diff --git a/src/events/PollEndEvent.ts b/src/v1-old/events/PollEndEvent.ts similarity index 100% rename from src/events/PollEndEvent.ts rename to src/v1-old/events/PollEndEvent.ts diff --git a/src/events/PollResponseEvent.ts b/src/v1-old/events/PollResponseEvent.ts similarity index 100% rename from src/events/PollResponseEvent.ts rename to src/v1-old/events/PollResponseEvent.ts diff --git a/src/events/PollStartEvent.ts b/src/v1-old/events/PollStartEvent.ts similarity index 100% rename from src/events/PollStartEvent.ts rename to src/v1-old/events/PollStartEvent.ts diff --git a/src/events/message_types.ts b/src/v1-old/events/message_types.ts similarity index 100% rename from src/events/message_types.ts rename to src/v1-old/events/message_types.ts diff --git a/src/events/poll_types.ts b/src/v1-old/events/poll_types.ts similarity index 100% rename from src/events/poll_types.ts rename to src/v1-old/events/poll_types.ts diff --git a/src/events/relationship_types.ts b/src/v1-old/events/relationship_types.ts similarity index 100% rename from src/events/relationship_types.ts rename to src/v1-old/events/relationship_types.ts diff --git a/src/index.ts b/src/v1-old/index.ts similarity index 100% rename from src/index.ts rename to src/v1-old/index.ts diff --git a/src/interpreters/legacy/MRoomMessage.ts b/src/v1-old/interpreters/legacy/MRoomMessage.ts similarity index 100% rename from src/interpreters/legacy/MRoomMessage.ts rename to src/v1-old/interpreters/legacy/MRoomMessage.ts diff --git a/src/interpreters/modern/MMessage.ts b/src/v1-old/interpreters/modern/MMessage.ts similarity index 100% rename from src/interpreters/modern/MMessage.ts rename to src/v1-old/interpreters/modern/MMessage.ts diff --git a/src/interpreters/modern/MPoll.ts b/src/v1-old/interpreters/modern/MPoll.ts similarity index 100% rename from src/interpreters/modern/MPoll.ts rename to src/v1-old/interpreters/modern/MPoll.ts diff --git a/src/types.ts b/src/v1-old/types.ts similarity index 100% rename from src/types.ts rename to src/v1-old/types.ts diff --git a/src/utility/MessageMatchers.ts b/src/v1-old/utility/MessageMatchers.ts similarity index 100% rename from src/utility/MessageMatchers.ts rename to src/v1-old/utility/MessageMatchers.ts diff --git a/src/utility/events.ts b/src/v1-old/utility/events.ts similarity index 100% rename from src/utility/events.ts rename to src/v1-old/utility/events.ts diff --git a/test/ExtensibleEvents.test.ts b/test/v1-old/ExtensibleEvents.test.ts similarity index 99% rename from test/ExtensibleEvents.test.ts rename to test/v1-old/ExtensibleEvents.test.ts index 614d9a6..76664c4 100644 --- a/test/ExtensibleEvents.test.ts +++ b/test/v1-old/ExtensibleEvents.test.ts @@ -42,7 +42,7 @@ import { PollStartEvent, REFERENCE_RELATION, UnstableValue, -} from "../src"; +} from "../../src/v1-old"; describe("ExtensibleEvents", () => { afterEach(() => { diff --git a/test/NamespacedMap.test.ts b/test/v1-old/NamespacedMap.test.ts similarity index 99% rename from test/NamespacedMap.test.ts rename to test/v1-old/NamespacedMap.test.ts index 429a647..a111b7a 100644 --- a/test/NamespacedMap.test.ts +++ b/test/v1-old/NamespacedMap.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {NamespacedMap, NamespacedValue, UnstableValue} from "../src"; +import {NamespacedMap, NamespacedValue, UnstableValue} from "../../src/v1-old"; import {STABLE_VALUE, UNSTABLE_VALUE} from "./NamespacedValue.test"; type TestableNamespacedMap = {internalMap: Map} & NamespacedMap; diff --git a/test/NamespacedValue.test.ts b/test/v1-old/NamespacedValue.test.ts similarity index 99% rename from test/NamespacedValue.test.ts rename to test/v1-old/NamespacedValue.test.ts index 91d638c..c1165ac 100644 --- a/test/NamespacedValue.test.ts +++ b/test/v1-old/NamespacedValue.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {NamespacedValue, UnstableValue} from "../src"; +import {NamespacedValue, UnstableValue} from "../../src/v1-old"; export const STABLE_VALUE = "org.example.stable"; export const UNSTABLE_VALUE = "org.example.unstable"; diff --git a/test/events/EmoteEvent.test.ts b/test/v1-old/events/EmoteEvent.test.ts similarity index 99% rename from test/events/EmoteEvent.test.ts rename to test/v1-old/events/EmoteEvent.test.ts index 08426cf..ea1779b 100644 --- a/test/events/EmoteEvent.test.ts +++ b/test/v1-old/events/EmoteEvent.test.ts @@ -26,7 +26,7 @@ import { M_NOTICE, M_NOTICE_EVENT_CONTENT, M_TEXT, -} from "../../src"; +} from "../../../src/v1-old"; describe("EmoteEvent", () => { it("should parse m.text", () => { diff --git a/test/events/ExtensibleEvent.test.ts b/test/v1-old/events/ExtensibleEvent.test.ts similarity index 94% rename from test/events/ExtensibleEvent.test.ts rename to test/v1-old/events/ExtensibleEvent.test.ts index 8c28b0d..2ad278e 100644 --- a/test/events/ExtensibleEvent.test.ts +++ b/test/v1-old/events/ExtensibleEvent.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {EventType, ExtensibleEvent, IPartialEvent} from "../../src"; +import {EventType, ExtensibleEvent, IPartialEvent} from "../../../src/v1-old"; class MockEvent extends ExtensibleEvent { public constructor(wireEvent: IPartialEvent) { diff --git a/test/events/MessageEvent.test.ts b/test/v1-old/events/MessageEvent.test.ts similarity index 99% rename from test/events/MessageEvent.test.ts rename to test/v1-old/events/MessageEvent.test.ts index 705dea7..824324b 100644 --- a/test/events/MessageEvent.test.ts +++ b/test/v1-old/events/MessageEvent.test.ts @@ -26,7 +26,7 @@ import { M_NOTICE_EVENT_CONTENT, M_TEXT, MessageEvent, -} from "../../src"; +} from "../../../src/v1-old"; describe("MessageEvent", () => { it("should parse m.text", () => { diff --git a/test/events/NoticeEvent.test.ts b/test/v1-old/events/NoticeEvent.test.ts similarity index 99% rename from test/events/NoticeEvent.test.ts rename to test/v1-old/events/NoticeEvent.test.ts index a02853f..97845e7 100644 --- a/test/events/NoticeEvent.test.ts +++ b/test/v1-old/events/NoticeEvent.test.ts @@ -26,7 +26,7 @@ import { M_NOTICE_EVENT_CONTENT, M_TEXT, NoticeEvent, -} from "../../src"; +} from "../../../src/v1-old"; describe("NoticeEvent", () => { it("should parse m.text", () => { diff --git a/test/events/PollEndEvent.test.ts b/test/v1-old/events/PollEndEvent.test.ts similarity index 99% rename from test/events/PollEndEvent.test.ts rename to test/v1-old/events/PollEndEvent.test.ts index 13d3b31..38bbe77 100644 --- a/test/events/PollEndEvent.test.ts +++ b/test/v1-old/events/PollEndEvent.test.ts @@ -22,7 +22,7 @@ import { M_TEXT, PollEndEvent, REFERENCE_RELATION, -} from "../../src"; +} from "../../../src/v1-old"; describe("PollEndEvent", () => { // Note: throughout these tests we don't really bother testing that diff --git a/test/events/PollResponseEvent.test.ts b/test/v1-old/events/PollResponseEvent.test.ts similarity index 99% rename from test/events/PollResponseEvent.test.ts rename to test/v1-old/events/PollResponseEvent.test.ts index aa01380..cb6decd 100644 --- a/test/events/PollResponseEvent.test.ts +++ b/test/v1-old/events/PollResponseEvent.test.ts @@ -25,7 +25,7 @@ import { PollResponseEvent, PollStartEvent, REFERENCE_RELATION, -} from "../../src"; +} from "../../../src/v1-old"; const SAMPLE_POLL = new PollStartEvent({ type: M_POLL_START.name, diff --git a/test/events/PollStartEvent.test.ts b/test/v1-old/events/PollStartEvent.test.ts similarity index 99% rename from test/events/PollStartEvent.test.ts rename to test/v1-old/events/PollStartEvent.test.ts index 53ca107..f3fd333 100644 --- a/test/events/PollStartEvent.test.ts +++ b/test/v1-old/events/PollStartEvent.test.ts @@ -25,7 +25,7 @@ import { POLL_ANSWER, PollAnswerSubevent, PollStartEvent, -} from "../../src"; +} from "../../../src/v1-old"; describe("PollAnswerSubevent", () => { // Note: throughout these tests we don't really bother testing that diff --git a/test/interpreters/legacy/MRoomMessage.test.ts b/test/v1-old/interpreters/legacy/MRoomMessage.test.ts similarity index 99% rename from test/interpreters/legacy/MRoomMessage.test.ts rename to test/v1-old/interpreters/legacy/MRoomMessage.test.ts index 0c11d53..39ab135 100644 --- a/test/interpreters/legacy/MRoomMessage.test.ts +++ b/test/v1-old/interpreters/legacy/MRoomMessage.test.ts @@ -26,7 +26,7 @@ import { MessageEvent, NoticeEvent, parseMRoomMessage, -} from "../../../src"; +} from "../../../../src/v1-old"; describe("parseMRoomMessage", () => { it("should return an unmodified MessageEvent when using extensible events", () => { diff --git a/test/interpreters/modern/MMessage.test.ts b/test/v1-old/interpreters/modern/MMessage.test.ts similarity index 99% rename from test/interpreters/modern/MMessage.test.ts rename to test/v1-old/interpreters/modern/MMessage.test.ts index 7d31562..f283f38 100644 --- a/test/interpreters/modern/MMessage.test.ts +++ b/test/v1-old/interpreters/modern/MMessage.test.ts @@ -25,7 +25,7 @@ import { M_TEXT, NoticeEvent, parseMMessage, -} from "../../../src"; +} from "../../../../src/v1-old"; describe("parseMMessage", () => { it("should return an unmodified MessageEvent", () => { diff --git a/test/interpreters/modern/MPoll.test.ts b/test/v1-old/interpreters/modern/MPoll.test.ts similarity index 99% rename from test/interpreters/modern/MPoll.test.ts rename to test/v1-old/interpreters/modern/MPoll.test.ts index cfa63ff..1bfe17c 100644 --- a/test/interpreters/modern/MPoll.test.ts +++ b/test/v1-old/interpreters/modern/MPoll.test.ts @@ -29,7 +29,7 @@ import { PollResponseEvent, PollStartEvent, REFERENCE_RELATION, -} from "../../../src"; +} from "../../../../src/v1-old"; describe("parseMPoll", () => { it("should return an unmodified PollStartEvent", () => { diff --git a/test/utility/MessageMatchers.test.ts b/test/v1-old/utility/MessageMatchers.test.ts similarity index 99% rename from test/utility/MessageMatchers.test.ts rename to test/v1-old/utility/MessageMatchers.test.ts index e45c4a2..48a4e34 100644 --- a/test/utility/MessageMatchers.test.ts +++ b/test/v1-old/utility/MessageMatchers.test.ts @@ -24,7 +24,7 @@ import { M_MESSAGE_EVENT_CONTENT, M_NOTICE, M_TEXT, -} from "../../src"; +} from "../../../src/v1-old"; describe("isEventLike", () => { it("should match legacy text", () => { diff --git a/test/utility/events.test.ts b/test/v1-old/utility/events.test.ts similarity index 98% rename from test/utility/events.test.ts rename to test/v1-old/utility/events.test.ts index fcd5729..d31cb6b 100644 --- a/test/utility/events.test.ts +++ b/test/v1-old/utility/events.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {isEventTypeSame, NamespacedValue} from "../../src"; +import {isEventTypeSame, NamespacedValue} from "../../../src/v1-old"; describe("isEventTypeSame", () => { it("should match string and string", () => { From bf1b8c0ebe423ff39fdebd24ab5f0760d777d89d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 9 Dec 2022 14:37:11 -0700 Subject: [PATCH 02/22] Add initial content blocks system --- package.json | 4 + src/AjvContainer.ts | 32 +++ src/content_blocks/ArrayBlock.ts | 47 ++++ src/content_blocks/BaseBlock.ts | 51 +++++ src/content_blocks/BooleanBlock.ts | 46 ++++ src/content_blocks/IntegerBlock.ts | 46 ++++ src/content_blocks/InvalidBlockError.ts | 33 +++ src/content_blocks/ObjectBlock.ts | 46 ++++ src/content_blocks/StringBlock.ts | 46 ++++ src/content_blocks/m/MarkupBlock.ts | 126 +++++++++++ src/content_blocks/types_wire.d.ts | 27 +++ test/content_blocks/ArrayBlock.test.ts | 49 ++++ test/content_blocks/BaseBlock.test.ts | 48 ++++ test/content_blocks/BooleanBlock.test.ts | 51 +++++ test/content_blocks/IntegerBlock.test.ts | 57 +++++ test/content_blocks/InvalidBlockError.test.ts | 51 +++++ test/content_blocks/ObjectBlock.test.ts | 49 ++++ test/content_blocks/StringBlock.test.ts | 54 +++++ test/content_blocks/m/MarkupBlock.test.ts | 212 ++++++++++++++++++ .../m/__snapshots__/MarkupBlock.test.ts.snap | 62 +++++ yarn.lock | 42 ++++ 21 files changed, 1179 insertions(+) create mode 100644 src/AjvContainer.ts create mode 100644 src/content_blocks/ArrayBlock.ts create mode 100644 src/content_blocks/BaseBlock.ts create mode 100644 src/content_blocks/BooleanBlock.ts create mode 100644 src/content_blocks/IntegerBlock.ts create mode 100644 src/content_blocks/InvalidBlockError.ts create mode 100644 src/content_blocks/ObjectBlock.ts create mode 100644 src/content_blocks/StringBlock.ts create mode 100644 src/content_blocks/m/MarkupBlock.ts create mode 100644 src/content_blocks/types_wire.d.ts create mode 100644 test/content_blocks/ArrayBlock.test.ts create mode 100644 test/content_blocks/BaseBlock.test.ts create mode 100644 test/content_blocks/BooleanBlock.test.ts create mode 100644 test/content_blocks/IntegerBlock.test.ts create mode 100644 test/content_blocks/InvalidBlockError.test.ts create mode 100644 test/content_blocks/ObjectBlock.test.ts create mode 100644 test/content_blocks/StringBlock.test.ts create mode 100644 test/content_blocks/m/MarkupBlock.test.ts create mode 100644 test/content_blocks/m/__snapshots__/MarkupBlock.test.ts.snap diff --git a/package.json b/package.json index 714b121..7fe794c 100644 --- a/package.json +++ b/package.json @@ -33,5 +33,9 @@ "rimraf": "^3.0.2", "ts-jest": "^29.0.3", "typescript": "^4.9.3" + }, + "dependencies": { + "ajv": "^8.11.2", + "ajv-errors": "^3.0.0" } } diff --git a/src/AjvContainer.ts b/src/AjvContainer.ts new file mode 100644 index 0000000..3481f15 --- /dev/null +++ b/src/AjvContainer.ts @@ -0,0 +1,32 @@ +/* +Copyright 2022 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 Ajv from "ajv"; +import AjvErrors from "ajv-errors"; + +export class AjvContainer { + public static readonly ajv = new Ajv({ + allErrors: true, + }); + + static { + AjvErrors(AjvContainer.ajv); + } + + /* istanbul ignore next */ + // noinspection JSUnusedLocalSymbols + private constructor() {} +} diff --git a/src/content_blocks/ArrayBlock.ts b/src/content_blocks/ArrayBlock.ts new file mode 100644 index 0000000..d52fdb5 --- /dev/null +++ b/src/content_blocks/ArrayBlock.ts @@ -0,0 +1,47 @@ +/* +Copyright 2022 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 {InvalidBlockError} from "./InvalidBlockError"; +import {BaseBlock} from "./BaseBlock"; +import {ContentBlockWire} from "./types_wire"; +import {Schema} from "ajv"; +import {AjvContainer} from "../AjvContainer"; + +/** + * Represents an array-based content block. + * @module Content Blocks + */ +export abstract class ArrayBlock extends BaseBlock { + public static readonly schema: Schema = { + type: "array", + errorMessage: "should be an array value", + }; + + public static readonly validateFn = AjvContainer.ajv.compile(ArrayBlock.schema); + + /** + * Creates a new ArrayBlock. + * @param name The name of the block, for error messages and debugging. + * @param raw The block's value. + * @protected + */ + protected constructor(name: string, raw: TItem[]) { + super(name, raw); + if (!ArrayBlock.validateFn(raw)) { + throw new InvalidBlockError(name, ArrayBlock.validateFn.errors); + } + } +} diff --git a/src/content_blocks/BaseBlock.ts b/src/content_blocks/BaseBlock.ts new file mode 100644 index 0000000..aa69ee9 --- /dev/null +++ b/src/content_blocks/BaseBlock.ts @@ -0,0 +1,51 @@ +/* +Copyright 2022 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 {ContentBlockWire} from "./types_wire"; +import {InvalidBlockError} from "./InvalidBlockError"; + +/** + * The simplest form of a content block in its parsed form. + * @module Content Blocks + */ +export abstract class BaseBlock { + private _raw: T | undefined = undefined; + + public get raw(): T { + return this._raw!; + } + + protected set raw(val: T) { + if (val === undefined || val === null) { + throw new InvalidBlockError( + this.name, + "Block value must be defined. Use a null-capable parser instead of passing such a value.", + ); + } + + this._raw = val; + } + + /** + * Creates a new BaseBlock. + * @param name The name of the block, for error messages and debugging. + * @param raw The block's value. + * @protected + */ + protected constructor(public readonly name: string, raw: T) { + this.raw = raw; // reuse validation logic + } +} diff --git a/src/content_blocks/BooleanBlock.ts b/src/content_blocks/BooleanBlock.ts new file mode 100644 index 0000000..41a99ec --- /dev/null +++ b/src/content_blocks/BooleanBlock.ts @@ -0,0 +1,46 @@ +/* +Copyright 2022 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 {InvalidBlockError} from "./InvalidBlockError"; +import {BaseBlock} from "./BaseBlock"; +import {Schema} from "ajv"; +import {AjvContainer} from "../AjvContainer"; + +/** + * Represents a boolean-based content block. + * @module Content Blocks + */ +export abstract class BooleanBlock extends BaseBlock { + public static readonly schema: Schema = { + type: "boolean", + errorMessage: "should be a boolean value", + }; + + public static readonly validateFn = AjvContainer.ajv.compile(BooleanBlock.schema); + + /** + * Creates a new IntegerBlock. + * @param name The name of the block, for error messages and debugging. + * @param raw The block's value. + * @protected + */ + protected constructor(name: string, raw: boolean) { + super(name, raw); + if (!BooleanBlock.validateFn(raw)) { + throw new InvalidBlockError(name, BooleanBlock.validateFn.errors); + } + } +} diff --git a/src/content_blocks/IntegerBlock.ts b/src/content_blocks/IntegerBlock.ts new file mode 100644 index 0000000..261c4eb --- /dev/null +++ b/src/content_blocks/IntegerBlock.ts @@ -0,0 +1,46 @@ +/* +Copyright 2022 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 {InvalidBlockError} from "./InvalidBlockError"; +import {BaseBlock} from "./BaseBlock"; +import {AjvContainer} from "../AjvContainer"; +import {Schema} from "ajv"; + +/** + * Represents an integer-based content block. + * @module Content Blocks + */ +export abstract class IntegerBlock extends BaseBlock { + public static readonly schema: Schema = { + type: "integer", + errorMessage: "should be an integer value", + }; + + public static readonly validateFn = AjvContainer.ajv.compile(IntegerBlock.schema); + + /** + * Creates a new IntegerBlock. + * @param name The name of the block, for error messages and debugging. + * @param raw The block's value. + * @protected + */ + protected constructor(name: string, raw: number) { + super(name, raw); + if (!IntegerBlock.validateFn(raw)) { + throw new InvalidBlockError(name, IntegerBlock.validateFn.errors); + } + } +} diff --git a/src/content_blocks/InvalidBlockError.ts b/src/content_blocks/InvalidBlockError.ts new file mode 100644 index 0000000..d331869 --- /dev/null +++ b/src/content_blocks/InvalidBlockError.ts @@ -0,0 +1,33 @@ +/* +Copyright 2022 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 {ErrorObject} from "ajv"; + +/** + * Thrown when a content block is unforgivably unparsable. + * @module Content Blocks + */ +export class InvalidBlockError extends Error { + public constructor(blockName: string, message: string | ErrorObject[] | null | undefined) { + super( + `${blockName}: ${ + typeof message === "string" + ? message + : message?.map(m => (m.message ? m.message : JSON.stringify(m))).join(", ") ?? "Validation failed" + }`, + ); + } +} diff --git a/src/content_blocks/ObjectBlock.ts b/src/content_blocks/ObjectBlock.ts new file mode 100644 index 0000000..1dda247 --- /dev/null +++ b/src/content_blocks/ObjectBlock.ts @@ -0,0 +1,46 @@ +/* +Copyright 2022 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 {InvalidBlockError} from "./InvalidBlockError"; +import {BaseBlock} from "./BaseBlock"; +import {Schema} from "ajv"; +import {AjvContainer} from "../AjvContainer"; + +/** + * Represents an object-based content block. + * @module Content Blocks + */ +export abstract class ObjectBlock extends BaseBlock { + public static readonly schema: Schema = { + type: "object", + errorMessage: "should be an object value", + }; + + public static readonly validateFn = AjvContainer.ajv.compile(ObjectBlock.schema); + + /** + * Creates a new ObjectBlock. + * @param name The name of the block, for error messages and debugging. + * @param raw The block's value. + * @protected + */ + protected constructor(name: string, raw: T) { + super(name, raw); + if (!ObjectBlock.validateFn(raw)) { + throw new InvalidBlockError(name, ObjectBlock.validateFn.errors); + } + } +} diff --git a/src/content_blocks/StringBlock.ts b/src/content_blocks/StringBlock.ts new file mode 100644 index 0000000..790a1b9 --- /dev/null +++ b/src/content_blocks/StringBlock.ts @@ -0,0 +1,46 @@ +/* +Copyright 2022 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 {InvalidBlockError} from "./InvalidBlockError"; +import {BaseBlock} from "./BaseBlock"; +import {Schema} from "ajv"; +import {AjvContainer} from "../AjvContainer"; + +/** + * Represents a string-based content block. + * @module Content Blocks + */ +export abstract class StringBlock extends BaseBlock { + public static readonly schema: Schema = { + type: "string", + errorMessage: "should be a string value", + }; + + public static readonly validateFn = AjvContainer.ajv.compile(StringBlock.schema); + + /** + * Creates a new StringBlock. + * @param name The name of the block, for error messages and debugging. + * @param raw The block's value. + * @protected + */ + protected constructor(name: string, raw: string) { + super(name, raw); + if (!StringBlock.validateFn(raw)) { + throw new InvalidBlockError(name, StringBlock.validateFn.errors); + } + } +} diff --git a/src/content_blocks/m/MarkupBlock.ts b/src/content_blocks/m/MarkupBlock.ts new file mode 100644 index 0000000..c5ce4c1 --- /dev/null +++ b/src/content_blocks/m/MarkupBlock.ts @@ -0,0 +1,126 @@ +/* +Copyright 2022 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 {ArrayBlock} from "../ArrayBlock"; +import {Schema} from "ajv"; +import {AjvContainer} from "../../AjvContainer"; +import {InvalidBlockError} from "../InvalidBlockError"; + +/** + * A representation of text within a markup block. + * @module Matrix Content Blocks + */ +export type MarkupRepresentation = { + body: string; + mimetype?: string; +}; + +/** + * A "markup" block, or a block meant to communicate human-readable and human-rendered + * text, with optional mimetype. + * @module Matrix Content Blocks + */ +export class MarkupBlock extends ArrayBlock { + public static readonly schema = ArrayBlock.schema; + public static readonly validateFn = ArrayBlock.validateFn; + + /** + * Schema definition for the markup representation (list item) specifically. + * + * Note: Schema for the whole markup value type is handled by the ArrayBlock class. + */ + public static readonly representationSchema: Schema = { + type: "object", + properties: { + body: { + type: "string", + nullable: false, + }, + mimetype: { + type: "string", + nullable: false, + }, + }, + required: ["body"], + errorMessage: { + properties: { + body: "body should be a non-null string and is required", + mimetype: "mimetype should be a non-null string, or undefined (field not required)", + }, + }, + }; + + /** + * Validation function for markup representations (list items) specifically. + * + * Note: Validation for the whole markup value type is handled by the ArrayBlock class. + */ + public static readonly representationValidateFn = AjvContainer.ajv.compile(MarkupBlock.representationSchema); + + /** + * Parse errors for representations. Representations described here are *removed* from the + * block's `raw` type, thus not being considered for rendering. + */ + public readonly representationErrors = new Map< + {index: number; representation: MarkupRepresentation | unknown}, + InvalidBlockError + >(); + + /** + * Creates a new MarkupBlock + * + * Invalid representations will be removed from the `raw` value, excluding them from rendering. + * Errors can be found from representationErrors after creating the object. + * @param raw The block's value. + */ + public constructor(raw: MarkupRepresentation[]) { + super("m.markup", raw); + this.raw = raw.filter((r, i) => { + const bool = MarkupBlock.representationValidateFn(r); + if (!bool) { + this.representationErrors.set( + { + index: i, + representation: r, + }, + new InvalidBlockError(`m.markup[${i}]`, MarkupBlock.representationValidateFn.errors), + ); + } + return bool; + }); + } + + /** + * The text representation of the block, if one is present. + */ + public get text(): string | undefined { + return this.raw.find(m => m.mimetype === undefined || m.mimetype === "text/plain")?.body; + } + + /** + * The HTML representation of the block, if one is present. + */ + public get html(): string | undefined { + return this.raw.find(m => m.mimetype === "text/html")?.body; + } + + /** + * The ordered representations for this markup block. + */ + public get representations(): MarkupRepresentation[] { + return this.raw; + } +} diff --git a/src/content_blocks/types_wire.d.ts b/src/content_blocks/types_wire.d.ts new file mode 100644 index 0000000..8f400b5 --- /dev/null +++ b/src/content_blocks/types_wire.d.ts @@ -0,0 +1,27 @@ +/* +Copyright 2022 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. +*/ + +/** + * The wire types for content blocks. + * @module Content Blocks + */ +export module ContentBlockWire { + /** + * Possible value types for a content block. + * @module Content Blocks + */ + type Value = string | boolean | number | object | Value[]; +} diff --git a/test/content_blocks/ArrayBlock.test.ts b/test/content_blocks/ArrayBlock.test.ts new file mode 100644 index 0000000..fe5aea4 --- /dev/null +++ b/test/content_blocks/ArrayBlock.test.ts @@ -0,0 +1,49 @@ +/* +Copyright 2022 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 {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; +import {ArrayBlock} from "../../src/content_blocks/ArrayBlock"; + +class TestArrayBlock extends ArrayBlock { + public constructor(raw: any) { + super("TestBlock", raw as any[]); // lie to TS + } +} + +describe("ArrayBlock", () => { + it("should retain the block name", () => { + const block = new TestArrayBlock([]); + expect(block.name).toStrictEqual("TestBlock"); + }); + + it.each([null, undefined])("should reject null and undefined: %s", val => { + expect(() => new TestArrayBlock(val)).toThrow( + new InvalidBlockError( + "TestBlock", + "Block value must be defined. Use a null-capable parser instead of passing such a value.", + ), + ); + }); + + it.each([["values"], []])("should accept arrays: '%s'", (...val) => { + const block = new TestArrayBlock(val); + expect(block.raw).toStrictEqual(val); + }); + + it.each([42, "test", "", {}, false])("should reject non-arrays: '%s'", val => { + expect(() => new TestArrayBlock(val)).toThrow(new InvalidBlockError("TestBlock", "should be an array value")); + }); +}); diff --git a/test/content_blocks/BaseBlock.test.ts b/test/content_blocks/BaseBlock.test.ts new file mode 100644 index 0000000..6d81789 --- /dev/null +++ b/test/content_blocks/BaseBlock.test.ts @@ -0,0 +1,48 @@ +/* +Copyright 2022 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 {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; +import {BaseBlock} from "../../src/content_blocks/BaseBlock"; + +class TestBaseBlock extends BaseBlock { + public constructor(raw: any) { + super("TestBlock", raw as number); // lie to TS + } +} + +describe("BaseBlock", () => { + it("should retain the block name", () => { + const block = new TestBaseBlock(42); + expect(block.name).toStrictEqual("TestBlock"); + }); + + it.each([null, undefined])("should reject null and undefined: %s", val => { + expect(() => new TestBaseBlock(val)).toThrow( + new InvalidBlockError( + "TestBlock", + "Block value must be defined. Use a null-capable parser instead of passing such a value.", + ), + ); + }); + + it.each(["string", "", true, false, 42, 42.1, {hello: "world"}, [1, 2, 3], {}, []])( + "should accept wire values: '%s'", + val => { + const block = new TestBaseBlock(val); + expect(block.raw).toStrictEqual(val); + }, + ); +}); diff --git a/test/content_blocks/BooleanBlock.test.ts b/test/content_blocks/BooleanBlock.test.ts new file mode 100644 index 0000000..85b0e2e --- /dev/null +++ b/test/content_blocks/BooleanBlock.test.ts @@ -0,0 +1,51 @@ +/* +Copyright 2022 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 {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; +import {BooleanBlock} from "../../src/content_blocks/BooleanBlock"; + +class TestBooleanBlock extends BooleanBlock { + public constructor(raw: any) { + super("TestBlock", raw as boolean); // lie to TS + } +} + +describe("BooleanBlock", () => { + it("should retain the block name", () => { + const block = new TestBooleanBlock(false); + expect(block.name).toStrictEqual("TestBlock"); + }); + + it.each([null, undefined])("should reject null and undefined: %s", val => { + expect(() => new TestBooleanBlock(val)).toThrow( + new InvalidBlockError( + "TestBlock", + "Block value must be defined. Use a null-capable parser instead of passing such a value.", + ), + ); + }); + + it.each([true, false])("should accept booleans: '%s'", val => { + const block = new TestBooleanBlock(val); + expect(block.raw).toStrictEqual(val); + }); + + it.each([42, "test", "", {}, []])("should reject non-booleans: '%s'", val => { + expect(() => new TestBooleanBlock(val)).toThrow( + new InvalidBlockError("TestBlock", "should be a boolean value"), + ); + }); +}); diff --git a/test/content_blocks/IntegerBlock.test.ts b/test/content_blocks/IntegerBlock.test.ts new file mode 100644 index 0000000..c4acc59 --- /dev/null +++ b/test/content_blocks/IntegerBlock.test.ts @@ -0,0 +1,57 @@ +/* +Copyright 2022 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 {IntegerBlock} from "../../src/content_blocks/IntegerBlock"; +import {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; + +class TestIntegerBlock extends IntegerBlock { + public constructor(raw: any) { + super("TestBlock", raw as number); // lie to TS + } +} + +describe("IntegerBlock", () => { + it("should retain the block name", () => { + const block = new TestIntegerBlock(42); + expect(block.name).toStrictEqual("TestBlock"); + }); + + it.each([null, undefined])("should reject null and undefined: %s", val => { + expect(() => new TestIntegerBlock(val)).toThrow( + new InvalidBlockError( + "TestBlock", + "Block value must be defined. Use a null-capable parser instead of passing such a value.", + ), + ); + }); + + it("should accept integers", () => { + const block = new TestIntegerBlock(42); + expect(block.raw).toStrictEqual(42); + }); + + it.each(["42", Number.NaN, true, {}, []])("should reject non-numbers: '%s'", val => { + expect(() => new TestIntegerBlock(val)).toThrow( + new InvalidBlockError("TestBlock", "should be an integer value"), + ); + }); + + it("should decline floats", () => { + expect(() => new TestIntegerBlock(42.1)).toThrow( + new InvalidBlockError("TestBlock", "should be an integer value"), + ); + }); +}); diff --git a/test/content_blocks/InvalidBlockError.test.ts b/test/content_blocks/InvalidBlockError.test.ts new file mode 100644 index 0000000..73208d4 --- /dev/null +++ b/test/content_blocks/InvalidBlockError.test.ts @@ -0,0 +1,51 @@ +/* +Copyright 2022 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 {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; + +describe("InvalidBlockError", () => { + it("should use the block name in the error", () => { + const err = new InvalidBlockError("TestBlock", "my message"); + expect(err.message).toEqual("TestBlock: my message"); + }); + + it("should use error objects if given", () => { + const err = new InvalidBlockError("TestBlock", [ + { + message: "test message", + keyword: "test", + params: [], + instancePath: "#/unused", + schemaPath: "#/unused", + }, + { + // message: "test message", // this one has no message + keyword: "test", + params: [], + instancePath: "#/unused", + schemaPath: "#/unused", + }, + ]); + expect(err.message).toEqual( + 'TestBlock: test message, {"keyword":"test","params":[],"instancePath":"#/unused","schemaPath":"#/unused"}', + ); + }); + + it.each([null, undefined])("should use a default message when none is provided: '%s'", val => { + const err = new InvalidBlockError("TestBlock", val); + expect(err.message).toEqual("TestBlock: Validation failed"); + }); +}); diff --git a/test/content_blocks/ObjectBlock.test.ts b/test/content_blocks/ObjectBlock.test.ts new file mode 100644 index 0000000..7e271b6 --- /dev/null +++ b/test/content_blocks/ObjectBlock.test.ts @@ -0,0 +1,49 @@ +/* +Copyright 2022 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 {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; +import {ObjectBlock} from "../../src/content_blocks/ObjectBlock"; + +class TestObjectBlock extends ObjectBlock { + public constructor(raw: any) { + super("TestBlock", raw); // lie to TS + } +} + +describe("ObjectBlock", () => { + it("should retain the block name", () => { + const block = new TestObjectBlock({}); + expect(block.name).toStrictEqual("TestBlock"); + }); + + it.each([null, undefined])("should reject null and undefined: %s", val => { + expect(() => new TestObjectBlock(val)).toThrow( + new InvalidBlockError( + "TestBlock", + "Block value must be defined. Use a null-capable parser instead of passing such a value.", + ), + ); + }); + + it.each([{hello: "world"}, {}])("should accept objects: '%s'", val => { + const block = new TestObjectBlock(val); + expect(block.raw).toStrictEqual(val); + }); + + it.each([42, "test", "", [], false])("should reject non-objects: '%s'", val => { + expect(() => new TestObjectBlock(val)).toThrow(new InvalidBlockError("TestBlock", "should be an object value")); + }); +}); diff --git a/test/content_blocks/StringBlock.test.ts b/test/content_blocks/StringBlock.test.ts new file mode 100644 index 0000000..dee7ceb --- /dev/null +++ b/test/content_blocks/StringBlock.test.ts @@ -0,0 +1,54 @@ +/* +Copyright 2022 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 {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; +import {StringBlock} from "../../src/content_blocks/StringBlock"; + +class TestStringBlock extends StringBlock { + public constructor(raw: any) { + super("TestBlock", raw as string); // lie to TS + } +} + +describe("StringBlock", () => { + it("should retain the block name", () => { + const block = new TestStringBlock("test"); + expect(block.name).toStrictEqual("TestBlock"); + }); + + it.each([null, undefined])("should reject null and undefined: %s", val => { + expect(() => new TestStringBlock(val)).toThrow( + new InvalidBlockError( + "TestBlock", + "Block value must be defined. Use a null-capable parser instead of passing such a value.", + ), + ); + }); + + it("should accept strings", () => { + const block = new TestStringBlock("test"); + expect(block.raw).toStrictEqual("test"); + }); + + it("should accept empty strings", () => { + const block = new TestStringBlock(""); + expect(block.raw).toStrictEqual(""); + }); + + it.each([42, true, {}, []])("should reject non-strings: '%s'", val => { + expect(() => new TestStringBlock(val)).toThrow(new InvalidBlockError("TestBlock", "should be a string value")); + }); +}); diff --git a/test/content_blocks/m/MarkupBlock.test.ts b/test/content_blocks/m/MarkupBlock.test.ts new file mode 100644 index 0000000..5694880 --- /dev/null +++ b/test/content_blocks/m/MarkupBlock.test.ts @@ -0,0 +1,212 @@ +/* +Copyright 2022 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 {InvalidBlockError} from "../../../src/content_blocks/InvalidBlockError"; +import {MarkupBlock} from "../../../src/content_blocks/m/MarkupBlock"; + +describe("MarkupBlock", () => { + it("should have a sensible block name", () => { + const block = new MarkupBlock([{body: "test"}]); + expect(block.name).toStrictEqual("m.markup"); + }); + + it.each([null, undefined])("should reject null and undefined: %s", val => { + expect(() => new MarkupBlock(val as any)).toThrow( + new InvalidBlockError( + "m.markup", + "Block value must be defined. Use a null-capable parser instead of passing such a value.", + ), + ); + }); + + it("should be able to identify the text and HTML representations", () => { + const block = new MarkupBlock([ + {body: "neither", mimetype: "text/fail"}, + {body: "text here", mimetype: "text/plain"}, + {body: "html here", mimetype: "text/html"}, + ]); + // noinspection DuplicatedCode + expect(block.text).toStrictEqual("text here"); + expect(block.html).toStrictEqual("html here"); + expect(block.representationErrors).toBeDefined(); + expect(block.representationErrors.size).toStrictEqual(0); + expect(block.representations).toBeDefined(); + expect(block.representations.length).toStrictEqual(3); + expect(block.representations).toStrictEqual(block.raw); + }); + + it("should detect plain text as representations without mimetypes", () => { + const block = new MarkupBlock([ + {body: "fail", mimetype: "text/fail"}, + {body: "text here"}, + {body: "html here", mimetype: "text/html"}, + ]); + // noinspection DuplicatedCode + expect(block.text).toStrictEqual("text here"); + expect(block.html).toStrictEqual("html here"); + expect(block.representationErrors).toBeDefined(); + expect(block.representationErrors.size).toStrictEqual(0); + expect(block.representations).toBeDefined(); + expect(block.representations.length).toStrictEqual(3); + expect(block.representations).toStrictEqual(block.raw); + }); + + it("should be able to handle missing text", () => { + const block = new MarkupBlock([ + {body: "fail", mimetype: "text/fail"}, + {body: "html here", mimetype: "text/html"}, + ]); + expect(block.text).toBeUndefined(); + expect(block.html).toStrictEqual("html here"); + expect(block.representationErrors).toBeDefined(); + expect(block.representationErrors.size).toStrictEqual(0); + expect(block.representations).toBeDefined(); + expect(block.representations.length).toStrictEqual(2); + expect(block.representations).toStrictEqual(block.raw); + }); + + it("should be able to handle missing HTML", () => { + const block = new MarkupBlock([ + {body: "fail", mimetype: "text/fail"}, + {body: "text here", mimetype: "text/plain"}, + ]); + expect(block.text).toStrictEqual("text here"); + expect(block.html).toBeUndefined(); + expect(block.representationErrors).toBeDefined(); + expect(block.representationErrors.size).toStrictEqual(0); + expect(block.representations).toBeDefined(); + expect(block.representations.length).toStrictEqual(2); + expect(block.representations).toStrictEqual(block.raw); + }); + + it("should be able to handle neither text or HTML", () => { + const block = new MarkupBlock([{body: "nothing of use", mimetype: "text/fail"}]); + expect(block.text).toBeUndefined(); + expect(block.html).toBeUndefined(); + expect(block.representationErrors).toBeDefined(); + expect(block.representationErrors.size).toStrictEqual(0); + expect(block.representations).toBeDefined(); + expect(block.representations.length).toStrictEqual(1); + expect(block.representations).toStrictEqual(block.raw); + }); + + it("should use the first text representation", () => { + let block = new MarkupBlock([{body: "text here"}, {body: "not this text", mimetype: "text/plain"}]); + // noinspection DuplicatedCode + expect(block.text).toStrictEqual("text here"); + expect(block.representationErrors).toBeDefined(); + expect(block.representationErrors.size).toStrictEqual(0); + expect(block.representations).toBeDefined(); + expect(block.representations.length).toStrictEqual(2); + expect(block.representations).toStrictEqual(block.raw); + + // The same test, but inverting the undefined mimetype for safety + block = new MarkupBlock([{body: "text here", mimetype: "text/plain"}, {body: "not this text"}]); + // noinspection DuplicatedCode + expect(block.text).toStrictEqual("text here"); + expect(block.representationErrors).toBeDefined(); + expect(block.representationErrors.size).toStrictEqual(0); + expect(block.representations).toBeDefined(); + expect(block.representations.length).toStrictEqual(2); + expect(block.representations).toStrictEqual(block.raw); + }); + + it("should handle having no representations provided", () => { + const block = new MarkupBlock([]); + expect(block.text).toBeUndefined(); + expect(block.html).toBeUndefined(); + expect(block.representationErrors).toBeDefined(); + expect(block.representationErrors.size).toStrictEqual(0); + expect(block.representations).toBeDefined(); + expect(block.representations.length).toStrictEqual(0); + expect(block.representations).toStrictEqual(block.raw); + }); + + it("should validate each representation has a defined body", () => { + const block = new MarkupBlock([{mimetype: "text/plain"} as any]); + // noinspection DuplicatedCode + expect(block.text).toBeUndefined(); + expect(block.html).toBeUndefined(); + expect(block.representationErrors).toBeDefined(); + expect(block.representationErrors.size).toStrictEqual(1); + expect(Array.from(block.representationErrors.entries())[0]).toMatchSnapshot(); + expect(block.representations).toBeDefined(); + expect(block.representations.length).toStrictEqual(0); + expect(block.representations).toStrictEqual(block.raw); + }); + + it("should validate each representation has a valid body", () => { + const block = new MarkupBlock([{body: true as any, mimetype: "text/plain"}]); + // noinspection DuplicatedCode + expect(block.text).toBeUndefined(); + expect(block.html).toBeUndefined(); + expect(block.representationErrors).toBeDefined(); + expect(block.representationErrors.size).toStrictEqual(1); + expect(Array.from(block.representationErrors.entries())[0]).toMatchSnapshot(); + expect(block.representations).toBeDefined(); + expect(block.representations.length).toStrictEqual(0); + expect(block.representations).toStrictEqual(block.raw); + }); + + it("should validate each representation has a valid mimetype", () => { + const block = new MarkupBlock([{body: "text here", mimetype: true as any}]); + // noinspection DuplicatedCode + expect(block.text).toBeUndefined(); + expect(block.html).toBeUndefined(); + expect(block.representationErrors).toBeDefined(); + expect(block.representationErrors.size).toStrictEqual(1); + expect(Array.from(block.representationErrors.entries())[0]).toMatchSnapshot(); + expect(block.representations).toBeDefined(); + expect(block.representations.length).toStrictEqual(0); + expect(block.representations).toStrictEqual(block.raw); + }); + + it("should validate each representation is an object", () => { + const block = new MarkupBlock([true as any]); + // noinspection DuplicatedCode + expect(block.text).toBeUndefined(); + expect(block.html).toBeUndefined(); + expect(block.representationErrors).toBeDefined(); + expect(block.representationErrors.size).toStrictEqual(1); + expect(Array.from(block.representationErrors.entries())[0]).toMatchSnapshot(); + expect(block.representations).toBeDefined(); + expect(block.representations.length).toStrictEqual(0); + expect(block.representations).toStrictEqual(block.raw); + }); + + it("should remove representations mid-array if they are invalid", () => { + const block = new MarkupBlock([ + {body: "valid text"}, + {body: "more valid text", mimetype: "text/html"}, + {body: "text here", mimetype: true as any}, + {body: "even more text", mimetype: "text/plain"}, + ]); + expect(block.text).toStrictEqual("valid text"); + expect(block.html).toStrictEqual("more valid text"); + expect(block.representationErrors).toBeDefined(); + expect(block.representationErrors.size).toStrictEqual(1); + expect(Array.from(block.representationErrors.entries())[0]).toMatchSnapshot(); + expect(block.representations).toBeDefined(); + expect(block.representations.length).toStrictEqual(3); + expect(block.representations).toStrictEqual(block.raw); + }); + + it.each([42, true, {}, "test"])("should reject non-markups: '%s'", val => { + expect(() => new MarkupBlock(val as any)).toThrow( + new InvalidBlockError("m.markup", "should be an array value"), + ); + }); +}); diff --git a/test/content_blocks/m/__snapshots__/MarkupBlock.test.ts.snap b/test/content_blocks/m/__snapshots__/MarkupBlock.test.ts.snap new file mode 100644 index 0000000..cb5b233 --- /dev/null +++ b/test/content_blocks/m/__snapshots__/MarkupBlock.test.ts.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MarkupBlock should remove representations mid-array if they are invalid 1`] = ` +[ + { + "index": 2, + "representation": { + "body": "text here", + "mimetype": true, + }, + }, + [Error: m.markup[2]: mimetype should be a non-null string, or undefined (field not required)], +] +`; + +exports[`MarkupBlock should validate each representation has a defined body 1`] = ` +[ + { + "index": 0, + "representation": { + "mimetype": "text/plain", + }, + }, + [Error: m.markup[0]: must have required property 'body'], +] +`; + +exports[`MarkupBlock should validate each representation has a valid body 1`] = ` +[ + { + "index": 0, + "representation": { + "body": true, + "mimetype": "text/plain", + }, + }, + [Error: m.markup[0]: body should be a non-null string and is required], +] +`; + +exports[`MarkupBlock should validate each representation has a valid mimetype 1`] = ` +[ + { + "index": 0, + "representation": { + "body": "text here", + "mimetype": true, + }, + }, + [Error: m.markup[0]: mimetype should be a non-null string, or undefined (field not required)], +] +`; + +exports[`MarkupBlock should validate each representation is an object 1`] = ` +[ + { + "index": 0, + "representation": true, + }, + [Error: m.markup[0]: must be object], +] +`; diff --git a/yarn.lock b/yarn.lock index 5a056a1..3fe4846 100644 --- a/yarn.lock +++ b/yarn.lock @@ -765,6 +765,21 @@ dependencies: "@types/yargs-parser" "*" +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@^8.11.2: + version "8.11.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.2.tgz#aecb20b50607acf2569b6382167b65a96008bb78" + integrity sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -1141,6 +1156,11 @@ expect@^29.0.0, expect@^29.3.1: jest-message-util "^29.3.1" jest-util "^29.3.1" +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -1751,6 +1771,11 @@ json-parse-even-better-errors@^2.3.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +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== + json5@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" @@ -1980,6 +2005,11 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + react-is@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" @@ -1990,6 +2020,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +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== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -2231,6 +2266,13 @@ update-browserslist-db@^1.0.9: escalade "^3.1.1" picocolors "^1.0.0" +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + v8-to-istanbul@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" From 952fe795008da19f139aa64ed3e9c997c2d74296 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 9 Dec 2022 14:37:37 -0700 Subject: [PATCH 03/22] Use an automated solution for index files --- .ctiignore | 5 + .husky/pre-commit | 2 +- package.json | 4 +- src/index.ts | 33 ++ src/v1-old/index.ts | 46 --- yarn.lock | 769 +++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 800 insertions(+), 59 deletions(-) create mode 100644 .ctiignore create mode 100644 src/index.ts delete mode 100644 src/v1-old/index.ts diff --git a/.ctiignore b/.ctiignore new file mode 100644 index 0000000..4332403 --- /dev/null +++ b/.ctiignore @@ -0,0 +1,5 @@ +{ + "src/AjvContainer.ts": "*", + "**/*.d.ts": "*", + "test/**": "*" +} diff --git a/.husky/pre-commit b/.husky/pre-commit index 2f53ba1..9c9401a 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -yarn format +yarn idx && yarn format diff --git a/package.json b/package.json index 7fe794c..41e3cc7 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "prepare": "husky install", "prepublishOnly": "yarn build", "clean": "rimraf lib", - "build": "yarn clean && tsc -p tsconfig.build.json", + "idx": "ctix single -p tsconfig.build.json --startAt src --output src/index.ts --overwrite --noBackup --useComment", + "build": "yarn clean && yarn idx && tsc -p tsconfig.build.json", "start": "tsc -p tsconfig.build.json -w", "test": "jest", "format": "prettier --config .prettierrc \"{src,test}/**/*.ts\" --write", @@ -27,6 +28,7 @@ "devDependencies": { "@types/jest": "^29.2.3", "@types/node": "^16", + "ctix": "^1.7.0", "husky": "^8.0.2", "jest": "^29.3.1", "prettier": "^2.8.0", diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..9bb74f3 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,33 @@ +// created from ctix + +// created from ctix + +export * from "./v1-old/interpreters/legacy/MRoomMessage"; +export * from "./v1-old/interpreters/modern/MMessage"; +export * from "./v1-old/interpreters/modern/MPoll"; +export * from "./content_blocks/m/MarkupBlock"; +export * from "./v1-old/events/EmoteEvent"; +export * from "./v1-old/events/ExtensibleEvent"; +export * from "./v1-old/events/message_types"; +export * from "./v1-old/events/MessageEvent"; +export * from "./v1-old/events/NoticeEvent"; +export * from "./v1-old/events/poll_types"; +export * from "./v1-old/events/PollEndEvent"; +export * from "./v1-old/events/PollResponseEvent"; +export * from "./v1-old/events/PollStartEvent"; +export * from "./v1-old/events/relationship_types"; +export * from "./v1-old/utility/events"; +export * from "./v1-old/utility/MessageMatchers"; +export * from "./content_blocks/ArrayBlock"; +export * from "./content_blocks/BaseBlock"; +export * from "./content_blocks/BooleanBlock"; +export * from "./content_blocks/IntegerBlock"; +export * from "./content_blocks/InvalidBlockError"; +export * from "./content_blocks/ObjectBlock"; +export * from "./content_blocks/StringBlock"; +export * from "./v1-old/ExtensibleEvents"; +export * from "./v1-old/InvalidEventError"; +export * from "./v1-old/IPartialEvent"; +export * from "./v1-old/NamespacedMap"; +export * from "./v1-old/NamespacedValue"; +export * from "./v1-old/types"; diff --git a/src/v1-old/index.ts b/src/v1-old/index.ts deleted file mode 100644 index d588c1f..0000000 --- a/src/v1-old/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2022 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. -*/ - -// Package-level stuff -export * from "./ExtensibleEvents"; -export * from "./IPartialEvent"; -export * from "./InvalidEventError"; -export * from "./NamespacedValue"; -export * from "./NamespacedMap"; // utility -export * from "./types"; - -// Utilities -export * from "./utility/MessageMatchers"; -export * from "./utility/events"; - -// Legacy interpreters -export * from "./interpreters/legacy/MRoomMessage"; - -// Modern (or not-legacy) interpreters -export * from "./interpreters/modern/MMessage"; -export * from "./interpreters/modern/MPoll"; - -// Event objects -export * from "./events/relationship_types"; -export * from "./events/ExtensibleEvent"; -export * from "./events/message_types"; -export * from "./events/MessageEvent"; -export * from "./events/EmoteEvent"; -export * from "./events/NoticeEvent"; -export * from "./events/poll_types"; -export * from "./events/PollStartEvent"; -export * from "./events/PollResponseEvent"; -export * from "./events/PollEndEvent"; diff --git a/yarn.lock b/yarn.lock index 3fe4846..9f8b765 100644 --- a/yarn.lock +++ b/yarn.lock @@ -647,6 +647,27 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + "@sinclair/typebox@^0.24.1": version "0.24.51" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" @@ -666,6 +687,16 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@ts-morph/common@~0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.17.0.tgz#de0d405df10857907469fef8d9363893b4163fd1" + integrity sha512-RMSSvSfs9kb0VzkvQ2NWobwnj7TxCA9vI/IjR9bDHqgAyVbu2T0DN4wiKVqomyDWqO7dPr/tErSfq7urQ1Q37g== + dependencies: + fast-glob "^3.2.11" + minimatch "^5.1.0" + mkdirp "^1.0.4" + path-browserify "^1.0.1" + "@types/babel__core@^7.1.14": version "7.1.16" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.16.tgz#bc12c74b7d65e82d29876b5d0baf5c625ac58702" @@ -765,6 +796,13 @@ dependencies: "@types/yargs-parser" "*" +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + ajv-errors@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-3.0.0.tgz#e54f299f3a3d30fe144161e5f0d8d51196c527bc" @@ -826,6 +864,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + babel-jest@^29.3.1: version "29.3.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.3.1.tgz#05c83e0d128cd48c453eea851482a38782249f44" @@ -891,6 +934,20 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -899,6 +956,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -935,11 +999,35 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camel-case@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -955,6 +1043,15 @@ caniuse-lite@^1.0.30001400: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz#ec1ec1cfb0a93a34a0600d37903853030520a4e5" integrity sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA== +capital-case@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" + integrity sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + upper-case-first "^2.0.2" + chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -964,7 +1061,7 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -972,6 +1069,24 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +change-case@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/change-case/-/change-case-4.1.2.tgz#fedfc5f136045e2398c0410ee441f95704641e12" + integrity sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A== + dependencies: + camel-case "^4.1.2" + capital-case "^1.0.4" + constant-case "^3.0.4" + dot-case "^3.0.4" + header-case "^2.0.4" + no-case "^3.0.4" + param-case "^3.0.4" + pascal-case "^3.1.2" + path-case "^3.0.4" + sentence-case "^3.0.4" + snake-case "^3.0.4" + tslib "^2.0.3" + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -987,6 +1102,25 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-progress@^3.11.2: + version "3.11.2" + resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.11.2.tgz#f8c89bd157e74f3f2c43bcfb3505670b4d48fc77" + integrity sha512-lCPoS6ncgX4+rJu5bS3F/iCz17kZ9MPZ6dpuTtI0KXKABkhyXIdYB3Inby1OpaGti3YlI3EeEkM9AuWpelJrVA== + dependencies: + string-width "^4.2.3" + +cli-spinners@^2.5.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.7.0.tgz#f815fd30b5f9eaac02db604c7a231ed7cb2f797a" + integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw== + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -996,11 +1130,21 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= +code-block-writer@^11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-11.0.3.tgz#9eec2993edfb79bfae845fbc093758c0a0b73b76" + integrity sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw== + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" @@ -1030,11 +1174,25 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^2.0.7: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +constant-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1" + integrity sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + upper-case "^2.0.2" + convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" @@ -1056,6 +1214,50 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +ctix@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ctix/-/ctix-1.7.0.tgz#9d1f21f70ce8addb8d3157454ebbded4070c1014" + integrity sha512-jsPE19HdB1duGEKdZjf32xKN7F5vJGUFbvkXj3r2GaMx6Sw095o/exCuRHrEQEUS9/TUPv4ZHpPN5LrrGoAQQA== + dependencies: + chalk "^4.1.2" + change-case "^4.1.2" + cli-progress "^3.11.2" + dayjs "^1.11.5" + fast-glob "^3.2.12" + fast-safe-stringify "^2.1.1" + find-up "^5.0.0" + ignore "^5.2.0" + is-relative "^1.0.0" + json5 "^2.2.1" + jsonc-parser "^3.2.0" + minimatch "^5.1.0" + minimist "^1.2.7" + my-easy-fp "^0.15.0" + my-node-fp "^0.8.1" + my-only-either "^1.3.0" + ora "^5.4.1" + parse-gitignore "^2.0.0" + pino "^8.6.1" + pino-pretty "^9.1.1" + prettier "^2.7.1" + source-map-support "^0.5.21" + ts-morph "^16.0.0" + tslib "^2.4.0" + type-fest "^2.19.0" + typescript "^4.8.4" + upper-case-first "^2.0.2" + yargs "^17.6.0" + +dateformat@^4.6.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" + integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== + +dayjs@^1.11.5: + version "1.11.7" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" + integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== + debug@^4.1.0, debug@^4.1.1: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" @@ -1073,6 +1275,13 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + dependencies: + clone "^1.0.2" + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -1083,6 +1292,14 @@ diff-sequences@^29.3.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e" integrity sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ== +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + electron-to-chromium@^1.4.251: version "1.4.284" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" @@ -1098,6 +1315,13 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -1125,6 +1349,16 @@ esprima@^4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -1156,16 +1390,49 @@ expect@^29.0.0, expect@^29.3.1: jest-message-util "^29.3.1" jest-util "^29.3.1" +fast-copy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.0.tgz#875ebf33b13948ae012b6e51d33da5e6e7571ab8" + integrity sha512-4HzS+9pQ5Yxtv13Lhs1Z1unMXamBdn5nA4bEi1abYpDNSpSp7ODYQ1KPMF6nTatfEzgH6/zPvXKU1zvHiUjWlA== + fast-deep-equal@^3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-glob@^3.2.11, fast-glob@^3.2.12: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-redact@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa" + integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw== + +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + +fastq@^1.6.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.14.0.tgz#107f69d7295b11e0fccc264e1fc6389f623731ce" + integrity sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg== + dependencies: + reusify "^1.0.4" + fb-watchman@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" @@ -1188,6 +1455,14 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1223,6 +1498,13 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + glob@^7.1.3, glob@^7.1.4: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" @@ -1235,6 +1517,17 @@ glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.0: + version "8.0.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" + integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -1262,6 +1555,22 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +header-case@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063" + integrity sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q== + dependencies: + capital-case "^1.0.4" + tslib "^2.0.3" + +help-me@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/help-me/-/help-me-4.1.0.tgz#c105e78ba490d6fcaa61a3d0cd06e0054554efab" + integrity sha512-5HMrkOks2j8Fpu2j5nTLhrBhT7VwHwELpqnSnx802ckofys5MO2SkLpgSz3dgNFHV7IYFX2igm5CM75SmuYidw== + dependencies: + glob "^8.0.0" + readable-stream "^3.6.0" + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -1277,6 +1586,16 @@ husky@^8.0.2: resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.2.tgz#5816a60db02650f1f22c8b69b928fd6bcd77a236" integrity sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg== +ieee754@^1.1.13, ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" + integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== + import-local@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6" @@ -1298,7 +1617,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1315,6 +1634,11 @@ is-core-module@^2.2.0: dependencies: has "^1.0.3" +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -1325,16 +1649,47 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== +is-glob@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-relative@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" + integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== + dependencies: + is-unc-path "^1.0.0" + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +is-unc-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" + integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== + dependencies: + unc-path-regex "^0.1.2" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -1748,6 +2103,11 @@ jest@^29.3.1: import-local "^3.0.2" jest-cli "^29.3.1" +joycon@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" + integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -1781,6 +2141,11 @@ json5@^2.2.1: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== +jsonc-parser@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" + integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -1803,11 +2168,33 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -1839,6 +2226,11 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" @@ -1859,16 +2251,63 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1, minimatch@^5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.1.tgz#6c9dffcf9927ff2a31e74b5af11adf8b9604b022" + integrity sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.6, minimist@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +my-easy-fp@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/my-easy-fp/-/my-easy-fp-0.14.0.tgz#7a348f1f41f87975f04b53a9fe9f3ae85f2e1f1d" + integrity sha512-y1olfATc31Akn7MzWy+he1JNqkJa8UWyDupe533GmG9q+fX1imiVpYiPmVhJnp9Bs/OsF/pPGG1p9DXzgiRAzA== + +my-easy-fp@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/my-easy-fp/-/my-easy-fp-0.15.0.tgz#13f6c25821d843054d10e079a15a47aca4c58f96" + integrity sha512-VyF9+ITFDNX1+F1+7rDv/eSeItGvOg10l7zEKp1ETg/raAbB5K4VfqANSL3mQn8W8GTgX2UidrrinwvMM1H34g== + +my-node-fp@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/my-node-fp/-/my-node-fp-0.8.1.tgz#e15c09d8fa2e5364b390bcaec57a53e33ca55f2f" + integrity sha512-KXxZrDBqDXww6SNhRLyIhTHnuPM316OJkwtaNWyVsMibky8I1CRhzFxPloL0vZ1HzrgeYYHE+QDx/dfnU0fUYg== + dependencies: + my-easy-fp "^0.14.0" + +my-only-either@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/my-only-either/-/my-only-either-1.3.0.tgz#b44d736d0252b7c744d5ef79e9a14a2636d3d18f" + integrity sha512-Ww01dhgmQLLHwEww6b0F5eyPMz0zfCYId4SsP8p7VPxtxPrh6v+fwORrBDYAHgOcVE2Bg9+wwztDKkSEbuxZog== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -1891,20 +2330,40 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -once@^1.3.0: +on-exit-leak-free@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4" + integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" -onetime@^5.1.2: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -1912,7 +2371,7 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.1.0: +p-limit@^3.0.2, p-limit@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== @@ -1926,11 +2385,31 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +param-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +parse-gitignore@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/parse-gitignore/-/parse-gitignore-2.0.0.tgz#81156b265115c507129f3faea067b8476da3b642" + integrity sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog== + parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -1941,6 +2420,27 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + +path-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/path-case/-/path-case-3.0.4.tgz#9168645334eb942658375c56f80b4c0cb5f82c6f" + integrity sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -1971,6 +2471,56 @@ picomatch@^2.0.4, picomatch@^2.2.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== +pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3" + integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA== + dependencies: + readable-stream "^4.0.0" + split2 "^4.0.0" + +pino-pretty@^9.1.1: + version "9.1.1" + resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-9.1.1.tgz#e7d64c1db98266ca428ab56567b844ba780cd0e1" + integrity sha512-iJrnjgR4FWQIXZkUF48oNgoRI9BpyMhaEmihonHeCnZ6F50ZHAS4YGfGBT/ZVNsPmd+hzkIPGzjKdY08+/yAXw== + dependencies: + colorette "^2.0.7" + dateformat "^4.6.3" + fast-copy "^3.0.0" + fast-safe-stringify "^2.1.1" + help-me "^4.0.1" + joycon "^3.1.1" + minimist "^1.2.6" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.0.0" + pump "^3.0.0" + readable-stream "^4.0.0" + secure-json-parse "^2.4.0" + sonic-boom "^3.0.0" + strip-json-comments "^3.1.1" + +pino-std-serializers@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.0.0.tgz#4c20928a1bafca122fdc2a7a4a171ca1c5f9c526" + integrity sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ== + +pino@^8.6.1: + version "8.7.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-8.7.0.tgz#58621608a3d8540ae643cdd9194cdd94130c78d9" + integrity sha512-l9sA5uPxmZzwydhMWUcm1gI0YxNnYl8MfSr2h8cwLvOAzQLBLewzF247h/vqHe3/tt6fgtXeG9wdjjoetdI/vA== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport v1.0.0 + pino-std-serializers "^6.0.0" + process-warning "^2.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^3.1.0" + thread-stream "^2.0.0" + pirates@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" @@ -1983,6 +2533,11 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +prettier@^2.7.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.1.tgz#4e1fd11c34e2421bc1da9aea9bd8127cd0a35efc" + integrity sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg== + prettier@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.0.tgz#c7df58393c9ba77d6fba3921ae01faf994fb9dc9" @@ -1997,6 +2552,16 @@ pretty-format@^29.0.0, pretty-format@^29.3.1: ansi-styles "^5.0.0" react-is "^18.0.0" +process-warning@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.1.0.tgz#1e60e3bfe8183033bbc1e702c2da74f099422d1a" + integrity sha512-9C20RLxrZU/rFnxWncDkuF6O999NdIf3E1ws4B0ZeY3sRVPzWBMsYDE2lxjxhiXxg464cQTgKUGm8/i6y2YGXg== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + prompts@^2.0.1: version "2.4.1" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.1.tgz#befd3b1195ba052f9fd2fde8a486c4e82ee77f61" @@ -2005,16 +2570,58 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + react-is@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.2.0.tgz#a7ef523d3b39e4962b0db1a1af22777b10eeca46" + integrity sha512-gJrBHsaI3lgBoGMW/jHZsQ/o/TIWiu5ENCJG1BB7fuCKzpFM8GaS2UoBVt9NO+oI+3FcrBNbUkl3ilDe09aY4A== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -2050,6 +2657,19 @@ resolve@^1.20.0: is-core-module "^2.2.0" path-parse "^1.0.6" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -2057,11 +2677,33 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-stable-stringify@^2.3.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.1.tgz#34694bd8a30575b7f94792aa51527551bd733d61" + integrity sha512-dVHE6bMtS/bnL2mwualjc6IxEv1F+OCUpA46pKUj6F8uDbUM0jCCulPqRNPSnWwGNKx5etqMjZYdXtrm5KJZGA== + +secure-json-parse@^2.4.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.6.0.tgz#95d89f84adf32d76ff7800e68a673b129fe918b0" + integrity sha512-B9osKohb6L+EZ6Kve3wHKfsAClzOC/iISA2vSuCe5Jx5NAKiwitfxx8ZKYapHXr0sYRj7UZInT7pLb3rp2Yx6A== + semver@7.x: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" @@ -2081,6 +2723,15 @@ semver@^7.3.5: dependencies: lru-cache "^6.0.0" +sentence-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-3.0.4.tgz#3645a7b8c117c787fde8702056225bb62a45131f" + integrity sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + upper-case-first "^2.0.2" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -2093,16 +2744,16 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +signal-exit@^3.0.2, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + signal-exit@^3.0.3: version "3.0.5" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f" integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ== -signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -2113,6 +2764,21 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +sonic-boom@^3.0.0, sonic-boom@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.2.1.tgz#972ceab831b5840a08a002fa95a672008bda1c38" + integrity sha512-iITeTHxy3B9FGu8aVdiDXUVAcHMF9Ss0cCsAOo2HfCrmVGT3/DT5oYaeu0M/YKZDlKTvChEyPq0zI9Hf33EX6A== + dependencies: + atomic-sleep "^1.0.0" + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" @@ -2121,6 +2787,14 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" +source-map-support@^0.5.21: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map@^0.5.0: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -2131,6 +2805,11 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +split2@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" + integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -2160,6 +2839,13 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -2212,6 +2898,13 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +thread-stream@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.2.0.tgz#310c03a253f729094ce5d4638ef5186dfa80a9e8" + integrity sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ== + dependencies: + real-require "^0.2.0" + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -2243,6 +2936,19 @@ ts-jest@^29.0.3: semver "7.x" yargs-parser "^21.0.1" +ts-morph@^16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-16.0.0.tgz#35caca7c286dd70e09e5f72af47536bf3b6a27af" + integrity sha512-jGNF0GVpFj0orFw55LTsQxVYEUOCWBAbR5Ls7fTYE5pQsbW18ssTb/6UXx/GYAEjS+DQTp8VoTw0vqYMiaaQuw== + dependencies: + "@ts-morph/common" "~0.17.0" + code-block-writer "^11.0.3" + +tslib@^2.0.3, tslib@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" + integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -2253,11 +2959,26 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-fest@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + +typescript@^4.8.4: + version "4.9.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" + integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== + typescript@^4.9.3: version "4.9.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db" integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA== +unc-path-regex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + integrity sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg== + update-browserslist-db@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" @@ -2266,6 +2987,20 @@ update-browserslist-db@^1.0.9: escalade "^3.1.1" picocolors "^1.0.0" +upper-case-first@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324" + integrity sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg== + dependencies: + tslib "^2.0.3" + +upper-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-2.0.2.tgz#d89810823faab1df1549b7d97a76f8662bae6f7a" + integrity sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg== + dependencies: + tslib "^2.0.3" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -2273,6 +3008,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + v8-to-istanbul@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" @@ -2289,6 +3029,13 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -2333,7 +3080,7 @@ yargs-parser@^21.0.1, yargs-parser@^21.1.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^17.3.1: +yargs@^17.3.1, yargs@^17.6.0: version "17.6.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== From c929cbe95c5ac35344ff92f765d0d8d6ff44e3fa Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 9 Dec 2022 15:15:13 -0700 Subject: [PATCH 04/22] Reinstate NamespacedValue --- package.json | 2 +- src/{v1-old => }/NamespacedValue.ts | 0 src/content_blocks/m/MarkupBlock.ts | 3 +++ src/index.ts | 3 ++- src/types.ts | 20 +++++++++++++++++++ src/v1-old/ExtensibleEvents.ts | 4 ++-- src/v1-old/NamespacedMap.ts | 4 ++-- src/v1-old/events/MessageEvent.ts | 3 ++- src/v1-old/events/PollResponseEvent.ts | 2 +- src/v1-old/events/PollStartEvent.ts | 2 +- src/v1-old/events/message_types.ts | 2 +- src/v1-old/events/poll_types.ts | 2 +- src/v1-old/events/relationship_types.ts | 2 +- .../interpreters/legacy/MRoomMessage.ts | 4 ++-- src/v1-old/interpreters/modern/MMessage.ts | 2 +- src/v1-old/interpreters/modern/MPoll.ts | 2 +- src/v1-old/types.ts | 8 ++------ src/v1-old/utility/events.ts | 2 +- test/{v1-old => }/NamespacedValue.test.ts | 2 +- test/v1-old/ExtensibleEvents.test.ts | 5 ++--- test/v1-old/NamespacedMap.test.ts | 5 +++-- test/v1-old/events/EmoteEvent.test.ts | 2 +- test/v1-old/events/ExtensibleEvent.test.ts | 2 +- test/v1-old/events/MessageEvent.test.ts | 2 +- test/v1-old/events/NoticeEvent.test.ts | 2 +- test/v1-old/events/PollEndEvent.test.ts | 2 +- test/v1-old/events/PollResponseEvent.test.ts | 2 +- test/v1-old/events/PollStartEvent.test.ts | 2 +- .../interpreters/legacy/MRoomMessage.test.ts | 2 +- .../interpreters/modern/MMessage.test.ts | 2 +- test/v1-old/interpreters/modern/MPoll.test.ts | 2 +- test/v1-old/utility/MessageMatchers.test.ts | 2 +- test/v1-old/utility/events.test.ts | 2 +- 33 files changed, 62 insertions(+), 41 deletions(-) rename src/{v1-old => }/NamespacedValue.ts (100%) create mode 100644 src/types.ts rename test/{v1-old => }/NamespacedValue.test.ts (99%) diff --git a/package.json b/package.json index 41e3cc7..e91ae7c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "prepare": "husky install", "prepublishOnly": "yarn build", "clean": "rimraf lib", - "idx": "ctix single -p tsconfig.build.json --startAt src --output src/index.ts --overwrite --noBackup --useComment", + "idx": "ctix single -p tsconfig.build.json --startAt src --output src/index.ts --overwrite --noBackup --useComment && yarn format", "build": "yarn clean && yarn idx && tsc -p tsconfig.build.json", "start": "tsc -p tsconfig.build.json -w", "test": "jest", diff --git a/src/v1-old/NamespacedValue.ts b/src/NamespacedValue.ts similarity index 100% rename from src/v1-old/NamespacedValue.ts rename to src/NamespacedValue.ts diff --git a/src/content_blocks/m/MarkupBlock.ts b/src/content_blocks/m/MarkupBlock.ts index c5ce4c1..1444c3e 100644 --- a/src/content_blocks/m/MarkupBlock.ts +++ b/src/content_blocks/m/MarkupBlock.ts @@ -18,6 +18,7 @@ import {ArrayBlock} from "../ArrayBlock"; import {Schema} from "ajv"; import {AjvContainer} from "../../AjvContainer"; import {InvalidBlockError} from "../InvalidBlockError"; +import {UnstableValue} from "../../NamespacedValue"; /** * A representation of text within a markup block. @@ -37,6 +38,8 @@ export class MarkupBlock extends ArrayBlock { public static readonly schema = ArrayBlock.schema; public static readonly validateFn = ArrayBlock.validateFn; + public static readonly type = new UnstableValue("m.markup", "org.matrix.msc1767.markup"); + /** * Schema definition for the markup representation (list item) specifically. * diff --git a/src/index.ts b/src/index.ts index 9bb74f3..6e668f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,5 +29,6 @@ export * from "./v1-old/ExtensibleEvents"; export * from "./v1-old/InvalidEventError"; export * from "./v1-old/IPartialEvent"; export * from "./v1-old/NamespacedMap"; -export * from "./v1-old/NamespacedValue"; export * from "./v1-old/types"; +export * from "./NamespacedValue"; +export * from "./types"; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..974bbf0 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,20 @@ +/* +Copyright 2021 - 2022 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. +*/ + +/** + * Represents an optional type: can either be T or a falsy value. + */ +export type Optional = T | null | undefined; diff --git a/src/v1-old/ExtensibleEvents.ts b/src/v1-old/ExtensibleEvents.ts index c9ae05f..1a6b876 100644 --- a/src/v1-old/ExtensibleEvents.ts +++ b/src/v1-old/ExtensibleEvents.ts @@ -16,8 +16,8 @@ limitations under the License. import {IPartialEvent} from "./IPartialEvent"; import {ExtensibleEvent} from "./events/ExtensibleEvent"; -import {Optional} from "./types"; -import {NamespacedValue} from "./NamespacedValue"; +import {Optional} from "../types"; +import {NamespacedValue} from "../NamespacedValue"; import {NamespacedMap} from "./NamespacedMap"; import {InvalidEventError} from "./InvalidEventError"; import {LEGACY_M_ROOM_MESSAGE, parseMRoomMessage} from "./interpreters/legacy/MRoomMessage"; diff --git a/src/v1-old/NamespacedMap.ts b/src/v1-old/NamespacedMap.ts index aa2b411..62fe5d7 100644 --- a/src/v1-old/NamespacedMap.ts +++ b/src/v1-old/NamespacedMap.ts @@ -1,5 +1,5 @@ -import {NamespacedValue} from "./NamespacedValue"; -import {Optional} from "./types"; +import {NamespacedValue} from "../NamespacedValue"; +import {Optional} from "../types"; type NS = NamespacedValue; diff --git a/src/v1-old/events/MessageEvent.ts b/src/v1-old/events/MessageEvent.ts index a5306b6..f21a6f5 100644 --- a/src/v1-old/events/MessageEvent.ts +++ b/src/v1-old/events/MessageEvent.ts @@ -16,7 +16,7 @@ limitations under the License. import {ExtensibleEvent} from "./ExtensibleEvent"; import {IPartialEvent} from "../IPartialEvent"; -import {isOptionalAString, isProvided, Optional} from "../types"; +import {isOptionalAString, isProvided} from "../types"; import {InvalidEventError} from "../InvalidEventError"; import { IMessageRendering, @@ -28,6 +28,7 @@ import { M_TEXT, } from "./message_types"; import {EventType, isEventTypeSame} from "../utility/events"; +import {Optional} from "../../types"; /** * Represents a message event. Message events are the simplest form of event with diff --git a/src/v1-old/events/PollResponseEvent.ts b/src/v1-old/events/PollResponseEvent.ts index 6b0df70..7671c8d 100644 --- a/src/v1-old/events/PollResponseEvent.ts +++ b/src/v1-old/events/PollResponseEvent.ts @@ -21,7 +21,7 @@ import {InvalidEventError} from "../InvalidEventError"; import {PollStartEvent} from "./PollStartEvent"; import {REFERENCE_RELATION} from "./relationship_types"; import {EventType, isEventTypeSame} from "../utility/events"; -import {Optional} from "../types"; +import {Optional} from "../../types"; /** * Represents a poll response event. diff --git a/src/v1-old/events/PollStartEvent.ts b/src/v1-old/events/PollStartEvent.ts index bf7c7c6..8156918 100644 --- a/src/v1-old/events/PollStartEvent.ts +++ b/src/v1-old/events/PollStartEvent.ts @@ -27,7 +27,7 @@ import {IPartialEvent} from "../IPartialEvent"; import {MessageEvent} from "./MessageEvent"; import {M_TEXT} from "./message_types"; import {InvalidEventError} from "../InvalidEventError"; -import {NamespacedValue} from "../NamespacedValue"; +import {NamespacedValue} from "../../NamespacedValue"; import {EventType, isEventTypeSame} from "../utility/events"; import {ExtensibleEvent} from "./ExtensibleEvent"; import {isNumberFinite} from "../types"; diff --git a/src/v1-old/events/message_types.ts b/src/v1-old/events/message_types.ts index 06acad3..fdc2507 100644 --- a/src/v1-old/events/message_types.ts +++ b/src/v1-old/events/message_types.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {UnstableValue} from "../NamespacedValue"; +import {UnstableValue} from "../../NamespacedValue"; import {EitherAnd, OptionalPartial} from "../types"; /** diff --git a/src/v1-old/events/poll_types.ts b/src/v1-old/events/poll_types.ts index 0c1d96a..f217fd7 100644 --- a/src/v1-old/events/poll_types.ts +++ b/src/v1-old/events/poll_types.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {UnstableValue} from "../NamespacedValue"; +import {UnstableValue} from "../../NamespacedValue"; import {EitherAnd, TSNamespace} from "../types"; import {M_MESSAGE_EVENT_CONTENT} from "./message_types"; import {REFERENCE_RELATION, RELATES_TO_RELATIONSHIP} from "./relationship_types"; diff --git a/src/v1-old/events/relationship_types.ts b/src/v1-old/events/relationship_types.ts index 6017c31..c1793de 100644 --- a/src/v1-old/events/relationship_types.ts +++ b/src/v1-old/events/relationship_types.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {NamespacedValue} from "../NamespacedValue"; +import {NamespacedValue} from "../../NamespacedValue"; import {DefaultNever, TSNamespace} from "../types"; /** diff --git a/src/v1-old/interpreters/legacy/MRoomMessage.ts b/src/v1-old/interpreters/legacy/MRoomMessage.ts index 4e87805..662515f 100644 --- a/src/v1-old/interpreters/legacy/MRoomMessage.ts +++ b/src/v1-old/interpreters/legacy/MRoomMessage.ts @@ -15,12 +15,12 @@ limitations under the License. */ import {IPartialEvent} from "../../IPartialEvent"; -import {Optional} from "../../types"; +import {Optional} from "../../../types"; import {ExtensibleEvent} from "../../events/ExtensibleEvent"; import {MessageEvent} from "../../events/MessageEvent"; import {NoticeEvent} from "../../events/NoticeEvent"; import {EmoteEvent} from "../../events/EmoteEvent"; -import {NamespacedValue} from "../../NamespacedValue"; +import {NamespacedValue} from "../../../NamespacedValue"; import {M_HTML, M_MESSAGE, M_MESSAGE_EVENT_CONTENT, M_TEXT} from "../../events/message_types"; export const LEGACY_M_ROOM_MESSAGE = new NamespacedValue("m.room.message"); diff --git a/src/v1-old/interpreters/modern/MMessage.ts b/src/v1-old/interpreters/modern/MMessage.ts index 1845155..61f50b7 100644 --- a/src/v1-old/interpreters/modern/MMessage.ts +++ b/src/v1-old/interpreters/modern/MMessage.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {IPartialEvent} from "../../IPartialEvent"; -import {Optional} from "../../types"; +import {Optional} from "../../../types"; import {MessageEvent} from "../../events/MessageEvent"; import {M_EMOTE, M_MESSAGE_EVENT_CONTENT, M_NOTICE} from "../../events/message_types"; import {EmoteEvent} from "../../events/EmoteEvent"; diff --git a/src/v1-old/interpreters/modern/MPoll.ts b/src/v1-old/interpreters/modern/MPoll.ts index 8c9c75c..74204fb 100644 --- a/src/v1-old/interpreters/modern/MPoll.ts +++ b/src/v1-old/interpreters/modern/MPoll.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {IPartialEvent} from "../../IPartialEvent"; -import {Optional} from "../../types"; +import {Optional} from "../../../types"; import { M_POLL_END, M_POLL_END_EVENT_CONTENT, diff --git a/src/v1-old/types.ts b/src/v1-old/types.ts index da5a791..dc12cdd 100644 --- a/src/v1-old/types.ts +++ b/src/v1-old/types.ts @@ -14,12 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {NamespacedValue} from "./NamespacedValue"; - -/** - * Represents an optional type: can either be T or a falsy value. - */ -export type Optional = T | null | undefined; +import {NamespacedValue} from "../NamespacedValue"; +import {Optional} from "../types"; /** * Applies the same behaviour as `Partial`, but using `Optional` instead. diff --git a/src/v1-old/utility/events.ts b/src/v1-old/utility/events.ts index 3bc966c..6132e8e 100644 --- a/src/v1-old/utility/events.ts +++ b/src/v1-old/utility/events.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {NamespacedValue} from "../NamespacedValue"; +import {NamespacedValue} from "../../NamespacedValue"; import {isOptionalAString} from "../types"; /** diff --git a/test/v1-old/NamespacedValue.test.ts b/test/NamespacedValue.test.ts similarity index 99% rename from test/v1-old/NamespacedValue.test.ts rename to test/NamespacedValue.test.ts index c1165ac..91d638c 100644 --- a/test/v1-old/NamespacedValue.test.ts +++ b/test/NamespacedValue.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {NamespacedValue, UnstableValue} from "../../src/v1-old"; +import {NamespacedValue, UnstableValue} from "../src"; export const STABLE_VALUE = "org.example.stable"; export const UNSTABLE_VALUE = "org.example.unstable"; diff --git a/test/v1-old/ExtensibleEvents.test.ts b/test/v1-old/ExtensibleEvents.test.ts index 76664c4..9f3bef6 100644 --- a/test/v1-old/ExtensibleEvents.test.ts +++ b/test/v1-old/ExtensibleEvents.test.ts @@ -36,13 +36,12 @@ import { M_TEXT, MessageEvent, NoticeEvent, - Optional, PollEndEvent, PollResponseEvent, PollStartEvent, REFERENCE_RELATION, - UnstableValue, -} from "../../src/v1-old"; +} from "../../src"; +import {Optional, UnstableValue} from "../../src"; describe("ExtensibleEvents", () => { afterEach(() => { diff --git a/test/v1-old/NamespacedMap.test.ts b/test/v1-old/NamespacedMap.test.ts index a111b7a..8f467b3 100644 --- a/test/v1-old/NamespacedMap.test.ts +++ b/test/v1-old/NamespacedMap.test.ts @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {NamespacedMap, NamespacedValue, UnstableValue} from "../../src/v1-old"; -import {STABLE_VALUE, UNSTABLE_VALUE} from "./NamespacedValue.test"; +import {NamespacedMap} from "../../src"; +import {NamespacedValue, UnstableValue} from "../../src"; +import {STABLE_VALUE, UNSTABLE_VALUE} from "../NamespacedValue.test"; type TestableNamespacedMap = {internalMap: Map} & NamespacedMap; function asTestableMap(map: NamespacedMap): TestableNamespacedMap { diff --git a/test/v1-old/events/EmoteEvent.test.ts b/test/v1-old/events/EmoteEvent.test.ts index ea1779b..a7a55b7 100644 --- a/test/v1-old/events/EmoteEvent.test.ts +++ b/test/v1-old/events/EmoteEvent.test.ts @@ -26,7 +26,7 @@ import { M_NOTICE, M_NOTICE_EVENT_CONTENT, M_TEXT, -} from "../../../src/v1-old"; +} from "../../../src"; describe("EmoteEvent", () => { it("should parse m.text", () => { diff --git a/test/v1-old/events/ExtensibleEvent.test.ts b/test/v1-old/events/ExtensibleEvent.test.ts index 2ad278e..d9cf361 100644 --- a/test/v1-old/events/ExtensibleEvent.test.ts +++ b/test/v1-old/events/ExtensibleEvent.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {EventType, ExtensibleEvent, IPartialEvent} from "../../../src/v1-old"; +import {EventType, ExtensibleEvent, IPartialEvent} from "../../../src"; class MockEvent extends ExtensibleEvent { public constructor(wireEvent: IPartialEvent) { diff --git a/test/v1-old/events/MessageEvent.test.ts b/test/v1-old/events/MessageEvent.test.ts index 824324b..9e909ec 100644 --- a/test/v1-old/events/MessageEvent.test.ts +++ b/test/v1-old/events/MessageEvent.test.ts @@ -26,7 +26,7 @@ import { M_NOTICE_EVENT_CONTENT, M_TEXT, MessageEvent, -} from "../../../src/v1-old"; +} from "../../../src"; describe("MessageEvent", () => { it("should parse m.text", () => { diff --git a/test/v1-old/events/NoticeEvent.test.ts b/test/v1-old/events/NoticeEvent.test.ts index 97845e7..f4d5a0d 100644 --- a/test/v1-old/events/NoticeEvent.test.ts +++ b/test/v1-old/events/NoticeEvent.test.ts @@ -26,7 +26,7 @@ import { M_NOTICE_EVENT_CONTENT, M_TEXT, NoticeEvent, -} from "../../../src/v1-old"; +} from "../../../src"; describe("NoticeEvent", () => { it("should parse m.text", () => { diff --git a/test/v1-old/events/PollEndEvent.test.ts b/test/v1-old/events/PollEndEvent.test.ts index 38bbe77..e8ccf34 100644 --- a/test/v1-old/events/PollEndEvent.test.ts +++ b/test/v1-old/events/PollEndEvent.test.ts @@ -22,7 +22,7 @@ import { M_TEXT, PollEndEvent, REFERENCE_RELATION, -} from "../../../src/v1-old"; +} from "../../../src"; describe("PollEndEvent", () => { // Note: throughout these tests we don't really bother testing that diff --git a/test/v1-old/events/PollResponseEvent.test.ts b/test/v1-old/events/PollResponseEvent.test.ts index cb6decd..6666fc6 100644 --- a/test/v1-old/events/PollResponseEvent.test.ts +++ b/test/v1-old/events/PollResponseEvent.test.ts @@ -25,7 +25,7 @@ import { PollResponseEvent, PollStartEvent, REFERENCE_RELATION, -} from "../../../src/v1-old"; +} from "../../../src"; const SAMPLE_POLL = new PollStartEvent({ type: M_POLL_START.name, diff --git a/test/v1-old/events/PollStartEvent.test.ts b/test/v1-old/events/PollStartEvent.test.ts index f3fd333..50dc1ad 100644 --- a/test/v1-old/events/PollStartEvent.test.ts +++ b/test/v1-old/events/PollStartEvent.test.ts @@ -25,7 +25,7 @@ import { POLL_ANSWER, PollAnswerSubevent, PollStartEvent, -} from "../../../src/v1-old"; +} from "../../../src"; describe("PollAnswerSubevent", () => { // Note: throughout these tests we don't really bother testing that diff --git a/test/v1-old/interpreters/legacy/MRoomMessage.test.ts b/test/v1-old/interpreters/legacy/MRoomMessage.test.ts index 39ab135..265900a 100644 --- a/test/v1-old/interpreters/legacy/MRoomMessage.test.ts +++ b/test/v1-old/interpreters/legacy/MRoomMessage.test.ts @@ -26,7 +26,7 @@ import { MessageEvent, NoticeEvent, parseMRoomMessage, -} from "../../../../src/v1-old"; +} from "../../../../src"; describe("parseMRoomMessage", () => { it("should return an unmodified MessageEvent when using extensible events", () => { diff --git a/test/v1-old/interpreters/modern/MMessage.test.ts b/test/v1-old/interpreters/modern/MMessage.test.ts index f283f38..92fb50e 100644 --- a/test/v1-old/interpreters/modern/MMessage.test.ts +++ b/test/v1-old/interpreters/modern/MMessage.test.ts @@ -25,7 +25,7 @@ import { M_TEXT, NoticeEvent, parseMMessage, -} from "../../../../src/v1-old"; +} from "../../../../src"; describe("parseMMessage", () => { it("should return an unmodified MessageEvent", () => { diff --git a/test/v1-old/interpreters/modern/MPoll.test.ts b/test/v1-old/interpreters/modern/MPoll.test.ts index 1bfe17c..fd351fd 100644 --- a/test/v1-old/interpreters/modern/MPoll.test.ts +++ b/test/v1-old/interpreters/modern/MPoll.test.ts @@ -29,7 +29,7 @@ import { PollResponseEvent, PollStartEvent, REFERENCE_RELATION, -} from "../../../../src/v1-old"; +} from "../../../../src"; describe("parseMPoll", () => { it("should return an unmodified PollStartEvent", () => { diff --git a/test/v1-old/utility/MessageMatchers.test.ts b/test/v1-old/utility/MessageMatchers.test.ts index 48a4e34..cde3390 100644 --- a/test/v1-old/utility/MessageMatchers.test.ts +++ b/test/v1-old/utility/MessageMatchers.test.ts @@ -24,7 +24,7 @@ import { M_MESSAGE_EVENT_CONTENT, M_NOTICE, M_TEXT, -} from "../../../src/v1-old"; +} from "../../../src"; describe("isEventLike", () => { it("should match legacy text", () => { diff --git a/test/v1-old/utility/events.test.ts b/test/v1-old/utility/events.test.ts index d31cb6b..c9a48b5 100644 --- a/test/v1-old/utility/events.test.ts +++ b/test/v1-old/utility/events.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {isEventTypeSame, NamespacedValue} from "../../../src/v1-old"; +import {isEventTypeSame, NamespacedValue} from "../../../src"; describe("isEventTypeSame", () => { it("should match string and string", () => { From 02f99baa5b6c35a6083e0db11f39412c2d0e58c2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 9 Dec 2022 16:08:47 -0700 Subject: [PATCH 05/22] Define an m.notice content block type --- src/AjvContainer.ts | 64 ++++++++++++++- src/content_blocks/m/MarkupBlock.ts | 2 +- src/content_blocks/m/NoticeBlock.ts | 52 +++++++++++++ src/index.ts | 1 + src/types.ts | 5 ++ src/v1-old/events/message_types.ts | 3 +- src/v1-old/events/poll_types.ts | 3 +- src/v1-old/types.ts | 5 -- src/v1-old/utility/MessageMatchers.ts | 2 +- test/AjvContainer.test.ts | 38 +++++++++ test/__snapshots__/AjvContainer.test.ts.snap | 82 ++++++++++++++++++++ test/content_blocks/m/NoticeBlock.test.ts | 68 ++++++++++++++++ 12 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 src/content_blocks/m/NoticeBlock.ts create mode 100644 test/AjvContainer.test.ts create mode 100644 test/__snapshots__/AjvContainer.test.ts.snap create mode 100644 test/content_blocks/m/NoticeBlock.test.ts diff --git a/src/AjvContainer.ts b/src/AjvContainer.ts index 3481f15..e28677e 100644 --- a/src/AjvContainer.ts +++ b/src/AjvContainer.ts @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Ajv from "ajv"; +import Ajv, {Schema, SchemaObject} from "ajv"; import AjvErrors from "ajv-errors"; +import {NamespacedValue} from "./NamespacedValue"; export class AjvContainer { public static readonly ajv = new Ajv({ @@ -29,4 +30,65 @@ export class AjvContainer { /* istanbul ignore next */ // noinspection JSUnusedLocalSymbols private constructor() {} + + /** + * Creates a JSON Schema representation of the EitherAnd<> TypeScript type. + * @param ns The namespace to use in the EitherAnd<> type. + * @param schema The schema to use as a value type for the namespace options. + * @returns The EitherAnd<> type as a JSON Schema. + */ + public static eitherAnd( + ns: NamespacedValue, + schema: Schema, + ): {anyOf: SchemaObject[]; errorMessage: string} { + // Dev note: ajv currently doesn't have a useful type for this stuff, but ideally it'd be smart enough to + // have an "anyOf" type we can return. + // Also note that we don't use oneOf: we manually construct it through a Type A, or Type B, or Type A+B list. + if (!ns.altName) { + throw new Error("Cannot create an EitherAnd<> JSON schema type without both stable and unstable values"); + } + return { + errorMessage: `schema does not apply to ${ns.stable} or ${ns.unstable}`, + anyOf: [ + { + type: "object", + properties: { + [ns.name]: schema, + }, + required: [ns.name], + errorMessage: { + properties: { + [ns.name]: `${ns.name} is required`, + }, + }, + }, + { + type: "object", + properties: { + [ns.altName]: schema, + }, + required: [ns.altName], + errorMessage: { + properties: { + [ns.altName]: `${ns.altName} is required`, + }, + }, + }, + { + type: "object", + properties: { + [ns.name]: schema, + [ns.altName]: schema, + }, + required: [ns.name, ns.altName], + errorMessage: { + properties: { + [ns.name]: `${ns.name} is required`, + [ns.altName]: `${ns.altName} is required`, + }, + }, + }, + ], + }; + } } diff --git a/src/content_blocks/m/MarkupBlock.ts b/src/content_blocks/m/MarkupBlock.ts index 1444c3e..2c1e5f0 100644 --- a/src/content_blocks/m/MarkupBlock.ts +++ b/src/content_blocks/m/MarkupBlock.ts @@ -90,7 +90,7 @@ export class MarkupBlock extends ArrayBlock { * @param raw The block's value. */ public constructor(raw: MarkupRepresentation[]) { - super("m.markup", raw); + super(MarkupBlock.type.stable!, raw); this.raw = raw.filter((r, i) => { const bool = MarkupBlock.representationValidateFn(r); if (!bool) { diff --git a/src/content_blocks/m/NoticeBlock.ts b/src/content_blocks/m/NoticeBlock.ts new file mode 100644 index 0000000..d590a38 --- /dev/null +++ b/src/content_blocks/m/NoticeBlock.ts @@ -0,0 +1,52 @@ +/* +Copyright 2022 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 {MarkupBlock, MarkupRepresentation} from "./MarkupBlock"; +import {ObjectBlock} from "../ObjectBlock"; +import {EitherAnd} from "../../types"; +import {Schema} from "ajv"; +import {AjvContainer} from "../../AjvContainer"; +import {UnstableValue} from "../../NamespacedValue"; +import {InvalidBlockError} from "../InvalidBlockError"; + +type Primary = {[MarkupBlock.type.name]: MarkupRepresentation[]}; +type Secondary = {[MarkupBlock.type.altName]: MarkupRepresentation[]}; +type Value = EitherAnd; + +/** + * A "notice" block, or a block meant to represent that the surrounding event can + * be rendered as a notice. Contains a MarkupBlock internally. + * @module Matrix Content Blocks + * @see MarkupBlock + */ +export class NoticeBlock extends ObjectBlock { + public static readonly schema: Schema = AjvContainer.eitherAnd(MarkupBlock.type, MarkupBlock.schema); + + public static readonly validateFn = AjvContainer.ajv.compile(NoticeBlock.schema); + + public static readonly type = new UnstableValue("m.notice", "org.matrix.msc1767.notice"); + + public constructor(raw: Value) { + super(NoticeBlock.type.stable!, raw); + if (!NoticeBlock.validateFn(raw)) { + throw new InvalidBlockError(this.name, NoticeBlock.validateFn.errors); + } + } + + public get markup(): MarkupBlock { + return new MarkupBlock(MarkupBlock.type.findIn(this.raw)!); + } +} diff --git a/src/index.ts b/src/index.ts index 6e668f9..004c871 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export * from "./v1-old/interpreters/legacy/MRoomMessage"; export * from "./v1-old/interpreters/modern/MMessage"; export * from "./v1-old/interpreters/modern/MPoll"; export * from "./content_blocks/m/MarkupBlock"; +export * from "./content_blocks/m/NoticeBlock"; export * from "./v1-old/events/EmoteEvent"; export * from "./v1-old/events/ExtensibleEvent"; export * from "./v1-old/events/message_types"; diff --git a/src/types.ts b/src/types.ts index 974bbf0..6429d86 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,3 +18,8 @@ limitations under the License. * Represents an optional type: can either be T or a falsy value. */ export type Optional = T | null | undefined; + +/** + * Represents either just T1, just T2, or T1 and T2 mixed. + */ +export type EitherAnd = (T1 & T2) | T1 | T2; diff --git a/src/v1-old/events/message_types.ts b/src/v1-old/events/message_types.ts index fdc2507..d0be7f2 100644 --- a/src/v1-old/events/message_types.ts +++ b/src/v1-old/events/message_types.ts @@ -15,7 +15,8 @@ limitations under the License. */ import {UnstableValue} from "../../NamespacedValue"; -import {EitherAnd, OptionalPartial} from "../types"; +import {OptionalPartial} from "../types"; +import {EitherAnd} from "../../types"; /** * The namespaced value for m.message diff --git a/src/v1-old/events/poll_types.ts b/src/v1-old/events/poll_types.ts index f217fd7..8dcbfa7 100644 --- a/src/v1-old/events/poll_types.ts +++ b/src/v1-old/events/poll_types.ts @@ -15,9 +15,10 @@ limitations under the License. */ import {UnstableValue} from "../../NamespacedValue"; -import {EitherAnd, TSNamespace} from "../types"; +import {TSNamespace} from "../types"; import {M_MESSAGE_EVENT_CONTENT} from "./message_types"; import {REFERENCE_RELATION, RELATES_TO_RELATIONSHIP} from "./relationship_types"; +import {EitherAnd} from "../../types"; /** * Identifier for a disclosed poll. diff --git a/src/v1-old/types.ts b/src/v1-old/types.ts index dc12cdd..fac4c98 100644 --- a/src/v1-old/types.ts +++ b/src/v1-old/types.ts @@ -52,11 +52,6 @@ export function isNumberFinite(n: unknown): n is number { return Number.isFinite(n); } -/** - * Represents either just T1, just T2, or T1 and T2 mixed. - */ -export type EitherAnd = (T1 & T2) | T1 | T2; - /** * Represents the stable and unstable values of a given namespace. */ diff --git a/src/v1-old/utility/MessageMatchers.ts b/src/v1-old/utility/MessageMatchers.ts index f0fd751..64a96a2 100644 --- a/src/v1-old/utility/MessageMatchers.ts +++ b/src/v1-old/utility/MessageMatchers.ts @@ -16,7 +16,7 @@ limitations under the License. import {IPartialEvent} from "../IPartialEvent"; import {IPartialLegacyContent} from "../interpreters/legacy/MRoomMessage"; -import {EitherAnd} from "../types"; +import {EitherAnd} from "../../types"; import {M_EMOTE, M_MESSAGE, M_MESSAGE_EVENT_CONTENT, M_NOTICE} from "../events/message_types"; /** diff --git a/test/AjvContainer.test.ts b/test/AjvContainer.test.ts new file mode 100644 index 0000000..7274122 --- /dev/null +++ b/test/AjvContainer.test.ts @@ -0,0 +1,38 @@ +/* +Copyright 2022 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 {AjvContainer} from "../src/AjvContainer"; +import {NamespacedValue} from "../src"; + +describe("AjvContainer", () => { + describe("eitherAnd", () => { + it("should reject a namespaced value missing an altName", () => { + expect(() => { + AjvContainer.eitherAnd(new NamespacedValue("stable", null), {type: "object"}); + }).toThrow( + new Error("Cannot create an EitherAnd<> JSON schema type without both stable and unstable values"), + ); + }); + + it("should generate an appropriate schema type", () => { + const result = AjvContainer.eitherAnd(new NamespacedValue("stable", "unstable"), { + type: "object", + properties: {test: {type: "string"}}, + }); + expect(result).toMatchSnapshot(); + }); + }); +}); diff --git a/test/__snapshots__/AjvContainer.test.ts.snap b/test/__snapshots__/AjvContainer.test.ts.snap new file mode 100644 index 0000000..b8b39fe --- /dev/null +++ b/test/__snapshots__/AjvContainer.test.ts.snap @@ -0,0 +1,82 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AjvContainer eitherAnd should generate an appropriate schema type 1`] = ` +{ + "anyOf": [ + { + "errorMessage": { + "properties": { + "stable": "stable is required", + }, + }, + "properties": { + "stable": { + "properties": { + "test": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "required": [ + "stable", + ], + "type": "object", + }, + { + "errorMessage": { + "properties": { + "unstable": "unstable is required", + }, + }, + "properties": { + "unstable": { + "properties": { + "test": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "required": [ + "unstable", + ], + "type": "object", + }, + { + "errorMessage": { + "properties": { + "stable": "stable is required", + "unstable": "unstable is required", + }, + }, + "properties": { + "stable": { + "properties": { + "test": { + "type": "string", + }, + }, + "type": "object", + }, + "unstable": { + "properties": { + "test": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "required": [ + "stable", + "unstable", + ], + "type": "object", + }, + ], + "errorMessage": "schema does not apply to stable or unstable", +} +`; diff --git a/test/content_blocks/m/NoticeBlock.test.ts b/test/content_blocks/m/NoticeBlock.test.ts new file mode 100644 index 0000000..b385d7a --- /dev/null +++ b/test/content_blocks/m/NoticeBlock.test.ts @@ -0,0 +1,68 @@ +/* +Copyright 2022 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 {InvalidBlockError} from "../../../src/content_blocks/InvalidBlockError"; +import {NoticeBlock} from "../../../src/content_blocks/m/NoticeBlock"; + +describe("NoticeBlock", () => { + it("should have a sensible block name", () => { + const block = new NoticeBlock({"m.markup": [{body: "test"}]}); + expect(block.name).toStrictEqual("m.notice"); + }); + + it.each([null, undefined])("should reject null and undefined: %s", val => { + expect(() => new NoticeBlock(val as any)).toThrow( + new InvalidBlockError( + "m.notice", + "Block value must be defined. Use a null-capable parser instead of passing such a value.", + ), + ); + }); + + it("should locate a stable markup block", () => { + const block = new NoticeBlock({"m.markup": [{body: "test"}]}); + expect(block.markup).toBeDefined(); + expect(block.markup.text).toStrictEqual("test"); + }); + + it("should locate an unstable markup block", () => { + const block = new NoticeBlock({"org.matrix.msc1767.markup": [{body: "test"}]}); + expect(block.markup).toBeDefined(); + expect(block.markup.text).toStrictEqual("test"); + }); + + it("should prefer the unstable markup block if both are provided", () => { + // Dev note: this test will need updating when extensible events becomes stable + const block = new NoticeBlock({ + "m.markup": [{body: "stable text"}], + "org.matrix.msc1767.markup": [{body: "unstable text"}], + }); + expect(block.markup).toBeDefined(); + expect(block.markup.text).toStrictEqual("unstable text"); + }); + + it("should error if a markup block is not present", () => { + expect(() => new NoticeBlock({no_block: true} as any)).toThrow( + new InvalidBlockError("m.notice", "schema does not apply to m.markup or org.matrix.msc1767.markup"), + ); + }); + + it.each([42, true, [], "test"])("should reject non-notices: '%s'", val => { + expect(() => new NoticeBlock(val as any)).toThrow( + new InvalidBlockError("m.notice", "should be an object value"), + ); + }); +}); From ee48139d9c1daa2b86a250c3b0dcf13d93d74f5c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 9 Dec 2022 17:27:49 -0700 Subject: [PATCH 06/22] Add an m.emote block --- src/content_blocks/m/EmoteBlock.ts | 52 +++++++++++++++++++ src/index.ts | 1 + test/content_blocks/m/EmoteBlock.test.ts | 66 ++++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 src/content_blocks/m/EmoteBlock.ts create mode 100644 test/content_blocks/m/EmoteBlock.test.ts diff --git a/src/content_blocks/m/EmoteBlock.ts b/src/content_blocks/m/EmoteBlock.ts new file mode 100644 index 0000000..3a80391 --- /dev/null +++ b/src/content_blocks/m/EmoteBlock.ts @@ -0,0 +1,52 @@ +/* +Copyright 2022 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 {MarkupBlock, MarkupRepresentation} from "./MarkupBlock"; +import {ObjectBlock} from "../ObjectBlock"; +import {EitherAnd} from "../../types"; +import {Schema} from "ajv"; +import {AjvContainer} from "../../AjvContainer"; +import {UnstableValue} from "../../NamespacedValue"; +import {InvalidBlockError} from "../InvalidBlockError"; + +type Primary = {[MarkupBlock.type.name]: MarkupRepresentation[]}; +type Secondary = {[MarkupBlock.type.altName]: MarkupRepresentation[]}; +type Value = EitherAnd; + +/** + * An "emote" block, or a block meant to represent that the surrounding event can + * be rendered as an emote. Contains a MarkupBlock internally. + * @module Matrix Content Blocks + * @see MarkupBlock + */ +export class EmoteBlock extends ObjectBlock { + public static readonly schema: Schema = AjvContainer.eitherAnd(MarkupBlock.type, MarkupBlock.schema); + + public static readonly validateFn = AjvContainer.ajv.compile(EmoteBlock.schema); + + public static readonly type = new UnstableValue("m.emote", "org.matrix.msc1767.emote"); + + public constructor(raw: Value) { + super(EmoteBlock.type.stable!, raw); + if (!EmoteBlock.validateFn(raw)) { + throw new InvalidBlockError(this.name, EmoteBlock.validateFn.errors); + } + } + + public get markup(): MarkupBlock { + return new MarkupBlock(MarkupBlock.type.findIn(this.raw)!); + } +} diff --git a/src/index.ts b/src/index.ts index 004c871..f4e09b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export * from "./v1-old/interpreters/legacy/MRoomMessage"; export * from "./v1-old/interpreters/modern/MMessage"; export * from "./v1-old/interpreters/modern/MPoll"; +export * from "./content_blocks/m/EmoteBlock"; export * from "./content_blocks/m/MarkupBlock"; export * from "./content_blocks/m/NoticeBlock"; export * from "./v1-old/events/EmoteEvent"; diff --git a/test/content_blocks/m/EmoteBlock.test.ts b/test/content_blocks/m/EmoteBlock.test.ts new file mode 100644 index 0000000..2084ba5 --- /dev/null +++ b/test/content_blocks/m/EmoteBlock.test.ts @@ -0,0 +1,66 @@ +/* +Copyright 2022 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 {InvalidBlockError} from "../../../src/content_blocks/InvalidBlockError"; +import {EmoteBlock} from "../../../src/content_blocks/m/EmoteBlock"; + +describe("EmoteBlock", () => { + it("should have a sensible block name", () => { + const block = new EmoteBlock({"m.markup": [{body: "test"}]}); + expect(block.name).toStrictEqual("m.emote"); + }); + + it.each([null, undefined])("should reject null and undefined: %s", val => { + expect(() => new EmoteBlock(val as any)).toThrow( + new InvalidBlockError( + "m.emote", + "Block value must be defined. Use a null-capable parser instead of passing such a value.", + ), + ); + }); + + it("should locate a stable markup block", () => { + const block = new EmoteBlock({"m.markup": [{body: "test"}]}); + expect(block.markup).toBeDefined(); + expect(block.markup.text).toStrictEqual("test"); + }); + + it("should locate an unstable markup block", () => { + const block = new EmoteBlock({"org.matrix.msc1767.markup": [{body: "test"}]}); + expect(block.markup).toBeDefined(); + expect(block.markup.text).toStrictEqual("test"); + }); + + it("should prefer the unstable markup block if both are provided", () => { + // Dev note: this test will need updating when extensible events becomes stable + const block = new EmoteBlock({ + "m.markup": [{body: "stable text"}], + "org.matrix.msc1767.markup": [{body: "unstable text"}], + }); + expect(block.markup).toBeDefined(); + expect(block.markup.text).toStrictEqual("unstable text"); + }); + + it("should error if a markup block is not present", () => { + expect(() => new EmoteBlock({no_block: true} as any)).toThrow( + new InvalidBlockError("m.emote", "schema does not apply to m.markup or org.matrix.msc1767.markup"), + ); + }); + + it.each([42, true, [], "test"])("should reject non-emotes: '%s'", val => { + expect(() => new EmoteBlock(val as any)).toThrow(new InvalidBlockError("m.emote", "should be an object value")); + }); +}); From 737ff22c142e56ea321554ea6d785f8401295b0e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Dec 2022 18:47:26 -0700 Subject: [PATCH 07/22] Add a RoomEvent base type --- src/{v1-old => events}/InvalidEventError.ts | 13 +- src/events/RoomEvent.ts | 163 ++++++++++ src/events/types_wire.d.ts | 53 ++++ src/index.ts | 3 +- src/v1-old/ExtensibleEvents.ts | 2 +- src/v1-old/events/MessageEvent.ts | 9 +- src/v1-old/events/PollEndEvent.ts | 4 +- src/v1-old/events/PollResponseEvent.ts | 4 +- src/v1-old/events/PollStartEvent.ts | 10 +- test/events/InvalidEventError.test.ts | 51 ++++ test/events/RoomEvent.test.ts | 302 +++++++++++++++++++ test/v1-old/ExtensibleEvents.test.ts | 2 +- test/v1-old/events/EmoteEvent.test.ts | 10 +- test/v1-old/events/MessageEvent.test.ts | 8 +- test/v1-old/events/NoticeEvent.test.ts | 10 +- test/v1-old/events/PollEndEvent.test.ts | 6 +- test/v1-old/events/PollResponseEvent.test.ts | 6 +- test/v1-old/events/PollStartEvent.test.ts | 18 +- 18 files changed, 636 insertions(+), 38 deletions(-) rename src/{v1-old => events}/InvalidEventError.ts (62%) create mode 100644 src/events/RoomEvent.ts create mode 100644 src/events/types_wire.d.ts create mode 100644 test/events/InvalidEventError.test.ts create mode 100644 test/events/RoomEvent.test.ts diff --git a/src/v1-old/InvalidEventError.ts b/src/events/InvalidEventError.ts similarity index 62% rename from src/v1-old/InvalidEventError.ts rename to src/events/InvalidEventError.ts index d4415fe..4395c42 100644 --- a/src/v1-old/InvalidEventError.ts +++ b/src/events/InvalidEventError.ts @@ -14,11 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {ErrorObject} from "ajv"; + /** * Thrown when an event is unforgivably unparsable. + * @module Events */ export class InvalidEventError extends Error { - public constructor(message: string) { - super(message); + public constructor(eventName: string, message: string | ErrorObject[] | null | undefined) { + super( + `${eventName}: ${ + typeof message === "string" + ? message + : message?.map(m => (m.message ? m.message : JSON.stringify(m))).join(", ") ?? "Validation failed" + }`, + ); } } diff --git a/src/events/RoomEvent.ts b/src/events/RoomEvent.ts new file mode 100644 index 0000000..e281167 --- /dev/null +++ b/src/events/RoomEvent.ts @@ -0,0 +1,163 @@ +/* +Copyright 2022 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 {EventWire} from "./types_wire"; +import {Schema} from "ajv"; +import {AjvContainer} from "../AjvContainer"; +import {InvalidEventError} from "./InvalidEventError"; + +/** + * Minimum representation of a room (timeline or state) event in Matrix, for + * consumption by a parser. + * @module Events + */ +export abstract class RoomEvent> { + public static readonly schema: Schema = { + type: "object", + properties: { + room_id: { + type: "string", + minLength: 4, // sigil + colon + characters, in theory + pattern: "^!.+:.+$", + nullable: false, + }, + event_id: { + type: "string", + minLength: 2, // sigil + characters, in theory + pattern: "^\\$.+$", // they are opaque strings + nullable: false, + }, + type: { + type: "string", + nullable: false, + }, + state_key: { + type: "string", + nullable: false, + }, + sender: { + type: "string", + minLength: 4, // sigil + colon + characters, in theory + pattern: "^@.+:.+$", + nullable: false, + }, + content: { + type: "object", + nullable: false, + additionalProperties: true, + }, + origin_server_ts: { + type: "integer", + // Ideally we'd specify our 2^56 limit here, but it's a bit too + // weird for JSON Schema. + nullable: false, + }, + unsigned: { + type: "object", + nullable: false, + additionalProperties: true, + }, + }, + required: ["room_id", "event_id", "type", "sender", "content", "origin_server_ts"], + errorMessage: { + properties: { + room_id: "The room ID should be a string prefixed with `!` and contain a `:`, and is required", + event_id: "The event ID should be a string prefixed with `$`, and is required", + type: "The event type should be a string of zero or more characters, and is required", + state_key: "The state key should be a string of zero or more characters", + sender: "The sender should be a string prefixed with `@` and contain a `:`, and is required", + content: "The event content should at least be a defined object, and is required", + origin_server_ts: "The event timestamp should be a number, and is required", + unsigned: "The event's unsigned content should be a defined object", + }, + }, + }; + + public static readonly validateFn = AjvContainer.ajv.compile(RoomEvent.schema); + + /** + * Creates a new MatrixEvent, validating the event object itself. Implementations of + * this abstract class should only need to validate the content object rather than the + * whole event schema. + * @param name The name of the event. Used for debugging. + * @param raw The raw event itself. + * @protected + */ + protected constructor(public readonly name: string, public readonly raw: EventWire.RoomEvent) { + if (raw === null || raw === undefined) { + throw new InvalidEventError( + this.name, + "Event object must be defined. Use a null-capable parser instead of passing such a value.", + ); + } + if (!RoomEvent.validateFn(raw)) { + throw new InvalidEventError(this.name, RoomEvent.validateFn.errors); + } + } + + /** + * The room ID this event was sent in. + */ + public get roomId(): string { + return this.raw.room_id; + } + + /** + * The event ID this event was sent as. + */ + public get eventId(): string { + return this.raw.event_id; + } + + /** + * The raw, unparsed, content of this event. It is recommended to use the getters + * on the event object instead to retrieve relevant parts of the event, such as + * message text or image details. + */ + public get content(): Content { + return this.raw.content; + } + + /** + * The type of this event. + */ + public get type(): string { + return this.raw.type; + } + + /** + * The sender (user ID) of this event. + */ + public get sender(): string { + return this.raw.sender; + } + + /** + * The state key for this event, if present. Note that an empty string is a + * valid state key: check for undefined to determine presence of a state key. + */ + public get stateKey(): string | undefined { + return this.raw.state_key; + } + + /** + * The reported timestamp this event was sent at. Note that this is supplied + * by the sender, and will be zero if negative. + */ + public get timestamp(): number { + return this.raw.origin_server_ts < 0 ? 0 : this.raw.origin_server_ts; + } +} diff --git a/src/events/types_wire.d.ts b/src/events/types_wire.d.ts new file mode 100644 index 0000000..66087d9 --- /dev/null +++ b/src/events/types_wire.d.ts @@ -0,0 +1,53 @@ +/* +Copyright 2022 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 {ContentBlockWire} from "../content_blocks/types_wire"; + +/** + * The wire types for Matrix events. + * @module Events + */ +export module EventWire { + /** + * A Matrix event. Also called a ClientEvent by the Matrix Specification. + * @module Events + */ + interface RoomEvent { + room_id: string; + event_id: string; + type: string; + state_key?: string; + sender: string; + content: Content; + origin_server_ts: number; + unsigned?: object; + } + + /** + * A simple `content` schema for content block-supporting events. + * @module Events + */ + type BlockBasedContent = { + [k: string]: ContentBlockWire.Value; + }; + + /** + * An event's specific `content`, preventing unexplained extensibility at the + * type system level. + * @module Events + */ + type BlockSpecificContent = Blocks & {[k: string]: never}; +} diff --git a/src/index.ts b/src/index.ts index f4e09b4..44acc5e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,8 +27,9 @@ export * from "./content_blocks/IntegerBlock"; export * from "./content_blocks/InvalidBlockError"; export * from "./content_blocks/ObjectBlock"; export * from "./content_blocks/StringBlock"; +export * from "./events/InvalidEventError"; +export * from "./events/RoomEvent"; export * from "./v1-old/ExtensibleEvents"; -export * from "./v1-old/InvalidEventError"; export * from "./v1-old/IPartialEvent"; export * from "./v1-old/NamespacedMap"; export * from "./v1-old/types"; diff --git a/src/v1-old/ExtensibleEvents.ts b/src/v1-old/ExtensibleEvents.ts index 1a6b876..77f2254 100644 --- a/src/v1-old/ExtensibleEvents.ts +++ b/src/v1-old/ExtensibleEvents.ts @@ -19,7 +19,7 @@ import {ExtensibleEvent} from "./events/ExtensibleEvent"; import {Optional} from "../types"; import {NamespacedValue} from "../NamespacedValue"; import {NamespacedMap} from "./NamespacedMap"; -import {InvalidEventError} from "./InvalidEventError"; +import {InvalidEventError} from "../events/InvalidEventError"; import {LEGACY_M_ROOM_MESSAGE, parseMRoomMessage} from "./interpreters/legacy/MRoomMessage"; import {parseMMessage} from "./interpreters/modern/MMessage"; import {M_EMOTE, M_MESSAGE, M_NOTICE} from "./events/message_types"; diff --git a/src/v1-old/events/MessageEvent.ts b/src/v1-old/events/MessageEvent.ts index f21a6f5..6764512 100644 --- a/src/v1-old/events/MessageEvent.ts +++ b/src/v1-old/events/MessageEvent.ts @@ -17,7 +17,7 @@ limitations under the License. import {ExtensibleEvent} from "./ExtensibleEvent"; import {IPartialEvent} from "../IPartialEvent"; import {isOptionalAString, isProvided} from "../types"; -import {InvalidEventError} from "../InvalidEventError"; +import {InvalidEventError} from "../../events/InvalidEventError"; import { IMessageRendering, M_EMOTE, @@ -70,12 +70,13 @@ export class MessageEvent extends ExtensibleEvent { const mhtml = M_HTML.findIn(this.wireContent); if (isProvided(mmessage)) { if (!Array.isArray(mmessage)) { - throw new InvalidEventError("m.message contents must be an array"); + throw new InvalidEventError("MessageEventLegacy", "m.message contents must be an array"); } const text = mmessage.find(r => !isProvided(r.mimetype) || r.mimetype === "text/plain"); const html = mmessage.find(r => r.mimetype === "text/html"); - if (!text) throw new InvalidEventError("m.message is missing a plain text representation"); + if (!text) + throw new InvalidEventError("MessageEventLegacy", "m.message is missing a plain text representation"); this.text = text.body; this.html = html?.body; @@ -88,7 +89,7 @@ export class MessageEvent extends ExtensibleEvent { this.renderings.push({body: this.html, mimetype: "text/html"}); } } else { - throw new InvalidEventError("Missing textual representation for event"); + throw new InvalidEventError("MessageEventLegacy", "Missing textual representation for event"); } } diff --git a/src/v1-old/events/PollEndEvent.ts b/src/v1-old/events/PollEndEvent.ts index 92c1fb7..9519042 100644 --- a/src/v1-old/events/PollEndEvent.ts +++ b/src/v1-old/events/PollEndEvent.ts @@ -16,7 +16,7 @@ limitations under the License. import {M_POLL_END, M_POLL_END_EVENT_CONTENT} from "./poll_types"; import {IPartialEvent} from "../IPartialEvent"; -import {InvalidEventError} from "../InvalidEventError"; +import {InvalidEventError} from "../../events/InvalidEventError"; import {REFERENCE_RELATION} from "./relationship_types"; import {MessageEvent} from "./MessageEvent"; import {M_TEXT} from "./message_types"; @@ -48,7 +48,7 @@ export class PollEndEvent extends ExtensibleEvent { const rel = this.wireContent["m.relates_to"]; // noinspection SuspiciousTypeOfGuard if (!REFERENCE_RELATION.matches(rel?.rel_type) || typeof rel.event_id !== "string") { - throw new InvalidEventError("Relationship must be a reference to an event"); + throw new InvalidEventError("PollEndEventLegacy", "Relationship must be a reference to an event"); } this.pollEventId = rel.event_id; diff --git a/src/v1-old/events/PollResponseEvent.ts b/src/v1-old/events/PollResponseEvent.ts index 7671c8d..e9c6b13 100644 --- a/src/v1-old/events/PollResponseEvent.ts +++ b/src/v1-old/events/PollResponseEvent.ts @@ -17,7 +17,7 @@ limitations under the License. import {ExtensibleEvent} from "./ExtensibleEvent"; import {M_POLL_RESPONSE, M_POLL_RESPONSE_EVENT_CONTENT, M_POLL_RESPONSE_SUBTYPE} from "./poll_types"; import {IPartialEvent} from "../IPartialEvent"; -import {InvalidEventError} from "../InvalidEventError"; +import {InvalidEventError} from "../../events/InvalidEventError"; import {PollStartEvent} from "./PollStartEvent"; import {REFERENCE_RELATION} from "./relationship_types"; import {EventType, isEventTypeSame} from "../utility/events"; @@ -63,7 +63,7 @@ export class PollResponseEvent extends ExtensibleEvent const poll = M_POLL_START.findIn(this.wireContent); if (!poll?.question) { - throw new InvalidEventError("A question is required"); + throw new InvalidEventError("PollStartEventLegacy", "A question is required"); } this.question = new MessageEvent({type: "org.matrix.sdk.poll.question", content: poll.question}); @@ -139,7 +139,7 @@ export class PollStartEvent extends ExtensibleEvent this.maxSelections = isNumberFinite(poll.max_selections) && poll.max_selections > 0 ? poll.max_selections : 1; if (!Array.isArray(poll.answers)) { - throw new InvalidEventError("Poll answers must be an array"); + throw new InvalidEventError("PollStartEventLegacy", "Poll answers must be an array"); } const answers = poll.answers.slice(0, 20).map( a => @@ -149,7 +149,7 @@ export class PollStartEvent extends ExtensibleEvent }), ); if (answers.length <= 0) { - throw new InvalidEventError("No answers available"); + throw new InvalidEventError("PollStartEventLegacy", "No answers available"); } this.answers = answers; } diff --git a/test/events/InvalidEventError.test.ts b/test/events/InvalidEventError.test.ts new file mode 100644 index 0000000..fb9bd6c --- /dev/null +++ b/test/events/InvalidEventError.test.ts @@ -0,0 +1,51 @@ +/* +Copyright 2022 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 {InvalidEventError} from "../../src/events/InvalidEventError"; + +describe("InvalidEventError", () => { + it("should use the block name in the error", () => { + const err = new InvalidEventError("org.example.test", "my message"); + expect(err.message).toEqual("org.example.test: my message"); + }); + + it("should use error objects if given", () => { + const err = new InvalidEventError("org.example.test", [ + { + message: "test message", + keyword: "test", + params: [], + instancePath: "#/unused", + schemaPath: "#/unused", + }, + { + // message: "test message", // this one has no message + keyword: "test", + params: [], + instancePath: "#/unused", + schemaPath: "#/unused", + }, + ]); + expect(err.message).toEqual( + 'org.example.test: test message, {"keyword":"test","params":[],"instancePath":"#/unused","schemaPath":"#/unused"}', + ); + }); + + it.each([null, undefined])("should use a default message when none is provided: '%s'", val => { + const err = new InvalidEventError("org.example.test", val); + expect(err.message).toEqual("org.example.test: Validation failed"); + }); +}); diff --git a/test/events/RoomEvent.test.ts b/test/events/RoomEvent.test.ts new file mode 100644 index 0000000..f62a25e --- /dev/null +++ b/test/events/RoomEvent.test.ts @@ -0,0 +1,302 @@ +/* +Copyright 2022 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 {InvalidEventError, RoomEvent} from "../../src"; +import {EventWire} from "../../src/events/types_wire"; + +class TestEvent extends RoomEvent { + public constructor(raw: any) { + super("TestEvent", raw); // lie to TS + } +} + +describe("RoomEvent", () => { + testSharedRoomEventInputs("TestEvent", x => new TestEvent(x), {hello: "world"}); +}); + +/** + * Runs all the validation tests which can be shared across all event type classes. + * @param eventName The event name, for error matching. + * @param factory The factory to create a RoomEvent. + * @param safeContentInput The content to supply on an event during a non-throwing + * test. This should be the minimum to construct the event, not ideal conditions. + */ +export function testSharedRoomEventInputs( + eventName: string, + factory: (raw: EventWire.RoomEvent) => RoomEvent, + safeContentInput: EventWire.BlockBasedContent, +) { + describe("internal", () => { + it("should have valid inputs", () => { + expect(eventName).toBeDefined(); + expect(typeof eventName).toStrictEqual("string"); + expect(eventName.length).toBeGreaterThan(0); + + expect(factory).toBeDefined(); + expect(typeof factory).toStrictEqual("function"); + + expect(safeContentInput).toBeDefined(); + expect(typeof safeContentInput).toStrictEqual("object"); + }); + + it("should have a passing factory", () => { + const ev = factory({ + room_id: "!test:example.org", + event_id: "$event", + type: "org.example.test_event", + sender: "@user:example.org", + content: safeContentInput, + origin_server_ts: 1670894499800, + }); + expect(ev).toBeDefined(); + // noinspection SuspiciousTypeOfGuard + expect(ev instanceof RoomEvent).toStrictEqual(true); + }); + }); + + it.each([null, undefined])("should throw if given null or undefined: '%s'", val => { + expect(() => factory(val as any)).toThrow( + new InvalidEventError( + eventName, + "Event object must be defined. Use a null-capable parser instead of passing such a value.", + ), + ); + }); + + it.each([undefined, "", "with content"])("should handle valid event structures with state key of '%s'", skey => { + const raw: EventWire.RoomEvent = { + room_id: "!test:example.org", + event_id: "$event", + type: "org.example.test_event", + state_key: skey, + sender: "@user:example.org", + content: safeContentInput, + origin_server_ts: 1670894499800, + }; + const ev = factory(raw); + expect(ev).toBeDefined(); + expect(ev.raw).toStrictEqual(raw); + expect(ev.roomId).toStrictEqual(raw.room_id); + expect(ev.eventId).toStrictEqual(raw.event_id); + expect(ev.type).toStrictEqual(raw.type); + expect(ev.stateKey).toStrictEqual(raw.state_key); + expect(ev.sender).toStrictEqual(raw.sender); + expect(ev.timestamp).toStrictEqual(raw.origin_server_ts); + expect(ev.content).toStrictEqual(raw.content); + }); + + it("should retain the event name", () => { + const raw: EventWire.RoomEvent = { + room_id: "!test:example.org", + event_id: "$event", + type: "org.example.test_event", + sender: "@user:example.org", + content: safeContentInput, + origin_server_ts: 1670894499800, + }; + const ev = factory(raw); + expect(ev.name).toStrictEqual(eventName); + }); + + it.each([ + "", + "!wrong", + ":wrong!example", + "!:example", + "!:", + "!wrong:", + "$wrong", + "$wrong:example", + "#wrong", + "#wrong:example", + "@wrong", + "@wrong:example", + true, + null, + 42, + {hello: "world"}, + [1, 2, 3], + ])("should reject invalid room IDs: '%s'", val => { + expect(() => + factory({ + room_id: val as any, + event_id: "$event", + type: "org.example.test_event", + sender: "@user:example.org", + content: safeContentInput, + origin_server_ts: 1670894499800, + }), + ).toThrow( + new InvalidEventError( + eventName, + "The room ID should be a string prefixed with `!` and contain a `:`, and is required", + ), + ); + }); + + it.each([ + "", + "wrong$", + "!wrong", + "!wrong:example", + "#wrong", + "#wrong:example", + "@wrong", + "@wrong:example", + true, + null, + 42, + {hello: "world"}, + [1, 2, 3], + ])("should reject invalid event IDs: '%s'", val => { + expect(() => + factory({ + room_id: "!room:example.org", + event_id: val as any, + type: "org.example.test_event", + sender: "@user:example.org", + content: safeContentInput, + origin_server_ts: 1670894499800, + }), + ).toThrow( + new InvalidEventError(eventName, "The event ID should be a string prefixed with `$`, and is required"), + ); + }); + + it.each([true, null, 42, {hello: "world"}, [1, 2, 3]])("should reject invalid event types: '%s'", val => { + expect(() => + factory({ + room_id: "!room:example.org", + event_id: "$event", + type: val as any, + sender: "@user:example.org", + content: safeContentInput, + origin_server_ts: 1670894499800, + }), + ).toThrow( + new InvalidEventError( + eventName, + "The event type should be a string of zero or more characters, and is required", + ), + ); + }); + + it.each([true, null, 42, {hello: "world"}, [1, 2, 3]])("should reject invalid state keys: '%s'", val => { + expect(() => + factory({ + room_id: "!room:example.org", + event_id: "$event", + type: "org.example.test_event", + state_key: val as any, + sender: "@user:example.org", + content: safeContentInput, + origin_server_ts: 1670894499800, + }), + ).toThrow(new InvalidEventError(eventName, "The state key should be a string of zero or more characters")); + }); + + it.each([ + "", + "@wrong", + ":wrong@example", + "@:example", + "@:", + "@wrong:", + "$wrong", + "$wrong:example", + "#wrong", + "#wrong:example", + "!wrong", + "!wrong:example", + true, + null, + 42, + {hello: "world"}, + [1, 2, 3], + ])("should reject invalid senders (user IDs): '%s'", val => { + expect(() => + factory({ + room_id: "!room:example.org", + event_id: "$event", + type: "org.example.test_event", + sender: val as any, + content: safeContentInput, + origin_server_ts: 1670894499800, + }), + ).toThrow( + new InvalidEventError( + eventName, + "The sender should be a string prefixed with `@` and contain a `:`, and is required", + ), + ); + }); + + it.each([true, null, "test", 42.3, {hello: "world"}, [1, 2, 3]])("should reject invalid timestamps: '%s'", val => { + expect(() => + factory({ + room_id: "!room:example.org", + event_id: "$event", + type: "org.example.test_event", + sender: "@user:example.org", + content: safeContentInput, + origin_server_ts: val as any, + }), + ).toThrow(new InvalidEventError(eventName, "The event timestamp should be a number, and is required")); + }); + + it.each([true, null, "test", 42, [1, 2, 3]])("should reject invalid unsigned content: '%s'", val => { + expect(() => + factory({ + room_id: "!room:example.org", + event_id: "$event", + type: "org.example.test_event", + sender: "@user:example.org", + content: safeContentInput, + origin_server_ts: 1670894499800, + unsigned: val as any, + }), + ).toThrow(new InvalidEventError(eventName, "The event's unsigned content should be a defined object")); + }); + + it.each([true, null, "test", 42, [1, 2, 3]])("should reject invalid regular content: '%s'", val => { + expect(() => + factory({ + room_id: "!room:example.org", + event_id: "$event", + type: "org.example.test_event", + sender: "@user:example.org", + content: val as any, + origin_server_ts: 1670894499800, + }), + ).toThrow( + new InvalidEventError(eventName, "The event content should at least be a defined object, and is required"), + ); + }); + + it.each([-1670894499800, -1, 0])("should return a zero timestamp for negative timestamps: '%s'", val => { + const raw: EventWire.RoomEvent = { + room_id: "!test:example.org", + event_id: "$event", + type: "org.example.test_event", + sender: "@user:example.org", + content: safeContentInput, + origin_server_ts: val, + }; + const ev = factory(raw); + expect(ev).toBeDefined(); + expect(ev.timestamp).toStrictEqual(0); + }); +} diff --git a/test/v1-old/ExtensibleEvents.test.ts b/test/v1-old/ExtensibleEvents.test.ts index 9f3bef6..31ba03d 100644 --- a/test/v1-old/ExtensibleEvents.test.ts +++ b/test/v1-old/ExtensibleEvents.test.ts @@ -322,7 +322,7 @@ describe("ExtensibleEvents", () => { describe("parse errors", () => { function myForcedInvalidInterpreter(wireEvent: IPartialEvent): ExtensibleEvent { - throw new InvalidEventError("deliberate throw of invalid type"); + throw new InvalidEventError("InvalidEventTest", "deliberate throw of invalid type"); } function myExplodingInterpreter(wireEvent: IPartialEvent): ExtensibleEvent { diff --git a/test/v1-old/events/EmoteEvent.test.ts b/test/v1-old/events/EmoteEvent.test.ts index a7a55b7..8a2638b 100644 --- a/test/v1-old/events/EmoteEvent.test.ts +++ b/test/v1-old/events/EmoteEvent.test.ts @@ -90,7 +90,9 @@ describe("EmoteEvent", () => { hello: "world", } as any, // force invalid type }; - expect(() => new EmoteEvent(input)).toThrow(new InvalidEventError("Missing textual representation for event")); + expect(() => new EmoteEvent(input)).toThrow( + new InvalidEventError("MessageEventLegacy", "Missing textual representation for event"), + ); }); it("should fail to parse missing plain text in m.message", () => { @@ -101,7 +103,7 @@ describe("EmoteEvent", () => { }, }; expect(() => new EmoteEvent(input)).toThrow( - new InvalidEventError("m.message is missing a plain text representation"), + new InvalidEventError("MessageEventLegacy", "m.message is missing a plain text representation"), ); }); @@ -112,7 +114,9 @@ describe("EmoteEvent", () => { [M_MESSAGE.name]: "invalid", } as any, // force invalid type }; - expect(() => new EmoteEvent(input)).toThrow(new InvalidEventError("m.message contents must be an array")); + expect(() => new EmoteEvent(input)).toThrow( + new InvalidEventError("MessageEventLegacy", "m.message contents must be an array"), + ); }); describe("isEmote", () => { diff --git a/test/v1-old/events/MessageEvent.test.ts b/test/v1-old/events/MessageEvent.test.ts index 9e909ec..75376e2 100644 --- a/test/v1-old/events/MessageEvent.test.ts +++ b/test/v1-old/events/MessageEvent.test.ts @@ -113,7 +113,7 @@ describe("MessageEvent", () => { } as any, // force invalid type }; expect(() => new MessageEvent(input)).toThrow( - new InvalidEventError("Missing textual representation for event"), + new InvalidEventError("MessageEventLegacy", "Missing textual representation for event"), ); }); @@ -125,7 +125,7 @@ describe("MessageEvent", () => { }, }; expect(() => new MessageEvent(input)).toThrow( - new InvalidEventError("m.message is missing a plain text representation"), + new InvalidEventError("MessageEventLegacy", "m.message is missing a plain text representation"), ); }); @@ -136,7 +136,9 @@ describe("MessageEvent", () => { [M_MESSAGE.name]: "invalid", } as any, // force invalid type }; - expect(() => new MessageEvent(input)).toThrow(new InvalidEventError("m.message contents must be an array")); + expect(() => new MessageEvent(input)).toThrow( + new InvalidEventError("MessageEventLegacy", "m.message contents must be an array"), + ); }); describe("isEmote", () => { diff --git a/test/v1-old/events/NoticeEvent.test.ts b/test/v1-old/events/NoticeEvent.test.ts index f4d5a0d..004511b 100644 --- a/test/v1-old/events/NoticeEvent.test.ts +++ b/test/v1-old/events/NoticeEvent.test.ts @@ -90,7 +90,9 @@ describe("NoticeEvent", () => { hello: "world", } as any, // force invalid type }; - expect(() => new NoticeEvent(input)).toThrow(new InvalidEventError("Missing textual representation for event")); + expect(() => new NoticeEvent(input)).toThrow( + new InvalidEventError("MessageEventLegacy", "Missing textual representation for event"), + ); }); it("should fail to parse missing plain text in m.message", () => { @@ -101,7 +103,7 @@ describe("NoticeEvent", () => { }, }; expect(() => new NoticeEvent(input)).toThrow( - new InvalidEventError("m.message is missing a plain text representation"), + new InvalidEventError("MessageEventLegacy", "m.message is missing a plain text representation"), ); }); @@ -112,7 +114,9 @@ describe("NoticeEvent", () => { [M_MESSAGE.name]: "invalid", } as any, // force invalid type }; - expect(() => new NoticeEvent(input)).toThrow(new InvalidEventError("m.message contents must be an array")); + expect(() => new NoticeEvent(input)).toThrow( + new InvalidEventError("MessageEventLegacy", "m.message contents must be an array"), + ); }); describe("isEmote", () => { diff --git a/test/v1-old/events/PollEndEvent.test.ts b/test/v1-old/events/PollEndEvent.test.ts index e8ccf34..2bfea3f 100644 --- a/test/v1-old/events/PollEndEvent.test.ts +++ b/test/v1-old/events/PollEndEvent.test.ts @@ -54,7 +54,7 @@ describe("PollEndEvent", () => { } as any, // force invalid type }; expect(() => new PollEndEvent(input)).toThrow( - new InvalidEventError("Relationship must be a reference to an event"), + new InvalidEventError("PollEndEventLegacy", "Relationship must be a reference to an event"), ); }); @@ -70,7 +70,7 @@ describe("PollEndEvent", () => { } as any, // force invalid type }; expect(() => new PollEndEvent(input)).toThrow( - new InvalidEventError("Relationship must be a reference to an event"), + new InvalidEventError("PollEndEventLegacy", "Relationship must be a reference to an event"), ); }); @@ -87,7 +87,7 @@ describe("PollEndEvent", () => { } as any, // force invalid type }; expect(() => new PollEndEvent(input)).toThrow( - new InvalidEventError("Relationship must be a reference to an event"), + new InvalidEventError("PollEndEventLegacy", "Relationship must be a reference to an event"), ); }); diff --git a/test/v1-old/events/PollResponseEvent.test.ts b/test/v1-old/events/PollResponseEvent.test.ts index 6666fc6..342f07c 100644 --- a/test/v1-old/events/PollResponseEvent.test.ts +++ b/test/v1-old/events/PollResponseEvent.test.ts @@ -74,7 +74,7 @@ describe("PollResponseEvent", () => { } as any, // force invalid type }; expect(() => new PollResponseEvent(input)).toThrow( - new InvalidEventError("Relationship must be a reference to an event"), + new InvalidEventError("PollResponseEventLegacy", "Relationship must be a reference to an event"), ); }); @@ -91,7 +91,7 @@ describe("PollResponseEvent", () => { } as any, // force invalid type }; expect(() => new PollResponseEvent(input)).toThrow( - new InvalidEventError("Relationship must be a reference to an event"), + new InvalidEventError("PollResponseEventLegacy", "Relationship must be a reference to an event"), ); }); @@ -109,7 +109,7 @@ describe("PollResponseEvent", () => { } as any, // force invalid type }; expect(() => new PollResponseEvent(input)).toThrow( - new InvalidEventError("Relationship must be a reference to an event"), + new InvalidEventError("PollResponseEventLegacy", "Relationship must be a reference to an event"), ); }); diff --git a/test/v1-old/events/PollStartEvent.test.ts b/test/v1-old/events/PollStartEvent.test.ts index 50dc1ad..bf49269 100644 --- a/test/v1-old/events/PollStartEvent.test.ts +++ b/test/v1-old/events/PollStartEvent.test.ts @@ -52,7 +52,7 @@ describe("PollAnswerSubevent", () => { } as any, // force invalid type }; expect(() => new PollAnswerSubevent(input)).toThrow( - new InvalidEventError("Answer ID must be a non-empty string"), + new InvalidEventError("PollStartEventLegacy", "Answer ID must be a non-empty string"), ); }); @@ -122,7 +122,9 @@ describe("PollStartEvent", () => { [M_TEXT.name]: "FALLBACK Question here", } as any, // force invalid type }; - expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("A question is required")); + expect(() => new PollStartEvent(input)).toThrow( + new InvalidEventError("PollStartEventLegacy", "A question is required"), + ); }); it("should fail to parse a missing question", () => { @@ -141,7 +143,9 @@ describe("PollStartEvent", () => { }, } as any, // force invalid type }; - expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("A question is required")); + expect(() => new PollStartEvent(input)).toThrow( + new InvalidEventError("PollStartEventLegacy", "A question is required"), + ); }); it("should fail to parse non-array answers", () => { @@ -157,7 +161,9 @@ describe("PollStartEvent", () => { } as any, // force invalid type }, }; - expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("Poll answers must be an array")); + expect(() => new PollStartEvent(input)).toThrow( + new InvalidEventError("PollStartEventLegacy", "Poll answers must be an array"), + ); }); it("should fail to parse invalid answers", () => { @@ -189,7 +195,9 @@ describe("PollStartEvent", () => { } as any, // force invalid type }, }; - expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("No answers available")); + expect(() => new PollStartEvent(input)).toThrow( + new InvalidEventError("PollStartEventLegacy", "No answers available"), + ); }); it("should truncate answers at 20", () => { From cefa00eb870a9a99f15e9d36a146a0870b56d0d6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Dec 2022 19:05:57 -0700 Subject: [PATCH 08/22] Consolidate repetitive block testing --- test/content_blocks/ArrayBlock.test.ts | 20 +------ test/content_blocks/BaseBlock.test.ts | 65 ++++++++++++++++++----- test/content_blocks/BooleanBlock.test.ts | 22 +------- test/content_blocks/IntegerBlock.test.ts | 21 +------- test/content_blocks/ObjectBlock.test.ts | 20 +------ test/content_blocks/StringBlock.test.ts | 20 +------ test/content_blocks/m/EmoteBlock.test.ts | 19 +------ test/content_blocks/m/MarkupBlock.test.ts | 22 +------- test/content_blocks/m/NoticeBlock.test.ts | 21 +------- 9 files changed, 69 insertions(+), 161 deletions(-) diff --git a/test/content_blocks/ArrayBlock.test.ts b/test/content_blocks/ArrayBlock.test.ts index fe5aea4..77efd39 100644 --- a/test/content_blocks/ArrayBlock.test.ts +++ b/test/content_blocks/ArrayBlock.test.ts @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; import {ArrayBlock} from "../../src/content_blocks/ArrayBlock"; +import {testSharedContentBlockInputs} from "./BaseBlock.test"; class TestArrayBlock extends ArrayBlock { public constructor(raw: any) { @@ -24,26 +24,10 @@ class TestArrayBlock extends ArrayBlock { } describe("ArrayBlock", () => { - it("should retain the block name", () => { - const block = new TestArrayBlock([]); - expect(block.name).toStrictEqual("TestBlock"); - }); - - it.each([null, undefined])("should reject null and undefined: %s", val => { - expect(() => new TestArrayBlock(val)).toThrow( - new InvalidBlockError( - "TestBlock", - "Block value must be defined. Use a null-capable parser instead of passing such a value.", - ), - ); - }); + testSharedContentBlockInputs("TestBlock", [1, 2], x => new TestArrayBlock(x)); it.each([["values"], []])("should accept arrays: '%s'", (...val) => { const block = new TestArrayBlock(val); expect(block.raw).toStrictEqual(val); }); - - it.each([42, "test", "", {}, false])("should reject non-arrays: '%s'", val => { - expect(() => new TestArrayBlock(val)).toThrow(new InvalidBlockError("TestBlock", "should be an array value")); - }); }); diff --git a/test/content_blocks/BaseBlock.test.ts b/test/content_blocks/BaseBlock.test.ts index 6d81789..f183de6 100644 --- a/test/content_blocks/BaseBlock.test.ts +++ b/test/content_blocks/BaseBlock.test.ts @@ -24,25 +24,66 @@ class TestBaseBlock extends BaseBlock { } describe("BaseBlock", () => { + testSharedContentBlockInputs("TestBlock", undefined, x => new TestBaseBlock(x)); + + it.each(["string", "", true, false, 42, 42.1, {hello: "world"}, [1, 2, 3], {}, []])( + "should accept wire values: '%s'", + val => { + const block = new TestBaseBlock(val); + expect(block.raw).toStrictEqual(val); + }, + ); +}); + +type SafeValuesConditional = string | number | boolean | object | SafeValuesConditional[] | undefined; + +export function testSharedContentBlockInputs( + blockName: string, + safeValue: SafeValuesConditional, + factory: (x: any) => BaseBlock, +) { + describe("internal", () => { + it("should have valid inputs", () => { + expect(blockName).toBeDefined(); + expect(typeof blockName).toStrictEqual("string"); + expect(blockName.length).toBeGreaterThan(0); + + if (safeValue !== undefined) { + expect(safeValue).not.toBeNull(); + } + + expect(factory).toBeDefined(); + expect(typeof factory).toStrictEqual("function"); + }); + + it("should have a passing factory", () => { + const ev = factory(safeValue ?? 42); + expect(ev).toBeDefined(); + // noinspection SuspiciousTypeOfGuard + expect(ev instanceof BaseBlock).toStrictEqual(true); + }); + }); + it("should retain the block name", () => { - const block = new TestBaseBlock(42); - expect(block.name).toStrictEqual("TestBlock"); + const block = factory(safeValue ?? 42); + expect(block.name).toStrictEqual(blockName); }); it.each([null, undefined])("should reject null and undefined: %s", val => { - expect(() => new TestBaseBlock(val)).toThrow( + expect(() => factory(val)).toThrow( new InvalidBlockError( - "TestBlock", + blockName, "Block value must be defined. Use a null-capable parser instead of passing such a value.", ), ); }); - it.each(["string", "", true, false, 42, 42.1, {hello: "world"}, [1, 2, 3], {}, []])( - "should accept wire values: '%s'", - val => { - const block = new TestBaseBlock(val); - expect(block.raw).toStrictEqual(val); - }, - ); -}); + if (safeValue !== undefined) { + const toTest = ["string", "", true, false, 42, 42.1, {hello: "world"}, [1, 2, 3], {}, []].filter(x => + Array.isArray(safeValue) ? !Array.isArray(x) : typeof x !== typeof safeValue, + ); + it.each(toTest)("should reject invalid base types: '%s'", val => { + expect(() => factory(val as any)).toThrowError(InvalidBlockError); + }); + } +} diff --git a/test/content_blocks/BooleanBlock.test.ts b/test/content_blocks/BooleanBlock.test.ts index 85b0e2e..dd2bfa3 100644 --- a/test/content_blocks/BooleanBlock.test.ts +++ b/test/content_blocks/BooleanBlock.test.ts @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; import {BooleanBlock} from "../../src/content_blocks/BooleanBlock"; +import {testSharedContentBlockInputs} from "./BaseBlock.test"; class TestBooleanBlock extends BooleanBlock { public constructor(raw: any) { @@ -24,28 +24,10 @@ class TestBooleanBlock extends BooleanBlock { } describe("BooleanBlock", () => { - it("should retain the block name", () => { - const block = new TestBooleanBlock(false); - expect(block.name).toStrictEqual("TestBlock"); - }); - - it.each([null, undefined])("should reject null and undefined: %s", val => { - expect(() => new TestBooleanBlock(val)).toThrow( - new InvalidBlockError( - "TestBlock", - "Block value must be defined. Use a null-capable parser instead of passing such a value.", - ), - ); - }); + testSharedContentBlockInputs("TestBlock", true, x => new TestBooleanBlock(x)); it.each([true, false])("should accept booleans: '%s'", val => { const block = new TestBooleanBlock(val); expect(block.raw).toStrictEqual(val); }); - - it.each([42, "test", "", {}, []])("should reject non-booleans: '%s'", val => { - expect(() => new TestBooleanBlock(val)).toThrow( - new InvalidBlockError("TestBlock", "should be a boolean value"), - ); - }); }); diff --git a/test/content_blocks/IntegerBlock.test.ts b/test/content_blocks/IntegerBlock.test.ts index c4acc59..f0ecd88 100644 --- a/test/content_blocks/IntegerBlock.test.ts +++ b/test/content_blocks/IntegerBlock.test.ts @@ -16,6 +16,7 @@ limitations under the License. import {IntegerBlock} from "../../src/content_blocks/IntegerBlock"; import {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; +import {testSharedContentBlockInputs} from "./BaseBlock.test"; class TestIntegerBlock extends IntegerBlock { public constructor(raw: any) { @@ -24,31 +25,13 @@ class TestIntegerBlock extends IntegerBlock { } describe("IntegerBlock", () => { - it("should retain the block name", () => { - const block = new TestIntegerBlock(42); - expect(block.name).toStrictEqual("TestBlock"); - }); - - it.each([null, undefined])("should reject null and undefined: %s", val => { - expect(() => new TestIntegerBlock(val)).toThrow( - new InvalidBlockError( - "TestBlock", - "Block value must be defined. Use a null-capable parser instead of passing such a value.", - ), - ); - }); + testSharedContentBlockInputs("TestBlock", -1, x => new TestIntegerBlock(x)); it("should accept integers", () => { const block = new TestIntegerBlock(42); expect(block.raw).toStrictEqual(42); }); - it.each(["42", Number.NaN, true, {}, []])("should reject non-numbers: '%s'", val => { - expect(() => new TestIntegerBlock(val)).toThrow( - new InvalidBlockError("TestBlock", "should be an integer value"), - ); - }); - it("should decline floats", () => { expect(() => new TestIntegerBlock(42.1)).toThrow( new InvalidBlockError("TestBlock", "should be an integer value"), diff --git a/test/content_blocks/ObjectBlock.test.ts b/test/content_blocks/ObjectBlock.test.ts index 7e271b6..3f37ef5 100644 --- a/test/content_blocks/ObjectBlock.test.ts +++ b/test/content_blocks/ObjectBlock.test.ts @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; import {ObjectBlock} from "../../src/content_blocks/ObjectBlock"; +import {testSharedContentBlockInputs} from "./BaseBlock.test"; class TestObjectBlock extends ObjectBlock { public constructor(raw: any) { @@ -24,26 +24,10 @@ class TestObjectBlock extends ObjectBlock { } describe("ObjectBlock", () => { - it("should retain the block name", () => { - const block = new TestObjectBlock({}); - expect(block.name).toStrictEqual("TestBlock"); - }); - - it.each([null, undefined])("should reject null and undefined: %s", val => { - expect(() => new TestObjectBlock(val)).toThrow( - new InvalidBlockError( - "TestBlock", - "Block value must be defined. Use a null-capable parser instead of passing such a value.", - ), - ); - }); + testSharedContentBlockInputs("TestBlock", {obj: true}, x => new TestObjectBlock(x)); it.each([{hello: "world"}, {}])("should accept objects: '%s'", val => { const block = new TestObjectBlock(val); expect(block.raw).toStrictEqual(val); }); - - it.each([42, "test", "", [], false])("should reject non-objects: '%s'", val => { - expect(() => new TestObjectBlock(val)).toThrow(new InvalidBlockError("TestBlock", "should be an object value")); - }); }); diff --git a/test/content_blocks/StringBlock.test.ts b/test/content_blocks/StringBlock.test.ts index dee7ceb..7e2b534 100644 --- a/test/content_blocks/StringBlock.test.ts +++ b/test/content_blocks/StringBlock.test.ts @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; import {StringBlock} from "../../src/content_blocks/StringBlock"; +import {testSharedContentBlockInputs} from "./BaseBlock.test"; class TestStringBlock extends StringBlock { public constructor(raw: any) { @@ -24,19 +24,7 @@ class TestStringBlock extends StringBlock { } describe("StringBlock", () => { - it("should retain the block name", () => { - const block = new TestStringBlock("test"); - expect(block.name).toStrictEqual("TestBlock"); - }); - - it.each([null, undefined])("should reject null and undefined: %s", val => { - expect(() => new TestStringBlock(val)).toThrow( - new InvalidBlockError( - "TestBlock", - "Block value must be defined. Use a null-capable parser instead of passing such a value.", - ), - ); - }); + testSharedContentBlockInputs("TestBlock", "nostring", x => new TestStringBlock(x)); it("should accept strings", () => { const block = new TestStringBlock("test"); @@ -47,8 +35,4 @@ describe("StringBlock", () => { const block = new TestStringBlock(""); expect(block.raw).toStrictEqual(""); }); - - it.each([42, true, {}, []])("should reject non-strings: '%s'", val => { - expect(() => new TestStringBlock(val)).toThrow(new InvalidBlockError("TestBlock", "should be a string value")); - }); }); diff --git a/test/content_blocks/m/EmoteBlock.test.ts b/test/content_blocks/m/EmoteBlock.test.ts index 2084ba5..ed7e03e 100644 --- a/test/content_blocks/m/EmoteBlock.test.ts +++ b/test/content_blocks/m/EmoteBlock.test.ts @@ -16,21 +16,10 @@ limitations under the License. import {InvalidBlockError} from "../../../src/content_blocks/InvalidBlockError"; import {EmoteBlock} from "../../../src/content_blocks/m/EmoteBlock"; +import {testSharedContentBlockInputs} from "../BaseBlock.test"; describe("EmoteBlock", () => { - it("should have a sensible block name", () => { - const block = new EmoteBlock({"m.markup": [{body: "test"}]}); - expect(block.name).toStrictEqual("m.emote"); - }); - - it.each([null, undefined])("should reject null and undefined: %s", val => { - expect(() => new EmoteBlock(val as any)).toThrow( - new InvalidBlockError( - "m.emote", - "Block value must be defined. Use a null-capable parser instead of passing such a value.", - ), - ); - }); + testSharedContentBlockInputs("m.emote", {"m.markup": [{body: "test"}]}, x => new EmoteBlock(x)); it("should locate a stable markup block", () => { const block = new EmoteBlock({"m.markup": [{body: "test"}]}); @@ -59,8 +48,4 @@ describe("EmoteBlock", () => { new InvalidBlockError("m.emote", "schema does not apply to m.markup or org.matrix.msc1767.markup"), ); }); - - it.each([42, true, [], "test"])("should reject non-emotes: '%s'", val => { - expect(() => new EmoteBlock(val as any)).toThrow(new InvalidBlockError("m.emote", "should be an object value")); - }); }); diff --git a/test/content_blocks/m/MarkupBlock.test.ts b/test/content_blocks/m/MarkupBlock.test.ts index 5694880..637a2a1 100644 --- a/test/content_blocks/m/MarkupBlock.test.ts +++ b/test/content_blocks/m/MarkupBlock.test.ts @@ -14,23 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InvalidBlockError} from "../../../src/content_blocks/InvalidBlockError"; import {MarkupBlock} from "../../../src/content_blocks/m/MarkupBlock"; +import {testSharedContentBlockInputs} from "../BaseBlock.test"; describe("MarkupBlock", () => { - it("should have a sensible block name", () => { - const block = new MarkupBlock([{body: "test"}]); - expect(block.name).toStrictEqual("m.markup"); - }); - - it.each([null, undefined])("should reject null and undefined: %s", val => { - expect(() => new MarkupBlock(val as any)).toThrow( - new InvalidBlockError( - "m.markup", - "Block value must be defined. Use a null-capable parser instead of passing such a value.", - ), - ); - }); + testSharedContentBlockInputs("m.markup", [{body: "test"}], x => new MarkupBlock(x)); it("should be able to identify the text and HTML representations", () => { const block = new MarkupBlock([ @@ -203,10 +191,4 @@ describe("MarkupBlock", () => { expect(block.representations.length).toStrictEqual(3); expect(block.representations).toStrictEqual(block.raw); }); - - it.each([42, true, {}, "test"])("should reject non-markups: '%s'", val => { - expect(() => new MarkupBlock(val as any)).toThrow( - new InvalidBlockError("m.markup", "should be an array value"), - ); - }); }); diff --git a/test/content_blocks/m/NoticeBlock.test.ts b/test/content_blocks/m/NoticeBlock.test.ts index b385d7a..4ca359b 100644 --- a/test/content_blocks/m/NoticeBlock.test.ts +++ b/test/content_blocks/m/NoticeBlock.test.ts @@ -16,21 +16,10 @@ limitations under the License. import {InvalidBlockError} from "../../../src/content_blocks/InvalidBlockError"; import {NoticeBlock} from "../../../src/content_blocks/m/NoticeBlock"; +import {testSharedContentBlockInputs} from "../BaseBlock.test"; describe("NoticeBlock", () => { - it("should have a sensible block name", () => { - const block = new NoticeBlock({"m.markup": [{body: "test"}]}); - expect(block.name).toStrictEqual("m.notice"); - }); - - it.each([null, undefined])("should reject null and undefined: %s", val => { - expect(() => new NoticeBlock(val as any)).toThrow( - new InvalidBlockError( - "m.notice", - "Block value must be defined. Use a null-capable parser instead of passing such a value.", - ), - ); - }); + testSharedContentBlockInputs("m.notice", {"m.markup": [{body: "test"}]}, x => new NoticeBlock(x)); it("should locate a stable markup block", () => { const block = new NoticeBlock({"m.markup": [{body: "test"}]}); @@ -59,10 +48,4 @@ describe("NoticeBlock", () => { new InvalidBlockError("m.notice", "schema does not apply to m.markup or org.matrix.msc1767.markup"), ); }); - - it.each([42, true, [], "test"])("should reject non-notices: '%s'", val => { - expect(() => new NoticeBlock(val as any)).toThrow( - new InvalidBlockError("m.notice", "should be an object value"), - ); - }); }); From 126b824774f08cfbb1db57208eb67200367d4235 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Dec 2022 19:07:52 -0700 Subject: [PATCH 09/22] Optimize test imports --- test/content_blocks/ArrayBlock.test.ts | 2 +- test/content_blocks/BaseBlock.test.ts | 3 +-- test/content_blocks/BooleanBlock.test.ts | 2 +- test/content_blocks/IntegerBlock.test.ts | 3 +-- test/content_blocks/InvalidBlockError.test.ts | 2 +- test/content_blocks/ObjectBlock.test.ts | 2 +- test/content_blocks/StringBlock.test.ts | 2 +- test/content_blocks/m/EmoteBlock.test.ts | 3 +-- test/content_blocks/m/MarkupBlock.test.ts | 2 +- test/content_blocks/m/NoticeBlock.test.ts | 3 +-- test/events/InvalidEventError.test.ts | 2 +- test/v1-old/ExtensibleEvents.test.ts | 3 ++- test/v1-old/NamespacedMap.test.ts | 3 +-- 13 files changed, 14 insertions(+), 18 deletions(-) diff --git a/test/content_blocks/ArrayBlock.test.ts b/test/content_blocks/ArrayBlock.test.ts index 77efd39..c7f84f6 100644 --- a/test/content_blocks/ArrayBlock.test.ts +++ b/test/content_blocks/ArrayBlock.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ArrayBlock} from "../../src/content_blocks/ArrayBlock"; +import {ArrayBlock} from "../../src"; import {testSharedContentBlockInputs} from "./BaseBlock.test"; class TestArrayBlock extends ArrayBlock { diff --git a/test/content_blocks/BaseBlock.test.ts b/test/content_blocks/BaseBlock.test.ts index f183de6..669b296 100644 --- a/test/content_blocks/BaseBlock.test.ts +++ b/test/content_blocks/BaseBlock.test.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; -import {BaseBlock} from "../../src/content_blocks/BaseBlock"; +import {BaseBlock, InvalidBlockError} from "../../src"; class TestBaseBlock extends BaseBlock { public constructor(raw: any) { diff --git a/test/content_blocks/BooleanBlock.test.ts b/test/content_blocks/BooleanBlock.test.ts index dd2bfa3..f860c1c 100644 --- a/test/content_blocks/BooleanBlock.test.ts +++ b/test/content_blocks/BooleanBlock.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BooleanBlock} from "../../src/content_blocks/BooleanBlock"; +import {BooleanBlock} from "../../src"; import {testSharedContentBlockInputs} from "./BaseBlock.test"; class TestBooleanBlock extends BooleanBlock { diff --git a/test/content_blocks/IntegerBlock.test.ts b/test/content_blocks/IntegerBlock.test.ts index f0ecd88..747298d 100644 --- a/test/content_blocks/IntegerBlock.test.ts +++ b/test/content_blocks/IntegerBlock.test.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {IntegerBlock} from "../../src/content_blocks/IntegerBlock"; -import {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; +import {IntegerBlock, InvalidBlockError} from "../../src"; import {testSharedContentBlockInputs} from "./BaseBlock.test"; class TestIntegerBlock extends IntegerBlock { diff --git a/test/content_blocks/InvalidBlockError.test.ts b/test/content_blocks/InvalidBlockError.test.ts index 73208d4..c016e01 100644 --- a/test/content_blocks/InvalidBlockError.test.ts +++ b/test/content_blocks/InvalidBlockError.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InvalidBlockError} from "../../src/content_blocks/InvalidBlockError"; +import {InvalidBlockError} from "../../src"; describe("InvalidBlockError", () => { it("should use the block name in the error", () => { diff --git a/test/content_blocks/ObjectBlock.test.ts b/test/content_blocks/ObjectBlock.test.ts index 3f37ef5..9ccf571 100644 --- a/test/content_blocks/ObjectBlock.test.ts +++ b/test/content_blocks/ObjectBlock.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ObjectBlock} from "../../src/content_blocks/ObjectBlock"; +import {ObjectBlock} from "../../src"; import {testSharedContentBlockInputs} from "./BaseBlock.test"; class TestObjectBlock extends ObjectBlock { diff --git a/test/content_blocks/StringBlock.test.ts b/test/content_blocks/StringBlock.test.ts index 7e2b534..aefbcef 100644 --- a/test/content_blocks/StringBlock.test.ts +++ b/test/content_blocks/StringBlock.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {StringBlock} from "../../src/content_blocks/StringBlock"; +import {StringBlock} from "../../src"; import {testSharedContentBlockInputs} from "./BaseBlock.test"; class TestStringBlock extends StringBlock { diff --git a/test/content_blocks/m/EmoteBlock.test.ts b/test/content_blocks/m/EmoteBlock.test.ts index ed7e03e..3dfddb6 100644 --- a/test/content_blocks/m/EmoteBlock.test.ts +++ b/test/content_blocks/m/EmoteBlock.test.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InvalidBlockError} from "../../../src/content_blocks/InvalidBlockError"; -import {EmoteBlock} from "../../../src/content_blocks/m/EmoteBlock"; +import {EmoteBlock, InvalidBlockError} from "../../../src"; import {testSharedContentBlockInputs} from "../BaseBlock.test"; describe("EmoteBlock", () => { diff --git a/test/content_blocks/m/MarkupBlock.test.ts b/test/content_blocks/m/MarkupBlock.test.ts index 637a2a1..1ed611e 100644 --- a/test/content_blocks/m/MarkupBlock.test.ts +++ b/test/content_blocks/m/MarkupBlock.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MarkupBlock} from "../../../src/content_blocks/m/MarkupBlock"; +import {MarkupBlock} from "../../../src"; import {testSharedContentBlockInputs} from "../BaseBlock.test"; describe("MarkupBlock", () => { diff --git a/test/content_blocks/m/NoticeBlock.test.ts b/test/content_blocks/m/NoticeBlock.test.ts index 4ca359b..452e85f 100644 --- a/test/content_blocks/m/NoticeBlock.test.ts +++ b/test/content_blocks/m/NoticeBlock.test.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InvalidBlockError} from "../../../src/content_blocks/InvalidBlockError"; -import {NoticeBlock} from "../../../src/content_blocks/m/NoticeBlock"; +import {InvalidBlockError, NoticeBlock} from "../../../src"; import {testSharedContentBlockInputs} from "../BaseBlock.test"; describe("NoticeBlock", () => { diff --git a/test/events/InvalidEventError.test.ts b/test/events/InvalidEventError.test.ts index fb9bd6c..e1626b2 100644 --- a/test/events/InvalidEventError.test.ts +++ b/test/events/InvalidEventError.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InvalidEventError} from "../../src/events/InvalidEventError"; +import {InvalidEventError} from "../../src"; describe("InvalidEventError", () => { it("should use the block name in the error", () => { diff --git a/test/v1-old/ExtensibleEvents.test.ts b/test/v1-old/ExtensibleEvents.test.ts index 31ba03d..27ec9b7 100644 --- a/test/v1-old/ExtensibleEvents.test.ts +++ b/test/v1-old/ExtensibleEvents.test.ts @@ -36,12 +36,13 @@ import { M_TEXT, MessageEvent, NoticeEvent, + Optional, PollEndEvent, PollResponseEvent, PollStartEvent, REFERENCE_RELATION, + UnstableValue, } from "../../src"; -import {Optional, UnstableValue} from "../../src"; describe("ExtensibleEvents", () => { afterEach(() => { diff --git a/test/v1-old/NamespacedMap.test.ts b/test/v1-old/NamespacedMap.test.ts index 8f467b3..c7d4f15 100644 --- a/test/v1-old/NamespacedMap.test.ts +++ b/test/v1-old/NamespacedMap.test.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {NamespacedMap} from "../../src"; -import {NamespacedValue, UnstableValue} from "../../src"; +import {NamespacedMap, NamespacedValue, UnstableValue} from "../../src"; import {STABLE_VALUE, UNSTABLE_VALUE} from "../NamespacedValue.test"; type TestableNamespacedMap = {internalMap: Map} & NamespacedMap; From 13bf6094436a74860c9d48eeaf7807c6862a4bb9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Dec 2022 19:10:15 -0700 Subject: [PATCH 10/22] Flag the AjvContainer class just in case --- src/AjvContainer.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/AjvContainer.ts b/src/AjvContainer.ts index e28677e..a7c59e2 100644 --- a/src/AjvContainer.ts +++ b/src/AjvContainer.ts @@ -18,6 +18,11 @@ import Ajv, {Schema, SchemaObject} from "ajv"; import AjvErrors from "ajv-errors"; import {NamespacedValue} from "./NamespacedValue"; +/** + * Container for the ajv instance. + * @internal + * @protected + */ export class AjvContainer { public static readonly ajv = new Ajv({ allErrors: true, From 9a811883d014481a4f98ba38e3e6fcd1def74200 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Dec 2022 19:11:01 -0700 Subject: [PATCH 11/22] Flip naming of wire modules --- src/content_blocks/ArrayBlock.ts | 4 ++-- src/content_blocks/BaseBlock.ts | 4 ++-- src/content_blocks/types_wire.d.ts | 2 +- src/events/RoomEvent.ts | 6 +++--- src/events/types_wire.d.ts | 6 +++--- test/events/RoomEvent.test.ts | 12 ++++++------ 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/content_blocks/ArrayBlock.ts b/src/content_blocks/ArrayBlock.ts index d52fdb5..0ff6286 100644 --- a/src/content_blocks/ArrayBlock.ts +++ b/src/content_blocks/ArrayBlock.ts @@ -16,7 +16,7 @@ limitations under the License. import {InvalidBlockError} from "./InvalidBlockError"; import {BaseBlock} from "./BaseBlock"; -import {ContentBlockWire} from "./types_wire"; +import {WireContentBlock} from "./types_wire"; import {Schema} from "ajv"; import {AjvContainer} from "../AjvContainer"; @@ -24,7 +24,7 @@ import {AjvContainer} from "../AjvContainer"; * Represents an array-based content block. * @module Content Blocks */ -export abstract class ArrayBlock extends BaseBlock { +export abstract class ArrayBlock extends BaseBlock { public static readonly schema: Schema = { type: "array", errorMessage: "should be an array value", diff --git a/src/content_blocks/BaseBlock.ts b/src/content_blocks/BaseBlock.ts index aa69ee9..e44f220 100644 --- a/src/content_blocks/BaseBlock.ts +++ b/src/content_blocks/BaseBlock.ts @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ContentBlockWire} from "./types_wire"; +import {WireContentBlock} from "./types_wire"; import {InvalidBlockError} from "./InvalidBlockError"; /** * The simplest form of a content block in its parsed form. * @module Content Blocks */ -export abstract class BaseBlock { +export abstract class BaseBlock { private _raw: T | undefined = undefined; public get raw(): T { diff --git a/src/content_blocks/types_wire.d.ts b/src/content_blocks/types_wire.d.ts index 8f400b5..85bc0ff 100644 --- a/src/content_blocks/types_wire.d.ts +++ b/src/content_blocks/types_wire.d.ts @@ -18,7 +18,7 @@ limitations under the License. * The wire types for content blocks. * @module Content Blocks */ -export module ContentBlockWire { +export module WireContentBlock { /** * Possible value types for a content block. * @module Content Blocks diff --git a/src/events/RoomEvent.ts b/src/events/RoomEvent.ts index e281167..33cab06 100644 --- a/src/events/RoomEvent.ts +++ b/src/events/RoomEvent.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {EventWire} from "./types_wire"; +import {WireEvent} from "./types_wire"; import {Schema} from "ajv"; import {AjvContainer} from "../AjvContainer"; import {InvalidEventError} from "./InvalidEventError"; @@ -24,7 +24,7 @@ import {InvalidEventError} from "./InvalidEventError"; * consumption by a parser. * @module Events */ -export abstract class RoomEvent> { +export abstract class RoomEvent> { public static readonly schema: Schema = { type: "object", properties: { @@ -96,7 +96,7 @@ export abstract class RoomEvent) { + protected constructor(public readonly name: string, public readonly raw: WireEvent.RoomEvent) { if (raw === null || raw === undefined) { throw new InvalidEventError( this.name, diff --git a/src/events/types_wire.d.ts b/src/events/types_wire.d.ts index 66087d9..f6d2e84 100644 --- a/src/events/types_wire.d.ts +++ b/src/events/types_wire.d.ts @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ContentBlockWire} from "../content_blocks/types_wire"; +import {WireContentBlock} from "../content_blocks/types_wire"; /** * The wire types for Matrix events. * @module Events */ -export module EventWire { +export module WireEvent { /** * A Matrix event. Also called a ClientEvent by the Matrix Specification. * @module Events @@ -41,7 +41,7 @@ export module EventWire { * @module Events */ type BlockBasedContent = { - [k: string]: ContentBlockWire.Value; + [k: string]: WireContentBlock.Value; }; /** diff --git a/test/events/RoomEvent.test.ts b/test/events/RoomEvent.test.ts index f62a25e..26b61a0 100644 --- a/test/events/RoomEvent.test.ts +++ b/test/events/RoomEvent.test.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {InvalidEventError, RoomEvent} from "../../src"; -import {EventWire} from "../../src/events/types_wire"; +import {WireEvent} from "../../src/events/types_wire"; class TestEvent extends RoomEvent { public constructor(raw: any) { @@ -36,8 +36,8 @@ describe("RoomEvent", () => { */ export function testSharedRoomEventInputs( eventName: string, - factory: (raw: EventWire.RoomEvent) => RoomEvent, - safeContentInput: EventWire.BlockBasedContent, + factory: (raw: WireEvent.RoomEvent) => RoomEvent, + safeContentInput: WireEvent.BlockBasedContent, ) { describe("internal", () => { it("should have valid inputs", () => { @@ -77,7 +77,7 @@ export function testSharedRoomEventInputs( }); it.each([undefined, "", "with content"])("should handle valid event structures with state key of '%s'", skey => { - const raw: EventWire.RoomEvent = { + const raw: WireEvent.RoomEvent = { room_id: "!test:example.org", event_id: "$event", type: "org.example.test_event", @@ -99,7 +99,7 @@ export function testSharedRoomEventInputs( }); it("should retain the event name", () => { - const raw: EventWire.RoomEvent = { + const raw: WireEvent.RoomEvent = { room_id: "!test:example.org", event_id: "$event", type: "org.example.test_event", @@ -287,7 +287,7 @@ export function testSharedRoomEventInputs( }); it.each([-1670894499800, -1, 0])("should return a zero timestamp for negative timestamps: '%s'", val => { - const raw: EventWire.RoomEvent = { + const raw: WireEvent.RoomEvent = { room_id: "!test:example.org", event_id: "$event", type: "org.example.test_event", From db812d9b50e61908b02e491898a4ae5363c6d822 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Dec 2022 19:13:35 -0700 Subject: [PATCH 12/22] Format jest config because why not --- jest.config.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/jest.config.js b/jest.config.js index e0abe27..8bbbade 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,15 +1,15 @@ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - collectCoverage: true, - collectCoverageFrom: ["./src/**"], - coverageThreshold: { - global: { - lines: 100, - branches: 100, - functions: 100, - statements: -1, + preset: "ts-jest", + testEnvironment: "node", + collectCoverage: true, + collectCoverageFrom: ["./src/**"], + coverageThreshold: { + global: { + lines: 100, + branches: 100, + functions: 100, + statements: -1, + }, }, - }, }; From da95c26a1add5462dc8064ca440cafa4e5f893e6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 13 Dec 2022 14:22:02 -0700 Subject: [PATCH 13/22] Remove v1-old as it gets in the way of refactoring --- src/v1-old/ExtensibleEvents.ts | 171 ------- src/v1-old/IPartialEvent.ts | 23 - src/v1-old/NamespacedMap.ts | 99 ---- src/v1-old/events/EmoteEvent.ts | 62 --- src/v1-old/events/ExtensibleEvent.ts | 59 --- src/v1-old/events/MessageEvent.ts | 164 ------- src/v1-old/events/NoticeEvent.ts | 62 --- src/v1-old/events/PollEndEvent.ts | 95 ---- src/v1-old/events/PollResponseEvent.ts | 151 ------ src/v1-old/events/PollStartEvent.ts | 208 --------- src/v1-old/events/message_types.ts | 95 ---- src/v1-old/events/poll_types.ts | 119 ----- src/v1-old/events/relationship_types.ts | 39 -- .../interpreters/legacy/MRoomMessage.ts | 78 ---- src/v1-old/interpreters/modern/MMessage.ts | 33 -- src/v1-old/interpreters/modern/MPoll.ts | 44 -- src/v1-old/types.ts | 72 --- src/v1-old/utility/MessageMatchers.ts | 52 --- src/v1-old/utility/events.ts | 51 -- test/v1-old/ExtensibleEvents.test.ts | 373 --------------- test/v1-old/NamespacedMap.test.ts | 140 ------ test/v1-old/events/EmoteEvent.test.ts | 234 ---------- test/v1-old/events/ExtensibleEvent.test.ts | 40 -- test/v1-old/events/MessageEvent.test.ts | 256 ---------- test/v1-old/events/NoticeEvent.test.ts | 234 ---------- test/v1-old/events/PollEndEvent.test.ts | 121 ----- test/v1-old/events/PollResponseEvent.test.ts | 316 ------------- test/v1-old/events/PollStartEvent.test.ts | 436 ------------------ .../interpreters/legacy/MRoomMessage.test.ts | 157 ------- .../interpreters/modern/MMessage.test.ts | 87 ---- test/v1-old/interpreters/modern/MPoll.test.ts | 117 ----- test/v1-old/utility/MessageMatchers.test.ts | 144 ------ test/v1-old/utility/events.test.ts | 85 ---- 33 files changed, 4417 deletions(-) delete mode 100644 src/v1-old/ExtensibleEvents.ts delete mode 100644 src/v1-old/IPartialEvent.ts delete mode 100644 src/v1-old/NamespacedMap.ts delete mode 100644 src/v1-old/events/EmoteEvent.ts delete mode 100644 src/v1-old/events/ExtensibleEvent.ts delete mode 100644 src/v1-old/events/MessageEvent.ts delete mode 100644 src/v1-old/events/NoticeEvent.ts delete mode 100644 src/v1-old/events/PollEndEvent.ts delete mode 100644 src/v1-old/events/PollResponseEvent.ts delete mode 100644 src/v1-old/events/PollStartEvent.ts delete mode 100644 src/v1-old/events/message_types.ts delete mode 100644 src/v1-old/events/poll_types.ts delete mode 100644 src/v1-old/events/relationship_types.ts delete mode 100644 src/v1-old/interpreters/legacy/MRoomMessage.ts delete mode 100644 src/v1-old/interpreters/modern/MMessage.ts delete mode 100644 src/v1-old/interpreters/modern/MPoll.ts delete mode 100644 src/v1-old/types.ts delete mode 100644 src/v1-old/utility/MessageMatchers.ts delete mode 100644 src/v1-old/utility/events.ts delete mode 100644 test/v1-old/ExtensibleEvents.test.ts delete mode 100644 test/v1-old/NamespacedMap.test.ts delete mode 100644 test/v1-old/events/EmoteEvent.test.ts delete mode 100644 test/v1-old/events/ExtensibleEvent.test.ts delete mode 100644 test/v1-old/events/MessageEvent.test.ts delete mode 100644 test/v1-old/events/NoticeEvent.test.ts delete mode 100644 test/v1-old/events/PollEndEvent.test.ts delete mode 100644 test/v1-old/events/PollResponseEvent.test.ts delete mode 100644 test/v1-old/events/PollStartEvent.test.ts delete mode 100644 test/v1-old/interpreters/legacy/MRoomMessage.test.ts delete mode 100644 test/v1-old/interpreters/modern/MMessage.test.ts delete mode 100644 test/v1-old/interpreters/modern/MPoll.test.ts delete mode 100644 test/v1-old/utility/MessageMatchers.test.ts delete mode 100644 test/v1-old/utility/events.test.ts diff --git a/src/v1-old/ExtensibleEvents.ts b/src/v1-old/ExtensibleEvents.ts deleted file mode 100644 index 77f2254..0000000 --- a/src/v1-old/ExtensibleEvents.ts +++ /dev/null @@ -1,171 +0,0 @@ -/* -Copyright 2021 - 2022 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 {IPartialEvent} from "./IPartialEvent"; -import {ExtensibleEvent} from "./events/ExtensibleEvent"; -import {Optional} from "../types"; -import {NamespacedValue} from "../NamespacedValue"; -import {NamespacedMap} from "./NamespacedMap"; -import {InvalidEventError} from "../events/InvalidEventError"; -import {LEGACY_M_ROOM_MESSAGE, parseMRoomMessage} from "./interpreters/legacy/MRoomMessage"; -import {parseMMessage} from "./interpreters/modern/MMessage"; -import {M_EMOTE, M_MESSAGE, M_NOTICE} from "./events/message_types"; -import {M_POLL_END, M_POLL_RESPONSE, M_POLL_START} from "./events/poll_types"; -import {parseMPoll} from "./interpreters/modern/MPoll"; - -export type EventInterpreter = ( - wireEvent: IPartialEvent, -) => Optional; - -/** - * Utility class for parsing and identifying event types in a renderable form. An - * instance of this class can be created to change rendering preference depending - * on use-case. - */ -export class ExtensibleEvents { - // Dev note: this is manually reset by the unit tests - adjust name in both places - // if changed. - private static _defaultInstance: ExtensibleEvents = new ExtensibleEvents(); - - private interpreters = new NamespacedMap>([ - // Remember to add your unit test when adding to this! ("known events" test description) - [LEGACY_M_ROOM_MESSAGE, parseMRoomMessage], - [M_MESSAGE, parseMMessage], - [M_EMOTE, parseMMessage], - [M_NOTICE, parseMMessage], - [M_POLL_START, parseMPoll], - [M_POLL_RESPONSE, parseMPoll], - [M_POLL_END, parseMPoll], - ]); - - private _unknownInterpretOrder: NamespacedValue[] = [M_MESSAGE]; - - public constructor() {} - - /** - * Gets the default instance for all extensible event parsing. - */ - public static get defaultInstance(): ExtensibleEvents { - return ExtensibleEvents._defaultInstance; - } - - /** - * Gets the order the internal processor will use for unknown primary - * event types. - */ - public get unknownInterpretOrder(): NamespacedValue[] { - return this._unknownInterpretOrder; - } - - /** - * Sets the order the internal processor will use for unknown primary - * event types. - * @param {NamespacedValue[]} val The parsing order. - */ - public set unknownInterpretOrder(val: NamespacedValue[]) { - this._unknownInterpretOrder = val; - } - - /** - * Gets the order the internal processor will use for unknown primary - * event types. - */ - public static get unknownInterpretOrder(): NamespacedValue[] { - return ExtensibleEvents.defaultInstance.unknownInterpretOrder; - } - - /** - * Sets the order the internal processor will use for unknown primary - * event types. - * @param {NamespacedValue[]} val The parsing order. - */ - public static set unknownInterpretOrder(val: NamespacedValue[]) { - ExtensibleEvents.defaultInstance.unknownInterpretOrder = val; - } - - /** - * Registers a primary event type interpreter. Note that the interpreter might be - * called with non-primary events if the event is being parsed as a fallback. - * @param {NamespacedValue} wireEventType The event type. - * @param {EventInterpreter} interpreter The interpreter. - */ - public registerInterpreter(wireEventType: NamespacedValue, interpreter: EventInterpreter): void { - this.interpreters.set(wireEventType, interpreter); - } - - /** - * Registers a primary event type interpreter. Note that the interpreter might be - * called with non-primary events if the event is being parsed as a fallback. - * @param {NamespacedValue} wireEventType The event type. - * @param {EventInterpreter} interpreter The interpreter. - */ - public static registerInterpreter( - wireEventType: NamespacedValue, - interpreter: EventInterpreter, - ): void { - ExtensibleEvents.defaultInstance.registerInterpreter(wireEventType, interpreter); - } - - /** - * Parses an event, trying the primary event type first. If the primary type is not known - * then the content will be inspected to find the most suitable fallback. - * - * If the parsing failed or was a completely unknown type, this will return falsy. - * @param {IPartialEvent} wireFormat The event to parse. - * @returns {Optional} The parsed extensible event. - */ - public parse(wireFormat: IPartialEvent): Optional { - try { - if (this.interpreters.hasNamespaced(wireFormat.type)) { - return this.interpreters.getNamespaced(wireFormat.type)!(wireFormat); - } - - for (const tryType of this.unknownInterpretOrder) { - if (this.interpreters.has(tryType)) { - try { - const val = this.interpreters.get(tryType)!(wireFormat); - if (val) return val; - } catch (e) { - if (e instanceof InvalidEventError) { - continue; // clearly can't be parsed as the unknown type - } - // noinspection ExceptionCaughtLocallyJS - throw e; // re-throw everything else - } - } - } - - return null; // cannot be parsed - } catch (e) { - if (e instanceof InvalidEventError) { - return null; // fail parsing and move on - } - throw e; // re-throw everything else - } - } - - /** - * Parses an event, trying the primary event type first. If the primary type is not known - * then the content will be inspected to find the most suitable fallback. - * - * If the parsing failed or was a completely unknown type, this will return falsy. - * @param {IPartialEvent} wireFormat The event to parse. - * @returns {Optional} The parsed extensible event. - */ - public static parse(wireFormat: IPartialEvent): Optional { - return ExtensibleEvents.defaultInstance.parse(wireFormat); - } -} diff --git a/src/v1-old/IPartialEvent.ts b/src/v1-old/IPartialEvent.ts deleted file mode 100644 index 9ed886d..0000000 --- a/src/v1-old/IPartialEvent.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* -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. -*/ - -/** - * Partial types for a Matrix Event. - */ -export interface IPartialEvent { - type: string; - content: TContent; -} diff --git a/src/v1-old/NamespacedMap.ts b/src/v1-old/NamespacedMap.ts deleted file mode 100644 index 62fe5d7..0000000 --- a/src/v1-old/NamespacedMap.ts +++ /dev/null @@ -1,99 +0,0 @@ -import {NamespacedValue} from "../NamespacedValue"; -import {Optional} from "../types"; - -type NS = NamespacedValue; - -/** - * A `Map` implementation which accepts a NamespacedValue as a key, and arbitrary value. The - * namespaced value must be a string type. - */ -export class NamespacedMap { - // protected to make tests happy for access - protected internalMap = new Map(); - - /** - * Creates a new map with optional seed data. - * @param {Array<[NS, V]>} initial The seed data. - */ - public constructor(initial?: [NS, V][]) { - if (initial) { - for (const val of initial) { - this.set(val[0], val[1]); - } - } - } - - /** - * Gets a value from the map. If the value does not exist under - * either namespace option, falsy is returned. - * @param {NS} key The key. - * @returns {Optional} The value, or falsy. - */ - public get(key: NS): Optional { - if (key.name && this.internalMap.has(key.name)) { - return this.internalMap.get(key.name); - } - if (key.altName && this.internalMap.has(key.altName)) { - return this.internalMap.get(key.altName); - } - - return null; - } - - /** - * Sets a value in the map. - * @param {NS} key The key. - * @param {V} val The value. - */ - public set(key: NS, val: V): void { - if (key.name) { - this.internalMap.set(key.name, val); - } - if (key.altName) { - this.internalMap.set(key.altName, val); - } - } - - /** - * Determines if any of the valid namespaced values are present - * in the map. - * @param {NS} key The key. - * @returns {boolean} True if present. - */ - public has(key: NS): boolean { - return !!this.get(key); - } - - /** - * Removes all the namespaced values from the map. - * @param {NS} key The key. - */ - public delete(key: NS): void { - if (key.name) { - this.internalMap.delete(key.name); - } - if (key.altName) { - this.internalMap.delete(key.altName); - } - } - - /** - * Determines if the map contains a specific namespaced value - * instead of the parent NS type. - * @param {string} key The key. - * @returns {boolean} True if present. - */ - public hasNamespaced(key: string): boolean { - return this.internalMap.has(key); - } - - /** - * Gets a specific namespaced value from the map instead of the - * parent NS type. Returns falsy if not found. - * @param {string} key The key. - * @returns {Optional} The value, or falsy. - */ - public getNamespaced(key: string): Optional { - return this.internalMap.get(key); - } -} diff --git a/src/v1-old/events/EmoteEvent.ts b/src/v1-old/events/EmoteEvent.ts deleted file mode 100644 index 9fe7800..0000000 --- a/src/v1-old/events/EmoteEvent.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright 2022 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 {MessageEvent} from "./MessageEvent"; -import {IPartialEvent} from "../IPartialEvent"; -import {M_EMOTE, M_EMOTE_EVENT_CONTENT, M_HTML, M_TEXT} from "./message_types"; -import {EventType, isEventTypeSame} from "../utility/events"; - -// Emote events are just decorated message events - -/** - * Represents an emote. This is essentially a MessageEvent with - * emote characteristics considered. - */ -export class EmoteEvent extends MessageEvent { - public constructor(wireFormat: IPartialEvent) { - super(wireFormat); - } - - public get isEmote(): boolean { - return true; // override - } - - public isEquivalentTo(primaryEventType: EventType): boolean { - return isEventTypeSame(primaryEventType, M_EMOTE) || super.isEquivalentTo(primaryEventType); - } - - public serialize(): IPartialEvent { - const message = super.serialize(); - (message.content)["msgtype"] = "m.emote"; - return message; - } - - /** - * Creates a new EmoteEvent from text and HTML. - * @param {string} text The text. - * @param {string} html Optional HTML. - * @returns {MessageEvent} The representative message event. - */ - public static from(text: string, html?: string): EmoteEvent { - return new EmoteEvent({ - type: M_EMOTE.name, - content: { - [M_TEXT.name]: text, - [M_HTML.name]: html, - }, - }); - } -} diff --git a/src/v1-old/events/ExtensibleEvent.ts b/src/v1-old/events/ExtensibleEvent.ts deleted file mode 100644 index c6f38aa..0000000 --- a/src/v1-old/events/ExtensibleEvent.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2021 - 2022 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 {IPartialEvent} from "../IPartialEvent"; -import {EventType} from "../utility/events"; - -/** - * Represents an Extensible Event in Matrix. - */ -export abstract class ExtensibleEvent { - protected constructor(public readonly wireFormat: IPartialEvent) {} - - /** - * Shortcut to wireFormat.content - */ - public get wireContent(): TContent { - return this.wireFormat.content; - } - - /** - * Serializes the event into a format which can be used to send the - * event to the room. - * @returns {IPartialEvent} The serialized event. - */ - public abstract serialize(): IPartialEvent; - - /** - * Determines if this event is equivalent to the provided event type. - * This is recommended over `instanceof` checks due to issues in the JS - * runtime (and layering of dependencies in some projects). - * - * Implementations should pass this check off to their super classes - * if their own checks fail. Some primary implementations do not extend - * fallback classes given they support the primary type first. Thus, - * those classes may return false if asked about their fallback - * representation. - * - * Note that this only checks primary event types: legacy events, like - * m.room.message, should/will fail this check. - * @param {EventType} primaryEventType The (potentially namespaced) event - * type. - * @returns {boolean} True if this event *could* be represented as the - * given type. - */ - public abstract isEquivalentTo(primaryEventType: EventType): boolean; -} diff --git a/src/v1-old/events/MessageEvent.ts b/src/v1-old/events/MessageEvent.ts deleted file mode 100644 index 6764512..0000000 --- a/src/v1-old/events/MessageEvent.ts +++ /dev/null @@ -1,164 +0,0 @@ -/* -Copyright 2022 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 {ExtensibleEvent} from "./ExtensibleEvent"; -import {IPartialEvent} from "../IPartialEvent"; -import {isOptionalAString, isProvided} from "../types"; -import {InvalidEventError} from "../../events/InvalidEventError"; -import { - IMessageRendering, - M_EMOTE, - M_HTML, - M_MESSAGE, - M_MESSAGE_EVENT_CONTENT, - M_NOTICE, - M_TEXT, -} from "./message_types"; -import {EventType, isEventTypeSame} from "../utility/events"; -import {Optional} from "../../types"; - -/** - * Represents a message event. Message events are the simplest form of event with - * just text (optionally of different mimetypes, like HTML). - * - * Message events can additionally be an Emote or Notice, though typically those - * are represented as EmoteEvent and NoticeEvent respectively. - */ -export class MessageEvent extends ExtensibleEvent { - /** - * The default text for the event. - */ - public readonly text: string; - - /** - * The default HTML for the event, if provided. - */ - public readonly html: Optional; - - /** - * All the different renderings of the message. Note that this is the same - * format as an m.message body but may contain elements not found directly - * in the event content: this is because this is interpreted based off the - * other information available in the event. - */ - public readonly renderings: IMessageRendering[]; - - /** - * Creates a new MessageEvent from a pure format. Note that the event is - * *not* parsed here: it will be treated as a literal m.message primary - * typed event. - * @param {IPartialEvent} wireFormat The event. - */ - public constructor(wireFormat: IPartialEvent) { - super(wireFormat); - - const mmessage = M_MESSAGE.findIn(this.wireContent); - const mtext = M_TEXT.findIn(this.wireContent); - const mhtml = M_HTML.findIn(this.wireContent); - if (isProvided(mmessage)) { - if (!Array.isArray(mmessage)) { - throw new InvalidEventError("MessageEventLegacy", "m.message contents must be an array"); - } - const text = mmessage.find(r => !isProvided(r.mimetype) || r.mimetype === "text/plain"); - const html = mmessage.find(r => r.mimetype === "text/html"); - - if (!text) - throw new InvalidEventError("MessageEventLegacy", "m.message is missing a plain text representation"); - - this.text = text.body; - this.html = html?.body; - this.renderings = mmessage; - } else if (isOptionalAString(mtext)) { - this.text = mtext; - this.html = mhtml; - this.renderings = [{body: this.text, mimetype: "text/plain"}]; - if (this.html) { - this.renderings.push({body: this.html, mimetype: "text/html"}); - } - } else { - throw new InvalidEventError("MessageEventLegacy", "Missing textual representation for event"); - } - } - - /** - * Gets whether this message is considered an "emote". Note that a message - * might be an emote and notice at the same time: while technically possible, - * the event should be interpreted as one or the other. - */ - public get isEmote(): boolean { - return M_EMOTE.matches(this.wireFormat.type) || isProvided(M_EMOTE.findIn(this.wireFormat.content)); - } - - /** - * Gets whether this message is considered a "notice". Note that a message - * might be an emote and notice at the same time: while technically possible, - * the event should be interpreted as one or the other. - */ - public get isNotice(): boolean { - return M_NOTICE.matches(this.wireFormat.type) || isProvided(M_NOTICE.findIn(this.wireFormat.content)); - } - - public isEquivalentTo(primaryEventType: EventType): boolean { - return isEventTypeSame(primaryEventType, M_MESSAGE); - } - - protected serializeMMessageOnly(): M_MESSAGE_EVENT_CONTENT { - let messageRendering: M_MESSAGE_EVENT_CONTENT = { - [M_MESSAGE.name]: this.renderings, - }; - - // Use the shorthand if it's just a simple text event - if (this.renderings.length === 1) { - const mime = this.renderings[0].mimetype; - if (mime === undefined || mime === "text/plain") { - messageRendering = { - [M_TEXT.name]: this.renderings[0].body, - }; - } - } - - return messageRendering; - } - - public serialize(): IPartialEvent { - return { - type: "m.room.message", - content: { - ...this.serializeMMessageOnly(), - body: this.text, - msgtype: "m.text", - format: this.html ? "org.matrix.custom.html" : undefined, - formatted_body: this.html ?? undefined, - }, - }; - } - - /** - * Creates a new MessageEvent from text and HTML. - * @param {string} text The text. - * @param {string} html Optional HTML. - * @returns {MessageEvent} The representative message event. - */ - public static from(text: string, html?: string): MessageEvent { - return new MessageEvent({ - type: M_MESSAGE.name, - content: { - [M_TEXT.name]: text, - [M_HTML.name]: html, - }, - }); - } -} diff --git a/src/v1-old/events/NoticeEvent.ts b/src/v1-old/events/NoticeEvent.ts deleted file mode 100644 index 7e7ecd6..0000000 --- a/src/v1-old/events/NoticeEvent.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright 2022 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 {MessageEvent} from "./MessageEvent"; -import {IPartialEvent} from "../IPartialEvent"; -import {M_HTML, M_NOTICE, M_NOTICE_EVENT_CONTENT, M_TEXT} from "./message_types"; -import {EventType, isEventTypeSame} from "../utility/events"; - -// Notice events are just decorated message events - -/** - * Represents a notice. This is essentially a MessageEvent with - * notice characteristics considered. - */ -export class NoticeEvent extends MessageEvent { - public constructor(wireFormat: IPartialEvent) { - super(wireFormat); - } - - public get isNotice(): boolean { - return true; // override - } - - public isEquivalentTo(primaryEventType: EventType): boolean { - return isEventTypeSame(primaryEventType, M_NOTICE) || super.isEquivalentTo(primaryEventType); - } - - public serialize(): IPartialEvent { - const message = super.serialize(); - (message.content)["msgtype"] = "m.notice"; - return message; - } - - /** - * Creates a new NoticeEvent from text and HTML. - * @param {string} text The text. - * @param {string} html Optional HTML. - * @returns {MessageEvent} The representative message event. - */ - public static from(text: string, html?: string): NoticeEvent { - return new NoticeEvent({ - type: M_NOTICE.name, - content: { - [M_TEXT.name]: text, - [M_HTML.name]: html, - }, - }); - } -} diff --git a/src/v1-old/events/PollEndEvent.ts b/src/v1-old/events/PollEndEvent.ts deleted file mode 100644 index 9519042..0000000 --- a/src/v1-old/events/PollEndEvent.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright 2022 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 {M_POLL_END, M_POLL_END_EVENT_CONTENT} from "./poll_types"; -import {IPartialEvent} from "../IPartialEvent"; -import {InvalidEventError} from "../../events/InvalidEventError"; -import {REFERENCE_RELATION} from "./relationship_types"; -import {MessageEvent} from "./MessageEvent"; -import {M_TEXT} from "./message_types"; -import {EventType, isEventTypeSame} from "../utility/events"; -import {ExtensibleEvent} from "./ExtensibleEvent"; - -/** - * Represents a poll end/closure event. - */ -export class PollEndEvent extends ExtensibleEvent { - /** - * The poll start event ID referenced by the response. - */ - public readonly pollEventId: string; - - /** - * The closing message for the event. - */ - public readonly closingMessage: MessageEvent; - - /** - * Creates a new PollEndEvent from a pure format. Note that the event is *not* - * parsed here: it will be treated as a literal m.poll.response primary typed event. - * @param {IPartialEvent} wireFormat The event. - */ - public constructor(wireFormat: IPartialEvent) { - super(wireFormat); - - const rel = this.wireContent["m.relates_to"]; - // noinspection SuspiciousTypeOfGuard - if (!REFERENCE_RELATION.matches(rel?.rel_type) || typeof rel.event_id !== "string") { - throw new InvalidEventError("PollEndEventLegacy", "Relationship must be a reference to an event"); - } - - this.pollEventId = rel.event_id; - this.closingMessage = new MessageEvent(this.wireFormat); - } - - public isEquivalentTo(primaryEventType: EventType): boolean { - return isEventTypeSame(primaryEventType, M_POLL_END); - } - - public serialize(): IPartialEvent { - return { - type: M_POLL_END.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: this.pollEventId, - }, - [M_POLL_END.name]: {}, - ...this.closingMessage.serialize().content, - }, - }; - } - - /** - * Creates a new PollEndEvent from a poll event ID. - * @param {string} pollEventId The poll start event ID. - * @param {string} message A closing message, typically revealing the top answer. - * @returns {PollStartEvent} The representative poll closure event. - */ - public static from(pollEventId: string, message: string): PollEndEvent { - return new PollEndEvent({ - type: M_POLL_END.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: pollEventId, - }, - [M_POLL_END.name]: {}, - [M_TEXT.name]: message, - }, - }); - } -} diff --git a/src/v1-old/events/PollResponseEvent.ts b/src/v1-old/events/PollResponseEvent.ts deleted file mode 100644 index e9c6b13..0000000 --- a/src/v1-old/events/PollResponseEvent.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* -Copyright 2022 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 {ExtensibleEvent} from "./ExtensibleEvent"; -import {M_POLL_RESPONSE, M_POLL_RESPONSE_EVENT_CONTENT, M_POLL_RESPONSE_SUBTYPE} from "./poll_types"; -import {IPartialEvent} from "../IPartialEvent"; -import {InvalidEventError} from "../../events/InvalidEventError"; -import {PollStartEvent} from "./PollStartEvent"; -import {REFERENCE_RELATION} from "./relationship_types"; -import {EventType, isEventTypeSame} from "../utility/events"; -import {Optional} from "../../types"; - -/** - * Represents a poll response event. - */ -export class PollResponseEvent extends ExtensibleEvent { - private internalAnswerIds: string[] = []; - private internalSpoiled: boolean = false; - - /** - * The provided answers for the poll. Note that this may be falsy/unpredictable if - * the `spoiled` property is true. - */ - public get answerIds(): string[] { - return this.internalAnswerIds; - } - - /** - * The poll start event ID referenced by the response. - */ - public readonly pollEventId: string; - - /** - * Whether the vote is spoiled. - */ - public get spoiled(): boolean { - return this.internalSpoiled; - } - - /** - * Creates a new PollResponseEvent from a pure format. Note that the event is *not* - * parsed here: it will be treated as a literal m.poll.response primary typed event. - * - * To validate the response against a poll, call `validateAgainst` after creation. - * @param {IPartialEvent} wireFormat The event. - */ - public constructor(wireFormat: IPartialEvent) { - super(wireFormat); - - const rel = this.wireContent["m.relates_to"]; - // noinspection SuspiciousTypeOfGuard - if (!REFERENCE_RELATION.matches(rel?.rel_type) || typeof rel.event_id !== "string") { - throw new InvalidEventError("PollResponseEventLegacy", "Relationship must be a reference to an event"); - } - - this.pollEventId = rel.event_id; - this.validateAgainst(null); - } - - /** - * Validates the poll response using the poll start event as a frame of reference. This - * is used to determine if the vote is spoiled, whether the answers are valid, etc. - * - * Validating against a falsy poll start event resets the response event to be against - * an unknown poll, implying a valid (unspoiled) response. - * @param {Optional} poll The poll start event. - */ - public validateAgainst(poll: Optional): void { - const response = M_POLL_RESPONSE.findIn(this.wireContent); - if (!response?.answers || !Array.isArray(response.answers)) { - this.internalSpoiled = true; - this.internalAnswerIds = []; - return; - } - - let answers = response.answers; - // noinspection SuspiciousTypeOfGuard - We're checking the wire types because TS is only compile-time safe. - if (answers.some(a => typeof a !== "string") || answers.length === 0) { - this.internalSpoiled = true; - this.internalAnswerIds = []; - return; - } - - if (poll) { - if (answers.some(a => !poll.answers.some(pa => pa.id === a))) { - this.internalSpoiled = true; - this.internalAnswerIds = []; - return; - } - - answers = answers.slice(0, poll.maxSelections); - } - - this.internalAnswerIds = answers; - this.internalSpoiled = false; - } - - public isEquivalentTo(primaryEventType: EventType): boolean { - return isEventTypeSame(primaryEventType, M_POLL_RESPONSE); - } - - public serialize(): IPartialEvent { - return { - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: this.pollEventId, - }, - [M_POLL_RESPONSE.name]: { - answers: this.spoiled ? undefined : this.answerIds, - }, - }, - }; - } - - /** - * Creates a new PollResponseEvent from a set of answers. To spoil the vote, pass an empty - * answers array. - * @param {string} answers The user's answers. Should be valid from a poll's answer IDs. - * @param {string} pollEventId The poll start event ID. - * @returns {PollStartEvent} The representative poll response event. - */ - public static from(answers: string[], pollEventId: string): PollResponseEvent { - return new PollResponseEvent({ - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: pollEventId, - }, - [M_POLL_RESPONSE.name]: { - answers: answers, - }, - }, - }); - } -} diff --git a/src/v1-old/events/PollStartEvent.ts b/src/v1-old/events/PollStartEvent.ts deleted file mode 100644 index a871c24..0000000 --- a/src/v1-old/events/PollStartEvent.ts +++ /dev/null @@ -1,208 +0,0 @@ -/* -Copyright 2022 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 { - KNOWN_POLL_KIND, - M_POLL_KIND_DISCLOSED, - M_POLL_KIND_UNDISCLOSED, - M_POLL_START, - M_POLL_START_EVENT_CONTENT, - M_POLL_START_SUBTYPE, - POLL_ANSWER, -} from "./poll_types"; -import {IPartialEvent} from "../IPartialEvent"; -import {MessageEvent} from "./MessageEvent"; -import {M_TEXT} from "./message_types"; -import {InvalidEventError} from "../../events/InvalidEventError"; -import {NamespacedValue} from "../../NamespacedValue"; -import {EventType, isEventTypeSame} from "../utility/events"; -import {ExtensibleEvent} from "./ExtensibleEvent"; -import {isNumberFinite} from "../types"; - -/** - * Represents a poll answer. Note that this is represented as a subtype and is - * not registered as a parsable event - it is implied for usage exclusively - * within the PollStartEvent parsing. - */ -export class PollAnswerSubevent extends MessageEvent { - /** - * The answer ID. - */ - public readonly id: string; - - public constructor(wireFormat: IPartialEvent) { - super(wireFormat); - - const id = wireFormat.content.id; - if (!id || typeof id !== "string") { - throw new InvalidEventError("PollStartEventLegacy", "Answer ID must be a non-empty string"); - } - this.id = id; - } - - public serialize(): IPartialEvent { - return { - type: "org.matrix.sdk.poll.answer", - content: { - id: this.id, - ...this.serializeMMessageOnly(), - }, - }; - } - - /** - * Creates a new PollAnswerSubevent from ID and text. - * @param {string} id The answer ID (unique within the poll). - * @param {string} text The text. - * @returns {PollAnswerSubevent} The representative answer. - */ - public static from(id: string, text: string): PollAnswerSubevent { - return new PollAnswerSubevent({ - type: "org.matrix.sdk.poll.answer", - content: { - id: id, - [M_TEXT.name]: text, - }, - }); - } -} - -/** - * Represents a poll start event. - */ -export class PollStartEvent extends ExtensibleEvent { - /** - * The question being asked, as a MessageEvent node. - */ - public readonly question: MessageEvent; - - /** - * The interpreted kind of poll. Note that this will infer a value that is known to the - * SDK rather than verbatim - this means unknown types will be represented as undisclosed - * polls. - * - * To get the raw kind, use rawKind. - */ - public readonly kind: KNOWN_POLL_KIND; - - /** - * The true kind as provided by the event sender. Might not be valid. - */ - public readonly rawKind: string; - - /** - * The maximum number of selections a user is allowed to make. - */ - public readonly maxSelections: number; - - /** - * The possible answers for the poll. - */ - public readonly answers: PollAnswerSubevent[]; - - /** - * Creates a new PollStartEvent from a pure format. Note that the event is *not* - * parsed here: it will be treated as a literal m.poll.start primary typed event. - * @param {IPartialEvent} wireFormat The event. - */ - public constructor(wireFormat: IPartialEvent) { - super(wireFormat); - - const poll = M_POLL_START.findIn(this.wireContent); - - if (!poll?.question) { - throw new InvalidEventError("PollStartEventLegacy", "A question is required"); - } - - this.question = new MessageEvent({type: "org.matrix.sdk.poll.question", content: poll.question}); - - this.rawKind = poll.kind; - if (M_POLL_KIND_DISCLOSED.matches(this.rawKind)) { - this.kind = M_POLL_KIND_DISCLOSED; - } else { - this.kind = M_POLL_KIND_UNDISCLOSED; // default & assumed value - } - - this.maxSelections = isNumberFinite(poll.max_selections) && poll.max_selections > 0 ? poll.max_selections : 1; - - if (!Array.isArray(poll.answers)) { - throw new InvalidEventError("PollStartEventLegacy", "Poll answers must be an array"); - } - const answers = poll.answers.slice(0, 20).map( - a => - new PollAnswerSubevent({ - type: "org.matrix.sdk.poll.answer", - content: a, - }), - ); - if (answers.length <= 0) { - throw new InvalidEventError("PollStartEventLegacy", "No answers available"); - } - this.answers = answers; - } - - public isEquivalentTo(primaryEventType: EventType): boolean { - return isEventTypeSame(primaryEventType, M_POLL_START); - } - - public serialize(): IPartialEvent { - return { - type: M_POLL_START.name, - content: { - [M_POLL_START.name]: { - question: this.question.serialize().content, - kind: this.rawKind, - max_selections: this.maxSelections, - answers: this.answers.map(a => a.serialize().content), - }, - [M_TEXT.name]: `${this.question.text}\n${this.answers.map((a, i) => `${i + 1}. ${a.text}`).join("\n")}`, - }, - }; - } - - /** - * Creates a new PollStartEvent from question, answers, and metadata. - * @param {string} question The question to ask. - * @param {string} answers The answers. Should be unique within each other. - * @param {KNOWN_POLL_KIND|string} kind The kind of poll. - * @param {number} maxSelections The maximum number of selections. Must be 1 or higher. - * @returns {PollStartEvent} The representative poll start event. - */ - public static from( - question: string, - answers: string[], - kind: KNOWN_POLL_KIND | string, - maxSelections = 1, - ): PollStartEvent { - return new PollStartEvent({ - type: M_POLL_START.name, - content: { - [M_TEXT.name]: question, // unused by parsing - [M_POLL_START.name]: { - question: {[M_TEXT.name]: question}, - kind: kind instanceof NamespacedValue ? kind.name : kind, - max_selections: maxSelections, - answers: answers.map(a => ({id: makeId(), [M_TEXT.name]: a})), - }, - }, - }); - } -} - -const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; -function makeId(): string { - return [...Array(16)].map(() => LETTERS.charAt(Math.floor(Math.random() * LETTERS.length))).join(""); -} diff --git a/src/v1-old/events/message_types.ts b/src/v1-old/events/message_types.ts deleted file mode 100644 index d0be7f2..0000000 --- a/src/v1-old/events/message_types.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright 2022 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 {UnstableValue} from "../../NamespacedValue"; -import {OptionalPartial} from "../types"; -import {EitherAnd} from "../../types"; - -/** - * The namespaced value for m.message - */ -export const M_MESSAGE = new UnstableValue("m.message", "org.matrix.msc1767.message"); - -/** - * An m.message event rendering - */ -export interface IMessageRendering { - body: string; - mimetype?: string; -} - -/** - * The content for an m.message event - */ -export type M_MESSAGE_EVENT = EitherAnd< - {[M_MESSAGE.name]: IMessageRendering[]}, - {[M_MESSAGE.altName]: IMessageRendering[]} ->; - -/** - * The namespaced value for m.text - */ -export const M_TEXT = new UnstableValue("m.text", "org.matrix.msc1767.text"); - -/** - * The content for an m.text event - */ -export type M_TEXT_EVENT = EitherAnd<{[M_TEXT.name]: string}, {[M_TEXT.altName]: string}>; - -/** - * The namespaced value for m.html - */ -export const M_HTML = new UnstableValue("m.html", "org.matrix.msc1767.html"); - -/** - * The content for an m.html event - */ -export type M_HTML_EVENT = EitherAnd<{[M_HTML.name]: string}, {[M_HTML.altName]: string}>; - -/** - * The content for an m.message, m.text, or m.html event - */ -export type M_MESSAGE_EVENT_CONTENT = M_MESSAGE_EVENT | M_TEXT_EVENT | OptionalPartial; - -/** - * The namespaced value for m.emote - */ -export const M_EMOTE = new UnstableValue("m.emote", "org.matrix.msc1767.emote"); - -/** - * The event definition for an m.emote event (in content) - */ -export type M_EMOTE_EVENT = EitherAnd<{[M_EMOTE.name]?: {}}, {[M_EMOTE.altName]?: {}}>; - -/** - * The content for an m.emote event - */ -export type M_EMOTE_EVENT_CONTENT = M_MESSAGE_EVENT_CONTENT & M_EMOTE_EVENT; - -/** - * The namespaced value for m.notice - */ -export const M_NOTICE = new UnstableValue("m.notice", "org.matrix.msc1767.notice"); - -/** - * The event definition for an m.notice event (in content) - */ -export type M_NOTICE_EVENT = EitherAnd<{[M_NOTICE.name]?: {}}, {[M_NOTICE.altName]?: {}}>; - -/** - * The content for an m.notice event - */ -export type M_NOTICE_EVENT_CONTENT = M_MESSAGE_EVENT_CONTENT & M_NOTICE_EVENT; diff --git a/src/v1-old/events/poll_types.ts b/src/v1-old/events/poll_types.ts deleted file mode 100644 index 8dcbfa7..0000000 --- a/src/v1-old/events/poll_types.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* -Copyright 2022 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 {UnstableValue} from "../../NamespacedValue"; -import {TSNamespace} from "../types"; -import {M_MESSAGE_EVENT_CONTENT} from "./message_types"; -import {REFERENCE_RELATION, RELATES_TO_RELATIONSHIP} from "./relationship_types"; -import {EitherAnd} from "../../types"; - -/** - * Identifier for a disclosed poll. - */ -export const M_POLL_KIND_DISCLOSED = new UnstableValue("m.poll.disclosed", "org.matrix.msc3381.poll.disclosed"); - -/** - * Identifier for an undisclosed poll. - */ -export const M_POLL_KIND_UNDISCLOSED = new UnstableValue("m.poll.undisclosed", "org.matrix.msc3381.poll.undisclosed"); - -/** - * Any poll kind. - */ -export type POLL_KIND = - | TSNamespace - | TSNamespace - | string; - -/** - * Known poll kind namespaces. - */ -export type KNOWN_POLL_KIND = typeof M_POLL_KIND_DISCLOSED | typeof M_POLL_KIND_UNDISCLOSED; - -/** - * The namespaced value for m.poll.start - */ -export const M_POLL_START = new UnstableValue("m.poll.start", "org.matrix.msc3381.poll.start"); - -/** - * The m.poll.start type within event content - */ -export type M_POLL_START_SUBTYPE = { - question: M_MESSAGE_EVENT_CONTENT; - kind: POLL_KIND; - max_selections?: number; // default 1, always positive - answers: POLL_ANSWER[]; -}; - -/** - * A poll answer. - */ -export type POLL_ANSWER = M_MESSAGE_EVENT_CONTENT & {id: string}; - -/** - * The event definition for an m.poll.start event (in content) - */ -export type M_POLL_START_EVENT = EitherAnd< - {[M_POLL_START.name]: M_POLL_START_SUBTYPE}, - {[M_POLL_START.altName]: M_POLL_START_SUBTYPE} ->; - -/** - * The content for an m.poll.start event - */ -export type M_POLL_START_EVENT_CONTENT = M_POLL_START_EVENT & M_MESSAGE_EVENT_CONTENT; - -/** - * The namespaced value for m.poll.response - */ -export const M_POLL_RESPONSE = new UnstableValue("m.poll.response", "org.matrix.msc3381.poll.response"); - -/** - * The m.poll.response type within event content - */ -export type M_POLL_RESPONSE_SUBTYPE = { - answers: string[]; -}; - -/** - * The event definition for an m.poll.response event (in content) - */ -export type M_POLL_RESPONSE_EVENT = EitherAnd< - {[M_POLL_RESPONSE.name]: M_POLL_RESPONSE_SUBTYPE}, - {[M_POLL_RESPONSE.altName]: M_POLL_RESPONSE_SUBTYPE} ->; - -/** - * The content for an m.poll.response event - */ -export type M_POLL_RESPONSE_EVENT_CONTENT = M_POLL_RESPONSE_EVENT & RELATES_TO_RELATIONSHIP; - -/** - * The namespaced value for m.poll.end - */ -export const M_POLL_END = new UnstableValue("m.poll.end", "org.matrix.msc3381.poll.end"); - -/** - * The event definition for an m.poll.end event (in content) - */ -export type M_POLL_END_EVENT = EitherAnd<{[M_POLL_END.name]: {}}, {[M_POLL_END.altName]: {}}>; - -/** - * The content for an m.poll.end event - */ -export type M_POLL_END_EVENT_CONTENT = M_POLL_END_EVENT & - RELATES_TO_RELATIONSHIP & - M_MESSAGE_EVENT_CONTENT; diff --git a/src/v1-old/events/relationship_types.ts b/src/v1-old/events/relationship_types.ts deleted file mode 100644 index c1793de..0000000 --- a/src/v1-old/events/relationship_types.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2022 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 {NamespacedValue} from "../../NamespacedValue"; -import {DefaultNever, TSNamespace} from "../types"; - -/** - * The namespaced value for an m.reference relation - */ -export const REFERENCE_RELATION = new NamespacedValue("m.reference"); - -/** - * Represents any relation type - */ -export type ANY_RELATION = TSNamespace | string; - -/** - * An m.relates_to relationship - */ -export type RELATES_TO_RELATIONSHIP = { - "m.relates_to": { - // See https://github.com/microsoft/TypeScript/issues/23182#issuecomment-379091887 for array syntax - rel_type: [R] extends [never] ? ANY_RELATION : TSNamespace; - event_id: string; - } & DefaultNever; -}; diff --git a/src/v1-old/interpreters/legacy/MRoomMessage.ts b/src/v1-old/interpreters/legacy/MRoomMessage.ts deleted file mode 100644 index 662515f..0000000 --- a/src/v1-old/interpreters/legacy/MRoomMessage.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* -Copyright 2022 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 {IPartialEvent} from "../../IPartialEvent"; -import {Optional} from "../../../types"; -import {ExtensibleEvent} from "../../events/ExtensibleEvent"; -import {MessageEvent} from "../../events/MessageEvent"; -import {NoticeEvent} from "../../events/NoticeEvent"; -import {EmoteEvent} from "../../events/EmoteEvent"; -import {NamespacedValue} from "../../../NamespacedValue"; -import {M_HTML, M_MESSAGE, M_MESSAGE_EVENT_CONTENT, M_TEXT} from "../../events/message_types"; - -export const LEGACY_M_ROOM_MESSAGE = new NamespacedValue("m.room.message"); - -export interface IPartialLegacyContent { - body: string; - msgtype: string; - format?: string; - formatted_body?: string; -} - -export function parseMRoomMessage(wireEvent: IPartialEvent): Optional { - if (M_MESSAGE.findIn(wireEvent.content) || M_TEXT.findIn(wireEvent.content)) { - // We know enough about the event to coerce it into the right type - return new MessageEvent(wireEvent as unknown as IPartialEvent); - } - - if (!wireEvent.content) return null; - - const msgtype = wireEvent.content.msgtype; - const text = wireEvent.content.body; - const html = wireEvent.content.format === "org.matrix.custom.html" ? wireEvent.content.formatted_body : null; - - if (msgtype === "m.text") { - return new MessageEvent({ - ...wireEvent, - content: { - ...wireEvent.content, - [M_TEXT.name]: text, - [M_HTML.name]: html, - }, - }); - } else if (msgtype === "m.notice") { - return new NoticeEvent({ - ...wireEvent, - content: { - ...wireEvent.content, - [M_TEXT.name]: text, - [M_HTML.name]: html, - }, - }); - } else if (msgtype === "m.emote") { - return new EmoteEvent({ - ...wireEvent, - content: { - ...wireEvent.content, - [M_TEXT.name]: text, - [M_HTML.name]: html, - }, - }); - } else { - // TODO: Handle other types - return null; - } -} diff --git a/src/v1-old/interpreters/modern/MMessage.ts b/src/v1-old/interpreters/modern/MMessage.ts deleted file mode 100644 index 61f50b7..0000000 --- a/src/v1-old/interpreters/modern/MMessage.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright 2022 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 {IPartialEvent} from "../../IPartialEvent"; -import {Optional} from "../../../types"; -import {MessageEvent} from "../../events/MessageEvent"; -import {M_EMOTE, M_MESSAGE_EVENT_CONTENT, M_NOTICE} from "../../events/message_types"; -import {EmoteEvent} from "../../events/EmoteEvent"; -import {NoticeEvent} from "../../events/NoticeEvent"; - -export function parseMMessage(wireEvent: IPartialEvent): Optional { - if (M_EMOTE.matches(wireEvent.type)) { - return new EmoteEvent(wireEvent); - } else if (M_NOTICE.matches(wireEvent.type)) { - return new NoticeEvent(wireEvent); - } - - // default: return a generic message - return new MessageEvent(wireEvent); -} diff --git a/src/v1-old/interpreters/modern/MPoll.ts b/src/v1-old/interpreters/modern/MPoll.ts deleted file mode 100644 index 74204fb..0000000 --- a/src/v1-old/interpreters/modern/MPoll.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2022 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 {IPartialEvent} from "../../IPartialEvent"; -import {Optional} from "../../../types"; -import { - M_POLL_END, - M_POLL_END_EVENT_CONTENT, - M_POLL_RESPONSE, - M_POLL_RESPONSE_EVENT_CONTENT, - M_POLL_START, - M_POLL_START_EVENT_CONTENT, -} from "../../events/poll_types"; -import {PollStartEvent} from "../../events/PollStartEvent"; -import {PollResponseEvent} from "../../events/PollResponseEvent"; -import {PollEndEvent} from "../../events/PollEndEvent"; - -type PollContent = M_POLL_START_EVENT_CONTENT | M_POLL_RESPONSE_EVENT_CONTENT | M_POLL_END_EVENT_CONTENT; -type PollEvent = PollStartEvent | PollResponseEvent | PollEndEvent; - -export function parseMPoll(wireEvent: IPartialEvent): Optional { - if (M_POLL_START.matches(wireEvent.type)) { - return new PollStartEvent(wireEvent as IPartialEvent); - } else if (M_POLL_RESPONSE.matches(wireEvent.type)) { - return new PollResponseEvent(wireEvent as IPartialEvent); - } else if (M_POLL_END.matches(wireEvent.type)) { - return new PollEndEvent(wireEvent as IPartialEvent); - } - - return null; // not a poll event -} diff --git a/src/v1-old/types.ts b/src/v1-old/types.ts deleted file mode 100644 index fac4c98..0000000 --- a/src/v1-old/types.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* -Copyright 2021 - 2022 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 {NamespacedValue} from "../NamespacedValue"; -import {Optional} from "../types"; - -/** - * Applies the same behaviour as `Partial`, but using `Optional` instead. - */ -export type OptionalPartial = { - [P in keyof T]: Optional; -}; - -/** - * Determines if the given optional string is a defined string. - * @param {Optional} s The input string. - * @returns {boolean} True if the input is a defined string. - */ -export function isOptionalAString(s: Optional): s is string { - return isProvided(s) && typeof s === "string"; -} - -/** - * Determines if the given optional was provided a value. - * @param {Optional} s The optional to test. - * @returns {boolean} True if the value is defined. - */ -export function isProvided(s: Optional): boolean { - return s !== null && s !== undefined; -} - -/** - * TypeScript wrapper for `Number.isFinite`. - * @param n The number - * @returns True if the number is finite. - * @see Number#isFinite - */ -export function isNumberFinite(n: unknown): n is number { - return Number.isFinite(n); -} - -/** - * Represents the stable and unstable values of a given namespace. - */ -export type TSNamespace = N extends NamespacedValue - ? TSNamespaceValue | TSNamespaceValue - : never; - -/** - * Represents a namespaced value, if the value is a string. Used to extract provided types - * from a TSNamespace (in cases where only stable *or* unstable is provided). - */ -export type TSNamespaceValue = V extends string ? V : never; - -/** - * Creates a type which is V when T is `never`, otherwise T. - */ -// See https://github.com/microsoft/TypeScript/issues/23182#issuecomment-379091887 for details on the array syntax. -export type DefaultNever = [T] extends [never] ? V : T; diff --git a/src/v1-old/utility/MessageMatchers.ts b/src/v1-old/utility/MessageMatchers.ts deleted file mode 100644 index 64a96a2..0000000 --- a/src/v1-old/utility/MessageMatchers.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright 2022 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 {IPartialEvent} from "../IPartialEvent"; -import {IPartialLegacyContent} from "../interpreters/legacy/MRoomMessage"; -import {EitherAnd} from "../../types"; -import {M_EMOTE, M_MESSAGE, M_MESSAGE_EVENT_CONTENT, M_NOTICE} from "../events/message_types"; - -/** - * Represents a legacy m.room.message msgtype - */ -export enum LegacyMsgType { - Text = "m.text", - Notice = "m.notice", - Emote = "m.emote", - // TODO: The others -} - -/** - * Determines if the given partial event looks similar enough to the given legacy msgtype - * to count as that message type. - * @param {IPartialEvent>} event The event. - * @param {LegacyMsgType} msgtype The message type to compare for. - * @returns {boolean} True if the event appears to look similar enough to the msgtype. - */ -export function isEventLike( - event: IPartialEvent>, - msgtype: LegacyMsgType, -): boolean { - const content = event.content; - if (msgtype === LegacyMsgType.Text) { - return M_MESSAGE.matches(event.type) || (event.type === "m.room.message" && content?.["msgtype"] === "m.text"); - } else if (msgtype === LegacyMsgType.Emote) { - return M_EMOTE.matches(event.type) || (event.type === "m.room.message" && content?.["msgtype"] === "m.emote"); - } else if (msgtype === LegacyMsgType.Notice) { - return M_NOTICE.matches(event.type) || (event.type === "m.room.message" && content?.["msgtype"] === "m.notice"); - } - return false; -} diff --git a/src/v1-old/utility/events.ts b/src/v1-old/utility/events.ts deleted file mode 100644 index 6132e8e..0000000 --- a/src/v1-old/utility/events.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2022 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 {NamespacedValue} from "../../NamespacedValue"; -import {isOptionalAString} from "../types"; - -/** - * Represents a potentially namespaced event type. - */ -export type EventType = NamespacedValue | string; - -/** - * Determines if two event types are the same, including namespaces. - * @param {EventType} given The given event type. This will be compared - * against the expected type. - * @param {EventType} expected The expected event type. - * @returns {boolean} True if the given type matches the expected type. - */ -export function isEventTypeSame(given: EventType, expected: EventType): boolean { - if (typeof given === "string") { - if (typeof expected === "string") { - return expected === given; - } else { - return (expected as NamespacedValue).matches(given as string); - } - } else { - if (typeof expected === "string") { - return (given as NamespacedValue).matches(expected as string); - } else { - const expectedNs = expected as NamespacedValue; - const givenNs = given as NamespacedValue; - return ( - expectedNs.matches(givenNs.name) || - (isOptionalAString(givenNs.altName) && expectedNs.matches(givenNs.altName)) - ); - } - } -} diff --git a/test/v1-old/ExtensibleEvents.test.ts b/test/v1-old/ExtensibleEvents.test.ts deleted file mode 100644 index 27ec9b7..0000000 --- a/test/v1-old/ExtensibleEvents.test.ts +++ /dev/null @@ -1,373 +0,0 @@ -/* -Copyright 2022 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 { - EmoteEvent, - EventType, - ExtensibleEvent, - ExtensibleEvents, - InvalidEventError, - IPartialEvent, - IPartialLegacyContent, - M_EMOTE, - M_MESSAGE, - M_MESSAGE_EVENT_CONTENT, - M_NOTICE, - M_POLL_END, - M_POLL_END_EVENT_CONTENT, - M_POLL_KIND_DISCLOSED, - M_POLL_RESPONSE, - M_POLL_RESPONSE_EVENT_CONTENT, - M_POLL_START, - M_POLL_START_EVENT_CONTENT, - M_TEXT, - MessageEvent, - NoticeEvent, - Optional, - PollEndEvent, - PollResponseEvent, - PollStartEvent, - REFERENCE_RELATION, - UnstableValue, -} from "../../src"; - -describe("ExtensibleEvents", () => { - afterEach(() => { - // gutwrench the default instance into something safe/new to "reset" it - (ExtensibleEvents)._defaultInstance = new ExtensibleEvents(); - }); - - describe("static api", () => { - it("should return an instance by default", () => { - expect(ExtensibleEvents.defaultInstance).toBeDefined(); - }); - }); - - describe("unknown events", () => { - it("should parse unknown event types with a fallback to m.message", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - isThisATopLevelProp: true, - "org.example.message-like": { - hello: "world", - }, - [M_TEXT.name]: "Hello World", - }, - }; - const event: Optional = new ExtensibleEvents().parse(input); - expect(event).toBeDefined(); - expect(event instanceof MessageEvent).toBe(true); - expect(event!.isEquivalentTo(M_MESSAGE)).toBe(true); - const messageEvent = event as MessageEvent; - expect(messageEvent.text).toBe("Hello World"); - }); - - it("should return falsy for entirely unknown types", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - // Note lack of fallback opportunities - isThisATopLevelProp: true, - "org.example.message-like": { - hello: "world", - }, - }, - }; - const event = new ExtensibleEvents().parse(input); - expect(event).toBeFalsy(); - }); - - describe("static api", () => { - afterEach(() => { - ExtensibleEvents.unknownInterpretOrder = new ExtensibleEvents().unknownInterpretOrder; - }); - - it("should persist the unknown interpret order", () => { - expect(ExtensibleEvents.unknownInterpretOrder.length).toBeGreaterThan(0); - - const testValue1 = new UnstableValue(null, "org.matrix.example.feature1"); - const testValue2 = new UnstableValue(null, "org.matrix.example.feature2"); - const array = [testValue1, testValue2]; - ExtensibleEvents.unknownInterpretOrder = array; - - expect(ExtensibleEvents.unknownInterpretOrder).toBe(array); - }); - }); - }); - - describe("custom events", () => { - class MyCustomEvent extends ExtensibleEvent { - public constructor(wireEvent: IPartialEvent) { - super(wireEvent); - expect(wireEvent?.content.hello).toEqual("world"); - } - - public serialize(): IPartialEvent { - throw new Error("Not implemented for tests"); - } - - public isEquivalentTo(primaryEventType: EventType): boolean { - throw new Error("Not implemented for tests"); - } - } - - function myInterpreter(wireEvent: IPartialEvent): MyCustomEvent { - return new MyCustomEvent(wireEvent); - } - - const myNamespace = new UnstableValue(null, "org.example.custom.event"); - - it("should support custom interpreters", () => { - const input: IPartialEvent = { - type: myNamespace.name, - content: { - hello: "world", - }, - }; - - const extev = new ExtensibleEvents(); - - let event = extev.parse(input); - expect(event).toBeFalsy(); - - extev.registerInterpreter(myNamespace, myInterpreter); - event = extev.parse(input); - expect(event).toBeDefined(); - expect(event instanceof MyCustomEvent).toBe(true); - }); - - it("should support changing unknown parse order", () => { - const input: IPartialEvent = { - type: "org.example.custom.not.under.namespace", - content: { - hello: "world", - }, - }; - - const extev = new ExtensibleEvents(); - - let event = extev.parse(input); - expect(event).toBeFalsy(); - - extev.registerInterpreter(myNamespace, myInterpreter); - event = extev.parse(input); - expect(event).toBeFalsy(); - - extev.unknownInterpretOrder = [myNamespace, M_MESSAGE]; - event = extev.parse(input); - expect(event).toBeDefined(); - expect(event instanceof MyCustomEvent).toBe(true); - }); - - describe("static api", () => { - it("should support custom interpreters", () => { - const input: IPartialEvent = { - type: myNamespace.name, - content: { - hello: "world", - }, - }; - - let event = ExtensibleEvents.parse(input); - expect(event).toBeFalsy(); - - ExtensibleEvents.registerInterpreter(myNamespace, myInterpreter); - event = ExtensibleEvents.parse(input); - expect(event).toBeDefined(); - expect(event instanceof MyCustomEvent).toBe(true); - }); - }); - }); - - describe("known events", () => { - // Dev note: The "should parse X type" cases are not meant to be exhaustive. Just - // a quick check to make sure the event comes out on the other end as the correct - // type. - - it("should parse legacy m.text room message events", () => { - const input: IPartialEvent = { - type: "m.room.message", - content: { - msgtype: "m.text", - body: "Text here", - }, - }; - const message = new ExtensibleEvents().parse(input); - expect(message).toBeDefined(); - expect(message instanceof MessageEvent).toBe(true); - expect(message!.isEquivalentTo(M_MESSAGE)).toBe(true); - const messageEvent = message as MessageEvent; - expect(messageEvent.text).toEqual("Text here"); - }); - - it("should parse modern m.message events", () => { - const input: IPartialEvent = { - type: M_MESSAGE.name, - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new ExtensibleEvents().parse(input); - expect(message).toBeDefined(); - expect(message instanceof MessageEvent).toBe(true); - expect(message!.isEquivalentTo(M_MESSAGE)).toBe(true); - const messageEvent = message as MessageEvent; - expect(messageEvent.text).toEqual("Text here"); - }); - - it("should parse modern m.emote events", () => { - const input: IPartialEvent = { - type: M_EMOTE.name, - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new ExtensibleEvents().parse(input); - expect(message).toBeDefined(); - expect(message instanceof EmoteEvent).toBe(true); - expect(message!.isEquivalentTo(M_EMOTE)).toBe(true); - expect(message!.isEquivalentTo(M_MESSAGE)).toBe(true); - const messageEvent = message as EmoteEvent; - expect(messageEvent.text).toEqual("Text here"); - }); - - it("should parse modern m.notice events", () => { - const input: IPartialEvent = { - type: M_NOTICE.name, - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new ExtensibleEvents().parse(input); - expect(message).toBeDefined(); - expect(message instanceof NoticeEvent).toBe(true); - expect(message!.isEquivalentTo(M_NOTICE)).toBe(true); - expect(message!.isEquivalentTo(M_MESSAGE)).toBe(true); - const messageEvent = message as NoticeEvent; - expect(messageEvent.text).toEqual("Text here"); - }); - - it("should parse m.poll.start events", () => { - const input: IPartialEvent = { - type: M_POLL_START.name, - content: { - [M_TEXT.name]: "FALLBACK Question here", - [M_POLL_START.name]: { - question: {[M_TEXT.name]: "Question here"}, - kind: M_POLL_KIND_DISCLOSED.name, - max_selections: 1, - answers: [ - {id: "one", [M_TEXT.name]: "ONE"}, - {id: "two", [M_TEXT.name]: "TWO"}, - {id: "thr", [M_TEXT.name]: "THR"}, - ], - }, - }, - }; - const poll = new ExtensibleEvents().parse(input); - expect(poll).toBeDefined(); - expect(poll instanceof PollStartEvent).toBe(true); - expect(poll!.isEquivalentTo(M_POLL_START)).toBe(true); - }); - - it("should parse m.poll.response events", () => { - const input: IPartialEvent = { - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_RESPONSE.name]: { - answers: ["one"], - }, - }, - }; - const response = new ExtensibleEvents().parse(input); - expect(response).toBeDefined(); - expect(response instanceof PollResponseEvent).toBe(true); - expect(response!.isEquivalentTo(M_POLL_RESPONSE)).toBe(true); - }); - - it("should parse m.poll.end events", () => { - const input: IPartialEvent = { - type: M_POLL_END.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_TEXT.name]: "FALLBACK Closure notice here", - [M_POLL_END.name]: {}, - }, - }; - const poll = new ExtensibleEvents().parse(input); - expect(poll).toBeDefined(); - expect(poll instanceof PollEndEvent).toBe(true); - }); - }); - - describe("parse errors", () => { - function myForcedInvalidInterpreter(wireEvent: IPartialEvent): ExtensibleEvent { - throw new InvalidEventError("InvalidEventTest", "deliberate throw of invalid type"); - } - - function myExplodingInterpreter(wireEvent: IPartialEvent): ExtensibleEvent { - throw new Error("deliberate throw"); - } - - it("should return null when InvalidEventError is raised", () => { - const extev = new ExtensibleEvents(); - const namespace = new UnstableValue(null, "org.matrix.example"); - extev.registerInterpreter(namespace, myForcedInvalidInterpreter); - const result = extev.parse({type: namespace.name, content: {unused: true}}); - expect(result).toBeNull(); - }); - - it("should return null when no known parser is found", () => { - const extev = new ExtensibleEvents(); - const namespace = new UnstableValue(null, "org.matrix.example"); - const result = extev.parse({type: namespace.name, content: {unused: true}}); - expect(result).toBeNull(); - }); - - it("should throw if the parser throws an unknown error", () => { - const extev = new ExtensibleEvents(); - const namespace = new UnstableValue(null, "org.matrix.example"); - extev.registerInterpreter(namespace, myExplodingInterpreter); - expect(() => extev.parse({type: namespace.name, content: {unused: true}})).toThrow("deliberate throw"); - }); - - it("should throw if the parser throws an unknown error during unknown interpret order", () => { - const extev = new ExtensibleEvents(); - const namespace = new UnstableValue(null, "org.matrix.example"); - const namespace2 = new UnstableValue(null, "org.matrix.example2"); - extev.registerInterpreter(namespace, myExplodingInterpreter); - extev.unknownInterpretOrder = [namespace]; - expect(() => extev.parse({type: namespace2.name, content: {unused: true}})).toThrow("deliberate throw"); - }); - - describe("static api", () => { - it("should return null when InvalidEventError is raised", () => { - const namespace = new UnstableValue(null, "org.matrix.example"); - ExtensibleEvents.registerInterpreter(namespace, myForcedInvalidInterpreter); - const result = ExtensibleEvents.parse({type: namespace.name, content: {unused: true}}); - expect(result).toBeNull(); - }); - }); - }); -}); diff --git a/test/v1-old/NamespacedMap.test.ts b/test/v1-old/NamespacedMap.test.ts deleted file mode 100644 index c7d4f15..0000000 --- a/test/v1-old/NamespacedMap.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* -Copyright 2022 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 {NamespacedMap, NamespacedValue, UnstableValue} from "../../src"; -import {STABLE_VALUE, UNSTABLE_VALUE} from "../NamespacedValue.test"; - -type TestableNamespacedMap = {internalMap: Map} & NamespacedMap; -function asTestableMap(map: NamespacedMap): TestableNamespacedMap { - return map as TestableNamespacedMap; -} - -const STABLE_UNSTABLE_NS = new NamespacedValue(`${STABLE_VALUE}.st_ust`, `${UNSTABLE_VALUE}.st_ust`); -const STABLE_ONLY_NS = new NamespacedValue(`${STABLE_VALUE}.st_only`, null); -const UNSTABLE_ONLY_NS = new UnstableValue(null, `${UNSTABLE_VALUE}.ust_only`); - -describe("NamespacedMap", () => { - it("should support no initial entries", () => { - const map = asTestableMap(new NamespacedMap()); - expect(map.internalMap.size).toBe(0); - }); - - it("should support initial entries", () => { - const map = asTestableMap( - new NamespacedMap([ - [STABLE_UNSTABLE_NS, "val1"], - [STABLE_ONLY_NS, "val2"], - [UNSTABLE_ONLY_NS, "val3"], - ]), - ); - expect(map.internalMap.size).toBe(4); - expect(map.internalMap.get(STABLE_UNSTABLE_NS.name)).toBe("val1"); - expect(map.internalMap.get(STABLE_UNSTABLE_NS.altName ?? "TEST_FAIL")).toBe("val1"); - expect(map.internalMap.get(STABLE_ONLY_NS.name)).toBe("val2"); - expect(map.internalMap.get(STABLE_ONLY_NS.altName ?? "TEST_FAIL")).toBeFalsy(); - expect(map.internalMap.get(UNSTABLE_ONLY_NS.name)).toBe("val3"); - expect(map.internalMap.get(UNSTABLE_ONLY_NS.altName)).toBeFalsy(); - }); - - it("should set both stable and unstable", () => { - const map = asTestableMap(new NamespacedMap()); - map.set(STABLE_UNSTABLE_NS, "val1"); - expect(map.internalMap.size).toBe(2); - expect(map.internalMap.get(STABLE_UNSTABLE_NS.name)).toBe("val1"); - expect(map.internalMap.get(STABLE_UNSTABLE_NS.altName ?? "TEST_FAIL")).toBe("val1"); - expect(map.hasNamespaced(STABLE_UNSTABLE_NS.name)).toBe(true); - expect(map.hasNamespaced(STABLE_UNSTABLE_NS.altName ?? "TEST_FAIL")).toBe(true); - expect(map.getNamespaced(STABLE_UNSTABLE_NS.name)).toBe("val1"); - expect(map.getNamespaced(STABLE_UNSTABLE_NS.altName ?? "TEST_FAIL")).toBe("val1"); - expect(map.has(STABLE_UNSTABLE_NS)).toBe(true); - expect(map.get(STABLE_UNSTABLE_NS)).toBe("val1"); - }); - - it("should lookup by altName (unstable) if it is the only option", () => { - const map = asTestableMap(new NamespacedMap()); - map.set(STABLE_UNSTABLE_NS, "val1"); - const tempNs = new NamespacedValue("wrong_stable", STABLE_UNSTABLE_NS.unstable); - expect(map.internalMap.size).toBe(2); - expect(map.internalMap.get(tempNs.name ?? "TEST_FAIL")).toBeUndefined(); - expect(map.internalMap.get(tempNs.altName ?? "TEST_FAIL")).toBe("val1"); - expect(map.hasNamespaced(tempNs.name ?? "TEST_FAIL")).toBe(false); - expect(map.hasNamespaced(tempNs.altName ?? "TEST_FAIL")).toBe(true); - expect(map.getNamespaced(tempNs.name ?? "TEST_FAIL")).toBeUndefined(); - expect(map.getNamespaced(tempNs.altName ?? "TEST_FAIL")).toBe("val1"); - expect(map.has(tempNs)).toBe(true); - expect(map.get(tempNs)).toBe("val1"); - }); - - describe("get", () => { - it("should return null if no valid keys are found", () => { - const map = asTestableMap(new NamespacedMap()); - expect(map.internalMap.size).toBe(0); - expect(map.get(STABLE_UNSTABLE_NS)).toBeNull(); - }); - }); - - it("should only set stable if available", () => { - const map = asTestableMap(new NamespacedMap()); - map.set(STABLE_ONLY_NS, "val1"); - expect(map.internalMap.size).toBe(1); - expect(map.internalMap.get(STABLE_ONLY_NS.name)).toBe("val1"); - expect(map.internalMap.get(STABLE_ONLY_NS.altName ?? "TEST_FAIL")).toBeFalsy(); - expect(map.hasNamespaced(STABLE_ONLY_NS.name)).toBe(true); - expect(map.hasNamespaced(STABLE_ONLY_NS.altName ?? "TEST_FAIL")).toBe(false); - expect(map.getNamespaced(STABLE_ONLY_NS.name)).toBe("val1"); - expect(map.getNamespaced(STABLE_ONLY_NS.altName ?? "TEST_FAIL")).toBeFalsy(); - expect(map.has(STABLE_ONLY_NS)).toBe(true); - expect(map.get(STABLE_ONLY_NS)).toBe("val1"); - }); - - it("should only set unstable if available", () => { - const map = asTestableMap(new NamespacedMap()); - map.set(UNSTABLE_ONLY_NS, "val1"); - expect(map.internalMap.size).toBe(1); - expect(map.internalMap.get(UNSTABLE_ONLY_NS.name)).toBe("val1"); - expect(map.internalMap.get(UNSTABLE_ONLY_NS.altName)).toBeFalsy(); - expect(map.hasNamespaced(UNSTABLE_ONLY_NS.name)).toBe(true); - expect(map.hasNamespaced(UNSTABLE_ONLY_NS.altName)).toBe(false); - expect(map.getNamespaced(UNSTABLE_ONLY_NS.name)).toBe("val1"); - expect(map.getNamespaced(UNSTABLE_ONLY_NS.altName)).toBeFalsy(); - expect(map.has(UNSTABLE_ONLY_NS)).toBe(true); - expect(map.get(UNSTABLE_ONLY_NS)).toBe("val1"); - }); - - it("should delete both keys when requested", () => { - const map = asTestableMap(new NamespacedMap()); - map.set(STABLE_UNSTABLE_NS, "val1"); - expect(map.internalMap.size).toBe(2); - map.delete(STABLE_UNSTABLE_NS); - expect(map.internalMap.size).toBe(0); - }); - - it("should delete just stable when requested", () => { - const map = asTestableMap(new NamespacedMap()); - map.set(STABLE_ONLY_NS, "val1"); - expect(map.internalMap.size).toBe(1); - map.delete(STABLE_ONLY_NS); - expect(map.internalMap.size).toBe(0); - }); - - it("should delete just unstable when requested", () => { - const map = asTestableMap(new NamespacedMap()); - map.set(UNSTABLE_ONLY_NS, "val1"); - expect(map.internalMap.size).toBe(1); - map.delete(UNSTABLE_ONLY_NS); - expect(map.internalMap.size).toBe(0); - }); -}); diff --git a/test/v1-old/events/EmoteEvent.test.ts b/test/v1-old/events/EmoteEvent.test.ts deleted file mode 100644 index 8a2638b..0000000 --- a/test/v1-old/events/EmoteEvent.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -/* -Copyright 2022 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 { - EmoteEvent, - InvalidEventError, - IPartialEvent, - M_EMOTE, - M_EMOTE_EVENT_CONTENT, - M_HTML, - M_MESSAGE, - M_MESSAGE_EVENT_CONTENT, - M_NOTICE, - M_NOTICE_EVENT_CONTENT, - M_TEXT, -} from "../../../src"; - -describe("EmoteEvent", () => { - it("should parse m.text", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new EmoteEvent(input); - expect(message.text).toBe("Text here"); - expect(message.html).toBeFalsy(); - expect(message.renderings.length).toBe(1); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - }); - - it("should parse m.html", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - [M_HTML.name]: "HTML here", - }, - }; - const message = new EmoteEvent(input); - expect(message.text).toBe("Text here"); - expect(message.html).toBe("HTML here"); - expect(message.renderings.length).toBe(2); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - }); - - it("should parse m.message", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_MESSAGE.name]: [ - {body: "Text here", mimetype: "text/plain"}, - {body: "HTML here", mimetype: "text/html"}, - {body: "MD here", mimetype: "text/markdown"}, - ], - - // These should be ignored - [M_TEXT.name]: "WRONG Text here", - [M_HTML.name]: "WRONG HTML here", - }, - }; - const message = new EmoteEvent(input); - expect(message.text).toBe("Text here"); - expect(message.html).toBe("HTML here"); - expect(message.renderings.length).toBe(3); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/markdown" && r.body === "MD here")).toBe(true); - }); - - it("should fail to parse missing text", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - hello: "world", - } as any, // force invalid type - }; - expect(() => new EmoteEvent(input)).toThrow( - new InvalidEventError("MessageEventLegacy", "Missing textual representation for event"), - ); - }); - - it("should fail to parse missing plain text in m.message", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_MESSAGE.name]: [{body: "HTML here", mimetype: "text/html"}], - }, - }; - expect(() => new EmoteEvent(input)).toThrow( - new InvalidEventError("MessageEventLegacy", "m.message is missing a plain text representation"), - ); - }); - - it("should fail to parse non-array m.message", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_MESSAGE.name]: "invalid", - } as any, // force invalid type - }; - expect(() => new EmoteEvent(input)).toThrow( - new InvalidEventError("MessageEventLegacy", "m.message contents must be an array"), - ); - }); - - describe("isEmote", () => { - it("should be true by default", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new EmoteEvent(input); - expect(message.isEmote).toBe(true); - }); - - it("should be true when using an emote subtype", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - [M_EMOTE.name]: {}, - }, - }; - const message = new EmoteEvent(input); - expect(message.isEmote).toBe(true); - }); - - it("should be true when using an emote primary type", () => { - const input: IPartialEvent = { - type: M_EMOTE.name, - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new EmoteEvent(input); - expect(message.isEmote).toBe(true); - }); - }); - - describe("isNotice", () => { - it("should be false by default", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new EmoteEvent(input); - expect(message.isNotice).toBe(false); - }); - - it("should be true when using a notice subtype", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - [M_NOTICE.name]: {}, - }, - }; - const message = new EmoteEvent(input); - expect(message.isNotice).toBe(true); - }); - - it("should be true when using a notice primary type", () => { - const input: IPartialEvent = { - type: M_NOTICE.name, - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new EmoteEvent(input); - expect(message.isNotice).toBe(true); - }); - }); - - describe("from & serialize", () => { - it("should serialize to a legacy fallback", () => { - const message = EmoteEvent.from("Text here", "HTML here"); - expect(message.text).toBe("Text here"); - expect(message.html).toBe("HTML here"); - expect(message.renderings.length).toBe(2); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - - const serialized = message.serialize(); - expect(serialized.type).toBe("m.room.message"); - expect(serialized.content).toMatchObject({ - [M_MESSAGE.name]: [ - {body: "Text here", mimetype: "text/plain"}, - {body: "HTML here", mimetype: "text/html"}, - ], - body: "Text here", - msgtype: "m.emote", - format: "org.matrix.custom.html", - formatted_body: "HTML here", - }); - }); - - it("should serialize non-html content to a legacy fallback", () => { - const message = EmoteEvent.from("Text here"); - expect(message.text).toBe("Text here"); - expect(message.renderings.length).toBe(1); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - - const serialized = message.serialize(); - expect(serialized.type).toBe("m.room.message"); - expect(serialized.content).toMatchObject({ - [M_TEXT.name]: "Text here", - body: "Text here", - msgtype: "m.emote", - format: undefined, - formatted_body: undefined, - }); - }); - }); -}); diff --git a/test/v1-old/events/ExtensibleEvent.test.ts b/test/v1-old/events/ExtensibleEvent.test.ts deleted file mode 100644 index d9cf361..0000000 --- a/test/v1-old/events/ExtensibleEvent.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright 2022 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 {EventType, ExtensibleEvent, IPartialEvent} from "../../../src"; - -class MockEvent extends ExtensibleEvent { - public constructor(wireEvent: IPartialEvent) { - super(wireEvent); - } - - public serialize(): IPartialEvent { - throw new Error("Not implemented for tests"); - } - - public isEquivalentTo(primaryEventType: EventType): boolean { - throw new Error("Not implemented for tests"); - } -} - -describe("ExtensibleEvent", () => { - it("should expose the wire event directly", () => { - const input: IPartialEvent = {type: "org.example.custom", content: {hello: "world"}}; - const event = new MockEvent(input); - expect(event.wireFormat).toBe(input); - expect(event.wireContent).toBe(input.content); - }); -}); diff --git a/test/v1-old/events/MessageEvent.test.ts b/test/v1-old/events/MessageEvent.test.ts deleted file mode 100644 index 75376e2..0000000 --- a/test/v1-old/events/MessageEvent.test.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* -Copyright 2022 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 { - InvalidEventError, - IPartialEvent, - M_EMOTE, - M_EMOTE_EVENT_CONTENT, - M_HTML, - M_MESSAGE, - M_MESSAGE_EVENT_CONTENT, - M_NOTICE, - M_NOTICE_EVENT_CONTENT, - M_TEXT, - MessageEvent, -} from "../../../src"; - -describe("MessageEvent", () => { - it("should parse m.text", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new MessageEvent(input); - expect(message.text).toBe("Text here"); - expect(message.html).toBeFalsy(); - expect(message.renderings.length).toBe(1); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - }); - - it("should parse m.html", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - [M_HTML.name]: "HTML here", - }, - }; - const message = new MessageEvent(input); - expect(message.text).toBe("Text here"); - expect(message.html).toBe("HTML here"); - expect(message.renderings.length).toBe(2); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - }); - - it("should parse m.message", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_MESSAGE.name]: [ - {body: "Text here", mimetype: "text/plain"}, - {body: "HTML here", mimetype: "text/html"}, - {body: "MD here", mimetype: "text/markdown"}, - ], - - // These should be ignored - [M_TEXT.name]: "WRONG Text here", - [M_HTML.name]: "WRONG HTML here", - }, - }; - const message = new MessageEvent(input); - expect(message.text).toBe("Text here"); - expect(message.html).toBe("HTML here"); - expect(message.renderings.length).toBe(3); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/markdown" && r.body === "MD here")).toBe(true); - }); - - it("should not find HTML if there isn't any", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_MESSAGE.name]: [ - {body: "Text here", mimetype: "text/plain"}, - {body: "MD here", mimetype: "text/markdown"}, - ], - - // These should be ignored - [M_TEXT.name]: "WRONG Text here", - [M_HTML.name]: "WRONG HTML here", - }, - }; - const message = new MessageEvent(input); - expect(message.text).toBe("Text here"); - expect(message.html).toBeUndefined(); - expect(message.renderings.length).toBe(2); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/markdown" && r.body === "MD here")).toBe(true); - }); - - it("should fail to parse missing text", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - hello: "world", - } as any, // force invalid type - }; - expect(() => new MessageEvent(input)).toThrow( - new InvalidEventError("MessageEventLegacy", "Missing textual representation for event"), - ); - }); - - it("should fail to parse missing plain text in m.message", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_MESSAGE.name]: [{body: "HTML here", mimetype: "text/html"}], - }, - }; - expect(() => new MessageEvent(input)).toThrow( - new InvalidEventError("MessageEventLegacy", "m.message is missing a plain text representation"), - ); - }); - - it("should fail to parse non-array m.message", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_MESSAGE.name]: "invalid", - } as any, // force invalid type - }; - expect(() => new MessageEvent(input)).toThrow( - new InvalidEventError("MessageEventLegacy", "m.message contents must be an array"), - ); - }); - - describe("isEmote", () => { - it("should be false by default", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new MessageEvent(input); - expect(message.isEmote).toBe(false); - }); - - it("should be true when using an emote subtype", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - [M_EMOTE.name]: {}, - }, - }; - const message = new MessageEvent(input); - expect(message.isEmote).toBe(true); - }); - - it("should be true when using an emote primary type", () => { - const input: IPartialEvent = { - type: M_EMOTE.name, - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new MessageEvent(input); - expect(message.isEmote).toBe(true); - }); - }); - - describe("isNotice", () => { - it("should be false by default", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new MessageEvent(input); - expect(message.isNotice).toBe(false); - }); - - it("should be true when using a notice subtype", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - [M_NOTICE.name]: {}, - }, - }; - const message = new MessageEvent(input); - expect(message.isNotice).toBe(true); - }); - - it("should be true when using a notice primary type", () => { - const input: IPartialEvent = { - type: M_NOTICE.name, - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new MessageEvent(input); - expect(message.isNotice).toBe(true); - }); - }); - - describe("from & serialize", () => { - it("should serialize to a legacy fallback", () => { - const message = MessageEvent.from("Text here", "HTML here"); - expect(message.text).toBe("Text here"); - expect(message.html).toBe("HTML here"); - expect(message.renderings.length).toBe(2); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - - const serialized = message.serialize(); - expect(serialized.type).toBe("m.room.message"); - expect(serialized.content).toMatchObject({ - [M_MESSAGE.name]: [ - {body: "Text here", mimetype: "text/plain"}, - {body: "HTML here", mimetype: "text/html"}, - ], - body: "Text here", - msgtype: "m.text", - format: "org.matrix.custom.html", - formatted_body: "HTML here", - }); - }); - - it("should serialize non-html content to a legacy fallback", () => { - const message = MessageEvent.from("Text here"); - expect(message.text).toBe("Text here"); - expect(message.renderings.length).toBe(1); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - - const serialized = message.serialize(); - expect(serialized.type).toBe("m.room.message"); - expect(serialized.content).toMatchObject({ - [M_TEXT.name]: "Text here", - body: "Text here", - msgtype: "m.text", - format: undefined, - formatted_body: undefined, - }); - }); - }); -}); diff --git a/test/v1-old/events/NoticeEvent.test.ts b/test/v1-old/events/NoticeEvent.test.ts deleted file mode 100644 index 004511b..0000000 --- a/test/v1-old/events/NoticeEvent.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -/* -Copyright 2022 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 { - InvalidEventError, - IPartialEvent, - M_EMOTE, - M_EMOTE_EVENT_CONTENT, - M_HTML, - M_MESSAGE, - M_MESSAGE_EVENT_CONTENT, - M_NOTICE, - M_NOTICE_EVENT_CONTENT, - M_TEXT, - NoticeEvent, -} from "../../../src"; - -describe("NoticeEvent", () => { - it("should parse m.text", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new NoticeEvent(input); - expect(message.text).toBe("Text here"); - expect(message.html).toBeFalsy(); - expect(message.renderings.length).toBe(1); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - }); - - it("should parse m.html", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - [M_HTML.name]: "HTML here", - }, - }; - const message = new NoticeEvent(input); - expect(message.text).toBe("Text here"); - expect(message.html).toBe("HTML here"); - expect(message.renderings.length).toBe(2); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - }); - - it("should parse m.message", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_MESSAGE.name]: [ - {body: "Text here", mimetype: "text/plain"}, - {body: "HTML here", mimetype: "text/html"}, - {body: "MD here", mimetype: "text/markdown"}, - ], - - // These should be ignored - [M_TEXT.name]: "WRONG Text here", - [M_HTML.name]: "WRONG HTML here", - }, - }; - const message = new NoticeEvent(input); - expect(message.text).toBe("Text here"); - expect(message.html).toBe("HTML here"); - expect(message.renderings.length).toBe(3); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/markdown" && r.body === "MD here")).toBe(true); - }); - - it("should fail to parse missing text", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - hello: "world", - } as any, // force invalid type - }; - expect(() => new NoticeEvent(input)).toThrow( - new InvalidEventError("MessageEventLegacy", "Missing textual representation for event"), - ); - }); - - it("should fail to parse missing plain text in m.message", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_MESSAGE.name]: [{body: "HTML here", mimetype: "text/html"}], - }, - }; - expect(() => new NoticeEvent(input)).toThrow( - new InvalidEventError("MessageEventLegacy", "m.message is missing a plain text representation"), - ); - }); - - it("should fail to parse non-array m.message", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_MESSAGE.name]: "invalid", - } as any, // force invalid type - }; - expect(() => new NoticeEvent(input)).toThrow( - new InvalidEventError("MessageEventLegacy", "m.message contents must be an array"), - ); - }); - - describe("isEmote", () => { - it("should be false by default", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new NoticeEvent(input); - expect(message.isEmote).toBe(false); - }); - - it("should be true when using an emote subtype", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - [M_EMOTE.name]: {}, - }, - }; - const message = new NoticeEvent(input); - expect(message.isEmote).toBe(true); - }); - - it("should be true when using an emote primary type", () => { - const input: IPartialEvent = { - type: M_EMOTE.name, - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new NoticeEvent(input); - expect(message.isEmote).toBe(true); - }); - }); - - describe("isNotice", () => { - it("should be true by default", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new NoticeEvent(input); - expect(message.isNotice).toBe(true); - }); - - it("should be true when using a notice subtype", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - [M_NOTICE.name]: {}, - }, - }; - const message = new NoticeEvent(input); - expect(message.isNotice).toBe(true); - }); - - it("should be true when using a notice primary type", () => { - const input: IPartialEvent = { - type: M_NOTICE.name, - content: { - [M_TEXT.name]: "Text here", - }, - }; - const message = new NoticeEvent(input); - expect(message.isNotice).toBe(true); - }); - }); - - describe("from & serialize", () => { - it("should serialize to a legacy fallback", () => { - const message = NoticeEvent.from("Text here", "HTML here"); - expect(message.text).toBe("Text here"); - expect(message.html).toBe("HTML here"); - expect(message.renderings.length).toBe(2); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - - const serialized = message.serialize(); - expect(serialized.type).toBe("m.room.message"); - expect(serialized.content).toMatchObject({ - [M_MESSAGE.name]: [ - {body: "Text here", mimetype: "text/plain"}, - {body: "HTML here", mimetype: "text/html"}, - ], - body: "Text here", - msgtype: "m.notice", - format: "org.matrix.custom.html", - formatted_body: "HTML here", - }); - }); - - it("should serialize non-html content to a legacy fallback", () => { - const message = NoticeEvent.from("Text here"); - expect(message.text).toBe("Text here"); - expect(message.renderings.length).toBe(1); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - - const serialized = message.serialize(); - expect(serialized.type).toBe("m.room.message"); - expect(serialized.content).toMatchObject({ - [M_TEXT.name]: "Text here", - body: "Text here", - msgtype: "m.notice", - format: undefined, - formatted_body: undefined, - }); - }); - }); -}); diff --git a/test/v1-old/events/PollEndEvent.test.ts b/test/v1-old/events/PollEndEvent.test.ts deleted file mode 100644 index 2bfea3f..0000000 --- a/test/v1-old/events/PollEndEvent.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* -Copyright 2022 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 { - InvalidEventError, - IPartialEvent, - M_POLL_END, - M_POLL_END_EVENT_CONTENT, - M_TEXT, - PollEndEvent, - REFERENCE_RELATION, -} from "../../../src"; - -describe("PollEndEvent", () => { - // Note: throughout these tests we don't really bother testing that - // MessageEvent is doing its job. It has its own tests to worry about. - - it("should parse a poll closure", () => { - const input: IPartialEvent = { - type: M_POLL_END.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_END.name]: {}, - [M_TEXT.name]: "Poll closed", - }, - }; - const event = new PollEndEvent(input); - expect(event.pollEventId).toBe("$poll"); - expect(event.closingMessage.text).toBe("Poll closed"); - }); - - it("should fail to parse a missing relationship", () => { - const input: IPartialEvent = { - type: M_POLL_END.name, - content: { - [M_POLL_END.name]: {}, - [M_TEXT.name]: "Poll closed", - } as any, // force invalid type - }; - expect(() => new PollEndEvent(input)).toThrow( - new InvalidEventError("PollEndEventLegacy", "Relationship must be a reference to an event"), - ); - }); - - it("should fail to parse a missing relationship event ID", () => { - const input: IPartialEvent = { - type: M_POLL_END.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - }, - [M_POLL_END.name]: {}, - [M_TEXT.name]: "Poll closed", - } as any, // force invalid type - }; - expect(() => new PollEndEvent(input)).toThrow( - new InvalidEventError("PollEndEventLegacy", "Relationship must be a reference to an event"), - ); - }); - - it("should fail to parse an improper relationship", () => { - const input: IPartialEvent = { - type: M_POLL_END.name, - content: { - "m.relates_to": { - rel_type: "org.example.not-relationship", - event_id: "$poll", - }, - [M_POLL_END.name]: {}, - [M_TEXT.name]: "Poll closed", - } as any, // force invalid type - }; - expect(() => new PollEndEvent(input)).toThrow( - new InvalidEventError("PollEndEventLegacy", "Relationship must be a reference to an event"), - ); - }); - - describe("from & serialize", () => { - it("should serialize to a poll end event", () => { - const event = PollEndEvent.from("$poll", "Poll closed"); - expect(event.pollEventId).toBe("$poll"); - expect(event.closingMessage.text).toBe("Poll closed"); - - const serialized = event.serialize(); - expect(M_POLL_END.matches(serialized.type)).toBe(true); - expect(serialized.content).toMatchObject({ - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_END.name]: {}, - [M_TEXT.name]: expect.any(String), // tested by MessageEvent tests - }); - }); - }); - - describe("isEquivalentTo", () => { - it("should consider itself the same for M_POLL_END types", () => { - const event = PollEndEvent.from("$poll", "Poll closed"); - expect(event.isEquivalentTo(M_POLL_END.name)).toBe(true); - expect(event.isEquivalentTo(M_POLL_END.altName)).toBe(true); - expect(event.isEquivalentTo("org.matrix.random")).toBe(false); - }); - }); -}); diff --git a/test/v1-old/events/PollResponseEvent.test.ts b/test/v1-old/events/PollResponseEvent.test.ts deleted file mode 100644 index 342f07c..0000000 --- a/test/v1-old/events/PollResponseEvent.test.ts +++ /dev/null @@ -1,316 +0,0 @@ -/* -Copyright 2022 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 { - InvalidEventError, - IPartialEvent, - M_POLL_KIND_DISCLOSED, - M_POLL_RESPONSE, - M_POLL_RESPONSE_EVENT_CONTENT, - M_POLL_START, - M_TEXT, - PollResponseEvent, - PollStartEvent, - REFERENCE_RELATION, -} from "../../../src"; - -const SAMPLE_POLL = new PollStartEvent({ - type: M_POLL_START.name, - content: { - [M_TEXT.name]: "FALLBACK Question here", - [M_POLL_START.name]: { - question: {[M_TEXT.name]: "Question here"}, - kind: M_POLL_KIND_DISCLOSED.name, - max_selections: 2, - answers: [ - {id: "one", [M_TEXT.name]: "ONE"}, - {id: "two", [M_TEXT.name]: "TWO"}, - {id: "thr", [M_TEXT.name]: "THR"}, - ], - }, - }, -}); - -describe("PollResponseEvent", () => { - it("should parse a poll response", () => { - const input: IPartialEvent = { - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_RESPONSE.name]: { - answers: ["one"], - }, - }, - }; - const response = new PollResponseEvent(input); - expect(response.spoiled).toBe(false); - expect(response.answerIds).toMatchObject(["one"]); - expect(response.pollEventId).toBe("$poll"); - }); - - it("should fail to parse a missing relationship", () => { - const input: IPartialEvent = { - type: M_POLL_RESPONSE.name, - content: { - [M_POLL_RESPONSE.name]: { - answers: ["one"], - }, - } as any, // force invalid type - }; - expect(() => new PollResponseEvent(input)).toThrow( - new InvalidEventError("PollResponseEventLegacy", "Relationship must be a reference to an event"), - ); - }); - - it("should fail to parse a missing relationship event ID", () => { - const input: IPartialEvent = { - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - }, - [M_POLL_RESPONSE.name]: { - answers: ["one"], - }, - } as any, // force invalid type - }; - expect(() => new PollResponseEvent(input)).toThrow( - new InvalidEventError("PollResponseEventLegacy", "Relationship must be a reference to an event"), - ); - }); - - it("should fail to parse an improper relationship", () => { - const input: IPartialEvent = { - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: "org.example.not-relationship", - event_id: "$poll", - }, - [M_POLL_RESPONSE.name]: { - answers: ["one"], - }, - } as any, // force invalid type - }; - expect(() => new PollResponseEvent(input)).toThrow( - new InvalidEventError("PollResponseEventLegacy", "Relationship must be a reference to an event"), - ); - }); - - describe("validateAgainst", () => { - it("should spoil the vote when no response subevent", () => { - const input: IPartialEvent = { - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - } as any, // force invalid type - }; - const response = new PollResponseEvent(input); - expect(response.spoiled).toBe(true); - - response.validateAgainst(SAMPLE_POLL); - expect(response.spoiled).toBe(true); - }); - - it("should spoil the vote when no answers", () => { - const input: IPartialEvent = { - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_RESPONSE.name]: {}, - } as any, // force invalid type - }; - const response = new PollResponseEvent(input); - expect(response.spoiled).toBe(true); - - response.validateAgainst(SAMPLE_POLL); - expect(response.spoiled).toBe(true); - }); - - it("should spoil the vote when answers are empty", () => { - const input: IPartialEvent = { - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_RESPONSE.name]: { - answers: [], - }, - }, - }; - const response = new PollResponseEvent(input); - expect(response.spoiled).toBe(true); - - response.validateAgainst(SAMPLE_POLL); - expect(response.spoiled).toBe(true); - }); - - it("should spoil the vote when answers are empty", () => { - const input: IPartialEvent = { - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_RESPONSE.name]: { - answers: [], - }, - }, - }; - const response = new PollResponseEvent(input); - expect(response.spoiled).toBe(true); - - response.validateAgainst(SAMPLE_POLL); - expect(response.spoiled).toBe(true); - }); - - it("should spoil the vote when answers are not strings", () => { - const input: IPartialEvent = { - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_RESPONSE.name]: { - answers: [1, 2, 3], - }, - } as any, // force invalid type - }; - const response = new PollResponseEvent(input); - expect(response.spoiled).toBe(true); - - response.validateAgainst(SAMPLE_POLL); - expect(response.spoiled).toBe(true); - }); - - it("should spoil the vote when answers is not an array", () => { - const input: IPartialEvent = { - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_RESPONSE.name]: { - answers: "yes", - }, - } as any, // force invalid type - }; - const response = new PollResponseEvent(input); - expect(response.spoiled).toBe(true); - - response.validateAgainst(SAMPLE_POLL); - expect(response.spoiled).toBe(true); - }); - - describe("consumer usage", () => { - it("should spoil the vote when invalid answers are given", () => { - const input: IPartialEvent = { - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_RESPONSE.name]: { - answers: ["A", "B", "C"], - }, - }, - }; - const response = new PollResponseEvent(input); - expect(response.spoiled).toBe(false); // it won't know better - - response.validateAgainst(SAMPLE_POLL); - expect(response.spoiled).toBe(true); - }); - - it("should truncate answers to the poll max selections", () => { - const input: IPartialEvent = { - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_RESPONSE.name]: { - answers: ["one", "two", "thr"], - }, - }, - }; - const response = new PollResponseEvent(input); - expect(response.spoiled).toBe(false); // it won't know better - expect(response.answerIds).toMatchObject(["one", "two", "thr"]); - - response.validateAgainst(SAMPLE_POLL); - expect(response.spoiled).toBe(false); - expect(response.answerIds).toMatchObject(["one", "two"]); - }); - }); - }); - - describe("from & serialize", () => { - it("should serialize to a poll response event", () => { - const response = PollResponseEvent.from(["A", "B", "C"], "$poll"); - expect(response.spoiled).toBe(false); - expect(response.answerIds).toMatchObject(["A", "B", "C"]); - expect(response.pollEventId).toBe("$poll"); - - const serialized = response.serialize(); - expect(M_POLL_RESPONSE.matches(serialized.type)).toBe(true); - expect(serialized.content).toMatchObject({ - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_RESPONSE.name]: { - answers: ["A", "B", "C"], - }, - }); - }); - - it("should serialize a spoiled vote", () => { - const response = PollResponseEvent.from([], "$poll"); - expect(response.spoiled).toBe(true); - expect(response.answerIds).toMatchObject([]); - expect(response.pollEventId).toBe("$poll"); - - const serialized = response.serialize(); - expect(M_POLL_RESPONSE.matches(serialized.type)).toBe(true); - expect(serialized.content).toMatchObject({ - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_RESPONSE.name]: { - answers: undefined, - }, - }); - }); - }); -}); diff --git a/test/v1-old/events/PollStartEvent.test.ts b/test/v1-old/events/PollStartEvent.test.ts deleted file mode 100644 index bf49269..0000000 --- a/test/v1-old/events/PollStartEvent.test.ts +++ /dev/null @@ -1,436 +0,0 @@ -/* -Copyright 2022 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 { - InvalidEventError, - IPartialEvent, - M_POLL_KIND_DISCLOSED, - M_POLL_KIND_UNDISCLOSED, - M_POLL_START, - M_POLL_START_EVENT_CONTENT, - M_TEXT, - POLL_ANSWER, - PollAnswerSubevent, - PollStartEvent, -} from "../../../src"; - -describe("PollAnswerSubevent", () => { - // Note: throughout these tests we don't really bother testing that - // MessageEvent is doing its job. It has its own tests to worry about. - - it("should parse an answer representation", () => { - const input: IPartialEvent = { - type: "org.matrix.sdk.poll.answer", - content: { - id: "one", - [M_TEXT.name]: "ONE", - }, - }; - const answer = new PollAnswerSubevent(input); - expect(answer.id).toBe("one"); - expect(answer.text).toBe("ONE"); - }); - - it("should fail to parse answers without an ID", () => { - const input: IPartialEvent = { - type: "org.matrix.sdk.poll.answer", - content: { - [M_TEXT.name]: "ONE", - } as any, // force invalid type - }; - expect(() => new PollAnswerSubevent(input)).toThrow( - new InvalidEventError("PollStartEventLegacy", "Answer ID must be a non-empty string"), - ); - }); - - it("should fail to parse answers without text", () => { - const input: IPartialEvent = { - type: "org.matrix.sdk.poll.answer", - content: { - id: "one", - } as any, // force invalid type - }; - expect(() => new PollAnswerSubevent(input)).toThrow(); // we don't check message - that'll be MessageEvent's problem - }); - - describe("from & serialize", () => { - it("should serialize to a placeholder representation", () => { - const answer = PollAnswerSubevent.from("one", "ONE"); - expect(answer.id).toBe("one"); - expect(answer.text).toBe("ONE"); - - const serialized = answer.serialize(); - expect(serialized.type).toBe("org.matrix.sdk.poll.answer"); - expect(serialized.content).toMatchObject({ - id: "one", - [M_TEXT.name]: expect.any(String), // tested by MessageEvent - }); - }); - }); -}); - -describe("PollStartEvent", () => { - // Note: throughout these tests we don't really bother testing that - // MessageEvent is doing its job. It has its own tests to worry about. - - it("should parse a poll", () => { - const input: IPartialEvent = { - type: M_POLL_START.name, - content: { - [M_TEXT.name]: "FALLBACK Question here", - [M_POLL_START.name]: { - question: {[M_TEXT.name]: "Question here"}, - kind: M_POLL_KIND_DISCLOSED.name, - max_selections: 2, - answers: [ - {id: "one", [M_TEXT.name]: "ONE"}, - {id: "two", [M_TEXT.name]: "TWO"}, - {id: "thr", [M_TEXT.name]: "THR"}, - ], - }, - }, - }; - const poll = new PollStartEvent(input); - expect(poll.question).toBeDefined(); - expect(poll.question.text).toBe("Question here"); - expect(poll.kind).toBe(M_POLL_KIND_DISCLOSED); - expect(M_POLL_KIND_DISCLOSED.matches(poll.rawKind)).toBe(true); - expect(poll.maxSelections).toBe(2); - expect(poll.answers.length).toBe(3); - expect(poll.answers.some(a => a.id === "one" && a.text === "ONE")).toBe(true); - expect(poll.answers.some(a => a.id === "two" && a.text === "TWO")).toBe(true); - expect(poll.answers.some(a => a.id === "thr" && a.text === "THR")).toBe(true); - }); - - it("should fail to parse a missing subevent", () => { - const input: IPartialEvent = { - type: M_POLL_START.name, - content: { - [M_TEXT.name]: "FALLBACK Question here", - } as any, // force invalid type - }; - expect(() => new PollStartEvent(input)).toThrow( - new InvalidEventError("PollStartEventLegacy", "A question is required"), - ); - }); - - it("should fail to parse a missing question", () => { - const input: IPartialEvent = { - type: M_POLL_START.name, - content: { - [M_TEXT.name]: "FALLBACK Question here", - [M_POLL_START.name]: { - kind: M_POLL_KIND_DISCLOSED.name, - max_selections: 2, - answers: [ - {id: "one", [M_TEXT.name]: "ONE"}, - {id: "two", [M_TEXT.name]: "TWO"}, - {id: "thr", [M_TEXT.name]: "THR"}, - ], - }, - } as any, // force invalid type - }; - expect(() => new PollStartEvent(input)).toThrow( - new InvalidEventError("PollStartEventLegacy", "A question is required"), - ); - }); - - it("should fail to parse non-array answers", () => { - const input: IPartialEvent = { - type: M_POLL_START.name, - content: { - [M_TEXT.name]: "FALLBACK Question here", - [M_POLL_START.name]: { - question: {[M_TEXT.name]: "Question here"}, - kind: M_POLL_KIND_DISCLOSED.name, - max_selections: 2, - answers: "one", - } as any, // force invalid type - }, - }; - expect(() => new PollStartEvent(input)).toThrow( - new InvalidEventError("PollStartEventLegacy", "Poll answers must be an array"), - ); - }); - - it("should fail to parse invalid answers", () => { - const input: IPartialEvent = { - type: M_POLL_START.name, - content: { - [M_TEXT.name]: "FALLBACK Question here", - [M_POLL_START.name]: { - question: {[M_TEXT.name]: "Question here"}, - kind: M_POLL_KIND_DISCLOSED.name, - max_selections: 2, - answers: [{id: "one"}, {[M_TEXT.name]: "TWO"}], - } as any, // force invalid type - }, - }; - expect(() => new PollStartEvent(input)).toThrow(); // error tested by PollAnswerSubevent tests - }); - - it("should fail to parse lack of answers", () => { - const input: IPartialEvent = { - type: M_POLL_START.name, - content: { - [M_TEXT.name]: "FALLBACK Question here", - [M_POLL_START.name]: { - question: {[M_TEXT.name]: "Question here"}, - kind: M_POLL_KIND_DISCLOSED.name, - max_selections: 2, - answers: [], - } as any, // force invalid type - }, - }; - expect(() => new PollStartEvent(input)).toThrow( - new InvalidEventError("PollStartEventLegacy", "No answers available"), - ); - }); - - it("should truncate answers at 20", () => { - const input: IPartialEvent = { - type: M_POLL_START.name, - content: { - [M_TEXT.name]: "FALLBACK Question here", - [M_POLL_START.name]: { - question: {[M_TEXT.name]: "Question here"}, - kind: M_POLL_KIND_DISCLOSED.name, - max_selections: 2, - answers: [ - {id: "01", [M_TEXT.name]: "A"}, - {id: "02", [M_TEXT.name]: "B"}, - {id: "03", [M_TEXT.name]: "C"}, - {id: "04", [M_TEXT.name]: "D"}, - {id: "05", [M_TEXT.name]: "E"}, - {id: "06", [M_TEXT.name]: "F"}, - {id: "07", [M_TEXT.name]: "G"}, - {id: "08", [M_TEXT.name]: "H"}, - {id: "09", [M_TEXT.name]: "I"}, - {id: "10", [M_TEXT.name]: "J"}, - {id: "11", [M_TEXT.name]: "K"}, - {id: "12", [M_TEXT.name]: "L"}, - {id: "13", [M_TEXT.name]: "M"}, - {id: "14", [M_TEXT.name]: "N"}, - {id: "15", [M_TEXT.name]: "O"}, - {id: "16", [M_TEXT.name]: "P"}, - {id: "17", [M_TEXT.name]: "Q"}, - {id: "18", [M_TEXT.name]: "R"}, - {id: "19", [M_TEXT.name]: "S"}, - {id: "20", [M_TEXT.name]: "T"}, - {id: "FAIL", [M_TEXT.name]: "U"}, - ], - }, - }, - }; - const poll = new PollStartEvent(input); - expect(poll.answers.length).toBe(20); - expect(poll.answers.some(a => a.id === "FAIL")).toBe(false); - }); - - it("should infer a kind from unknown kinds", () => { - const input: IPartialEvent = { - type: M_POLL_START.name, - content: { - [M_TEXT.name]: "FALLBACK Question here", - [M_POLL_START.name]: { - question: {[M_TEXT.name]: "Question here"}, - kind: "org.example.custom.poll.kind", - max_selections: 2, - answers: [ - {id: "01", [M_TEXT.name]: "A"}, - {id: "02", [M_TEXT.name]: "B"}, - {id: "03", [M_TEXT.name]: "C"}, - ], - }, - }, - }; - const poll = new PollStartEvent(input); - expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED); - expect(poll.rawKind).toBe("org.example.custom.poll.kind"); - }); - - it("should infer a kind from missing kinds", () => { - const input: IPartialEvent = { - type: M_POLL_START.name, - content: { - [M_TEXT.name]: "FALLBACK Question here", - [M_POLL_START.name]: { - question: {[M_TEXT.name]: "Question here"}, - max_selections: 2, - answers: [ - {id: "01", [M_TEXT.name]: "A"}, - {id: "02", [M_TEXT.name]: "B"}, - {id: "03", [M_TEXT.name]: "C"}, - ], - } as any, // force invalid type - }, - }; - const poll = new PollStartEvent(input); - expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED); - expect(poll.rawKind).toBeFalsy(); - }); - - it.each([NaN, -1, 0, null, "2", Infinity])("should assume invalid (%s) max_selections means 1", val => { - const input: IPartialEvent = { - type: M_POLL_START.name, - content: { - [M_TEXT.name]: "FALLBACK Question here", - [M_POLL_START.name]: { - question: {[M_TEXT.name]: "Question here"}, - max_selections: val, - answers: [ - {id: "01", [M_TEXT.name]: "A"}, - {id: "02", [M_TEXT.name]: "B"}, - {id: "03", [M_TEXT.name]: "C"}, - ], - } as any, // force invalid type - }, - }; - const poll = new PollStartEvent(input); - expect(poll.maxSelections).toBe(1); - }); - - it.each([2, 3, 1])("should use %s as max_selections when provided", val => { - const input: IPartialEvent = { - type: M_POLL_START.name, - content: { - [M_TEXT.name]: "FALLBACK Question here", - [M_POLL_START.name]: { - question: {[M_TEXT.name]: "Question here"}, - max_selections: val, - answers: [ - {id: "01", [M_TEXT.name]: "A"}, - {id: "02", [M_TEXT.name]: "B"}, - {id: "03", [M_TEXT.name]: "C"}, - ], - } as any, // force invalid type - }, - }; - const poll = new PollStartEvent(input); - expect(poll.maxSelections).toBe(val); - }); - - describe("from & serialize", () => { - it("should serialize with the minimum number of arguments", () => { - const poll = PollStartEvent.from("Question here", ["A", "B", "C"], M_POLL_KIND_DISCLOSED); - expect(poll.question.text).toBe("Question here"); - expect(poll.kind).toBe(M_POLL_KIND_DISCLOSED); - expect(M_POLL_KIND_DISCLOSED.matches(poll.rawKind)).toBe(true); - expect(poll.maxSelections).toBe(1); - expect(poll.answers.length).toBe(3); - expect(poll.answers.some(a => a.text === "A")).toBe(true); - expect(poll.answers.some(a => a.text === "B")).toBe(true); - expect(poll.answers.some(a => a.text === "C")).toBe(true); - - // Ids are non-empty and unique - expect(poll.answers[0].id).toHaveLength(16); - expect(poll.answers[1].id).toHaveLength(16); - expect(poll.answers[2].id).toHaveLength(16); - expect(poll.answers[0].id).not.toEqual(poll.answers[1].id); - expect(poll.answers[0].id).not.toEqual(poll.answers[2].id); - expect(poll.answers[1].id).not.toEqual(poll.answers[2].id); - - const serialized = poll.serialize(); - expect(M_POLL_START.matches(serialized.type)).toBe(true); - expect(serialized.content).toMatchObject({ - [M_TEXT.name]: "Question here\n1. A\n2. B\n3. C", - [M_POLL_START.name]: { - question: { - [M_TEXT.name]: expect.any(String), // tested by MessageEvent tests - }, - kind: M_POLL_KIND_DISCLOSED.name, - max_selections: 1, - answers: [ - // M_TEXT tested by MessageEvent tests - {id: expect.any(String), [M_TEXT.name]: expect.any(String)}, - {id: expect.any(String), [M_TEXT.name]: expect.any(String)}, - {id: expect.any(String), [M_TEXT.name]: expect.any(String)}, - ], - }, - }); - }); - - it("should serialize to a poll start event", () => { - const poll = PollStartEvent.from("Question here", ["A", "B", "C"], M_POLL_KIND_DISCLOSED, 2); - expect(poll.question.text).toBe("Question here"); - expect(poll.kind).toBe(M_POLL_KIND_DISCLOSED); - expect(M_POLL_KIND_DISCLOSED.matches(poll.rawKind)).toBe(true); - expect(poll.maxSelections).toBe(2); - expect(poll.answers.length).toBe(3); - expect(poll.answers.some(a => a.text === "A")).toBe(true); - expect(poll.answers.some(a => a.text === "B")).toBe(true); - expect(poll.answers.some(a => a.text === "C")).toBe(true); - - // Ids are non-empty and unique - expect(poll.answers[0].id).toHaveLength(16); - expect(poll.answers[1].id).toHaveLength(16); - expect(poll.answers[2].id).toHaveLength(16); - expect(poll.answers[0].id).not.toEqual(poll.answers[1].id); - expect(poll.answers[0].id).not.toEqual(poll.answers[2].id); - expect(poll.answers[1].id).not.toEqual(poll.answers[2].id); - - const serialized = poll.serialize(); - expect(M_POLL_START.matches(serialized.type)).toBe(true); - expect(serialized.content).toMatchObject({ - [M_TEXT.name]: "Question here\n1. A\n2. B\n3. C", - [M_POLL_START.name]: { - question: { - [M_TEXT.name]: expect.any(String), // tested by MessageEvent tests - }, - kind: M_POLL_KIND_DISCLOSED.name, - max_selections: 2, - answers: [ - // M_TEXT tested by MessageEvent tests - {id: expect.any(String), [M_TEXT.name]: expect.any(String)}, - {id: expect.any(String), [M_TEXT.name]: expect.any(String)}, - {id: expect.any(String), [M_TEXT.name]: expect.any(String)}, - ], - }, - }); - }); - - it("should serialize to a custom kind poll start event", () => { - const poll = PollStartEvent.from("Question here", ["A", "B", "C"], "org.example.poll.kind", 2); - expect(poll.question.text).toBe("Question here"); - expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED); - expect(poll.rawKind).toBe("org.example.poll.kind"); - expect(poll.maxSelections).toBe(2); - expect(poll.answers.length).toBe(3); - expect(poll.answers.some(a => a.text === "A")).toBe(true); - expect(poll.answers.some(a => a.text === "B")).toBe(true); - expect(poll.answers.some(a => a.text === "C")).toBe(true); - - const serialized = poll.serialize(); - expect(M_POLL_START.matches(serialized.type)).toBe(true); - expect(serialized.content).toMatchObject({ - [M_TEXT.name]: "Question here\n1. A\n2. B\n3. C", - [M_POLL_START.name]: { - question: { - [M_TEXT.name]: expect.any(String), // tested by MessageEvent tests - }, - kind: "org.example.poll.kind", - max_selections: 2, - answers: [ - // M_MESSAGE tested by MessageEvent tests - {id: expect.any(String), [M_TEXT.name]: expect.any(String)}, - {id: expect.any(String), [M_TEXT.name]: expect.any(String)}, - {id: expect.any(String), [M_TEXT.name]: expect.any(String)}, - ], - }, - }); - }); - }); -}); diff --git a/test/v1-old/interpreters/legacy/MRoomMessage.test.ts b/test/v1-old/interpreters/legacy/MRoomMessage.test.ts deleted file mode 100644 index 265900a..0000000 --- a/test/v1-old/interpreters/legacy/MRoomMessage.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* -Copyright 2022 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 { - EmoteEvent, - IPartialEvent, - IPartialLegacyContent, - M_EMOTE, - M_HTML, - M_MESSAGE, - M_NOTICE, - M_TEXT, - MessageEvent, - NoticeEvent, - parseMRoomMessage, -} from "../../../../src"; - -describe("parseMRoomMessage", () => { - it("should return an unmodified MessageEvent when using extensible events", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - [M_HTML.name]: "HTML here", - } as any, // force cast for tests - }; - const message = parseMRoomMessage(input) as MessageEvent; - expect(message).toBeDefined(); - expect(message instanceof MessageEvent).toBe(true); - expect(message.isEquivalentTo(M_MESSAGE)).toBe(true); - expect(message.html).toBe("HTML here"); - expect(message.text).toBe("Text here"); - expect(message.renderings.length).toBe(2); - expect(message.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - }); - - it("should interpret m.text as a MessageEvent", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - body: "Text here", - msgtype: "m.text", - format: "org.matrix.custom.html", - formatted_body: "HTML here", - }, - }; - const message = parseMRoomMessage(input) as MessageEvent; - expect(message).toBeDefined(); - expect(message instanceof MessageEvent).toBe(true); - expect(message.isEquivalentTo(M_MESSAGE)).toBe(true); - expect(message.html).toBe("HTML here"); - expect(message.text).toBe("Text here"); - expect(message.renderings.length).toBe(2); - expect(message.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - }); - - it("should support parsing without HTML", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - body: "Text here", - msgtype: "m.text", - }, - }; - const message = parseMRoomMessage(input) as MessageEvent; - expect(message).toBeDefined(); - expect(message instanceof MessageEvent).toBe(true); - expect(message.isEquivalentTo(M_MESSAGE)).toBe(true); - expect(message.html).toBeFalsy(); - expect(message.text).toBe("Text here"); - expect(message.renderings.length).toBe(1); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - }); - - it("should interpret m.emote as an EmoteEvent", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - body: "Text here", - msgtype: "m.emote", - format: "org.matrix.custom.html", - formatted_body: "HTML here", - }, - }; - const message = parseMRoomMessage(input) as MessageEvent; - expect(message).toBeDefined(); - expect(message instanceof EmoteEvent).toBe(true); - expect(message.isEquivalentTo(M_EMOTE)).toBe(true); - expect(message.isEquivalentTo(M_MESSAGE)).toBe(true); - expect(message.html).toBe("HTML here"); - expect(message.text).toBe("Text here"); - expect(message.renderings.length).toBe(2); - expect(message.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - }); - - it("should interpret m.notice as a NoticeEvent", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - body: "Text here", - msgtype: "m.notice", - format: "org.matrix.custom.html", - formatted_body: "HTML here", - }, - }; - const message = parseMRoomMessage(input) as MessageEvent; - expect(message).toBeDefined(); - expect(message instanceof NoticeEvent).toBe(true); - expect(message.isEquivalentTo(M_NOTICE)).toBe(true); - expect(message.isEquivalentTo(M_MESSAGE)).toBe(true); - expect(message.html).toBe("HTML here"); - expect(message.text).toBe("Text here"); - expect(message.renderings.length).toBe(2); - expect(message.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - }); - - it("should not interpret unknown msgtypes", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - body: "Text here", - msgtype: "org.example.custom", - format: "org.matrix.custom.html", - formatted_body: "HTML here", - }, - }; - const message = parseMRoomMessage(input); - expect(message).toBeFalsy(); - }); - - it("should not interpret events missing content", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - // @ts-ignore - content: null, - }; - const message = parseMRoomMessage(input); - expect(message).toBeNull(); - }); -}); diff --git a/test/v1-old/interpreters/modern/MMessage.test.ts b/test/v1-old/interpreters/modern/MMessage.test.ts deleted file mode 100644 index 92fb50e..0000000 --- a/test/v1-old/interpreters/modern/MMessage.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* -Copyright 2022 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 { - EmoteEvent, - IPartialEvent, - M_EMOTE, - M_HTML, - M_MESSAGE, - M_MESSAGE_EVENT_CONTENT, - M_NOTICE, - M_TEXT, - NoticeEvent, - parseMMessage, -} from "../../../../src"; - -describe("parseMMessage", () => { - it("should return an unmodified MessageEvent", () => { - const input: IPartialEvent = { - type: "org.example.message-like", - content: { - [M_TEXT.name]: "Text here", - [M_HTML.name]: "HTML here", - }, - }; - const message = parseMMessage(input); - expect(message).toBeDefined(); - expect(message!.html).toBe("HTML here"); - expect(message!.text).toBe("Text here"); - expect(message!.renderings.length).toBe(2); - expect(message!.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - expect(message!.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - }); - - it("should return an unmodified EmoteEvent", () => { - const input: IPartialEvent = { - type: M_EMOTE.name, - content: { - [M_TEXT.name]: "Text here", - [M_HTML.name]: "HTML here", - }, - }; - const message = parseMMessage(input); - expect(message).toBeDefined(); - expect(message instanceof EmoteEvent).toBe(true); - expect(message!.isEquivalentTo(M_EMOTE)).toBe(true); - expect(message!.isEquivalentTo(M_MESSAGE)).toBe(true); - expect(message!.html).toBe("HTML here"); - expect(message!.text).toBe("Text here"); - expect(message!.renderings.length).toBe(2); - expect(message!.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - expect(message!.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - }); - - it("should return an unmodified NoticeEvent", () => { - const input: IPartialEvent = { - type: M_NOTICE.name, - content: { - [M_TEXT.name]: "Text here", - [M_HTML.name]: "HTML here", - }, - }; - const message = parseMMessage(input); - expect(message).toBeDefined(); - expect(message instanceof NoticeEvent).toBe(true); - expect(message!.isEquivalentTo(M_NOTICE)).toBe(true); - expect(message!.isEquivalentTo(M_MESSAGE)).toBe(true); - expect(message!.html).toBe("HTML here"); - expect(message!.text).toBe("Text here"); - expect(message!.renderings.length).toBe(2); - expect(message!.renderings.some(r => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true); - expect(message!.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true); - }); -}); diff --git a/test/v1-old/interpreters/modern/MPoll.test.ts b/test/v1-old/interpreters/modern/MPoll.test.ts deleted file mode 100644 index fd351fd..0000000 --- a/test/v1-old/interpreters/modern/MPoll.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* -Copyright 2022 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 { - IPartialEvent, - M_POLL_END, - M_POLL_END_EVENT_CONTENT, - M_POLL_KIND_DISCLOSED, - M_POLL_RESPONSE, - M_POLL_RESPONSE_EVENT_CONTENT, - M_POLL_START, - M_POLL_START_EVENT_CONTENT, - M_TEXT, - parseMPoll, - PollEndEvent, - PollResponseEvent, - PollStartEvent, - REFERENCE_RELATION, -} from "../../../../src"; - -describe("parseMPoll", () => { - it("should return an unmodified PollStartEvent", () => { - const input: IPartialEvent = { - type: M_POLL_START.name, - content: { - [M_TEXT.name]: "FALLBACK Question here", - [M_POLL_START.name]: { - question: {[M_TEXT.name]: "Question here"}, - kind: M_POLL_KIND_DISCLOSED.name, - max_selections: 2, - answers: [ - {id: "one", [M_TEXT.name]: "ONE"}, - {id: "two", [M_TEXT.name]: "TWO"}, - {id: "thr", [M_TEXT.name]: "THR"}, - ], - }, - }, - }; - const poll = parseMPoll(input) as PollStartEvent; - // noinspection SuspiciousTypeOfGuard - expect(poll instanceof PollStartEvent).toBe(true); - expect(poll.isEquivalentTo(M_POLL_START)).toBe(true); - expect(poll.question).toBeDefined(); - expect(poll.question.text).toBe("Question here"); - expect(poll.kind).toBe(M_POLL_KIND_DISCLOSED); - expect(M_POLL_KIND_DISCLOSED.matches(poll.rawKind)).toBe(true); - expect(poll.maxSelections).toBe(2); - expect(poll.answers.length).toBe(3); - expect(poll.answers.some(a => a.id === "one" && a.text === "ONE")).toBe(true); - expect(poll.answers.some(a => a.id === "two" && a.text === "TWO")).toBe(true); - expect(poll.answers.some(a => a.id === "thr" && a.text === "THR")).toBe(true); - }); - - it("should return an unmodified PollResponseEvent", () => { - const input: IPartialEvent = { - type: M_POLL_RESPONSE.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_RESPONSE.name]: { - answers: ["one"], - }, - }, - }; - const response = parseMPoll(input) as PollResponseEvent; - // noinspection SuspiciousTypeOfGuard - expect(response instanceof PollResponseEvent).toBe(true); - expect(response.isEquivalentTo(M_POLL_RESPONSE)).toBe(true); - expect(response.spoiled).toBe(false); - expect(response.answerIds).toMatchObject(["one"]); - expect(response.pollEventId).toBe("$poll"); - }); - - it("should return an unmodified PollEndEvent", () => { - const input: IPartialEvent = { - type: M_POLL_END.name, - content: { - "m.relates_to": { - rel_type: REFERENCE_RELATION.name, - event_id: "$poll", - }, - [M_POLL_END.name]: {}, - [M_TEXT.name]: "Poll closed", - }, - }; - const event = parseMPoll(input) as PollEndEvent; - // noinspection SuspiciousTypeOfGuard - expect(event instanceof PollEndEvent).toBe(true); - expect(event.pollEventId).toBe("$poll"); - expect(event.closingMessage.text).toBe("Poll closed"); - }); - - it("should not attempt to parse non-poll events", () => { - const input: IPartialEvent = { - type: "not.a.poll", - content: {}, - }; - - const event = parseMPoll(input); - expect(event).toBeNull(); - }); -}); diff --git a/test/v1-old/utility/MessageMatchers.test.ts b/test/v1-old/utility/MessageMatchers.test.ts deleted file mode 100644 index cde3390..0000000 --- a/test/v1-old/utility/MessageMatchers.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* -Copyright 2022 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 { - IPartialEvent, - IPartialLegacyContent, - isEventLike, - LegacyMsgType, - M_EMOTE, - M_MESSAGE, - M_MESSAGE_EVENT_CONTENT, - M_NOTICE, - M_TEXT, -} from "../../../src"; - -describe("isEventLike", () => { - it("should match legacy text", () => { - const input1: IPartialEvent = { - type: "m.room.message", - content: {msgtype: "m.text", body: "a"}, - }; - const input2: IPartialEvent = {type: M_MESSAGE.name, content: {[M_TEXT.name]: "a"}}; - const input3: IPartialEvent = { - type: "org.example.message-like", - content: {[M_TEXT.name]: "a"}, - }; - - expect(isEventLike(input1, LegacyMsgType.Text)).toBe(true); - expect(isEventLike(input2, LegacyMsgType.Text)).toBe(true); - expect(isEventLike(input3, LegacyMsgType.Text)).toBe(false); - - expect(isEventLike(input1, LegacyMsgType.Notice)).toBe(false); - expect(isEventLike(input2, LegacyMsgType.Notice)).toBe(false); - expect(isEventLike(input3, LegacyMsgType.Notice)).toBe(false); - - expect(isEventLike(input1, LegacyMsgType.Emote)).toBe(false); - expect(isEventLike(input2, LegacyMsgType.Emote)).toBe(false); - expect(isEventLike(input3, LegacyMsgType.Emote)).toBe(false); - }); - - it("should match legacy emotes", () => { - const input1: IPartialEvent = { - type: "m.room.message", - content: {msgtype: "m.emote", body: "a"}, - }; - const input2: IPartialEvent = {type: M_EMOTE.name, content: {[M_TEXT.name]: "a"}}; - const input3: IPartialEvent = { - type: "org.example.message-like", - content: {[M_TEXT.name]: "a"}, - }; - - expect(isEventLike(input1, LegacyMsgType.Text)).toBe(false); - expect(isEventLike(input2, LegacyMsgType.Text)).toBe(false); - expect(isEventLike(input3, LegacyMsgType.Text)).toBe(false); - - expect(isEventLike(input1, LegacyMsgType.Notice)).toBe(false); - expect(isEventLike(input2, LegacyMsgType.Notice)).toBe(false); - expect(isEventLike(input3, LegacyMsgType.Notice)).toBe(false); - - expect(isEventLike(input1, LegacyMsgType.Emote)).toBe(true); - expect(isEventLike(input2, LegacyMsgType.Emote)).toBe(true); - expect(isEventLike(input3, LegacyMsgType.Emote)).toBe(false); - }); - - it("should match legacy notices", () => { - const input1: IPartialEvent = { - type: "m.room.message", - content: {msgtype: "m.notice", body: "a"}, - }; - const input2: IPartialEvent = {type: M_NOTICE.name, content: {[M_TEXT.name]: "a"}}; - const input3: IPartialEvent = { - type: "org.example.message-like", - content: {[M_TEXT.name]: "a"}, - }; - - expect(isEventLike(input1, LegacyMsgType.Text)).toBe(false); - expect(isEventLike(input2, LegacyMsgType.Text)).toBe(false); - expect(isEventLike(input3, LegacyMsgType.Text)).toBe(false); - - expect(isEventLike(input1, LegacyMsgType.Notice)).toBe(true); - expect(isEventLike(input2, LegacyMsgType.Notice)).toBe(true); - expect(isEventLike(input3, LegacyMsgType.Notice)).toBe(false); - - expect(isEventLike(input1, LegacyMsgType.Emote)).toBe(false); - expect(isEventLike(input2, LegacyMsgType.Emote)).toBe(false); - expect(isEventLike(input3, LegacyMsgType.Emote)).toBe(false); - }); - - it("should not match unknown msgtypes", () => { - const input1: IPartialEvent = { - type: "m.room.message", - content: {msgtype: "org.example.custom", body: "a"}, - }; - const input2: IPartialEvent = {type: M_TEXT.name, content: {[M_TEXT.name]: "a"}}; - const input3: IPartialEvent = { - type: "org.example.message-like", - content: {[M_TEXT.name]: "a"}, - }; - - expect(isEventLike(input1, LegacyMsgType.Text)).toBe(false); - expect(isEventLike(input2, LegacyMsgType.Text)).toBe(false); - expect(isEventLike(input3, LegacyMsgType.Text)).toBe(false); - - expect(isEventLike(input1, LegacyMsgType.Notice)).toBe(false); - expect(isEventLike(input2, LegacyMsgType.Notice)).toBe(false); - expect(isEventLike(input3, LegacyMsgType.Notice)).toBe(false); - - expect(isEventLike(input1, LegacyMsgType.Emote)).toBe(false); - expect(isEventLike(input2, LegacyMsgType.Emote)).toBe(false); - expect(isEventLike(input3, LegacyMsgType.Emote)).toBe(false); - - // @ts-ignore - expect(isEventLike(input1, "m.video")).toBe(false); - // @ts-ignore - expect(isEventLike(input2, "m.video")).toBe(false); - // @ts-ignore - expect(isEventLike(input3, "m.video")).toBe(false); - }); - - it("should not explode on missing event content", () => { - const input1: IPartialEvent = { - type: "m.room.message", - // @ts-ignore - content: null, - }; - - expect(isEventLike(input1, LegacyMsgType.Text)).toBe(false); - expect(isEventLike(input1, LegacyMsgType.Notice)).toBe(false); - expect(isEventLike(input1, LegacyMsgType.Emote)).toBe(false); - }); -}); diff --git a/test/v1-old/utility/events.test.ts b/test/v1-old/utility/events.test.ts deleted file mode 100644 index c9a48b5..0000000 --- a/test/v1-old/utility/events.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* -Copyright 2022 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 {isEventTypeSame, NamespacedValue} from "../../../src"; - -describe("isEventTypeSame", () => { - it("should match string and string", () => { - const a = "org.example.message-like"; - const b = "org.example.different"; - - expect(isEventTypeSame(a, b)).toBe(false); - expect(isEventTypeSame(b, a)).toBe(false); - - expect(isEventTypeSame(a, a)).toBe(true); - expect(isEventTypeSame(b, b)).toBe(true); - }); - - it("should match string and namespace", () => { - const a = "org.example.message-like"; - const b = new NamespacedValue("org.example.stable", "org.example.unstable"); - - expect(isEventTypeSame(a, b)).toBe(false); - expect(isEventTypeSame(b, a)).toBe(false); - - expect(isEventTypeSame(a, a)).toBe(true); - expect(isEventTypeSame(b, b)).toBe(true); - expect(isEventTypeSame(b.name, b)).toBe(true); - expect(isEventTypeSame(b.altName ?? "TEST_FAIL", b)).toBe(true); - expect(isEventTypeSame(b, b.name)).toBe(true); - expect(isEventTypeSame(b, b.altName ?? "TEST_FAIL")).toBe(true); - }); - - it("should match namespace and namespace", () => { - const a = new NamespacedValue("org.example.stable1", "org.example.unstable1"); - const b = new NamespacedValue("org.example.stable2", "org.example.unstable2"); - - expect(isEventTypeSame(a, b)).toBe(false); - expect(isEventTypeSame(b, a)).toBe(false); - - expect(isEventTypeSame(a, a)).toBe(true); - expect(isEventTypeSame(a.name, a)).toBe(true); - expect(isEventTypeSame(a.altName ?? "TEST_FAIL", a)).toBe(true); - expect(isEventTypeSame(a, a.name)).toBe(true); - expect(isEventTypeSame(a, a.altName ?? "TEST_FAIL")).toBe(true); - - expect(isEventTypeSame(b, b)).toBe(true); - expect(isEventTypeSame(b.name, b)).toBe(true); - expect(isEventTypeSame(b.altName ?? "TEST_FAIL", b)).toBe(true); - expect(isEventTypeSame(b, b.name)).toBe(true); - expect(isEventTypeSame(b, b.altName ?? "TEST_FAIL")).toBe(true); - }); - - it("should match namespaces of different pointers", () => { - const a = new NamespacedValue("org.example.stable", "org.example.unstable"); - const b = new NamespacedValue("org.example.stable", "org.example.unstable"); - - expect(isEventTypeSame(a, b)).toBe(true); - expect(isEventTypeSame(b, a)).toBe(true); - - expect(isEventTypeSame(a, a)).toBe(true); - expect(isEventTypeSame(a.name, a)).toBe(true); - expect(isEventTypeSame(a.altName ?? "TEST_FAIL", a)).toBe(true); - expect(isEventTypeSame(a, a.name)).toBe(true); - expect(isEventTypeSame(a, a.altName ?? "TEST_FAIL")).toBe(true); - - expect(isEventTypeSame(b, b)).toBe(true); - expect(isEventTypeSame(b.name, b)).toBe(true); - expect(isEventTypeSame(b.altName ?? "TEST_FAIL", b)).toBe(true); - expect(isEventTypeSame(b, b.name)).toBe(true); - expect(isEventTypeSame(b, b.altName ?? "TEST_FAIL")).toBe(true); - }); -}); From 643b7d5f6f73c32aebebf486c7f33c89ace7b7da Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 13 Dec 2022 14:31:28 -0700 Subject: [PATCH 14/22] Add support for message events --- src/content_blocks/m/EmoteBlock.ts | 19 ++++--- src/content_blocks/m/MarkupBlock.ts | 25 +++++---- src/content_blocks/m/NoticeBlock.ts | 19 ++++--- src/events/m/MessageEvent.ts | 75 ++++++++++++++++++++++++++ src/index.ts | 20 +------ test/events/RoomEvent.test.ts | 12 ++--- test/events/m/MessageEvent.test.ts | 83 +++++++++++++++++++++++++++++ 7 files changed, 207 insertions(+), 46 deletions(-) create mode 100644 src/events/m/MessageEvent.ts create mode 100644 test/events/m/MessageEvent.test.ts diff --git a/src/content_blocks/m/EmoteBlock.ts b/src/content_blocks/m/EmoteBlock.ts index 3a80391..f791ec6 100644 --- a/src/content_blocks/m/EmoteBlock.ts +++ b/src/content_blocks/m/EmoteBlock.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MarkupBlock, MarkupRepresentation} from "./MarkupBlock"; +import {MarkupBlock, WireMarkupBlock} from "./MarkupBlock"; import {ObjectBlock} from "../ObjectBlock"; import {EitherAnd} from "../../types"; import {Schema} from "ajv"; @@ -22,9 +22,16 @@ import {AjvContainer} from "../../AjvContainer"; import {UnstableValue} from "../../NamespacedValue"; import {InvalidBlockError} from "../InvalidBlockError"; -type Primary = {[MarkupBlock.type.name]: MarkupRepresentation[]}; -type Secondary = {[MarkupBlock.type.altName]: MarkupRepresentation[]}; -type Value = EitherAnd; +/** + * Types for emote blocks over the wire. + * @module Matrix Content Blocks + * @see EmoteBlock + */ +export module WireEmoteBlock { + type Primary = {[MarkupBlock.type.name]: WireMarkupBlock.Representation[]}; + type Secondary = {[MarkupBlock.type.altName]: WireMarkupBlock.Representation[]}; + export type Value = EitherAnd; +} /** * An "emote" block, or a block meant to represent that the surrounding event can @@ -32,14 +39,14 @@ type Value = EitherAnd; * @module Matrix Content Blocks * @see MarkupBlock */ -export class EmoteBlock extends ObjectBlock { +export class EmoteBlock extends ObjectBlock { public static readonly schema: Schema = AjvContainer.eitherAnd(MarkupBlock.type, MarkupBlock.schema); public static readonly validateFn = AjvContainer.ajv.compile(EmoteBlock.schema); public static readonly type = new UnstableValue("m.emote", "org.matrix.msc1767.emote"); - public constructor(raw: Value) { + public constructor(raw: WireEmoteBlock.Value) { super(EmoteBlock.type.stable!, raw); if (!EmoteBlock.validateFn(raw)) { throw new InvalidBlockError(this.name, EmoteBlock.validateFn.errors); diff --git a/src/content_blocks/m/MarkupBlock.ts b/src/content_blocks/m/MarkupBlock.ts index 2c1e5f0..7f59cbc 100644 --- a/src/content_blocks/m/MarkupBlock.ts +++ b/src/content_blocks/m/MarkupBlock.ts @@ -21,20 +21,27 @@ import {InvalidBlockError} from "../InvalidBlockError"; import {UnstableValue} from "../../NamespacedValue"; /** - * A representation of text within a markup block. + * Types for markup blocks over the wire. * @module Matrix Content Blocks + * @see MarkupBlock */ -export type MarkupRepresentation = { - body: string; - mimetype?: string; -}; +export module WireMarkupBlock { + /** + * A representation of text within a markup block. + * @module Matrix Content Blocks + */ + export type Representation = { + body: string; + mimetype?: string; + }; +} /** * A "markup" block, or a block meant to communicate human-readable and human-rendered * text, with optional mimetype. * @module Matrix Content Blocks */ -export class MarkupBlock extends ArrayBlock { +export class MarkupBlock extends ArrayBlock { public static readonly schema = ArrayBlock.schema; public static readonly validateFn = ArrayBlock.validateFn; @@ -78,7 +85,7 @@ export class MarkupBlock extends ArrayBlock { * block's `raw` type, thus not being considered for rendering. */ public readonly representationErrors = new Map< - {index: number; representation: MarkupRepresentation | unknown}, + {index: number; representation: WireMarkupBlock.Representation | unknown}, InvalidBlockError >(); @@ -89,7 +96,7 @@ export class MarkupBlock extends ArrayBlock { * Errors can be found from representationErrors after creating the object. * @param raw The block's value. */ - public constructor(raw: MarkupRepresentation[]) { + public constructor(raw: WireMarkupBlock.Representation[]) { super(MarkupBlock.type.stable!, raw); this.raw = raw.filter((r, i) => { const bool = MarkupBlock.representationValidateFn(r); @@ -123,7 +130,7 @@ export class MarkupBlock extends ArrayBlock { /** * The ordered representations for this markup block. */ - public get representations(): MarkupRepresentation[] { + public get representations(): WireMarkupBlock.Representation[] { return this.raw; } } diff --git a/src/content_blocks/m/NoticeBlock.ts b/src/content_blocks/m/NoticeBlock.ts index d590a38..bc8fb80 100644 --- a/src/content_blocks/m/NoticeBlock.ts +++ b/src/content_blocks/m/NoticeBlock.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MarkupBlock, MarkupRepresentation} from "./MarkupBlock"; +import {MarkupBlock, WireMarkupBlock} from "./MarkupBlock"; import {ObjectBlock} from "../ObjectBlock"; import {EitherAnd} from "../../types"; import {Schema} from "ajv"; @@ -22,9 +22,16 @@ import {AjvContainer} from "../../AjvContainer"; import {UnstableValue} from "../../NamespacedValue"; import {InvalidBlockError} from "../InvalidBlockError"; -type Primary = {[MarkupBlock.type.name]: MarkupRepresentation[]}; -type Secondary = {[MarkupBlock.type.altName]: MarkupRepresentation[]}; -type Value = EitherAnd; +/** + * Types for notice blocks over the wire. + * @module Matrix Content Blocks + * @see NoticeBlock + */ +export module WireNoticeBlock { + type Primary = {[MarkupBlock.type.name]: WireMarkupBlock.Representation[]}; + type Secondary = {[MarkupBlock.type.altName]: WireMarkupBlock.Representation[]}; + export type Value = EitherAnd; +} /** * A "notice" block, or a block meant to represent that the surrounding event can @@ -32,14 +39,14 @@ type Value = EitherAnd; * @module Matrix Content Blocks * @see MarkupBlock */ -export class NoticeBlock extends ObjectBlock { +export class NoticeBlock extends ObjectBlock { public static readonly schema: Schema = AjvContainer.eitherAnd(MarkupBlock.type, MarkupBlock.schema); public static readonly validateFn = AjvContainer.ajv.compile(NoticeBlock.schema); public static readonly type = new UnstableValue("m.notice", "org.matrix.msc1767.notice"); - public constructor(raw: Value) { + public constructor(raw: WireNoticeBlock.Value) { super(NoticeBlock.type.stable!, raw); if (!NoticeBlock.validateFn(raw)) { throw new InvalidBlockError(this.name, NoticeBlock.validateFn.errors); diff --git a/src/events/m/MessageEvent.ts b/src/events/m/MessageEvent.ts new file mode 100644 index 0000000..8dceca6 --- /dev/null +++ b/src/events/m/MessageEvent.ts @@ -0,0 +1,75 @@ +/* +Copyright 2022 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 {RoomEvent} from "../RoomEvent"; +import {MarkupBlock, WireMarkupBlock} from "../../content_blocks/m/MarkupBlock"; +import {EitherAnd} from "../../types"; +import {Schema} from "ajv"; +import {AjvContainer} from "../../AjvContainer"; +import {UnstableValue} from "../../NamespacedValue"; +import {WireEvent} from "../types_wire"; +import {InvalidEventError} from "../InvalidEventError"; + +/** + * Types for message events over the wire. + * @module Matrix Events + * @see MessageEvent + */ +export module WireMessageEvent { + type PrimaryContent = {[MarkupBlock.type.name]: WireMarkupBlock.Representation[]}; + type SecondaryContent = {[MarkupBlock.type.altName]: WireMarkupBlock.Representation[]}; + export type ContentValue = EitherAnd; +} + +/** + * A message event, containing a MarkupBlock for content. + * @module Matrix Events + * @see MarkupBlock + */ +export class MessageEvent extends RoomEvent { + public static readonly contentSchema: Schema = AjvContainer.eitherAnd(MarkupBlock.type, MarkupBlock.schema); + public static readonly contentValidateFn = AjvContainer.ajv.compile(MessageEvent.contentSchema); + + public static readonly type = new UnstableValue("m.message", "org.matrix.msc1767.message"); + + public constructor(raw: WireEvent.RoomEvent) { + super(MessageEvent.type.stable!, raw); + if (!MessageEvent.contentValidateFn(this.content)) { + throw new InvalidEventError(this.name, MessageEvent.contentValidateFn.errors); + } + } + + /** + * The markup block for the event. + */ + public get markup(): MarkupBlock { + return new MarkupBlock(MarkupBlock.type.findIn(this.content)!); + } + + /** + The text representation of the event, if one is present. + */ + public get text(): string | undefined { + return this.markup.text; + } + + /** + The HTML representation of the event, if one is present. + */ + public get html(): string | undefined { + return this.markup.html; + } +} diff --git a/src/index.ts b/src/index.ts index 44acc5e..0e84e88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,24 +2,10 @@ // created from ctix -export * from "./v1-old/interpreters/legacy/MRoomMessage"; -export * from "./v1-old/interpreters/modern/MMessage"; -export * from "./v1-old/interpreters/modern/MPoll"; export * from "./content_blocks/m/EmoteBlock"; export * from "./content_blocks/m/MarkupBlock"; export * from "./content_blocks/m/NoticeBlock"; -export * from "./v1-old/events/EmoteEvent"; -export * from "./v1-old/events/ExtensibleEvent"; -export * from "./v1-old/events/message_types"; -export * from "./v1-old/events/MessageEvent"; -export * from "./v1-old/events/NoticeEvent"; -export * from "./v1-old/events/poll_types"; -export * from "./v1-old/events/PollEndEvent"; -export * from "./v1-old/events/PollResponseEvent"; -export * from "./v1-old/events/PollStartEvent"; -export * from "./v1-old/events/relationship_types"; -export * from "./v1-old/utility/events"; -export * from "./v1-old/utility/MessageMatchers"; +export * from "./events/m/MessageEvent"; export * from "./content_blocks/ArrayBlock"; export * from "./content_blocks/BaseBlock"; export * from "./content_blocks/BooleanBlock"; @@ -29,9 +15,5 @@ export * from "./content_blocks/ObjectBlock"; export * from "./content_blocks/StringBlock"; export * from "./events/InvalidEventError"; export * from "./events/RoomEvent"; -export * from "./v1-old/ExtensibleEvents"; -export * from "./v1-old/IPartialEvent"; -export * from "./v1-old/NamespacedMap"; -export * from "./v1-old/types"; export * from "./NamespacedValue"; export * from "./types"; diff --git a/test/events/RoomEvent.test.ts b/test/events/RoomEvent.test.ts index 26b61a0..4b4b020 100644 --- a/test/events/RoomEvent.test.ts +++ b/test/events/RoomEvent.test.ts @@ -34,10 +34,10 @@ describe("RoomEvent", () => { * @param safeContentInput The content to supply on an event during a non-throwing * test. This should be the minimum to construct the event, not ideal conditions. */ -export function testSharedRoomEventInputs( +export function testSharedRoomEventInputs( eventName: string, - factory: (raw: WireEvent.RoomEvent) => RoomEvent, - safeContentInput: WireEvent.BlockBasedContent, + factory: (raw: WireEvent.RoomEvent) => RoomEvent, + safeContentInput: W, ) { describe("internal", () => { it("should have valid inputs", () => { @@ -77,7 +77,7 @@ export function testSharedRoomEventInputs( }); it.each([undefined, "", "with content"])("should handle valid event structures with state key of '%s'", skey => { - const raw: WireEvent.RoomEvent = { + const raw: WireEvent.RoomEvent = { room_id: "!test:example.org", event_id: "$event", type: "org.example.test_event", @@ -99,7 +99,7 @@ export function testSharedRoomEventInputs( }); it("should retain the event name", () => { - const raw: WireEvent.RoomEvent = { + const raw: WireEvent.RoomEvent = { room_id: "!test:example.org", event_id: "$event", type: "org.example.test_event", @@ -287,7 +287,7 @@ export function testSharedRoomEventInputs( }); it.each([-1670894499800, -1, 0])("should return a zero timestamp for negative timestamps: '%s'", val => { - const raw: WireEvent.RoomEvent = { + const raw: WireEvent.RoomEvent = { room_id: "!test:example.org", event_id: "$event", type: "org.example.test_event", diff --git a/test/events/m/MessageEvent.test.ts b/test/events/m/MessageEvent.test.ts new file mode 100644 index 0000000..f43c09e --- /dev/null +++ b/test/events/m/MessageEvent.test.ts @@ -0,0 +1,83 @@ +/* +Copyright 2022 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 {InvalidEventError, MessageEvent} from "../../../src"; +import {testSharedRoomEventInputs} from "../RoomEvent.test"; +import {WireEvent} from "../../../src/events/types_wire"; + +describe("MessageEvent", () => { + testSharedRoomEventInputs("m.message", x => new MessageEvent(x), {"m.markup": [{body: "test"}]}); + + const templateEvent: WireEvent.RoomEvent = { + room_id: "!test:example.org", + event_id: "$event", + type: "m.message", + sender: "@user:example.org", + content: {}, + origin_server_ts: 1670894499800, + }; + + it("should locate a stable markup block", () => { + const block = new MessageEvent({...templateEvent, content: {"m.markup": [{body: "test"}]}}); + expect(block.markup).toBeDefined(); + expect(block.markup.text).toStrictEqual("test"); + expect(block.text).toStrictEqual("test"); + }); + + it("should locate an unstable markup block", () => { + const event = new MessageEvent({...templateEvent, content: {"org.matrix.msc1767.markup": [{body: "test"}]}}); + expect(event.markup).toBeDefined(); + expect(event.markup.text).toStrictEqual("test"); + expect(event.text).toStrictEqual("test"); + }); + + it("should prefer the unstable markup block if both are provided", () => { + // Dev note: this test will need updating when extensible events becomes stable + const event = new MessageEvent({ + ...templateEvent, + content: { + "m.markup": [{body: "stable text"}], + "org.matrix.msc1767.markup": [{body: "unstable text"}], + }, + }); + expect(event.markup).toBeDefined(); + expect(event.markup.text).toStrictEqual("unstable text"); + expect(event.text).toStrictEqual("unstable text"); + }); + + it("should proxy the text and html from the markup block", () => { + const block = new MessageEvent({ + ...templateEvent, + content: { + "m.markup": [ + {body: "test plain", mimetype: "text/plain"}, + {body: "test html", mimetype: "text/html"}, + ], + }, + }); + expect(block.markup).toBeDefined(); + expect(block.markup.text).toStrictEqual("test plain"); + expect(block.text).toStrictEqual("test plain"); + expect(block.markup.html).toStrictEqual("test html"); + expect(block.html).toStrictEqual("test html"); + }); + + it("should error if a markup block is not present", () => { + expect(() => new MessageEvent({...templateEvent, content: {no_block: true} as any})).toThrow( + new InvalidEventError("m.message", "schema does not apply to m.markup or org.matrix.msc1767.markup"), + ); + }); +}); From c7e146723888c26130eac405c55585da51c7a32e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 13 Dec 2022 14:48:35 -0700 Subject: [PATCH 15/22] Use lazily loaded values for performance --- src/LazyValue.ts | 41 ++++++++++++++++++++ src/content_blocks/m/EmoteBlock.ts | 5 ++- src/content_blocks/m/MarkupBlock.ts | 12 ++++-- src/content_blocks/m/NoticeBlock.ts | 5 ++- src/events/m/MessageEvent.ts | 5 ++- src/index.ts | 1 + test/LazyValue.test.ts | 60 +++++++++++++++++++++++++++++ 7 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 src/LazyValue.ts create mode 100644 test/LazyValue.test.ts diff --git a/src/LazyValue.ts b/src/LazyValue.ts new file mode 100644 index 0000000..5a23aa3 --- /dev/null +++ b/src/LazyValue.ts @@ -0,0 +1,41 @@ +/* +Copyright 2022 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. +*/ + +/** + * Represents a lazily-loaded value. When accessed for the first time, the + * value will be cached for the lifetime of the object. The getter function + * will be released when the value is cached. + */ +export class LazyValue { + private cached: T | undefined; + private getter: (() => T) | undefined; + + public constructor(getter: () => T) { + // manually copy, so we don't expose the `undefined` type option + this.getter = getter; + } + + public get value(): T { + if (this.getter !== undefined) { + this.cached = this.getter(); + this.getter = undefined; + } + // Force a cast rather than assert because it's possible for `T` to + // be undefined or null (we don't stop people from making bad decisions + // in this class). + return this.cached as T; + } +} diff --git a/src/content_blocks/m/EmoteBlock.ts b/src/content_blocks/m/EmoteBlock.ts index f791ec6..a0cad6b 100644 --- a/src/content_blocks/m/EmoteBlock.ts +++ b/src/content_blocks/m/EmoteBlock.ts @@ -21,6 +21,7 @@ import {Schema} from "ajv"; import {AjvContainer} from "../../AjvContainer"; import {UnstableValue} from "../../NamespacedValue"; import {InvalidBlockError} from "../InvalidBlockError"; +import {LazyValue} from "../../LazyValue"; /** * Types for emote blocks over the wire. @@ -46,6 +47,8 @@ export class EmoteBlock extends ObjectBlock { public static readonly type = new UnstableValue("m.emote", "org.matrix.msc1767.emote"); + private lazyMarkup = new LazyValue(() => new MarkupBlock(MarkupBlock.type.findIn(this.raw)!)); + public constructor(raw: WireEmoteBlock.Value) { super(EmoteBlock.type.stable!, raw); if (!EmoteBlock.validateFn(raw)) { @@ -54,6 +57,6 @@ export class EmoteBlock extends ObjectBlock { } public get markup(): MarkupBlock { - return new MarkupBlock(MarkupBlock.type.findIn(this.raw)!); + return this.lazyMarkup.value; } } diff --git a/src/content_blocks/m/MarkupBlock.ts b/src/content_blocks/m/MarkupBlock.ts index 7f59cbc..14e3e19 100644 --- a/src/content_blocks/m/MarkupBlock.ts +++ b/src/content_blocks/m/MarkupBlock.ts @@ -19,6 +19,7 @@ import {Schema} from "ajv"; import {AjvContainer} from "../../AjvContainer"; import {InvalidBlockError} from "../InvalidBlockError"; import {UnstableValue} from "../../NamespacedValue"; +import {LazyValue} from "../../LazyValue"; /** * Types for markup blocks over the wire. @@ -89,6 +90,11 @@ export class MarkupBlock extends ArrayBlock { InvalidBlockError >(); + private lazyText = new LazyValue( + () => this.raw.find(m => m.mimetype === undefined || m.mimetype === "text/plain")?.body, + ); + private lazyHtml = new LazyValue(() => this.raw.find(m => m.mimetype === "text/html")?.body); + /** * Creates a new MarkupBlock * @@ -117,20 +123,20 @@ export class MarkupBlock extends ArrayBlock { * The text representation of the block, if one is present. */ public get text(): string | undefined { - return this.raw.find(m => m.mimetype === undefined || m.mimetype === "text/plain")?.body; + return this.lazyText.value; } /** * The HTML representation of the block, if one is present. */ public get html(): string | undefined { - return this.raw.find(m => m.mimetype === "text/html")?.body; + return this.lazyHtml.value; } /** * The ordered representations for this markup block. */ public get representations(): WireMarkupBlock.Representation[] { - return this.raw; + return [...this.raw]; // clone to prevent mutation } } diff --git a/src/content_blocks/m/NoticeBlock.ts b/src/content_blocks/m/NoticeBlock.ts index bc8fb80..cbc2179 100644 --- a/src/content_blocks/m/NoticeBlock.ts +++ b/src/content_blocks/m/NoticeBlock.ts @@ -21,6 +21,7 @@ import {Schema} from "ajv"; import {AjvContainer} from "../../AjvContainer"; import {UnstableValue} from "../../NamespacedValue"; import {InvalidBlockError} from "../InvalidBlockError"; +import {LazyValue} from "../../LazyValue"; /** * Types for notice blocks over the wire. @@ -46,6 +47,8 @@ export class NoticeBlock extends ObjectBlock { public static readonly type = new UnstableValue("m.notice", "org.matrix.msc1767.notice"); + private lazyMarkup = new LazyValue(() => new MarkupBlock(MarkupBlock.type.findIn(this.raw)!)); + public constructor(raw: WireNoticeBlock.Value) { super(NoticeBlock.type.stable!, raw); if (!NoticeBlock.validateFn(raw)) { @@ -54,6 +57,6 @@ export class NoticeBlock extends ObjectBlock { } public get markup(): MarkupBlock { - return new MarkupBlock(MarkupBlock.type.findIn(this.raw)!); + return this.lazyMarkup.value; } } diff --git a/src/events/m/MessageEvent.ts b/src/events/m/MessageEvent.ts index 8dceca6..4c839b4 100644 --- a/src/events/m/MessageEvent.ts +++ b/src/events/m/MessageEvent.ts @@ -22,6 +22,7 @@ import {AjvContainer} from "../../AjvContainer"; import {UnstableValue} from "../../NamespacedValue"; import {WireEvent} from "../types_wire"; import {InvalidEventError} from "../InvalidEventError"; +import {LazyValue} from "../../LazyValue"; /** * Types for message events over the wire. @@ -45,6 +46,8 @@ export class MessageEvent extends RoomEvent { public static readonly type = new UnstableValue("m.message", "org.matrix.msc1767.message"); + private lazyMarkup = new LazyValue(() => new MarkupBlock(MarkupBlock.type.findIn(this.content)!)); + public constructor(raw: WireEvent.RoomEvent) { super(MessageEvent.type.stable!, raw); if (!MessageEvent.contentValidateFn(this.content)) { @@ -56,7 +59,7 @@ export class MessageEvent extends RoomEvent { * The markup block for the event. */ public get markup(): MarkupBlock { - return new MarkupBlock(MarkupBlock.type.findIn(this.content)!); + return this.lazyMarkup.value; } /** diff --git a/src/index.ts b/src/index.ts index 0e84e88..e076ca1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,5 +15,6 @@ export * from "./content_blocks/ObjectBlock"; export * from "./content_blocks/StringBlock"; export * from "./events/InvalidEventError"; export * from "./events/RoomEvent"; +export * from "./LazyValue"; export * from "./NamespacedValue"; export * from "./types"; diff --git a/test/LazyValue.test.ts b/test/LazyValue.test.ts new file mode 100644 index 0000000..01cd207 --- /dev/null +++ b/test/LazyValue.test.ts @@ -0,0 +1,60 @@ +/* +Copyright 2022 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 {InvalidEventError, LazyValue} from "../src"; + +// @ts-ignore - we know we're exposing private fields as public +class TestableLazyValue extends LazyValue { + public cached: T | undefined; + public getter: (() => T) | undefined; +} + +describe("LazyValue", () => { + it("should cache the getter's value on read", () => { + const val = new TestableLazyValue(() => 42); + expect(val.cached).toBeUndefined(); + expect(val.getter).toBeDefined(); + + const ret = val.value; + expect(ret).toStrictEqual(42); + expect(val.cached).toStrictEqual(42); + expect(val.getter).toBeUndefined(); + }); + + it.each([null, undefined])("should handle null and undefined as types: '%s'", x => { + const val = new TestableLazyValue(() => x); + expect(val.cached).toBeUndefined(); + expect(val.getter).toBeDefined(); + + const ret = val.value; + expect(ret).toStrictEqual(x); + expect(val.cached).toStrictEqual(x); + expect(val.getter).toBeUndefined(); + }); + + it("should pass errors up normally", () => { + expect(() => { + new TestableLazyValue(() => { + throw new InvalidEventError("TestEvent", "You should see me"); + }); + }).not.toThrow(); + expect(() => { + new TestableLazyValue(() => { + throw new InvalidEventError("TestEvent", "You should see me"); + }).value; + }).toThrow(new InvalidEventError("TestEvent", "You should see me")); + }); +}); From e3df70fc58e4ff4bb83ea2c1a1721745524a5db5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 13 Dec 2022 15:35:46 -0700 Subject: [PATCH 16/22] Add notice and emote events Unfortunately this doesn't use class inheritance because TypeScript only allows one constructor at a time (making it hard to extend MessageEvent). Ultimately, we kinda want consumers to handle these events as special though, so maybe it's fine? --- src/events/m/EmoteEvent.ts | 75 ++++++++++++++++++++++++++++ src/events/m/NoticeEvent.ts | 75 ++++++++++++++++++++++++++++ src/index.ts | 2 + test/events/m/EmoteEvent.test.ts | 83 +++++++++++++++++++++++++++++++ test/events/m/NoticeEvent.test.ts | 83 +++++++++++++++++++++++++++++++ 5 files changed, 318 insertions(+) create mode 100644 src/events/m/EmoteEvent.ts create mode 100644 src/events/m/NoticeEvent.ts create mode 100644 test/events/m/EmoteEvent.test.ts create mode 100644 test/events/m/NoticeEvent.test.ts diff --git a/src/events/m/EmoteEvent.ts b/src/events/m/EmoteEvent.ts new file mode 100644 index 0000000..ce534b1 --- /dev/null +++ b/src/events/m/EmoteEvent.ts @@ -0,0 +1,75 @@ +/* +Copyright 2022 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 {WireMessageEvent} from "./MessageEvent"; +import {WireEvent} from "../types_wire"; +import {UnstableValue} from "../../NamespacedValue"; +import {MarkupBlock} from "../../content_blocks/m/MarkupBlock"; +import {RoomEvent} from "../RoomEvent"; +import {Schema} from "ajv"; +import {AjvContainer} from "../../AjvContainer"; +import {LazyValue} from "../../LazyValue"; +import {InvalidEventError} from "../InvalidEventError"; + +/** + * Types for emote events over the wire. + * @module Matrix Events + * @see EmoteEvent + */ +export module WireEmoteEvent { + export type ContentValue = WireMessageEvent.ContentValue; +} + +/** + * An emote event, containing a MarkupBlock for content. + * @module Matrix Events + * @see MarkupBlock + */ +export class EmoteEvent extends RoomEvent { + public static readonly contentSchema: Schema = AjvContainer.eitherAnd(MarkupBlock.type, MarkupBlock.schema); + public static readonly contentValidateFn = AjvContainer.ajv.compile(EmoteEvent.contentSchema); + public static readonly type = new UnstableValue("m.emote", "org.matrix.msc1767.emote"); + + private lazyMarkup = new LazyValue(() => new MarkupBlock(MarkupBlock.type.findIn(this.content)!)); + + public constructor(raw: WireEvent.RoomEvent) { + super(EmoteEvent.type.stable!, raw); + if (!EmoteEvent.contentValidateFn(this.content)) { + throw new InvalidEventError(this.name, EmoteEvent.contentValidateFn.errors); + } + } + + /** + * The markup block for the event. + */ + public get markup(): MarkupBlock { + return this.lazyMarkup.value; + } + + /** + The text representation of the event, if one is present. + */ + public get text(): string | undefined { + return this.markup.text; + } + + /** + The HTML representation of the event, if one is present. + */ + public get html(): string | undefined { + return this.markup.html; + } +} diff --git a/src/events/m/NoticeEvent.ts b/src/events/m/NoticeEvent.ts new file mode 100644 index 0000000..ec8dbb1 --- /dev/null +++ b/src/events/m/NoticeEvent.ts @@ -0,0 +1,75 @@ +/* +Copyright 2022 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 {WireMessageEvent} from "./MessageEvent"; +import {WireEvent} from "../types_wire"; +import {UnstableValue} from "../../NamespacedValue"; +import {MarkupBlock} from "../../content_blocks/m/MarkupBlock"; +import {RoomEvent} from "../RoomEvent"; +import {Schema} from "ajv"; +import {AjvContainer} from "../../AjvContainer"; +import {LazyValue} from "../../LazyValue"; +import {InvalidEventError} from "../InvalidEventError"; + +/** + * Types for notice events over the wire. + * @module Matrix Events + * @see NoticeEvent + */ +export module WireNoticeEvent { + export type ContentValue = WireMessageEvent.ContentValue; +} + +/** + * A notice event, containing a MarkupBlock for content. + * @module Matrix Events + * @see MarkupBlock + */ +export class NoticeEvent extends RoomEvent { + public static readonly contentSchema: Schema = AjvContainer.eitherAnd(MarkupBlock.type, MarkupBlock.schema); + public static readonly contentValidateFn = AjvContainer.ajv.compile(NoticeEvent.contentSchema); + public static readonly type = new UnstableValue("m.notice", "org.matrix.msc1767.notice"); + + private lazyMarkup = new LazyValue(() => new MarkupBlock(MarkupBlock.type.findIn(this.content)!)); + + public constructor(raw: WireEvent.RoomEvent) { + super(NoticeEvent.type.stable!, raw); + if (!NoticeEvent.contentValidateFn(this.content)) { + throw new InvalidEventError(this.name, NoticeEvent.contentValidateFn.errors); + } + } + + /** + * The markup block for the event. + */ + public get markup(): MarkupBlock { + return this.lazyMarkup.value; + } + + /** + The text representation of the event, if one is present. + */ + public get text(): string | undefined { + return this.markup.text; + } + + /** + The HTML representation of the event, if one is present. + */ + public get html(): string | undefined { + return this.markup.html; + } +} diff --git a/src/index.ts b/src/index.ts index e076ca1..69f4a4a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,9 @@ export * from "./content_blocks/m/EmoteBlock"; export * from "./content_blocks/m/MarkupBlock"; export * from "./content_blocks/m/NoticeBlock"; +export * from "./events/m/EmoteEvent"; export * from "./events/m/MessageEvent"; +export * from "./events/m/NoticeEvent"; export * from "./content_blocks/ArrayBlock"; export * from "./content_blocks/BaseBlock"; export * from "./content_blocks/BooleanBlock"; diff --git a/test/events/m/EmoteEvent.test.ts b/test/events/m/EmoteEvent.test.ts new file mode 100644 index 0000000..1d0f6f6 --- /dev/null +++ b/test/events/m/EmoteEvent.test.ts @@ -0,0 +1,83 @@ +/* +Copyright 2022 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 {InvalidEventError, EmoteEvent} from "../../../src"; +import {testSharedRoomEventInputs} from "../RoomEvent.test"; +import {WireEvent} from "../../../src/events/types_wire"; + +describe("EmoteEvent", () => { + testSharedRoomEventInputs("m.emote", x => new EmoteEvent(x), {"m.markup": [{body: "test"}]}); + + const templateEvent: WireEvent.RoomEvent = { + room_id: "!test:example.org", + event_id: "$event", + type: "m.emote", + sender: "@user:example.org", + content: {}, + origin_server_ts: 1670894499800, + }; + + it("should locate a stable markup block", () => { + const block = new EmoteEvent({...templateEvent, content: {"m.markup": [{body: "test"}]}}); + expect(block.markup).toBeDefined(); + expect(block.markup.text).toStrictEqual("test"); + expect(block.text).toStrictEqual("test"); + }); + + it("should locate an unstable markup block", () => { + const event = new EmoteEvent({...templateEvent, content: {"org.matrix.msc1767.markup": [{body: "test"}]}}); + expect(event.markup).toBeDefined(); + expect(event.markup.text).toStrictEqual("test"); + expect(event.text).toStrictEqual("test"); + }); + + it("should prefer the unstable markup block if both are provided", () => { + // Dev note: this test will need updating when extensible events becomes stable + const event = new EmoteEvent({ + ...templateEvent, + content: { + "m.markup": [{body: "stable text"}], + "org.matrix.msc1767.markup": [{body: "unstable text"}], + }, + }); + expect(event.markup).toBeDefined(); + expect(event.markup.text).toStrictEqual("unstable text"); + expect(event.text).toStrictEqual("unstable text"); + }); + + it("should proxy the text and html from the markup block", () => { + const block = new EmoteEvent({ + ...templateEvent, + content: { + "m.markup": [ + {body: "test plain", mimetype: "text/plain"}, + {body: "test html", mimetype: "text/html"}, + ], + }, + }); + expect(block.markup).toBeDefined(); + expect(block.markup.text).toStrictEqual("test plain"); + expect(block.text).toStrictEqual("test plain"); + expect(block.markup.html).toStrictEqual("test html"); + expect(block.html).toStrictEqual("test html"); + }); + + it("should error if a markup block is not present", () => { + expect(() => new EmoteEvent({...templateEvent, content: {no_block: true} as any})).toThrow( + new InvalidEventError("m.emote", "schema does not apply to m.markup or org.matrix.msc1767.markup"), + ); + }); +}); diff --git a/test/events/m/NoticeEvent.test.ts b/test/events/m/NoticeEvent.test.ts new file mode 100644 index 0000000..648f082 --- /dev/null +++ b/test/events/m/NoticeEvent.test.ts @@ -0,0 +1,83 @@ +/* +Copyright 2022 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 {InvalidEventError, NoticeEvent} from "../../../src"; +import {testSharedRoomEventInputs} from "../RoomEvent.test"; +import {WireEvent} from "../../../src/events/types_wire"; + +describe("NoticeEvent", () => { + testSharedRoomEventInputs("m.notice", x => new NoticeEvent(x), {"m.markup": [{body: "test"}]}); + + const templateEvent: WireEvent.RoomEvent = { + room_id: "!test:example.org", + event_id: "$event", + type: "m.notice", + sender: "@user:example.org", + content: {}, + origin_server_ts: 1670894499800, + }; + + it("should locate a stable markup block", () => { + const block = new NoticeEvent({...templateEvent, content: {"m.markup": [{body: "test"}]}}); + expect(block.markup).toBeDefined(); + expect(block.markup.text).toStrictEqual("test"); + expect(block.text).toStrictEqual("test"); + }); + + it("should locate an unstable markup block", () => { + const event = new NoticeEvent({...templateEvent, content: {"org.matrix.msc1767.markup": [{body: "test"}]}}); + expect(event.markup).toBeDefined(); + expect(event.markup.text).toStrictEqual("test"); + expect(event.text).toStrictEqual("test"); + }); + + it("should prefer the unstable markup block if both are provided", () => { + // Dev note: this test will need updating when extensible events becomes stable + const event = new NoticeEvent({ + ...templateEvent, + content: { + "m.markup": [{body: "stable text"}], + "org.matrix.msc1767.markup": [{body: "unstable text"}], + }, + }); + expect(event.markup).toBeDefined(); + expect(event.markup.text).toStrictEqual("unstable text"); + expect(event.text).toStrictEqual("unstable text"); + }); + + it("should proxy the text and html from the markup block", () => { + const block = new NoticeEvent({ + ...templateEvent, + content: { + "m.markup": [ + {body: "test plain", mimetype: "text/plain"}, + {body: "test html", mimetype: "text/html"}, + ], + }, + }); + expect(block.markup).toBeDefined(); + expect(block.markup.text).toStrictEqual("test plain"); + expect(block.text).toStrictEqual("test plain"); + expect(block.markup.html).toStrictEqual("test html"); + expect(block.html).toStrictEqual("test html"); + }); + + it("should error if a markup block is not present", () => { + expect(() => new NoticeEvent({...templateEvent, content: {no_block: true} as any})).toThrow( + new InvalidEventError("m.notice", "schema does not apply to m.markup or org.matrix.msc1767.markup"), + ); + }); +}); From 11b8a8b73dc7242cd33edb380cd846511cb8bfa0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Dec 2022 09:56:41 -0700 Subject: [PATCH 17/22] Test to ensure larger/more verbose content bodies work --- test/events/RoomEvent.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/events/RoomEvent.test.ts b/test/events/RoomEvent.test.ts index 4b4b020..12bd249 100644 --- a/test/events/RoomEvent.test.ts +++ b/test/events/RoomEvent.test.ts @@ -299,4 +299,21 @@ export function testSharedRoomEventInputs { + const raw: WireEvent.RoomEvent = { + room_id: "!test:example.org", + event_id: "$event", + type: "org.example.test_event", + sender: "@user:example.org", + content: { + ...safeContentInput, + "org.matrix.sdk.events.test_value": 42, + }, + origin_server_ts: 1670894499800, + }; + const ev = factory(raw); + expect(ev).toBeDefined(); + expect(ev.raw).toStrictEqual(raw); + }); } From e98e2443c770f708386338f82fcabaf83615b5da Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Dec 2022 17:06:14 -0700 Subject: [PATCH 18/22] Add an event parser --- .ctiignore | 1 + src/events/EventParser.ts | 234 +++++++++++++++++ src/events/m/EmoteEvent.ts | 25 ++ src/events/m/MessageEvent.ts | 21 ++ src/events/m/NoticeEvent.ts | 25 ++ src/index.ts | 1 + test/events/EventParser.test.ts | 443 ++++++++++++++++++++++++++++++++ 7 files changed, 750 insertions(+) create mode 100644 src/events/EventParser.ts create mode 100644 test/events/EventParser.test.ts diff --git a/.ctiignore b/.ctiignore index 4332403..dbc1325 100644 --- a/.ctiignore +++ b/.ctiignore @@ -1,4 +1,5 @@ { + "src/events/EventParser.ts": ["addInternalKnownEventParser", "addInternalUnknownEventParser", "InternalOrderCategorization"], "src/AjvContainer.ts": "*", "**/*.d.ts": "*", "test/**": "*" diff --git a/src/events/EventParser.ts b/src/events/EventParser.ts new file mode 100644 index 0000000..7f557e0 --- /dev/null +++ b/src/events/EventParser.ts @@ -0,0 +1,234 @@ +/* +Copyright 2022 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 {WireEvent} from "./types_wire"; +import {RoomEvent} from "./RoomEvent"; +import {NamespacedValue} from "../NamespacedValue"; +import {InvalidEventError} from "./InvalidEventError"; +import {InvalidBlockError} from "../content_blocks/InvalidBlockError"; + +/** + * Represents an event factory for purposes of event parsing. + * @module Event Parsing + */ +export type ParsedEventFactory = ( + x: WireEvent.RoomEvent, +) => RoomEvent; + +/** + * Represents a parser for unknown events, returning undefined if the event does + * not apply. The parser should modify the `type` and `content` of the event before + * returning the `RoomEvent<>` object, allowing consumers to see what the event got + * interpreted as. + * + * Throwing InvalidBlockError and InvalidEventError are valid if the supplied event + * *looks* compatible, but actually isn't. For example, an event containing a markup + * block, but that markup block being invalid. + * @module Event Parsing + */ +export type UnknownEventParser = ( + x: WireEvent.RoomEvent, +) => RoomEvent | undefined; + +const internalKnownEvents = new Map>(); + +// For efficiency, we maintain two arrays instead of one with tuples. This allows +// us to return the "default" parsers with ease (and without iterating the array). +// The array isn't terribly large, though it has potential to be a visible performance +// bottleneck over enough time. +// +// Note: We very carefully manage these arrays in addInternalUnknownEventParser() +const internalOrderedUnknownParsers: UnknownEventParser[] = []; +const internalOrderedUnknownCategories: InternalOrderCategorization[] = []; + +/** + * Used internally by the events-sdk to determine how to group an unknown event + * parser. + * @internal + */ +export enum InternalOrderCategorization { + // These are grouped by rough principles, ensuring that any event types + // categorized at the same level are not conflicting (ie: an unknown event + // with a file, image, and text block wouldn't get "incorrectly" deemed just + // a plain file upload). + // + // The naming/grouping of these is very much arbitrary. The only consistent + // piece is smaller numbers being considered first-checked. For example, a + // parser at "level 10" would be tried before a "level 12" parser. + OtherMedia = 0, // videos, audio + ImageMedia = 1, + RichTextOrFile = 2, // plain files, text with attributes (emotes, notices) + TextOnly = 3, +} + +/** + * Add an internally-known event type to the parser. This should not be called outside + * of the SDK itself. + * @internal + * @param namespace The namespace for the event. + * @param factory The event creation factory. + */ +export function addInternalKnownEventParser< + S extends string, + U extends string, + T extends object, + C extends WireEvent.BlockBasedContent, +>(namespace: NamespacedValue, factory: ParsedEventFactory): void { + if (namespace.stable) { + internalKnownEvents.set(namespace.stable, factory); + } + if (namespace.unstable) { + internalKnownEvents.set(namespace.unstable, factory); + } +} + +/** + * Add an unknown event parser for a known event type. This should not be called outside + * of the SDK itself. + * @param priority The priority of this parser. See InternalOrderCategorization + * @param parser The parser function. + * @see InternalOrderCategorization + */ +export function addInternalUnknownEventParser( + priority: InternalOrderCategorization, + parser: UnknownEventParser, +): void { + // First, find the index we'll want to insert at using a binary search. + // We use a binary search because it's empirically faster than more traditional + // approaches for a set size of 10-100 entries. + // Source: https://stackoverflow.com/a/21822316 + // Validation: https://gist.github.com/turt2live/1f7bdf75a0fb0923fe4bee577471841f + let lowIdx = 0; + let highIdx = internalOrderedUnknownCategories.length; + while (lowIdx < highIdx) { + const midIdx = (lowIdx + highIdx) >>> 1; // integer division by 2 + if (internalOrderedUnknownCategories[midIdx] < priority) { + lowIdx = midIdx + 1; + } else { + highIdx = midIdx; + } + } + + // Splice the new elements into the arrays + internalOrderedUnknownCategories.splice(lowIdx, 0, priority); + internalOrderedUnknownParsers.splice(lowIdx, 0, parser); +} + +/** + * Parses known and unknown events to determine their validity. The SDK's internally + * known events will always be included in the parser: there is no need to add them + * explicitly. + * + * Consumers may wish to adjust the "unknown interpret order" to customize functionality + * for when the parser encounters an event type it does not have a factory for. + * @module Event Parsing + */ +export class EventParser { + private typeMap = new Map>(internalKnownEvents); + private unknownInterpretOrder: UnknownEventParser[] = [...internalOrderedUnknownParsers]; + + /** + * All the known event types for this parser. + */ + public get knownEventTypes(): string[] { + return Array.from(this.typeMap.keys()); + } + + /** + * The default unknown event type parse order. Custom events would normally + * get prepended to this array to ensure they get checked first. + */ + public get defaultUnknownEventParsers(): UnknownEventParser[] { + return [...internalOrderedUnknownParsers]; + } + + /** + * The unknown event type parsers currently known to this instance. + */ + public get unknownEventParsers(): UnknownEventParser[] { + return [...this.unknownInterpretOrder]; + } + + /** + * Adds or overwrites a known type in the event parser. Does not affect the + * "unknown event" parse order. + * @param namespace The namespace for the event type. + * @param factory The factory used to create the event object. + */ + public addKnownType( + namespace: NamespacedValue, + factory: ParsedEventFactory, + ): void { + if (namespace.stable) { + this.typeMap.set(namespace.stable, factory); + } + if (namespace.unstable) { + this.typeMap.set(namespace.unstable, factory); + } + } + + /** + * Sets the "unknown event type" parser order, where the first element of the + * provided array will be tried first. The first matching parser will be used. + * + * Note that this overwrites the parsers rather than appends/prepends: callers + * should use the defaultUnknownEventParsers property to prepend their custom + * events prior to calling this set function. + * @param ordered The parsers, ordered by which parser should be tried first. + * @see defaultUnknownEventParsers + */ + public setUnknownParsers( + ordered: UnknownEventParser[], + ): void { + this.unknownInterpretOrder = ordered; + } + + /** + * Parses an event. If the event type is known to the parser, it will be parsed + * as that type. If the event type is unknown, the "unknown event" parsers will + * be used to return an appropriate type. If no parser applies after both sets + * are checked, undefined is returned. + * + * An event returned by this function might have different attributes (event type, + * content, etc) than the event supplied in the function: this is to allow callers + * to check what type of event the parser is treating given type as. For example, + * if an `org.example.sample` event type was given here, the function might return + * an event type which matches `m.message`. + * + * Note that this function can throw InvalidBlockError and InvalidEventError, or + * other errors, if the given event is unforgivably unable to be parsed. + * @param event The event to try parsing. + * @returns The parsed event, or undefined if unable. + */ + public parse(event: WireEvent.RoomEvent): RoomEvent | undefined { + if (this.typeMap.has(event.type)) { + return this.typeMap.get(event.type)!(event); + } + for (const parser of this.unknownInterpretOrder) { + try { + const ev = parser(event); + if (ev !== undefined) return ev; + } catch (e) { + if (e instanceof InvalidEventError || e instanceof InvalidBlockError) { + // consume these errors - we'll just try the next parser + } else { + throw e; // definitely re-throw everything else though + } + } + } + return undefined; // unknown event + } +} diff --git a/src/events/m/EmoteEvent.ts b/src/events/m/EmoteEvent.ts index ce534b1..ac2259f 100644 --- a/src/events/m/EmoteEvent.ts +++ b/src/events/m/EmoteEvent.ts @@ -23,6 +23,8 @@ import {Schema} from "ajv"; import {AjvContainer} from "../../AjvContainer"; import {LazyValue} from "../../LazyValue"; import {InvalidEventError} from "../InvalidEventError"; +import {addInternalKnownEventParser, addInternalUnknownEventParser, InternalOrderCategorization} from "../EventParser"; +import {EmoteBlock, WireEmoteBlock} from "../../content_blocks/m/EmoteBlock"; /** * Types for emote events over the wire. @@ -45,6 +47,29 @@ export class EmoteEvent extends RoomEvent { private lazyMarkup = new LazyValue(() => new MarkupBlock(MarkupBlock.type.findIn(this.content)!)); + static { + // Register the event type as a default event type + addInternalKnownEventParser( + EmoteEvent.type, + (x: WireEvent.RoomEvent) => new EmoteEvent(x), + ); + + // Also register an unknown event parser for handling + addInternalUnknownEventParser(InternalOrderCategorization.RichTextOrFile, x => { + const possibleBlock = EmoteBlock.type.findIn(x.content); + if (!!possibleBlock) { + const block = new EmoteBlock(possibleBlock as WireEmoteBlock.Value); + return new EmoteEvent({ + ...x, + type: EmoteEvent.type.name, + content: block.raw, // extract the `m.markup` block out of the `m.emote` block + }); + } else { + return undefined; // not likely to be parsable by us + } + }); + } + public constructor(raw: WireEvent.RoomEvent) { super(EmoteEvent.type.stable!, raw); if (!EmoteEvent.contentValidateFn(this.content)) { diff --git a/src/events/m/MessageEvent.ts b/src/events/m/MessageEvent.ts index 4c839b4..4d0399c 100644 --- a/src/events/m/MessageEvent.ts +++ b/src/events/m/MessageEvent.ts @@ -23,6 +23,7 @@ import {UnstableValue} from "../../NamespacedValue"; import {WireEvent} from "../types_wire"; import {InvalidEventError} from "../InvalidEventError"; import {LazyValue} from "../../LazyValue"; +import {addInternalKnownEventParser, addInternalUnknownEventParser, InternalOrderCategorization} from "../EventParser"; /** * Types for message events over the wire. @@ -48,6 +49,26 @@ export class MessageEvent extends RoomEvent { private lazyMarkup = new LazyValue(() => new MarkupBlock(MarkupBlock.type.findIn(this.content)!)); + static { + // Register the event type as a default event type + addInternalKnownEventParser( + MessageEvent.type, + (x: WireEvent.RoomEvent) => new MessageEvent(x), + ); + + // Also register an unknown event parser for handling + addInternalUnknownEventParser(InternalOrderCategorization.TextOnly, x => { + if (MarkupBlock.type.findIn(x.content)) { + return new MessageEvent({ + ...x, + type: MessageEvent.type.name, + }); + } else { + return undefined; // not likely to be parsable by us + } + }); + } + public constructor(raw: WireEvent.RoomEvent) { super(MessageEvent.type.stable!, raw); if (!MessageEvent.contentValidateFn(this.content)) { diff --git a/src/events/m/NoticeEvent.ts b/src/events/m/NoticeEvent.ts index ec8dbb1..da5e645 100644 --- a/src/events/m/NoticeEvent.ts +++ b/src/events/m/NoticeEvent.ts @@ -23,6 +23,8 @@ import {Schema} from "ajv"; import {AjvContainer} from "../../AjvContainer"; import {LazyValue} from "../../LazyValue"; import {InvalidEventError} from "../InvalidEventError"; +import {addInternalKnownEventParser, addInternalUnknownEventParser, InternalOrderCategorization} from "../EventParser"; +import {NoticeBlock, WireNoticeBlock} from "../../content_blocks/m/NoticeBlock"; /** * Types for notice events over the wire. @@ -45,6 +47,29 @@ export class NoticeEvent extends RoomEvent { private lazyMarkup = new LazyValue(() => new MarkupBlock(MarkupBlock.type.findIn(this.content)!)); + static { + // Register the event type as a default event type + addInternalKnownEventParser( + NoticeEvent.type, + (x: WireEvent.RoomEvent) => new NoticeEvent(x), + ); + + // Also register an unknown event parser for handling + addInternalUnknownEventParser(InternalOrderCategorization.RichTextOrFile, x => { + const possibleBlock = NoticeBlock.type.findIn(x.content); + if (!!possibleBlock) { + const block = new NoticeBlock(possibleBlock as WireNoticeBlock.Value); + return new NoticeEvent({ + ...x, + type: NoticeEvent.type.name, + content: block.raw, // extract the `m.markup` block out of the `m.notice` block + }); + } else { + return undefined; // not likely to be parsable by us + } + }); + } + public constructor(raw: WireEvent.RoomEvent) { super(NoticeEvent.type.stable!, raw); if (!NoticeEvent.contentValidateFn(this.content)) { diff --git a/src/index.ts b/src/index.ts index 69f4a4a..3aa32fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ export * from "./content_blocks/IntegerBlock"; export * from "./content_blocks/InvalidBlockError"; export * from "./content_blocks/ObjectBlock"; export * from "./content_blocks/StringBlock"; +export {type ParsedEventFactory, type UnknownEventParser, EventParser} from "./events/EventParser"; export * from "./events/InvalidEventError"; export * from "./events/RoomEvent"; export * from "./LazyValue"; diff --git a/test/events/EventParser.test.ts b/test/events/EventParser.test.ts new file mode 100644 index 0000000..b9c8bfd --- /dev/null +++ b/test/events/EventParser.test.ts @@ -0,0 +1,443 @@ +/* +Copyright 2022 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 { + EitherAnd, + EmoteBlock, + EmoteEvent, + EventParser, + InvalidBlockError, + InvalidEventError, + MessageEvent, + NamespacedValue, + NoticeBlock, + NoticeEvent, + ParsedEventFactory, + RoomEvent, + UnstableValue, + WireEmoteBlock, + WireMessageEvent, + WireNoticeBlock, +} from "../../src"; +import {WireEvent} from "../../src/events/types_wire"; + +type TestCustomEventContent = { + "org.example.custom": { + hello: string; + is_test: boolean; + }; +}; +class TestCustomEvent extends RoomEvent { + public static readonly type = new UnstableValue(null, "org.example.custom"); + + public constructor(raw: WireEvent.RoomEvent) { + super(TestCustomEvent.type.name, raw); + // DANGER: We do NOT validate content schema like we're supposed to! + } +} + +describe("EventParser", () => { + it("should have the built-in types defined as known event types", () => { + const expected = [MessageEvent.type, EmoteEvent.type, NoticeEvent.type] + .map(ns => [ns.stable, ns.unstable]) + .reduce((p, c) => [...p, ...c], []) + .filter(x => !!x) + .sort(); + const parser = new EventParser(); + expect(parser.knownEventTypes.sort()).toStrictEqual(expected); + }); + + it("should have some unknown event parsers for built-in types", () => { + const parser = new EventParser(); + expect(parser.defaultUnknownEventParsers.length).toBeGreaterThan(0); + + // Unfortunately we can't realistically check to see if the actual parsers + // have the correct values, but it shows that we registered *something* if + // there's at least one available. + }); + + describe("known event types", () => { + it("should parse a known message event as a message", () => { + const ev: WireEvent.RoomEvent = { + room_id: "!room:example.org", + event_id: "$test", + type: "m.message", + state_key: undefined, + sender: "@user:example.org", + origin_server_ts: 1671145380506, + content: { + "m.markup": [{body: "test"}], + }, + }; + const parser = new EventParser(); + const parsed = parser.parse(ev); + expect(parsed).toBeDefined(); + expect(parsed!.type).toStrictEqual("m.message"); + // noinspection SuspiciousTypeOfGuard + expect(parsed instanceof MessageEvent).toStrictEqual(true); + expect((parsed as unknown as MessageEvent).text).toStrictEqual("test"); + }); + + it("should parse a known emote event as an emote", () => { + const ev: WireEvent.RoomEvent = { + room_id: "!room:example.org", + event_id: "$test", + type: "m.emote", + state_key: undefined, + sender: "@user:example.org", + origin_server_ts: 1671145380506, + content: { + "m.markup": [{body: "test"}], + }, + }; + const parser = new EventParser(); + const parsed = parser.parse(ev); + expect(parsed).toBeDefined(); + expect(parsed!.type).toStrictEqual("m.emote"); + // noinspection SuspiciousTypeOfGuard + expect(parsed instanceof EmoteEvent).toStrictEqual(true); + expect((parsed as unknown as EmoteEvent).text).toStrictEqual("test"); + }); + + it("should parse a known notice event as a notice", () => { + const ev: WireEvent.RoomEvent = { + room_id: "!room:example.org", + event_id: "$test", + type: "m.notice", + state_key: undefined, + sender: "@user:example.org", + origin_server_ts: 1671145380506, + content: { + "m.markup": [{body: "test"}], + }, + }; + const parser = new EventParser(); + const parsed = parser.parse(ev); + expect(parsed).toBeDefined(); + expect(parsed!.type).toStrictEqual("m.notice"); + // noinspection SuspiciousTypeOfGuard + expect(parsed instanceof NoticeEvent).toStrictEqual(true); + expect((parsed as unknown as NoticeEvent).text).toStrictEqual("test"); + }); + + describe("custom", () => { + it("should parse a custom event as that custom event", () => { + const factory: ParsedEventFactory = x => + new TestCustomEvent(x); + const parser1 = new EventParser(); + const parser2 = new EventParser(); + parser1.addKnownType(TestCustomEvent.type, factory); + + // check to make sure the known type didn't leak across instances + expect(parser1.knownEventTypes.includes(TestCustomEvent.type.name)).toStrictEqual(true); + expect(parser2.knownEventTypes.includes(TestCustomEvent.type.name)).toStrictEqual(false); + + // Try parsing the custom event + const ev: WireEvent.RoomEvent = { + room_id: "!room:example.org", + event_id: "$test", + type: "org.example.custom", + state_key: undefined, + sender: "@user:example.org", + origin_server_ts: 1671145380506, + content: { + "org.example.custom": { + hello: "world", + is_test: true, + }, + }, + }; + const parsed = parser1.parse(ev); + expect(parsed).toBeDefined(); + expect(parsed!.type).toStrictEqual("org.example.custom"); + // noinspection SuspiciousTypeOfGuard + expect(parsed instanceof TestCustomEvent).toStrictEqual(true); + + // And to be doubly sure, the other parser fails to find anything useful: + const unknownParsed = parser2.parse(ev); + expect(unknownParsed).toBeUndefined(); + }); + }); + }); + + describe("unknown event types", () => { + it("should parse an unknown, message-like, event as a message", () => { + const ev: WireEvent.RoomEvent = { + room_id: "!room:example.org", + event_id: "$test", + type: "org.example.custom", + state_key: undefined, + sender: "@user:example.org", + origin_server_ts: 1671145380506, + content: { + "m.markup": [{body: "test"}], + }, + }; + // noinspection DuplicatedCode + const parser = new EventParser(); + const parsed = parser.parse(ev); + expect(parsed).toBeDefined(); + expect(parsed!.type).toStrictEqual(MessageEvent.type.name); + // noinspection SuspiciousTypeOfGuard + expect(parsed instanceof MessageEvent).toStrictEqual(true); + expect((parsed as unknown as MessageEvent).text).toStrictEqual("test"); + }); + + it("should parse an unknown, emote-like, event as an emote", () => { + const ev: WireEvent.RoomEvent< + EitherAnd< + {[EmoteBlock.type.name]: WireEmoteBlock.Value}, + {[EmoteBlock.type.altName]: WireEmoteBlock.Value} + > + > = { + room_id: "!room:example.org", + event_id: "$test", + type: "org.example.custom", + state_key: undefined, + sender: "@user:example.org", + origin_server_ts: 1671145380506, + content: { + "m.emote": { + "m.markup": [{body: "test"}], + }, + }, + }; + // noinspection DuplicatedCode + const parser = new EventParser(); + const parsed = parser.parse(ev); + expect(parsed).toBeDefined(); + expect(parsed!.type).toStrictEqual(EmoteEvent.type.name); + // noinspection SuspiciousTypeOfGuard + expect(parsed instanceof EmoteEvent).toStrictEqual(true); + expect((parsed as unknown as EmoteEvent).text).toStrictEqual("test"); + }); + + it("should parse an unknown, notice-like, event as a notice", () => { + const ev: WireEvent.RoomEvent< + EitherAnd< + {[NoticeBlock.type.name]: WireNoticeBlock.Value}, + {[NoticeBlock.type.altName]: WireNoticeBlock.Value} + > + > = { + room_id: "!room:example.org", + event_id: "$test", + type: "org.example.custom", + state_key: undefined, + sender: "@user:example.org", + origin_server_ts: 1671145380506, + content: { + "m.notice": { + "m.markup": [{body: "test"}], + }, + }, + }; + // noinspection DuplicatedCode + const parser = new EventParser(); + const parsed = parser.parse(ev); + expect(parsed).toBeDefined(); + expect(parsed!.type).toStrictEqual(NoticeEvent.type.name); + // noinspection SuspiciousTypeOfGuard + expect(parsed instanceof NoticeEvent).toStrictEqual(true); + expect((parsed as unknown as NoticeEvent).text).toStrictEqual("test"); + }); + + describe("custom", () => { + it("should respect the custom unknown event parser order", () => { + const customParser = (x: WireEvent.RoomEvent) => + new TestCustomEvent({ + ...x, + type: TestCustomEvent.type.name, + content: { + "org.example.custom": (x.content as any)["org.example.custom"] ?? { + hello: "wrong", + is_test: true, + }, + }, + }); + const ev: WireEvent.RoomEvent = { + room_id: "!room:example.org", + event_id: "$test", + type: "org.example.custom", + state_key: undefined, + sender: "@user:example.org", + origin_server_ts: 1671145380506, + content: { + "m.notice": { + "m.markup": [{body: "parsed as notice"}], + }, + "m.markup": [{body: "parsed as plain"}], + "org.example.custom": { + hello: "parsed as custom", + is_test: true, + }, + }, + }; + const parser = new EventParser(); + + // Should parse the event as a notice first + let parsed = parser.parse(ev); + expect(parsed).toBeDefined(); + expect(parsed!.type).toStrictEqual(NoticeEvent.type.name); + // noinspection SuspiciousTypeOfGuard + expect(parsed instanceof NoticeEvent).toStrictEqual(true); + expect((parsed as unknown as NoticeEvent).text).toStrictEqual("parsed as notice"); + + // If we reverse the order, it should parse as text + parser.setUnknownParsers(parser.defaultUnknownEventParsers.reverse()); + parsed = parser.parse(ev); + expect(parsed).toBeDefined(); + expect(parsed!.type).toStrictEqual(MessageEvent.type.name); + // noinspection SuspiciousTypeOfGuard + expect(parsed instanceof MessageEvent).toStrictEqual(true); + expect((parsed as unknown as MessageEvent).text).toStrictEqual("parsed as plain"); + + // Similarly, if we add our custom parser to the front then it should parse as that + parser.setUnknownParsers([customParser, ...parser.defaultUnknownEventParsers]); + parsed = parser.parse(ev); + expect(parsed!.type).toStrictEqual(TestCustomEvent.type.name); + // noinspection SuspiciousTypeOfGuard + expect(parsed instanceof TestCustomEvent).toStrictEqual(true); + expect((parsed as unknown as TestCustomEvent).content["org.example.custom"].hello).toStrictEqual( + "parsed as custom", + ); + + // also similarly, it should parse it as a notice if we put our custom parser last + parser.setUnknownParsers([...parser.defaultUnknownEventParsers, customParser]); + parsed = parser.parse(ev); + expect(parsed).toBeDefined(); + expect(parsed!.type).toStrictEqual(NoticeEvent.type.name); + // noinspection SuspiciousTypeOfGuard + expect(parsed instanceof NoticeEvent).toStrictEqual(true); + expect((parsed as unknown as NoticeEvent).text).toStrictEqual("parsed as notice"); + + // final check: did any of this affect other instances of parsers? (it shouldn't) + const parser2 = new EventParser(); + expect(parser2.unknownEventParsers).toStrictEqual(parser2.defaultUnknownEventParsers); + }); + }); + }); + + it("should throw if the known event parser throws", () => { + const ev: WireEvent.RoomEvent = { + room_id: "!room:example.org", + event_id: "$test", + type: TestCustomEvent.type.name, + state_key: undefined, + sender: "@user:example.org", + origin_server_ts: 1671145380506, + content: {}, // shouldn't matter what the content is: it should explode before we get there + }; + + const parser = new EventParser(); + + parser.addKnownType(TestCustomEvent.type, () => { + throw new InvalidEventError("TestEvent", "Test Case Throw"); + }); + expect(() => parser.parse(ev)).toThrow(new InvalidEventError("TestEvent", "Test Case Throw")); + + parser.addKnownType(TestCustomEvent.type, () => { + throw new InvalidBlockError("TestEvent", "Test Case Throw"); + }); + expect(() => parser.parse(ev)).toThrow(new InvalidBlockError("TestEvent", "Test Case Throw")); + + parser.addKnownType(TestCustomEvent.type, () => { + throw new Error("Test Case Throw"); + }); + expect(() => parser.parse(ev)).toThrow(new Error("Test Case Throw")); + }); + + it("should throw when the unknown event parser throws unexpectedly", () => { + const ev: WireEvent.RoomEvent = { + room_id: "!room:example.org", + event_id: "$test", + type: TestCustomEvent.type.name, + state_key: undefined, + sender: "@user:example.org", + origin_server_ts: 1671145380506, + content: {}, // shouldn't matter what the content is: it should explode before we get there + }; + + const parser = new EventParser(); + + parser.setUnknownParsers([ + () => { + throw new InvalidEventError("TestEvent", "Test Case Throw"); + }, + ]); + expect(parser.parse(ev)).toBeUndefined(); // no throw + + parser.setUnknownParsers([ + () => { + throw new InvalidBlockError("TestEvent", "Test Case Throw"); + }, + ]); + expect(parser.parse(ev)).toBeUndefined(); // no throw + + parser.setUnknownParsers([ + () => { + throw new Error("Test Case Throw"); + }, + ]); + expect(() => parser.parse(ev)).toThrow(new Error("Test Case Throw")); + }); + + it("should not parse events that don't match any criteria", () => { + const ev: WireEvent.RoomEvent = { + room_id: "!room:example.org", + event_id: "$test", + type: "org.example.custom", + state_key: undefined, + sender: "@user:example.org", + origin_server_ts: 1671145380506, + content: {}, + }; + const parser = new EventParser(); + expect(parser.parse(ev)).toBeUndefined(); + }); + + it("should not return a mutable copy of the default unknown parsers", () => { + const parser = new EventParser(); + const firstParser = parser.defaultUnknownEventParsers[0]; + parser.defaultUnknownEventParsers.reverse(); + expect(parser.defaultUnknownEventParsers[0]).toStrictEqual(firstParser); + }); + + it("should not return a mutable copy of the known unknown parsers", () => { + const parser = new EventParser(); + const firstParser = parser.unknownEventParsers[0]; + parser.unknownEventParsers.reverse(); + expect(parser.unknownEventParsers[0]).toStrictEqual(firstParser); + }); + + it.each([ + ["stable", null], + [null, "unstable"], + ["stable", "unstable"], + ])("should register stable and unstable values as known: s='%s', u='%s'", (stable, unstable) => { + const ns = new NamespacedValue(stable, unstable); + const parser = new EventParser(); + parser.addKnownType(ns, () => { + throw new Error("unused"); + }); + + if (stable !== null) { + expect(parser.knownEventTypes).toContain(stable); + } + if (unstable !== null) { + expect(parser.knownEventTypes).toContain(unstable); + } + }); +}); From d63b97629594c93289bc752d8bc8700a5aa43155 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Dec 2022 17:08:34 -0700 Subject: [PATCH 19/22] Expose wire types in index by not using d.ts files --- src/content_blocks/{types_wire.d.ts => types_wire.ts} | 2 +- src/events/{types_wire.d.ts => types_wire.ts} | 6 +++--- src/index.ts | 2 ++ test/events/EventParser.test.ts | 2 +- test/events/RoomEvent.test.ts | 3 +-- test/events/m/EmoteEvent.test.ts | 3 +-- test/events/m/MessageEvent.test.ts | 3 +-- test/events/m/NoticeEvent.test.ts | 3 +-- 8 files changed, 11 insertions(+), 13 deletions(-) rename src/content_blocks/{types_wire.d.ts => types_wire.ts} (91%) rename src/events/{types_wire.d.ts => types_wire.ts} (86%) diff --git a/src/content_blocks/types_wire.d.ts b/src/content_blocks/types_wire.ts similarity index 91% rename from src/content_blocks/types_wire.d.ts rename to src/content_blocks/types_wire.ts index 85bc0ff..2e51b60 100644 --- a/src/content_blocks/types_wire.d.ts +++ b/src/content_blocks/types_wire.ts @@ -23,5 +23,5 @@ export module WireContentBlock { * Possible value types for a content block. * @module Content Blocks */ - type Value = string | boolean | number | object | Value[]; + export type Value = string | boolean | number | object | Value[]; } diff --git a/src/events/types_wire.d.ts b/src/events/types_wire.ts similarity index 86% rename from src/events/types_wire.d.ts rename to src/events/types_wire.ts index f6d2e84..f0023f4 100644 --- a/src/events/types_wire.d.ts +++ b/src/events/types_wire.ts @@ -25,7 +25,7 @@ export module WireEvent { * A Matrix event. Also called a ClientEvent by the Matrix Specification. * @module Events */ - interface RoomEvent { + export interface RoomEvent { room_id: string; event_id: string; type: string; @@ -40,7 +40,7 @@ export module WireEvent { * A simple `content` schema for content block-supporting events. * @module Events */ - type BlockBasedContent = { + export type BlockBasedContent = { [k: string]: WireContentBlock.Value; }; @@ -49,5 +49,5 @@ export module WireEvent { * type system level. * @module Events */ - type BlockSpecificContent = Blocks & {[k: string]: never}; + export type BlockSpecificContent = Blocks & {[k: string]: never}; } diff --git a/src/index.ts b/src/index.ts index 3aa32fa..6472995 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,9 +15,11 @@ export * from "./content_blocks/IntegerBlock"; export * from "./content_blocks/InvalidBlockError"; export * from "./content_blocks/ObjectBlock"; export * from "./content_blocks/StringBlock"; +export * from "./content_blocks/types_wire"; export {type ParsedEventFactory, type UnknownEventParser, EventParser} from "./events/EventParser"; export * from "./events/InvalidEventError"; export * from "./events/RoomEvent"; +export * from "./events/types_wire"; export * from "./LazyValue"; export * from "./NamespacedValue"; export * from "./types"; diff --git a/test/events/EventParser.test.ts b/test/events/EventParser.test.ts index b9c8bfd..d7a0dc1 100644 --- a/test/events/EventParser.test.ts +++ b/test/events/EventParser.test.ts @@ -29,10 +29,10 @@ import { RoomEvent, UnstableValue, WireEmoteBlock, + WireEvent, WireMessageEvent, WireNoticeBlock, } from "../../src"; -import {WireEvent} from "../../src/events/types_wire"; type TestCustomEventContent = { "org.example.custom": { diff --git a/test/events/RoomEvent.test.ts b/test/events/RoomEvent.test.ts index 12bd249..dfbe20e 100644 --- a/test/events/RoomEvent.test.ts +++ b/test/events/RoomEvent.test.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InvalidEventError, RoomEvent} from "../../src"; -import {WireEvent} from "../../src/events/types_wire"; +import {InvalidEventError, RoomEvent, WireEvent} from "../../src"; class TestEvent extends RoomEvent { public constructor(raw: any) { diff --git a/test/events/m/EmoteEvent.test.ts b/test/events/m/EmoteEvent.test.ts index 1d0f6f6..4efcbc0 100644 --- a/test/events/m/EmoteEvent.test.ts +++ b/test/events/m/EmoteEvent.test.ts @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InvalidEventError, EmoteEvent} from "../../../src"; +import {EmoteEvent, InvalidEventError, WireEvent} from "../../../src"; import {testSharedRoomEventInputs} from "../RoomEvent.test"; -import {WireEvent} from "../../../src/events/types_wire"; describe("EmoteEvent", () => { testSharedRoomEventInputs("m.emote", x => new EmoteEvent(x), {"m.markup": [{body: "test"}]}); diff --git a/test/events/m/MessageEvent.test.ts b/test/events/m/MessageEvent.test.ts index f43c09e..6932304 100644 --- a/test/events/m/MessageEvent.test.ts +++ b/test/events/m/MessageEvent.test.ts @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InvalidEventError, MessageEvent} from "../../../src"; +import {InvalidEventError, MessageEvent, WireEvent} from "../../../src"; import {testSharedRoomEventInputs} from "../RoomEvent.test"; -import {WireEvent} from "../../../src/events/types_wire"; describe("MessageEvent", () => { testSharedRoomEventInputs("m.message", x => new MessageEvent(x), {"m.markup": [{body: "test"}]}); diff --git a/test/events/m/NoticeEvent.test.ts b/test/events/m/NoticeEvent.test.ts index 648f082..951b08c 100644 --- a/test/events/m/NoticeEvent.test.ts +++ b/test/events/m/NoticeEvent.test.ts @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InvalidEventError, NoticeEvent} from "../../../src"; +import {InvalidEventError, NoticeEvent, WireEvent} from "../../../src"; import {testSharedRoomEventInputs} from "../RoomEvent.test"; -import {WireEvent} from "../../../src/events/types_wire"; describe("NoticeEvent", () => { testSharedRoomEventInputs("m.notice", x => new NoticeEvent(x), {"m.markup": [{body: "test"}]}); From aa1c45b1ab46c834033980d9b5b040e00a1a0f6b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Dec 2022 17:33:06 -0700 Subject: [PATCH 20/22] Test for events being state events when they shouldn't be (and inverse) --- src/events/RoomEvent.ts | 20 ++++- src/events/m/EmoteEvent.ts | 2 +- src/events/m/MessageEvent.ts | 2 +- src/events/m/NoticeEvent.ts | 2 +- test/events/RoomEvent.test.ts | 129 +++++++++++++++++++++++------ test/events/m/EmoteEvent.test.ts | 2 +- test/events/m/MessageEvent.test.ts | 2 +- test/events/m/NoticeEvent.test.ts | 2 +- 8 files changed, 129 insertions(+), 32 deletions(-) diff --git a/src/events/RoomEvent.ts b/src/events/RoomEvent.ts index 33cab06..f13758e 100644 --- a/src/events/RoomEvent.ts +++ b/src/events/RoomEvent.ts @@ -94,9 +94,16 @@ export abstract class RoomEvent) { + protected constructor( + public readonly name: string, + public readonly raw: WireEvent.RoomEvent, + isStateEvent = false, + ) { if (raw === null || raw === undefined) { throw new InvalidEventError( this.name, @@ -106,6 +113,17 @@ export abstract class RoomEvent { } public constructor(raw: WireEvent.RoomEvent) { - super(EmoteEvent.type.stable!, raw); + super(EmoteEvent.type.stable!, raw, false); if (!EmoteEvent.contentValidateFn(this.content)) { throw new InvalidEventError(this.name, EmoteEvent.contentValidateFn.errors); } diff --git a/src/events/m/MessageEvent.ts b/src/events/m/MessageEvent.ts index 4d0399c..4ee530e 100644 --- a/src/events/m/MessageEvent.ts +++ b/src/events/m/MessageEvent.ts @@ -70,7 +70,7 @@ export class MessageEvent extends RoomEvent { } public constructor(raw: WireEvent.RoomEvent) { - super(MessageEvent.type.stable!, raw); + super(MessageEvent.type.stable!, raw, false); if (!MessageEvent.contentValidateFn(this.content)) { throw new InvalidEventError(this.name, MessageEvent.contentValidateFn.errors); } diff --git a/src/events/m/NoticeEvent.ts b/src/events/m/NoticeEvent.ts index da5e645..ba3e0ab 100644 --- a/src/events/m/NoticeEvent.ts +++ b/src/events/m/NoticeEvent.ts @@ -71,7 +71,7 @@ export class NoticeEvent extends RoomEvent { } public constructor(raw: WireEvent.RoomEvent) { - super(NoticeEvent.type.stable!, raw); + super(NoticeEvent.type.stable!, raw, false); if (!NoticeEvent.contentValidateFn(this.content)) { throw new InvalidEventError(this.name, NoticeEvent.contentValidateFn.errors); } diff --git a/test/events/RoomEvent.test.ts b/test/events/RoomEvent.test.ts index dfbe20e..23fd646 100644 --- a/test/events/RoomEvent.test.ts +++ b/test/events/RoomEvent.test.ts @@ -17,27 +17,34 @@ limitations under the License. import {InvalidEventError, RoomEvent, WireEvent} from "../../src"; class TestEvent extends RoomEvent { - public constructor(raw: any) { - super("TestEvent", raw); // lie to TS + public constructor(raw: any, isState: boolean) { + super("TestEvent", raw, isState); // lie to TS } } describe("RoomEvent", () => { - testSharedRoomEventInputs("TestEvent", x => new TestEvent(x), {hello: "world"}); + testSharedRoomEventInputs("TestEvent", undefined, x => new TestEvent(x, false), {hello: "world"}); + testSharedRoomEventInputs("TestEvent", "non-empty", x => new TestEvent(x, true), {hello: "world"}); + testSharedRoomEventInputs("TestEvent", "", x => new TestEvent(x, true), {hello: "world"}); }); /** * Runs all the validation tests which can be shared across all event type classes. * @param eventName The event name, for error matching. + * @param safeStateKey Undefined to denote the event cannot be a state event, a + * string otherwise. * @param factory The factory to create a RoomEvent. * @param safeContentInput The content to supply on an event during a non-throwing * test. This should be the minimum to construct the event, not ideal conditions. */ export function testSharedRoomEventInputs( eventName: string, + safeStateKey: undefined | string, factory: (raw: WireEvent.RoomEvent) => RoomEvent, safeContentInput: W, ) { + testStateEventCharacteristics(eventName, safeStateKey, factory, safeContentInput); + describe("internal", () => { it("should have valid inputs", () => { expect(eventName).toBeDefined(); @@ -59,6 +66,7 @@ export function testSharedRoomEventInputs { - const raw: WireEvent.RoomEvent = { - room_id: "!test:example.org", - event_id: "$event", - type: "org.example.test_event", - state_key: skey, - sender: "@user:example.org", - content: safeContentInput, - origin_server_ts: 1670894499800, - }; - const ev = factory(raw); - expect(ev).toBeDefined(); - expect(ev.raw).toStrictEqual(raw); - expect(ev.roomId).toStrictEqual(raw.room_id); - expect(ev.eventId).toStrictEqual(raw.event_id); - expect(ev.type).toStrictEqual(raw.type); - expect(ev.stateKey).toStrictEqual(raw.state_key); - expect(ev.sender).toStrictEqual(raw.sender); - expect(ev.timestamp).toStrictEqual(raw.origin_server_ts); - expect(ev.content).toStrictEqual(raw.content); - }); - it("should retain the event name", () => { const raw: WireEvent.RoomEvent = { room_id: "!test:example.org", @@ -105,6 +91,7 @@ export function testSharedRoomEventInputs( + eventName: string, + safeStateKey: undefined | string, + factory: (raw: WireEvent.RoomEvent) => RoomEvent, + safeContentInput: W, +) { + describe("internal", () => { + it("should have valid inputs", () => { + expect(eventName).toBeDefined(); + expect(typeof eventName).toStrictEqual("string"); + expect(eventName.length).toBeGreaterThan(0); + + expect(factory).toBeDefined(); + expect(typeof factory).toStrictEqual("function"); + }); + + // Note: unlike other repeatable tests, we don't validate that the factory actually works + // because we expect it to end up throwing for the values we give it. + }); + + it.each(safeStateKey !== undefined ? [safeStateKey] : [undefined])( + "should handle valid event structures with state key of '%s'", + skey => { + const raw: WireEvent.RoomEvent = { + room_id: "!test:example.org", + event_id: "$event", + type: "org.example.test_event", + state_key: skey, + sender: "@user:example.org", + content: safeContentInput, + origin_server_ts: 1670894499800, + }; + const ev = factory(raw); + expect(ev).toBeDefined(); + expect(ev.raw).toStrictEqual(raw); + expect(ev.roomId).toStrictEqual(raw.room_id); + expect(ev.eventId).toStrictEqual(raw.event_id); + expect(ev.type).toStrictEqual(raw.type); + expect(ev.stateKey).toStrictEqual(raw.state_key); + expect(ev.sender).toStrictEqual(raw.sender); + expect(ev.timestamp).toStrictEqual(raw.origin_server_ts); + expect(ev.content).toStrictEqual(raw.content); + }, + ); + + if (safeStateKey !== undefined) { + it("should reject events without a state key", () => { + const ev: WireEvent.RoomEvent = { + room_id: "!test:example.org", + event_id: "$event", + type: "org.example.not_used", + sender: "@user:example.org", + content: {}, + origin_server_ts: 1670894499800, + }; + expect(() => factory(ev)).toThrow( + new InvalidEventError( + eventName, + "This event is only allowed to be a state event and must be converted accordingly.", + ), + ); + }); + } else { + it.each(["", "not_empty"])("should reject state events with state key: '%s'", val => { + const ev: WireEvent.RoomEvent = { + room_id: "!test:example.org", + event_id: "$event", + type: "org.example.not_used", + state_key: val, + sender: "@user:example.org", + content: {}, + origin_server_ts: 1670894499800, + }; + expect(() => factory(ev)).toThrow( + new InvalidEventError( + eventName, + "This event is not allowed to be a state event and must be converted accordingly.", + ), + ); + }); + } +} diff --git a/test/events/m/EmoteEvent.test.ts b/test/events/m/EmoteEvent.test.ts index 4efcbc0..2eb3554 100644 --- a/test/events/m/EmoteEvent.test.ts +++ b/test/events/m/EmoteEvent.test.ts @@ -18,7 +18,7 @@ import {EmoteEvent, InvalidEventError, WireEvent} from "../../../src"; import {testSharedRoomEventInputs} from "../RoomEvent.test"; describe("EmoteEvent", () => { - testSharedRoomEventInputs("m.emote", x => new EmoteEvent(x), {"m.markup": [{body: "test"}]}); + testSharedRoomEventInputs("m.emote", undefined, x => new EmoteEvent(x), {"m.markup": [{body: "test"}]}); const templateEvent: WireEvent.RoomEvent = { room_id: "!test:example.org", diff --git a/test/events/m/MessageEvent.test.ts b/test/events/m/MessageEvent.test.ts index 6932304..38878a8 100644 --- a/test/events/m/MessageEvent.test.ts +++ b/test/events/m/MessageEvent.test.ts @@ -18,7 +18,7 @@ import {InvalidEventError, MessageEvent, WireEvent} from "../../../src"; import {testSharedRoomEventInputs} from "../RoomEvent.test"; describe("MessageEvent", () => { - testSharedRoomEventInputs("m.message", x => new MessageEvent(x), {"m.markup": [{body: "test"}]}); + testSharedRoomEventInputs("m.message", undefined, x => new MessageEvent(x), {"m.markup": [{body: "test"}]}); const templateEvent: WireEvent.RoomEvent = { room_id: "!test:example.org", diff --git a/test/events/m/NoticeEvent.test.ts b/test/events/m/NoticeEvent.test.ts index 951b08c..a1c2155 100644 --- a/test/events/m/NoticeEvent.test.ts +++ b/test/events/m/NoticeEvent.test.ts @@ -18,7 +18,7 @@ import {InvalidEventError, NoticeEvent, WireEvent} from "../../../src"; import {testSharedRoomEventInputs} from "../RoomEvent.test"; describe("NoticeEvent", () => { - testSharedRoomEventInputs("m.notice", x => new NoticeEvent(x), {"m.markup": [{body: "test"}]}); + testSharedRoomEventInputs("m.notice", undefined, x => new NoticeEvent(x), {"m.markup": [{body: "test"}]}); const templateEvent: WireEvent.RoomEvent = { room_id: "!test:example.org", From b0ce205c882941ce06a4a0792752abab02133e68 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Dec 2022 18:06:47 -0700 Subject: [PATCH 21/22] Update the README to match new layout; expose AjvContainer --- .ctiignore | 1 - README.md | 170 ++++++++++++++++++++++++++++++++++++++++++--------- src/index.ts | 1 + 3 files changed, 143 insertions(+), 29 deletions(-) diff --git a/.ctiignore b/.ctiignore index dbc1325..42e8f1c 100644 --- a/.ctiignore +++ b/.ctiignore @@ -1,6 +1,5 @@ { "src/events/EventParser.ts": ["addInternalKnownEventParser", "addInternalUnknownEventParser", "InternalOrderCategorization"], - "src/AjvContainer.ts": "*", "**/*.d.ts": "*", "test/**": "*" } diff --git a/README.md b/README.md index dc93b5a..77e2d2c 100644 --- a/README.md +++ b/README.md @@ -14,56 +14,170 @@ architecture is up to the task of handling proper extensible events. ## Usage: Parsing events ```typescript -const parsed = ExtensibleEvents.parse({ - type: "m.room.message", +const parser = new EventParser(); +const parsed = parser.parse({ + type: "org.matrix.msc1767.message", content: { - "msgtype": "m.text", - "body": "Hello world!" + "org.matrix.msc1767.markup": [ + { "body": "this is my message text" }, + ], }, - // and other fields -}) as MessageEvent; + // and other required fields +}); -// Using instanceof can be unsafe in some cases, but casting the -// response in TypeScript (as above) should be safe given this -// if statement will block non-message types anyhow. -if (parsed?.isEquivalentTo(M_MESSAGE)) { +if (parsed instanceof MessageEvent) { console.log(parsed.text); } ``` -*Note*: `instanceof` isn't considered safe for projects which might be running multiple copies -of the library, such as in clients which have layers needing access to the events-sdk individually. +It is recommended to cache your `EventParser` instance for performance reasons, and for ease of use +when adding custom events. -If you would like to register your own handling of events, use the following: +Registering your own events is easy, and we recommend creating your own block objects for handling the +contents of events: ```typescript -type MyContent = M_MESSAGE_EVENT_CONTENT & { - field: string; -}; +// There are a number of built-in block types for simple primitives +// BooleanBlock, IntegerBlock, StringBlock -class MyEvent extends MessageEvent { - public readonly field: string; +// For object-based blocks, the following can be used: +type MyObjectBlockWireType = { + my_property: string; // or whatever your block's properties are on the wire +}; - constructor(wireFormat: IPartialEvent) { - // Parse the text bit of the event - super(wireFormat); +class MyObjectBlock extends ObjectBlock { + public static readonly schema: Schema = { + // This is a JSON Schema + type: "object", + properties: { + my_property: { + type: "string", + nullable: false, + }, + }, + required: ["my_property"], + errorMessage: { + properties: { + my_property: "my_property should be a non-null string and is required", + }, + }, + }; + + public static readonly validateFn = AjvContainer.ajv.compile(MyObjectBlock.schema); + + public static readonly type = new UnstableValue(null, "org.example.my_custom_block"); + + public constructor(raw: MyObjectBlockWireType) { + super(MyObjectBlock.type.name, raw); + if (!MyObjectBlock.validateFn(raw)) { + throw new InvalidBlockError(this.name, MyObjectBlock.validateFn.errors); + } + } +} - this.field = wireFormat.content?.field; +// For array-based blocks, we define the contents (items) slightly differently: +type MyArrayItemWireType = { + my_property: string; // or whatever +}; // your item type can also be a primitive, like integers, booleans, and strings. + +class MyArrayBlock extends ArrayBlock { + public static readonly schema = ArrayBlock.schema; + public static readonly validateFn = ArrayBlock.validateFn; + + public static readonly itemSchema: Schema = { + // This is a JSON Schema + type: "object", + properties: { + my_property: { + type: "string", + nullable: false, + }, + }, + required: ["my_property"], + errorMessage: { + properties: { + my_property: "my_property should be a non-null string and is required", + }, + }, + }; + public static readonly itemValidateFn = AjvContainer.ajv.compile(MyArrayBlock.itemSchema); + + public static readonly type = new UnstableValue(null, "org.example.my_custom_block"); + + public constructor(raw: MyArrayItemWireType[]) { + super(MyArrayBlock.type.name, raw); + this.raw = raw.filter(x => { + const bool = MyArrayBlock.itemValidateFn(x); + if (!bool) { + // Do something with the error. It might be valid to throw, as we do here, or + // use `.filter()`'s ability to exclude items from the final array. + throw new InvalidBlockError(this.name, MyArrayBlock.itemValidateFn.errors); + } + return bool; + }); } } +``` -function parseMyEvent(wireEvent: IPartialEvent): Optional { - // If you need to convert a legacy format, this is where you'd do it. Your - // event class should be able to be instatiated outside of this parse function. - return new MyEvent(wireEvent); +Then, we can define a custom event: + +```typescript +type MyWireContent = EitherAnd< + { [MyObjectBlock.type.name]: MyObjectBlockWireType }, + { [MyObjectBlock.type.altName]: MyObjectBlockWireType } +>; + +class MyCustomEvent extends RoomEvent { + public static readonly contentSchema: Schema = AjvContainer.eitherAnd(MyObjectBlock.type, MyObjectBlock.schema); + public static readonly contentValidateFn = AjvContainer.ajv.compile(MyCustomEvent.contentSchema); + + public static readonly type = new UnstableValue(null, "org.example.my_custom_event"); + + public constructor(raw: WireEvent.RoomEvent) { + super(MyCustomEvent.type.name, raw, false); // see docs + if (!MyCustomEvent.contentValidateFn(this.content)) { + throw new InvalidEventError(this.name, MyCustomEvent.contentValidateFn.errors); + } + } } +``` + +and finally we can register it in a parser instance: -ExtensibleEvents.registerInterpreter("org.example.my_event_type", parseMyEvent); -ExtensibleEvents.unknownInterpretOrder.push("org.example.my_event_type"); +```typescript +const parser = new EventParser(); +parser.addKnownType(MyCustomEvent.type, x => new MyCustomEvent(x)); ``` +If you'd also like to register an "unknown event type" handler, that can be done like so: + +```typescript +const myParser: UnknownEventParser = x => { + const possibleBlock = MyObjectBlock.type.findIn(x.content); + if (!!possibleBlock) { + const block = new MyObjectBlock(possibleBlock as MyObjectBlockWireType); + return new MyCustomEvent({ + ...x, + type: MyCustomEvent.type.name, // required - override the event type + content: { + [MyObjectBlock.name]: block.raw, + }, // technically optional, but good practice: clean up the event's content for handling. + }); + } + return undefined; // else, we don't care about it +}; +parser.setUnknownParsers([myParser, ...parser.defaultUnknownEventParsers]); +``` + +Putting your parser at the start of the array will ensure it gets called first. Including the default parsers +is also optional, though recommended. + ## Usage: Making events + +***TODO: This needs refactoring*** + + Most event objects have a `from` static function which takes common details of an event and returns an instance of that event for later serialization. diff --git a/src/index.ts b/src/index.ts index 6472995..0c07879 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ export {type ParsedEventFactory, type UnknownEventParser, EventParser} from "./e export * from "./events/InvalidEventError"; export * from "./events/RoomEvent"; export * from "./events/types_wire"; +export * from "./AjvContainer"; export * from "./LazyValue"; export * from "./NamespacedValue"; export * from "./types"; From f5502a646bf7a4230daf930f52d17afbc1aa5268 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Dec 2022 18:18:11 -0700 Subject: [PATCH 22/22] De-flag the AjvContainer in docs --- src/AjvContainer.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/AjvContainer.ts b/src/AjvContainer.ts index a7c59e2..a1abb4a 100644 --- a/src/AjvContainer.ts +++ b/src/AjvContainer.ts @@ -19,9 +19,7 @@ import AjvErrors from "ajv-errors"; import {NamespacedValue} from "./NamespacedValue"; /** - * Container for the ajv instance. - * @internal - * @protected + * Container for the ajv instance, the SDK's schema validator of choice. */ export class AjvContainer { public static readonly ajv = new Ajv({