1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-23 17:02:25 +03:00

Convert Room and RoomState to Typescript

This commit is contained in:
Michael Telatynski
2021-06-18 15:29:45 +01:00
parent 2c6858f149
commit 7aa5a79c86
9 changed files with 3149 additions and 3105 deletions

View File

@@ -37,3 +37,5 @@ export enum Preset {
TrustedPrivateChat = "trusted_private_chat", TrustedPrivateChat = "trusted_private_chat",
PublicChat = "public_chat", PublicChat = "public_chat",
} }
export type ResizeMethod = "crop" | "scale";

View File

@@ -32,7 +32,6 @@ import { Group } from "./models/group";
import { EventTimeline } from "./models/event-timeline"; import { EventTimeline } from "./models/event-timeline";
import { PushAction, PushProcessor } from "./pushprocessor"; import { PushAction, PushProcessor } from "./pushprocessor";
import { AutoDiscovery } from "./autodiscovery"; import { AutoDiscovery } from "./autodiscovery";
import { MatrixError } from "./http-api";
import * as olmlib from "./crypto/olmlib"; import * as olmlib from "./crypto/olmlib";
import { decodeBase64, encodeBase64 } from "./crypto/olmlib"; import { decodeBase64, encodeBase64 } from "./crypto/olmlib";
import { ReEmitter } from './ReEmitter'; import { ReEmitter } from './ReEmitter';
@@ -40,6 +39,7 @@ import { RoomList } from './crypto/RoomList';
import { logger } from './logger'; import { logger } from './logger';
import { SERVICE_TYPES } from './service-types'; import { SERVICE_TYPES } from './service-types';
import { import {
MatrixError,
MatrixHttpApi, MatrixHttpApi,
PREFIX_IDENTITY_V2, PREFIX_IDENTITY_V2,
PREFIX_MEDIA_R0, PREFIX_MEDIA_R0,
@@ -64,7 +64,7 @@ import {
import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
import type Request from "request"; import type Request from "request";
import { MatrixScheduler } from "./scheduler"; import { MatrixScheduler } from "./scheduler";
import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo } from "./matrix"; import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo, NotificationCountType } from "./matrix";
import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store"; import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store";
import { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store"; import { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store";
import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store"; import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store";
@@ -94,7 +94,8 @@ import {
IJoinRoomOpts, IJoinRoomOpts,
IPaginateOpts, IPaginateOpts,
IPresenceOpts, IPresenceOpts,
IRedactOpts, IRoomDirectoryOptions, IRedactOpts,
IRoomDirectoryOptions,
ISearchOpts, ISearchOpts,
ISendEventResponse, ISendEventResponse,
IUploadOpts, IUploadOpts,
@@ -348,6 +349,26 @@ export interface IStoredClientOpts extends IStartClientOpts {
canResetEntireTimeline: ResetTimelineCallback; canResetEntireTimeline: ResetTimelineCallback;
} }
export enum RoomVersionStability {
Stable = "stable",
Unstable = "unstable",
}
export interface IRoomVersionsCapability {
default: string;
available: Record<string, RoomVersionStability>;
}
export interface IChangePasswordCapability {
enabled: boolean;
}
interface ICapabilities {
[key: string]: any;
"m.change_password"?: IChangePasswordCapability;
"m.room_versions"?: IRoomVersionsCapability;
}
/** /**
* Represents a Matrix Client. Only directly construct this if you want to use * Represents a Matrix Client. Only directly construct this if you want to use
* custom modules. Normally, {@link createClient} should be used * custom modules. Normally, {@link createClient} should be used
@@ -408,7 +429,7 @@ export class MatrixClient extends EventEmitter {
protected serverVersionsPromise: Promise<any>; protected serverVersionsPromise: Promise<any>;
protected cachedCapabilities: { protected cachedCapabilities: {
capabilities: Record<string, any>; capabilities: ICapabilities;
expiration: number; expiration: number;
}; };
protected clientWellKnown: any; protected clientWellKnown: any;
@@ -529,7 +550,7 @@ export class MatrixClient extends EventEmitter {
const room = this.getRoom(event.getRoomId()); const room = this.getRoom(event.getRoomId());
if (!room) return; if (!room) return;
const currentCount = room.getUnreadNotificationCount("highlight"); const currentCount = room.getUnreadNotificationCount(NotificationCountType.Highlight);
// Ensure the unread counts are kept up to date if the event is encrypted // Ensure the unread counts are kept up to date if the event is encrypted
// We also want to make sure that the notification count goes up if we already // We also want to make sure that the notification count goes up if we already
@@ -545,12 +566,12 @@ export class MatrixClient extends EventEmitter {
let newCount = currentCount; let newCount = currentCount;
if (newHighlight && !oldHighlight) newCount++; if (newHighlight && !oldHighlight) newCount++;
if (!newHighlight && oldHighlight) newCount--; if (!newHighlight && oldHighlight) newCount--;
room.setUnreadNotificationCount("highlight", newCount); room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount);
// Fix 'Mentions Only' rooms from not having the right badge count // Fix 'Mentions Only' rooms from not having the right badge count
const totalCount = room.getUnreadNotificationCount('total'); const totalCount = room.getUnreadNotificationCount(NotificationCountType.Total);
if (totalCount < newCount) { if (totalCount < newCount) {
room.setUnreadNotificationCount('total', newCount); room.setUnreadNotificationCount(NotificationCountType.Total, newCount);
} }
} }
} }
@@ -1058,7 +1079,7 @@ export class MatrixClient extends EventEmitter {
* @return {Promise} Resolves to the capabilities of the homeserver * @return {Promise} Resolves to the capabilities of the homeserver
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
public getCapabilities(fresh = false): Promise<Record<string, any>> { public getCapabilities(fresh = false): Promise<ICapabilities> {
const now = new Date().getTime(); const now = new Date().getTime();
if (this.cachedCapabilities && !fresh) { if (this.cachedCapabilities && !fresh) {
@@ -1076,7 +1097,7 @@ export class MatrixClient extends EventEmitter {
return null; // otherwise consume the error return null; // otherwise consume the error
}).then((r) => { }).then((r) => {
if (!r) r = {}; if (!r) r = {};
const capabilities = r["capabilities"] || {}; const capabilities: ICapabilities = r["capabilities"] || {};
// If the capabilities missed the cache, cache it for a shorter amount // If the capabilities missed the cache, cache it for a shorter amount
// of time to try and refresh them later. // of time to try and refresh them later.
@@ -1085,7 +1106,7 @@ export class MatrixClient extends EventEmitter {
: 60000 + (Math.random() * 5000); : 60000 + (Math.random() * 5000);
this.cachedCapabilities = { this.cachedCapabilities = {
capabilities: capabilities, capabilities,
expiration: now + cacheMs, expiration: now + cacheMs,
}; };
@@ -3544,7 +3565,7 @@ export class MatrixClient extends EventEmitter {
const room = this.getRoom(event.getRoomId()); const room = this.getRoom(event.getRoomId());
if (room) { if (room) {
room._addLocalEchoReceipt(this.credentials.userId, event, receiptType); room.addLocalEchoReceipt(this.credentials.userId, event, receiptType);
} }
return promise; return promise;
} }
@@ -3614,7 +3635,7 @@ export class MatrixClient extends EventEmitter {
throw new Error(`Cannot set read receipt to a pending event (${rrEventId})`); throw new Error(`Cannot set read receipt to a pending event (${rrEventId})`);
} }
if (room) { if (room) {
room._addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read"); room.addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read");
} }
} }
@@ -4230,7 +4251,7 @@ export class MatrixClient extends EventEmitter {
// reduce the required number of events appropriately // reduce the required number of events appropriately
limit = limit - numAdded; limit = limit - numAdded;
const prom = new Promise((resolve, reject) => { const prom = new Promise<Room>((resolve, reject) => {
// wait for a time before doing this request // wait for a time before doing this request
// (which may be 0 in order not to special case the code paths) // (which may be 0 in order not to special case the code paths)
sleep(timeToWaitMs).then(() => { sleep(timeToWaitMs).then(() => {

View File

@@ -39,7 +39,7 @@ export function getHttpUriForMxc(
width: number, width: number,
height: number, height: number,
resizeMethod: string, resizeMethod: string,
allowDirectLinks: boolean, allowDirectLinks = false,
): string { ): string {
if (typeof mxc !== "string" || !mxc) { if (typeof mxc !== "string" || !mxc) {
return ''; return '';

View File

@@ -70,11 +70,15 @@ interface IContent {
"m.relates_to"?: IEventRelation; "m.relates_to"?: IEventRelation;
} }
type StrippedState = Required<Pick<IEvent, "content" | "state_key" | "type" | "sender">>;
interface IUnsigned { interface IUnsigned {
age?: number; age?: number;
prev_sender?: string; prev_sender?: string;
prev_content?: IContent; prev_content?: IContent;
redacted_because?: IEvent; redacted_because?: IEvent;
transaction_id?: string;
invite_room_state?: StrippedState[];
} }
export interface IEvent { export interface IEvent {

View File

@@ -1,832 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 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.
*/
/**
* @module models/room-state
*/
import { EventEmitter } from "events";
import { RoomMember } from "./room-member";
import { logger } from '../logger';
import * as utils from "../utils";
import { EventType } from "../@types/event";
// possible statuses for out-of-band member loading
const OOB_STATUS_NOTSTARTED = 1;
const OOB_STATUS_INPROGRESS = 2;
const OOB_STATUS_FINISHED = 3;
/**
* Construct room state.
*
* Room State represents the state of the room at a given point.
* It can be mutated by adding state events to it.
* There are two types of room member associated with a state event:
* normal member objects (accessed via getMember/getMembers) which mutate
* with the state to represent the current state of that room/user, eg.
* the object returned by getMember('@bob:example.com') will mutate to
* get a different display name if Bob later changes his display name
* in the room.
* There are also 'sentinel' members (accessed via getSentinelMember).
* These also represent the state of room members at the point in time
* represented by the RoomState object, but unlike objects from getMember,
* sentinel objects will always represent the room state as at the time
* getSentinelMember was called, so if Bob subsequently changes his display
* name, a room member object previously acquired with getSentinelMember
* will still have his old display name. Calling getSentinelMember again
* after the display name change will return a new RoomMember object
* with Bob's new display name.
*
* @constructor
* @param {?string} roomId Optional. The ID of the room which has this state.
* If none is specified it just tracks paginationTokens, useful for notifTimelineSet
* @param {?object} oobMemberFlags Optional. The state of loading out of bound members.
* As the timeline might get reset while they are loading, this state needs to be inherited
* and shared when the room state is cloned for the new timeline.
* This should only be passed from clone.
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
* on the user's ID.
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
* events dictionary, keyed on the event type and then the state_key value.
* @prop {string} paginationToken The pagination token for this state.
*/
export function RoomState(roomId, oobMemberFlags = undefined) {
this.roomId = roomId;
this.members = {
// userId: RoomMember
};
this.events = new Map(); // Map<eventType, Map<stateKey, MatrixEvent>>
this.paginationToken = null;
this._sentinels = {
// userId: RoomMember
};
this._updateModifiedTime();
// stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
this._displayNameToUserIds = {};
this._userIdsToDisplayNames = {};
this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite
this._joinedMemberCount = null; // cache of the number of joined members
// joined members count from summary api
// once set, we know the server supports the summary api
// and we should only trust that
// we could also only trust that before OOB members
// are loaded but doesn't seem worth the hassle atm
this._summaryJoinedMemberCount = null;
// same for invited member count
this._invitedMemberCount = null;
this._summaryInvitedMemberCount = null;
if (!oobMemberFlags) {
oobMemberFlags = {
status: OOB_STATUS_NOTSTARTED,
};
}
this._oobMemberFlags = oobMemberFlags;
}
utils.inherits(RoomState, EventEmitter);
/**
* Returns the number of joined members in this room
* This method caches the result.
* @return {integer} The number of members in this room whose membership is 'join'
*/
RoomState.prototype.getJoinedMemberCount = function() {
if (this._summaryJoinedMemberCount !== null) {
return this._summaryJoinedMemberCount;
}
if (this._joinedMemberCount === null) {
this._joinedMemberCount = this.getMembers().reduce((count, m) => {
return m.membership === 'join' ? count + 1 : count;
}, 0);
}
return this._joinedMemberCount;
};
/**
* Set the joined member count explicitly (like from summary part of the sync response)
* @param {number} count the amount of joined members
*/
RoomState.prototype.setJoinedMemberCount = function(count) {
this._summaryJoinedMemberCount = count;
};
/**
* Returns the number of invited members in this room
* @return {integer} The number of members in this room whose membership is 'invite'
*/
RoomState.prototype.getInvitedMemberCount = function() {
if (this._summaryInvitedMemberCount !== null) {
return this._summaryInvitedMemberCount;
}
if (this._invitedMemberCount === null) {
this._invitedMemberCount = this.getMembers().reduce((count, m) => {
return m.membership === 'invite' ? count + 1 : count;
}, 0);
}
return this._invitedMemberCount;
};
/**
* Set the amount of invited members in this room
* @param {number} count the amount of invited members
*/
RoomState.prototype.setInvitedMemberCount = function(count) {
this._summaryInvitedMemberCount = count;
};
/**
* Get all RoomMembers in this room.
* @return {Array<RoomMember>} A list of RoomMembers.
*/
RoomState.prototype.getMembers = function() {
return Object.values(this.members);
};
/**
* Get all RoomMembers in this room, excluding the user IDs provided.
* @param {Array<string>} excludedIds The user IDs to exclude.
* @return {Array<RoomMember>} A list of RoomMembers.
*/
RoomState.prototype.getMembersExcept = function(excludedIds) {
return Object.values(this.members)
.filter((m) => !excludedIds.includes(m.userId));
};
/**
* Get a room member by their user ID.
* @param {string} userId The room member's user ID.
* @return {RoomMember} The member or null if they do not exist.
*/
RoomState.prototype.getMember = function(userId) {
return this.members[userId] || null;
};
/**
* Get a room member whose properties will not change with this room state. You
* typically want this if you want to attach a RoomMember to a MatrixEvent which
* may no longer be represented correctly by Room.currentState or Room.oldState.
* The term 'sentinel' refers to the fact that this RoomMember is an unchanging
* guardian for state at this particular point in time.
* @param {string} userId The room member's user ID.
* @return {RoomMember} The member or null if they do not exist.
*/
RoomState.prototype.getSentinelMember = function(userId) {
if (!userId) return null;
let sentinel = this._sentinels[userId];
if (sentinel === undefined) {
sentinel = new RoomMember(this.roomId, userId);
const member = this.members[userId];
if (member) {
sentinel.setMembershipEvent(member.events.member, this);
}
this._sentinels[userId] = sentinel;
}
return sentinel;
};
/**
* Get state events from the state of the room.
* @param {string} eventType The event type of the state event.
* @param {string} stateKey Optional. The state_key of the state event. If
* this is <code>undefined</code> then all matching state events will be
* returned.
* @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was
* <code>undefined</code>, else a single event (or null if no match found).
*/
RoomState.prototype.getStateEvents = function(eventType, stateKey) {
if (!this.events.has(eventType)) {
// no match
return stateKey === undefined ? [] : null;
}
if (stateKey === undefined) { // return all values
return Array.from(this.events.get(eventType).values());
}
const event = this.events.get(eventType).get(stateKey);
return event ? event : null;
};
/**
* Creates a copy of this room state so that mutations to either won't affect the other.
* @return {RoomState} the copy of the room state
*/
RoomState.prototype.clone = function() {
const copy = new RoomState(this.roomId, this._oobMemberFlags);
// Ugly hack: because setStateEvents will mark
// members as susperseding future out of bound members
// if loading is in progress (through _oobMemberFlags)
// since these are not new members, we're merely copying them
// set the status to not started
// after copying, we set back the status
const status = this._oobMemberFlags.status;
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
Array.from(this.events.values()).forEach((eventsByStateKey) => {
copy.setStateEvents(Array.from(eventsByStateKey.values()));
});
// Ugly hack: see above
this._oobMemberFlags.status = status;
if (this._summaryInvitedMemberCount !== null) {
copy.setInvitedMemberCount(this.getInvitedMemberCount());
}
if (this._summaryJoinedMemberCount !== null) {
copy.setJoinedMemberCount(this.getJoinedMemberCount());
}
// copy out of band flags if needed
if (this._oobMemberFlags.status == OOB_STATUS_FINISHED) {
// copy markOutOfBand flags
this.getMembers().forEach((member) => {
if (member.isOutOfBand()) {
const copyMember = copy.getMember(member.userId);
copyMember.markOutOfBand();
}
});
}
return copy;
};
/**
* Add previously unknown state events.
* When lazy loading members while back-paginating,
* the relevant room state for the timeline chunk at the end
* of the chunk can be set with this method.
* @param {MatrixEvent[]} events state events to prepend
*/
RoomState.prototype.setUnknownStateEvents = function(events) {
const unknownStateEvents = events.filter((event) => {
return !this.events.has(event.getType()) ||
!this.events.get(event.getType()).has(event.getStateKey());
});
this.setStateEvents(unknownStateEvents);
};
/**
* Add an array of one or more state MatrixEvents, overwriting
* any existing state with the same {type, stateKey} tuple. Will fire
* "RoomState.events" for every event added. May fire "RoomState.members"
* if there are <code>m.room.member</code> events.
* @param {MatrixEvent[]} stateEvents a list of state events for this room.
* @fires module:client~MatrixClient#event:"RoomState.members"
* @fires module:client~MatrixClient#event:"RoomState.newMember"
* @fires module:client~MatrixClient#event:"RoomState.events"
*/
RoomState.prototype.setStateEvents = function(stateEvents) {
const self = this;
this._updateModifiedTime();
// update the core event dict
stateEvents.forEach(function(event) {
if (event.getRoomId() !== self.roomId) {
return;
}
if (!event.isState()) {
return;
}
const lastStateEvent = self._getStateEventMatching(event);
self._setStateEvent(event);
if (event.getType() === "m.room.member") {
_updateDisplayNameCache(
self, event.getStateKey(), event.getContent().displayname,
);
_updateThirdPartyTokenCache(self, event);
}
self.emit("RoomState.events", event, self, lastStateEvent);
});
// update higher level data structures. This needs to be done AFTER the
// core event dict as these structures may depend on other state events in
// the given array (e.g. disambiguating display names in one go to do both
// clashing names rather than progressively which only catches 1 of them).
stateEvents.forEach(function(event) {
if (event.getRoomId() !== self.roomId) {
return;
}
if (!event.isState()) {
return;
}
if (event.getType() === "m.room.member") {
const userId = event.getStateKey();
// leave events apparently elide the displayname or avatar_url,
// so let's fake one up so that we don't leak user ids
// into the timeline
if (event.getContent().membership === "leave" ||
event.getContent().membership === "ban") {
event.getContent().avatar_url =
event.getContent().avatar_url ||
event.getPrevContent().avatar_url;
event.getContent().displayname =
event.getContent().displayname ||
event.getPrevContent().displayname;
}
const member = self._getOrCreateMember(userId, event);
member.setMembershipEvent(event, self);
self._updateMember(member);
self.emit("RoomState.members", event, self, member);
} else if (event.getType() === "m.room.power_levels") {
// events with unknown state keys should be ignored
// and should not aggregate onto members power levels
if (event.getStateKey() !== "") {
return;
}
const members = Object.values(self.members);
members.forEach(function(member) {
// We only propagate `RoomState.members` event if the
// power levels has been changed
// large room suffer from large re-rendering especially when not needed
const oldLastModified = member.getLastModifiedTime();
member.setPowerLevelEvent(event);
if (oldLastModified !== member.getLastModifiedTime()) {
self.emit("RoomState.members", event, self, member);
}
});
// assume all our sentinels are now out-of-date
self._sentinels = {};
}
});
};
/**
* Looks up a member by the given userId, and if it doesn't exist,
* create it and emit the `RoomState.newMember` event.
* This method makes sure the member is added to the members dictionary
* before emitting, as this is done from setStateEvents and _setOutOfBandMember.
* @param {string} userId the id of the user to look up
* @param {MatrixEvent} event the membership event for the (new) member. Used to emit.
* @fires module:client~MatrixClient#event:"RoomState.newMember"
* @returns {RoomMember} the member, existing or newly created.
*/
RoomState.prototype._getOrCreateMember = function(userId, event) {
let member = this.members[userId];
if (!member) {
member = new RoomMember(this.roomId, userId);
// add member to members before emitting any events,
// as event handlers often lookup the member
this.members[userId] = member;
this.emit("RoomState.newMember", event, this, member);
}
return member;
};
RoomState.prototype._setStateEvent = function(event) {
if (!this.events.has(event.getType())) {
this.events.set(event.getType(), new Map());
}
this.events.get(event.getType()).set(event.getStateKey(), event);
};
RoomState.prototype._getStateEventMatching = function(event) {
if (!this.events.has(event.getType())) return null;
return this.events.get(event.getType()).get(event.getStateKey());
};
RoomState.prototype._updateMember = function(member) {
// this member may have a power level already, so set it.
const pwrLvlEvent = this.getStateEvents("m.room.power_levels", "");
if (pwrLvlEvent) {
member.setPowerLevelEvent(pwrLvlEvent);
}
// blow away the sentinel which is now outdated
delete this._sentinels[member.userId];
this.members[member.userId] = member;
this._joinedMemberCount = null;
this._invitedMemberCount = null;
};
/**
* Get the out-of-band members loading state, whether loading is needed or not.
* Note that loading might be in progress and hence isn't needed.
* @return {bool} whether or not the members of this room need to be loaded
*/
RoomState.prototype.needsOutOfBandMembers = function() {
return this._oobMemberFlags.status === OOB_STATUS_NOTSTARTED;
};
/**
* Mark this room state as waiting for out-of-band members,
* ensuring it doesn't ask for them to be requested again
* through needsOutOfBandMembers
*/
RoomState.prototype.markOutOfBandMembersStarted = function() {
if (this._oobMemberFlags.status !== OOB_STATUS_NOTSTARTED) {
return;
}
this._oobMemberFlags.status = OOB_STATUS_INPROGRESS;
};
/**
* Mark this room state as having failed to fetch out-of-band members
*/
RoomState.prototype.markOutOfBandMembersFailed = function() {
if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
return;
}
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
};
/**
* Clears the loaded out-of-band members
*/
RoomState.prototype.clearOutOfBandMembers = function() {
let count = 0;
Object.keys(this.members).forEach((userId) => {
const member = this.members[userId];
if (member.isOutOfBand()) {
++count;
delete this.members[userId];
}
});
logger.log(`LL: RoomState removed ${count} members...`);
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
};
/**
* Sets the loaded out-of-band members.
* @param {MatrixEvent[]} stateEvents array of membership state events
*/
RoomState.prototype.setOutOfBandMembers = function(stateEvents) {
logger.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
return;
}
logger.log(`LL: RoomState put in OOB_STATUS_FINISHED state ...`);
this._oobMemberFlags.status = OOB_STATUS_FINISHED;
stateEvents.forEach((e) => this._setOutOfBandMember(e));
};
/**
* Sets a single out of band member, used by both setOutOfBandMembers and clone
* @param {MatrixEvent} stateEvent membership state event
*/
RoomState.prototype._setOutOfBandMember = function(stateEvent) {
if (stateEvent.getType() !== 'm.room.member') {
return;
}
const userId = stateEvent.getStateKey();
const existingMember = this.getMember(userId);
// never replace members received as part of the sync
if (existingMember && !existingMember.isOutOfBand()) {
return;
}
const member = this._getOrCreateMember(userId, stateEvent);
member.setMembershipEvent(stateEvent, this);
// needed to know which members need to be stored seperately
// as they are not part of the sync accumulator
// this is cleared by setMembershipEvent so when it's updated through /sync
member.markOutOfBand();
_updateDisplayNameCache(this, member.userId, member.name);
this._setStateEvent(stateEvent);
this._updateMember(member);
this.emit("RoomState.members", stateEvent, this, member);
};
/**
* Set the current typing event for this room.
* @param {MatrixEvent} event The typing event
*/
RoomState.prototype.setTypingEvent = function(event) {
Object.values(this.members).forEach(function(member) {
member.setTypingEvent(event);
});
};
/**
* Get the m.room.member event which has the given third party invite token.
*
* @param {string} token The token
* @return {?MatrixEvent} The m.room.member event or null
*/
RoomState.prototype.getInviteForThreePidToken = function(token) {
return this._tokenToInvite[token] || null;
};
/**
* Update the last modified time to the current time.
*/
RoomState.prototype._updateModifiedTime = function() {
this._modified = Date.now();
};
/**
* Get the timestamp when this room state was last updated. This timestamp is
* updated when this object has received new state events.
* @return {number} The timestamp
*/
RoomState.prototype.getLastModifiedTime = function() {
return this._modified;
};
/**
* Get user IDs with the specified or similar display names.
* @param {string} displayName The display name to get user IDs from.
* @return {string[]} An array of user IDs or an empty array.
*/
RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
return this._displayNameToUserIds[utils.removeHiddenChars(displayName)] || [];
};
/**
* Returns true if userId is in room, event is not redacted and either sender of
* mxEvent or has power level sufficient to redact events other than their own.
* @param {MatrixEvent} mxEvent The event to test permission for
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given used ID can redact given event
*/
RoomState.prototype.maySendRedactionForEvent = function(mxEvent, userId) {
const member = this.getMember(userId);
if (!member || member.membership === 'leave') return false;
if (mxEvent.status || mxEvent.isRedacted()) return false;
// The user may have been the sender, but they can't redact their own message
// if redactions are blocked.
const canRedact = this.maySendEvent("m.room.redaction", userId);
if (mxEvent.getSender() === userId) return canRedact;
return this._hasSufficientPowerLevelFor('redact', member.powerLevel);
};
/**
* Returns true if the given power level is sufficient for action
* @param {string} action The type of power level to check
* @param {number} powerLevel The power level of the member
* @return {boolean} true if the given power level is sufficient
*/
RoomState.prototype._hasSufficientPowerLevelFor = function(action, powerLevel) {
const powerLevelsEvent = this.getStateEvents('m.room.power_levels', '');
let powerLevels = {};
if (powerLevelsEvent) {
powerLevels = powerLevelsEvent.getContent();
}
let requiredLevel = 50;
if (utils.isNumber(powerLevels[action])) {
requiredLevel = powerLevels[action];
}
return powerLevel >= requiredLevel;
};
/**
* Short-form for maySendEvent('m.room.message', userId)
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* message events into the given room.
*/
RoomState.prototype.maySendMessage = function(userId) {
return this._maySendEventOfType('m.room.message', userId, false);
};
/**
* Returns true if the given user ID has permission to send a normal
* event of type `eventType` into this room.
* @param {string} eventType The type of event to test
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* the given type of event into this room,
* according to the room's state.
*/
RoomState.prototype.maySendEvent = function(eventType, userId) {
return this._maySendEventOfType(eventType, userId, false);
};
/**
* Returns true if the given MatrixClient has permission to send a state
* event of type `stateEventType` into this room.
* @param {string} stateEventType The type of state events to test
* @param {MatrixClient} cli The client to test permission for
* @return {boolean} true if the given client should be permitted to send
* the given type of state event into this room,
* according to the room's state.
*/
RoomState.prototype.mayClientSendStateEvent = function(stateEventType, cli) {
if (cli.isGuest()) {
return false;
}
return this.maySendStateEvent(stateEventType, cli.credentials.userId);
};
/**
* Returns true if the given user ID has permission to send a state
* event of type `stateEventType` into this room.
* @param {string} stateEventType The type of state events to test
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* the given type of state event into this room,
* according to the room's state.
*/
RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
return this._maySendEventOfType(stateEventType, userId, true);
};
/**
* Returns true if the given user ID has permission to send a normal or state
* event of type `eventType` into this room.
* @param {string} eventType The type of event to test
* @param {string} userId The user ID of the user to test permission for
* @param {boolean} state If true, tests if the user may send a state
event of this type. Otherwise tests whether
they may send a regular event.
* @return {boolean} true if the given user ID should be permitted to send
* the given type of event into this room,
* according to the room's state.
*/
RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
const power_levels_event = this.getStateEvents('m.room.power_levels', '');
let power_levels;
let events_levels = {};
let state_default = 0;
let events_default = 0;
let powerLevel = 0;
if (power_levels_event) {
power_levels = power_levels_event.getContent();
events_levels = power_levels.events || {};
if (Number.isFinite(power_levels.state_default)) {
state_default = power_levels.state_default;
} else {
state_default = 50;
}
const userPowerLevel = power_levels.users && power_levels.users[userId];
if (Number.isFinite(userPowerLevel)) {
powerLevel = userPowerLevel;
} else if (Number.isFinite(power_levels.users_default)) {
powerLevel = power_levels.users_default;
}
if (Number.isFinite(power_levels.events_default)) {
events_default = power_levels.events_default;
}
}
let required_level = state ? state_default : events_default;
if (Number.isFinite(events_levels[eventType])) {
required_level = events_levels[eventType];
}
return powerLevel >= required_level;
};
/**
* Returns true if the given user ID has permission to trigger notification
* of type `notifLevelKey`
* @param {string} notifLevelKey The level of notification to test (eg. 'room')
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID has permission to trigger a
* notification of this type.
*/
RoomState.prototype.mayTriggerNotifOfType = function(notifLevelKey, userId) {
const member = this.getMember(userId);
if (!member) {
return false;
}
const powerLevelsEvent = this.getStateEvents('m.room.power_levels', '');
let notifLevel = 50;
if (
powerLevelsEvent &&
powerLevelsEvent.getContent() &&
powerLevelsEvent.getContent().notifications &&
utils.isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey])
) {
notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey];
}
return member.powerLevel >= notifLevel;
};
/**
* Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
* @returns {string} the join_rule applied to this room
*/
RoomState.prototype.getJoinRule = function() {
const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, "");
const joinRuleContent = joinRuleEvent ? joinRuleEvent.getContent() : {};
return joinRuleContent["join_rule"] || "invite";
};
function _updateThirdPartyTokenCache(roomState, memberEvent) {
if (!memberEvent.getContent().third_party_invite) {
return;
}
const token = (memberEvent.getContent().third_party_invite.signed || {}).token;
if (!token) {
return;
}
const threePidInvite = roomState.getStateEvents(
"m.room.third_party_invite", token,
);
if (!threePidInvite) {
return;
}
roomState._tokenToInvite[token] = memberEvent;
}
function _updateDisplayNameCache(roomState, userId, displayName) {
const oldName = roomState._userIdsToDisplayNames[userId];
delete roomState._userIdsToDisplayNames[userId];
if (oldName) {
// Remove the old name from the cache.
// We clobber the user_id > name lookup but the name -> [user_id] lookup
// means we need to remove that user ID from that array rather than nuking
// the lot.
const strippedOldName = utils.removeHiddenChars(oldName);
const existingUserIds = roomState._displayNameToUserIds[strippedOldName];
if (existingUserIds) {
// remove this user ID from this array
const filteredUserIDs = existingUserIds.filter((id) => id !== userId);
roomState._displayNameToUserIds[strippedOldName] = filteredUserIDs;
}
}
roomState._userIdsToDisplayNames[userId] = displayName;
const strippedDisplayname = displayName && utils.removeHiddenChars(displayName);
// an empty stripped displayname (undefined/'') will be set to MXID in room-member.js
if (strippedDisplayname) {
if (!roomState._displayNameToUserIds[strippedDisplayname]) {
roomState._displayNameToUserIds[strippedDisplayname] = [];
}
roomState._displayNameToUserIds[strippedDisplayname].push(userId);
}
}
/**
* Fires whenever the event dictionary in room state is updated.
* @event module:client~MatrixClient#"RoomState.events"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.events dictionary
* was updated.
* @param {MatrixEvent} prevEvent The event being replaced by the new state, if
* known. Note that this can differ from `getPrevContent()` on the new state event
* as this is the store's view of the last state, not the previous state provided
* by the server.
* @example
* matrixClient.on("RoomState.events", function(event, state, prevEvent){
* var newStateEvent = event;
* });
*/
/**
* Fires whenever a member in the members dictionary is updated in any way.
* @event module:client~MatrixClient#"RoomState.members"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.members dictionary
* was updated.
* @param {RoomMember} member The room member that was updated.
* @example
* matrixClient.on("RoomState.members", function(event, state, member){
* var newMembershipState = member.membership;
* });
*/
/**
* Fires whenever a member is added to the members dictionary. The RoomMember
* will not be fully populated yet (e.g. no membership state) but will already
* be available in the members dictionary.
* @event module:client~MatrixClient#"RoomState.newMember"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.members dictionary
* was updated with a new entry.
* @param {RoomMember} member The room member that was added.
* @example
* matrixClient.on("RoomState.newMember", function(event, state, member){
* // add event listeners on 'member'
* });
*/

