diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts index e1e6f0513a1..b42acfff1df 100644 --- a/spec/unit/embedded.spec.ts +++ b/spec/unit/embedded.spec.ts @@ -75,6 +75,14 @@ class MockWidgetApi extends EventEmitter { public transport = { reply: jest.fn() }; } +declare module "../../src/types" { + interface StateEvents { + "org.example.foo": { + hello: string; + }; + } +} + describe("RoomWidgetClient", () => { let widgetApi: MockedObject; let client: MatrixClient; diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index f80531f86bb..c87ab3177f3 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -38,30 +38,31 @@ import * as testUtils from "../test-utils/test-utils"; import { makeBeaconInfoContent } from "../../src/content-helpers"; import { M_BEACON_INFO } from "../../src/@types/beacon"; import { - ContentHelpers, ClientPrefix, + ConditionKind, + ContentHelpers, Direction, EventTimeline, + EventTimelineSet, + getHttpUriForMxc, ICreateRoomOpts, + IPushRule, IRequestOpts, MatrixError, MatrixHttpApi, MatrixScheduler, Method, - Room, - EventTimelineSet, PushRuleActionName, - TweakName, + Room, RuleId, - IPushRule, - ConditionKind, - getHttpUriForMxc, + TweakName, } from "../../src"; import { supportsMatrixCall } from "../../src/webrtc/call"; import { makeBeaconEvent } from "../test-utils/beacon"; import { IGNORE_INVITES_ACCOUNT_EVENT_KEY, POLICIES_ACCOUNT_EVENT_TYPE, + PolicyRecommendation, PolicyScope, } from "../../src/models/invites-ignorer"; import { IOlmDevice } from "../../src/crypto/algorithms/megolm"; @@ -2082,10 +2083,10 @@ describe("MatrixClient", function () { await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID); // Add a rule in the new source room. - await client.sendStateEvent(NEW_SOURCE_ROOM_ID, PolicyScope.User, { + await client.sendStateEvent(NEW_SOURCE_ROOM_ID, EventType.PolicyRuleUser, { entity: "*:example.org", reason: "just a test", - recommendation: "m.ban", + recommendation: PolicyRecommendation.Ban, }); // We should reject this invite. @@ -2172,8 +2173,8 @@ describe("MatrixClient", function () { // Check where it shows up. const targetRoomId = ignoreInvites2.target; const targetRoom = client.getRoom(targetRoomId) as WrappedRoom; - expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy(); - expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy(); + expect(targetRoom._state.get(EventType.PolicyRuleUser)[eventId]).toBeTruthy(); + expect(newSourceRoom._state.get(EventType.PolicyRuleUser)?.[eventId]).toBeFalsy(); }); }); diff --git a/src/@types/common.ts b/src/@types/common.ts index 77b856faf93..fc8e7af7328 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -15,3 +15,8 @@ limitations under the License. */ export type NonEmptyArray = [T, ...T[]]; + +// Based on https://stackoverflow.com/a/53229857/3532235 +export type Without = { [P in Exclude]?: never }; +export type XOR = T | U extends object ? (Without & U) | (Without & T) : T | U; +export type Writeable = { -readonly [P in keyof T]: T[P] }; diff --git a/src/@types/event.ts b/src/@types/event.ts index caa87f9f82e..a6dafcfa1b9 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -15,6 +15,34 @@ limitations under the License. */ import { UnstableValue } from "../NamespacedValue"; +import { + PolicyRuleEventContent, + RoomAvatarEventContent, + RoomCanonicalAliasEventContent, + RoomCreateEventContent, + RoomEncryptionEventContent, + RoomGuestAccessEventContent, + RoomHistoryVisibilityEventContent, + RoomJoinRulesEventContent, + RoomMemberEventContent, + RoomNameEventContent, + RoomPinnedEventsEventContent, + RoomPowerLevelsEventContent, + RoomServerAclEventContent, + RoomThirdPartyInviteEventContent, + RoomTombstoneEventContent, + RoomTopicEventContent, + SpaceChildEventContent, + SpaceParentEventContent, +} from "./state_events"; +import { + ExperimentalGroupCallRoomMemberState, + IGroupCallRoomMemberState, + IGroupCallRoomState, +} from "../webrtc/groupCall"; +import { MSC3089EventContent } from "../models/MSC3089Branch"; +import { M_BEACON_INFO, MBeaconInfoEventContent } from "./beacon"; +import { XOR } from "./common"; export enum EventType { // Room state events @@ -35,6 +63,11 @@ export enum EventType { RoomTombstone = "m.room.tombstone", RoomPredecessor = "org.matrix.msc3946.room_predecessor", + // Moderation policy lists + PolicyRuleUser = "m.policy.rule.user", + PolicyRuleRoom = "m.policy.rule.room", + PolicyRuleServer = "m.policy.rule.server", + SpaceChild = "m.space.child", SpaceParent = "m.space.parent", @@ -260,3 +293,38 @@ export interface IEncryptedFile { hashes: { [alg: string]: string }; v: string; } + +export interface StateEvents { + [EventType.RoomCanonicalAlias]: RoomCanonicalAliasEventContent; + [EventType.RoomCreate]: RoomCreateEventContent; + [EventType.RoomJoinRules]: RoomJoinRulesEventContent; + [EventType.RoomMember]: RoomMemberEventContent; + // XXX: Spec says this event has 3 required fields but kicking such an invitation requires sending `{}` + [EventType.RoomThirdPartyInvite]: XOR; + [EventType.RoomPowerLevels]: RoomPowerLevelsEventContent; + [EventType.RoomName]: RoomNameEventContent; + [EventType.RoomTopic]: RoomTopicEventContent; + [EventType.RoomAvatar]: RoomAvatarEventContent; + [EventType.RoomPinnedEvents]: RoomPinnedEventsEventContent; + [EventType.RoomEncryption]: RoomEncryptionEventContent; + [EventType.RoomHistoryVisibility]: RoomHistoryVisibilityEventContent; + [EventType.RoomGuestAccess]: RoomGuestAccessEventContent; + [EventType.RoomServerAcl]: RoomServerAclEventContent; + [EventType.RoomTombstone]: RoomTombstoneEventContent; + [EventType.SpaceChild]: SpaceChildEventContent; + [EventType.SpaceParent]: SpaceParentEventContent; + + [EventType.PolicyRuleUser]: XOR; + [EventType.PolicyRuleRoom]: XOR; + [EventType.PolicyRuleServer]: XOR; + + // MSC3401 + [EventType.GroupCallPrefix]: IGroupCallRoomState; + [EventType.GroupCallMemberPrefix]: XOR; + + // MSC3089 + [UNSTABLE_MSC3089_BRANCH.name]: MSC3089EventContent; + + // MSC3672 + [M_BEACON_INFO.name]: MBeaconInfoEventContent; +} diff --git a/src/@types/partials.ts b/src/@types/partials.ts index 49f92f32759..6aaea79963e 100644 --- a/src/@types/partials.ts +++ b/src/@types/partials.ts @@ -14,19 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -export interface IImageInfo { - size?: number; - mimetype?: string; - thumbnail_info?: { - // eslint-disable-line camelcase - w?: number; - h?: number; - size?: number; - mimetype?: string; - }; - w?: number; - h?: number; -} +import { ImageInfo } from "./media"; + +/** + * @deprecated use {@link ImageInfo} instead. + */ +export type IImageInfo = ImageInfo; export enum Visibility { Public = "public", diff --git a/src/@types/state_events.ts b/src/@types/state_events.ts new file mode 100644 index 00000000000..0e077a9e47f --- /dev/null +++ b/src/@types/state_events.ts @@ -0,0 +1,144 @@ +/* +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 { RoomType } from "./event"; +import { GuestAccess, HistoryVisibility, RestrictedAllowType } from "./partials"; +import { ImageInfo } from "./media"; +import { PolicyRecommendation } from "../models/invites-ignorer"; + +export interface RoomCanonicalAliasEventContent { + alias?: string; + alt_aliases?: string[]; +} + +export interface RoomCreateEventContent { + "creator"?: string; + "m.federate"?: boolean; + "predecessor"?: { + event_id: string; + room_id: string; + }; + "room_version"?: string; + "type"?: RoomType; +} + +export interface RoomJoinRulesEventContent { + allow?: { + room_id: string; + type: RestrictedAllowType; + }[]; +} + +export interface RoomMemberEventContent { + avatar_url?: string; + displayname?: string; + is_direct?: boolean; + join_authorised_via_users_server?: string; + membership: "invite" | "join" | "knock" | "leave" | "ban"; + reason?: string; + third_party_invite?: { + display_name: string; + signed: { + mxid: string; + token: string; + ts: number; + }; + }; +} + +export interface RoomThirdPartyInviteEventContent { + display_name: string; + key_validity_url: string; + public_key: string; + public_keys: { + key_validity_url?: string; + public_key: string; + }[]; +} + +export interface RoomPowerLevelsEventContent { + ban?: number; + events?: { [eventType: string]: number }; + events_default?: number; + invite?: number; + kick?: number; + notifications?: { + room?: number; + }; + redact?: number; + state_default?: number; + users?: { [userId: string]: number }; + users_default?: number; +} + +export interface RoomNameEventContent { + name: string; +} + +export interface RoomTopicEventContent { + topic: string; +} + +export interface RoomAvatarEventContent { + url?: string; + info?: ImageInfo; +} + +export interface RoomPinnedEventsEventContent { + pinned: string[]; +} + +export interface RoomEncryptionEventContent { + algorithm: "m.megolm.v1.aes-sha2"; + rotation_period_ms?: number; + rotation_period_msgs?: number; +} + +export interface RoomHistoryVisibilityEventContent { + history_visibility: HistoryVisibility; +} + +export interface RoomGuestAccessEventContent { + guest_access: GuestAccess; +} + +export interface RoomServerAclEventContent { + allow?: string[]; + allow_ip_literals?: boolean; + deny?: string[]; +} + +export interface RoomTombstoneEventContent { + body: string; + replacement_room: string; +} + +export interface SpaceChildEventContent { + order?: string; + suggested?: boolean; + via?: string[]; +} + +export interface SpaceParentEventContent { + canonical?: boolean; + via?: string[]; +} + +export interface PolicyRuleEventContent { + entity: string; + reason: string; + recommendation: PolicyRecommendation; +} diff --git a/src/client.ts b/src/client.ts index 56ec1030cd6..0233749ae8c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -35,14 +35,14 @@ import { import { StubStore } from "./store/stub"; import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call"; import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter"; -import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from "./webrtc/callEventHandler"; +import { CallEventHandler, CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "./webrtc/callEventHandler"; import { GroupCallEventHandler, GroupCallEventHandlerEvent, GroupCallEventHandlerEventHandlerMap, } from "./webrtc/groupCallEventHandler"; import * as utils from "./utils"; -import { replaceParam, QueryDict, sleep, noUnsafeEventProps, safeSet } from "./utils"; +import { noUnsafeEventProps, QueryDict, replaceParam, safeSet, sleep } from "./utils"; import { Direction, EventTimeline } from "./models/event-timeline"; import { IActionsObject, PushProcessor } from "./pushprocessor"; import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery"; @@ -64,12 +64,12 @@ import { IdentityPrefix, IHttpOpts, IRequestOpts, - TokenRefreshFunction, MatrixError, MatrixHttpApi, MediaPrefix, Method, retryNetworkOperation, + TokenRefreshFunction, Upload, UploadOpts, UploadResponse, @@ -145,11 +145,20 @@ import { RelationType, RoomCreateTypeField, RoomType, + StateEvents, UNSTABLE_MSC3088_ENABLED, UNSTABLE_MSC3088_PURPOSE, UNSTABLE_MSC3089_TREE_SUBTYPE, } from "./@types/event"; -import { IdServerUnbindResult, IImageInfo, JoinRule, Preset, Visibility } from "./@types/partials"; +import { + GuestAccess, + HistoryVisibility, + IdServerUnbindResult, + IImageInfo, + JoinRule, + Preset, + Visibility, +} from "./@types/partials"; import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper"; import { randomString } from "./randomstring"; import { BackupManager, IKeyBackup, IKeyBackupCheck, IPreparedKeyBackupVersion, TrustInfo } from "./crypto/backup"; @@ -6704,7 +6713,7 @@ export class MatrixClient extends TypedEventEmitter( roomId: string, - eventType: string, - content: IContent, + eventType: K, + content: StateEvents[K], stateKey = "", opts: IRequestOpts = {}, ): Promise { @@ -8406,7 +8420,7 @@ export class MatrixClient extends TypedEventEmitter() || {}; const viewLevel = pls["users_default"] || 0; const editLevel = pls["events_default"] || 50; const adminLevel = pls["events"]?.[EventType.RoomPowerLevels] || 100; @@ -234,7 +235,7 @@ export class MSC3089TreeSpace { this.roomId, EventType.SpaceChild, { - via: [this.client.getDomain()], + via: [this.client.getDomain()!], }, directory.roomId, ); @@ -243,7 +244,7 @@ export class MSC3089TreeSpace { directory.roomId, EventType.SpaceParent, { - via: [this.client.getDomain()], + via: [this.client.getDomain()!], }, this.roomId, ); @@ -450,7 +451,9 @@ export class MSC3089TreeSpace { // XXX: We should be creating gaps to avoid conflicts lastOrder = lastOrder ? nextString(lastOrder) : DEFAULT_ALPHABET[0]; const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, target.roomId); - const content = currentChild?.getContent() ?? { via: [this.client.getDomain()] }; + const content = currentChild?.getContent() ?? { + via: [this.client.getDomain()!], + }; await this.client.sendStateEvent( parentRoom.roomId, EventType.SpaceChild, @@ -473,7 +476,7 @@ export class MSC3089TreeSpace { // Now we can finally update our own order state const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, this.roomId); - const content = currentChild?.getContent() ?? { via: [this.client.getDomain()] }; + const content = currentChild?.getContent() ?? { via: [this.client.getDomain()!] }; await this.client.sendStateEvent( parentRoom.roomId, EventType.SpaceChild, diff --git a/src/models/invites-ignorer.ts b/src/models/invites-ignorer.ts index 026c4a2bbad..a76d6a83a57 100644 --- a/src/models/invites-ignorer.ts +++ b/src/models/invites-ignorer.ts @@ -22,6 +22,7 @@ import { EventTimeline } from "./event-timeline"; import { Preset } from "../@types/partials"; import { globToRegexp } from "../utils"; import { Room } from "./room"; +import { EventType, StateEvents } from "../@types/event"; /// The event type storing the user's individual policies. /// @@ -37,7 +38,7 @@ export const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new UnstableValue( ); /// The types of recommendations understood. -enum PolicyRecommendation { +export enum PolicyRecommendation { Ban = "m.ban", } @@ -64,6 +65,12 @@ export enum PolicyScope { Server = "m.policy.server", } +const scopeToEventTypeMap: Record = { + [PolicyScope.User]: EventType.PolicyRuleUser, + [PolicyScope.Room]: EventType.PolicyRuleRoom, + [PolicyScope.Server]: EventType.PolicyRuleServer, +}; + /** * A container for ignored invites. * @@ -87,7 +94,7 @@ export class IgnoredInvites { */ public async addRule(scope: PolicyScope, entity: string, reason: string): Promise { const target = await this.getOrCreateTargetRoom(); - const response = await this.client.sendStateEvent(target.roomId, scope, { + const response = await this.client.sendStateEvent(target.roomId, scopeToEventTypeMap[scope], { entity, reason, recommendation: PolicyRecommendation.Ban, @@ -173,7 +180,7 @@ export class IgnoredInvites { { scope: PolicyScope.User, entities: [sender] }, { scope: PolicyScope.Server, entities: [senderServer, roomServer] }, ]) { - const events = state.getStateEvents(scope); + const events = state.getStateEvents(scopeToEventTypeMap[scope]); for (const event of events) { const content = event.getContent(); if (content?.recommendation != PolicyRecommendation.Ban) { diff --git a/src/types.ts b/src/types.ts index 1edb3979564..97a89002c99 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,3 +23,5 @@ limitations under the License. export type * from "./@types/media"; export * from "./@types/membership"; +export type * from "./@types/event"; +export type * from "./@types/state_events"; diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index e5711986894..a33f676b1b7 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -35,6 +35,7 @@ import { import { SummaryStatsReportGatherer } from "./stats/summaryStatsReportGatherer"; import { CallFeedStatsReporter } from "./stats/callFeedStatsReporter"; import { KnownMembership } from "../@types/membership"; +import { CallMembershipData } from "../matrixrtc/CallMembership"; export enum GroupCallIntent { Ring = "m.ring", @@ -167,6 +168,7 @@ export interface IGroupCallDataChannelOptions { export interface IGroupCallRoomState { "m.intent": GroupCallIntent; "m.type": GroupCallType; + "m.terminated"?: GroupCallTerminationReason; "io.element.ptt"?: boolean; // TODO: Specify data-channels "dataChannelsEnabled"?: boolean; @@ -196,6 +198,11 @@ export interface IGroupCallRoomMemberState { "m.calls": IGroupCallRoomMemberCallState[]; } +// XXX: this hasn't made it into the MSC yet +export interface ExperimentalGroupCallRoomMemberState { + memberships: CallMembershipData[]; +} + export enum GroupCallState { LocalCallFeedUninitialized = "local_call_feed_uninitialized", InitializingLocalCallFeed = "initializing_local_call_feed",