1 | ;
|
2 |
|
3 | var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
|
4 |
|
5 | Object.defineProperty(exports, "__esModule", {
|
6 | value: true
|
7 | });
|
8 | exports.RoomState = RoomState;
|
9 |
|
10 | var _events = require("events");
|
11 |
|
12 | var _roomMember = require("./room-member");
|
13 |
|
14 | var _logger = require("../logger");
|
15 |
|
16 | var utils = _interopRequireWildcard(require("../utils"));
|
17 |
|
18 | /*
|
19 | Copyright 2015, 2016 OpenMarket Ltd
|
20 | Copyright 2019 The Matrix.org Foundation C.I.C.
|
21 |
|
22 | Licensed under the Apache License, Version 2.0 (the "License");
|
23 | you may not use this file except in compliance with the License.
|
24 | You may obtain a copy of the License at
|
25 |
|
26 | http://www.apache.org/licenses/LICENSE-2.0
|
27 |
|
28 | Unless required by applicable law or agreed to in writing, software
|
29 | distributed under the License is distributed on an "AS IS" BASIS,
|
30 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
31 | See the License for the specific language governing permissions and
|
32 | limitations under the License.
|
33 | */
|
34 |
|
35 | /**
|
36 | * @module models/room-state
|
37 | */
|
38 | // possible statuses for out-of-band member loading
|
39 | const OOB_STATUS_NOTSTARTED = 1;
|
40 | const OOB_STATUS_INPROGRESS = 2;
|
41 | const OOB_STATUS_FINISHED = 3;
|
42 | /**
|
43 | * Construct room state.
|
44 | *
|
45 | * Room State represents the state of the room at a given point.
|
46 | * It can be mutated by adding state events to it.
|
47 | * There are two types of room member associated with a state event:
|
48 | * normal member objects (accessed via getMember/getMembers) which mutate
|
49 | * with the state to represent the current state of that room/user, eg.
|
50 | * the object returned by getMember('@bob:example.com') will mutate to
|
51 | * get a different display name if Bob later changes his display name
|
52 | * in the room.
|
53 | * There are also 'sentinel' members (accessed via getSentinelMember).
|
54 | * These also represent the state of room members at the point in time
|
55 | * represented by the RoomState object, but unlike objects from getMember,
|
56 | * sentinel objects will always represent the room state as at the time
|
57 | * getSentinelMember was called, so if Bob subsequently changes his display
|
58 | * name, a room member object previously acquired with getSentinelMember
|
59 | * will still have his old display name. Calling getSentinelMember again
|
60 | * after the display name change will return a new RoomMember object
|
61 | * with Bob's new display name.
|
62 | *
|
63 | * @constructor
|
64 | * @param {?string} roomId Optional. The ID of the room which has this state.
|
65 | * If none is specified it just tracks paginationTokens, useful for notifTimelineSet
|
66 | * @param {?object} oobMemberFlags Optional. The state of loading out of bound members.
|
67 | * As the timeline might get reset while they are loading, this state needs to be inherited
|
68 | * and shared when the room state is cloned for the new timeline.
|
69 | * This should only be passed from clone.
|
70 | * @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
|
71 | * on the user's ID.
|
72 | * @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
|
73 | * events dictionary, keyed on the event type and then the state_key value.
|
74 | * @prop {string} paginationToken The pagination token for this state.
|
75 | */
|
76 |
|
77 | function RoomState(roomId, oobMemberFlags = undefined) {
|
78 | this.roomId = roomId;
|
79 | this.members = {// userId: RoomMember
|
80 | };
|
81 | this.events = {// eventType: { stateKey: MatrixEvent }
|
82 | };
|
83 | this.paginationToken = null;
|
84 | this._sentinels = {// userId: RoomMember
|
85 | };
|
86 |
|
87 | this._updateModifiedTime(); // stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
|
88 |
|
89 |
|
90 | this._displayNameToUserIds = {};
|
91 | this._userIdsToDisplayNames = {};
|
92 | this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite
|
93 |
|
94 | this._joinedMemberCount = null; // cache of the number of joined members
|
95 | // joined members count from summary api
|
96 | // once set, we know the server supports the summary api
|
97 | // and we should only trust that
|
98 | // we could also only trust that before OOB members
|
99 | // are loaded but doesn't seem worth the hassle atm
|
100 |
|
101 | this._summaryJoinedMemberCount = null; // same for invited member count
|
102 |
|
103 | this._invitedMemberCount = null;
|
104 | this._summaryInvitedMemberCount = null;
|
105 |
|
106 | if (!oobMemberFlags) {
|
107 | oobMemberFlags = {
|
108 | status: OOB_STATUS_NOTSTARTED
|
109 | };
|
110 | }
|
111 |
|
112 | this._oobMemberFlags = oobMemberFlags;
|
113 | }
|
114 |
|
115 | utils.inherits(RoomState, _events.EventEmitter);
|
116 | /**
|
117 | * Returns the number of joined members in this room
|
118 | * This method caches the result.
|
119 | * @return {integer} The number of members in this room whose membership is 'join'
|
120 | */
|
121 |
|
122 | RoomState.prototype.getJoinedMemberCount = function () {
|
123 | if (this._summaryJoinedMemberCount !== null) {
|
124 | return this._summaryJoinedMemberCount;
|
125 | }
|
126 |
|
127 | if (this._joinedMemberCount === null) {
|
128 | this._joinedMemberCount = this.getMembers().reduce((count, m) => {
|
129 | return m.membership === 'join' ? count + 1 : count;
|
130 | }, 0);
|
131 | }
|
132 |
|
133 | return this._joinedMemberCount;
|
134 | };
|
135 | /**
|
136 | * Set the joined member count explicitly (like from summary part of the sync response)
|
137 | * @param {number} count the amount of joined members
|
138 | */
|
139 |
|
140 |
|
141 | RoomState.prototype.setJoinedMemberCount = function (count) {
|
142 | this._summaryJoinedMemberCount = count;
|
143 | };
|
144 | /**
|
145 | * Returns the number of invited members in this room
|
146 | * @return {integer} The number of members in this room whose membership is 'invite'
|
147 | */
|
148 |
|
149 |
|
150 | RoomState.prototype.getInvitedMemberCount = function () {
|
151 | if (this._summaryInvitedMemberCount !== null) {
|
152 | return this._summaryInvitedMemberCount;
|
153 | }
|
154 |
|
155 | if (this._invitedMemberCount === null) {
|
156 | this._invitedMemberCount = this.getMembers().reduce((count, m) => {
|
157 | return m.membership === 'invite' ? count + 1 : count;
|
158 | }, 0);
|
159 | }
|
160 |
|
161 | return this._invitedMemberCount;
|
162 | };
|
163 | /**
|
164 | * Set the amount of invited members in this room
|
165 | * @param {number} count the amount of invited members
|
166 | */
|
167 |
|
168 |
|
169 | RoomState.prototype.setInvitedMemberCount = function (count) {
|
170 | this._summaryInvitedMemberCount = count;
|
171 | };
|
172 | /**
|
173 | * Get all RoomMembers in this room.
|
174 | * @return {Array<RoomMember>} A list of RoomMembers.
|
175 | */
|
176 |
|
177 |
|
178 | RoomState.prototype.getMembers = function () {
|
179 | return utils.values(this.members);
|
180 | };
|
181 | /**
|
182 | * Get all RoomMembers in this room, excluding the user IDs provided.
|
183 | * @param {Array<string>} excludedIds The user IDs to exclude.
|
184 | * @return {Array<RoomMember>} A list of RoomMembers.
|
185 | */
|
186 |
|
187 |
|
188 | RoomState.prototype.getMembersExcept = function (excludedIds) {
|
189 | return utils.values(this.members).filter(m => !excludedIds.includes(m.userId));
|
190 | };
|
191 | /**
|
192 | * Get a room member by their user ID.
|
193 | * @param {string} userId The room member's user ID.
|
194 | * @return {RoomMember} The member or null if they do not exist.
|
195 | */
|
196 |
|
197 |
|
198 | RoomState.prototype.getMember = function (userId) {
|
199 | return this.members[userId] || null;
|
200 | };
|
201 | /**
|
202 | * Get a room member whose properties will not change with this room state. You
|
203 | * typically want this if you want to attach a RoomMember to a MatrixEvent which
|
204 | * may no longer be represented correctly by Room.currentState or Room.oldState.
|
205 | * The term 'sentinel' refers to the fact that this RoomMember is an unchanging
|
206 | * guardian for state at this particular point in time.
|
207 | * @param {string} userId The room member's user ID.
|
208 | * @return {RoomMember} The member or null if they do not exist.
|
209 | */
|
210 |
|
211 |
|
212 | RoomState.prototype.getSentinelMember = function (userId) {
|
213 | if (!userId) return null;
|
214 | let sentinel = this._sentinels[userId];
|
215 |
|
216 | if (sentinel === undefined) {
|
217 | sentinel = new _roomMember.RoomMember(this.roomId, userId);
|
218 | const member = this.members[userId];
|
219 |
|
220 | if (member) {
|
221 | sentinel.setMembershipEvent(member.events.member, this);
|
222 | }
|
223 |
|
224 | this._sentinels[userId] = sentinel;
|
225 | }
|
226 |
|
227 | return sentinel;
|
228 | };
|
229 | /**
|
230 | * Get state events from the state of the room.
|
231 | * @param {string} eventType The event type of the state event.
|
232 | * @param {string} stateKey Optional. The state_key of the state event. If
|
233 | * this is <code>undefined</code> then all matching state events will be
|
234 | * returned.
|
235 | * @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was
|
236 | * <code>undefined</code>, else a single event (or null if no match found).
|
237 | */
|
238 |
|
239 |
|
240 | RoomState.prototype.getStateEvents = function (eventType, stateKey) {
|
241 | if (!this.events[eventType]) {
|
242 | // no match
|
243 | return stateKey === undefined ? [] : null;
|
244 | }
|
245 |
|
246 | if (stateKey === undefined) {
|
247 | // return all values
|
248 | return utils.values(this.events[eventType]);
|
249 | }
|
250 |
|
251 | const event = this.events[eventType][stateKey];
|
252 | return event ? event : null;
|
253 | };
|
254 | /**
|
255 | * Creates a copy of this room state so that mutations to either won't affect the other.
|
256 | * @return {RoomState} the copy of the room state
|
257 | */
|
258 |
|
259 |
|
260 | RoomState.prototype.clone = function () {
|
261 | const copy = new RoomState(this.roomId, this._oobMemberFlags); // Ugly hack: because setStateEvents will mark
|
262 | // members as susperseding future out of bound members
|
263 | // if loading is in progress (through _oobMemberFlags)
|
264 | // since these are not new members, we're merely copying them
|
265 | // set the status to not started
|
266 | // after copying, we set back the status
|
267 |
|
268 | const status = this._oobMemberFlags.status;
|
269 | this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
|
270 | Object.values(this.events).forEach(eventsByStateKey => {
|
271 | const eventsForType = Object.values(eventsByStateKey);
|
272 | copy.setStateEvents(eventsForType);
|
273 | }); // Ugly hack: see above
|
274 |
|
275 | this._oobMemberFlags.status = status;
|
276 |
|
277 | if (this._summaryInvitedMemberCount !== null) {
|
278 | copy.setInvitedMemberCount(this.getInvitedMemberCount());
|
279 | }
|
280 |
|
281 | if (this._summaryJoinedMemberCount !== null) {
|
282 | copy.setJoinedMemberCount(this.getJoinedMemberCount());
|
283 | } // copy out of band flags if needed
|
284 |
|
285 |
|
286 | if (this._oobMemberFlags.status == OOB_STATUS_FINISHED) {
|
287 | // copy markOutOfBand flags
|
288 | this.getMembers().forEach(member => {
|
289 | if (member.isOutOfBand()) {
|
290 | const copyMember = copy.getMember(member.userId);
|
291 | copyMember.markOutOfBand();
|
292 | }
|
293 | });
|
294 | }
|
295 |
|
296 | return copy;
|
297 | };
|
298 | /**
|
299 | * Add previously unknown state events.
|
300 | * When lazy loading members while back-paginating,
|
301 | * the relevant room state for the timeline chunk at the end
|
302 | * of the chunk can be set with this method.
|
303 | * @param {MatrixEvent[]} events state events to prepend
|
304 | */
|
305 |
|
306 |
|
307 | RoomState.prototype.setUnknownStateEvents = function (events) {
|
308 | const unknownStateEvents = events.filter(event => {
|
309 | return this.events[event.getType()] === undefined || this.events[event.getType()][event.getStateKey()] === undefined;
|
310 | });
|
311 | this.setStateEvents(unknownStateEvents);
|
312 | };
|
313 | /**
|
314 | * Add an array of one or more state MatrixEvents, overwriting
|
315 | * any existing state with the same {type, stateKey} tuple. Will fire
|
316 | * "RoomState.events" for every event added. May fire "RoomState.members"
|
317 | * if there are <code>m.room.member</code> events.
|
318 | * @param {MatrixEvent[]} stateEvents a list of state events for this room.
|
319 | * @fires module:client~MatrixClient#event:"RoomState.members"
|
320 | * @fires module:client~MatrixClient#event:"RoomState.newMember"
|
321 | * @fires module:client~MatrixClient#event:"RoomState.events"
|
322 | */
|
323 |
|
324 |
|
325 | RoomState.prototype.setStateEvents = function (stateEvents) {
|
326 | const self = this;
|
327 |
|
328 | this._updateModifiedTime(); // update the core event dict
|
329 |
|
330 |
|
331 | utils.forEach(stateEvents, function (event) {
|
332 | if (event.getRoomId() !== self.roomId) {
|
333 | return;
|
334 | }
|
335 |
|
336 | if (!event.isState()) {
|
337 | return;
|
338 | }
|
339 |
|
340 | self._setStateEvent(event);
|
341 |
|
342 | if (event.getType() === "m.room.member") {
|
343 | _updateDisplayNameCache(self, event.getStateKey(), event.getContent().displayname);
|
344 |
|
345 | _updateThirdPartyTokenCache(self, event);
|
346 | }
|
347 |
|
348 | self.emit("RoomState.events", event, self);
|
349 | }); // update higher level data structures. This needs to be done AFTER the
|
350 | // core event dict as these structures may depend on other state events in
|
351 | // the given array (e.g. disambiguating display names in one go to do both
|
352 | // clashing names rather than progressively which only catches 1 of them).
|
353 |
|
354 | utils.forEach(stateEvents, function (event) {
|
355 | if (event.getRoomId() !== self.roomId) {
|
356 | return;
|
357 | }
|
358 |
|
359 | if (!event.isState()) {
|
360 | return;
|
361 | }
|
362 |
|
363 | if (event.getType() === "m.room.member") {
|
364 | const userId = event.getStateKey(); // leave events apparently elide the displayname or avatar_url,
|
365 | // so let's fake one up so that we don't leak user ids
|
366 | // into the timeline
|
367 |
|
368 | if (event.getContent().membership === "leave" || event.getContent().membership === "ban") {
|
369 | event.getContent().avatar_url = event.getContent().avatar_url || event.getPrevContent().avatar_url;
|
370 | event.getContent().displayname = event.getContent().displayname || event.getPrevContent().displayname;
|
371 | }
|
372 |
|
373 | const member = self._getOrCreateMember(userId, event);
|
374 |
|
375 | member.setMembershipEvent(event, self);
|
376 |
|
377 | self._updateMember(member);
|
378 |
|
379 | self.emit("RoomState.members", event, self, member);
|
380 | } else if (event.getType() === "m.room.power_levels") {
|
381 | const members = utils.values(self.members);
|
382 | utils.forEach(members, function (member) {
|
383 | member.setPowerLevelEvent(event);
|
384 | self.emit("RoomState.members", event, self, member);
|
385 | }); // assume all our sentinels are now out-of-date
|
386 |
|
387 | self._sentinels = {};
|
388 | }
|
389 | });
|
390 | };
|
391 | /**
|
392 | * Looks up a member by the given userId, and if it doesn't exist,
|
393 | * create it and emit the `RoomState.newMember` event.
|
394 | * This method makes sure the member is added to the members dictionary
|
395 | * before emitting, as this is done from setStateEvents and _setOutOfBandMember.
|
396 | * @param {string} userId the id of the user to look up
|
397 | * @param {MatrixEvent} event the membership event for the (new) member. Used to emit.
|
398 | * @fires module:client~MatrixClient#event:"RoomState.newMember"
|
399 | * @returns {RoomMember} the member, existing or newly created.
|
400 | */
|
401 |
|
402 |
|
403 | RoomState.prototype._getOrCreateMember = function (userId, event) {
|
404 | let member = this.members[userId];
|
405 |
|
406 | if (!member) {
|
407 | member = new _roomMember.RoomMember(this.roomId, userId); // add member to members before emitting any events,
|
408 | // as event handlers often lookup the member
|
409 |
|
410 | this.members[userId] = member;
|
411 | this.emit("RoomState.newMember", event, this, member);
|
412 | }
|
413 |
|
414 | return member;
|
415 | };
|
416 |
|
417 | RoomState.prototype._setStateEvent = function (event) {
|
418 | if (this.events[event.getType()] === undefined) {
|
419 | this.events[event.getType()] = {};
|
420 | }
|
421 |
|
422 | this.events[event.getType()][event.getStateKey()] = event;
|
423 | };
|
424 |
|
425 | RoomState.prototype._updateMember = function (member) {
|
426 | // this member may have a power level already, so set it.
|
427 | const pwrLvlEvent = this.getStateEvents("m.room.power_levels", "");
|
428 |
|
429 | if (pwrLvlEvent) {
|
430 | member.setPowerLevelEvent(pwrLvlEvent);
|
431 | } // blow away the sentinel which is now outdated
|
432 |
|
433 |
|
434 | delete this._sentinels[member.userId];
|
435 | this.members[member.userId] = member;
|
436 | this._joinedMemberCount = null;
|
437 | this._invitedMemberCount = null;
|
438 | };
|
439 | /**
|
440 | * Get the out-of-band members loading state, whether loading is needed or not.
|
441 | * Note that loading might be in progress and hence isn't needed.
|
442 | * @return {bool} whether or not the members of this room need to be loaded
|
443 | */
|
444 |
|
445 |
|
446 | RoomState.prototype.needsOutOfBandMembers = function () {
|
447 | return this._oobMemberFlags.status === OOB_STATUS_NOTSTARTED;
|
448 | };
|
449 | /**
|
450 | * Mark this room state as waiting for out-of-band members,
|
451 | * ensuring it doesn't ask for them to be requested again
|
452 | * through needsOutOfBandMembers
|
453 | */
|
454 |
|
455 |
|
456 | RoomState.prototype.markOutOfBandMembersStarted = function () {
|
457 | if (this._oobMemberFlags.status !== OOB_STATUS_NOTSTARTED) {
|
458 | return;
|
459 | }
|
460 |
|
461 | this._oobMemberFlags.status = OOB_STATUS_INPROGRESS;
|
462 | };
|
463 | /**
|
464 | * Mark this room state as having failed to fetch out-of-band members
|
465 | */
|
466 |
|
467 |
|
468 | RoomState.prototype.markOutOfBandMembersFailed = function () {
|
469 | if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
|
470 | return;
|
471 | }
|
472 |
|
473 | this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
|
474 | };
|
475 | /**
|
476 | * Clears the loaded out-of-band members
|
477 | */
|
478 |
|
479 |
|
480 | RoomState.prototype.clearOutOfBandMembers = function () {
|
481 | let count = 0;
|
482 | Object.keys(this.members).forEach(userId => {
|
483 | const member = this.members[userId];
|
484 |
|
485 | if (member.isOutOfBand()) {
|
486 | ++count;
|
487 | delete this.members[userId];
|
488 | }
|
489 | });
|
490 |
|
491 | _logger.logger.log(`LL: RoomState removed ${count} members...`);
|
492 |
|
493 | this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
|
494 | };
|
495 | /**
|
496 | * Sets the loaded out-of-band members.
|
497 | * @param {MatrixEvent[]} stateEvents array of membership state events
|
498 | */
|
499 |
|
500 |
|
501 | RoomState.prototype.setOutOfBandMembers = function (stateEvents) {
|
502 | _logger.logger.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
|
503 |
|
504 | if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
|
505 | return;
|
506 | }
|
507 |
|
508 | _logger.logger.log(`LL: RoomState put in OOB_STATUS_FINISHED state ...`);
|
509 |
|
510 | this._oobMemberFlags.status = OOB_STATUS_FINISHED;
|
511 | stateEvents.forEach(e => this._setOutOfBandMember(e));
|
512 | };
|
513 | /**
|
514 | * Sets a single out of band member, used by both setOutOfBandMembers and clone
|
515 | * @param {MatrixEvent} stateEvent membership state event
|
516 | */
|
517 |
|
518 |
|
519 | RoomState.prototype._setOutOfBandMember = function (stateEvent) {
|
520 | if (stateEvent.getType() !== 'm.room.member') {
|
521 | return;
|
522 | }
|
523 |
|
524 | const userId = stateEvent.getStateKey();
|
525 | const existingMember = this.getMember(userId); // never replace members received as part of the sync
|
526 |
|
527 | if (existingMember && !existingMember.isOutOfBand()) {
|
528 | return;
|
529 | }
|
530 |
|
531 | const member = this._getOrCreateMember(userId, stateEvent);
|
532 |
|
533 | member.setMembershipEvent(stateEvent, this); // needed to know which members need to be stored seperately
|
534 | // as they are not part of the sync accumulator
|
535 | // this is cleared by setMembershipEvent so when it's updated through /sync
|
536 |
|
537 | member.markOutOfBand();
|
538 |
|
539 | _updateDisplayNameCache(this, member.userId, member.name);
|
540 |
|
541 | this._setStateEvent(stateEvent);
|
542 |
|
543 | this._updateMember(member);
|
544 |
|
545 | this.emit("RoomState.members", stateEvent, this, member);
|
546 | };
|
547 | /**
|
548 | * Set the current typing event for this room.
|
549 | * @param {MatrixEvent} event The typing event
|
550 | */
|
551 |
|
552 |
|
553 | RoomState.prototype.setTypingEvent = function (event) {
|
554 | utils.forEach(utils.values(this.members), function (member) {
|
555 | member.setTypingEvent(event);
|
556 | });
|
557 | };
|
558 | /**
|
559 | * Get the m.room.member event which has the given third party invite token.
|
560 | *
|
561 | * @param {string} token The token
|
562 | * @return {?MatrixEvent} The m.room.member event or null
|
563 | */
|
564 |
|
565 |
|
566 | RoomState.prototype.getInviteForThreePidToken = function (token) {
|
567 | return this._tokenToInvite[token] || null;
|
568 | };
|
569 | /**
|
570 | * Update the last modified time to the current time.
|
571 | */
|
572 |
|
573 |
|
574 | RoomState.prototype._updateModifiedTime = function () {
|
575 | this._modified = Date.now();
|
576 | };
|
577 | /**
|
578 | * Get the timestamp when this room state was last updated. This timestamp is
|
579 | * updated when this object has received new state events.
|
580 | * @return {number} The timestamp
|
581 | */
|
582 |
|
583 |
|
584 | RoomState.prototype.getLastModifiedTime = function () {
|
585 | return this._modified;
|
586 | };
|
587 | /**
|
588 | * Get user IDs with the specified or similar display names.
|
589 | * @param {string} displayName The display name to get user IDs from.
|
590 | * @return {string[]} An array of user IDs or an empty array.
|
591 | */
|
592 |
|
593 |
|
594 | RoomState.prototype.getUserIdsWithDisplayName = function (displayName) {
|
595 | return this._displayNameToUserIds[utils.removeHiddenChars(displayName)] || [];
|
596 | };
|
597 | /**
|
598 | * Returns true if userId is in room, event is not redacted and either sender of
|
599 | * mxEvent or has power level sufficient to redact events other than their own.
|
600 | * @param {MatrixEvent} mxEvent The event to test permission for
|
601 | * @param {string} userId The user ID of the user to test permission for
|
602 | * @return {boolean} true if the given used ID can redact given event
|
603 | */
|
604 |
|
605 |
|
606 | RoomState.prototype.maySendRedactionForEvent = function (mxEvent, userId) {
|
607 | const member = this.getMember(userId);
|
608 | if (!member || member.membership === 'leave') return false;
|
609 | if (mxEvent.status || mxEvent.isRedacted()) return false; // The user may have been the sender, but they can't redact their own message
|
610 | // if redactions are blocked.
|
611 |
|
612 | const canRedact = this.maySendEvent("m.room.redaction", userId);
|
613 | if (mxEvent.getSender() === userId) return canRedact;
|
614 | return this._hasSufficientPowerLevelFor('redact', member.powerLevel);
|
615 | };
|
616 | /**
|
617 | * Returns true if the given power level is sufficient for action
|
618 | * @param {string} action The type of power level to check
|
619 | * @param {number} powerLevel The power level of the member
|
620 | * @return {boolean} true if the given power level is sufficient
|
621 | */
|
622 |
|
623 |
|
624 | RoomState.prototype._hasSufficientPowerLevelFor = function (action, powerLevel) {
|
625 | const powerLevelsEvent = this.getStateEvents('m.room.power_levels', '');
|
626 | let powerLevels = {};
|
627 |
|
628 | if (powerLevelsEvent) {
|
629 | powerLevels = powerLevelsEvent.getContent();
|
630 | }
|
631 |
|
632 | let requiredLevel = 50;
|
633 |
|
634 | if (utils.isNumber(powerLevels[action])) {
|
635 | requiredLevel = powerLevels[action];
|
636 | }
|
637 |
|
638 | return powerLevel >= requiredLevel;
|
639 | };
|
640 | /**
|
641 | * Short-form for maySendEvent('m.room.message', userId)
|
642 | * @param {string} userId The user ID of the user to test permission for
|
643 | * @return {boolean} true if the given user ID should be permitted to send
|
644 | * message events into the given room.
|
645 | */
|
646 |
|
647 |
|
648 | RoomState.prototype.maySendMessage = function (userId) {
|
649 | return this._maySendEventOfType('m.room.message', userId, false);
|
650 | };
|
651 | /**
|
652 | * Returns true if the given user ID has permission to send a normal
|
653 | * event of type `eventType` into this room.
|
654 | * @param {string} eventType The type of event to test
|
655 | * @param {string} userId The user ID of the user to test permission for
|
656 | * @return {boolean} true if the given user ID should be permitted to send
|
657 | * the given type of event into this room,
|
658 | * according to the room's state.
|
659 | */
|
660 |
|
661 |
|
662 | RoomState.prototype.maySendEvent = function (eventType, userId) {
|
663 | return this._maySendEventOfType(eventType, userId, false);
|
664 | };
|
665 | /**
|
666 | * Returns true if the given MatrixClient has permission to send a state
|
667 | * event of type `stateEventType` into this room.
|
668 | * @param {string} stateEventType The type of state events to test
|
669 | * @param {MatrixClient} cli The client to test permission for
|
670 | * @return {boolean} true if the given client should be permitted to send
|
671 | * the given type of state event into this room,
|
672 | * according to the room's state.
|
673 | */
|
674 |
|
675 |
|
676 | RoomState.prototype.mayClientSendStateEvent = function (stateEventType, cli) {
|
677 | if (cli.isGuest()) {
|
678 | return false;
|
679 | }
|
680 |
|
681 | return this.maySendStateEvent(stateEventType, cli.credentials.userId);
|
682 | };
|
683 | /**
|
684 | * Returns true if the given user ID has permission to send a state
|
685 | * event of type `stateEventType` into this room.
|
686 | * @param {string} stateEventType The type of state events to test
|
687 | * @param {string} userId The user ID of the user to test permission for
|
688 | * @return {boolean} true if the given user ID should be permitted to send
|
689 | * the given type of state event into this room,
|
690 | * according to the room's state.
|
691 | */
|
692 |
|
693 |
|
694 | RoomState.prototype.maySendStateEvent = function (stateEventType, userId) {
|
695 | return this._maySendEventOfType(stateEventType, userId, true);
|
696 | };
|
697 | /**
|
698 | * Returns true if the given user ID has permission to send a normal or state
|
699 | * event of type `eventType` into this room.
|
700 | * @param {string} eventType The type of event to test
|
701 | * @param {string} userId The user ID of the user to test permission for
|
702 | * @param {boolean} state If true, tests if the user may send a state
|
703 | event of this type. Otherwise tests whether
|
704 | they may send a regular event.
|
705 | * @return {boolean} true if the given user ID should be permitted to send
|
706 | * the given type of event into this room,
|
707 | * according to the room's state.
|
708 | */
|
709 |
|
710 |
|
711 | RoomState.prototype._maySendEventOfType = function (eventType, userId, state) {
|
712 | const power_levels_event = this.getStateEvents('m.room.power_levels', '');
|
713 | let power_levels;
|
714 | let events_levels = {};
|
715 | let state_default = 0;
|
716 | let events_default = 0;
|
717 | let powerLevel = 0;
|
718 |
|
719 | if (power_levels_event) {
|
720 | power_levels = power_levels_event.getContent();
|
721 | events_levels = power_levels.events || {};
|
722 |
|
723 | if (Number.isFinite(power_levels.state_default)) {
|
724 | state_default = power_levels.state_default;
|
725 | } else {
|
726 | state_default = 50;
|
727 | }
|
728 |
|
729 | const userPowerLevel = power_levels.users && power_levels.users[userId];
|
730 |
|
731 | if (Number.isFinite(userPowerLevel)) {
|
732 | powerLevel = userPowerLevel;
|
733 | } else if (Number.isFinite(power_levels.users_default)) {
|
734 | powerLevel = power_levels.users_default;
|
735 | }
|
736 |
|
737 | if (Number.isFinite(power_levels.events_default)) {
|
738 | events_default = power_levels.events_default;
|
739 | }
|
740 | }
|
741 |
|
742 | let required_level = state ? state_default : events_default;
|
743 |
|
744 | if (Number.isFinite(events_levels[eventType])) {
|
745 | required_level = events_levels[eventType];
|
746 | }
|
747 |
|
748 | return powerLevel >= required_level;
|
749 | };
|
750 | /**
|
751 | * Returns true if the given user ID has permission to trigger notification
|
752 | * of type `notifLevelKey`
|
753 | * @param {string} notifLevelKey The level of notification to test (eg. 'room')
|
754 | * @param {string} userId The user ID of the user to test permission for
|
755 | * @return {boolean} true if the given user ID has permission to trigger a
|
756 | * notification of this type.
|
757 | */
|
758 |
|
759 |
|
760 | RoomState.prototype.mayTriggerNotifOfType = function (notifLevelKey, userId) {
|
761 | const member = this.getMember(userId);
|
762 |
|
763 | if (!member) {
|
764 | return false;
|
765 | }
|
766 |
|
767 | const powerLevelsEvent = this.getStateEvents('m.room.power_levels', '');
|
768 | let notifLevel = 50;
|
769 |
|
770 | if (powerLevelsEvent && powerLevelsEvent.getContent() && powerLevelsEvent.getContent().notifications && utils.isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey])) {
|
771 | notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey];
|
772 | }
|
773 |
|
774 | return member.powerLevel >= notifLevel;
|
775 | };
|
776 |
|
777 | function _updateThirdPartyTokenCache(roomState, memberEvent) {
|
778 | if (!memberEvent.getContent().third_party_invite) {
|
779 | return;
|
780 | }
|
781 |
|
782 | const token = (memberEvent.getContent().third_party_invite.signed || {}).token;
|
783 |
|
784 | if (!token) {
|
785 | return;
|
786 | }
|
787 |
|
788 | const threePidInvite = roomState.getStateEvents("m.room.third_party_invite", token);
|
789 |
|
790 | if (!threePidInvite) {
|
791 | return;
|
792 | }
|
793 |
|
794 | roomState._tokenToInvite[token] = memberEvent;
|
795 | }
|
796 |
|
797 | function _updateDisplayNameCache(roomState, userId, displayName) {
|
798 | const oldName = roomState._userIdsToDisplayNames[userId];
|
799 | delete roomState._userIdsToDisplayNames[userId];
|
800 |
|
801 | if (oldName) {
|
802 | // Remove the old name from the cache.
|
803 | // We clobber the user_id > name lookup but the name -> [user_id] lookup
|
804 | // means we need to remove that user ID from that array rather than nuking
|
805 | // the lot.
|
806 | const strippedOldName = utils.removeHiddenChars(oldName);
|
807 | const existingUserIds = roomState._displayNameToUserIds[strippedOldName];
|
808 |
|
809 | if (existingUserIds) {
|
810 | // remove this user ID from this array
|
811 | const filteredUserIDs = existingUserIds.filter(id => id !== userId);
|
812 | roomState._displayNameToUserIds[strippedOldName] = filteredUserIDs;
|
813 | }
|
814 | }
|
815 |
|
816 | roomState._userIdsToDisplayNames[userId] = displayName;
|
817 | const strippedDisplayname = displayName && utils.removeHiddenChars(displayName); // an empty stripped displayname (undefined/'') will be set to MXID in room-member.js
|
818 |
|
819 | if (strippedDisplayname) {
|
820 | if (!roomState._displayNameToUserIds[strippedDisplayname]) {
|
821 | roomState._displayNameToUserIds[strippedDisplayname] = [];
|
822 | }
|
823 |
|
824 | roomState._displayNameToUserIds[strippedDisplayname].push(userId);
|
825 | }
|
826 | }
|
827 | /**
|
828 | * Fires whenever the event dictionary in room state is updated.
|
829 | * @event module:client~MatrixClient#"RoomState.events"
|
830 | * @param {MatrixEvent} event The matrix event which caused this event to fire.
|
831 | * @param {RoomState} state The room state whose RoomState.events dictionary
|
832 | * was updated.
|
833 | * @example
|
834 | * matrixClient.on("RoomState.events", function(event, state){
|
835 | * var newStateEvent = event;
|
836 | * });
|
837 | */
|
838 |
|
839 | /**
|
840 | * Fires whenever a member in the members dictionary is updated in any way.
|
841 | * @event module:client~MatrixClient#"RoomState.members"
|
842 | * @param {MatrixEvent} event The matrix event which caused this event to fire.
|
843 | * @param {RoomState} state The room state whose RoomState.members dictionary
|
844 | * was updated.
|
845 | * @param {RoomMember} member The room member that was updated.
|
846 | * @example
|
847 | * matrixClient.on("RoomState.members", function(event, state, member){
|
848 | * var newMembershipState = member.membership;
|
849 | * });
|
850 | */
|
851 |
|
852 | /**
|
853 | * Fires whenever a member is added to the members dictionary. The RoomMember
|
854 | * will not be fully populated yet (e.g. no membership state) but will already
|
855 | * be available in the members dictionary.
|
856 | * @event module:client~MatrixClient#"RoomState.newMember"
|
857 | * @param {MatrixEvent} event The matrix event which caused this event to fire.
|
858 | * @param {RoomState} state The room state whose RoomState.members dictionary
|
859 | * was updated with a new entry.
|
860 | * @param {RoomMember} member The room member that was added.
|
861 | * @example
|
862 | * matrixClient.on("RoomState.newMember", function(event, state, member){
|
863 | * // add event listeners on 'member'
|
864 | * });
|
865 | */ |
\ | No newline at end of file |