825
src/models/room-state.ts Normal file
View File

@@ -0,0 +1,825 @@
/*
Copyright 2015 - 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.
*/
/**
* @module models/room-state
*/
import { EventEmitter } from "events";
import { RoomMember } from "./room-member";
import { logger } from '../logger';
import * as utils from "../utils";
import { EventType } from "../@types/event";
import { MatrixEvent } from "./event";
import { MatrixClient } from "../client";
// possible statuses for out-of-band member loading
enum OobStatus {
NotStarted,
InProgress,
Finished,
}
export class RoomState extends EventEmitter {
private sentinels: Record<string, RoomMember> = {}; // userId: RoomMember
// stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
private displayNameToUserIds: Record<string, string[]> = {};
private userIdsToDisplayNames: Record<string, string> = {};
private tokenToInvite: Record<string, MatrixEvent> = {}; // 3pid invite state_key to m.room.member invite
private joinedMemberCount: number = null; // cache of the number of joined members
// joined members count from summary api
// once set, we know the server supports the summary api
// and we should only trust that
// we could also only trust that before OOB members
// are loaded but doesn't seem worth the hassle atm
private summaryJoinedMemberCount: number = null;
// same for invited member count
private invitedMemberCount: number = null;
private summaryInvitedMemberCount: number = null;
private modified: number;
// XXX: Should be read-only
public members: Record<string, RoomMember> = {}; // userId: RoomMember
public events = new Map<string, Map<string, MatrixEvent>>(); // Map<eventType, Map<stateKey, MatrixEvent>>
public paginationToken: string = null;
/**
* Construct room state.
*
* Room State represents the state of the room at a given point.
* It can be mutated by adding state events to it.
* There are two types of room member associated with a state event:
* normal member objects (accessed via getMember/getMembers) which mutate
* with the state to represent the current state of that room/user, eg.
* the object returned by getMember('@bob:example.com') will mutate to
* get a different display name if Bob later changes his display name
* in the room.
* There are also 'sentinel' members (accessed via getSentinelMember).
* These also represent the state of room members at the point in time
* represented by the RoomState object, but unlike objects from getMember,
* sentinel objects will always represent the room state as at the time
* getSentinelMember was called, so if Bob subsequently changes his display
* name, a room member object previously acquired with getSentinelMember
* will still have his old display name. Calling getSentinelMember again
* after the display name change will return a new RoomMember object
* with Bob's new display name.
*
* @constructor
* @param {?string} roomId Optional. The ID of the room which has this state.
* If none is specified it just tracks paginationTokens, useful for notifTimelineSet
* @param {?object} oobMemberFlags Optional. The state of loading out of bound members.
* As the timeline might get reset while they are loading, this state needs to be inherited
* and shared when the room state is cloned for the new timeline.
* This should only be passed from clone.
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
* on the user's ID.
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
* events dictionary, keyed on the event type and then the state_key value.
* @prop {string} paginationToken The pagination token for this state.
*/
constructor(public readonly roomId: string, private oobMemberFlags = { status: OobStatus.NotStarted }) {
super();
this.updateModifiedTime();
}
/**
* Returns the number of joined members in this room
* This method caches the result.
* @return {number} The number of members in this room whose membership is 'join'
*/
public getJoinedMemberCount(): number {
if (this.summaryJoinedMemberCount !== null) {
return this.summaryJoinedMemberCount;
}
if (this.joinedMemberCount === null) {
this.joinedMemberCount = this.getMembers().reduce((count, m) => {
return m.membership === 'join' ? count + 1 : count;
}, 0);
}
return this.joinedMemberCount;
}
/**
* Set the joined member count explicitly (like from summary part of the sync response)
* @param {number} count the amount of joined members
*/
public setJoinedMemberCount(count: number): void {
this.summaryJoinedMemberCount = count;
}
/**
* Returns the number of invited members in this room
* @return {number} The number of members in this room whose membership is 'invite'
*/
public getInvitedMemberCount(): number {
if (this.summaryInvitedMemberCount !== null) {
return this.summaryInvitedMemberCount;
}
if (this.invitedMemberCount === null) {
this.invitedMemberCount = this.getMembers().reduce((count, m) => {
return m.membership === 'invite' ? count + 1 : count;
}, 0);
}
return this.invitedMemberCount;
}
/**
* Set the amount of invited members in this room
* @param {number} count the amount of invited members
*/
public setInvitedMemberCount(count: number): void {
this.summaryInvitedMemberCount = count;
}
/**
* Get all RoomMembers in this room.
* @return {Array<RoomMember>} A list of RoomMembers.
*/
public getMembers(): RoomMember[] {
return Object.values(this.members);
}
/**
* Get all RoomMembers in this room, excluding the user IDs provided.
* @param {Array<string>} excludedIds The user IDs to exclude.
* @return {Array<RoomMember>} A list of RoomMembers.
*/
public getMembersExcept(excludedIds: string[]): RoomMember[] {
return this.getMembers().filter((m) => !excludedIds.includes(m.userId));
}
/**
* Get a room member by their user ID.
* @param {string} userId The room member's user ID.
* @return {RoomMember} The member or null if they do not exist.
*/
public getMember(userId: string): RoomMember | null {
return this.members[userId] || null;
}
/**
* Get a room member whose properties will not change with this room state. You
* typically want this if you want to attach a RoomMember to a MatrixEvent which
* may no longer be represented correctly by Room.currentState or Room.oldState.
* The term 'sentinel' refers to the fact that this RoomMember is an unchanging
* guardian for state at this particular point in time.
* @param {string} userId The room member's user ID.
* @return {RoomMember} The member or null if they do not exist.
*/
public getSentinelMember(userId: string): RoomMember | null {
if (!userId) return null;
let sentinel = this.sentinels[userId];
if (sentinel === undefined) {
sentinel = new RoomMember(this.roomId, userId);
const member = this.members[userId];
if (member) {
sentinel.setMembershipEvent(member.events.member, this);
}
this.sentinels[userId] = sentinel;
}
return sentinel;
}
/**
* Get state events from the state of the room.
* @param {string} eventType The event type of the state event.
* @param {string} stateKey Optional. The state_key of the state event. If
* this is <code>undefined</code> then all matching state events will be
* returned.
* @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was
* <code>undefined</code>, else a single event (or null if no match found).
*/
public getStateEvents(eventType: string): MatrixEvent[];
public getStateEvents(eventType: string, stateKey: string): MatrixEvent;
public getStateEvents(eventType: string, stateKey?: string) {
if (!this.events.has(eventType)) {
// no match
return stateKey === undefined ? [] : null;
}
if (stateKey === undefined) { // return all values
return Array.from(this.events.get(eventType).values());
}
const event = this.events.get(eventType).get(stateKey);
return event ? event : null;
}
/**
* Creates a copy of this room state so that mutations to either won't affect the other.
* @return {RoomState} the copy of the room state
*/
public clone(): RoomState {
const copy = new RoomState(this.roomId, this.oobMemberFlags);
// Ugly hack: because setStateEvents will mark
// members as susperseding future out of bound members
// if loading is in progress (through oobMemberFlags)
// since these are not new members, we're merely copying them
// set the status to not started
// after copying, we set back the status
const status = this.oobMemberFlags.status;
this.oobMemberFlags.status = OobStatus.NotStarted;
Array.from(this.events.values()).forEach((eventsByStateKey) => {
copy.setStateEvents(Array.from(eventsByStateKey.values()));
});
// Ugly hack: see above
this.oobMemberFlags.status = status;
if (this.summaryInvitedMemberCount !== null) {
copy.setInvitedMemberCount(this.getInvitedMemberCount());
}
if (this.summaryJoinedMemberCount !== null) {
copy.setJoinedMemberCount(this.getJoinedMemberCount());
}
// copy out of band flags if needed
if (this.oobMemberFlags.status == OobStatus.Finished) {
// copy markOutOfBand flags
this.getMembers().forEach((member) => {
if (member.isOutOfBand()) {
const copyMember = copy.getMember(member.userId);
copyMember.markOutOfBand();
}
});
}
return copy;
}
/**
* Add previously unknown state events.
* When lazy loading members while back-paginating,
* the relevant room state for the timeline chunk at the end
* of the chunk can be set with this method.
* @param {MatrixEvent[]} events state events to prepend
*/
public setUnknownStateEvents(events: MatrixEvent[]): void {
const unknownStateEvents = events.filter((event) => {
return !this.events.has(event.getType()) ||
!this.events.get(event.getType()).has(event.getStateKey());
});
this.setStateEvents(unknownStateEvents);
}
/**
* Add an array of one or more state MatrixEvents, overwriting
* any existing state with the same {type, stateKey} tuple. Will fire
* "RoomState.events" for every event added. May fire "RoomState.members"
* if there are <code>m.room.member</code> events.
* @param {MatrixEvent[]} stateEvents a list of state events for this room.
* @fires module:client~MatrixClient#event:"RoomState.members"
* @fires module:client~MatrixClient#event:"RoomState.newMember"
* @fires module:client~MatrixClient#event:"RoomState.events"
*/
public setStateEvents(stateEvents: MatrixEvent[]) {
this.updateModifiedTime();
// update the core event dict
stateEvents.forEach((event) => {
if (event.getRoomId() !== this.roomId) {
return;
}
if (!event.isState()) {
return;
}
const lastStateEvent = this.getStateEventMatching(event);
this.setStateEvent(event);
if (event.getType() === EventType.RoomMember) {
this.updateDisplayNameCache(event.getStateKey(), event.getContent().displayname);
this.updateThirdPartyTokenCache(event);
}
this.emit("RoomState.events", event, this, lastStateEvent);
});
// update higher level data structures. This needs to be done AFTER the
// core event dict as these structures may depend on other state events in
// the given array (e.g. disambiguating display names in one go to do both
// clashing names rather than progressively which only catches 1 of them).
stateEvents.forEach((event) => {
if (event.getRoomId() !== this.roomId) {
return;
}
if (!event.isState()) {
return;
}
if (event.getType() === EventType.RoomMember) {
const userId = event.getStateKey();
// leave events apparently elide the displayname or avatar_url,
// so let's fake one up so that we don't leak user ids
// into the timeline
if (event.getContent().membership === "leave" ||
event.getContent().membership === "ban") {
event.getContent().avatar_url =
event.getContent().avatar_url ||
event.getPrevContent().avatar_url;
event.getContent().displayname =
event.getContent().displayname ||
event.getPrevContent().displayname;
}
const member = this.getOrCreateMember(userId, event);
member.setMembershipEvent(event, this);
this.updateMember(member);
this.emit("RoomState.members", event, this, member);
} else if (event.getType() === EventType.RoomPowerLevels) {
// events with unknown state keys should be ignored
// and should not aggregate onto members power levels
if (event.getStateKey() !== "") {
return;
}
const members = Object.values(this.members);
members.forEach((member) => {
// We only propagate `RoomState.members` event if the
// power levels has been changed
// large room suffer from large re-rendering especially when not needed
const oldLastModified = member.getLastModifiedTime();
member.setPowerLevelEvent(event);
if (oldLastModified !== member.getLastModifiedTime()) {
this.emit("RoomState.members", event, this, member);
}
});
// assume all our sentinels are now out-of-date
this.sentinels = {};
}
});
}
/**
* Looks up a member by the given userId, and if it doesn't exist,
* create it and emit the `RoomState.newMember` event.
* This method makes sure the member is added to the members dictionary
* before emitting, as this is done from setStateEvents and setOutOfBandMember.
* @param {string} userId the id of the user to look up
* @param {MatrixEvent} event the membership event for the (new) member. Used to emit.
* @fires module:client~MatrixClient#event:"RoomState.newMember"
* @returns {RoomMember} the member, existing or newly created.
*/
private getOrCreateMember(userId: string, event: MatrixEvent): RoomMember {
let member = this.members[userId];
if (!member) {
member = new RoomMember(this.roomId, userId);
// add member to members before emitting any events,
// as event handlers often lookup the member
this.members[userId] = member;
this.emit("RoomState.newMember", event, this, member);
}
return member;
}
private setStateEvent(event: MatrixEvent): void {
if (!this.events.has(event.getType())) {
this.events.set(event.getType(), new Map());
}
this.events.get(event.getType()).set(event.getStateKey(), event);
}
private getStateEventMatching(event: MatrixEvent): MatrixEvent | null {
if (!this.events.has(event.getType())) return null;
return this.events.get(event.getType()).get(event.getStateKey());
}
private updateMember(member: RoomMember): void {
// this member may have a power level already, so set it.
const pwrLvlEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
if (pwrLvlEvent) {
member.setPowerLevelEvent(pwrLvlEvent);
}
// blow away the sentinel which is now outdated
delete this.sentinels[member.userId];
this.members[member.userId] = member;
this.joinedMemberCount = null;
this.invitedMemberCount = null;
}
/**
* Get the out-of-band members loading state, whether loading is needed or not.
* Note that loading might be in progress and hence isn't needed.
* @return {boolean} whether or not the members of this room need to be loaded
*/
public needsOutOfBandMembers(): boolean {
return this.oobMemberFlags.status === OobStatus.NotStarted;
}
/**
* Mark this room state as waiting for out-of-band members,
* ensuring it doesn't ask for them to be requested again
* through needsOutOfBandMembers
*/
public markOutOfBandMembersStarted(): void {
if (this.oobMemberFlags.status !== OobStatus.NotStarted) {
return;
}
this.oobMemberFlags.status = OobStatus.InProgress;
}
/**
* Mark this room state as having failed to fetch out-of-band members
*/
public markOutOfBandMembersFailed(): void {
if (this.oobMemberFlags.status !== OobStatus.InProgress) {
return;
}
this.oobMemberFlags.status = OobStatus.NotStarted;
}
/**
* Clears the loaded out-of-band members
*/
public clearOutOfBandMembers(): void {
let count = 0;
Object.keys(this.members).forEach((userId) => {
const member = this.members[userId];
if (member.isOutOfBand()) {
++count;
delete this.members[userId];
}
});
logger.log(`LL: RoomState removed ${count} members...`);
this.oobMemberFlags.status = OobStatus.NotStarted;
}
/**
* Sets the loaded out-of-band members.
* @param {MatrixEvent[]} stateEvents array of membership state events
*/
public setOutOfBandMembers(stateEvents: MatrixEvent[]): void {
logger.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
if (this.oobMemberFlags.status !== OobStatus.InProgress) {
return;
}
logger.log(`LL: RoomState put in finished state ...`);
this.oobMemberFlags.status = OobStatus.Finished;
stateEvents.forEach((e) => this.setOutOfBandMember(e));
}
/**
* Sets a single out of band member, used by both setOutOfBandMembers and clone
* @param {MatrixEvent} stateEvent membership state event
*/
private setOutOfBandMember(stateEvent: MatrixEvent): void {
if (stateEvent.getType() !== EventType.RoomMember) {
return;
}
const userId = stateEvent.getStateKey();
const existingMember = this.getMember(userId);
// never replace members received as part of the sync
if (existingMember && !existingMember.isOutOfBand()) {
return;
}
const member = this.getOrCreateMember(userId, stateEvent);
member.setMembershipEvent(stateEvent, this);
// needed to know which members need to be stored seperately
// as they are not part of the sync accumulator
// this is cleared by setMembershipEvent so when it's updated through /sync
member.markOutOfBand();
this.updateDisplayNameCache(member.userId, member.name);
this.setStateEvent(stateEvent);
this.updateMember(member);
this.emit("RoomState.members", stateEvent, this, member);
}
/**
* Set the current typing event for this room.
* @param {MatrixEvent} event The typing event
*/
public setTypingEvent(event: MatrixEvent): void {
Object.values(this.members).forEach(function(member) {
member.setTypingEvent(event);
});
}
/**
* Get the m.room.member event which has the given third party invite token.
*
* @param {string} token The token
* @return {?MatrixEvent} The m.room.member event or null
*/
public getInviteForThreePidToken(token: string): MatrixEvent | null {
return this.tokenToInvite[token] || null;
}
/**
* Update the last modified time to the current time.
*/
private updateModifiedTime(): void {
this.modified = Date.now();
}
/**
* Get the timestamp when this room state was last updated. This timestamp is
* updated when this object has received new state events.
* @return {number} The timestamp
*/
public getLastModifiedTime(): number {
return this.modified;
}
/**
* Get user IDs with the specified or similar display names.
* @param {string} displayName The display name to get user IDs from.
* @return {string[]} An array of user IDs or an empty array.
*/
public getUserIdsWithDisplayName(displayName: string): string[] {
return this.displayNameToUserIds[utils.removeHiddenChars(displayName)] || [];
}
/**
* Returns true if userId is in room, event is not redacted and either sender of
* mxEvent or has power level sufficient to redact events other than their own.
* @param {MatrixEvent} mxEvent The event to test permission for
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given used ID can redact given event
*/
public maySendRedactionForEvent(mxEvent: MatrixEvent, userId: string): boolean {
const member = this.getMember(userId);
if (!member || member.membership === 'leave') return false;
if (mxEvent.status || mxEvent.isRedacted()) return false;
// The user may have been the sender, but they can't redact their own message
// if redactions are blocked.
const canRedact = this.maySendEvent(EventType.RoomRedaction, userId);
if (mxEvent.getSender() === userId) return canRedact;
return this.hasSufficientPowerLevelFor('redact', member.powerLevel);
}
/**
* Returns true if the given power level is sufficient for action
* @param {string} action The type of power level to check
* @param {number} powerLevel The power level of the member
* @return {boolean} true if the given power level is sufficient
*/
private hasSufficientPowerLevelFor(action: string, powerLevel: number): boolean {
const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
let powerLevels = {};
if (powerLevelsEvent) {
powerLevels = powerLevelsEvent.getContent();
}
let requiredLevel = 50;
if (utils.isNumber(powerLevels[action])) {
requiredLevel = powerLevels[action];
}
return powerLevel >= requiredLevel;
}
/**
* Short-form for maySendEvent('m.room.message', userId)
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* message events into the given room.
*/
public maySendMessage(userId: string): boolean {
return this.maySendEventOfType(EventType.RoomMessage, userId, false);
}
/**
* Returns true if the given user ID has permission to send a normal
* event of type `eventType` into this room.
* @param {string} eventType The type of event to test
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* the given type of event into this room,
* according to the room's state.
*/
public maySendEvent(eventType: EventType | string, userId: string): boolean {
return this.maySendEventOfType(eventType, userId, false);
}
/**
* Returns true if the given MatrixClient has permission to send a state
* event of type `stateEventType` into this room.
* @param {string} stateEventType The type of state events to test
* @param {MatrixClient} cli The client to test permission for
* @return {boolean} true if the given client should be permitted to send
* the given type of state event into this room,
* according to the room's state.
*/
public mayClientSendStateEvent(stateEventType: EventType | string, cli: MatrixClient): boolean {
if (cli.isGuest()) {
return false;
}
return this.maySendStateEvent(stateEventType, cli.credentials.userId);
}
/**
* Returns true if the given user ID has permission to send a state
* event of type `stateEventType` into this room.
* @param {string} stateEventType The type of state events to test
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* the given type of state event into this room,
* according to the room's state.
*/
public maySendStateEvent(stateEventType: EventType | string, userId: string): boolean {
return this.maySendEventOfType(stateEventType, userId, true);
}
/**
* Returns true if the given user ID has permission to send a normal or state
* event of type `eventType` into this room.
* @param {string} eventType The type of event to test
* @param {string} userId The user ID of the user to test permission for
* @param {boolean} state If true, tests if the user may send a state
event of this type. Otherwise tests whether
they may send a regular event.
* @return {boolean} true if the given user ID should be permitted to send
* the given type of event into this room,
* according to the room's state.
*/
private maySendEventOfType(eventType: EventType | string, userId: string, state: boolean): boolean {
const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, '');
let powerLevels;
let eventsLevels = {};
let stateDefault = 0;
let eventsDefault = 0;
let powerLevel = 0;
if (powerLevelsEvent) {
powerLevels = powerLevelsEvent.getContent();
eventsLevels = powerLevels.events || {};
if (Number.isFinite(powerLevels.state_default)) {
stateDefault = powerLevels.state_default;
} else {
stateDefault = 50;
}
const userPowerLevel = powerLevels.users && powerLevels.users[userId];
if (Number.isFinite(userPowerLevel)) {
powerLevel = userPowerLevel;
} else if (Number.isFinite(powerLevels.users_default)) {
powerLevel = powerLevels.users_default;
}
if (Number.isFinite(powerLevels.events_default)) {
eventsDefault = powerLevels.events_default;
}
}
let requiredLevel = state ? stateDefault : eventsDefault;
if (Number.isFinite(eventsLevels[eventType])) {
requiredLevel = eventsLevels[eventType];
}
return powerLevel >= requiredLevel;
}
/**
* Returns true if the given user ID has permission to trigger notification
* of type `notifLevelKey`
* @param {string} notifLevelKey The level of notification to test (eg. 'room')
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID has permission to trigger a
* notification of this type.
*/
public mayTriggerNotifOfType(notifLevelKey: string, userId: string): boolean {
const member = this.getMember(userId);
if (!member) {
return false;
}
const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, '');
let notifLevel = 50;
if (
powerLevelsEvent &&
powerLevelsEvent.getContent() &&
powerLevelsEvent.getContent().notifications &&
utils.isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey])
) {
notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey];
}
return member.powerLevel >= notifLevel;
}
/**
* Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
* @returns {string} the join_rule applied to this room
*/
public getJoinRule(): string {
const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, "");
const joinRuleContent = joinRuleEvent ? joinRuleEvent.getContent() : {};
return joinRuleContent["join_rule"] || "invite";
}
private updateThirdPartyTokenCache(memberEvent: MatrixEvent): void {
if (!memberEvent.getContent().third_party_invite) {
return;
}
const token = (memberEvent.getContent().third_party_invite.signed || {}).token;
if (!token) {
return;
}
const threePidInvite = this.getStateEvents(EventType.RoomThirdPartyInvite, token);
if (!threePidInvite) {
return;
}
this.tokenToInvite[token] = memberEvent;
}
private updateDisplayNameCache(userId: string, displayName: string): void {
const oldName = this.userIdsToDisplayNames[userId];
delete this.userIdsToDisplayNames[userId];
if (oldName) {
// Remove the old name from the cache.
// We clobber the user_id > name lookup but the name -> [user_id] lookup
// means we need to remove that user ID from that array rather than nuking
// the lot.
const strippedOldName = utils.removeHiddenChars(oldName);
const existingUserIds = this.displayNameToUserIds[strippedOldName];
if (existingUserIds) {
// remove this user ID from this array
const filteredUserIDs = existingUserIds.filter((id) => id !== userId);
this.displayNameToUserIds[strippedOldName] = filteredUserIDs;
}
}
this.userIdsToDisplayNames[userId] = displayName;
const strippedDisplayname = displayName && utils.removeHiddenChars(displayName);
// an empty stripped displayname (undefined/'') will be set to MXID in room-member.js
if (strippedDisplayname) {
if (!this.displayNameToUserIds[strippedDisplayname]) {
this.displayNameToUserIds[strippedDisplayname] = [];
}
this.displayNameToUserIds[strippedDisplayname].push(userId);
}
}
}
/**
* Fires whenever the event dictionary in room state is updated.
* @event module:client~MatrixClient#"RoomState.events"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.events dictionary
* was updated.
* @param {MatrixEvent} prevEvent The event being replaced by the new state, if
* known. Note that this can differ from `getPrevContent()` on the new state event
* as this is the store's view of the last state, not the previous state provided
* by the server.
* @example
* matrixClient.on("RoomState.events", function(event, state, prevEvent){
* var newStateEvent = event;
* });
*/
/**
* Fires whenever a member in the members dictionary is updated in any way.
* @event module:client~MatrixClient#"RoomState.members"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.members dictionary
* was updated.
* @param {RoomMember} member The room member that was updated.
* @example
* matrixClient.on("RoomState.members", function(event, state, member){
* var newMembershipState = member.membership;
* });
*/
/**
* Fires whenever a member is added to the members dictionary. The RoomMember
* will not be fully populated yet (e.g. no membership state) but will already
* be available in the members dictionary.
* @event module:client~MatrixClient#"RoomState.newMember"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.members dictionary
* was updated with a new entry.
* @param {RoomMember} member The room member that was added.
* @example
* matrixClient.on("RoomState.newMember", function(event, state, member){
* // add event listeners on 'member'
* });
*/

View File

@@ -18,12 +18,18 @@ limitations under the License.
* @module models/room-summary * @module models/room-summary
*/ */
export interface IRoomSummary {
"m.heroes": string[];
"m.joined_member_count": number;
"m.invited_member_count": number;
}
interface IInfo { interface IInfo {
title: string; title: string;
desc: string; desc?: string;
numMembers: number; numMembers?: number;
aliases: string[]; aliases?: string[];
timestamp: number; timestamp?: number;
} }
/** /**

File diff suppressed because it is too large Load Diff

2272
src/models/room.ts Normal file

File diff suppressed because it is too large Load Diff