UNPKG

69.3 kBJavaScriptView Raw
1"use strict";
2
3var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
4
5Object.defineProperty(exports, "__esModule", {
6 value: true
7});
8exports.Room = Room;
9
10var _events = require("events");
11
12var _eventTimelineSet = require("./event-timeline-set");
13
14var _eventTimeline = require("./event-timeline");
15
16var _contentRepo = require("../content-repo");
17
18var utils = _interopRequireWildcard(require("../utils"));
19
20var _event = require("./event");
21
22var _roomMember = require("./room-member");
23
24var _roomSummary = require("./room-summary");
25
26var _logger = require("../logger");
27
28var _ReEmitter = require("../ReEmitter");
29
30/*
31Copyright 2015, 2016 OpenMarket Ltd
32Copyright 2018, 2019 New Vector Ltd
33Copyright 2019 The Matrix.org Foundation C.I.C.
34
35Licensed under the Apache License, Version 2.0 (the "License");
36you may not use this file except in compliance with the License.
37You may obtain a copy of the License at
38
39 http://www.apache.org/licenses/LICENSE-2.0
40
41Unless required by applicable law or agreed to in writing, software
42distributed under the License is distributed on an "AS IS" BASIS,
43WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44See the License for the specific language governing permissions and
45limitations under the License.
46*/
47
48/**
49 * @module models/room
50 */
51// These constants are used as sane defaults when the homeserver doesn't support
52// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
53// the same as the common default room version whereas SAFE_ROOM_VERSIONS are the
54// room versions which are considered okay for people to run without being asked
55// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
56// return an m.room_versions capability.
57const KNOWN_SAFE_ROOM_VERSION = '5';
58const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5'];
59
60function synthesizeReceipt(userId, event, receiptType) {
61 // console.log("synthesizing receipt for "+event.getId());
62 // This is really ugly because JS has no way to express an object literal
63 // where the name of a key comes from an expression
64 const fakeReceipt = {
65 content: {},
66 type: "m.receipt",
67 room_id: event.getRoomId()
68 };
69 fakeReceipt.content[event.getId()] = {};
70 fakeReceipt.content[event.getId()][receiptType] = {};
71 fakeReceipt.content[event.getId()][receiptType][userId] = {
72 ts: event.getTs()
73 };
74 return new _event.MatrixEvent(fakeReceipt);
75}
76/**
77 * Construct a new Room.
78 *
79 * <p>For a room, we store an ordered sequence of timelines, which may or may not
80 * be continuous. Each timeline lists a series of events, as well as tracking
81 * the room state at the start and the end of the timeline. It also tracks
82 * forward and backward pagination tokens, as well as containing links to the
83 * next timeline in the sequence.
84 *
85 * <p>There is one special timeline - the 'live' timeline, which represents the
86 * timeline to which events are being added in real-time as they are received
87 * from the /sync API. Note that you should not retain references to this
88 * timeline - even if it is the current timeline right now, it may not remain
89 * so if the server gives us a timeline gap in /sync.
90 *
91 * <p>In order that we can find events from their ids later, we also maintain a
92 * map from event_id to timeline and index.
93 *
94 * @constructor
95 * @alias module:models/room
96 * @param {string} roomId Required. The ID of this room.
97 * @param {MatrixClient} client Required. The client, used to lazy load members.
98 * @param {string} myUserId Required. The ID of the syncing user.
99 * @param {Object=} opts Configuration options
100 * @param {*} opts.storageToken Optional. The token which a data store can use
101 * to remember the state of the room. What this means is dependent on the store
102 * implementation.
103 *
104 * @param {String=} opts.pendingEventOrdering Controls where pending messages
105 * appear in a room's timeline. If "<b>chronological</b>", messages will appear
106 * in the timeline when the call to <code>sendEvent</code> was made. If
107 * "<b>detached</b>", pending messages will appear in a separate list,
108 * accessbile via {@link module:models/room#getPendingEvents}. Default:
109 * "chronological".
110 * @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
111 * timeline support.
112 * @param {boolean} [opts.unstableClientRelationAggregation = false]
113 * Optional. Set to true to enable client-side aggregation of event relations
114 * via `EventTimelineSet#getRelationsForEvent`.
115 * This feature is currently unstable and the API may change without notice.
116 *
117 * @prop {string} roomId The ID of this room.
118 * @prop {string} name The human-readable display name for this room.
119 * @prop {Array<MatrixEvent>} timeline The live event timeline for this room,
120 * with the oldest event at index 0. Present for backwards compatibility -
121 * prefer getLiveTimeline().getEvents().
122 * @prop {object} tags Dict of room tags; the keys are the tag name and the values
123 * are any metadata associated with the tag - e.g. { "fav" : { order: 1 } }
124 * @prop {object} accountData Dict of per-room account_data events; the keys are the
125 * event type and the values are the events.
126 * @prop {RoomState} oldState The state of the room at the time of the oldest
127 * event in the live timeline. Present for backwards compatibility -
128 * prefer getLiveTimeline().getState(EventTimeline.BACKWARDS).
129 * @prop {RoomState} currentState The state of the room at the time of the
130 * newest event in the timeline. Present for backwards compatibility -
131 * prefer getLiveTimeline().getState(EventTimeline.FORWARDS).
132 * @prop {RoomSummary} summary The room summary.
133 * @prop {*} storageToken A token which a data store can use to remember
134 * the state of the room.
135 */
136
137
138function Room(roomId, client, myUserId, opts) {
139 opts = opts || {};
140 opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
141 this.reEmitter = new _ReEmitter.ReEmitter(this);
142
143 if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
144 throw new Error("opts.pendingEventOrdering MUST be either 'chronological' or " + "'detached'. Got: '" + opts.pendingEventOrdering + "'");
145 }
146
147 this.myUserId = myUserId;
148 this.roomId = roomId;
149 this.name = roomId;
150 this.tags = {// $tagName: { $metadata: $value },
151 // $tagName: { $metadata: $value },
152 };
153 this.accountData = {// $eventType: $event
154 };
155 this.summary = null;
156 this.storageToken = opts.storageToken;
157 this._opts = opts;
158 this._txnToEvent = {}; // Pending in-flight requests { string: MatrixEvent }
159 // receipts should clobber based on receipt_type and user_id pairs hence
160 // the form of this structure. This is sub-optimal for the exposed APIs
161 // which pass in an event ID and get back some receipts, so we also store
162 // a pre-cached list for this purpose.
163
164 this._receipts = {// receipt_type: {
165 // user_id: {
166 // eventId: <event_id>,
167 // data: <receipt_data>
168 // }
169 // }
170 };
171 this._receiptCacheByEventId = {// $event_id: [{
172 // type: $type,
173 // userId: $user_id,
174 // data: <receipt data>
175 // }]
176 }; // only receipts that came from the server, not synthesized ones
177
178 this._realReceipts = {};
179 this._notificationCounts = {}; // all our per-room timeline sets. the first one is the unfiltered ones;
180 // the subsequent ones are the filtered ones in no particular order.
181
182 this._timelineSets = [new _eventTimelineSet.EventTimelineSet(this, opts)];
183 this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), ["Room.timeline", "Room.timelineReset"]);
184
185 this._fixUpLegacyTimelineFields(); // any filtered timeline sets we're maintaining for this room
186
187
188 this._filteredTimelineSets = {// filter_id: timelineSet
189 };
190
191 if (this._opts.pendingEventOrdering == "detached") {
192 this._pendingEventList = [];
193 } // read by megolm; boolean value - null indicates "use global value"
194
195
196 this._blacklistUnverifiedDevices = null;
197 this._selfMembership = null;
198 this._summaryHeroes = null; // awaited by getEncryptionTargetMembers while room members are loading
199
200 this._client = client;
201
202 if (!this._opts.lazyLoadMembers) {
203 this._membersPromise = Promise.resolve();
204 } else {
205 this._membersPromise = null;
206 }
207}
208
209utils.inherits(Room, _events.EventEmitter);
210/**
211 * Gets the version of the room
212 * @returns {string} The version of the room, or null if it could not be determined
213 */
214
215Room.prototype.getVersion = function () {
216 const createEvent = this.currentState.getStateEvents("m.room.create", "");
217
218 if (!createEvent) {
219 _logger.logger.warn("Room " + this.room_id + " does not have an m.room.create event");
220
221 return '1';
222 }
223
224 const ver = createEvent.getContent()['room_version'];
225 if (ver === undefined) return '1';
226 return ver;
227};
228/**
229 * Determines whether this room needs to be upgraded to a new version
230 * @returns {string?} What version the room should be upgraded to, or null if
231 * the room does not require upgrading at this time.
232 * @deprecated Use #getRecommendedVersion() instead
233 */
234
235
236Room.prototype.shouldUpgradeToVersion = function () {
237 // TODO: Remove this function.
238 // This makes assumptions about which versions are safe, and can easily
239 // be wrong. Instead, people are encouraged to use getRecommendedVersion
240 // which determines a safer value. This function doesn't use that function
241 // because this is not async-capable, and to avoid breaking the contract
242 // we're deprecating this.
243 if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) {
244 return KNOWN_SAFE_ROOM_VERSION;
245 }
246
247 return null;
248};
249/**
250 * Determines the recommended room version for the room. This returns an
251 * object with 3 properties: <code>version</code> as the new version the
252 * room should be upgraded to (may be the same as the current version);
253 * <code>needsUpgrade</code> to indicate if the room actually can be
254 * upgraded (ie: does the current version not match?); and <code>urgent</code>
255 * to indicate if the new version patches a vulnerability in a previous
256 * version.
257 * @returns {Promise<{version: string, needsUpgrade: bool, urgent: bool}>}
258 * Resolves to the version the room should be upgraded to.
259 */
260
261
262Room.prototype.getRecommendedVersion = async function () {
263 const capabilities = await this._client.getCapabilities();
264 let versionCap = capabilities["m.room_versions"];
265
266 if (!versionCap) {
267 versionCap = {
268 default: KNOWN_SAFE_ROOM_VERSION,
269 available: {}
270 };
271
272 for (const safeVer of SAFE_ROOM_VERSIONS) {
273 versionCap.available[safeVer] = "stable";
274 }
275 }
276
277 let result = this._checkVersionAgainstCapability(versionCap);
278
279 if (result.urgent && result.needsUpgrade) {
280 // Something doesn't feel right: we shouldn't need to update
281 // because the version we're on should be in the protocol's
282 // namespace. This usually means that the server was updated
283 // before the client was, making us think the newest possible
284 // room version is not stable. As a solution, we'll refresh
285 // the capability we're using to determine this.
286 _logger.logger.warn("Refreshing room version capability because the server looks " + "to be supporting a newer room version we don't know about.");
287
288 const caps = await this._client.getCapabilities(true);
289 versionCap = caps["m.room_versions"];
290
291 if (!versionCap) {
292 _logger.logger.warn("No room version capability - assuming upgrade required.");
293
294 return result;
295 } else {
296 result = this._checkVersionAgainstCapability(versionCap);
297 }
298 }
299
300 return result;
301};
302
303Room.prototype._checkVersionAgainstCapability = function (versionCap) {
304 const currentVersion = this.getVersion();
305
306 _logger.logger.log(`[${this.roomId}] Current version: ${currentVersion}`);
307
308 _logger.logger.log(`[${this.roomId}] Version capability: `, versionCap);
309
310 const result = {
311 version: currentVersion,
312 needsUpgrade: false,
313 urgent: false
314 }; // If the room is on the default version then nothing needs to change
315
316 if (currentVersion === versionCap.default) return result;
317 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
318 // off the version being in the Matrix spec namespace or not (if the version
319 // is in the current namespace and unstable, the room is probably vulnerable).
320
321 if (!stableVersions.includes(currentVersion)) {
322 result.version = versionCap.default;
323 result.needsUpgrade = true;
324 result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g);
325
326 if (result.urgent) {
327 _logger.logger.warn(`URGENT upgrade required on ${this.roomId}`);
328 } else {
329 _logger.logger.warn(`Non-urgent upgrade required on ${this.roomId}`);
330 }
331
332 return result;
333 } // The room is on a stable, but non-default, version by this point.
334 // No upgrade needed.
335
336
337 return result;
338};
339/**
340 * Determines whether the given user is permitted to perform a room upgrade
341 * @param {String} userId The ID of the user to test against
342 * @returns {bool} True if the given user is permitted to upgrade the room
343 */
344
345
346Room.prototype.userMayUpgradeRoom = function (userId) {
347 return this.currentState.maySendStateEvent("m.room.tombstone", userId);
348};
349/**
350 * Get the list of pending sent events for this room
351 *
352 * @return {module:models/event.MatrixEvent[]} A list of the sent events
353 * waiting for remote echo.
354 *
355 * @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
356 */
357
358
359Room.prototype.getPendingEvents = function () {
360 if (this._opts.pendingEventOrdering !== "detached") {
361 throw new Error("Cannot call getPendingEvents with pendingEventOrdering == " + this._opts.pendingEventOrdering);
362 }
363
364 return this._pendingEventList;
365};
366/**
367 * Check whether the pending event list contains a given event by ID.
368 *
369 * @param {string} eventId The event ID to check for.
370 * @return {boolean}
371 * @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
372 */
373
374
375Room.prototype.hasPendingEvent = function (eventId) {
376 if (this._opts.pendingEventOrdering !== "detached") {
377 throw new Error("Cannot call hasPendingEvent with pendingEventOrdering == " + this._opts.pendingEventOrdering);
378 }
379
380 return this._pendingEventList.some(event => event.getId() === eventId);
381};
382/**
383 * Get the live unfiltered timeline for this room.
384 *
385 * @return {module:models/event-timeline~EventTimeline} live timeline
386 */
387
388
389Room.prototype.getLiveTimeline = function () {
390 return this.getUnfilteredTimelineSet().getLiveTimeline();
391};
392/**
393 * Get the timestamp of the last message in the room
394 *
395 * @return {number} the timestamp of the last message in the room
396 */
397
398
399Room.prototype.getLastActiveTimestamp = function () {
400 const timeline = this.getLiveTimeline();
401 const events = timeline.getEvents();
402
403 if (events.length) {
404 const lastEvent = events[events.length - 1];
405 return lastEvent.getTs();
406 } else {
407 return Number.MIN_SAFE_INTEGER;
408 }
409};
410/**
411 * @param {string} myUserId the user id for the logged in member
412 * @return {string} the membership type (join | leave | invite) for the logged in user
413 */
414
415
416Room.prototype.getMyMembership = function () {
417 return this._selfMembership;
418};
419/**
420 * If this room is a DM we're invited to,
421 * try to find out who invited us
422 * @return {string} user id of the inviter
423 */
424
425
426Room.prototype.getDMInviter = function () {
427 if (this.myUserId) {
428 const me = this.getMember(this.myUserId);
429
430 if (me) {
431 return me.getDMInviter();
432 }
433 }
434
435 if (this._selfMembership === "invite") {
436 // fall back to summary information
437 const memberCount = this.getInvitedAndJoinedMemberCount();
438
439 if (memberCount == 2 && this._summaryHeroes.length) {
440 return this._summaryHeroes[0];
441 }
442 }
443};
444/**
445 * Assuming this room is a DM room, tries to guess with which user.
446 * @return {string} user id of the other member (could be syncing user)
447 */
448
449
450Room.prototype.guessDMUserId = function () {
451 const me = this.getMember(this.myUserId);
452
453 if (me) {
454 const inviterId = me.getDMInviter();
455
456 if (inviterId) {
457 return inviterId;
458 }
459 } // remember, we're assuming this room is a DM,
460 // so returning the first member we find should be fine
461
462
463 const hasHeroes = Array.isArray(this._summaryHeroes) && this._summaryHeroes.length;
464
465 if (hasHeroes) {
466 return this._summaryHeroes[0];
467 }
468
469 const members = this.currentState.getMembers();
470 const anyMember = members.find(m => m.userId !== this.myUserId);
471
472 if (anyMember) {
473 return anyMember.userId;
474 } // it really seems like I'm the only user in the room
475 // so I probably created a room with just me in it
476 // and marked it as a DM. Ok then
477
478
479 return this.myUserId;
480};
481
482Room.prototype.getAvatarFallbackMember = function () {
483 const memberCount = this.getInvitedAndJoinedMemberCount();
484
485 if (memberCount > 2) {
486 return;
487 }
488
489 const hasHeroes = Array.isArray(this._summaryHeroes) && this._summaryHeroes.length;
490
491 if (hasHeroes) {
492 const availableMember = this._summaryHeroes.map(userId => {
493 return this.getMember(userId);
494 }).find(member => !!member);
495
496 if (availableMember) {
497 return availableMember;
498 }
499 }
500
501 const members = this.currentState.getMembers(); // could be different than memberCount
502 // as this includes left members
503
504 if (members.length <= 2) {
505 const availableMember = members.find(m => {
506 return m.userId !== this.myUserId;
507 });
508
509 if (availableMember) {
510 return availableMember;
511 }
512 } // if all else fails, try falling back to a user,
513 // and create a one-off member for it
514
515
516 if (hasHeroes) {
517 const availableUser = this._summaryHeroes.map(userId => {
518 return this._client.getUser(userId);
519 }).find(user => !!user);
520
521 if (availableUser) {
522 const member = new _roomMember.RoomMember(this.roomId, availableUser.userId);
523 member.user = availableUser;
524 return member;
525 }
526 }
527};
528/**
529 * Sets the membership this room was received as during sync
530 * @param {string} membership join | leave | invite
531 */
532
533
534Room.prototype.updateMyMembership = function (membership) {
535 const prevMembership = this._selfMembership;
536 this._selfMembership = membership;
537
538 if (prevMembership !== membership) {
539 if (membership === "leave") {
540 this._cleanupAfterLeaving();
541 }
542
543 this.emit("Room.myMembership", this, membership, prevMembership);
544 }
545};
546
547Room.prototype._loadMembersFromServer = async function () {
548 const lastSyncToken = this._client.store.getSyncToken();
549
550 const queryString = utils.encodeParams({
551 not_membership: "leave",
552 at: lastSyncToken
553 });
554 const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, {
555 $roomId: this.roomId
556 });
557 const http = this._client._http;
558 const response = await http.authedRequest(undefined, "GET", path);
559 return response.chunk;
560};
561
562Room.prototype._loadMembers = async function () {
563 // were the members loaded from the server?
564 let fromServer = false;
565 let rawMembersEvents = await this._client.store.getOutOfBandMembers(this.roomId);
566
567 if (rawMembersEvents === null) {
568 fromServer = true;
569 rawMembersEvents = await this._loadMembersFromServer();
570
571 _logger.logger.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`);
572 }
573
574 const memberEvents = rawMembersEvents.map(this._client.getEventMapper());
575 return {
576 memberEvents,
577 fromServer
578 };
579};
580/**
581 * Preloads the member list in case lazy loading
582 * of memberships is in use. Can be called multiple times,
583 * it will only preload once.
584 * @return {Promise} when preloading is done and
585 * accessing the members on the room will take
586 * all members in the room into account
587 */
588
589
590Room.prototype.loadMembersIfNeeded = function () {
591 if (this._membersPromise) {
592 return this._membersPromise;
593 } // mark the state so that incoming messages while
594 // the request is in flight get marked as superseding
595 // the OOB members
596
597
598 this.currentState.markOutOfBandMembersStarted();
599
600 const inMemoryUpdate = this._loadMembers().then(result => {
601 this.currentState.setOutOfBandMembers(result.memberEvents); // now the members are loaded, start to track the e2e devices if needed
602
603 if (this._client.isCryptoEnabled() && this._client.isRoomEncrypted(this.roomId)) {
604 this._client._crypto.trackRoomDevices(this.roomId);
605 }
606
607 return result.fromServer;
608 }).catch(err => {
609 // allow retries on fail
610 this._membersPromise = null;
611 this.currentState.markOutOfBandMembersFailed();
612 throw err;
613 }); // update members in storage, but don't wait for it
614
615
616 inMemoryUpdate.then(fromServer => {
617 if (fromServer) {
618 const oobMembers = this.currentState.getMembers().filter(m => m.isOutOfBand()).map(m => m.events.member.event);
619
620 _logger.logger.log(`LL: telling store to write ${oobMembers.length}` + ` members for room ${this.roomId}`);
621
622 const store = this._client.store;
623 return store.setOutOfBandMembers(this.roomId, oobMembers) // swallow any IDB error as we don't want to fail
624 // because of this
625 .catch(err => {
626 _logger.logger.log("LL: storing OOB room members failed, oh well", err);
627 });
628 }
629 }).catch(err => {
630 // as this is not awaited anywhere,
631 // at least show the error in the console
632 _logger.logger.error(err);
633 });
634 this._membersPromise = inMemoryUpdate;
635 return this._membersPromise;
636};
637/**
638 * Removes the lazily loaded members from storage if needed
639 */
640
641
642Room.prototype.clearLoadedMembersIfNeeded = async function () {
643 if (this._opts.lazyLoadMembers && this._membersPromise) {
644 await this.loadMembersIfNeeded();
645 await this._client.store.clearOutOfBandMembers(this.roomId);
646 this.currentState.clearOutOfBandMembers();
647 this._membersPromise = null;
648 }
649};
650/**
651 * called when sync receives this room in the leave section
652 * to do cleanup after leaving a room. Possibly called multiple times.
653 */
654
655
656Room.prototype._cleanupAfterLeaving = function () {
657 this.clearLoadedMembersIfNeeded().catch(err => {
658 _logger.logger.error(`error after clearing loaded members from ` + `room ${this.roomId} after leaving`);
659
660 _logger.logger.log(err);
661 });
662};
663/**
664 * Reset the live timeline of all timelineSets, and start new ones.
665 *
666 * <p>This is used when /sync returns a 'limited' timeline.
667 *
668 * @param {string=} backPaginationToken token for back-paginating the new timeline
669 * @param {string=} forwardPaginationToken token for forward-paginating the old live timeline,
670 * if absent or null, all timelines are reset, removing old ones (including the previous live
671 * timeline which would otherwise be unable to paginate forwards without this token).
672 * Removing just the old live timeline whilst preserving previous ones is not supported.
673 */
674
675
676Room.prototype.resetLiveTimeline = function (backPaginationToken, forwardPaginationToken) {
677 for (let i = 0; i < this._timelineSets.length; i++) {
678 this._timelineSets[i].resetLiveTimeline(backPaginationToken, forwardPaginationToken);
679 }
680
681 this._fixUpLegacyTimelineFields();
682};
683/**
684 * Fix up this.timeline, this.oldState and this.currentState
685 *
686 * @private
687 */
688
689
690Room.prototype._fixUpLegacyTimelineFields = function () {
691 // maintain this.timeline as a reference to the live timeline,
692 // and this.oldState and this.currentState as references to the
693 // state at the start and end of that timeline. These are more
694 // for backwards-compatibility than anything else.
695 this.timeline = this.getLiveTimeline().getEvents();
696 this.oldState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.BACKWARDS);
697 this.currentState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS);
698};
699/**
700 * Returns whether there are any devices in the room that are unverified
701 *
702 * Note: Callers should first check if crypto is enabled on this device. If it is
703 * disabled, then we aren't tracking room devices at all, so we can't answer this, and an
704 * error will be thrown.
705 *
706 * @return {bool} the result
707 */
708
709
710Room.prototype.hasUnverifiedDevices = async function () {
711 if (!this._client.isRoomEncrypted(this.roomId)) {
712 return false;
713 }
714
715 const e2eMembers = await this.getEncryptionTargetMembers();
716
717 for (const member of e2eMembers) {
718 const devices = await this._client.getStoredDevicesForUser(member.userId);
719
720 if (devices.some(device => device.isUnverified())) {
721 return true;
722 }
723 }
724
725 return false;
726};
727/**
728 * Return the timeline sets for this room.
729 * @return {EventTimelineSet[]} array of timeline sets for this room
730 */
731
732
733Room.prototype.getTimelineSets = function () {
734 return this._timelineSets;
735};
736/**
737 * Helper to return the main unfiltered timeline set for this room
738 * @return {EventTimelineSet} room's unfiltered timeline set
739 */
740
741
742Room.prototype.getUnfilteredTimelineSet = function () {
743 return this._timelineSets[0];
744};
745/**
746 * Get the timeline which contains the given event from the unfiltered set, if any
747 *
748 * @param {string} eventId event ID to look for
749 * @return {?module:models/event-timeline~EventTimeline} timeline containing
750 * the given event, or null if unknown
751 */
752
753
754Room.prototype.getTimelineForEvent = function (eventId) {
755 return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId);
756};
757/**
758 * Add a new timeline to this room's unfiltered timeline set
759 *
760 * @return {module:models/event-timeline~EventTimeline} newly-created timeline
761 */
762
763
764Room.prototype.addTimeline = function () {
765 return this.getUnfilteredTimelineSet().addTimeline();
766};
767/**
768 * Get an event which is stored in our unfiltered timeline set
769 *
770 * @param {string} eventId event ID to look for
771 * @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
772 */
773
774
775Room.prototype.findEventById = function (eventId) {
776 return this.getUnfilteredTimelineSet().findEventById(eventId);
777};
778/**
779 * Get one of the notification counts for this room
780 * @param {String} type The type of notification count to get. default: 'total'
781 * @return {Number} The notification count, or undefined if there is no count
782 * for this type.
783 */
784
785
786Room.prototype.getUnreadNotificationCount = function (type) {
787 type = type || 'total';
788 return this._notificationCounts[type];
789};
790/**
791 * Set one of the notification counts for this room
792 * @param {String} type The type of notification count to set.
793 * @param {Number} count The new count
794 */
795
796
797Room.prototype.setUnreadNotificationCount = function (type, count) {
798 this._notificationCounts[type] = count;
799};
800
801Room.prototype.setSummary = function (summary) {
802 const heroes = summary["m.heroes"];
803 const joinedCount = summary["m.joined_member_count"];
804 const invitedCount = summary["m.invited_member_count"];
805
806 if (Number.isInteger(joinedCount)) {
807 this.currentState.setJoinedMemberCount(joinedCount);
808 }
809
810 if (Number.isInteger(invitedCount)) {
811 this.currentState.setInvitedMemberCount(invitedCount);
812 }
813
814 if (Array.isArray(heroes)) {
815 // be cautious about trusting server values,
816 // and make sure heroes doesn't contain our own id
817 // just to be sure
818 this._summaryHeroes = heroes.filter(userId => {
819 return userId !== this.myUserId;
820 });
821 }
822};
823/**
824 * Whether to send encrypted messages to devices within this room.
825 * @param {Boolean} value true to blacklist unverified devices, null
826 * to use the global value for this room.
827 */
828
829
830Room.prototype.setBlacklistUnverifiedDevices = function (value) {
831 this._blacklistUnverifiedDevices = value;
832};
833/**
834 * Whether to send encrypted messages to devices within this room.
835 * @return {Boolean} true if blacklisting unverified devices, null
836 * if the global value should be used for this room.
837 */
838
839
840Room.prototype.getBlacklistUnverifiedDevices = function () {
841 return this._blacklistUnverifiedDevices;
842};
843/**
844 * Get the avatar URL for a room if one was set.
845 * @param {String} baseUrl The homeserver base URL. See
846 * {@link module:client~MatrixClient#getHomeserverUrl}.
847 * @param {Number} width The desired width of the thumbnail.
848 * @param {Number} height The desired height of the thumbnail.
849 * @param {string} resizeMethod The thumbnail resize method to use, either
850 * "crop" or "scale".
851 * @param {boolean} allowDefault True to allow an identicon for this room if an
852 * avatar URL wasn't explicitly set. Default: true. (Deprecated)
853 * @return {?string} the avatar URL or null.
854 */
855
856
857Room.prototype.getAvatarUrl = function (baseUrl, width, height, resizeMethod, allowDefault) {
858 const roomAvatarEvent = this.currentState.getStateEvents("m.room.avatar", "");
859
860 if (allowDefault === undefined) {
861 allowDefault = true;
862 }
863
864 if (!roomAvatarEvent && !allowDefault) {
865 return null;
866 }
867
868 const mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null;
869
870 if (mainUrl) {
871 return (0, _contentRepo.getHttpUriForMxc)(baseUrl, mainUrl, width, height, resizeMethod);
872 } else if (allowDefault) {
873 return (0, _contentRepo.getIdenticonUri)(baseUrl, this.roomId, width, height);
874 }
875
876 return null;
877};
878/**
879 * Get the aliases this room has according to the room's state
880 * The aliases returned by this function may not necessarily
881 * still point to this room.
882 * @return {array} The room's alias as an array of strings
883 */
884
885
886Room.prototype.getAliases = function () {
887 const aliasStrings = [];
888 const aliasEvents = this.currentState.getStateEvents("m.room.aliases");
889
890 if (aliasEvents) {
891 for (let i = 0; i < aliasEvents.length; ++i) {
892 const aliasEvent = aliasEvents[i];
893
894 if (utils.isArray(aliasEvent.getContent().aliases)) {
895 const filteredAliases = aliasEvent.getContent().aliases.filter(a => {
896 if (typeof a !== "string") return false;
897 if (a[0] !== '#') return false;
898 if (!a.endsWith(`:${aliasEvent.getStateKey()}`)) return false; // It's probably valid by here.
899
900 return true;
901 });
902 Array.prototype.push.apply(aliasStrings, filteredAliases);
903 }
904 }
905 }
906
907 return aliasStrings;
908};
909/**
910 * Get this room's canonical alias
911 * The alias returned by this function may not necessarily
912 * still point to this room.
913 * @return {?string} The room's canonical alias, or null if there is none
914 */
915
916
917Room.prototype.getCanonicalAlias = function () {
918 const canonicalAlias = this.currentState.getStateEvents("m.room.canonical_alias", "");
919
920 if (canonicalAlias) {
921 return canonicalAlias.getContent().alias;
922 }
923
924 return null;
925};
926/**
927 * Add events to a timeline
928 *
929 * <p>Will fire "Room.timeline" for each event added.
930 *
931 * @param {MatrixEvent[]} events A list of events to add.
932 *
933 * @param {boolean} toStartOfTimeline True to add these events to the start
934 * (oldest) instead of the end (newest) of the timeline. If true, the oldest
935 * event will be the <b>last</b> element of 'events'.
936 *
937 * @param {module:models/event-timeline~EventTimeline} timeline timeline to
938 * add events to.
939 *
940 * @param {string=} paginationToken token for the next batch of events
941 *
942 * @fires module:client~MatrixClient#event:"Room.timeline"
943 *
944 */
945
946
947Room.prototype.addEventsToTimeline = function (events, toStartOfTimeline, timeline, paginationToken) {
948 timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken);
949};
950/**
951 * Get a member from the current room state.
952 * @param {string} userId The user ID of the member.
953 * @return {RoomMember} The member or <code>null</code>.
954 */
955
956
957Room.prototype.getMember = function (userId) {
958 return this.currentState.getMember(userId);
959};
960/**
961 * Get a list of members whose membership state is "join".
962 * @return {RoomMember[]} A list of currently joined members.
963 */
964
965
966Room.prototype.getJoinedMembers = function () {
967 return this.getMembersWithMembership("join");
968};
969/**
970 * Returns the number of joined members in this room
971 * This method caches the result.
972 * This is a wrapper around the method of the same name in roomState, returning
973 * its result for the room's current state.
974 * @return {integer} The number of members in this room whose membership is 'join'
975 */
976
977
978Room.prototype.getJoinedMemberCount = function () {
979 return this.currentState.getJoinedMemberCount();
980};
981/**
982 * Returns the number of invited members in this room
983 * @return {integer} The number of members in this room whose membership is 'invite'
984 */
985
986
987Room.prototype.getInvitedMemberCount = function () {
988 return this.currentState.getInvitedMemberCount();
989};
990/**
991 * Returns the number of invited + joined members in this room
992 * @return {integer} The number of members in this room whose membership is 'invite' or 'join'
993 */
994
995
996Room.prototype.getInvitedAndJoinedMemberCount = function () {
997 return this.getInvitedMemberCount() + this.getJoinedMemberCount();
998};
999/**
1000 * Get a list of members with given membership state.
1001 * @param {string} membership The membership state.
1002 * @return {RoomMember[]} A list of members with the given membership state.
1003 */
1004
1005
1006Room.prototype.getMembersWithMembership = function (membership) {
1007 return utils.filter(this.currentState.getMembers(), function (m) {
1008 return m.membership === membership;
1009 });
1010};
1011/**
1012 * Get a list of members we should be encrypting for in this room
1013 * @return {Promise<RoomMember[]>} A list of members who
1014 * we should encrypt messages for in this room.
1015 */
1016
1017
1018Room.prototype.getEncryptionTargetMembers = async function () {
1019 await this.loadMembersIfNeeded();
1020 let members = this.getMembersWithMembership("join");
1021
1022 if (this.shouldEncryptForInvitedMembers()) {
1023 members = members.concat(this.getMembersWithMembership("invite"));
1024 }
1025
1026 return members;
1027};
1028/**
1029 * Determine whether we should encrypt messages for invited users in this room
1030 * @return {boolean} if we should encrypt messages for invited users
1031 */
1032
1033
1034Room.prototype.shouldEncryptForInvitedMembers = function () {
1035 const ev = this.currentState.getStateEvents("m.room.history_visibility", "");
1036 return ev && ev.getContent() && ev.getContent().history_visibility !== "joined";
1037};
1038/**
1039 * Get the default room name (i.e. what a given user would see if the
1040 * room had no m.room.name)
1041 * @param {string} userId The userId from whose perspective we want
1042 * to calculate the default name
1043 * @return {string} The default room name
1044 */
1045
1046
1047Room.prototype.getDefaultRoomName = function (userId) {
1048 return calculateRoomName(this, userId, true);
1049};
1050/**
1051* Check if the given user_id has the given membership state.
1052* @param {string} userId The user ID to check.
1053* @param {string} membership The membership e.g. <code>'join'</code>
1054* @return {boolean} True if this user_id has the given membership state.
1055*/
1056
1057
1058Room.prototype.hasMembershipState = function (userId, membership) {
1059 const member = this.getMember(userId);
1060
1061 if (!member) {
1062 return false;
1063 }
1064
1065 return member.membership === membership;
1066};
1067/**
1068 * Add a timelineSet for this room with the given filter
1069 * @param {Filter} filter The filter to be applied to this timelineSet
1070 * @return {EventTimelineSet} The timelineSet
1071 */
1072
1073
1074Room.prototype.getOrCreateFilteredTimelineSet = function (filter) {
1075 if (this._filteredTimelineSets[filter.filterId]) {
1076 return this._filteredTimelineSets[filter.filterId];
1077 }
1078
1079 const opts = Object.assign({
1080 filter: filter
1081 }, this._opts);
1082 const timelineSet = new _eventTimelineSet.EventTimelineSet(this, opts);
1083 this.reEmitter.reEmit(timelineSet, ["Room.timeline", "Room.timelineReset"]);
1084 this._filteredTimelineSets[filter.filterId] = timelineSet;
1085
1086 this._timelineSets.push(timelineSet); // populate up the new timelineSet with filtered events from our live
1087 // unfiltered timeline.
1088 //
1089 // XXX: This is risky as our timeline
1090 // may have grown huge and so take a long time to filter.
1091 // see https://github.com/vector-im/vector-web/issues/2109
1092
1093
1094 const unfilteredLiveTimeline = this.getLiveTimeline();
1095 unfilteredLiveTimeline.getEvents().forEach(function (event) {
1096 timelineSet.addLiveEvent(event);
1097 }); // find the earliest unfiltered timeline
1098
1099 let timeline = unfilteredLiveTimeline;
1100
1101 while (timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS)) {
1102 timeline = timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS);
1103 }
1104
1105 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
1106 // in the filtered events from nothing, but Mark says it's an abuse of the API
1107 // to do so:
1108 //
1109 // timelineSet.resetLiveTimeline(
1110 // unfilteredLiveTimeline.getPaginationToken(EventTimeline.FORWARDS)
1111 // );
1112
1113 return timelineSet;
1114};
1115/**
1116 * Forget the timelineSet for this room with the given filter
1117 *
1118 * @param {Filter} filter the filter whose timelineSet is to be forgotten
1119 */
1120
1121
1122Room.prototype.removeFilteredTimelineSet = function (filter) {
1123 const timelineSet = this._filteredTimelineSets[filter.filterId];
1124 delete this._filteredTimelineSets[filter.filterId];
1125
1126 const i = this._timelineSets.indexOf(timelineSet);
1127
1128 if (i > -1) {
1129 this._timelineSets.splice(i, 1);
1130 }
1131};
1132/**
1133 * Add an event to the end of this room's live timelines. Will fire
1134 * "Room.timeline".
1135 *
1136 * @param {MatrixEvent} event Event to be added
1137 * @param {string?} duplicateStrategy 'ignore' or 'replace'
1138 * @fires module:client~MatrixClient#event:"Room.timeline"
1139 * @private
1140 */
1141
1142
1143Room.prototype._addLiveEvent = function (event, duplicateStrategy) {
1144 if (event.isRedaction()) {
1145 const redactId = event.event.redacts; // if we know about this event, redact its contents now.
1146
1147 const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
1148
1149 if (redactedEvent) {
1150 redactedEvent.makeRedacted(event); // If this is in the current state, replace it with the redacted version
1151
1152 if (redactedEvent.getStateKey()) {
1153 const currentStateEvent = this.currentState.getStateEvents(redactedEvent.getType(), redactedEvent.getStateKey());
1154
1155 if (currentStateEvent.getId() === redactedEvent.getId()) {
1156 this.currentState.setStateEvents([redactedEvent]);
1157 }
1158 }
1159
1160 this.emit("Room.redaction", event, this); // TODO: we stash user displaynames (among other things) in
1161 // RoomMember objects which are then attached to other events
1162 // (in the sender and target fields). We should get those
1163 // RoomMember objects to update themselves when the events that
1164 // they are based on are changed.
1165 } // FIXME: apply redactions to notification list
1166 // NB: We continue to add the redaction event to the timeline so
1167 // clients can say "so and so redacted an event" if they wish to. Also
1168 // this may be needed to trigger an update.
1169
1170 }
1171
1172 if (event.getUnsigned().transaction_id) {
1173 const existingEvent = this._txnToEvent[event.getUnsigned().transaction_id];
1174
1175 if (existingEvent) {
1176 // remote echo of an event we sent earlier
1177 this._handleRemoteEcho(event, existingEvent);
1178
1179 return;
1180 }
1181 } // add to our timeline sets
1182
1183
1184 for (let i = 0; i < this._timelineSets.length; i++) {
1185 this._timelineSets[i].addLiveEvent(event, duplicateStrategy);
1186 } // synthesize and inject implicit read receipts
1187 // Done after adding the event because otherwise the app would get a read receipt
1188 // pointing to an event that wasn't yet in the timeline
1189 // Don't synthesize RR for m.room.redaction as this causes the RR to go missing.
1190
1191
1192 if (event.sender && event.getType() !== "m.room.redaction") {
1193 this.addReceipt(synthesizeReceipt(event.sender.userId, event, "m.read"), true); // Any live events from a user could be taken as implicit
1194 // presence information: evidence that they are currently active.
1195 // ...except in a world where we use 'user.currentlyActive' to reduce
1196 // presence spam, this isn't very useful - we'll get a transition when
1197 // they are no longer currently active anyway. So don't bother to
1198 // reset the lastActiveAgo and lastPresenceTs from the RoomState's user.
1199 }
1200};
1201/**
1202 * Add a pending outgoing event to this room.
1203 *
1204 * <p>The event is added to either the pendingEventList, or the live timeline,
1205 * depending on the setting of opts.pendingEventOrdering.
1206 *
1207 * <p>This is an internal method, intended for use by MatrixClient.
1208 *
1209 * @param {module:models/event.MatrixEvent} event The event to add.
1210 *
1211 * @param {string} txnId Transaction id for this outgoing event
1212 *
1213 * @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
1214 *
1215 * @throws if the event doesn't have status SENDING, or we aren't given a
1216 * unique transaction id.
1217 */
1218
1219
1220Room.prototype.addPendingEvent = function (event, txnId) {
1221 if (event.status !== _event.EventStatus.SENDING) {
1222 throw new Error("addPendingEvent called on an event with status " + event.status);
1223 }
1224
1225 if (this._txnToEvent[txnId]) {
1226 throw new Error("addPendingEvent called on an event with known txnId " + txnId);
1227 } // call setEventMetadata to set up event.sender etc
1228 // as event is shared over all timelineSets, we set up its metadata based
1229 // on the unfiltered timelineSet.
1230
1231
1232 _eventTimeline.EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS), false);
1233
1234 this._txnToEvent[txnId] = event;
1235
1236 if (this._opts.pendingEventOrdering == "detached") {
1237 if (this._pendingEventList.some(e => e.status === _event.EventStatus.NOT_SENT)) {
1238 _logger.logger.warn("Setting event as NOT_SENT due to messages in the same state");
1239
1240 event.setStatus(_event.EventStatus.NOT_SENT);
1241 }
1242
1243 this._pendingEventList.push(event);
1244
1245 if (event.isRelation()) {
1246 // For pending events, add them to the relations collection immediately.
1247 // (The alternate case below already covers this as part of adding to
1248 // the timeline set.)
1249 this._aggregateNonLiveRelation(event);
1250 }
1251
1252 if (event.isRedaction()) {
1253 const redactId = event.event.redacts;
1254
1255 let redactedEvent = this._pendingEventList && this._pendingEventList.find(e => e.getId() === redactId);
1256
1257 if (!redactedEvent) {
1258 redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
1259 }
1260
1261 if (redactedEvent) {
1262 redactedEvent.markLocallyRedacted(event);
1263 this.emit("Room.redaction", event, this);
1264 }
1265 }
1266 } else {
1267 for (let i = 0; i < this._timelineSets.length; i++) {
1268 const timelineSet = this._timelineSets[i];
1269
1270 if (timelineSet.getFilter()) {
1271 if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
1272 timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), false);
1273 }
1274 } else {
1275 timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), false);
1276 }
1277 }
1278 }
1279
1280 this.emit("Room.localEchoUpdated", event, this, null, null);
1281};
1282/**
1283 * Used to aggregate the local echo for a relation, and also
1284 * for re-applying a relation after it's redaction has been cancelled,
1285 * as the local echo for the redaction of the relation would have
1286 * un-aggregated the relation. Note that this is different from regular messages,
1287 * which are just kept detached for their local echo.
1288 *
1289 * Also note that live events are aggregated in the live EventTimelineSet.
1290 * @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated.
1291 */
1292
1293
1294Room.prototype._aggregateNonLiveRelation = function (event) {
1295 // TODO: We should consider whether this means it would be a better
1296 // design to lift the relations handling up to the room instead.
1297 for (let i = 0; i < this._timelineSets.length; i++) {
1298 const timelineSet = this._timelineSets[i];
1299
1300 if (timelineSet.getFilter()) {
1301 if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
1302 timelineSet.aggregateRelations(event);
1303 }
1304 } else {
1305 timelineSet.aggregateRelations(event);
1306 }
1307 }
1308};
1309/**
1310 * Deal with the echo of a message we sent.
1311 *
1312 * <p>We move the event to the live timeline if it isn't there already, and
1313 * update it.
1314 *
1315 * @param {module:models/event.MatrixEvent} remoteEvent The event received from
1316 * /sync
1317 * @param {module:models/event.MatrixEvent} localEvent The local echo, which
1318 * should be either in the _pendingEventList or the timeline.
1319 *
1320 * @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
1321 * @private
1322 */
1323
1324
1325Room.prototype._handleRemoteEcho = function (remoteEvent, localEvent) {
1326 const oldEventId = localEvent.getId();
1327 const newEventId = remoteEvent.getId();
1328 const oldStatus = localEvent.status; // no longer pending
1329
1330 delete this._txnToEvent[remoteEvent.getUnsigned().transaction_id]; // if it's in the pending list, remove it
1331
1332 if (this._pendingEventList) {
1333 utils.removeElement(this._pendingEventList, function (ev) {
1334 return ev.getId() == oldEventId;
1335 }, false);
1336 } // replace the event source (this will preserve the plaintext payload if
1337 // any, which is good, because we don't want to try decoding it again).
1338
1339
1340 localEvent.handleRemoteEcho(remoteEvent.event);
1341
1342 for (let i = 0; i < this._timelineSets.length; i++) {
1343 const timelineSet = this._timelineSets[i]; // if it's already in the timeline, update the timeline map. If it's not, add it.
1344
1345 timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
1346 }
1347
1348 this.emit("Room.localEchoUpdated", localEvent, this, oldEventId, oldStatus);
1349};
1350/* a map from current event status to a list of allowed next statuses
1351 */
1352
1353
1354const ALLOWED_TRANSITIONS = {};
1355ALLOWED_TRANSITIONS[_event.EventStatus.ENCRYPTING] = [_event.EventStatus.SENDING, _event.EventStatus.NOT_SENT];
1356ALLOWED_TRANSITIONS[_event.EventStatus.SENDING] = [_event.EventStatus.ENCRYPTING, _event.EventStatus.QUEUED, _event.EventStatus.NOT_SENT, _event.EventStatus.SENT];
1357ALLOWED_TRANSITIONS[_event.EventStatus.QUEUED] = [_event.EventStatus.SENDING, _event.EventStatus.CANCELLED];
1358ALLOWED_TRANSITIONS[_event.EventStatus.SENT] = [];
1359ALLOWED_TRANSITIONS[_event.EventStatus.NOT_SENT] = [_event.EventStatus.SENDING, _event.EventStatus.QUEUED, _event.EventStatus.CANCELLED];
1360ALLOWED_TRANSITIONS[_event.EventStatus.CANCELLED] = [];
1361/**
1362 * Update the status / event id on a pending event, to reflect its transmission
1363 * progress.
1364 *
1365 * <p>This is an internal method.
1366 *
1367 * @param {MatrixEvent} event local echo event
1368 * @param {EventStatus} newStatus status to assign
1369 * @param {string} newEventId new event id to assign. Ignored unless
1370 * newStatus == EventStatus.SENT.
1371 * @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
1372 */
1373
1374Room.prototype.updatePendingEvent = function (event, newStatus, newEventId) {
1375 _logger.logger.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()}`); // if the message was sent, we expect an event id
1376
1377
1378 if (newStatus == _event.EventStatus.SENT && !newEventId) {
1379 throw new Error("updatePendingEvent called with status=SENT, " + "but no new event id");
1380 } // SENT races against /sync, so we have to special-case it.
1381
1382
1383 if (newStatus == _event.EventStatus.SENT) {
1384 const timeline = this.getUnfilteredTimelineSet().eventIdToTimeline(newEventId);
1385
1386 if (timeline) {
1387 // we've already received the event via the event stream.
1388 // nothing more to do here.
1389 return;
1390 }
1391 }
1392
1393 const oldStatus = event.status;
1394 const oldEventId = event.getId();
1395
1396 if (!oldStatus) {
1397 throw new Error("updatePendingEventStatus called on an event which is " + "not a local echo.");
1398 }
1399
1400 const allowed = ALLOWED_TRANSITIONS[oldStatus];
1401
1402 if (!allowed || allowed.indexOf(newStatus) < 0) {
1403 throw new Error("Invalid EventStatus transition " + oldStatus + "->" + newStatus);
1404 }
1405
1406 event.setStatus(newStatus);
1407
1408 if (newStatus == _event.EventStatus.SENT) {
1409 // update the event id
1410 event.replaceLocalEventId(newEventId); // if the event was already in the timeline (which will be the case if
1411 // opts.pendingEventOrdering==chronological), we need to update the
1412 // timeline map.
1413
1414 for (let i = 0; i < this._timelineSets.length; i++) {
1415 this._timelineSets[i].replaceEventId(oldEventId, newEventId);
1416 }
1417 } else if (newStatus == _event.EventStatus.CANCELLED) {
1418 // remove it from the pending event list, or the timeline.
1419 if (this._pendingEventList) {
1420 const idx = this._pendingEventList.findIndex(ev => ev.getId() === oldEventId);
1421
1422 if (idx !== -1) {
1423 const [removedEvent] = this._pendingEventList.splice(idx, 1);
1424
1425 if (removedEvent.isRedaction()) {
1426 this._revertRedactionLocalEcho(removedEvent);
1427 }
1428 }
1429 }
1430
1431 this.removeEvent(oldEventId);
1432 }
1433
1434 this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus);
1435};
1436
1437Room.prototype._revertRedactionLocalEcho = function (redactionEvent) {
1438 const redactId = redactionEvent.event.redacts;
1439
1440 if (!redactId) {
1441 return;
1442 }
1443
1444 const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
1445
1446 if (redactedEvent) {
1447 redactedEvent.unmarkLocallyRedacted(); // re-render after undoing redaction
1448
1449 this.emit("Room.redactionCancelled", redactionEvent, this); // reapply relation now redaction failed
1450
1451 if (redactedEvent.isRelation()) {
1452 this._aggregateNonLiveRelation(redactedEvent);
1453 }
1454 }
1455};
1456/**
1457 * Add some events to this room. This can include state events, message
1458 * events and typing notifications. These events are treated as "live" so
1459 * they will go to the end of the timeline.
1460 *
1461 * @param {MatrixEvent[]} events A list of events to add.
1462 *
1463 * @param {string} duplicateStrategy Optional. Applies to events in the
1464 * timeline only. If this is 'replace' then if a duplicate is encountered, the
1465 * event passed to this function will replace the existing event in the
1466 * timeline. If this is not specified, or is 'ignore', then the event passed to
1467 * this function will be ignored entirely, preserving the existing event in the
1468 * timeline. Events are identical based on their event ID <b>only</b>.
1469 *
1470 * @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
1471 */
1472
1473
1474Room.prototype.addLiveEvents = function (events, duplicateStrategy) {
1475 let i;
1476
1477 if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
1478 throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
1479 } // sanity check that the live timeline is still live
1480
1481
1482 for (i = 0; i < this._timelineSets.length; i++) {
1483 const liveTimeline = this._timelineSets[i].getLiveTimeline();
1484
1485 if (liveTimeline.getPaginationToken(_eventTimeline.EventTimeline.FORWARDS)) {
1486 throw new Error("live timeline " + i + " is no longer live - it has a pagination token " + "(" + liveTimeline.getPaginationToken(_eventTimeline.EventTimeline.FORWARDS) + ")");
1487 }
1488
1489 if (liveTimeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.FORWARDS)) {
1490 throw new Error("live timeline " + i + " is no longer live - " + "it has a neighbouring timeline");
1491 }
1492 }
1493
1494 for (i = 0; i < events.length; i++) {
1495 // TODO: We should have a filter to say "only add state event
1496 // types X Y Z to the timeline".
1497 this._addLiveEvent(events[i], duplicateStrategy);
1498 }
1499};
1500/**
1501 * Adds/handles ephemeral events such as typing notifications and read receipts.
1502 * @param {MatrixEvent[]} events A list of events to process
1503 */
1504
1505
1506Room.prototype.addEphemeralEvents = function (events) {
1507 for (const event of events) {
1508 if (event.getType() === 'm.typing') {
1509 this.currentState.setTypingEvent(event);
1510 } else if (event.getType() === 'm.receipt') {
1511 this.addReceipt(event);
1512 } // else ignore - life is too short for us to care about these events
1513
1514 }
1515};
1516/**
1517 * Removes events from this room.
1518 * @param {String[]} eventIds A list of eventIds to remove.
1519 */
1520
1521
1522Room.prototype.removeEvents = function (eventIds) {
1523 for (let i = 0; i < eventIds.length; ++i) {
1524 this.removeEvent(eventIds[i]);
1525 }
1526};
1527/**
1528 * Removes a single event from this room.
1529 *
1530 * @param {String} eventId The id of the event to remove
1531 *
1532 * @return {bool} true if the event was removed from any of the room's timeline sets
1533 */
1534
1535
1536Room.prototype.removeEvent = function (eventId) {
1537 let removedAny = false;
1538
1539 for (let i = 0; i < this._timelineSets.length; i++) {
1540 const removed = this._timelineSets[i].removeEvent(eventId);
1541
1542 if (removed) {
1543 if (removed.isRedaction()) {
1544 this._revertRedactionLocalEcho(removed);
1545 }
1546
1547 removedAny = true;
1548 }
1549 }
1550
1551 return removedAny;
1552};
1553/**
1554 * Recalculate various aspects of the room, including the room name and
1555 * room summary. Call this any time the room's current state is modified.
1556 * May fire "Room.name" if the room name is updated.
1557 * @fires module:client~MatrixClient#event:"Room.name"
1558 */
1559
1560
1561Room.prototype.recalculate = function () {
1562 // set fake stripped state events if this is an invite room so logic remains
1563 // consistent elsewhere.
1564 const self = this;
1565 const membershipEvent = this.currentState.getStateEvents("m.room.member", this.myUserId);
1566
1567 if (membershipEvent && membershipEvent.getContent().membership === "invite") {
1568 const strippedStateEvents = membershipEvent.event.invite_room_state || [];
1569 utils.forEach(strippedStateEvents, function (strippedEvent) {
1570 const existingEvent = self.currentState.getStateEvents(strippedEvent.type, strippedEvent.state_key);
1571
1572 if (!existingEvent) {
1573 // set the fake stripped event instead
1574 self.currentState.setStateEvents([new _event.MatrixEvent({
1575 type: strippedEvent.type,
1576 state_key: strippedEvent.state_key,
1577 content: strippedEvent.content,
1578 event_id: "$fake" + Date.now(),
1579 room_id: self.roomId,
1580 user_id: self.myUserId // technically a lie
1581
1582 })]);
1583 }
1584 });
1585 }
1586
1587 const oldName = this.name;
1588 this.name = calculateRoomName(this, this.myUserId);
1589 this.summary = new _roomSummary.RoomSummary(this.roomId, {
1590 title: this.name
1591 });
1592
1593 if (oldName !== this.name) {
1594 this.emit("Room.name", this);
1595 }
1596};
1597/**
1598 * Get a list of user IDs who have <b>read up to</b> the given event.
1599 * @param {MatrixEvent} event the event to get read receipts for.
1600 * @return {String[]} A list of user IDs.
1601 */
1602
1603
1604Room.prototype.getUsersReadUpTo = function (event) {
1605 return this.getReceiptsForEvent(event).filter(function (receipt) {
1606 return receipt.type === "m.read";
1607 }).map(function (receipt) {
1608 return receipt.userId;
1609 });
1610};
1611/**
1612 * Get the ID of the event that a given user has read up to, or null if we
1613 * have received no read receipts from them.
1614 * @param {String} userId The user ID to get read receipt event ID for
1615 * @param {Boolean} ignoreSynthesized If true, return only receipts that have been
1616 * sent by the server, not implicit ones generated
1617 * by the JS SDK.
1618 * @return {String} ID of the latest event that the given user has read, or null.
1619 */
1620
1621
1622Room.prototype.getEventReadUpTo = function (userId, ignoreSynthesized) {
1623 let receipts = this._receipts;
1624
1625 if (ignoreSynthesized) {
1626 receipts = this._realReceipts;
1627 }
1628
1629 if (receipts["m.read"] === undefined || receipts["m.read"][userId] === undefined) {
1630 return null;
1631 }
1632
1633 return receipts["m.read"][userId].eventId;
1634};
1635/**
1636 * Determines if the given user has read a particular event ID with the known
1637 * history of the room. This is not a definitive check as it relies only on
1638 * what is available to the room at the time of execution.
1639 * @param {String} userId The user ID to check the read state of.
1640 * @param {String} eventId The event ID to check if the user read.
1641 * @returns {Boolean} True if the user has read the event, false otherwise.
1642 */
1643
1644
1645Room.prototype.hasUserReadEvent = function (userId, eventId) {
1646 const readUpToId = this.getEventReadUpTo(userId, false);
1647 if (readUpToId === eventId) return true;
1648
1649 if (this.timeline.length && this.timeline[this.timeline.length - 1].getSender() && this.timeline[this.timeline.length - 1].getSender() === userId) {
1650 // It doesn't matter where the event is in the timeline, the user has read
1651 // it because they've sent the latest event.
1652 return true;
1653 }
1654
1655 for (let i = this.timeline.length - 1; i >= 0; --i) {
1656 const ev = this.timeline[i]; // If we encounter the target event first, the user hasn't read it
1657 // however if we encounter the readUpToId first then the user has read
1658 // it. These rules apply because we're iterating bottom-up.
1659
1660 if (ev.getId() === eventId) return false;
1661 if (ev.getId() === readUpToId) return true;
1662 } // We don't know if the user has read it, so assume not.
1663
1664
1665 return false;
1666};
1667/**
1668 * Get a list of receipts for the given event.
1669 * @param {MatrixEvent} event the event to get receipts for
1670 * @return {Object[]} A list of receipts with a userId, type and data keys or
1671 * an empty list.
1672 */
1673
1674
1675Room.prototype.getReceiptsForEvent = function (event) {
1676 return this._receiptCacheByEventId[event.getId()] || [];
1677};
1678/**
1679 * Add a receipt event to the room.
1680 * @param {MatrixEvent} event The m.receipt event.
1681 * @param {Boolean} fake True if this event is implicit
1682 */
1683
1684
1685Room.prototype.addReceipt = function (event, fake) {
1686 // event content looks like:
1687 // content: {
1688 // $event_id: {
1689 // $receipt_type: {
1690 // $user_id: {
1691 // ts: $timestamp
1692 // }
1693 // }
1694 // }
1695 // }
1696 if (fake === undefined) {
1697 fake = false;
1698 }
1699
1700 if (!fake) {
1701 this._addReceiptsToStructure(event, this._realReceipts); // we don't bother caching real receipts by event ID
1702 // as there's nothing that would read it.
1703
1704 }
1705
1706 this._addReceiptsToStructure(event, this._receipts);
1707
1708 this._receiptCacheByEventId = this._buildReceiptCache(this._receipts); // send events after we've regenerated the cache, otherwise things that
1709 // listened for the event would read from a stale cache
1710
1711 this.emit("Room.receipt", event, this);
1712};
1713/**
1714 * Add a receipt event to the room.
1715 * @param {MatrixEvent} event The m.receipt event.
1716 * @param {Object} receipts The object to add receipts to
1717 */
1718
1719
1720Room.prototype._addReceiptsToStructure = function (event, receipts) {
1721 const self = this;
1722 utils.keys(event.getContent()).forEach(function (eventId) {
1723 utils.keys(event.getContent()[eventId]).forEach(function (receiptType) {
1724 utils.keys(event.getContent()[eventId][receiptType]).forEach(function (userId) {
1725 const receipt = event.getContent()[eventId][receiptType][userId];
1726
1727 if (!receipts[receiptType]) {
1728 receipts[receiptType] = {};
1729 }
1730
1731 const existingReceipt = receipts[receiptType][userId];
1732
1733 if (!existingReceipt) {
1734 receipts[receiptType][userId] = {};
1735 } else {
1736 // we only want to add this receipt if we think it is later
1737 // than the one we already have. (This is managed
1738 // server-side, but because we synthesize RRs locally we
1739 // have to do it here too.)
1740 const ordering = self.getUnfilteredTimelineSet().compareEventOrdering(existingReceipt.eventId, eventId);
1741
1742 if (ordering !== null && ordering >= 0) {
1743 return;
1744 }
1745 }
1746
1747 receipts[receiptType][userId] = {
1748 eventId: eventId,
1749 data: receipt
1750 };
1751 });
1752 });
1753 });
1754};
1755/**
1756 * Build and return a map of receipts by event ID
1757 * @param {Object} receipts A map of receipts
1758 * @return {Object} Map of receipts by event ID
1759 */
1760
1761
1762Room.prototype._buildReceiptCache = function (receipts) {
1763 const receiptCacheByEventId = {};
1764 utils.keys(receipts).forEach(function (receiptType) {
1765 utils.keys(receipts[receiptType]).forEach(function (userId) {
1766 const receipt = receipts[receiptType][userId];
1767
1768 if (!receiptCacheByEventId[receipt.eventId]) {
1769 receiptCacheByEventId[receipt.eventId] = [];
1770 }
1771
1772 receiptCacheByEventId[receipt.eventId].push({
1773 userId: userId,
1774 type: receiptType,
1775 data: receipt.data
1776 });
1777 });
1778 });
1779 return receiptCacheByEventId;
1780};
1781/**
1782 * Add a temporary local-echo receipt to the room to reflect in the
1783 * client the fact that we've sent one.
1784 * @param {string} userId The user ID if the receipt sender
1785 * @param {MatrixEvent} e The event that is to be acknowledged
1786 * @param {string} receiptType The type of receipt
1787 */
1788
1789
1790Room.prototype._addLocalEchoReceipt = function (userId, e, receiptType) {
1791 this.addReceipt(synthesizeReceipt(userId, e, receiptType), true);
1792};
1793/**
1794 * Update the room-tag event for the room. The previous one is overwritten.
1795 * @param {MatrixEvent} event the m.tag event
1796 */
1797
1798
1799Room.prototype.addTags = function (event) {
1800 // event content looks like:
1801 // content: {
1802 // tags: {
1803 // $tagName: { $metadata: $value },
1804 // $tagName: { $metadata: $value },
1805 // }
1806 // }
1807 // XXX: do we need to deep copy here?
1808 this.tags = event.getContent().tags || {}; // XXX: we could do a deep-comparison to see if the tags have really
1809 // changed - but do we want to bother?
1810
1811 this.emit("Room.tags", event, this);
1812};
1813/**
1814 * Update the account_data events for this room, overwriting events of the same type.
1815 * @param {Array<MatrixEvent>} events an array of account_data events to add
1816 */
1817
1818
1819Room.prototype.addAccountData = function (events) {
1820 for (let i = 0; i < events.length; i++) {
1821 const event = events[i];
1822
1823 if (event.getType() === "m.tag") {
1824 this.addTags(event);
1825 }
1826
1827 this.accountData[event.getType()] = event;
1828 this.emit("Room.accountData", event, this);
1829 }
1830};
1831/**
1832 * Access account_data event of given event type for this room
1833 * @param {string} type the type of account_data event to be accessed
1834 * @return {?MatrixEvent} the account_data event in question
1835 */
1836
1837
1838Room.prototype.getAccountData = function (type) {
1839 return this.accountData[type];
1840};
1841/**
1842 * Returns wheter the syncing user has permission to send a message in the room
1843 * @return {boolean} true if the user should be permitted to send
1844 * message events into the room.
1845 */
1846
1847
1848Room.prototype.maySendMessage = function () {
1849 return this.getMyMembership() === 'join' && this.currentState.maySendEvent('m.room.message', this.myUserId);
1850};
1851/**
1852 * This is an internal method. Calculates the name of the room from the current
1853 * room state.
1854 * @param {Room} room The matrix room.
1855 * @param {string} userId The client's user ID. Used to filter room members
1856 * correctly.
1857 * @param {bool} ignoreRoomNameEvent Return the implicit room name that we'd see if there
1858 * was no m.room.name event.
1859 * @return {string} The calculated room name.
1860 */
1861
1862
1863function calculateRoomName(room, userId, ignoreRoomNameEvent) {
1864 if (!ignoreRoomNameEvent) {
1865 // check for an alias, if any. for now, assume first alias is the
1866 // official one.
1867 const mRoomName = room.currentState.getStateEvents("m.room.name", "");
1868
1869 if (mRoomName && mRoomName.getContent() && mRoomName.getContent().name) {
1870 return mRoomName.getContent().name;
1871 }
1872 }
1873
1874 let alias = room.getCanonicalAlias();
1875
1876 if (!alias) {
1877 const aliases = room.getAliases();
1878
1879 if (aliases.length) {
1880 alias = aliases[0];
1881 }
1882 }
1883
1884 if (alias) {
1885 return alias;
1886 }
1887
1888 const joinedMemberCount = room.currentState.getJoinedMemberCount();
1889 const invitedMemberCount = room.currentState.getInvitedMemberCount(); // -1 because these numbers include the syncing user
1890
1891 const inviteJoinCount = joinedMemberCount + invitedMemberCount - 1; // get members that are NOT ourselves and are actually in the room.
1892
1893 let otherNames = null;
1894
1895 if (room._summaryHeroes) {
1896 // if we have a summary, the member state events
1897 // should be in the room state
1898 otherNames = room._summaryHeroes.map(userId => {
1899 const member = room.getMember(userId);
1900 return member ? member.name : userId;
1901 });
1902 } else {
1903 let otherMembers = room.currentState.getMembers().filter(m => {
1904 return m.userId !== userId && (m.membership === "invite" || m.membership === "join");
1905 }); // make sure members have stable order
1906
1907 otherMembers.sort((a, b) => a.userId.localeCompare(b.userId)); // only 5 first members, immitate _summaryHeroes
1908
1909 otherMembers = otherMembers.slice(0, 5);
1910 otherNames = otherMembers.map(m => m.name);
1911 }
1912
1913 if (inviteJoinCount) {
1914 return memberNamesToRoomName(otherNames, inviteJoinCount);
1915 }
1916
1917 const myMembership = room.getMyMembership(); // if I have created a room and invited people throuh
1918 // 3rd party invites
1919
1920 if (myMembership == 'join') {
1921 const thirdPartyInvites = room.currentState.getStateEvents("m.room.third_party_invite");
1922
1923 if (thirdPartyInvites && thirdPartyInvites.length) {
1924 const thirdPartyNames = thirdPartyInvites.map(i => {
1925 return i.getContent().display_name;
1926 });
1927 return `Inviting ${memberNamesToRoomName(thirdPartyNames)}`;
1928 }
1929 } // let's try to figure out who was here before
1930
1931
1932 let leftNames = otherNames; // if we didn't have heroes, try finding them in the room state
1933
1934 if (!leftNames.length) {
1935 leftNames = room.currentState.getMembers().filter(m => {
1936 return m.userId !== userId && m.membership !== "invite" && m.membership !== "join";
1937 }).map(m => m.name);
1938 }
1939
1940 if (leftNames.length) {
1941 return `Empty room (was ${memberNamesToRoomName(leftNames)})`;
1942 } else {
1943 return "Empty room";
1944 }
1945}
1946
1947function memberNamesToRoomName(names, count = names.length + 1) {
1948 const countWithoutMe = count - 1;
1949
1950 if (!names.length) {
1951 return "Empty room";
1952 } else if (names.length === 1 && countWithoutMe <= 1) {
1953 return names[0];
1954 } else if (names.length === 2 && countWithoutMe <= 2) {
1955 return `${names[0]} and ${names[1]}`;
1956 } else {
1957 const plural = countWithoutMe > 1;
1958
1959 if (plural) {
1960 return `${names[0]} and ${countWithoutMe} others`;
1961 } else {
1962 return `${names[0]} and 1 other`;
1963 }
1964 }
1965}
1966/**
1967 * Fires when an event we had previously received is redacted.
1968 *
1969 * (Note this is *not* fired when the redaction happens before we receive the
1970 * event).
1971 *
1972 * @event module:client~MatrixClient#"Room.redaction"
1973 * @param {MatrixEvent} event The matrix redaction event
1974 * @param {Room} room The room containing the redacted event
1975 */
1976
1977/**
1978 * Fires when an event that was previously redacted isn't anymore.
1979 * This happens when the redaction couldn't be sent and
1980 * was subsequently cancelled by the user. Redactions have a local echo
1981 * which is undone in this scenario.
1982 *
1983 * @event module:client~MatrixClient#"Room.redactionCancelled"
1984 * @param {MatrixEvent} event The matrix redaction event that was cancelled.
1985 * @param {Room} room The room containing the unredacted event
1986 */
1987
1988/**
1989 * Fires whenever the name of a room is updated.
1990 * @event module:client~MatrixClient#"Room.name"
1991 * @param {Room} room The room whose Room.name was updated.
1992 * @example
1993 * matrixClient.on("Room.name", function(room){
1994 * var newName = room.name;
1995 * });
1996 */
1997
1998/**
1999 * Fires whenever a receipt is received for a room
2000 * @event module:client~MatrixClient#"Room.receipt"
2001 * @param {event} event The receipt event
2002 * @param {Room} room The room whose receipts was updated.
2003 * @example
2004 * matrixClient.on("Room.receipt", function(event, room){
2005 * var receiptContent = event.getContent();
2006 * });
2007 */
2008
2009/**
2010 * Fires whenever a room's tags are updated.
2011 * @event module:client~MatrixClient#"Room.tags"
2012 * @param {event} event The tags event
2013 * @param {Room} room The room whose Room.tags was updated.
2014 * @example
2015 * matrixClient.on("Room.tags", function(event, room){
2016 * var newTags = event.getContent().tags;
2017 * if (newTags["favourite"]) showStar(room);
2018 * });
2019 */
2020
2021/**
2022 * Fires whenever a room's account_data is updated.
2023 * @event module:client~MatrixClient#"Room.accountData"
2024 * @param {event} event The account_data event
2025 * @param {Room} room The room whose account_data was updated.
2026 * @example
2027 * matrixClient.on("Room.accountData", function(event, room){
2028 * if (event.getType() === "m.room.colorscheme") {
2029 * applyColorScheme(event.getContents());
2030 * }
2031 * });
2032 */
2033
2034/**
2035 * Fires when the status of a transmitted event is updated.
2036 *
2037 * <p>When an event is first transmitted, a temporary copy of the event is
2038 * inserted into the timeline, with a temporary event id, and a status of
2039 * 'SENDING'.
2040 *
2041 * <p>Once the echo comes back from the server, the content of the event
2042 * (MatrixEvent.event) is replaced by the complete event from the homeserver,
2043 * thus updating its event id, as well as server-generated fields such as the
2044 * timestamp. Its status is set to null.
2045 *
2046 * <p>Once the /send request completes, if the remote echo has not already
2047 * arrived, the event is updated with a new event id and the status is set to
2048 * 'SENT'. The server-generated fields are of course not updated yet.
2049 *
2050 * <p>If the /send fails, In this case, the event's status is set to
2051 * 'NOT_SENT'. If it is later resent, the process starts again, setting the
2052 * status to 'SENDING'. Alternatively, the message may be cancelled, which
2053 * removes the event from the room, and sets the status to 'CANCELLED'.
2054 *
2055 * <p>This event is raised to reflect each of the transitions above.
2056 *
2057 * @event module:client~MatrixClient#"Room.localEchoUpdated"
2058 *
2059 * @param {MatrixEvent} event The matrix event which has been updated
2060 *
2061 * @param {Room} room The room containing the redacted event
2062 *
2063 * @param {string} oldEventId The previous event id (the temporary event id,
2064 * except when updating a successfully-sent event when its echo arrives)
2065 *
2066 * @param {EventStatus} oldStatus The previous event status.
2067 */
\No newline at end of file