matrix-js-sdk
Version:
Matrix Client-Server SDK for Javascript
1,386 lines (1,124 loc) • 82.2 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Room = exports.NotificationCountType = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _events = require("events");
var _eventTimelineSet = require("./event-timeline-set");
var _eventTimeline = require("./event-timeline");
var _contentRepo = require("../content-repo");
var utils = _interopRequireWildcard(require("../utils"));
var _event = require("./event");
var _roomMember = require("./room-member");
var _roomSummary = require("./room-summary");
var _logger = require("../logger");
var _ReEmitter = require("../ReEmitter");
var _event2 = require("../@types/event");
var _client = require("../client");
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
// These constants are used as sane defaults when the homeserver doesn't support
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
// the same as the common default room version whereas SAFE_ROOM_VERSIONS are the
// room versions which are considered okay for people to run without being asked
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
// return an m.room_versions capability.
const KNOWN_SAFE_ROOM_VERSION = '6';
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6'];
function synthesizeReceipt(userId, event, receiptType) {
// console.log("synthesizing receipt for "+event.getId());
// This is really ugly because JS has no way to express an object literal
// where the name of a key comes from an expression
const fakeReceipt = {
content: {},
type: "m.receipt",
room_id: event.getRoomId()
};
fakeReceipt.content[event.getId()] = {};
fakeReceipt.content[event.getId()][receiptType] = {};
fakeReceipt.content[event.getId()][receiptType][userId] = {
ts: event.getTs()
};
return new _event.MatrixEvent(fakeReceipt);
}
let NotificationCountType;
exports.NotificationCountType = NotificationCountType;
(function (NotificationCountType) {
NotificationCountType["Highlight"] = "highlight";
NotificationCountType["Total"] = "total";
})(NotificationCountType || (exports.NotificationCountType = NotificationCountType = {}));
class Room extends _events.EventEmitter {
// Pending in-flight requests { string: MatrixEvent }
// receipts should clobber based on receipt_type and user_id pairs hence
// the form of this structure. This is sub-optimal for the exposed APIs
// which pass in an event ID and get back some receipts, so we also store
// a pre-cached list for this purpose.
// { receipt_type: { user_id: IReceipt } }
// { event_id: IReceipt2[] }
// only receipts that came from the server, not synthesized ones
// any filtered timeline sets we're maintaining for this room
// filter_id: timelineSet
// read by megolm via getter; boolean value - null indicates "use global value"
// flags to stop logspam about missing m.room.create events
// XXX: These should be read-only
// $tagName: { $metadata: $value }
// $eventType: $event
// legacy fields
/**
* Construct a new Room.
*
* <p>For a room, we store an ordered sequence of timelines, which may or may not
* be continuous. Each timeline lists a series of events, as well as tracking
* the room state at the start and the end of the timeline. It also tracks
* forward and backward pagination tokens, as well as containing links to the
* next timeline in the sequence.
*
* <p>There is one special timeline - the 'live' timeline, which represents the
* timeline to which events are being added in real-time as they are received
* from the /sync API. Note that you should not retain references to this
* timeline - even if it is the current timeline right now, it may not remain
* so if the server gives us a timeline gap in /sync.
*
* <p>In order that we can find events from their ids later, we also maintain a
* map from event_id to timeline and index.
*
* @constructor
* @alias module:models/room
* @param {string} roomId Required. The ID of this room.
* @param {MatrixClient} client Required. The client, used to lazy load members.
* @param {string} myUserId Required. The ID of the syncing user.
* @param {Object=} opts Configuration options
* @param {*} opts.storageToken Optional. The token which a data store can use
* to remember the state of the room. What this means is dependent on the store
* implementation.
*
* @param {String=} opts.pendingEventOrdering Controls where pending messages
* appear in a room's timeline. If "<b>chronological</b>", messages will appear
* in the timeline when the call to <code>sendEvent</code> was made. If
* "<b>detached</b>", pending messages will appear in a separate list,
* accessible via {@link module:models/room#getPendingEvents}. Default:
* "chronological".
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
* timeline support.
* @param {boolean} [opts.unstableClientRelationAggregation = false]
* Optional. Set to true to enable client-side aggregation of event relations
* via `EventTimelineSet#getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
*
* @prop {string} roomId The ID of this room.
* @prop {string} name The human-readable display name for this room.
* @prop {string} normalizedName The un-homoglyphed name for this room.
* @prop {Array<MatrixEvent>} timeline The live event timeline for this room,
* with the oldest event at index 0. Present for backwards compatibility -
* prefer getLiveTimeline().getEvents().
* @prop {object} tags Dict of room tags; the keys are the tag name and the values
* are any metadata associated with the tag - e.g. { "fav" : { order: 1 } }
* @prop {object} accountData Dict of per-room account_data events; the keys are the
* event type and the values are the events.
* @prop {RoomState} oldState The state of the room at the time of the oldest
* event in the live timeline. Present for backwards compatibility -
* prefer getLiveTimeline().getState(EventTimeline.BACKWARDS).
* @prop {RoomState} currentState The state of the room at the time of the
* newest event in the timeline. Present for backwards compatibility -
* prefer getLiveTimeline().getState(EventTimeline.FORWARDS).
* @prop {RoomSummary} summary The room summary.
* @prop {*} storageToken A token which a data store can use to remember
* the state of the room.
*/
constructor(roomId, client, myUserId, opts = {}) {
super(); // In some cases, we add listeners for every displayed Matrix event, so it's
// common to have quite a few more than the default limit.
this.roomId = roomId;
this.client = client;
this.myUserId = myUserId;
this.opts = opts;
(0, _defineProperty2.default)(this, "reEmitter", void 0);
(0, _defineProperty2.default)(this, "txnToEvent", {});
(0, _defineProperty2.default)(this, "receipts", {});
(0, _defineProperty2.default)(this, "receiptCacheByEventId", {});
(0, _defineProperty2.default)(this, "realReceipts", {});
(0, _defineProperty2.default)(this, "notificationCounts", {});
(0, _defineProperty2.default)(this, "timelineSets", void 0);
(0, _defineProperty2.default)(this, "filteredTimelineSets", {});
(0, _defineProperty2.default)(this, "pendingEventList", void 0);
(0, _defineProperty2.default)(this, "blacklistUnverifiedDevices", null);
(0, _defineProperty2.default)(this, "selfMembership", null);
(0, _defineProperty2.default)(this, "summaryHeroes", null);
(0, _defineProperty2.default)(this, "getTypeWarning", false);
(0, _defineProperty2.default)(this, "getVersionWarning", false);
(0, _defineProperty2.default)(this, "membersPromise", void 0);
(0, _defineProperty2.default)(this, "name", void 0);
(0, _defineProperty2.default)(this, "normalizedName", void 0);
(0, _defineProperty2.default)(this, "tags", {});
(0, _defineProperty2.default)(this, "accountData", {});
(0, _defineProperty2.default)(this, "summary", null);
(0, _defineProperty2.default)(this, "storageToken", void 0);
(0, _defineProperty2.default)(this, "timeline", void 0);
(0, _defineProperty2.default)(this, "oldState", void 0);
(0, _defineProperty2.default)(this, "currentState", void 0);
this.setMaxListeners(100);
this.reEmitter = new _ReEmitter.ReEmitter(this);
opts.pendingEventOrdering = opts.pendingEventOrdering || _client.PendingEventOrdering.Chronological;
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
throw new Error("opts.pendingEventOrdering MUST be either 'chronological' or " + "'detached'. Got: '" + opts.pendingEventOrdering + "'");
}
this.name = roomId; // all our per-room timeline sets. the first one is the unfiltered ones;
// the subsequent ones are the filtered ones in no particular order.
this.timelineSets = [new _eventTimelineSet.EventTimelineSet(this, opts)];
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), ["Room.timeline", "Room.timelineReset"]);
this.fixUpLegacyTimelineFields();
if (this.opts.pendingEventOrdering == "detached") {
this.pendingEventList = [];
const serializedPendingEventList = client.sessionStore.store.getItem(pendingEventsKey(this.roomId));
if (serializedPendingEventList) {
JSON.parse(serializedPendingEventList).forEach(async serializedEvent => {
const event = new _event.MatrixEvent(serializedEvent);
if (event.getType() === _event2.EventType.RoomMessageEncrypted) {
await event.attemptDecryption(this.client.crypto);
}
event.setStatus(_event.EventStatus.NOT_SENT);
this.addPendingEvent(event, event.getTxnId());
});
}
} // awaited by getEncryptionTargetMembers while room members are loading
if (!this.opts.lazyLoadMembers) {
this.membersPromise = Promise.resolve(false);
} else {
this.membersPromise = null;
}
}
/**
* Bulk decrypt critical events in a room
*
* Critical events represents the minimal set of events to decrypt
* for a typical UI to function properly
*
* - Last event of every room (to generate likely message preview)
* - All events up to the read receipt (to calculate an accurate notification count)
*
* @returns {Promise} Signals when all events have been decrypted
*/
decryptCriticalEvents() {
const readReceiptEventId = this.getEventReadUpTo(this.client.getUserId(), true);
const events = this.getLiveTimeline().getEvents();
const readReceiptTimelineIndex = events.findIndex(matrixEvent => {
return matrixEvent.event.event_id === readReceiptEventId;
});
const decryptionPromises = events.slice(readReceiptTimelineIndex).filter(event => event.shouldAttemptDecryption()).reverse().map(event => event.attemptDecryption(this.client.crypto, {
isRetry: true
}));
return Promise.allSettled(decryptionPromises);
}
/**
* Bulk decrypt events in a room
*
* @returns {Promise} Signals when all events have been decrypted
*/
decryptAllEvents() {
const decryptionPromises = this.getUnfilteredTimelineSet().getLiveTimeline().getEvents().filter(event => event.shouldAttemptDecryption()).reverse().map(event => event.attemptDecryption(this.client.crypto, {
isRetry: true
}));
return Promise.allSettled(decryptionPromises);
}
/**
* Gets the version of the room
* @returns {string} The version of the room, or null if it could not be determined
*/
getVersion() {
const createEvent = this.currentState.getStateEvents(_event2.EventType.RoomCreate, "");
if (!createEvent) {
if (!this.getVersionWarning) {
_logger.logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event");
this.getVersionWarning = true;
}
return '1';
}
const ver = createEvent.getContent()['room_version'];
if (ver === undefined) return '1';
return ver;
}
/**
* Determines whether this room needs to be upgraded to a new version
* @returns {string?} What version the room should be upgraded to, or null if
* the room does not require upgrading at this time.
* @deprecated Use #getRecommendedVersion() instead
*/
shouldUpgradeToVersion() {
// TODO: Remove this function.
// This makes assumptions about which versions are safe, and can easily
// be wrong. Instead, people are encouraged to use getRecommendedVersion
// which determines a safer value. This function doesn't use that function
// because this is not async-capable, and to avoid breaking the contract
// we're deprecating this.
if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) {
return KNOWN_SAFE_ROOM_VERSION;
}
return null;
}
/**
* Determines the recommended room version for the room. This returns an
* object with 3 properties: <code>version</code> as the new version the
* room should be upgraded to (may be the same as the current version);
* <code>needsUpgrade</code> to indicate if the room actually can be
* upgraded (ie: does the current version not match?); and <code>urgent</code>
* to indicate if the new version patches a vulnerability in a previous
* version.
* @returns {Promise<{version: string, needsUpgrade: boolean, urgent: boolean}>}
* Resolves to the version the room should be upgraded to.
*/
async getRecommendedVersion() {
const capabilities = await this.client.getCapabilities();
let versionCap = capabilities["m.room_versions"];
if (!versionCap) {
versionCap = {
default: KNOWN_SAFE_ROOM_VERSION,
available: {}
};
for (const safeVer of SAFE_ROOM_VERSIONS) {
versionCap.available[safeVer] = _client.RoomVersionStability.Stable;
}
}
let result = this.checkVersionAgainstCapability(versionCap);
if (result.urgent && result.needsUpgrade) {
// Something doesn't feel right: we shouldn't need to update
// because the version we're on should be in the protocol's
// namespace. This usually means that the server was updated
// before the client was, making us think the newest possible
// room version is not stable. As a solution, we'll refresh
// the capability we're using to determine this.
_logger.logger.warn("Refreshing room version capability because the server looks " + "to be supporting a newer room version we don't know about.");
const caps = await this.client.getCapabilities(true);
versionCap = caps["m.room_versions"];
if (!versionCap) {
_logger.logger.warn("No room version capability - assuming upgrade required.");
return result;
} else {
result = this.checkVersionAgainstCapability(versionCap);
}
}
return result;
}
checkVersionAgainstCapability(versionCap) {
const currentVersion = this.getVersion();
_logger.logger.log(`[${this.roomId}] Current version: ${currentVersion}`);
_logger.logger.log(`[${this.roomId}] Version capability: `, versionCap);
const result = {
version: currentVersion,
needsUpgrade: false,
urgent: false
}; // If the room is on the default version then nothing needs to change
if (currentVersion === versionCap.default) return result;
const stableVersions = Object.keys(versionCap.available).filter(v => versionCap.available[v] === 'stable'); // Check if the room is on an unstable version. We determine urgency based
// off the version being in the Matrix spec namespace or not (if the version
// is in the current namespace and unstable, the room is probably vulnerable).
if (!stableVersions.includes(currentVersion)) {
result.version = versionCap.default;
result.needsUpgrade = true;
result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g);
if (result.urgent) {
_logger.logger.warn(`URGENT upgrade required on ${this.roomId}`);
} else {
_logger.logger.warn(`Non-urgent upgrade required on ${this.roomId}`);
}
return result;
} // The room is on a stable, but non-default, version by this point.
// No upgrade needed.
return result;
}
/**
* Determines whether the given user is permitted to perform a room upgrade
* @param {String} userId The ID of the user to test against
* @returns {boolean} True if the given user is permitted to upgrade the room
*/
userMayUpgradeRoom(userId) {
return this.currentState.maySendStateEvent(_event2.EventType.RoomTombstone, userId);
}
/**
* Get the list of pending sent events for this room
*
* @return {module:models/event.MatrixEvent[]} A list of the sent events
* waiting for remote echo.
*
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/
getPendingEvents() {
if (this.opts.pendingEventOrdering !== "detached") {
throw new Error("Cannot call getPendingEvents with pendingEventOrdering == " + this.opts.pendingEventOrdering);
}
return this.pendingEventList;
}
/**
* Removes a pending event for this room
*
* @param {string} eventId
* @return {boolean} True if an element was removed.
*/
removePendingEvent(eventId) {
if (this.opts.pendingEventOrdering !== "detached") {
throw new Error("Cannot call removePendingEvent with pendingEventOrdering == " + this.opts.pendingEventOrdering);
}
const removed = utils.removeElement(this.pendingEventList, function (ev) {
return ev.getId() == eventId;
}, false);
this.savePendingEvents();
return removed;
}
/**
* Check whether the pending event list contains a given event by ID.
* If pending event ordering is not "detached" then this returns false.
*
* @param {string} eventId The event ID to check for.
* @return {boolean}
*/
hasPendingEvent(eventId) {
if (this.opts.pendingEventOrdering !== "detached") {
return false;
}
return this.pendingEventList.some(event => event.getId() === eventId);
}
/**
* Get a specific event from the pending event list, if configured, null otherwise.
*
* @param {string} eventId The event ID to check for.
* @return {MatrixEvent}
*/
getPendingEvent(eventId) {
if (this.opts.pendingEventOrdering !== "detached") {
return null;
}
return this.pendingEventList.find(event => event.getId() === eventId);
}
/**
* Get the live unfiltered timeline for this room.
*
* @return {module:models/event-timeline~EventTimeline} live timeline
*/
getLiveTimeline() {
return this.getUnfilteredTimelineSet().getLiveTimeline();
}
/**
* Get the timestamp of the last message in the room
*
* @return {number} the timestamp of the last message in the room
*/
getLastActiveTimestamp() {
const timeline = this.getLiveTimeline();
const events = timeline.getEvents();
if (events.length) {
const lastEvent = events[events.length - 1];
return lastEvent.getTs();
} else {
return Number.MIN_SAFE_INTEGER;
}
}
/**
* @return {string} the membership type (join | leave | invite) for the logged in user
*/
getMyMembership() {
return this.selfMembership;
}
/**
* If this room is a DM we're invited to,
* try to find out who invited us
* @return {string} user id of the inviter
*/
getDMInviter() {
if (this.myUserId) {
const me = this.getMember(this.myUserId);
if (me) {
return me.getDMInviter();
}
}
if (this.selfMembership === "invite") {
// fall back to summary information
const memberCount = this.getInvitedAndJoinedMemberCount();
if (memberCount == 2 && this.summaryHeroes.length) {
return this.summaryHeroes[0];
}
}
}
/**
* Assuming this room is a DM room, tries to guess with which user.
* @return {string} user id of the other member (could be syncing user)
*/
guessDMUserId() {
const me = this.getMember(this.myUserId);
if (me) {
const inviterId = me.getDMInviter();
if (inviterId) {
return inviterId;
}
} // remember, we're assuming this room is a DM,
// so returning the first member we find should be fine
const hasHeroes = Array.isArray(this.summaryHeroes) && this.summaryHeroes.length;
if (hasHeroes) {
return this.summaryHeroes[0];
}
const members = this.currentState.getMembers();
const anyMember = members.find(m => m.userId !== this.myUserId);
if (anyMember) {
return anyMember.userId;
} // it really seems like I'm the only user in the room
// so I probably created a room with just me in it
// and marked it as a DM. Ok then
return this.myUserId;
}
getAvatarFallbackMember() {
const memberCount = this.getInvitedAndJoinedMemberCount();
if (memberCount > 2) {
return;
}
const hasHeroes = Array.isArray(this.summaryHeroes) && this.summaryHeroes.length;
if (hasHeroes) {
const availableMember = this.summaryHeroes.map(userId => {
return this.getMember(userId);
}).find(member => !!member);
if (availableMember) {
return availableMember;
}
}
const members = this.currentState.getMembers(); // could be different than memberCount
// as this includes left members
if (members.length <= 2) {
const availableMember = members.find(m => {
return m.userId !== this.myUserId;
});
if (availableMember) {
return availableMember;
}
} // if all else fails, try falling back to a user,
// and create a one-off member for it
if (hasHeroes) {
const availableUser = this.summaryHeroes.map(userId => {
return this.client.getUser(userId);
}).find(user => !!user);
if (availableUser) {
const member = new _roomMember.RoomMember(this.roomId, availableUser.userId);
member.user = availableUser;
return member;
}
}
}
/**
* Sets the membership this room was received as during sync
* @param {string} membership join | leave | invite
*/
updateMyMembership(membership) {
const prevMembership = this.selfMembership;
this.selfMembership = membership;
if (prevMembership !== membership) {
if (membership === "leave") {
this.cleanupAfterLeaving();
}
this.emit("Room.myMembership", this, membership, prevMembership);
}
}
async loadMembersFromServer() {
const lastSyncToken = this.client.store.getSyncToken();
const queryString = utils.encodeParams({
not_membership: "leave",
at: lastSyncToken
});
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, {
$roomId: this.roomId
});
const http = this.client.http;
const response = await http.authedRequest(undefined, "GET", path);
return response.chunk;
}
async loadMembers() {
// were the members loaded from the server?
let fromServer = false;
let rawMembersEvents = await this.client.store.getOutOfBandMembers(this.roomId);
if (rawMembersEvents === null) {
fromServer = true;
rawMembersEvents = await this.loadMembersFromServer();
_logger.logger.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`);
}
const memberEvents = rawMembersEvents.map(this.client.getEventMapper());
return {
memberEvents,
fromServer
};
}
/**
* Preloads the member list in case lazy loading
* of memberships is in use. Can be called multiple times,
* it will only preload once.
* @return {Promise} when preloading is done and
* accessing the members on the room will take
* all members in the room into account
*/
loadMembersIfNeeded() {
if (this.membersPromise) {
return this.membersPromise;
} // mark the state so that incoming messages while
// the request is in flight get marked as superseding
// the OOB members
this.currentState.markOutOfBandMembersStarted();
const inMemoryUpdate = this.loadMembers().then(result => {
this.currentState.setOutOfBandMembers(result.memberEvents); // now the members are loaded, start to track the e2e devices if needed
if (this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId)) {
this.client.crypto.trackRoomDevices(this.roomId);
}
return result.fromServer;
}).catch(err => {
// allow retries on fail
this.membersPromise = null;
this.currentState.markOutOfBandMembersFailed();
throw err;
}); // update members in storage, but don't wait for it
inMemoryUpdate.then(fromServer => {
if (fromServer) {
const oobMembers = this.currentState.getMembers().filter(m => m.isOutOfBand()).map(m => m.events.member.event);
_logger.logger.log(`LL: telling store to write ${oobMembers.length}` + ` members for room ${this.roomId}`);
const store = this.client.store;
return store.setOutOfBandMembers(this.roomId, oobMembers) // swallow any IDB error as we don't want to fail
// because of this
.catch(err => {
_logger.logger.log("LL: storing OOB room members failed, oh well", err);
});
}
}).catch(err => {
// as this is not awaited anywhere,
// at least show the error in the console
_logger.logger.error(err);
});
this.membersPromise = inMemoryUpdate;
return this.membersPromise;
}
/**
* Removes the lazily loaded members from storage if needed
*/
async clearLoadedMembersIfNeeded() {
if (this.opts.lazyLoadMembers && this.membersPromise) {
await this.loadMembersIfNeeded();
await this.client.store.clearOutOfBandMembers(this.roomId);
this.currentState.clearOutOfBandMembers();
this.membersPromise = null;
}
}
/**
* called when sync receives this room in the leave section
* to do cleanup after leaving a room. Possibly called multiple times.
*/
cleanupAfterLeaving() {
this.clearLoadedMembersIfNeeded().catch(err => {
_logger.logger.error(`error after clearing loaded members from ` + `room ${this.roomId} after leaving`);
_logger.logger.log(err);
});
}
/**
* Reset the live timeline of all timelineSets, and start new ones.
*
* <p>This is used when /sync returns a 'limited' timeline.
*
* @param {string=} backPaginationToken token for back-paginating the new timeline
* @param {string=} forwardPaginationToken token for forward-paginating the old live timeline,
* if absent or null, all timelines are reset, removing old ones (including the previous live
* timeline which would otherwise be unable to paginate forwards without this token).
* Removing just the old live timeline whilst preserving previous ones is not supported.
*/
resetLiveTimeline(backPaginationToken, forwardPaginationToken) {
for (let i = 0; i < this.timelineSets.length; i++) {
this.timelineSets[i].resetLiveTimeline(backPaginationToken, forwardPaginationToken);
}
this.fixUpLegacyTimelineFields();
}
/**
* Fix up this.timeline, this.oldState and this.currentState
*
* @private
*/
fixUpLegacyTimelineFields() {
// maintain this.timeline as a reference to the live timeline,
// and this.oldState and this.currentState as references to the
// state at the start and end of that timeline. These are more
// for backwards-compatibility than anything else.
this.timeline = this.getLiveTimeline().getEvents();
this.oldState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.BACKWARDS);
this.currentState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS);
}
/**
* Returns whether there are any devices in the room that are unverified
*
* Note: Callers should first check if crypto is enabled on this device. If it is
* disabled, then we aren't tracking room devices at all, so we can't answer this, and an
* error will be thrown.
*
* @return {boolean} the result
*/
async hasUnverifiedDevices() {
if (!this.client.isRoomEncrypted(this.roomId)) {
return false;
}
const e2eMembers = await this.getEncryptionTargetMembers();
for (const member of e2eMembers) {
const devices = this.client.getStoredDevicesForUser(member.userId);
if (devices.some(device => device.isUnverified())) {
return true;
}
}
return false;
}
/**
* Return the timeline sets for this room.
* @return {EventTimelineSet[]} array of timeline sets for this room
*/
getTimelineSets() {
return this.timelineSets;
}
/**
* Helper to return the main unfiltered timeline set for this room
* @return {EventTimelineSet} room's unfiltered timeline set
*/
getUnfilteredTimelineSet() {
return this.timelineSets[0];
}
/**
* Get the timeline which contains the given event from the unfiltered set, if any
*
* @param {string} eventId event ID to look for
* @return {?module:models/event-timeline~EventTimeline} timeline containing
* the given event, or null if unknown
*/
getTimelineForEvent(eventId) {
return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId);
}
/**
* Add a new timeline to this room's unfiltered timeline set
*
* @return {module:models/event-timeline~EventTimeline} newly-created timeline
*/
addTimeline() {
return this.getUnfilteredTimelineSet().addTimeline();
}
/**
* Get an event which is stored in our unfiltered timeline set
*
* @param {string} eventId event ID to look for
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
*/
findEventById(eventId) {
return this.getUnfilteredTimelineSet().findEventById(eventId);
}
/**
* Get one of the notification counts for this room
* @param {String} type The type of notification count to get. default: 'total'
* @return {Number} The notification count, or undefined if there is no count
* for this type.
*/
getUnreadNotificationCount(type = NotificationCountType.Total) {
return this.notificationCounts[type];
}
/**
* Set one of the notification counts for this room
* @param {String} type The type of notification count to set.
* @param {Number} count The new count
*/
setUnreadNotificationCount(type, count) {
this.notificationCounts[type] = count;
}
setSummary(summary) {
const heroes = summary["m.heroes"];
const joinedCount = summary["m.joined_member_count"];
const invitedCount = summary["m.invited_member_count"];
if (Number.isInteger(joinedCount)) {
this.currentState.setJoinedMemberCount(joinedCount);
}
if (Number.isInteger(invitedCount)) {
this.currentState.setInvitedMemberCount(invitedCount);
}
if (Array.isArray(heroes)) {
// be cautious about trusting server values,
// and make sure heroes doesn't contain our own id
// just to be sure
this.summaryHeroes = heroes.filter(userId => {
return userId !== this.myUserId;
});
}
}
/**
* Whether to send encrypted messages to devices within this room.
* @param {Boolean} value true to blacklist unverified devices, null
* to use the global value for this room.
*/
setBlacklistUnverifiedDevices(value) {
this.blacklistUnverifiedDevices = value;
}
/**
* Whether to send encrypted messages to devices within this room.
* @return {Boolean} true if blacklisting unverified devices, null
* if the global value should be used for this room.
*/
getBlacklistUnverifiedDevices() {
return this.blacklistUnverifiedDevices;
}
/**
* Get the avatar URL for a room if one was set.
* @param {String} baseUrl The homeserver base URL. See
* {@link module:client~MatrixClient#getHomeserverUrl}.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {boolean} allowDefault True to allow an identicon for this room if an
* avatar URL wasn't explicitly set. Default: true. (Deprecated)
* @return {?string} the avatar URL or null.
*/
getAvatarUrl(baseUrl, width, height, resizeMethod, allowDefault = true) {
const roomAvatarEvent = this.currentState.getStateEvents(_event2.EventType.RoomAvatar, "");
if (!roomAvatarEvent && !allowDefault) {
return null;
}
const mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null;
if (mainUrl) {
return (0, _contentRepo.getHttpUriForMxc)(baseUrl, mainUrl, width, height, resizeMethod);
}
return null;
}
/**
* Get the mxc avatar url for the room, if one was set.
* @return {string} the mxc avatar url or falsy
*/
getMxcAvatarUrl() {
var _this$currentState$ge, _this$currentState$ge2;
return ((_this$currentState$ge = this.currentState.getStateEvents(_event2.EventType.RoomAvatar, "")) === null || _this$currentState$ge === void 0 ? void 0 : (_this$currentState$ge2 = _this$currentState$ge.getContent()) === null || _this$currentState$ge2 === void 0 ? void 0 : _this$currentState$ge2.url) || null;
}
/**
* Get the aliases this room has according to the room's state
* The aliases returned by this function may not necessarily
* still point to this room.
* @return {array} The room's alias as an array of strings
*/
getAliases() {
const aliasStrings = [];
const aliasEvents = this.currentState.getStateEvents(_event2.EventType.RoomAliases);
if (aliasEvents) {
for (let i = 0; i < aliasEvents.length; ++i) {
const aliasEvent = aliasEvents[i];
if (Array.isArray(aliasEvent.getContent().aliases)) {
const filteredAliases = aliasEvent.getContent().aliases.filter(a => {
if (typeof a !== "string") return false;
if (a[0] !== '#') return false;
if (!a.endsWith(`:${aliasEvent.getStateKey()}`)) return false; // It's probably valid by here.
return true;
});
Array.prototype.push.apply(aliasStrings, filteredAliases);
}
}
}
return aliasStrings;
}
/**
* Get this room's canonical alias
* The alias returned by this function may not necessarily
* still point to this room.
* @return {?string} The room's canonical alias, or null if there is none
*/
getCanonicalAlias() {
const canonicalAlias = this.currentState.getStateEvents(_event2.EventType.RoomCanonicalAlias, "");
if (canonicalAlias) {
return canonicalAlias.getContent().alias || null;
}
return null;
}
/**
* Get this room's alternative aliases
* @return {array} The room's alternative aliases, or an empty array
*/
getAltAliases() {
const canonicalAlias = this.currentState.getStateEvents(_event2.EventType.RoomCanonicalAlias, "");
if (canonicalAlias) {
return canonicalAlias.getContent().alt_aliases || [];
}
return [];
}
/**
* Add events to a timeline
*
* <p>Will fire "Room.timeline" for each event added.
*
* @param {MatrixEvent[]} events A list of events to add.
*
* @param {boolean} toStartOfTimeline True to add these events to the start
* (oldest) instead of the end (newest) of the timeline. If true, the oldest
* event will be the <b>last</b> element of 'events'.
*
* @param {module:models/event-timeline~EventTimeline} timeline timeline to
* add events to.
*
* @param {string=} paginationToken token for the next batch of events
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*
*/
addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken) {
timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken);
}
/**
* Get a member from the current room state.
* @param {string} userId The user ID of the member.
* @return {RoomMember} The member or <code>null</code>.
*/
getMember(userId) {
return this.currentState.getMember(userId);
}
/**
* Get all currently loaded members from the current
* room state.
* @returns {RoomMember[]} Room members
*/
getMembers() {
return this.currentState.getMembers();
}
/**
* Get a list of members whose membership state is "join".
* @return {RoomMember[]} A list of currently joined members.
*/
getJoinedMembers() {
return this.getMembersWithMembership("join");
}
/**
* Returns the number of joined members in this room
* This method caches the result.
* This is a wrapper around the method of the same name in roomState, returning
* its result for the room's current state.
* @return {number} The number of members in this room whose membership is 'join'
*/
getJoinedMemberCount() {
return this.currentState.getJoinedMemberCount();
}
/**
* Returns the number of invited members in this room
* @return {number} The number of members in this room whose membership is 'invite'
*/
getInvitedMemberCount() {
return this.currentState.getInvitedMemberCount();
}
/**
* Returns the number of invited + joined members in this room
* @return {number} The number of members in this room whose membership is 'invite' or 'join'
*/
getInvitedAndJoinedMemberCount() {
return this.getInvitedMemberCount() + this.getJoinedMemberCount();
}
/**
* Get a list of members with given membership state.
* @param {string} membership The membership state.
* @return {RoomMember[]} A list of members with the given membership state.
*/
getMembersWithMembership(membership) {
return this.currentState.getMembers().filter(function (m) {
return m.membership === membership;
});
}
/**
* Get a list of members we should be encrypting for in this room
* @return {Promise<RoomMember[]>} A list of members who
* we should encrypt messages for in this room.
*/
async getEncryptionTargetMembers() {
await this.loadMembersIfNeeded();
let members = this.getMembersWithMembership("join");
if (this.shouldEncryptForInvitedMembers()) {
members = members.concat(this.getMembersWithMembership("invite"));
}
return members;
}
/**
* Determine whether we should encrypt messages for invited users in this room
* @return {boolean} if we should encrypt messages for invited users
*/
shouldEncryptForInvitedMembers() {
var _ev$getContent;
const ev = this.currentState.getStateEvents(_event2.EventType.RoomHistoryVisibility, "");
return (ev === null || ev === void 0 ? void 0 : (_ev$getContent = ev.getContent()) === null || _ev$getContent === void 0 ? void 0 : _ev$getContent.history_visibility) !== "joined";
}
/**
* Get the default room name (i.e. what a given user would see if the
* room had no m.room.name)
* @param {string} userId The userId from whose perspective we want
* to calculate the default name
* @return {string} The default room name
*/
getDefaultRoomName(userId) {
return this.calculateRoomName(userId, true);
}
/**
* Check if the given user_id has the given membership state.
* @param {string} userId The user ID to check.
* @param {string} membership The membership e.g. <code>'join'</code>
* @return {boolean} True if this user_id has the given membership state.
*/
hasMembershipState(userId, membership) {
const member = this.getMember(userId);
if (!member) {
return false;
}
return member.membership === membership;
}
/**
* Add a timelineSet for this room with the given filter
* @param {Filter} filter The filter to be applied to this timelineSet
* @return {EventTimelineSet} The timelineSet
*/
getOrCreateFilteredTimelineSet(filter) {
if (this.filteredTimelineSets[filter.filterId]) {
return this.filteredTimelineSets[filter.filterId];
}
const opts = Object.assign({
filter: filter
}, this.opts);
const timelineSet = new _eventTimelineSet.EventTimelineSet(this, opts);
this.reEmitter.reEmit(timelineSet, ["Room.timeline", "Room.timelineReset"]);
this.filteredTimelineSets[filter.filterId] = timelineSet;
this.timelineSets.push(timelineSet); // populate up the new timelineSet with filtered events from our live
// unfiltered timeline.
//
// XXX: This is risky as our timeline
// may have grown huge and so take a long time to filter.
// see https://github.com/vector-im/vector-web/issues/2109
const unfilteredLiveTimeline = this.getLiveTimeline();
unfilteredLiveTimeline.getEvents().forEach(function (event) {
timelineSet.addLiveEvent(event);
}); // find the earliest unfiltered timeline
let timeline = unfilteredLiveTimeline;
while (timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS)) {
timeline = timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS);
}
timelineSet.getLiveTimeline().setPaginationToken(timeline.getPaginationToken(_eventTimeline.EventTimeline.BACKWARDS), _eventTimeline.EventTimeline.BACKWARDS); // alternatively, we could try to do something like this to try and re-paginate
// in the filtered events from nothing, but Mark says it's an abuse of the API
// to do so:
//
// timelineSet.resetLiveTimeline(
// unfilteredLiveTimeline.getPaginationToken(EventTimeline.FORWARDS)
// );
return timelineSet;
}
/**
* Forget the timelineSet for this room with the given filter
*
* @param {Filter} filter the filter whose timelineSet is to be forgotten
*/
removeFilteredTimelineSet(filter) {
const timelineSet = this.filteredTimelineSets[filter.filterId];
delete this.filteredTimelineSets[filter.filterId];
const i = this.timelineSets.indexOf(timelineSet);
if (i > -1) {
this.timelineSets.splice(i, 1);
}
}
/**
* Add an event to the end of this room's live timelines. Will fire
* "Room.timeline".
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @param {boolean} fromCache whether the sync response came from cache
* @fires module:client~MatrixClient#event:"Room.timeline"
* @private
*/
addLiveEvent(event, duplicateStrategy, fromCache = false) {
if (event.isRedaction()) {
const redactId = event.event.redacts; // if we know about this event, redact its contents now.
const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
if (redactedEvent) {
redactedEvent.makeRedacted(event); // If this is in the current state, replace it with the redacted version
if (redactedEvent.getStateKey()) {
const currentStateEvent = this.currentState.getStateEvents(redactedEvent.getType(), redactedEvent.getStateKey());
if (currentStateEvent.getId() === redactedEvent.getId()) {
this.currentState.setStateEvents([redactedEvent]);
}
}
this.emit("Room.redaction", event, this); // TODO: we stash user displaynames (among other things) in
// RoomMember objects which are then attached to other events
// (in the sender and target fields). We should get those
// RoomMember objects to update themselves when the events that
// they are based on are changed.
} // FIXME: apply redactions to notification list
// NB: We continue to add the redaction event to the timeline so
// clients can say "so and so redacted an event" if they wish to. Also
// this may be needed to trigger an update.
}
if (event.getUnsigned().transaction_id) {
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
if (existingEvent) {
// remote echo of an event we sent earlier
this.handleRemoteEcho(event, existingEvent);
return;
}
} // add to our timeline sets
for (let i = 0; i < this.timelineSets.length; i++) {
this.timelineSets[i].addLiveEvent(event, duplicateStrategy, fromCache);
} // synthesize and inject implicit read receipts
// Done after adding the event because otherwise the app would get a read receipt
// pointing to an event that wasn't yet in the timeline
// Don't synthesize RR for m.room.redaction as this causes the RR to go missing.
if (event.sender && event.getType() !== _event2.EventType.RoomRedaction) {
this.addReceipt(synthesizeReceipt(event.sender.userId, event, "m.read"), true); // Any live events from a user could be taken as implicit
// presence information: evidence that they are currently active.
// ...except in a world where we use 'user.currentlyActive' to reduce
// presence spam, this isn't very useful - we'll get a transition when
// they are no longer currently active anyway. So don't bother to
// reset the lastActiveAgo and lastPresenceTs from the RoomState's user.
}
}
/**
* Add a pending outgoing event to this room.
*
* <p>The event is added to either the pendingEventList, or the live timeline,
* depending on the setting of opts.pendingEventOrdering.
*
* <p>This is an internal method, intended for use by MatrixClient.
*
* @param {module:models/event.MatrixEvent} event The event to add.
*
* @param {string} txnId Transaction id for this outgoing event
*
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
*
* @throws if the event doesn't have status SENDING, or we aren't given a
* unique transaction id.
*/
addPendingEvent(event, txnId) {
if (event.status !== _event.EventStatus.SENDING && event.status !== _event.EventStatus.NOT_SENT) {
throw new Error("addPendingEvent called on an event with status " + event.status);
}
if (this.txnToEvent[txnId]) {
throw new Error("addPendingEvent called on an event with known txnId " + txnId);
} // call setEventMetadata to set up event.sender etc
// as event is shared over all timelineSets, we set up its metadata based
// on the unfiltered timelineSet.
_eventTimeline.EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS), false);
this.txnToEvent[txnId] = event;
if (this.opts.pendingEventOrdering == "detached") {
if (this.pendingEventList.some(e => e.status === _event.EventStatus.NOT_SENT)) {
_logger.logger.warn("Setting event as NOT_SENT due to messages in the same state");
event.setStatus(_event.EventStatus.NOT_SENT);
}
this.pendingEventList.push(event);
this.savePendingEvents();
if (event.isRelation()) {
// For pending events, add them to the relations collection immediately.
// (The alternate case below already covers this as part of adding to
// the timeline set.)
this.aggregateNonLiveRelation(event);
}
if (event.isRedaction()) {
const redactId = event.event.redacts;
let redactedEvent = this.pendingEventList && this.pendingEventList.find(e => e.getId() === redactId);
if (!redactedEvent) {
redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
}
if (redactedEvent) {
redactedEvent.markLocallyRedacted(event);
this.emit("Room.redaction", event, this);
}
}
} else {
for (let