diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index 0d348caef..55c513a3a 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -50,6 +50,7 @@ import { ClientEvent, createClient, CryptoEvent, + HistoryVisibility, IClaimOTKsResult, IContent, IDownloadKeyResult, @@ -59,11 +60,11 @@ import { MatrixClient, MatrixEvent, MatrixEventEvent, + MsgType, PendingEventOrdering, Room, RoomMember, RoomStateEvent, - HistoryVisibility, } from "../../../src/matrix"; import { DeviceInfo } from "../../../src/crypto/deviceinfo"; import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver"; @@ -1925,7 +1926,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expectAliceKeyQuery({ device_keys: { "@other:user": {} }, failures: {} }); aliceClient.on(RoomStateEvent.NewMember, (_e, _s, member: RoomMember) => { if (member.userId == "@other:user") { - aliceClient.sendMessage(testRoomId, { msgtype: "m.text", body: "Hello, World" }); + aliceClient.sendMessage(testRoomId, { msgtype: MsgType.Text, body: "Hello, World" }); } }); diff --git a/spec/integ/crypto/olm-encryption-spec.ts b/spec/integ/crypto/olm-encryption-spec.ts index ff79bf28b..5977bda74 100644 --- a/spec/integ/crypto/olm-encryption-spec.ts +++ b/spec/integ/crypto/olm-encryption-spec.ts @@ -34,7 +34,7 @@ import { logger } from "../../../src/logger"; import * as testUtils from "../../test-utils/test-utils"; import { TestClient } from "../../TestClient"; import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../../src/client"; -import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../../src/matrix"; +import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent, MsgType } from "../../../src/matrix"; import { DeviceInfo } from "../../../src/crypto/deviceinfo"; import { KnownMembership } from "../../../src/@types/membership"; @@ -217,7 +217,7 @@ async function expectBobSendMessageRequest(): Promise { } function sendMessage(client: MatrixClient): Promise { - return client.sendMessage(roomId, { msgtype: "m.text", body: "Hello, World" }); + return client.sendMessage(roomId, { msgtype: MsgType.Text, body: "Hello, World" }); } async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise { diff --git a/spec/integ/matrix-client-retrying.spec.ts b/spec/integ/matrix-client-retrying.spec.ts index d62e968a0..b78e3c7b4 100644 --- a/spec/integ/matrix-client-retrying.spec.ts +++ b/spec/integ/matrix-client-retrying.spec.ts @@ -16,7 +16,7 @@ limitations under the License. import HttpBackend from "matrix-mock-request"; -import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix"; +import { EventStatus, MatrixClient, MatrixScheduler, MsgType, RoomEvent } from "../../src/matrix"; import { Room } from "../../src/models/room"; import { TestClient } from "../TestClient"; @@ -60,7 +60,7 @@ describe("MatrixClient retrying", function () { // send a couple of events; the second will be queued const p1 = client! .sendMessage(roomId, { - msgtype: "m.text", + msgtype: MsgType.Text, body: "m1", }) .then( @@ -77,7 +77,7 @@ describe("MatrixClient retrying", function () { // never gets resolved. // https://github.com/matrix-org/matrix-js-sdk/issues/496 client!.sendMessage(roomId, { - msgtype: "m.text", + msgtype: MsgType.Text, body: "m2", }); diff --git a/spec/integ/matrix-client-unread-notifications.spec.ts b/spec/integ/matrix-client-unread-notifications.spec.ts index 7b35cbbf5..8518d0864 100644 --- a/spec/integ/matrix-client-unread-notifications.spec.ts +++ b/spec/integ/matrix-client-unread-notifications.spec.ts @@ -152,7 +152,7 @@ describe("MatrixClient syncing", () => { await client!.sendEvent(roomId, EventType.Reaction, { "m.relates_to": { rel_type: RelationType.Annotation, - event_id: threadReply.getId(), + event_id: threadReply.getId()!, key: "", }, }); diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts index b42acfff1..d9ac5a62f 100644 --- a/spec/unit/embedded.spec.ts +++ b/spec/unit/embedded.spec.ts @@ -81,6 +81,12 @@ declare module "../../src/types" { hello: string; }; } + + interface TimelineEvents { + "org.matrix.rageshake_request": { + request_id: number; + }; + } } describe("RoomWidgetClient", () => { diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 5f0cddda3..62b479f89 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -22,6 +22,7 @@ import { Filter } from "../../src/filter"; import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace"; import { EventType, + MsgType, RelationType, RoomCreateTypeField, RoomType, @@ -73,6 +74,7 @@ import { StubStore } from "../../src/store/stub"; import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from "../../src/secret-storage"; import { CryptoBackend } from "../../src/common-crypto/CryptoBackend"; import { KnownMembership } from "../../src/@types/membership"; +import { RoomMessageEventContent } from "../../src/@types/events"; jest.useFakeTimers(); @@ -567,7 +569,7 @@ describe("MatrixClient", function () { describe("sendEvent", () => { const roomId = "!room:example.org"; const body = "This is the body"; - const content = { body }; + const content = { body, msgtype: MsgType.Text } satisfies RoomMessageEventContent; it("overload without threadId works", async () => { const eventId = "$eventId:example.org"; @@ -662,12 +664,13 @@ describe("MatrixClient", function () { const content = { body, + "msgtype": MsgType.Text, "m.relates_to": { "m.in_reply_to": { event_id: "$other:event", }, }, - }; + } satisfies RoomMessageEventContent; const room = new Room(roomId, client, userId); mocked(store.getRoom).mockReturnValue(room); diff --git a/spec/unit/models/MSC3089TreeSpace.spec.ts b/spec/unit/models/MSC3089TreeSpace.spec.ts index 8f000d844..5a63c282c 100644 --- a/spec/unit/models/MSC3089TreeSpace.spec.ts +++ b/spec/unit/models/MSC3089TreeSpace.spec.ts @@ -26,6 +26,7 @@ import { import { DEFAULT_ALPHABET } from "../../../src/utils"; import { MatrixError } from "../../../src/http-api"; import { KnownMembership } from "../../../src/@types/membership"; +import { EncryptedFile } from "../../../src/@types/media"; describe("MSC3089TreeSpace", () => { let client: MatrixClient; @@ -947,7 +948,7 @@ describe("MSC3089TreeSpace", () => { const fileInfo = { mimetype: "text/plain", // other fields as required by encryption, but ignored here - }; + } as unknown as EncryptedFile; const fileEventId = "$file"; const fileName = "My File.txt"; const fileContents = "This is a test file"; @@ -1007,7 +1008,7 @@ describe("MSC3089TreeSpace", () => { const fileInfo = { mimetype: "text/plain", // other fields as required by encryption, but ignored here - }; + } as unknown as EncryptedFile; const fileEventId = "$file"; const fileName = "My File.txt"; const fileContents = "This is a test file"; diff --git a/src/@types/event.ts b/src/@types/event.ts index 6f11720a2..0a144a352 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -41,8 +41,23 @@ import { IGroupCallRoomState, } from "../webrtc/groupCall"; import { MSC3089EventContent } from "../models/MSC3089Branch"; -import { M_BEACON_INFO, MBeaconInfoEventContent } from "./beacon"; +import { M_BEACON, M_BEACON_INFO, MBeaconEventContent, MBeaconInfoEventContent } from "./beacon"; import { XOR } from "./common"; +import { ReactionEventContent, RoomMessageEventContent, StickerEventContent } from "./events"; +import { + MCallAnswer, + MCallBase, + MCallCandidates, + MCallHangupReject, + MCallInviteNegotiate, + MCallReplacesEvent, + MCallSelectAnswer, + SDPStreamMetadata, + SDPStreamMetadataKey, +} from "../webrtc/callEventTypes"; +import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types"; +import { EncryptedFile } from "./media"; +import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls"; export enum EventType { // Room state events @@ -283,21 +298,37 @@ export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue( */ export const UNSIGNED_THREAD_ID_FIELD = new UnstableValue("thread_id", "org.matrix.msc4023.thread_id"); -export interface IEncryptedFile { - url: string; - mimetype?: string; - key: { - alg: string; - key_ops: string[]; // eslint-disable-line camelcase - kty: string; - k: string; - ext: boolean; - }; - iv: string; - hashes: { [alg: string]: string }; - v: string; +/** + * @deprecated in favour of {@link EncryptedFile} + */ +export type IEncryptedFile = EncryptedFile; + +/** + * Mapped type from event type to content type for all specified non-state room events. + */ +export interface TimelineEvents { + [EventType.RoomMessage]: RoomMessageEventContent; + [EventType.Sticker]: StickerEventContent; + [EventType.Reaction]: ReactionEventContent; + [EventType.CallReplaces]: MCallReplacesEvent; + [EventType.CallAnswer]: MCallAnswer; + [EventType.CallSelectAnswer]: MCallSelectAnswer; + [EventType.CallNegotiate]: Omit; + [EventType.CallInvite]: MCallInviteNegotiate; + [EventType.CallCandidates]: MCallCandidates; + [EventType.CallHangup]: MCallHangupReject; + [EventType.CallReject]: MCallHangupReject; + [EventType.CallSDPStreamMetadataChangedPrefix]: MCallBase & { [SDPStreamMetadataKey]: SDPStreamMetadata }; + [EventType.CallEncryptionKeysPrefix]: EncryptionKeysEventContent; + [EventType.CallNotify]: ICallNotifyContent; + [M_BEACON.name]: MBeaconEventContent; + [M_POLL_START.name]: PollStartEventContent; + [M_POLL_END.name]: PollEndEventContent; } +/** + * Mapped type from event type to content type for all specified room state events. + */ export interface StateEvents { [EventType.RoomCanonicalAlias]: RoomCanonicalAliasEventContent; [EventType.RoomCreate]: RoomCreateEventContent; diff --git a/src/@types/events.ts b/src/@types/events.ts new file mode 100644 index 000000000..59c41d201 --- /dev/null +++ b/src/@types/events.ts @@ -0,0 +1,119 @@ +/* +Copyright 2024 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 { MsgType, RelationType } from "./event"; +import { FileInfo, ImageInfo, MediaEventContent } from "./media"; +import { XOR } from "./common"; + +interface BaseTimelineEvent { + "body": string; + "m.mentions"?: { + user_ids?: string[]; + room?: boolean; + }; +} + +interface ReplyEvent { + "m.relates_to"?: { + "m.in_reply_to"?: { + event_id: string; + }; + }; +} + +interface NoRelationEvent { + "m.new_content"?: never; + "m.relates_to"?: never; +} + +/** + * Partial content format of timeline events with rel_type `m.replace` + * + * @see https://spec.matrix.org/v1.9/client-server-api/#event-replacements + */ +export interface ReplacementEvent { + "m.new_content": T; + "m.relates_to": { + event_id: string; + rel_type: RelationType.Replace; + }; +} + +/** + * Partial content format of timeline events with rel_type other than `m.replace` + * + * @see https://spec.matrix.org/v1.9/client-server-api/#forming-relationships-between-events + */ +export interface RelationEvent { + "m.new_content"?: never; + "m.relates_to": { + event_id: string; + rel_type: Exclude; + }; +} + +/** + * Content format of timeline events with type `m.room.message` and `msgtype` `m.text`, `m.emote`, or `m.notice` + * + * @see https://spec.matrix.org/v1.9/client-server-api/#mroommessage + */ +export interface RoomMessageTextEventContent extends BaseTimelineEvent { + msgtype: MsgType.Text | MsgType.Emote | MsgType.Notice; + format?: "org.matrix.custom.html"; + formatted_body?: string; +} + +/** + * Content format of timeline events with type `m.room.message` and `msgtype` `m.location` + * + * @see https://spec.matrix.org/v1.9/client-server-api/#mlocation + */ +export interface RoomMessageLocationEventContent extends BaseTimelineEvent { + body: string; + geo_uri: string; + info: Pick; + msgtype: MsgType.Location; +} + +type MessageEventContent = RoomMessageTextEventContent | RoomMessageLocationEventContent | MediaEventContent; + +export type RoomMessageEventContent = BaseTimelineEvent & + XOR, RelationEvent>, XOR> & + MessageEventContent; + +/** + * Content format of timeline events with type `m.sticker` + * + * @see https://spec.matrix.org/v1.9/client-server-api/#msticker + */ +export interface StickerEventContent extends BaseTimelineEvent { + body: string; + info: ImageInfo; + url: string; +} + +/** + * Content format of timeline events with type `m.reaction` + * + * @see https://spec.matrix.org/v1.9/client-server-api/#mreaction + */ +export interface ReactionEventContent { + "m.relates_to": { + event_id: string; + key: string; + rel_type: RelationType.Annotation; + }; +} diff --git a/src/client.ts b/src/client.ts index 8bf277071..6de5cd5d7 100644 --- a/src/client.ts +++ b/src/client.ts @@ -145,6 +145,7 @@ import { RoomCreateTypeField, RoomType, StateEvents, + TimelineEvents, UNSTABLE_MSC3088_ENABLED, UNSTABLE_MSC3088_PURPOSE, UNSTABLE_MSC3089_TREE_SUBTYPE, @@ -223,6 +224,7 @@ import { RegisterRequest, RegisterResponse } from "./@types/registration"; import { MatrixRTCSessionManager } from "./matrixrtc/MatrixRTCSessionManager"; import { getRelationsThreadFilter } from "./thread-utils"; import { KnownMembership, Membership } from "./@types/membership"; +import { RoomMessageEventContent, StickerEventContent } from "./@types/events"; import { ImageInfo } from "./@types/media"; export type Store = IStore; @@ -4551,12 +4553,17 @@ export class MatrixClient extends TypedEventEmitter; - public sendEvent( + public sendEvent( + roomId: string, + eventType: K, + content: TimelineEvents[K], + txnId?: string, + ): Promise; + public sendEvent( roomId: string, threadId: string | null, - eventType: string, - content: IContent, + eventType: K, + content: TimelineEvents[K], txnId?: string, ): Promise; public sendEvent( @@ -4943,27 +4950,27 @@ export class MatrixClient extends TypedEventEmitter; + public sendMessage(roomId: string, content: RoomMessageEventContent, txnId?: string): Promise; public sendMessage( roomId: string, threadId: string | null, - content: IContent, + content: RoomMessageEventContent, txnId?: string, ): Promise; public sendMessage( roomId: string, - threadId: string | null | IContent, - content?: IContent | string, + threadId: string | null | RoomMessageEventContent, + content?: RoomMessageEventContent | string, txnId?: string, ): Promise { if (typeof threadId !== "string" && threadId !== null) { txnId = content as string; - content = threadId as IContent; + content = threadId as RoomMessageEventContent; threadId = null; } - const eventType: string = EventType.RoomMessage; - const sendContent: IContent = content as IContent; + const eventType = EventType.RoomMessage; + const sendContent = content as RoomMessageEventContent; return this.sendEvent(roomId, threadId as string | null, eventType, sendContent, txnId); } @@ -5076,10 +5083,10 @@ export class MatrixClient extends TypedEventEmitter { + public async getFileInfo(): Promise<{ info: EncryptedFile; httpUrl: string }> { const event = await this.getFileEvent(); const file = event.getOriginalContent()["file"]; @@ -186,7 +194,7 @@ export class MSC3089Branch { public async createNewVersion( name: string, encryptedContents: FileType, - info: Partial, + info: EncryptedFile, additionalContent?: IContent, ): Promise { const fileEventResponse = await this.directory.createFile(name, encryptedContents, info, { diff --git a/src/models/MSC3089TreeSpace.ts b/src/models/MSC3089TreeSpace.ts index 3890afeb7..76aae6558 100644 --- a/src/models/MSC3089TreeSpace.ts +++ b/src/models/MSC3089TreeSpace.ts @@ -17,7 +17,7 @@ limitations under the License. import promiseRetry from "p-retry"; import { MatrixClient } from "../client"; -import { EventType, IEncryptedFile, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../@types/event"; +import { EventType, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../@types/event"; import { Room } from "./room"; import { logger } from "../logger"; import { IContent, MatrixEvent } from "./event"; @@ -35,6 +35,7 @@ import { ISendEventResponse } from "../@types/requests"; import { FileType } from "../http-api"; import { KnownMembership } from "../@types/membership"; import { RoomPowerLevelsEventContent, SpaceChildEventContent } from "../@types/state_events"; +import { EncryptedFile, FileContent } from "../@types/media"; /** * The recommended defaults for a tree space's power levels. Note that this @@ -79,6 +80,12 @@ export enum TreePermissions { Owner = "owner", // "Admin" or PL100 } +declare module "../@types/media" { + interface FileContent { + [UNSTABLE_MSC3089_LEAF.name]?: {}; + } +} + /** * Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) * file tree Space. Note that this is UNSTABLE and subject to breaking changes @@ -502,7 +509,7 @@ export class MSC3089TreeSpace { public async createFile( name: string, encryptedContents: FileType, - info: Partial, + info: EncryptedFile, additionalContent?: IContent, ): Promise { const { content_uri: mxc } = await this.client.uploadContent(encryptedContents, { @@ -510,7 +517,7 @@ export class MSC3089TreeSpace { }); info.url = mxc; - const fileContent = { + const fileContent: FileContent = { msgtype: MsgType.File, body: name, url: mxc, @@ -529,7 +536,7 @@ export class MSC3089TreeSpace { ...additionalContent, ...fileContent, [UNSTABLE_MSC3089_LEAF.name]: {}, - }); + } as FileContent); await this.client.sendStateEvent( this.roomId, diff --git a/src/types.ts b/src/types.ts index adfd4e5b3..c1469b170 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,6 +24,7 @@ limitations under the License. export type * from "./@types/media"; export * from "./@types/membership"; export type * from "./@types/event"; +export type * from "./@types/events"; export type * from "./@types/state_events"; /** The different methods for device and user verification */ diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 92b4862de..45d35f60c 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -26,8 +26,8 @@ import { parse as parseSdp, write as writeSdp } from "sdp-transform"; import { logger } from "../logger"; import { checkObjectHasKeys, isNullOrUndefined, recursivelyAssign } from "../utils"; -import { IContent, MatrixEvent } from "../models/event"; -import { EventType, ToDeviceMessageId } from "../@types/event"; +import { MatrixEvent } from "../models/event"; +import { EventType, TimelineEvents, ToDeviceMessageId } from "../@types/event"; import { RoomMember } from "../models/room-member"; import { randomString } from "../randomstring"; import { @@ -293,13 +293,24 @@ function getCodecParamMods(isPtt: boolean): CodecParamsMod[] { return mods; } +type CallEventType = + | EventType.CallReplaces + | EventType.CallAnswer + | EventType.CallSelectAnswer + | EventType.CallNegotiate + | EventType.CallInvite + | EventType.CallCandidates + | EventType.CallHangup + | EventType.CallReject + | EventType.CallSDPStreamMetadataChangedPrefix; + export interface VoipEvent { type: "toDevice" | "sendEvent"; eventType: string; userId?: string; opponentDeviceId?: string; roomId?: string; - content: Record; + content: TimelineEvents[CallEventType]; } /** @@ -406,7 +417,7 @@ export class MatrixCall extends TypedEventEmitter(); + private remoteCandidateBuffer = new Map(); private remoteAssertedIdentity?: AssertedIdentity; private remoteSDPStreamMetadata?: SDPStreamMetadata; @@ -1156,7 +1167,7 @@ export class MatrixCall extends TypedEventEmitter = {}; // Don't send UserHangup reason to older clients if ((this.opponentVersion && this.opponentVersion !== 0) || reason !== CallErrorCode.UserHangup) { content["reason"] = reason; @@ -1916,7 +1927,7 @@ export class MatrixCall extends TypedEventEmitter { - const realContent = Object.assign({}, content, { + private async sendVoipEvent>( + eventType: K, + content: Omit, + ): Promise { + const realContent = { + ...content, version: VOIP_PROTO_VERSION, call_id: this.callId, party_id: this.ourPartyId, conf_id: this.groupCallId, - }); + } as TimelineEvents[K]; if (this.opponentDeviceId) { const toDeviceSeq = this.toDeviceSeq++; @@ -2729,7 +2745,9 @@ export class MatrixCall extends TypedEventEmitter candidate.toJSON()) }; + const content: Pick = { + candidates: candidates.map((candidate) => candidate.toJSON()), + }; if (this.candidatesEnded) { // If there are no more candidates, signal this by adding an empty string candidate content.candidates.push({ @@ -2923,7 +2941,7 @@ export class MatrixCall extends TypedEventEmitter { + private async addIceCandidates(candidates: RTCIceCandidate[] | MCallCandidates["candidates"]): Promise { for (const candidate of candidates) { if ( (candidate.sdpMid === null || candidate.sdpMid === undefined) && diff --git a/src/webrtc/callEventTypes.ts b/src/webrtc/callEventTypes.ts index f06ed5b0d..0be2b2d4d 100644 --- a/src/webrtc/callEventTypes.ts +++ b/src/webrtc/callEventTypes.ts @@ -34,6 +34,7 @@ export interface CallReplacesTarget { export interface MCallBase { call_id: string; + conf_id?: string; version: string | number; party_id?: string; sender_session_id?: string; @@ -82,7 +83,7 @@ export interface MCAllAssertedIdentity extends MCallBase { } export interface MCallCandidates extends MCallBase { - candidates: RTCIceCandidate[]; + candidates: Omit[]; } export interface MCallHangupReject extends MCallBase {