UNPKG

154 kBJavaScriptView Raw
1/*
2Copyright 2015, 2016 OpenMarket Ltd
3Copyright 2017 Vector Creations Ltd
4Copyright 2018-2019 New Vector Ltd
5
6Licensed under the Apache License, Version 2.0 (the "License");
7you may not use this file except in compliance with the License.
8You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12Unless required by applicable law or agreed to in writing, software
13distributed under the License is distributed on an "AS IS" BASIS,
14WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15See the License for the specific language governing permissions and
16limitations under the License.
17*/
18"use strict";
19
20const PushProcessor = require('./pushprocessor');
21
22/**
23 * This is an internal module. See {@link MatrixClient} for the public class.
24 * @module client
25 */
26const EventEmitter = require("events").EventEmitter;
27import Promise from 'bluebird';
28const url = require('url');
29
30const httpApi = require("./http-api");
31const MatrixEvent = require("./models/event").MatrixEvent;
32const EventStatus = require("./models/event").EventStatus;
33const EventTimeline = require("./models/event-timeline");
34const SearchResult = require("./models/search-result");
35const StubStore = require("./store/stub");
36const webRtcCall = require("./webrtc/call");
37const utils = require("./utils");
38const contentRepo = require("./content-repo");
39const Filter = require("./filter");
40const SyncApi = require("./sync");
41const MatrixBaseApis = require("./base-apis");
42const MatrixError = httpApi.MatrixError;
43const ContentHelpers = require("./content-helpers");
44const olmlib = require("./crypto/olmlib");
45
46import ReEmitter from './ReEmitter';
47import RoomList from './crypto/RoomList';
48
49import Crypto from './crypto';
50import { isCryptoAvailable } from './crypto';
51import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey';
52import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password';
53import { randomString } from './randomstring';
54
55// Disable warnings for now: we use deprecated bluebird functions
56// and need to migrate, but they spam the console with warnings.
57Promise.config({warnings: false});
58
59
60const SCROLLBACK_DELAY_MS = 3000;
61const CRYPTO_ENABLED = isCryptoAvailable();
62const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
63
64function keysFromRecoverySession(sessions, decryptionKey, roomId) {
65 const keys = [];
66 for (const [sessionId, sessionData] of Object.entries(sessions)) {
67 try {
68 const decrypted = keyFromRecoverySession(sessionData, decryptionKey);
69 decrypted.session_id = sessionId;
70 decrypted.room_id = roomId;
71 keys.push(decrypted);
72 } catch (e) {
73 console.log("Failed to decrypt session from backup");
74 }
75 }
76 return keys;
77}
78
79function keyFromRecoverySession(session, decryptionKey) {
80 return JSON.parse(decryptionKey.decrypt(
81 session.session_data.ephemeral,
82 session.session_data.mac,
83 session.session_data.ciphertext,
84 ));
85}
86
87/**
88 * Construct a Matrix Client. Only directly construct this if you want to use
89 * custom modules. Normally, {@link createClient} should be used
90 * as it specifies 'sensible' defaults for these modules.
91 * @constructor
92 * @extends {external:EventEmitter}
93 * @extends {module:base-apis~MatrixBaseApis}
94 *
95 * @param {Object} opts The configuration options for this client.
96 * @param {string} opts.baseUrl Required. The base URL to the client-server
97 * HTTP API.
98 * @param {string} opts.idBaseUrl Optional. The base identity server URL for
99 * identity server requests.
100 * @param {Function} opts.request Required. The function to invoke for HTTP
101 * requests. The value of this property is typically <code>require("request")
102 * </code> as it returns a function which meets the required interface. See
103 * {@link requestFunction} for more information.
104 *
105 * @param {string} opts.accessToken The access_token for this user.
106 *
107 * @param {string} opts.userId The user ID for this user.
108 *
109 * @param {Object=} opts.store The data store to use. If not specified,
110 * this client will not store any HTTP responses.
111 *
112 * @param {string=} opts.deviceId A unique identifier for this device; used for
113 * tracking things like crypto keys and access tokens. If not specified,
114 * end-to-end crypto will be disabled.
115 *
116 * @param {Object=} opts.sessionStore A store to be used for end-to-end crypto
117 * session data. This should be a {@link
118 * module:store/session/webstorage~WebStorageSessionStore|WebStorageSessionStore},
119 * or an object implementing the same interface. If not specified,
120 * end-to-end crypto will be disabled.
121 *
122 * @param {Object} opts.scheduler Optional. The scheduler to use. If not
123 * specified, this client will not retry requests on failure. This client
124 * will supply its own processing function to
125 * {@link module:scheduler~MatrixScheduler#setProcessFunction}.
126 *
127 * @param {Object} opts.queryParams Optional. Extra query parameters to append
128 * to all requests with this client. Useful for application services which require
129 * <code>?user_id=</code>.
130 *
131 * @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of
132 * time to wait before timing out HTTP requests. If not specified, there is no timeout.
133 *
134 * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
135 * Authorization header instead of query param to send the access token to the server.
136 *
137 * @param {boolean} [opts.timelineSupport = false] Set to true to enable
138 * improved timeline support ({@link
139 * module:client~MatrixClient#getEventTimeline getEventTimeline}). It is
140 * disabled by default for compatibility with older clients - in particular to
141 * maintain support for back-paginating the live timeline after a '/sync'
142 * result with a gap.
143 *
144 * @param {module:crypto.store.base~CryptoStore} opts.cryptoStore
145 * crypto store implementation.
146 *
147 * @param {Array} [opts.verificationMethods] Optional. The verification method
148 * that the application can handle. Each element should be an item from {@link
149 * module:crypto~verificationMethods verificationMethods}, or a class that
150 * implements the {$link module:crypto/verification/Base verifier interface}.
151 */
152function MatrixClient(opts) {
153 // Allow trailing slash in HS url
154 if (opts.baseUrl && opts.baseUrl.endsWith("/")) {
155 opts.baseUrl = opts.baseUrl.substr(0, opts.baseUrl.length - 1);
156 }
157
158 // Allow trailing slash in IS url
159 if (opts.idBaseUrl && opts.idBaseUrl.endsWith("/")) {
160 opts.idBaseUrl = opts.idBaseUrl.substr(0, opts.idBaseUrl.length - 1);
161 }
162
163 MatrixBaseApis.call(this, opts);
164
165 this.olmVersion = null; // Populated after initCrypto is done
166
167 this.reEmitter = new ReEmitter(this);
168
169 this.store = opts.store || new StubStore();
170
171 this.deviceId = opts.deviceId || null;
172
173 const userId = (opts.userId || null);
174 this.credentials = {
175 userId: userId,
176 };
177
178 this.scheduler = opts.scheduler;
179 if (this.scheduler) {
180 const self = this;
181 this.scheduler.setProcessFunction(function(eventToSend) {
182 const room = self.getRoom(eventToSend.getRoomId());
183 if (eventToSend.status !== EventStatus.SENDING) {
184 _updatePendingEventStatus(room, eventToSend,
185 EventStatus.SENDING);
186 }
187 return _sendEventHttpRequest(self, eventToSend);
188 });
189 }
190 this.clientRunning = false;
191
192 this.callList = {
193 // callId: MatrixCall
194 };
195
196 // try constructing a MatrixCall to see if we are running in an environment
197 // which has WebRTC. If we are, listen for and handle m.call.* events.
198 const call = webRtcCall.createNewMatrixCall(this);
199 this._supportsVoip = false;
200 if (call) {
201 setupCallEventHandler(this);
202 this._supportsVoip = true;
203 }
204 this._syncingRetry = null;
205 this._syncApi = null;
206 this._peekSync = null;
207 this._isGuest = false;
208 this._ongoingScrollbacks = {};
209 this.timelineSupport = Boolean(opts.timelineSupport);
210 this.urlPreviewCache = {};
211 this._notifTimelineSet = null;
212
213 this._crypto = null;
214 this._cryptoStore = opts.cryptoStore;
215 this._sessionStore = opts.sessionStore;
216 this._verificationMethods = opts.verificationMethods;
217
218 this._forceTURN = opts.forceTURN || false;
219
220 // List of which rooms have encryption enabled: separate from crypto because
221 // we still want to know which rooms are encrypted even if crypto is disabled:
222 // we don't want to start sending unencrypted events to them.
223 this._roomList = new RoomList(this._cryptoStore, this._sessionStore);
224
225 // The pushprocessor caches useful things, so keep one and re-use it
226 this._pushProcessor = new PushProcessor(this);
227
228 this._serverSupportsLazyLoading = null;
229
230 this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp }
231}
232utils.inherits(MatrixClient, EventEmitter);
233utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
234
235/**
236 * Clear any data out of the persistent stores used by the client.
237 *
238 * @returns {Promise} Promise which resolves when the stores have been cleared.
239 */
240MatrixClient.prototype.clearStores = function() {
241 if (this._clientRunning) {
242 throw new Error("Cannot clear stores while client is running");
243 }
244
245 const promises = [];
246
247 promises.push(this.store.deleteAllData());
248 if (this._cryptoStore) {
249 promises.push(this._cryptoStore.deleteAllData());
250 }
251 return Promise.all(promises);
252};
253
254/**
255 * Get the user-id of the logged-in user
256 *
257 * @return {?string} MXID for the logged-in user, or null if not logged in
258 */
259MatrixClient.prototype.getUserId = function() {
260 if (this.credentials && this.credentials.userId) {
261 return this.credentials.userId;
262 }
263 return null;
264};
265
266/**
267 * Get the domain for this client's MXID
268 * @return {?string} Domain of this MXID
269 */
270MatrixClient.prototype.getDomain = function() {
271 if (this.credentials && this.credentials.userId) {
272 return this.credentials.userId.replace(/^.*?:/, '');
273 }
274 return null;
275};
276
277/**
278 * Get the local part of the current user ID e.g. "foo" in "@foo:bar".
279 * @return {?string} The user ID localpart or null.
280 */
281MatrixClient.prototype.getUserIdLocalpart = function() {
282 if (this.credentials && this.credentials.userId) {
283 return this.credentials.userId.split(":")[0].substring(1);
284 }
285 return null;
286};
287
288/**
289 * Get the device ID of this client
290 * @return {?string} device ID
291 */
292MatrixClient.prototype.getDeviceId = function() {
293 return this.deviceId;
294};
295
296
297/**
298 * Check if the runtime environment supports VoIP calling.
299 * @return {boolean} True if VoIP is supported.
300 */
301MatrixClient.prototype.supportsVoip = function() {
302 return this._supportsVoip;
303};
304
305/**
306 * Set whether VoIP calls are forced to use only TURN
307 * candidates. This is the same as the forceTURN option
308 * when creating the client.
309 * @param {bool} forceTURN True to force use of TURN servers
310 */
311MatrixClient.prototype.setForceTURN = function(forceTURN) {
312 this._forceTURN = forceTURN;
313};
314
315/**
316 * Get the current sync state.
317 * @return {?string} the sync state, which may be null.
318 * @see module:client~MatrixClient#event:"sync"
319 */
320MatrixClient.prototype.getSyncState = function() {
321 if (!this._syncApi) {
322 return null;
323 }
324 return this._syncApi.getSyncState();
325};
326
327/**
328 * Returns the additional data object associated with
329 * the current sync state, or null if there is no
330 * such data.
331 * Sync errors, if available, are put in the 'error' key of
332 * this object.
333 * @return {?Object}
334 */
335MatrixClient.prototype.getSyncStateData = function() {
336 if (!this._syncApi) {
337 return null;
338 }
339 return this._syncApi.getSyncStateData();
340};
341
342/**
343 * Return whether the client is configured for a guest account.
344 * @return {boolean} True if this is a guest access_token (or no token is supplied).
345 */
346MatrixClient.prototype.isGuest = function() {
347 return this._isGuest;
348};
349
350/**
351 * Return the provided scheduler, if any.
352 * @return {?module:scheduler~MatrixScheduler} The scheduler or null
353 */
354MatrixClient.prototype.getScheduler = function() {
355 return this.scheduler;
356};
357
358/**
359 * Set whether this client is a guest account. <b>This method is experimental
360 * and may change without warning.</b>
361 * @param {boolean} isGuest True if this is a guest account.
362 */
363MatrixClient.prototype.setGuest = function(isGuest) {
364 // EXPERIMENTAL:
365 // If the token is a macaroon, it should be encoded in it that it is a 'guest'
366 // access token, which means that the SDK can determine this entirely without
367 // the dev manually flipping this flag.
368 this._isGuest = isGuest;
369};
370
371/**
372 * Retry a backed off syncing request immediately. This should only be used when
373 * the user <b>explicitly</b> attempts to retry their lost connection.
374 * @return {boolean} True if this resulted in a request being retried.
375 */
376MatrixClient.prototype.retryImmediately = function() {
377 return this._syncApi.retryImmediately();
378};
379
380/**
381 * Return the global notification EventTimelineSet, if any
382 *
383 * @return {EventTimelineSet} the globl notification EventTimelineSet
384 */
385MatrixClient.prototype.getNotifTimelineSet = function() {
386 return this._notifTimelineSet;
387};
388
389/**
390 * Set the global notification EventTimelineSet
391 *
392 * @param {EventTimelineSet} notifTimelineSet
393 */
394MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
395 this._notifTimelineSet = notifTimelineSet;
396};
397
398/**
399 * Gets the capabilities of the homeserver. Always returns an object of
400 * capability keys and their options, which may be empty.
401 * @return {module:client.Promise} Resolves to the capabilities of the homeserver
402 * @return {module:http-api.MatrixError} Rejects: with an error response.
403 */
404MatrixClient.prototype.getCapabilities = function() {
405 if (this._cachedCapabilities) {
406 const now = new Date().getTime();
407 if (now - this._cachedCapabilities.lastUpdated <= CAPABILITIES_CACHE_MS) {
408 return Promise.resolve(this._cachedCapabilities.capabilities);
409 }
410 }
411
412 // We swallow errors because we need a default object anyhow
413 return this._http.authedRequest(
414 undefined, "GET", "/capabilities",
415 ).catch(() => null).then((r) => {
416 if (!r) r = {};
417 const capabilities = r["capabilities"] || {};
418 this._cachedCapabilities = {
419 capabilities: capabilities,
420 lastUpdated: new Date().getTime(),
421 };
422 return capabilities;
423 });
424};
425
426// Crypto bits
427// ===========
428
429/**
430 * Initialise support for end-to-end encryption in this client
431 *
432 * You should call this method after creating the matrixclient, but *before*
433 * calling `startClient`, if you want to support end-to-end encryption.
434 *
435 * It will return a Promise which will resolve when the crypto layer has been
436 * successfully initialised.
437 */
438MatrixClient.prototype.initCrypto = async function() {
439 if (!isCryptoAvailable()) {
440 throw new Error(
441 `End-to-end encryption not supported in this js-sdk build: did ` +
442 `you remember to load the olm library?`,
443 );
444 }
445
446 if (this._crypto) {
447 console.warn("Attempt to re-initialise e2e encryption on MatrixClient");
448 return;
449 }
450
451 if (!this._sessionStore) {
452 // this is temporary, the sessionstore is supposed to be going away
453 throw new Error(`Cannot enable encryption: no sessionStore provided`);
454 }
455 if (!this._cryptoStore) {
456 // the cryptostore is provided by sdk.createClient, so this shouldn't happen
457 throw new Error(`Cannot enable encryption: no cryptoStore provided`);
458 }
459
460 // initialise the list of encrypted rooms (whether or not crypto is enabled)
461 await this._roomList.init();
462
463 const userId = this.getUserId();
464 if (userId === null) {
465 throw new Error(
466 `Cannot enable encryption on MatrixClient with unknown userId: ` +
467 `ensure userId is passed in createClient().`,
468 );
469 }
470 if (this.deviceId === null) {
471 throw new Error(
472 `Cannot enable encryption on MatrixClient with unknown deviceId: ` +
473 `ensure deviceId is passed in createClient().`,
474 );
475 }
476
477 const crypto = new Crypto(
478 this,
479 this._sessionStore,
480 userId, this.deviceId,
481 this.store,
482 this._cryptoStore,
483 this._roomList,
484 this._verificationMethods,
485 );
486
487 this.reEmitter.reEmit(crypto, [
488 "crypto.keyBackupFailed",
489 "crypto.keyBackupSessionsRemaining",
490 "crypto.roomKeyRequest",
491 "crypto.roomKeyRequestCancellation",
492 "crypto.warning",
493 ]);
494
495 await crypto.init();
496
497 this.olmVersion = Crypto.getOlmVersion();
498
499
500 // if crypto initialisation was successful, tell it to attach its event
501 // handlers.
502 crypto.registerEventHandlers(this);
503 this._crypto = crypto;
504};
505
506
507/**
508 * Is end-to-end crypto enabled for this client.
509 * @return {boolean} True if end-to-end is enabled.
510 */
511MatrixClient.prototype.isCryptoEnabled = function() {
512 return this._crypto !== null;
513};
514
515
516/**
517 * Get the Ed25519 key for this device
518 *
519 * @return {?string} base64-encoded ed25519 key. Null if crypto is
520 * disabled.
521 */
522MatrixClient.prototype.getDeviceEd25519Key = function() {
523 if (!this._crypto) {
524 return null;
525 }
526 return this._crypto.getDeviceEd25519Key();
527};
528
529/**
530 * Upload the device keys to the homeserver.
531 * @return {object} A promise that will resolve when the keys are uploaded.
532 */
533MatrixClient.prototype.uploadKeys = function() {
534 if (this._crypto === null) {
535 throw new Error("End-to-end encryption disabled");
536 }
537
538 return this._crypto.uploadDeviceKeys();
539};
540
541/**
542 * Download the keys for a list of users and stores the keys in the session
543 * store.
544 * @param {Array} userIds The users to fetch.
545 * @param {bool} forceDownload Always download the keys even if cached.
546 *
547 * @return {Promise} A promise which resolves to a map userId->deviceId->{@link
548 * module:crypto~DeviceInfo|DeviceInfo}.
549 */
550MatrixClient.prototype.downloadKeys = function(userIds, forceDownload) {
551 if (this._crypto === null) {
552 return Promise.reject(new Error("End-to-end encryption disabled"));
553 }
554 return this._crypto.downloadKeys(userIds, forceDownload);
555};
556
557/**
558 * Get the stored device keys for a user id
559 *
560 * @param {string} userId the user to list keys for.
561 *
562 * @return {Promise<module:crypto-deviceinfo[]>} list of devices
563 */
564MatrixClient.prototype.getStoredDevicesForUser = async function(userId) {
565 if (this._crypto === null) {
566 throw new Error("End-to-end encryption disabled");
567 }
568 return this._crypto.getStoredDevicesForUser(userId) || [];
569};
570
571/**
572 * Get the stored device key for a user id and device id
573 *
574 * @param {string} userId the user to list keys for.
575 * @param {string} deviceId unique identifier for the device
576 *
577 * @return {Promise<?module:crypto-deviceinfo>} device or null
578 */
579MatrixClient.prototype.getStoredDevice = async function(userId, deviceId) {
580 if (this._crypto === null) {
581 throw new Error("End-to-end encryption disabled");
582 }
583 return this._crypto.getStoredDevice(userId, deviceId) || null;
584};
585
586/**
587 * Mark the given device as verified
588 *
589 * @param {string} userId owner of the device
590 * @param {string} deviceId unique identifier for the device
591 *
592 * @param {boolean=} verified whether to mark the device as verified. defaults
593 * to 'true'.
594 *
595 * @returns {Promise}
596 *
597 * @fires module:client~event:MatrixClient"deviceVerificationChanged"
598 */
599MatrixClient.prototype.setDeviceVerified = function(userId, deviceId, verified) {
600 if (verified === undefined) {
601 verified = true;
602 }
603 const prom = _setDeviceVerification(this, userId, deviceId, verified, null);
604
605 // if one of the user's own devices is being marked as verified / unverified,
606 // check the key backup status, since whether or not we use this depends on
607 // whether it has a signature from a verified device
608 if (userId == this.credentials.userId) {
609 this._crypto.checkKeyBackup();
610 }
611 return prom;
612};
613
614/**
615 * Mark the given device as blocked/unblocked
616 *
617 * @param {string} userId owner of the device
618 * @param {string} deviceId unique identifier for the device
619 *
620 * @param {boolean=} blocked whether to mark the device as blocked. defaults
621 * to 'true'.
622 *
623 * @returns {Promise}
624 *
625 * @fires module:client~event:MatrixClient"deviceVerificationChanged"
626 */
627MatrixClient.prototype.setDeviceBlocked = function(userId, deviceId, blocked) {
628 if (blocked === undefined) {
629 blocked = true;
630 }
631 return _setDeviceVerification(this, userId, deviceId, null, blocked);
632};
633
634/**
635 * Mark the given device as known/unknown
636 *
637 * @param {string} userId owner of the device
638 * @param {string} deviceId unique identifier for the device
639 *
640 * @param {boolean=} known whether to mark the device as known. defaults
641 * to 'true'.
642 *
643 * @returns {Promise}
644 *
645 * @fires module:client~event:MatrixClient"deviceVerificationChanged"
646 */
647MatrixClient.prototype.setDeviceKnown = function(userId, deviceId, known) {
648 if (known === undefined) {
649 known = true;
650 }
651 return _setDeviceVerification(this, userId, deviceId, null, null, known);
652};
653
654async function _setDeviceVerification(
655 client, userId, deviceId, verified, blocked, known,
656) {
657 if (!client._crypto) {
658 throw new Error("End-to-End encryption disabled");
659 }
660 const dev = await client._crypto.setDeviceVerification(
661 userId, deviceId, verified, blocked, known,
662 );
663 client.emit("deviceVerificationChanged", userId, deviceId, dev);
664}
665
666/**
667 * Request a key verification from another user.
668 *
669 * @param {string} userId the user to request verification with
670 * @param {Array} devices array of device IDs to send requests to. Defaults to
671 * all devices owned by the user
672 * @param {Array} methods array of verification methods to use. Defaults to
673 * all known methods
674 *
675 * @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
676 * when the request is accepted by the other user
677 */
678MatrixClient.prototype.requestVerification = function(userId, devices, methods) {
679 if (this._crypto === null) {
680 throw new Error("End-to-end encryption disabled");
681 }
682 return this._crypto.requestVerification(userId, devices);
683};
684
685/**
686 * Begin a key verification.
687 *
688 * @param {string} method the verification method to use
689 * @param {string} userId the user to verify keys with
690 * @param {string} deviceId the device to verify
691 *
692 * @returns {module:crypto/verification/Base} a verification object
693 */
694MatrixClient.prototype.beginKeyVerification = function(
695 method, userId, deviceId,
696) {
697 if (this._crypto === null) {
698 throw new Error("End-to-end encryption disabled");
699 }
700 return this._crypto.beginKeyVerification(method, userId, deviceId);
701};
702
703/**
704 * Set the global override for whether the client should ever send encrypted
705 * messages to unverified devices. This provides the default for rooms which
706 * do not specify a value.
707 *
708 * @param {boolean} value whether to blacklist all unverified devices by default
709 */
710MatrixClient.prototype.setGlobalBlacklistUnverifiedDevices = function(value) {
711 if (this._crypto === null) {
712 throw new Error("End-to-end encryption disabled");
713 }
714 this._crypto.setGlobalBlacklistUnverifiedDevices(value);
715};
716
717/**
718 * @return {boolean} whether to blacklist all unverified devices by default
719 */
720MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() {
721 if (this._crypto === null) {
722 throw new Error("End-to-end encryption disabled");
723 }
724 return this._crypto.getGlobalBlacklistUnverifiedDevices();
725};
726
727/**
728 * Get e2e information on the device that sent an event
729 *
730 * @param {MatrixEvent} event event to be checked
731 *
732 * @return {Promise<module:crypto/deviceinfo?>}
733 */
734MatrixClient.prototype.getEventSenderDeviceInfo = async function(event) {
735 if (!this._crypto) {
736 return null;
737 }
738
739 return this._crypto.getEventSenderDeviceInfo(event);
740};
741
742/**
743 * Check if the sender of an event is verified
744 *
745 * @param {MatrixEvent} event event to be checked
746 *
747 * @return {boolean} true if the sender of this event has been verified using
748 * {@link module:client~MatrixClient#setDeviceVerified|setDeviceVerified}.
749 */
750MatrixClient.prototype.isEventSenderVerified = async function(event) {
751 const device = await this.getEventSenderDeviceInfo(event);
752 if (!device) {
753 return false;
754 }
755 return device.isVerified();
756};
757
758/**
759 * Cancel a room key request for this event if one is ongoing and resend the
760 * request.
761 * @param {MatrixEvent} event event of which to cancel and resend the room
762 * key request.
763 */
764MatrixClient.prototype.cancelAndResendEventRoomKeyRequest = function(event) {
765 event.cancelAndResendKeyRequest(this._crypto);
766};
767
768/**
769 * Enable end-to-end encryption for a room.
770 * @param {string} roomId The room ID to enable encryption in.
771 * @param {object} config The encryption config for the room.
772 * @return {Promise} A promise that will resolve when encryption is set up.
773 */
774MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
775 if (!this._crypto) {
776 throw new Error("End-to-End encryption disabled");
777 }
778 return this._crypto.setRoomEncryption(roomId, config);
779};
780
781/**
782 * Whether encryption is enabled for a room.
783 * @param {string} roomId the room id to query.
784 * @return {bool} whether encryption is enabled.
785 */
786MatrixClient.prototype.isRoomEncrypted = function(roomId) {
787 const room = this.getRoom(roomId);
788 if (!room) {
789 // we don't know about this room, so can't determine if it should be
790 // encrypted. Let's assume not.
791 return false;
792 }
793
794 // if there is an 'm.room.encryption' event in this room, it should be
795 // encrypted (independently of whether we actually support encryption)
796 const ev = room.currentState.getStateEvents("m.room.encryption", "");
797 if (ev) {
798 return true;
799 }
800
801 // we don't have an m.room.encrypted event, but that might be because
802 // the server is hiding it from us. Check the store to see if it was
803 // previously encrypted.
804 return this._roomList.isRoomEncrypted(roomId);
805};
806
807/**
808 * Forces the current outbound group session to be discarded such
809 * that another one will be created next time an event is sent.
810 *
811 * @param {string} roomId The ID of the room to discard the session for
812 *
813 * This should not normally be necessary.
814 */
815MatrixClient.prototype.forceDiscardSession = function(roomId) {
816 if (!this._crypto) {
817 throw new Error("End-to-End encryption disabled");
818 }
819 this._crypto.forceDiscardSession(roomId);
820};
821
822/**
823 * Get a list containing all of the room keys
824 *
825 * This should be encrypted before returning it to the user.
826 *
827 * @return {module:client.Promise} a promise which resolves to a list of
828 * session export objects
829 */
830MatrixClient.prototype.exportRoomKeys = function() {
831 if (!this._crypto) {
832 return Promise.reject(new Error("End-to-end encryption disabled"));
833 }
834 return this._crypto.exportRoomKeys();
835};
836
837/**
838 * Import a list of room keys previously exported by exportRoomKeys
839 *
840 * @param {Object[]} keys a list of session export objects
841 *
842 * @return {module:client.Promise} a promise which resolves when the keys
843 * have been imported
844 */
845MatrixClient.prototype.importRoomKeys = function(keys) {
846 if (!this._crypto) {
847 throw new Error("End-to-end encryption disabled");
848 }
849 return this._crypto.importRoomKeys(keys);
850};
851
852/**
853 * Force a re-check of the local key backup status against
854 * what's on the server.
855 *
856 * @returns {Object} Object with backup info (as returned by
857 * getKeyBackupVersion) in backupInfo and
858 * trust information (as returned by isKeyBackupTrusted)
859 * in trustInfo.
860 */
861MatrixClient.prototype.checkKeyBackup = function() {
862 return this._crypto.checkKeyBackup();
863};
864
865/**
866 * Get information about the current key backup.
867 * @returns {Promise} Information object from API or null
868 */
869MatrixClient.prototype.getKeyBackupVersion = function() {
870 return this._http.authedRequest(
871 undefined, "GET", "/room_keys/version",
872 ).then((res) => {
873 if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) {
874 const err = "Unknown backup algorithm: " + res.algorithm;
875 return Promise.reject(err);
876 } else if (!(typeof res.auth_data === "object")
877 || !res.auth_data.public_key) {
878 const err = "Invalid backup data returned";
879 return Promise.reject(err);
880 } else {
881 return res;
882 }
883 }).catch((e) => {
884 if (e.errcode === 'M_NOT_FOUND') {
885 return null;
886 } else {
887 throw e;
888 }
889 });
890};
891
892/**
893 * @param {object} info key backup info dict from getKeyBackupVersion()
894 * @return {object} {
895 * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
896 * sigs: [
897 * valid: [bool],
898 * device: [DeviceInfo],
899 * ]
900 * }
901 */
902MatrixClient.prototype.isKeyBackupTrusted = function(info) {
903 return this._crypto.isKeyBackupTrusted(info);
904};
905
906/**
907 * @returns {bool} true if the client is configured to back up keys to
908 * the server, otherwise false.
909 */
910MatrixClient.prototype.getKeyBackupEnabled = function() {
911 if (this._crypto === null) {
912 throw new Error("End-to-end encryption disabled");
913 }
914 return Boolean(this._crypto.backupKey);
915};
916
917/**
918 * Enable backing up of keys, using data previously returned from
919 * getKeyBackupVersion.
920 *
921 * @param {object} info Backup information object as returned by getKeyBackupVersion
922 */
923MatrixClient.prototype.enableKeyBackup = function(info) {
924 if (this._crypto === null) {
925 throw new Error("End-to-end encryption disabled");
926 }
927
928 this._crypto.backupInfo = info;
929 if (this._crypto.backupKey) this._crypto.backupKey.free();
930 this._crypto.backupKey = new global.Olm.PkEncryption();
931 this._crypto.backupKey.set_recipient_key(info.auth_data.public_key);
932
933 this.emit('crypto.keyBackupStatus', true);
934
935 // There may be keys left over from a partially completed backup, so
936 // schedule a send to check.
937 this._crypto.scheduleKeyBackupSend();
938};
939
940/**
941 * Disable backing up of keys.
942 */
943MatrixClient.prototype.disableKeyBackup = function() {
944 if (this._crypto === null) {
945 throw new Error("End-to-end encryption disabled");
946 }
947
948 this._crypto.backupInfo = null;
949 if (this._crypto.backupKey) this._crypto.backupKey.free();
950 this._crypto.backupKey = null;
951
952 this.emit('crypto.keyBackupStatus', false);
953};
954
955/**
956 * Set up the data required to create a new backup version. The backup version
957 * will not be created and enabled until createKeyBackupVersion is called.
958 *
959 * @param {string} password Passphrase string that can be entered by the user
960 * when restoring the backup as an alternative to entering the recovery key.
961 * Optional.
962 *
963 * @returns {Promise<object>} Object that can be passed to createKeyBackupVersion and
964 * additionally has a 'recovery_key' member with the user-facing recovery key string.
965 */
966MatrixClient.prototype.prepareKeyBackupVersion = async function(password) {
967 if (this._crypto === null) {
968 throw new Error("End-to-end encryption disabled");
969 }
970
971 const decryption = new global.Olm.PkDecryption();
972 try {
973 let publicKey;
974 const authData = {};
975 if (password) {
976 const keyInfo = await keyForNewBackup(password);
977 publicKey = decryption.init_with_private_key(keyInfo.key);
978 authData.private_key_salt = keyInfo.salt;
979 authData.private_key_iterations = keyInfo.iterations;
980 } else {
981 publicKey = decryption.generate_key();
982 }
983
984 authData.public_key = publicKey;
985
986 return {
987 algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
988 auth_data: authData,
989 recovery_key: encodeRecoveryKey(decryption.get_private_key()),
990 };
991 } finally {
992 decryption.free();
993 }
994};
995
996/**
997 * Create a new key backup version and enable it, using the information return
998 * from prepareKeyBackupVersion.
999 *
1000 * @param {object} info Info object from prepareKeyBackupVersion
1001 * @returns {Promise<object>} Object with 'version' param indicating the version created
1002 */
1003MatrixClient.prototype.createKeyBackupVersion = function(info) {
1004 if (this._crypto === null) {
1005 throw new Error("End-to-end encryption disabled");
1006 }
1007
1008 const data = {
1009 algorithm: info.algorithm,
1010 auth_data: info.auth_data,
1011 };
1012 return this._crypto._signObject(data.auth_data).then(() => {
1013 return this._http.authedRequest(
1014 undefined, "POST", "/room_keys/version", undefined, data,
1015 );
1016 }).then((res) => {
1017 this.enableKeyBackup({
1018 algorithm: info.algorithm,
1019 auth_data: info.auth_data,
1020 version: res.version,
1021 });
1022 return res;
1023 });
1024};
1025
1026MatrixClient.prototype.deleteKeyBackupVersion = function(version) {
1027 if (this._crypto === null) {
1028 throw new Error("End-to-end encryption disabled");
1029 }
1030
1031 // If we're currently backing up to this backup... stop.
1032 // (We start using it automatically in createKeyBackupVersion
1033 // so this is symmetrical).
1034 if (this._crypto.backupInfo && this._crypto.backupInfo.version === version) {
1035 this.disableKeyBackup();
1036 }
1037
1038 const path = utils.encodeUri("/room_keys/version/$version", {
1039 $version: version,
1040 });
1041
1042 return this._http.authedRequest(
1043 undefined, "DELETE", path, undefined, undefined,
1044 );
1045};
1046
1047MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version) {
1048 let path;
1049 if (sessionId !== undefined) {
1050 path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", {
1051 $roomId: roomId,
1052 $sessionId: sessionId,
1053 });
1054 } else if (roomId !== undefined) {
1055 path = utils.encodeUri("/room_keys/keys/$roomId", {
1056 $roomId: roomId,
1057 });
1058 } else {
1059 path = "/room_keys/keys";
1060 }
1061 const queryData = version === undefined ? undefined : { version: version };
1062 return {
1063 path: path,
1064 queryData: queryData,
1065 };
1066};
1067
1068/**
1069 * Back up session keys to the homeserver.
1070 * @param {string} roomId ID of the room that the keys are for Optional.
1071 * @param {string} sessionId ID of the session that the keys are for Optional.
1072 * @param {integer} version backup version Optional.
1073 * @param {object} data Object keys to send
1074 * @return {module:client.Promise} a promise that will resolve when the keys
1075 * are uploaded
1076 */
1077MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data) {
1078 if (this._crypto === null) {
1079 throw new Error("End-to-end encryption disabled");
1080 }
1081
1082 const path = this._makeKeyBackupPath(roomId, sessionId, version);
1083 return this._http.authedRequest(
1084 undefined, "PUT", path.path, path.queryData, data,
1085 );
1086};
1087
1088/**
1089 * Marks all group sessions as needing to be backed up and schedules them to
1090 * upload in the background as soon as possible.
1091 */
1092MatrixClient.prototype.scheduleAllGroupSessionsForBackup = async function() {
1093 if (this._crypto === null) {
1094 throw new Error("End-to-end encryption disabled");
1095 }
1096
1097 await this._crypto.scheduleAllGroupSessionsForBackup();
1098};
1099
1100MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
1101 try {
1102 decodeRecoveryKey(recoveryKey);
1103 return true;
1104 } catch (e) {
1105 return false;
1106 }
1107};
1108
1109MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
1110
1111MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
1112 password, targetRoomId, targetSessionId, backupInfo,
1113) {
1114 const privKey = await keyForExistingBackup(backupInfo, password);
1115 return this._restoreKeyBackup(
1116 privKey, targetRoomId, targetSessionId, backupInfo,
1117 );
1118};
1119
1120MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function(
1121 recoveryKey, targetRoomId, targetSessionId, backupInfo,
1122) {
1123 const privKey = decodeRecoveryKey(recoveryKey);
1124 return this._restoreKeyBackup(
1125 privKey, targetRoomId, targetSessionId, backupInfo,
1126 );
1127};
1128
1129MatrixClient.prototype._restoreKeyBackup = function(
1130 privKey, targetRoomId, targetSessionId, backupInfo,
1131) {
1132 if (this._crypto === null) {
1133 throw new Error("End-to-end encryption disabled");
1134 }
1135 let totalKeyCount = 0;
1136 let keys = [];
1137
1138 const path = this._makeKeyBackupPath(
1139 targetRoomId, targetSessionId, backupInfo.version,
1140 );
1141
1142 const decryption = new global.Olm.PkDecryption();
1143 let backupPubKey;
1144 try {
1145 backupPubKey = decryption.init_with_private_key(privKey);
1146 } catch(e) {
1147 decryption.free();
1148 throw e;
1149 }
1150
1151 // If the pubkey computed from the private data we've been given
1152 // doesn't match the one in the auth_data, the user has enetered
1153 // a different recovery key / the wrong passphrase.
1154 if (backupPubKey !== backupInfo.auth_data.public_key) {
1155 return Promise.reject({errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY});
1156 }
1157
1158 return this._http.authedRequest(
1159 undefined, "GET", path.path, path.queryData,
1160 ).then((res) => {
1161 if (res.rooms) {
1162 for (const [roomId, roomData] of Object.entries(res.rooms)) {
1163 if (!roomData.sessions) continue;
1164
1165 totalKeyCount += Object.keys(roomData.sessions).length;
1166 const roomKeys = keysFromRecoverySession(
1167 roomData.sessions, decryption, roomId, roomKeys,
1168 );
1169 for (const k of roomKeys) {
1170 k.room_id = roomId;
1171 keys.push(k);
1172 }
1173 }
1174 } else if (res.sessions) {
1175 totalKeyCount = Object.keys(res.sessions).length;
1176 keys = keysFromRecoverySession(
1177 res.sessions, decryption, targetRoomId, keys,
1178 );
1179 } else {
1180 totalKeyCount = 1;
1181 try {
1182 const key = keyFromRecoverySession(res, decryption);
1183 key.room_id = targetRoomId;
1184 key.session_id = targetSessionId;
1185 keys.push(key);
1186 } catch (e) {
1187 console.log("Failed to decrypt session from backup");
1188 }
1189 }
1190
1191 return this.importRoomKeys(keys);
1192 }).then(() => {
1193 return this._crypto.setTrustedBackupPubKey(backupPubKey);
1194 }).then(() => {
1195 return {total: totalKeyCount, imported: keys.length};
1196 }).finally(() => {
1197 decryption.free();
1198 });
1199};
1200
1201MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version) {
1202 if (this._crypto === null) {
1203 throw new Error("End-to-end encryption disabled");
1204 }
1205
1206 const path = this._makeKeyBackupPath(roomId, sessionId, version);
1207 return this._http.authedRequest(
1208 undefined, "DELETE", path.path, path.queryData,
1209 );
1210};
1211
1212// Group ops
1213// =========
1214// Operations on groups that come down the sync stream (ie. ones the
1215// user is a member of or invited to)
1216
1217/**
1218 * Get the group for the given group ID.
1219 * This function will return a valid group for any group for which a Group event
1220 * has been emitted.
1221 * @param {string} groupId The group ID
1222 * @return {Group} The Group or null if the group is not known or there is no data store.
1223 */
1224MatrixClient.prototype.getGroup = function(groupId) {
1225 return this.store.getGroup(groupId);
1226};
1227
1228/**
1229 * Retrieve all known groups.
1230 * @return {Group[]} A list of groups, or an empty list if there is no data store.
1231 */
1232MatrixClient.prototype.getGroups = function() {
1233 return this.store.getGroups();
1234};
1235
1236/**
1237 * Get the config for the media repository.
1238 * @param {module:client.callback} callback Optional.
1239 * @return {module:client.Promise} Resolves with an object containing the config.
1240 */
1241MatrixClient.prototype.getMediaConfig = function(callback) {
1242 return this._http.authedRequestWithPrefix(
1243 callback, "GET", "/config", undefined, undefined, httpApi.PREFIX_MEDIA_R0,
1244 );
1245};
1246
1247// Room ops
1248// ========
1249
1250/**
1251 * Get the room for the given room ID.
1252 * This function will return a valid room for any room for which a Room event
1253 * has been emitted. Note in particular that other events, eg. RoomState.members
1254 * will be emitted for a room before this function will return the given room.
1255 * @param {string} roomId The room ID
1256 * @return {Room} The Room or null if it doesn't exist or there is no data store.
1257 */
1258MatrixClient.prototype.getRoom = function(roomId) {
1259 return this.store.getRoom(roomId);
1260};
1261
1262/**
1263 * Retrieve all known rooms.
1264 * @return {Room[]} A list of rooms, or an empty list if there is no data store.
1265 */
1266MatrixClient.prototype.getRooms = function() {
1267 return this.store.getRooms();
1268};
1269
1270/**
1271 * Retrieve all rooms that should be displayed to the user
1272 * This is essentially getRooms() with some rooms filtered out, eg. old versions
1273 * of rooms that have been replaced or (in future) other rooms that have been
1274 * marked at the protocol level as not to be displayed to the user.
1275 * @return {Room[]} A list of rooms, or an empty list if there is no data store.
1276 */
1277MatrixClient.prototype.getVisibleRooms = function() {
1278 const allRooms = this.store.getRooms();
1279
1280 const replacedRooms = new Set();
1281 for (const r of allRooms) {
1282 const createEvent = r.currentState.getStateEvents('m.room.create', '');
1283 // invites are included in this list and we don't know their create events yet
1284 if (createEvent) {
1285 const predecessor = createEvent.getContent()['predecessor'];
1286 if (predecessor && predecessor['room_id']) {
1287 replacedRooms.add(predecessor['room_id']);
1288 }
1289 }
1290 }
1291
1292 return allRooms.filter((r) => {
1293 const tombstone = r.currentState.getStateEvents('m.room.tombstone', '');
1294 if (tombstone && replacedRooms.has(r.roomId)) {
1295 return false;
1296 }
1297 return true;
1298 });
1299};
1300
1301/**
1302 * Retrieve a user.
1303 * @param {string} userId The user ID to retrieve.
1304 * @return {?User} A user or null if there is no data store or the user does
1305 * not exist.
1306 */
1307MatrixClient.prototype.getUser = function(userId) {
1308 return this.store.getUser(userId);
1309};
1310
1311/**
1312 * Retrieve all known users.
1313 * @return {User[]} A list of users, or an empty list if there is no data store.
1314 */
1315MatrixClient.prototype.getUsers = function() {
1316 return this.store.getUsers();
1317};
1318
1319// User Account Data operations
1320// ============================
1321
1322/**
1323 * Set account data event for the current user.
1324 * @param {string} eventType The event type
1325 * @param {Object} contents the contents object for the event
1326 * @param {module:client.callback} callback Optional.
1327 * @return {module:client.Promise} Resolves: TODO
1328 * @return {module:http-api.MatrixError} Rejects: with an error response.
1329 */
1330MatrixClient.prototype.setAccountData = function(eventType, contents, callback) {
1331 const path = utils.encodeUri("/user/$userId/account_data/$type", {
1332 $userId: this.credentials.userId,
1333 $type: eventType,
1334 });
1335 return this._http.authedRequest(
1336 callback, "PUT", path, undefined, contents,
1337 );
1338};
1339
1340/**
1341 * Get account data event of given type for the current user.
1342 * @param {string} eventType The event type
1343 * @return {?object} The contents of the given account data event
1344 */
1345MatrixClient.prototype.getAccountData = function(eventType) {
1346 return this.store.getAccountData(eventType);
1347};
1348
1349/**
1350 * Gets the users that are ignored by this client
1351 * @returns {string[]} The array of users that are ignored (empty if none)
1352 */
1353MatrixClient.prototype.getIgnoredUsers = function() {
1354 const event = this.getAccountData("m.ignored_user_list");
1355 if (!event || !event.getContent() || !event.getContent()["ignored_users"]) return [];
1356 return Object.keys(event.getContent()["ignored_users"]);
1357};
1358
1359/**
1360 * Sets the users that the current user should ignore.
1361 * @param {string[]} userIds the user IDs to ignore
1362 * @param {module:client.callback} [callback] Optional.
1363 * @return {module:client.Promise} Resolves: Account data event
1364 * @return {module:http-api.MatrixError} Rejects: with an error response.
1365 */
1366MatrixClient.prototype.setIgnoredUsers = function(userIds, callback) {
1367 const content = {ignored_users: {}};
1368 userIds.map((u) => content.ignored_users[u] = {});
1369 return this.setAccountData("m.ignored_user_list", content, callback);
1370};
1371
1372/**
1373 * Gets whether or not a specific user is being ignored by this client.
1374 * @param {string} userId the user ID to check
1375 * @returns {boolean} true if the user is ignored, false otherwise
1376 */
1377MatrixClient.prototype.isUserIgnored = function(userId) {
1378 return this.getIgnoredUsers().indexOf(userId) !== -1;
1379};
1380
1381// Room operations
1382// ===============
1383
1384/**
1385 * Join a room. If you have already joined the room, this will no-op.
1386 * @param {string} roomIdOrAlias The room ID or room alias to join.
1387 * @param {Object} opts Options when joining the room.
1388 * @param {boolean} opts.syncRoom True to do a room initial sync on the resulting
1389 * room. If false, the <strong>returned Room object will have no current state.
1390 * </strong> Default: true.
1391 * @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite,
1392 * the signing URL is passed in this parameter.
1393 * @param {string[]} opts.viaServers The server names to try and join through in
1394 * addition to those that are automatically chosen.
1395 * @param {module:client.callback} callback Optional.
1396 * @return {module:client.Promise} Resolves: Room object.
1397 * @return {module:http-api.MatrixError} Rejects: with an error response.
1398 */
1399MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
1400 // to help people when upgrading..
1401 if (utils.isFunction(opts)) {
1402 throw new Error("Expected 'opts' object, got function.");
1403 }
1404 opts = opts || {};
1405 if (opts.syncRoom === undefined) {
1406 opts.syncRoom = true;
1407 }
1408
1409 const room = this.getRoom(roomIdOrAlias);
1410 if (room && room.hasMembershipState(this.credentials.userId, "join")) {
1411 return Promise.resolve(room);
1412 }
1413
1414 let sign_promise = Promise.resolve();
1415
1416 if (opts.inviteSignUrl) {
1417 sign_promise = this._http.requestOtherUrl(
1418 undefined, 'POST',
1419 opts.inviteSignUrl, { mxid: this.credentials.userId },
1420 );
1421 }
1422
1423 const queryString = {};
1424 if (opts.viaServers) {
1425 queryString["server_name"] = opts.viaServers;
1426 }
1427
1428 const reqOpts = {qsStringifyOptions: {arrayFormat: 'repeat'}};
1429
1430 const defer = Promise.defer();
1431
1432 const self = this;
1433 sign_promise.then(function(signed_invite_object) {
1434 const data = {};
1435 if (signed_invite_object) {
1436 data.third_party_signed = signed_invite_object;
1437 }
1438
1439 const path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias});
1440 return self._http.authedRequest(
1441 undefined, "POST", path, queryString, data, reqOpts);
1442 }).then(function(res) {
1443 const roomId = res.room_id;
1444 const syncApi = new SyncApi(self, self._clientOpts);
1445 const room = syncApi.createRoom(roomId);
1446 if (opts.syncRoom) {
1447 // v2 will do this for us
1448 // return syncApi.syncRoom(room);
1449 }
1450 return Promise.resolve(room);
1451 }).done(function(room) {
1452 _resolve(callback, defer, room);
1453 }, function(err) {
1454 _reject(callback, defer, err);
1455 });
1456 return defer.promise;
1457};
1458
1459/**
1460 * Resend an event.
1461 * @param {MatrixEvent} event The event to resend.
1462 * @param {Room} room Optional. The room the event is in. Will update the
1463 * timeline entry if provided.
1464 * @return {module:client.Promise} Resolves: TODO
1465 * @return {module:http-api.MatrixError} Rejects: with an error response.
1466 */
1467MatrixClient.prototype.resendEvent = function(event, room) {
1468 _updatePendingEventStatus(room, event, EventStatus.SENDING);
1469 return _sendEvent(this, room, event);
1470};
1471
1472/**
1473 * Cancel a queued or unsent event.
1474 *
1475 * @param {MatrixEvent} event Event to cancel
1476 * @throws Error if the event is not in QUEUED or NOT_SENT state
1477 */
1478MatrixClient.prototype.cancelPendingEvent = function(event) {
1479 if ([EventStatus.QUEUED, EventStatus.NOT_SENT].indexOf(event.status) < 0) {
1480 throw new Error("cannot cancel an event with status " + event.status);
1481 }
1482
1483 // first tell the scheduler to forget about it, if it's queued
1484 if (this.scheduler) {
1485 this.scheduler.removeEventFromQueue(event);
1486 }
1487
1488 // then tell the room about the change of state, which will remove it
1489 // from the room's list of pending events.
1490 const room = this.getRoom(event.getRoomId());
1491 _updatePendingEventStatus(room, event, EventStatus.CANCELLED);
1492};
1493
1494/**
1495 * @param {string} roomId
1496 * @param {string} name
1497 * @param {module:client.callback} callback Optional.
1498 * @return {module:client.Promise} Resolves: TODO
1499 * @return {module:http-api.MatrixError} Rejects: with an error response.
1500 */
1501MatrixClient.prototype.setRoomName = function(roomId, name, callback) {
1502 return this.sendStateEvent(roomId, "m.room.name", {name: name},
1503 undefined, callback);
1504};
1505
1506/**
1507 * @param {string} roomId
1508 * @param {string} topic
1509 * @param {module:client.callback} callback Optional.
1510 * @return {module:client.Promise} Resolves: TODO
1511 * @return {module:http-api.MatrixError} Rejects: with an error response.
1512 */
1513MatrixClient.prototype.setRoomTopic = function(roomId, topic, callback) {
1514 return this.sendStateEvent(roomId, "m.room.topic", {topic: topic},
1515 undefined, callback);
1516};
1517
1518/**
1519 * @param {string} roomId
1520 * @param {module:client.callback} callback Optional.
1521 * @return {module:client.Promise} Resolves: TODO
1522 * @return {module:http-api.MatrixError} Rejects: with an error response.
1523 */
1524MatrixClient.prototype.getRoomTags = function(roomId, callback) {
1525 const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/", {
1526 $userId: this.credentials.userId,
1527 $roomId: roomId,
1528 });
1529 return this._http.authedRequest(
1530 callback, "GET", path, undefined,
1531 );
1532};
1533
1534/**
1535 * @param {string} roomId
1536 * @param {string} tagName name of room tag to be set
1537 * @param {object} metadata associated with that tag to be stored
1538 * @param {module:client.callback} callback Optional.
1539 * @return {module:client.Promise} Resolves: TODO
1540 * @return {module:http-api.MatrixError} Rejects: with an error response.
1541 */
1542MatrixClient.prototype.setRoomTag = function(roomId, tagName, metadata, callback) {
1543 const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", {
1544 $userId: this.credentials.userId,
1545 $roomId: roomId,
1546 $tag: tagName,
1547 });
1548 return this._http.authedRequest(
1549 callback, "PUT", path, undefined, metadata,
1550 );
1551};
1552
1553/**
1554 * @param {string} roomId
1555 * @param {string} tagName name of room tag to be removed
1556 * @param {module:client.callback} callback Optional.
1557 * @return {module:client.Promise} Resolves: TODO
1558 * @return {module:http-api.MatrixError} Rejects: with an error response.
1559 */
1560MatrixClient.prototype.deleteRoomTag = function(roomId, tagName, callback) {
1561 const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", {
1562 $userId: this.credentials.userId,
1563 $roomId: roomId,
1564 $tag: tagName,
1565 });
1566 return this._http.authedRequest(
1567 callback, "DELETE", path, undefined, undefined,
1568 );
1569};
1570
1571/**
1572 * @param {string} roomId
1573 * @param {string} eventType event type to be set
1574 * @param {object} content event content
1575 * @param {module:client.callback} callback Optional.
1576 * @return {module:client.Promise} Resolves: TODO
1577 * @return {module:http-api.MatrixError} Rejects: with an error response.
1578 */
1579MatrixClient.prototype.setRoomAccountData = function(roomId, eventType,
1580 content, callback) {
1581 const path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", {
1582 $userId: this.credentials.userId,
1583 $roomId: roomId,
1584 $type: eventType,
1585 });
1586 return this._http.authedRequest(
1587 callback, "PUT", path, undefined, content,
1588 );
1589};
1590
1591/**
1592 * Set a user's power level.
1593 * @param {string} roomId
1594 * @param {string} userId
1595 * @param {Number} powerLevel
1596 * @param {MatrixEvent} event
1597 * @param {module:client.callback} callback Optional.
1598 * @return {module:client.Promise} Resolves: TODO
1599 * @return {module:http-api.MatrixError} Rejects: with an error response.
1600 */
1601MatrixClient.prototype.setPowerLevel = function(roomId, userId, powerLevel,
1602 event, callback) {
1603 let content = {
1604 users: {},
1605 };
1606 if (event && event.getType() === "m.room.power_levels") {
1607 // take a copy of the content to ensure we don't corrupt
1608 // existing client state with a failed power level change
1609 content = utils.deepCopy(event.getContent());
1610 }
1611 content.users[userId] = powerLevel;
1612 const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", {
1613 $roomId: roomId,
1614 });
1615 return this._http.authedRequest(
1616 callback, "PUT", path, undefined, content,
1617 );
1618};
1619
1620/**
1621 * @param {string} roomId
1622 * @param {string} eventType
1623 * @param {Object} content
1624 * @param {string} txnId Optional.
1625 * @param {module:client.callback} callback Optional.
1626 * @return {module:client.Promise} Resolves: TODO
1627 * @return {module:http-api.MatrixError} Rejects: with an error response.
1628 */
1629MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
1630 callback) {
1631 if (utils.isFunction(txnId)) {
1632 callback = txnId; txnId = undefined;
1633 }
1634
1635 if (!txnId) {
1636 txnId = this.makeTxnId();
1637 }
1638
1639 console.log(`sendEvent of type ${eventType} in ${roomId} with txnId ${txnId}`);
1640
1641 // we always construct a MatrixEvent when sending because the store and
1642 // scheduler use them. We'll extract the params back out if it turns out
1643 // the client has no scheduler or store.
1644 const room = this.getRoom(roomId);
1645 const localEvent = new MatrixEvent({
1646 event_id: "~" + roomId + ":" + txnId,
1647 user_id: this.credentials.userId,
1648 room_id: roomId,
1649 type: eventType,
1650 origin_server_ts: new Date().getTime(),
1651 content: content,
1652 });
1653 localEvent._txnId = txnId;
1654 localEvent.status = EventStatus.SENDING;
1655
1656 // add this event immediately to the local store as 'sending'.
1657 if (room) {
1658 room.addPendingEvent(localEvent, txnId);
1659 }
1660
1661 // addPendingEvent can change the state to NOT_SENT if it believes
1662 // that there's other events that have failed. We won't bother to
1663 // try sending the event if the state has changed as such.
1664 if (localEvent.status === EventStatus.NOT_SENT) {
1665 return Promise.reject(new Error("Event blocked by other events not yet sent"));
1666 }
1667
1668 return _sendEvent(this, room, localEvent, callback);
1669};
1670
1671
1672// encrypts the event if necessary
1673// adds the event to the queue, or sends it
1674// marks the event as sent/unsent
1675// returns a promise which resolves with the result of the send request
1676function _sendEvent(client, room, event, callback) {
1677 // Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections,
1678 // so that we can handle synchronous and asynchronous exceptions with the
1679 // same code path.
1680 return Promise.resolve().then(function() {
1681 const encryptionPromise = _encryptEventIfNeeded(client, event, room);
1682
1683 if (!encryptionPromise) {
1684 return null;
1685 }
1686
1687 _updatePendingEventStatus(room, event, EventStatus.ENCRYPTING);
1688 return encryptionPromise.then(() => {
1689 _updatePendingEventStatus(room, event, EventStatus.SENDING);
1690 });
1691 }).then(function() {
1692 let promise;
1693 // this event may be queued
1694 if (client.scheduler) {
1695 // if this returns a promsie then the scheduler has control now and will
1696 // resolve/reject when it is done. Internally, the scheduler will invoke
1697 // processFn which is set to this._sendEventHttpRequest so the same code
1698 // path is executed regardless.
1699 promise = client.scheduler.queueEvent(event);
1700 if (promise && client.scheduler.getQueueForEvent(event).length > 1) {
1701 // event is processed FIFO so if the length is 2 or more we know
1702 // this event is stuck behind an earlier event.
1703 _updatePendingEventStatus(room, event, EventStatus.QUEUED);
1704 }
1705 }
1706
1707 if (!promise) {
1708 promise = _sendEventHttpRequest(client, event);
1709 }
1710 return promise;
1711 }).then(function(res) { // the request was sent OK
1712 if (room) {
1713 room.updatePendingEvent(event, EventStatus.SENT, res.event_id);
1714 }
1715 if (callback) {
1716 callback(null, res);
1717 }
1718 return res;
1719 }, function(err) {
1720 // the request failed to send.
1721 console.error("Error sending event", err.stack || err);
1722
1723 try {
1724 // set the error on the event before we update the status:
1725 // updating the status emits the event, so the state should be
1726 // consistent at that point.
1727 event.error = err;
1728 _updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
1729 // also put the event object on the error: the caller will need this
1730 // to resend or cancel the event
1731 err.event = event;
1732
1733 if (callback) {
1734 callback(err);
1735 }
1736 } catch (err2) {
1737 console.error("Exception in error handler!", err2.stack || err);
1738 }
1739 throw err;
1740 });
1741}
1742
1743/**
1744 * Encrypt an event according to the configuration of the room, if necessary.
1745 *
1746 * @param {MatrixClient} client
1747 *
1748 * @param {module:models/event.MatrixEvent} event event to be sent
1749 *
1750 * @param {module:models/room?} room destination room. Null if the destination
1751 * is not a room we have seen over the sync pipe.
1752 *
1753 * @return {module:client.Promise?} Promise which resolves when the event has been
1754 * encrypted, or null if nothing was needed
1755 */
1756
1757function _encryptEventIfNeeded(client, event, room) {
1758 if (event.isEncrypted()) {
1759 // this event has already been encrypted; this happens if the
1760 // encryption step succeeded, but the send step failed on the first
1761 // attempt.
1762 return null;
1763 }
1764
1765 if (!client.isRoomEncrypted(event.getRoomId())) {
1766 // looks like this room isn't encrypted.
1767 return null;
1768 }
1769
1770 if (!client._crypto) {
1771 throw new Error(
1772 "This room is configured to use encryption, but your client does " +
1773 "not support encryption.",
1774 );
1775 }
1776
1777 return client._crypto.encryptEvent(event, room);
1778}
1779
1780function _updatePendingEventStatus(room, event, newStatus) {
1781 if (room) {
1782 room.updatePendingEvent(event, newStatus);
1783 } else {
1784 event.status = newStatus;
1785 }
1786}
1787
1788function _sendEventHttpRequest(client, event) {
1789 const txnId = event._txnId ? event._txnId : client.makeTxnId();
1790
1791 const pathParams = {
1792 $roomId: event.getRoomId(),
1793 $eventType: event.getWireType(),
1794 $stateKey: event.getStateKey(),
1795 $txnId: txnId,
1796 };
1797
1798 let path;
1799
1800 if (event.isState()) {
1801 let pathTemplate = "/rooms/$roomId/state/$eventType";
1802 if (event.getStateKey() && event.getStateKey().length > 0) {
1803 pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey";
1804 }
1805 path = utils.encodeUri(pathTemplate, pathParams);
1806 } else {
1807 path = utils.encodeUri(
1808 "/rooms/$roomId/send/$eventType/$txnId", pathParams,
1809 );
1810 }
1811
1812 return client._http.authedRequest(
1813 undefined, "PUT", path, undefined, event.getWireContent(),
1814 ).then((res) => {
1815 console.log(
1816 `Event sent to ${event.getRoomId()} with event id ${res.event_id}`,
1817 );
1818 return res;
1819 });
1820}
1821
1822/**
1823 * @param {string} roomId
1824 * @param {Object} content
1825 * @param {string} txnId Optional.
1826 * @param {module:client.callback} callback Optional.
1827 * @return {module:client.Promise} Resolves: TODO
1828 * @return {module:http-api.MatrixError} Rejects: with an error response.
1829 */
1830MatrixClient.prototype.sendMessage = function(roomId, content, txnId, callback) {
1831 if (utils.isFunction(txnId)) {
1832 callback = txnId; txnId = undefined;
1833 }
1834 return this.sendEvent(
1835 roomId, "m.room.message", content, txnId, callback,
1836 );
1837};
1838
1839/**
1840 * @param {string} roomId
1841 * @param {string} body
1842 * @param {string} txnId Optional.
1843 * @param {module:client.callback} callback Optional.
1844 * @return {module:client.Promise} Resolves: TODO
1845 * @return {module:http-api.MatrixError} Rejects: with an error response.
1846 */
1847MatrixClient.prototype.sendTextMessage = function(roomId, body, txnId, callback) {
1848 const content = ContentHelpers.makeTextMessage(body);
1849 return this.sendMessage(roomId, content, txnId, callback);
1850};
1851
1852/**
1853 * @param {string} roomId
1854 * @param {string} body
1855 * @param {string} txnId Optional.
1856 * @param {module:client.callback} callback Optional.
1857 * @return {module:client.Promise} Resolves: TODO
1858 * @return {module:http-api.MatrixError} Rejects: with an error response.
1859 */
1860MatrixClient.prototype.sendNotice = function(roomId, body, txnId, callback) {
1861 const content = ContentHelpers.makeNotice(body);
1862 return this.sendMessage(roomId, content, txnId, callback);
1863};
1864
1865/**
1866 * @param {string} roomId
1867 * @param {string} body
1868 * @param {string} txnId Optional.
1869 * @param {module:client.callback} callback Optional.
1870 * @return {module:client.Promise} Resolves: TODO
1871 * @return {module:http-api.MatrixError} Rejects: with an error response.
1872 */
1873MatrixClient.prototype.sendEmoteMessage = function(roomId, body, txnId, callback) {
1874 const content = ContentHelpers.makeEmoteMessage(body);
1875 return this.sendMessage(roomId, content, txnId, callback);
1876};
1877
1878/**
1879 * @param {string} roomId
1880 * @param {string} url
1881 * @param {Object} info
1882 * @param {string} text
1883 * @param {module:client.callback} callback Optional.
1884 * @return {module:client.Promise} Resolves: TODO
1885 * @return {module:http-api.MatrixError} Rejects: with an error response.
1886 */
1887MatrixClient.prototype.sendImageMessage = function(roomId, url, info, text, callback) {
1888 if (utils.isFunction(text)) {
1889 callback = text; text = undefined;
1890 }
1891 if (!text) {
1892 text = "Image";
1893 }
1894 const content = {
1895 msgtype: "m.image",
1896 url: url,
1897 info: info,
1898 body: text,
1899 };
1900 return this.sendMessage(roomId, content, callback);
1901};
1902
1903/**
1904 * @param {string} roomId
1905 * @param {string} url
1906 * @param {Object} info
1907 * @param {string} text
1908 * @param {module:client.callback} callback Optional.
1909 * @return {module:client.Promise} Resolves: TODO
1910 * @return {module:http-api.MatrixError} Rejects: with an error response.
1911 */
1912MatrixClient.prototype.sendStickerMessage = function(roomId, url, info, text, callback) {
1913 if (utils.isFunction(text)) {
1914 callback = text; text = undefined;
1915 }
1916 if (!text) {
1917 text = "Sticker";
1918 }
1919 const content = {
1920 url: url,
1921 info: info,
1922 body: text,
1923 };
1924 return this.sendEvent(
1925 roomId, "m.sticker", content, callback, undefined,
1926 );
1927};
1928
1929/**
1930 * @param {string} roomId
1931 * @param {string} body
1932 * @param {string} htmlBody
1933 * @param {module:client.callback} callback Optional.
1934 * @return {module:client.Promise} Resolves: TODO
1935 * @return {module:http-api.MatrixError} Rejects: with an error response.
1936 */
1937MatrixClient.prototype.sendHtmlMessage = function(roomId, body, htmlBody, callback) {
1938 const content = ContentHelpers.makeHtmlMessage(body, htmlBody);
1939 return this.sendMessage(roomId, content, callback);
1940};
1941
1942/**
1943 * @param {string} roomId
1944 * @param {string} body
1945 * @param {string} htmlBody
1946 * @param {module:client.callback} callback Optional.
1947 * @return {module:client.Promise} Resolves: TODO
1948 * @return {module:http-api.MatrixError} Rejects: with an error response.
1949 */
1950MatrixClient.prototype.sendHtmlNotice = function(roomId, body, htmlBody, callback) {
1951 const content = ContentHelpers.makeHtmlNotice(body, htmlBody);
1952 return this.sendMessage(roomId, content, callback);
1953};
1954
1955/**
1956 * @param {string} roomId
1957 * @param {string} body
1958 * @param {string} htmlBody
1959 * @param {module:client.callback} callback Optional.
1960 * @return {module:client.Promise} Resolves: TODO
1961 * @return {module:http-api.MatrixError} Rejects: with an error response.
1962 */
1963MatrixClient.prototype.sendHtmlEmote = function(roomId, body, htmlBody, callback) {
1964 const content = ContentHelpers.makeHtmlEmote(body, htmlBody);
1965 return this.sendMessage(roomId, content, callback);
1966};
1967
1968/**
1969 * Send a receipt.
1970 * @param {Event} event The event being acknowledged
1971 * @param {string} receiptType The kind of receipt e.g. "m.read"
1972 * @param {module:client.callback} callback Optional.
1973 * @return {module:client.Promise} Resolves: TODO
1974 * @return {module:http-api.MatrixError} Rejects: with an error response.
1975 */
1976MatrixClient.prototype.sendReceipt = function(event, receiptType, callback) {
1977 if (this.isGuest()) {
1978 return Promise.resolve({}); // guests cannot send receipts so don't bother.
1979 }
1980
1981 const path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
1982 $roomId: event.getRoomId(),
1983 $receiptType: receiptType,
1984 $eventId: event.getId(),
1985 });
1986 const promise = this._http.authedRequest(
1987 callback, "POST", path, undefined, {},
1988 );
1989
1990 const room = this.getRoom(event.getRoomId());
1991 if (room) {
1992 room._addLocalEchoReceipt(this.credentials.userId, event, receiptType);
1993 }
1994 return promise;
1995};
1996
1997/**
1998 * Send a read receipt.
1999 * @param {Event} event The event that has been read.
2000 * @param {module:client.callback} callback Optional.
2001 * @return {module:client.Promise} Resolves: TODO
2002 * @return {module:http-api.MatrixError} Rejects: with an error response.
2003 */
2004MatrixClient.prototype.sendReadReceipt = function(event, callback) {
2005 return this.sendReceipt(event, "m.read", callback);
2006};
2007
2008/**
2009 * Set a marker to indicate the point in a room before which the user has read every
2010 * event. This can be retrieved from room account data (the event type is `m.fully_read`)
2011 * and displayed as a horizontal line in the timeline that is visually distinct to the
2012 * position of the user's own read receipt.
2013 * @param {string} roomId ID of the room that has been read
2014 * @param {string} eventId ID of the event that has been read
2015 * @param {string} rrEvent the event tracked by the read receipt. This is here for
2016 * convenience because the RR and the RM are commonly updated at the same time as each
2017 * other. The local echo of this receipt will be done if set. Optional.
2018 * @return {module:client.Promise} Resolves: the empty object, {}.
2019 */
2020MatrixClient.prototype.setRoomReadMarkers = function(roomId, eventId, rrEvent) {
2021 const rmEventId = eventId;
2022 let rrEventId;
2023
2024 // Add the optional RR update, do local echo like `sendReceipt`
2025 if (rrEvent) {
2026 rrEventId = rrEvent.getId();
2027 const room = this.getRoom(roomId);
2028 if (room) {
2029 room._addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read");
2030 }
2031 }
2032
2033 return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId);
2034};
2035
2036/**
2037 * Get a preview of the given URL as of (roughly) the given point in time,
2038 * described as an object with OpenGraph keys and associated values.
2039 * Attributes may be synthesized where actual OG metadata is lacking.
2040 * Caches results to prevent hammering the server.
2041 * @param {string} url The URL to get preview data for
2042 * @param {Number} ts The preferred point in time that the preview should
2043 * describe (ms since epoch). The preview returned will either be the most
2044 * recent one preceding this timestamp if available, or failing that the next
2045 * most recent available preview.
2046 * @param {module:client.callback} callback Optional.
2047 * @return {module:client.Promise} Resolves: Object of OG metadata.
2048 * @return {module:http-api.MatrixError} Rejects: with an error response.
2049 * May return synthesized attributes if the URL lacked OG meta.
2050 */
2051MatrixClient.prototype.getUrlPreview = function(url, ts, callback) {
2052 const key = ts + "_" + url;
2053 const og = this.urlPreviewCache[key];
2054 if (og) {
2055 return Promise.resolve(og);
2056 }
2057
2058 const self = this;
2059 return this._http.authedRequestWithPrefix(
2060 callback, "GET", "/preview_url", {
2061 url: url,
2062 ts: ts,
2063 }, undefined, httpApi.PREFIX_MEDIA_R0,
2064 ).then(function(response) {
2065 // TODO: expire cache occasionally
2066 self.urlPreviewCache[key] = response;
2067 return response;
2068 });
2069};
2070
2071/**
2072 * @param {string} roomId
2073 * @param {boolean} isTyping
2074 * @param {Number} timeoutMs
2075 * @param {module:client.callback} callback Optional.
2076 * @return {module:client.Promise} Resolves: TODO
2077 * @return {module:http-api.MatrixError} Rejects: with an error response.
2078 */
2079MatrixClient.prototype.sendTyping = function(roomId, isTyping, timeoutMs, callback) {
2080 if (this.isGuest()) {
2081 return Promise.resolve({}); // guests cannot send typing notifications so don't bother.
2082 }
2083
2084 const path = utils.encodeUri("/rooms/$roomId/typing/$userId", {
2085 $roomId: roomId,
2086 $userId: this.credentials.userId,
2087 });
2088 const data = {
2089 typing: isTyping,
2090 };
2091 if (isTyping) {
2092 data.timeout = timeoutMs ? timeoutMs : 20000;
2093 }
2094 return this._http.authedRequest(
2095 callback, "PUT", path, undefined, data,
2096 );
2097};
2098
2099/**
2100 * @param {string} roomId
2101 * @param {string} userId
2102 * @param {module:client.callback} callback Optional.
2103 * @return {module:client.Promise} Resolves: TODO
2104 * @return {module:http-api.MatrixError} Rejects: with an error response.
2105 */
2106MatrixClient.prototype.invite = function(roomId, userId, callback) {
2107 return _membershipChange(this, roomId, userId, "invite", undefined,
2108 callback);
2109};
2110
2111/**
2112 * Invite a user to a room based on their email address.
2113 * @param {string} roomId The room to invite the user to.
2114 * @param {string} email The email address to invite.
2115 * @param {module:client.callback} callback Optional.
2116 * @return {module:client.Promise} Resolves: TODO
2117 * @return {module:http-api.MatrixError} Rejects: with an error response.
2118 */
2119MatrixClient.prototype.inviteByEmail = function(roomId, email, callback) {
2120 return this.inviteByThreePid(
2121 roomId, "email", email, callback,
2122 );
2123};
2124
2125/**
2126 * Invite a user to a room based on a third-party identifier.
2127 * @param {string} roomId The room to invite the user to.
2128 * @param {string} medium The medium to invite the user e.g. "email".
2129 * @param {string} address The address for the specified medium.
2130 * @param {module:client.callback} callback Optional.
2131 * @return {module:client.Promise} Resolves: TODO
2132 * @return {module:http-api.MatrixError} Rejects: with an error response.
2133 */
2134MatrixClient.prototype.inviteByThreePid = function(roomId, medium, address, callback) {
2135 const path = utils.encodeUri(
2136 "/rooms/$roomId/invite",
2137 { $roomId: roomId },
2138 );
2139
2140 const identityServerUrl = this.getIdentityServerUrl(true);
2141 if (!identityServerUrl) {
2142 return Promise.reject(new MatrixError({
2143 error: "No supplied identity server URL",
2144 errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM",
2145 }));
2146 }
2147
2148 return this._http.authedRequest(callback, "POST", path, undefined, {
2149 id_server: identityServerUrl,
2150 medium: medium,
2151 address: address,
2152 });
2153};
2154
2155/**
2156 * @param {string} roomId
2157 * @param {module:client.callback} callback Optional.
2158 * @return {module:client.Promise} Resolves: TODO
2159 * @return {module:http-api.MatrixError} Rejects: with an error response.
2160 */
2161MatrixClient.prototype.leave = function(roomId, callback) {
2162 return _membershipChange(this, roomId, undefined, "leave", undefined,
2163 callback);
2164};
2165
2166/**
2167 * @param {string} roomId
2168 * @param {string} userId
2169 * @param {string} reason Optional.
2170 * @param {module:client.callback} callback Optional.
2171 * @return {module:client.Promise} Resolves: TODO
2172 * @return {module:http-api.MatrixError} Rejects: with an error response.
2173 */
2174MatrixClient.prototype.ban = function(roomId, userId, reason, callback) {
2175 return _membershipChange(this, roomId, userId, "ban", reason,
2176 callback);
2177};
2178
2179/**
2180 * @param {string} roomId
2181 * @param {boolean} deleteRoom True to delete the room from the store on success.
2182 * Default: true.
2183 * @param {module:client.callback} callback Optional.
2184 * @return {module:client.Promise} Resolves: TODO
2185 * @return {module:http-api.MatrixError} Rejects: with an error response.
2186 */
2187MatrixClient.prototype.forget = function(roomId, deleteRoom, callback) {
2188 if (deleteRoom === undefined) {
2189 deleteRoom = true;
2190 }
2191 const promise = _membershipChange(this, roomId, undefined, "forget", undefined,
2192 callback);
2193 if (!deleteRoom) {
2194 return promise;
2195 }
2196 const self = this;
2197 return promise.then(function(response) {
2198 self.store.removeRoom(roomId);
2199 self.emit("deleteRoom", roomId);
2200 return response;
2201 });
2202};
2203
2204/**
2205 * @param {string} roomId
2206 * @param {string} userId
2207 * @param {module:client.callback} callback Optional.
2208 * @return {module:client.Promise} Resolves: Object (currently empty)
2209 * @return {module:http-api.MatrixError} Rejects: with an error response.
2210 */
2211MatrixClient.prototype.unban = function(roomId, userId, callback) {
2212 // unbanning != set their state to leave: this used to be
2213 // the case, but was then changed so that leaving was always
2214 // a revoking of priviledge, otherwise two people racing to
2215 // kick / ban someone could end up banning and then un-banning
2216 // them.
2217 const path = utils.encodeUri("/rooms/$roomId/unban", {
2218 $roomId: roomId,
2219 });
2220 const data = {
2221 user_id: userId,
2222 };
2223 return this._http.authedRequest(
2224 callback, "POST", path, undefined, data,
2225 );
2226};
2227
2228/**
2229 * @param {string} roomId
2230 * @param {string} userId
2231 * @param {string} reason Optional.
2232 * @param {module:client.callback} callback Optional.
2233 * @return {module:client.Promise} Resolves: TODO
2234 * @return {module:http-api.MatrixError} Rejects: with an error response.
2235 */
2236MatrixClient.prototype.kick = function(roomId, userId, reason, callback) {
2237 return _setMembershipState(
2238 this, roomId, userId, "leave", reason, callback,
2239 );
2240};
2241
2242/**
2243 * This is an internal method.
2244 * @param {MatrixClient} client
2245 * @param {string} roomId
2246 * @param {string} userId
2247 * @param {string} membershipValue
2248 * @param {string} reason
2249 * @param {module:client.callback} callback Optional.
2250 * @return {module:client.Promise} Resolves: TODO
2251 * @return {module:http-api.MatrixError} Rejects: with an error response.
2252 */
2253function _setMembershipState(client, roomId, userId, membershipValue, reason,
2254 callback) {
2255 if (utils.isFunction(reason)) {
2256 callback = reason; reason = undefined;
2257 }
2258
2259 const path = utils.encodeUri(
2260 "/rooms/$roomId/state/m.room.member/$userId",
2261 { $roomId: roomId, $userId: userId},
2262 );
2263
2264 return client._http.authedRequest(callback, "PUT", path, undefined, {
2265 membership: membershipValue,
2266 reason: reason,
2267 });
2268}
2269
2270/**
2271 * This is an internal method.
2272 * @param {MatrixClient} client
2273 * @param {string} roomId
2274 * @param {string} userId
2275 * @param {string} membership
2276 * @param {string} reason
2277 * @param {module:client.callback} callback Optional.
2278 * @return {module:client.Promise} Resolves: TODO
2279 * @return {module:http-api.MatrixError} Rejects: with an error response.
2280 */
2281function _membershipChange(client, roomId, userId, membership, reason, callback) {
2282 if (utils.isFunction(reason)) {
2283 callback = reason; reason = undefined;
2284 }
2285
2286 const path = utils.encodeUri("/rooms/$room_id/$membership", {
2287 $room_id: roomId,
2288 $membership: membership,
2289 });
2290 return client._http.authedRequest(
2291 callback, "POST", path, undefined, {
2292 user_id: userId, // may be undefined e.g. on leave
2293 reason: reason,
2294 },
2295 );
2296}
2297
2298/**
2299 * Obtain a dict of actions which should be performed for this event according
2300 * to the push rules for this user. Caches the dict on the event.
2301 * @param {MatrixEvent} event The event to get push actions for.
2302 * @return {module:pushprocessor~PushAction} A dict of actions to perform.
2303 */
2304MatrixClient.prototype.getPushActionsForEvent = function(event) {
2305 if (!event.getPushActions()) {
2306 event.setPushActions(this._pushProcessor.actionsForEvent(event));
2307 }
2308 return event.getPushActions();
2309};
2310
2311// Profile operations
2312// ==================
2313
2314/**
2315 * @param {string} info The kind of info to set (e.g. 'avatar_url')
2316 * @param {Object} data The JSON object to set.
2317 * @param {module:client.callback} callback Optional.
2318 * @return {module:client.Promise} Resolves: TODO
2319 * @return {module:http-api.MatrixError} Rejects: with an error response.
2320 */
2321MatrixClient.prototype.setProfileInfo = function(info, data, callback) {
2322 const path = utils.encodeUri("/profile/$userId/$info", {
2323 $userId: this.credentials.userId,
2324 $info: info,
2325 });
2326 return this._http.authedRequest(
2327 callback, "PUT", path, undefined, data,
2328 );
2329};
2330
2331/**
2332 * @param {string} name
2333 * @param {module:client.callback} callback Optional.
2334 * @return {module:client.Promise} Resolves: TODO
2335 * @return {module:http-api.MatrixError} Rejects: with an error response.
2336 */
2337MatrixClient.prototype.setDisplayName = function(name, callback) {
2338 return this.setProfileInfo(
2339 "displayname", { displayname: name }, callback,
2340 );
2341};
2342
2343/**
2344 * @param {string} url
2345 * @param {module:client.callback} callback Optional.
2346 * @return {module:client.Promise} Resolves: TODO
2347 * @return {module:http-api.MatrixError} Rejects: with an error response.
2348 */
2349MatrixClient.prototype.setAvatarUrl = function(url, callback) {
2350 return this.setProfileInfo(
2351 "avatar_url", { avatar_url: url }, callback,
2352 );
2353};
2354
2355/**
2356 * Turn an MXC URL into an HTTP one. <strong>This method is experimental and
2357 * may change.</strong>
2358 * @param {string} mxcUrl The MXC URL
2359 * @param {Number} width The desired width of the thumbnail.
2360 * @param {Number} height The desired height of the thumbnail.
2361 * @param {string} resizeMethod The thumbnail resize method to use, either
2362 * "crop" or "scale".
2363 * @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
2364 * directly. Fetching such URLs will leak information about the user to
2365 * anyone they share a room with. If false, will return null for such URLs.
2366 * @return {?string} the avatar URL or null.
2367 */
2368MatrixClient.prototype.mxcUrlToHttp =
2369 function(mxcUrl, width, height, resizeMethod, allowDirectLinks) {
2370 return contentRepo.getHttpUriForMxc(
2371 this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks,
2372 );
2373};
2374
2375/**
2376 * Sets a new status message for the user. The message may be null/falsey
2377 * to clear the message.
2378 * @param {string} newMessage The new message to set.
2379 * @return {module:client.Promise} Resolves: to nothing
2380 * @return {module:http-api.MatrixError} Rejects: with an error response.
2381 */
2382MatrixClient.prototype._unstable_setStatusMessage = function(newMessage) {
2383 const type = "im.vector.user_status";
2384 return Promise.all(this.getRooms().map((room) => {
2385 const isJoined = room.getMyMembership() === "join";
2386 const looksLikeDm = room.getInvitedAndJoinedMemberCount() === 2;
2387 if (!isJoined || !looksLikeDm) {
2388 return Promise.resolve();
2389 }
2390 // Check power level separately as it's a bit more expensive.
2391 const maySend = room.currentState.mayClientSendStateEvent(type, this);
2392 if (!maySend) {
2393 return Promise.resolve();
2394 }
2395 return this.sendStateEvent(room.roomId, type, {
2396 status: newMessage,
2397 }, this.getUserId());
2398 }));
2399};
2400
2401/**
2402 * @param {Object} opts Options to apply
2403 * @param {string} opts.presence One of "online", "offline" or "unavailable"
2404 * @param {string} opts.status_msg The status message to attach.
2405 * @param {module:client.callback} callback Optional.
2406 * @return {module:client.Promise} Resolves: TODO
2407 * @return {module:http-api.MatrixError} Rejects: with an error response.
2408 * @throws If 'presence' isn't a valid presence enum value.
2409 */
2410MatrixClient.prototype.setPresence = function(opts, callback) {
2411 const path = utils.encodeUri("/presence/$userId/status", {
2412 $userId: this.credentials.userId,
2413 });
2414
2415 if (typeof opts === "string") {
2416 opts = { presence: opts };
2417 }
2418
2419 const validStates = ["offline", "online", "unavailable"];
2420 if (validStates.indexOf(opts.presence) == -1) {
2421 throw new Error("Bad presence value: " + opts.presence);
2422 }
2423 return this._http.authedRequest(
2424 callback, "PUT", path, undefined, opts,
2425 );
2426};
2427
2428function _presenceList(callback, client, opts, method) {
2429 const path = utils.encodeUri("/presence/list/$userId", {
2430 $userId: client.credentials.userId,
2431 });
2432 return client._http.authedRequest(callback, method, path, undefined, opts);
2433}
2434
2435/**
2436* Retrieve current user presence list.
2437* @param {module:client.callback} callback Optional.
2438* @return {module:client.Promise} Resolves: TODO
2439* @return {module:http-api.MatrixError} Rejects: with an error response.
2440*/
2441MatrixClient.prototype.getPresenceList = function(callback) {
2442 return _presenceList(callback, this, undefined, "GET");
2443};
2444
2445/**
2446* Add users to the current user presence list.
2447* @param {module:client.callback} callback Optional.
2448* @param {string[]} userIds
2449* @return {module:client.Promise} Resolves: TODO
2450* @return {module:http-api.MatrixError} Rejects: with an error response.
2451*/
2452MatrixClient.prototype.inviteToPresenceList = function(callback, userIds) {
2453 const opts = {"invite": userIds};
2454 return _presenceList(callback, this, opts, "POST");
2455};
2456
2457/**
2458* Drop users from the current user presence list.
2459* @param {module:client.callback} callback Optional.
2460* @param {string[]} userIds
2461* @return {module:client.Promise} Resolves: TODO
2462* @return {module:http-api.MatrixError} Rejects: with an error response.
2463**/
2464MatrixClient.prototype.dropFromPresenceList = function(callback, userIds) {
2465 const opts = {"drop": userIds};
2466 return _presenceList(callback, this, opts, "POST");
2467};
2468
2469/**
2470 * Retrieve older messages from the given room and put them in the timeline.
2471 *
2472 * If this is called multiple times whilst a request is ongoing, the <i>same</i>
2473 * Promise will be returned. If there was a problem requesting scrollback, there
2474 * will be a small delay before another request can be made (to prevent tight-looping
2475 * when there is no connection).
2476 *
2477 * @param {Room} room The room to get older messages in.
2478 * @param {Integer} limit Optional. The maximum number of previous events to
2479 * pull in. Default: 30.
2480 * @param {module:client.callback} callback Optional.
2481 * @return {module:client.Promise} Resolves: Room. If you are at the beginning
2482 * of the timeline, <code>Room.oldState.paginationToken</code> will be
2483 * <code>null</code>.
2484 * @return {module:http-api.MatrixError} Rejects: with an error response.
2485 */
2486MatrixClient.prototype.scrollback = function(room, limit, callback) {
2487 if (utils.isFunction(limit)) {
2488 callback = limit; limit = undefined;
2489 }
2490 limit = limit || 30;
2491 let timeToWaitMs = 0;
2492
2493 let info = this._ongoingScrollbacks[room.roomId] || {};
2494 if (info.promise) {
2495 return info.promise;
2496 } else if (info.errorTs) {
2497 const timeWaitedMs = Date.now() - info.errorTs;
2498 timeToWaitMs = Math.max(SCROLLBACK_DELAY_MS - timeWaitedMs, 0);
2499 }
2500
2501 if (room.oldState.paginationToken === null) {
2502 return Promise.resolve(room); // already at the start.
2503 }
2504 // attempt to grab more events from the store first
2505 const numAdded = this.store.scrollback(room, limit).length;
2506 if (numAdded === limit) {
2507 // store contained everything we needed.
2508 return Promise.resolve(room);
2509 }
2510 // reduce the required number of events appropriately
2511 limit = limit - numAdded;
2512
2513 const defer = Promise.defer();
2514 info = {
2515 promise: defer.promise,
2516 errorTs: null,
2517 };
2518 const self = this;
2519 // wait for a time before doing this request
2520 // (which may be 0 in order not to special case the code paths)
2521 Promise.delay(timeToWaitMs).then(function() {
2522 return self._createMessagesRequest(
2523 room.roomId,
2524 room.oldState.paginationToken,
2525 limit,
2526 'b');
2527 }).done(function(res) {
2528 const matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self));
2529 if (res.state) {
2530 const stateEvents = utils.map(res.state, _PojoToMatrixEventMapper(self));
2531 room.currentState.setUnknownStateEvents(stateEvents);
2532 }
2533 room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline());
2534 room.oldState.paginationToken = res.end;
2535 if (res.chunk.length === 0) {
2536 room.oldState.paginationToken = null;
2537 }
2538 self.store.storeEvents(room, matrixEvents, res.end, true);
2539 self._ongoingScrollbacks[room.roomId] = null;
2540 _resolve(callback, defer, room);
2541 }, function(err) {
2542 self._ongoingScrollbacks[room.roomId] = {
2543 errorTs: Date.now(),
2544 };
2545 _reject(callback, defer, err);
2546 });
2547 this._ongoingScrollbacks[room.roomId] = info;
2548 return defer.promise;
2549};
2550
2551/**
2552 * Get an EventTimeline for the given event
2553 *
2554 * <p>If the EventTimelineSet object already has the given event in its store, the
2555 * corresponding timeline will be returned. Otherwise, a /context request is
2556 * made, and used to construct an EventTimeline.
2557 *
2558 * @param {EventTimelineSet} timelineSet The timelineSet to look for the event in
2559 * @param {string} eventId The ID of the event to look for
2560 *
2561 * @return {module:client.Promise} Resolves:
2562 * {@link module:models/event-timeline~EventTimeline} including the given
2563 * event
2564 */
2565MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) {
2566 // don't allow any timeline support unless it's been enabled.
2567 if (!this.timelineSupport) {
2568 throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
2569 " parameter to true when creating MatrixClient to enable" +
2570 " it.");
2571 }
2572
2573 if (timelineSet.getTimelineForEvent(eventId)) {
2574 return Promise.resolve(timelineSet.getTimelineForEvent(eventId));
2575 }
2576
2577 const path = utils.encodeUri(
2578 "/rooms/$roomId/context/$eventId", {
2579 $roomId: timelineSet.room.roomId,
2580 $eventId: eventId,
2581 },
2582 );
2583
2584 let params = undefined;
2585 if (this._clientOpts.lazyLoadMembers) {
2586 params = {filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER)};
2587 }
2588
2589 // TODO: we should implement a backoff (as per scrollback()) to deal more
2590 // nicely with HTTP errors.
2591 const self = this;
2592 const promise =
2593 self._http.authedRequest(undefined, "GET", path, params,
2594 ).then(function(res) {
2595 if (!res.event) {
2596 throw new Error("'event' not in '/context' result - homeserver too old?");
2597 }
2598
2599 // by the time the request completes, the event might have ended up in
2600 // the timeline.
2601 if (timelineSet.getTimelineForEvent(eventId)) {
2602 return timelineSet.getTimelineForEvent(eventId);
2603 }
2604
2605 // we start with the last event, since that's the point at which we
2606 // have known state.
2607 // events_after is already backwards; events_before is forwards.
2608 res.events_after.reverse();
2609 const events = res.events_after
2610 .concat([res.event])
2611 .concat(res.events_before);
2612 const matrixEvents = utils.map(events, self.getEventMapper());
2613
2614 let timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId());
2615 if (!timeline) {
2616 timeline = timelineSet.addTimeline();
2617 timeline.initialiseState(utils.map(res.state,
2618 self.getEventMapper()));
2619 timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
2620 } else {
2621 const stateEvents = utils.map(res.state, self.getEventMapper());
2622 timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(stateEvents);
2623 }
2624 timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start);
2625
2626 // there is no guarantee that the event ended up in "timeline" (we
2627 // might have switched to a neighbouring timeline) - so check the
2628 // room's index again. On the other hand, there's no guarantee the
2629 // event ended up anywhere, if it was later redacted, so we just
2630 // return the timeline we first thought of.
2631 const tl = timelineSet.getTimelineForEvent(eventId) || timeline;
2632 return tl;
2633 });
2634 return promise;
2635};
2636
2637/**
2638 * Makes a request to /messages with the appropriate lazy loading filter set.
2639 * XXX: if we do get rid of scrollback (as it's not used at the moment),
2640 * we could inline this method again in paginateEventTimeline as that would
2641 * then be the only call-site
2642 * @param {string} roomId
2643 * @param {string} fromToken
2644 * @param {number} limit the maximum amount of events the retrieve
2645 * @param {string} dir 'f' or 'b'
2646 * @param {Filter} timelineFilter the timeline filter to pass
2647 * @return {Promise}
2648 */
2649MatrixClient.prototype._createMessagesRequest =
2650function(roomId, fromToken, limit, dir, timelineFilter = undefined) {
2651 const path = utils.encodeUri(
2652 "/rooms/$roomId/messages", {$roomId: roomId},
2653 );
2654 if (limit === undefined) {
2655 limit = 30;
2656 }
2657 const params = {
2658 from: fromToken,
2659 limit: limit,
2660 dir: dir,
2661 };
2662
2663 let filter = null;
2664 if (this._clientOpts.lazyLoadMembers) {
2665 // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
2666 // so the timelineFilter doesn't get written into it below
2667 filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER);
2668 }
2669 if (timelineFilter) {
2670 // XXX: it's horrific that /messages' filter parameter doesn't match
2671 // /sync's one - see https://matrix.org/jira/browse/SPEC-451
2672 filter = filter || {};
2673 Object.assign(filter, timelineFilter.getRoomTimelineFilterComponent());
2674 }
2675 if (filter) {
2676 params.filter = JSON.stringify(filter);
2677 }
2678 return this._http.authedRequest(undefined, "GET", path, params);
2679};
2680
2681/**
2682 * Take an EventTimeline, and back/forward-fill results.
2683 *
2684 * @param {module:models/event-timeline~EventTimeline} eventTimeline timeline
2685 * object to be updated
2686 * @param {Object} [opts]
2687 * @param {bool} [opts.backwards = false] true to fill backwards,
2688 * false to go forwards
2689 * @param {number} [opts.limit = 30] number of events to request
2690 *
2691 * @return {module:client.Promise} Resolves to a boolean: false if there are no
2692 * events and we reached either end of the timeline; else true.
2693 */
2694MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
2695 const isNotifTimeline = (eventTimeline.getTimelineSet() === this._notifTimelineSet);
2696
2697 // TODO: we should implement a backoff (as per scrollback()) to deal more
2698 // nicely with HTTP errors.
2699 opts = opts || {};
2700 const backwards = opts.backwards || false;
2701
2702 if (isNotifTimeline) {
2703 if (!backwards) {
2704 throw new Error("paginateNotifTimeline can only paginate backwards");
2705 }
2706 }
2707
2708 const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
2709
2710 const token = eventTimeline.getPaginationToken(dir);
2711 if (!token) {
2712 // no token - no results.
2713 return Promise.resolve(false);
2714 }
2715
2716 const pendingRequest = eventTimeline._paginationRequests[dir];
2717
2718 if (pendingRequest) {
2719 // already a request in progress - return the existing promise
2720 return pendingRequest;
2721 }
2722
2723 let path, params, promise;
2724 const self = this;
2725
2726 if (isNotifTimeline) {
2727 path = "/notifications";
2728 params = {
2729 limit: ('limit' in opts) ? opts.limit : 30,
2730 only: 'highlight',
2731 };
2732
2733 if (token && token !== "end") {
2734 params.from = token;
2735 }
2736
2737 promise =
2738 this._http.authedRequestWithPrefix(undefined, "GET", path, params,
2739 undefined, httpApi.PREFIX_UNSTABLE,
2740 ).then(function(res) {
2741 const token = res.next_token;
2742 const matrixEvents = [];
2743
2744 for (let i = 0; i < res.notifications.length; i++) {
2745 const notification = res.notifications[i];
2746 const event = self.getEventMapper()(notification.event);
2747 event.setPushActions(
2748 PushProcessor.actionListToActionsObject(notification.actions),
2749 );
2750 event.event.room_id = notification.room_id; // XXX: gutwrenching
2751 matrixEvents[i] = event;
2752 }
2753
2754 eventTimeline.getTimelineSet()
2755 .addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
2756
2757 // if we've hit the end of the timeline, we need to stop trying to
2758 // paginate. We need to keep the 'forwards' token though, to make sure
2759 // we can recover from gappy syncs.
2760 if (backwards && !res.next_token) {
2761 eventTimeline.setPaginationToken(null, dir);
2762 }
2763 return res.next_token ? true : false;
2764 }).finally(function() {
2765 eventTimeline._paginationRequests[dir] = null;
2766 });
2767 eventTimeline._paginationRequests[dir] = promise;
2768 } else {
2769 const room = this.getRoom(eventTimeline.getRoomId());
2770 if (!room) {
2771 throw new Error("Unknown room " + eventTimeline.getRoomId());
2772 }
2773
2774 promise = this._createMessagesRequest(
2775 eventTimeline.getRoomId(),
2776 token,
2777 opts.limit,
2778 dir,
2779 eventTimeline.getFilter());
2780 promise.then(function(res) {
2781 if (res.state) {
2782 const roomState = eventTimeline.getState(dir);
2783 const stateEvents = utils.map(res.state, self.getEventMapper());
2784 roomState.setUnknownStateEvents(stateEvents);
2785 }
2786 const token = res.end;
2787 const matrixEvents = utils.map(res.chunk, self.getEventMapper());
2788 eventTimeline.getTimelineSet()
2789 .addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
2790
2791 // if we've hit the end of the timeline, we need to stop trying to
2792 // paginate. We need to keep the 'forwards' token though, to make sure
2793 // we can recover from gappy syncs.
2794 if (backwards && res.end == res.start) {
2795 eventTimeline.setPaginationToken(null, dir);
2796 }
2797 return res.end != res.start;
2798 }).finally(function() {
2799 eventTimeline._paginationRequests[dir] = null;
2800 });
2801 eventTimeline._paginationRequests[dir] = promise;
2802 }
2803
2804 return promise;
2805};
2806
2807/**
2808 * Reset the notifTimelineSet entirely, paginating in some historical notifs as
2809 * a starting point for subsequent pagination.
2810 */
2811MatrixClient.prototype.resetNotifTimelineSet = function() {
2812 if (!this._notifTimelineSet) {
2813 return;
2814 }
2815
2816 // FIXME: This thing is a total hack, and results in duplicate events being
2817 // added to the timeline both from /sync and /notifications, and lots of
2818 // slow and wasteful processing and pagination. The correct solution is to
2819 // extend /messages or /search or something to filter on notifications.
2820
2821 // use the fictitious token 'end'. in practice we would ideally give it
2822 // the oldest backwards pagination token from /sync, but /sync doesn't
2823 // know about /notifications, so we have no choice but to start paginating
2824 // from the current point in time. This may well overlap with historical
2825 // notifs which are then inserted into the timeline by /sync responses.
2826 this._notifTimelineSet.resetLiveTimeline('end', null);
2827
2828 // we could try to paginate a single event at this point in order to get
2829 // a more valid pagination token, but it just ends up with an out of order
2830 // timeline. given what a mess this is and given we're going to have duplicate
2831 // events anyway, just leave it with the dummy token for now.
2832 /*
2833 this.paginateNotifTimeline(this._notifTimelineSet.getLiveTimeline(), {
2834 backwards: true,
2835 limit: 1
2836 });
2837 */
2838};
2839
2840/**
2841 * Peek into a room and receive updates about the room. This only works if the
2842 * history visibility for the room is world_readable.
2843 * @param {String} roomId The room to attempt to peek into.
2844 * @return {module:client.Promise} Resolves: Room object
2845 * @return {module:http-api.MatrixError} Rejects: with an error response.
2846 */
2847MatrixClient.prototype.peekInRoom = function(roomId) {
2848 if (this._peekSync) {
2849 this._peekSync.stopPeeking();
2850 }
2851 this._peekSync = new SyncApi(this, this._clientOpts);
2852 return this._peekSync.peek(roomId);
2853};
2854
2855/**
2856 * Stop any ongoing room peeking.
2857 */
2858MatrixClient.prototype.stopPeeking = function() {
2859 if (this._peekSync) {
2860 this._peekSync.stopPeeking();
2861 this._peekSync = null;
2862 }
2863};
2864
2865/**
2866 * Set r/w flags for guest access in a room.
2867 * @param {string} roomId The room to configure guest access in.
2868 * @param {Object} opts Options
2869 * @param {boolean} opts.allowJoin True to allow guests to join this room. This
2870 * implicitly gives guests write access. If false or not given, guests are
2871 * explicitly forbidden from joining the room.
2872 * @param {boolean} opts.allowRead True to set history visibility to
2873 * be world_readable. This gives guests read access *from this point forward*.
2874 * If false or not given, history visibility is not modified.
2875 * @return {module:client.Promise} Resolves: TODO
2876 * @return {module:http-api.MatrixError} Rejects: with an error response.
2877 */
2878MatrixClient.prototype.setGuestAccess = function(roomId, opts) {
2879 const writePromise = this.sendStateEvent(roomId, "m.room.guest_access", {
2880 guest_access: opts.allowJoin ? "can_join" : "forbidden",
2881 });
2882
2883 let readPromise = Promise.resolve();
2884 if (opts.allowRead) {
2885 readPromise = this.sendStateEvent(roomId, "m.room.history_visibility", {
2886 history_visibility: "world_readable",
2887 });
2888 }
2889
2890 return Promise.all([readPromise, writePromise]);
2891};
2892
2893// Registration/Login operations
2894// =============================
2895
2896/**
2897 * Requests an email verification token for the purposes of registration.
2898 * This API proxies the Identity Server /validate/email/requestToken API,
2899 * adding registration-specific behaviour. Specifically, if an account with
2900 * the given email address already exists, it will either send an email
2901 * to the address informing them of this or return M_THREEPID_IN_USE
2902 * (which one is up to the Home Server).
2903 *
2904 * requestEmailToken calls the equivalent API directly on the ID server,
2905 * therefore bypassing the registration-specific logic.
2906 *
2907 * Parameters and return value are as for requestEmailToken
2908
2909 * @param {string} email As requestEmailToken
2910 * @param {string} clientSecret As requestEmailToken
2911 * @param {number} sendAttempt As requestEmailToken
2912 * @param {string} nextLink As requestEmailToken
2913 * @return {module:client.Promise} Resolves: As requestEmailToken
2914 */
2915MatrixClient.prototype.requestRegisterEmailToken = function(email, clientSecret,
2916 sendAttempt, nextLink) {
2917 return this._requestTokenFromEndpoint(
2918 "/register/email/requestToken",
2919 {
2920 email: email,
2921 client_secret: clientSecret,
2922 send_attempt: sendAttempt,
2923 next_link: nextLink,
2924 },
2925 );
2926};
2927
2928/**
2929 * Requests a text message verification token for the purposes of registration.
2930 * This API proxies the Identity Server /validate/msisdn/requestToken API,
2931 * adding registration-specific behaviour, as with requestRegisterEmailToken.
2932 *
2933 * @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in which
2934 * phoneNumber should be parsed relative to.
2935 * @param {string} phoneNumber The phone number, in national or international format
2936 * @param {string} clientSecret As requestEmailToken
2937 * @param {number} sendAttempt As requestEmailToken
2938 * @param {string} nextLink As requestEmailToken
2939 * @return {module:client.Promise} Resolves: As requestEmailToken
2940 */
2941MatrixClient.prototype.requestRegisterMsisdnToken = function(phoneCountry, phoneNumber,
2942 clientSecret, sendAttempt, nextLink) {
2943 return this._requestTokenFromEndpoint(
2944 "/register/msisdn/requestToken",
2945 {
2946 country: phoneCountry,
2947 phone_number: phoneNumber,
2948 client_secret: clientSecret,
2949 send_attempt: sendAttempt,
2950 next_link: nextLink,
2951 },
2952 );
2953};
2954
2955/**
2956 * Requests an email verification token for the purposes of adding a
2957 * third party identifier to an account.
2958 * This API proxies the Identity Server /validate/email/requestToken API,
2959 * adding specific behaviour for the addition of email addresses to an
2960 * account. Specifically, if an account with
2961 * the given email address already exists, it will either send an email
2962 * to the address informing them of this or return M_THREEPID_IN_USE
2963 * (which one is up to the Home Server).
2964 *
2965 * requestEmailToken calls the equivalent API directly on the ID server,
2966 * therefore bypassing the email addition specific logic.
2967 *
2968 * @param {string} email As requestEmailToken
2969 * @param {string} clientSecret As requestEmailToken
2970 * @param {number} sendAttempt As requestEmailToken
2971 * @param {string} nextLink As requestEmailToken
2972 * @return {module:client.Promise} Resolves: As requestEmailToken
2973 */
2974MatrixClient.prototype.requestAdd3pidEmailToken = function(email, clientSecret,
2975 sendAttempt, nextLink) {
2976 return this._requestTokenFromEndpoint(
2977 "/account/3pid/email/requestToken",
2978 {
2979 email: email,
2980 client_secret: clientSecret,
2981 send_attempt: sendAttempt,
2982 next_link: nextLink,
2983 },
2984 );
2985};
2986
2987/**
2988 * Requests a text message verification token for the purposes of adding a
2989 * third party identifier to an account.
2990 * This API proxies the Identity Server /validate/email/requestToken API,
2991 * adding specific behaviour for the addition of phone numbers to an
2992 * account, as requestAdd3pidEmailToken.
2993 *
2994 * @param {string} phoneCountry As requestRegisterMsisdnToken
2995 * @param {string} phoneNumber As requestRegisterMsisdnToken
2996 * @param {string} clientSecret As requestEmailToken
2997 * @param {number} sendAttempt As requestEmailToken
2998 * @param {string} nextLink As requestEmailToken
2999 * @return {module:client.Promise} Resolves: As requestEmailToken
3000 */
3001MatrixClient.prototype.requestAdd3pidMsisdnToken = function(phoneCountry, phoneNumber,
3002 clientSecret, sendAttempt, nextLink) {
3003 return this._requestTokenFromEndpoint(
3004 "/account/3pid/msisdn/requestToken",
3005 {
3006 country: phoneCountry,
3007 phone_number: phoneNumber,
3008 client_secret: clientSecret,
3009 send_attempt: sendAttempt,
3010 next_link: nextLink,
3011 },
3012 );
3013};
3014
3015/**
3016 * Requests an email verification token for the purposes of resetting
3017 * the password on an account.
3018 * This API proxies the Identity Server /validate/email/requestToken API,
3019 * adding specific behaviour for the password resetting. Specifically,
3020 * if no account with the given email address exists, it may either
3021 * return M_THREEPID_NOT_FOUND or send an email
3022 * to the address informing them of this (which one is up to the Home Server).
3023 *
3024 * requestEmailToken calls the equivalent API directly on the ID server,
3025 * therefore bypassing the password reset specific logic.
3026 *
3027 * @param {string} email As requestEmailToken
3028 * @param {string} clientSecret As requestEmailToken
3029 * @param {number} sendAttempt As requestEmailToken
3030 * @param {string} nextLink As requestEmailToken
3031 * @param {module:client.callback} callback Optional. As requestEmailToken
3032 * @return {module:client.Promise} Resolves: As requestEmailToken
3033 */
3034MatrixClient.prototype.requestPasswordEmailToken = function(email, clientSecret,
3035 sendAttempt, nextLink) {
3036 return this._requestTokenFromEndpoint(
3037 "/account/password/email/requestToken",
3038 {
3039 email: email,
3040 client_secret: clientSecret,
3041 send_attempt: sendAttempt,
3042 next_link: nextLink,
3043 },
3044 );
3045};
3046
3047/**
3048 * Requests a text message verification token for the purposes of resetting
3049 * the password on an account.
3050 * This API proxies the Identity Server /validate/email/requestToken API,
3051 * adding specific behaviour for the password resetting, as requestPasswordEmailToken.
3052 *
3053 * @param {string} phoneCountry As requestRegisterMsisdnToken
3054 * @param {string} phoneNumber As requestRegisterMsisdnToken
3055 * @param {string} clientSecret As requestEmailToken
3056 * @param {number} sendAttempt As requestEmailToken
3057 * @param {string} nextLink As requestEmailToken
3058 * @return {module:client.Promise} Resolves: As requestEmailToken
3059 */
3060MatrixClient.prototype.requestPasswordMsisdnToken = function(phoneCountry, phoneNumber,
3061 clientSecret, sendAttempt, nextLink) {
3062 return this._requestTokenFromEndpoint(
3063 "/account/password/msisdn/requestToken",
3064 {
3065 country: phoneCountry,
3066 phone_number: phoneNumber,
3067 client_secret: clientSecret,
3068 send_attempt: sendAttempt,
3069 next_link: nextLink,
3070 },
3071 );
3072};
3073
3074/**
3075 * Internal utility function for requesting validation tokens from usage-specific
3076 * requestToken endpoints.
3077 *
3078 * @param {string} endpoint The endpoint to send the request to
3079 * @param {object} params Parameters for the POST request
3080 * @return {module:client.Promise} Resolves: As requestEmailToken
3081 */
3082MatrixClient.prototype._requestTokenFromEndpoint = function(endpoint, params) {
3083 const id_server_url = url.parse(this.idBaseUrl);
3084 if (id_server_url.host === null) {
3085 throw new Error("Invalid ID server URL: " + this.idBaseUrl);
3086 }
3087
3088 const postParams = Object.assign({}, params, {
3089 id_server: id_server_url.host,
3090 });
3091 return this._http.request(
3092 undefined, "POST", endpoint, undefined,
3093 postParams,
3094 );
3095};
3096
3097
3098// Push operations
3099// ===============
3100
3101/**
3102 * Get the room-kind push rule associated with a room.
3103 * @param {string} scope "global" or device-specific.
3104 * @param {string} roomId the id of the room.
3105 * @return {object} the rule or undefined.
3106 */
3107MatrixClient.prototype.getRoomPushRule = function(scope, roomId) {
3108 // There can be only room-kind push rule per room
3109 // and its id is the room id.
3110 if (this.pushRules) {
3111 for (let i = 0; i < this.pushRules[scope].room.length; i++) {
3112 const rule = this.pushRules[scope].room[i];
3113 if (rule.rule_id === roomId) {
3114 return rule;
3115 }
3116 }
3117 } else {
3118 throw new Error(
3119 "SyncApi.sync() must be done before accessing to push rules.",
3120 );
3121 }
3122};
3123
3124/**
3125 * Set a room-kind muting push rule in a room.
3126 * The operation also updates MatrixClient.pushRules at the end.
3127 * @param {string} scope "global" or device-specific.
3128 * @param {string} roomId the id of the room.
3129 * @param {string} mute the mute state.
3130 * @return {module:client.Promise} Resolves: result object
3131 * @return {module:http-api.MatrixError} Rejects: with an error response.
3132 */
3133MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
3134 const self = this;
3135 let deferred, hasDontNotifyRule;
3136
3137 // Get the existing room-kind push rule if any
3138 const roomPushRule = this.getRoomPushRule(scope, roomId);
3139 if (roomPushRule) {
3140 if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
3141 hasDontNotifyRule = true;
3142 }
3143 }
3144
3145 if (!mute) {
3146 // Remove the rule only if it is a muting rule
3147 if (hasDontNotifyRule) {
3148 deferred = this.deletePushRule(scope, "room", roomPushRule.rule_id);
3149 }
3150 } else {
3151 if (!roomPushRule) {
3152 deferred = this.addPushRule(scope, "room", roomId, {
3153 actions: ["dont_notify"],
3154 });
3155 } else if (!hasDontNotifyRule) {
3156 // Remove the existing one before setting the mute push rule
3157 // This is a workaround to SYN-590 (Push rule update fails)
3158 deferred = Promise.defer();
3159 this.deletePushRule(scope, "room", roomPushRule.rule_id)
3160 .done(function() {
3161 self.addPushRule(scope, "room", roomId, {
3162 actions: ["dont_notify"],
3163 }).done(function() {
3164 deferred.resolve();
3165 }, function(err) {
3166 deferred.reject(err);
3167 });
3168 }, function(err) {
3169 deferred.reject(err);
3170 });
3171
3172 deferred = deferred.promise;
3173 }
3174 }
3175
3176 if (deferred) {
3177 // Update this.pushRules when the operation completes
3178 const ruleRefreshDeferred = Promise.defer();
3179 deferred.done(function() {
3180 self.getPushRules().done(function(result) {
3181 self.pushRules = result;
3182 ruleRefreshDeferred.resolve();
3183 }, function(err) {
3184 ruleRefreshDeferred.reject(err);
3185 });
3186 }, function(err) {
3187 // Update it even if the previous operation fails. This can help the
3188 // app to recover when push settings has been modifed from another client
3189 self.getPushRules().done(function(result) {
3190 self.pushRules = result;
3191 ruleRefreshDeferred.reject(err);
3192 }, function(err2) {
3193 ruleRefreshDeferred.reject(err);
3194 });
3195 });
3196 return ruleRefreshDeferred.promise;
3197 }
3198};
3199
3200// Search
3201// ======
3202
3203/**
3204 * Perform a server-side search for messages containing the given text.
3205 * @param {Object} opts Options for the search.
3206 * @param {string} opts.query The text to query.
3207 * @param {string=} opts.keys The keys to search on. Defaults to all keys. One
3208 * of "content.body", "content.name", "content.topic".
3209 * @param {module:client.callback} callback Optional.
3210 * @return {module:client.Promise} Resolves: TODO
3211 * @return {module:http-api.MatrixError} Rejects: with an error response.
3212 */
3213MatrixClient.prototype.searchMessageText = function(opts, callback) {
3214 const roomEvents = {
3215 search_term: opts.query,
3216 };
3217
3218 if ('keys' in opts) {
3219 roomEvents.keys = opts.keys;
3220 }
3221
3222 return this.search({
3223 body: {
3224 search_categories: {
3225 room_events: roomEvents,
3226 },
3227 },
3228 }, callback);
3229};
3230
3231/**
3232 * Perform a server-side search for room events.
3233 *
3234 * The returned promise resolves to an object containing the fields:
3235 *
3236 * * {number} count: estimate of the number of results
3237 * * {string} next_batch: token for back-pagination; if undefined, there are
3238 * no more results
3239 * * {Array} highlights: a list of words to highlight from the stemming
3240 * algorithm
3241 * * {Array} results: a list of results
3242 *
3243 * Each entry in the results list is a {module:models/search-result.SearchResult}.
3244 *
3245 * @param {Object} opts
3246 * @param {string} opts.term the term to search for
3247 * @param {Object} opts.filter a JSON filter object to pass in the request
3248 * @return {module:client.Promise} Resolves: result object
3249 * @return {module:http-api.MatrixError} Rejects: with an error response.
3250 */
3251MatrixClient.prototype.searchRoomEvents = function(opts) {
3252 // TODO: support groups
3253
3254 const body = {
3255 search_categories: {
3256 room_events: {
3257 search_term: opts.term,
3258 filter: opts.filter,
3259 order_by: "recent",
3260 event_context: {
3261 before_limit: 1,
3262 after_limit: 1,
3263 include_profile: true,
3264 },
3265 },
3266 },
3267 };
3268
3269 const searchResults = {
3270 _query: body,
3271 results: [],
3272 highlights: [],
3273 };
3274
3275 return this.search({body: body}).then(
3276 this._processRoomEventsSearch.bind(this, searchResults),
3277 );
3278};
3279
3280/**
3281 * Take a result from an earlier searchRoomEvents call, and backfill results.
3282 *
3283 * @param {object} searchResults the results object to be updated
3284 * @return {module:client.Promise} Resolves: updated result object
3285 * @return {Error} Rejects: with an error response.
3286 */
3287MatrixClient.prototype.backPaginateRoomEventsSearch = function(searchResults) {
3288 // TODO: we should implement a backoff (as per scrollback()) to deal more
3289 // nicely with HTTP errors.
3290
3291 if (!searchResults.next_batch) {
3292 return Promise.reject(new Error("Cannot backpaginate event search any further"));
3293 }
3294
3295 if (searchResults.pendingRequest) {
3296 // already a request in progress - return the existing promise
3297 return searchResults.pendingRequest;
3298 }
3299
3300 const searchOpts = {
3301 body: searchResults._query,
3302 next_batch: searchResults.next_batch,
3303 };
3304
3305 const promise = this.search(searchOpts).then(
3306 this._processRoomEventsSearch.bind(this, searchResults),
3307 ).finally(function() {
3308 searchResults.pendingRequest = null;
3309 });
3310 searchResults.pendingRequest = promise;
3311
3312 return promise;
3313};
3314
3315/**
3316 * helper for searchRoomEvents and backPaginateRoomEventsSearch. Processes the
3317 * response from the API call and updates the searchResults
3318 *
3319 * @param {Object} searchResults
3320 * @param {Object} response
3321 * @return {Object} searchResults
3322 * @private
3323 */
3324MatrixClient.prototype._processRoomEventsSearch = function(searchResults, response) {
3325 const room_events = response.search_categories.room_events;
3326
3327 searchResults.count = room_events.count;
3328 searchResults.next_batch = room_events.next_batch;
3329
3330 // combine the highlight list with our existing list; build an object
3331 // to avoid O(N^2) fail
3332 const highlights = {};
3333 room_events.highlights.forEach(function(hl) {
3334 highlights[hl] = 1;
3335 });
3336 searchResults.highlights.forEach(function(hl) {
3337 highlights[hl] = 1;
3338 });
3339
3340 // turn it back into a list.
3341 searchResults.highlights = Object.keys(highlights);
3342
3343 // append the new results to our existing results
3344 for (let i = 0; i < room_events.results.length; i++) {
3345 const sr = SearchResult.fromJson(room_events.results[i], this.getEventMapper());
3346 searchResults.results.push(sr);
3347 }
3348 return searchResults;
3349};
3350
3351
3352/**
3353 * Populate the store with rooms the user has left.
3354 * @return {module:client.Promise} Resolves: TODO - Resolved when the rooms have
3355 * been added to the data store.
3356 * @return {module:http-api.MatrixError} Rejects: with an error response.
3357 */
3358MatrixClient.prototype.syncLeftRooms = function() {
3359 // Guard against multiple calls whilst ongoing and multiple calls post success
3360 if (this._syncedLeftRooms) {
3361 return Promise.resolve([]); // don't call syncRooms again if it succeeded.
3362 }
3363 if (this._syncLeftRoomsPromise) {
3364 return this._syncLeftRoomsPromise; // return the ongoing request
3365 }
3366 const self = this;
3367 const syncApi = new SyncApi(this, this._clientOpts);
3368 this._syncLeftRoomsPromise = syncApi.syncLeftRooms();
3369
3370 // cleanup locks
3371 this._syncLeftRoomsPromise.then(function(res) {
3372 console.log("Marking success of sync left room request");
3373 self._syncedLeftRooms = true; // flip the bit on success
3374 }).finally(function() {
3375 self._syncLeftRoomsPromise = null; // cleanup ongoing request state
3376 });
3377
3378 return this._syncLeftRoomsPromise;
3379};
3380
3381// Filters
3382// =======
3383
3384/**
3385 * Create a new filter.
3386 * @param {Object} content The HTTP body for the request
3387 * @return {Filter} Resolves to a Filter object.
3388 * @return {module:http-api.MatrixError} Rejects: with an error response.
3389 */
3390MatrixClient.prototype.createFilter = function(content) {
3391 const self = this;
3392 const path = utils.encodeUri("/user/$userId/filter", {
3393 $userId: this.credentials.userId,
3394 });
3395 return this._http.authedRequest(
3396 undefined, "POST", path, undefined, content,
3397 ).then(function(response) {
3398 // persist the filter
3399 const filter = Filter.fromJson(
3400 self.credentials.userId, response.filter_id, content,
3401 );
3402 self.store.storeFilter(filter);
3403 return filter;
3404 });
3405};
3406
3407/**
3408 * Retrieve a filter.
3409 * @param {string} userId The user ID of the filter owner
3410 * @param {string} filterId The filter ID to retrieve
3411 * @param {boolean} allowCached True to allow cached filters to be returned.
3412 * Default: True.
3413 * @return {module:client.Promise} Resolves: TODO
3414 * @return {module:http-api.MatrixError} Rejects: with an error response.
3415 */
3416MatrixClient.prototype.getFilter = function(userId, filterId, allowCached) {
3417 if (allowCached) {
3418 const filter = this.store.getFilter(userId, filterId);
3419 if (filter) {
3420 return Promise.resolve(filter);
3421 }
3422 }
3423
3424 const self = this;
3425 const path = utils.encodeUri("/user/$userId/filter/$filterId", {
3426 $userId: userId,
3427 $filterId: filterId,
3428 });
3429
3430 return this._http.authedRequest(
3431 undefined, "GET", path, undefined, undefined,
3432 ).then(function(response) {
3433 // persist the filter
3434 const filter = Filter.fromJson(
3435 userId, filterId, response,
3436 );
3437 self.store.storeFilter(filter);
3438 return filter;
3439 });
3440};
3441
3442/**
3443 * @param {string} filterName
3444 * @param {Filter} filter
3445 * @return {Promise<String>} Filter ID
3446 */
3447MatrixClient.prototype.getOrCreateFilter = function(filterName, filter) {
3448 const filterId = this.store.getFilterIdByName(filterName);
3449 let promise = Promise.resolve();
3450 const self = this;
3451
3452 if (filterId) {
3453 // check that the existing filter matches our expectations
3454 promise = self.getFilter(self.credentials.userId,
3455 filterId, true,
3456 ).then(function(existingFilter) {
3457 const oldDef = existingFilter.getDefinition();
3458 const newDef = filter.getDefinition();
3459
3460 if (utils.deepCompare(oldDef, newDef)) {
3461 // super, just use that.
3462 // debuglog("Using existing filter ID %s: %s", filterId,
3463 // JSON.stringify(oldDef));
3464 return Promise.resolve(filterId);
3465 }
3466 // debuglog("Existing filter ID %s: %s; new filter: %s",
3467 // filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
3468 self.store.setFilterIdByName(filterName, undefined);
3469 return undefined;
3470 }, function(error) {
3471 // Synapse currently returns the following when the filter cannot be found:
3472 // {
3473 // errcode: "M_UNKNOWN",
3474 // name: "M_UNKNOWN",
3475 // message: "No row found",
3476 // data: Object, httpStatus: 404
3477 // }
3478 if (error.httpStatus === 404 &&
3479 (error.errcode === "M_UNKNOWN" || error.errcode === "M_NOT_FOUND")) {
3480 // Clear existing filterId from localStorage
3481 // if it no longer exists on the server
3482 self.store.setFilterIdByName(filterName, undefined);
3483 // Return a undefined value for existingId further down the promise chain
3484 return undefined;
3485 } else {
3486 throw error;
3487 }
3488 });
3489 }
3490
3491 return promise.then(function(existingId) {
3492 if (existingId) {
3493 return existingId;
3494 }
3495
3496 // create a new filter
3497 return self.createFilter(filter.getDefinition(),
3498 ).then(function(createdFilter) {
3499 // debuglog("Created new filter ID %s: %s", createdFilter.filterId,
3500 // JSON.stringify(createdFilter.getDefinition()));
3501 self.store.setFilterIdByName(filterName, createdFilter.filterId);
3502 return createdFilter.filterId;
3503 });
3504 });
3505};
3506
3507
3508/**
3509 * Gets a bearer token from the Home Server that the user can
3510 * present to a third party in order to prove their ownership
3511 * of the Matrix account they are logged into.
3512 * @return {module:client.Promise} Resolves: Token object
3513 * @return {module:http-api.MatrixError} Rejects: with an error response.
3514 */
3515MatrixClient.prototype.getOpenIdToken = function() {
3516 const path = utils.encodeUri("/user/$userId/openid/request_token", {
3517 $userId: this.credentials.userId,
3518 });
3519
3520 return this._http.authedRequest(
3521 undefined, "POST", path, undefined, {},
3522 );
3523};
3524
3525
3526// VoIP operations
3527// ===============
3528
3529/**
3530 * @param {module:client.callback} callback Optional.
3531 * @return {module:client.Promise} Resolves: TODO
3532 * @return {module:http-api.MatrixError} Rejects: with an error response.
3533 */
3534MatrixClient.prototype.turnServer = function(callback) {
3535 return this._http.authedRequest(callback, "GET", "/voip/turnServer");
3536};
3537
3538/**
3539 * Get the TURN servers for this home server.
3540 * @return {Array<Object>} The servers or an empty list.
3541 */
3542MatrixClient.prototype.getTurnServers = function() {
3543 return this._turnServers || [];
3544};
3545
3546// Higher level APIs
3547// =================
3548
3549// TODO: stuff to handle:
3550// local echo
3551// event dup suppression? - apparently we should still be doing this
3552// tracking current display name / avatar per-message
3553// pagination
3554// re-sending (including persisting pending messages to be sent)
3555// - Need a nice way to callback the app for arbitrary events like
3556// displayname changes
3557// due to ambiguity (or should this be on a chat-specific layer)?
3558// reconnect after connectivity outages
3559
3560
3561/**
3562 * High level helper method to begin syncing and poll for new events. To listen for these
3563 * events, add a listener for {@link module:client~MatrixClient#event:"event"}
3564 * via {@link module:client~MatrixClient#on}. Alternatively, listen for specific
3565 * state change events.
3566 * @param {Object=} opts Options to apply when syncing.
3567 * @param {Number=} opts.initialSyncLimit The event <code>limit=</code> to apply
3568 * to initial sync. Default: 8.
3569 * @param {Boolean=} opts.includeArchivedRooms True to put <code>archived=true</code>
3570 * on the <code>/initialSync</code> request. Default: false.
3571 * @param {Boolean=} opts.resolveInvitesToProfiles True to do /profile requests
3572 * on every invite event if the displayname/avatar_url is not known for this user ID.
3573 * Default: false.
3574 *
3575 * @param {String=} opts.pendingEventOrdering Controls where pending messages
3576 * appear in a room's timeline. If "<b>chronological</b>", messages will appear
3577 * in the timeline when the call to <code>sendEvent</code> was made. If
3578 * "<b>detached</b>", pending messages will appear in a separate list,
3579 * accessbile via {@link module:models/room#getPendingEvents}. Default:
3580 * "chronological".
3581 *
3582 * @param {Number=} opts.pollTimeout The number of milliseconds to wait on /sync.
3583 * Default: 30000 (30 seconds).
3584 *
3585 * @param {Filter=} opts.filter The filter to apply to /sync calls. This will override
3586 * the opts.initialSyncLimit, which would normally result in a timeline limit filter.
3587 *
3588 * @param {Boolean=} opts.disablePresence True to perform syncing without automatically
3589 * updating presence.
3590 * @param {Boolean=} opts.lazyLoadMembers True to not load all membership events during
3591 * initial sync but fetch them when needed by calling `loadOutOfBandMembers`
3592 * This will override the filter option at this moment.
3593 */
3594MatrixClient.prototype.startClient = async function(opts) {
3595 if (this.clientRunning) {
3596 // client is already running.
3597 return;
3598 }
3599 this.clientRunning = true;
3600 // backwards compat for when 'opts' was 'historyLen'.
3601 if (typeof opts === "number") {
3602 opts = {
3603 initialSyncLimit: opts,
3604 };
3605 }
3606
3607 if (this._crypto) {
3608 this._crypto.uploadDeviceKeys().done();
3609 this._crypto.start();
3610 }
3611
3612 // periodically poll for turn servers if we support voip
3613 checkTurnServers(this);
3614
3615 if (this._syncApi) {
3616 // This shouldn't happen since we thought the client was not running
3617 console.error("Still have sync object whilst not running: stopping old one");
3618 this._syncApi.stop();
3619 }
3620
3621 // shallow-copy the opts dict before modifying and storing it
3622 opts = Object.assign({}, opts);
3623
3624 opts.crypto = this._crypto;
3625 opts.canResetEntireTimeline = (roomId) => {
3626 if (!this._canResetTimelineCallback) {
3627 return false;
3628 }
3629 return this._canResetTimelineCallback(roomId);
3630 };
3631 this._clientOpts = opts;
3632 this._syncApi = new SyncApi(this, opts);
3633 this._syncApi.sync();
3634};
3635
3636/**
3637 * store client options with boolean/string/numeric values
3638 * to know in the next session what flags the sync data was
3639 * created with (e.g. lazy loading)
3640 * @param {object} opts the complete set of client options
3641 * @return {Promise} for store operation */
3642MatrixClient.prototype._storeClientOptions = function() {
3643 const primTypes = ["boolean", "string", "number"];
3644 const serializableOpts = Object.entries(this._clientOpts)
3645 .filter(([key, value]) => {
3646 return primTypes.includes(typeof value);
3647 })
3648 .reduce((obj, [key, value]) => {
3649 obj[key] = value;
3650 return obj;
3651 }, {});
3652 return this.store.storeClientOptions(serializableOpts);
3653};
3654
3655/**
3656 * High level helper method to stop the client from polling and allow a
3657 * clean shutdown.
3658 */
3659MatrixClient.prototype.stopClient = function() {
3660 console.log('stopping MatrixClient');
3661
3662 this.clientRunning = false;
3663 // TODO: f.e. Room => self.store.storeRoom(room) ?
3664 if (this._syncApi) {
3665 this._syncApi.stop();
3666 this._syncApi = null;
3667 }
3668 if (this._crypto) {
3669 this._crypto.stop();
3670 }
3671 if (this._peekSync) {
3672 this._peekSync.stopPeeking();
3673 }
3674 global.clearTimeout(this._checkTurnServersTimeoutID);
3675};
3676
3677/*
3678 * Query the server to see if it support members lazy loading
3679 * @return {Promise<boolean>} true if server supports lazy loading
3680 */
3681MatrixClient.prototype.doesServerSupportLazyLoading = async function() {
3682 if (this._serverSupportsLazyLoading === null) {
3683 const response = await this._http.request(
3684 undefined, // callback
3685 "GET", "/_matrix/client/versions",
3686 undefined, // queryParams
3687 undefined, // data
3688 {
3689 prefix: '',
3690 },
3691 );
3692 const unstableFeatures = response["unstable_features"];
3693 this._serverSupportsLazyLoading =
3694 unstableFeatures && unstableFeatures["m.lazy_load_members"];
3695 }
3696 return this._serverSupportsLazyLoading;
3697};
3698
3699/*
3700 * Get if lazy loading members is being used.
3701 * @return {boolean} Whether or not members are lazy loaded by this client
3702 */
3703MatrixClient.prototype.hasLazyLoadMembersEnabled = function() {
3704 return !!this._clientOpts.lazyLoadMembers;
3705};
3706
3707/*
3708 * Set a function which is called when /sync returns a 'limited' response.
3709 * It is called with a room ID and returns a boolean. It should return 'true' if the SDK
3710 * can SAFELY remove events from this room. It may not be safe to remove events if there
3711 * are other references to the timelines for this room, e.g because the client is
3712 * actively viewing events in this room.
3713 * Default: returns false.
3714 * @param {Function} cb The callback which will be invoked.
3715 */
3716MatrixClient.prototype.setCanResetTimelineCallback = function(cb) {
3717 this._canResetTimelineCallback = cb;
3718};
3719
3720/**
3721 * Get the callback set via `setCanResetTimelineCallback`.
3722 * @return {?Function} The callback or null
3723 */
3724MatrixClient.prototype.getCanResetTimelineCallback = function() {
3725 return this._canResetTimelineCallback;
3726};
3727
3728function setupCallEventHandler(client) {
3729 const candidatesByCall = {
3730 // callId: [Candidate]
3731 };
3732
3733 // Maintain a buffer of events before the client has synced for the first time.
3734 // This buffer will be inspected to see if we should send incoming call
3735 // notifications. It needs to be buffered to correctly determine if an
3736 // incoming call has had a matching answer/hangup.
3737 let callEventBuffer = [];
3738 let isClientPrepared = false;
3739 client.on("sync", function(state) {
3740 if (state === "PREPARED") {
3741 isClientPrepared = true;
3742 const ignoreCallIds = {}; // Set<String>
3743 // inspect the buffer and mark all calls which have been answered
3744 // or hung up before passing them to the call event handler.
3745 for (let i = callEventBuffer.length - 1; i >= 0; i--) {
3746 const ev = callEventBuffer[i];
3747 if (ev.getType() === "m.call.answer" ||
3748 ev.getType() === "m.call.hangup") {
3749 ignoreCallIds[ev.getContent().call_id] = "yep";
3750 }
3751 }
3752 // now loop through the buffer chronologically and inject them
3753 callEventBuffer.forEach(function(e) {
3754 if (ignoreCallIds[e.getContent().call_id]) {
3755 // This call has previously been ansered or hung up: ignore it
3756 return;
3757 }
3758 callEventHandler(e);
3759 });
3760 callEventBuffer = [];
3761 }
3762 });
3763
3764 client.on("event", onEvent);
3765
3766 function onEvent(event) {
3767 if (event.getType().indexOf("m.call.") !== 0) {
3768 // not a call event
3769 if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
3770 // not *yet* a call event, but might become one...
3771 event.once("Event.decrypted", onEvent);
3772 }
3773 return;
3774 }
3775 if (!isClientPrepared) {
3776 callEventBuffer.push(event);
3777 return;
3778 }
3779 callEventHandler(event);
3780 }
3781
3782 function callEventHandler(event) {
3783 const content = event.getContent();
3784 let call = content.call_id ? client.callList[content.call_id] : undefined;
3785 let i;
3786 //console.log("RECV %s content=%s", event.getType(), JSON.stringify(content));
3787
3788 if (event.getType() === "m.call.invite") {
3789 if (event.getSender() === client.credentials.userId) {
3790 return; // ignore invites you send
3791 }
3792
3793 if (event.getAge() > content.lifetime) {
3794 return; // expired call
3795 }
3796
3797 if (call && call.state === "ended") {
3798 return; // stale/old invite event
3799 }
3800 if (call) {
3801 console.log(
3802 "WARN: Already have a MatrixCall with id %s but got an " +
3803 "invite. Clobbering.",
3804 content.call_id,
3805 );
3806 }
3807
3808 call = webRtcCall.createNewMatrixCall(client, event.getRoomId(), {
3809 forceTURN: client._forceTURN,
3810 });
3811 if (!call) {
3812 console.log(
3813 "Incoming call ID " + content.call_id + " but this client " +
3814 "doesn't support WebRTC",
3815 );
3816 // don't hang up the call: there could be other clients
3817 // connected that do support WebRTC and declining the
3818 // the call on their behalf would be really annoying.
3819 return;
3820 }
3821
3822 call.callId = content.call_id;
3823 call._initWithInvite(event);
3824 client.callList[call.callId] = call;
3825
3826 // if we stashed candidate events for that call ID, play them back now
3827 if (candidatesByCall[call.callId]) {
3828 for (i = 0; i < candidatesByCall[call.callId].length; i++) {
3829 call._gotRemoteIceCandidate(
3830 candidatesByCall[call.callId][i],
3831 );
3832 }
3833 }
3834
3835 // Were we trying to call that user (room)?
3836 let existingCall;
3837 const existingCalls = utils.values(client.callList);
3838 for (i = 0; i < existingCalls.length; ++i) {
3839 const thisCall = existingCalls[i];
3840 if (call.roomId === thisCall.roomId &&
3841 thisCall.direction === 'outbound' &&
3842 (["wait_local_media", "create_offer", "invite_sent"].indexOf(
3843 thisCall.state) !== -1)) {
3844 existingCall = thisCall;
3845 break;
3846 }
3847 }
3848
3849 if (existingCall) {
3850 // If we've only got to wait_local_media or create_offer and
3851 // we've got an invite, pick the incoming call because we know
3852 // we haven't sent our invite yet otherwise, pick whichever
3853 // call has the lowest call ID (by string comparison)
3854 if (existingCall.state === 'wait_local_media' ||
3855 existingCall.state === 'create_offer' ||
3856 existingCall.callId > call.callId) {
3857 console.log(
3858 "Glare detected: answering incoming call " + call.callId +
3859 " and canceling outgoing call " + existingCall.callId,
3860 );
3861 existingCall._replacedBy(call);
3862 call.answer();
3863 } else {
3864 console.log(
3865 "Glare detected: rejecting incoming call " + call.callId +
3866 " and keeping outgoing call " + existingCall.callId,
3867 );
3868 call.hangup();
3869 }
3870 } else {
3871 client.emit("Call.incoming", call);
3872 }
3873 } else if (event.getType() === 'm.call.answer') {
3874 if (!call) {
3875 return;
3876 }
3877 if (event.getSender() === client.credentials.userId) {
3878 if (call.state === 'ringing') {
3879 call._onAnsweredElsewhere(content);
3880 }
3881 } else {
3882 call._receivedAnswer(content);
3883 }
3884 } else if (event.getType() === 'm.call.candidates') {
3885 if (event.getSender() === client.credentials.userId) {
3886 return;
3887 }
3888 if (!call) {
3889 // store the candidates; we may get a call eventually.
3890 if (!candidatesByCall[content.call_id]) {
3891 candidatesByCall[content.call_id] = [];
3892 }
3893 candidatesByCall[content.call_id] = candidatesByCall[
3894 content.call_id
3895 ].concat(content.candidates);
3896 } else {
3897 for (i = 0; i < content.candidates.length; i++) {
3898 call._gotRemoteIceCandidate(content.candidates[i]);
3899 }
3900 }
3901 } else if (event.getType() === 'm.call.hangup') {
3902 // Note that we also observe our own hangups here so we can see
3903 // if we've already rejected a call that would otherwise be valid
3904 if (!call) {
3905 // if not live, store the fact that the call has ended because
3906 // we're probably getting events backwards so
3907 // the hangup will come before the invite
3908 call = webRtcCall.createNewMatrixCall(client, event.getRoomId());
3909 if (call) {
3910 call.callId = content.call_id;
3911 call._initWithHangup(event);
3912 client.callList[content.call_id] = call;
3913 }
3914 } else {
3915 if (call.state !== 'ended') {
3916 call._onHangupReceived(content);
3917 delete client.callList[content.call_id];
3918 }
3919 }
3920 }
3921 }
3922}
3923
3924function checkTurnServers(client) {
3925 if (!client._supportsVoip) {
3926 return;
3927 }
3928 if (client.isGuest()) {
3929 return; // guests can't access TURN servers
3930 }
3931
3932 client.turnServer().done(function(res) {
3933 if (res.uris) {
3934 console.log("Got TURN URIs: " + res.uris + " refresh in " +
3935 res.ttl + " secs");
3936 // map the response to a format that can be fed to
3937 // RTCPeerConnection
3938 const servers = {
3939 urls: res.uris,
3940 username: res.username,
3941 credential: res.password,
3942 };
3943 client._turnServers = [servers];
3944 // re-fetch when we're about to reach the TTL
3945 client._checkTurnServersTimeoutID = setTimeout(() => {
3946 checkTurnServers(client);
3947 }, (res.ttl || (60 * 60)) * 1000 * 0.9);
3948 }
3949 }, function(err) {
3950 console.error("Failed to get TURN URIs");
3951 client._checkTurnServersTimeoutID =
3952 setTimeout(function() {
3953 checkTurnServers(client);
3954}, 60000);
3955 });
3956}
3957
3958function _reject(callback, defer, err) {
3959 if (callback) {
3960 callback(err);
3961 }
3962 defer.reject(err);
3963}
3964
3965function _resolve(callback, defer, res) {
3966 if (callback) {
3967 callback(null, res);
3968 }
3969 defer.resolve(res);
3970}
3971
3972function _PojoToMatrixEventMapper(client) {
3973 function mapper(plainOldJsObject) {
3974 const event = new MatrixEvent(plainOldJsObject);
3975 if (event.isEncrypted()) {
3976 client.reEmitter.reEmit(event, [
3977 "Event.decrypted",
3978 ]);
3979 event.attemptDecryption(client._crypto);
3980 }
3981 return event;
3982 }
3983 return mapper;
3984}
3985
3986/**
3987 * @return {Function}
3988 */
3989MatrixClient.prototype.getEventMapper = function() {
3990 return _PojoToMatrixEventMapper(this);
3991};
3992
3993// Identity Server Operations
3994// ==========================
3995
3996/**
3997 * Generates a random string suitable for use as a client secret. <strong>This
3998 * method is experimental and may change.</strong>
3999 * @return {string} A new client secret
4000 */
4001MatrixClient.prototype.generateClientSecret = function() {
4002 return randomString(32);
4003};
4004
4005/** */
4006module.exports.MatrixClient = MatrixClient;
4007/** */
4008module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
4009
4010// MatrixClient Event JSDocs
4011
4012/**
4013 * Fires whenever the SDK receives a new event.
4014 * <p>
4015 * This is only fired for live events received via /sync - it is not fired for
4016 * events received over context, search, or pagination APIs.
4017 *
4018 * @event module:client~MatrixClient#"event"
4019 * @param {MatrixEvent} event The matrix event which caused this event to fire.
4020 * @example
4021 * matrixClient.on("event", function(event){
4022 * var sender = event.getSender();
4023 * });
4024 */
4025
4026/**
4027 * Fires whenever the SDK receives a new to-device event.
4028 * @event module:client~MatrixClient#"toDeviceEvent"
4029 * @param {MatrixEvent} event The matrix event which caused this event to fire.
4030 * @example
4031 * matrixClient.on("toDeviceEvent", function(event){
4032 * var sender = event.getSender();
4033 * });
4034 */
4035
4036/**
4037 * Fires whenever the SDK's syncing state is updated. The state can be one of:
4038 * <ul>
4039 *
4040 * <li>PREPARED: The client has synced with the server at least once and is
4041 * ready for methods to be called on it. This will be immediately followed by
4042 * a state of SYNCING. <i>This is the equivalent of "syncComplete" in the
4043 * previous API.</i></li>
4044 *
4045 * <li>CATCHUP: The client has detected the connection to the server might be
4046 * available again and will now try to do a sync again. As this sync might take
4047 * a long time (depending how long ago was last synced, and general server
4048 * performance) the client is put in this mode so the UI can reflect trying
4049 * to catch up with the server after losing connection.</li>
4050 *
4051 * <li>SYNCING : The client is currently polling for new events from the server.
4052 * This will be called <i>after</i> processing latest events from a sync.</li>
4053 *
4054 * <li>ERROR : The client has had a problem syncing with the server. If this is
4055 * called <i>before</i> PREPARED then there was a problem performing the initial
4056 * sync. If this is called <i>after</i> PREPARED then there was a problem polling
4057 * the server for updates. This may be called multiple times even if the state is
4058 * already ERROR. <i>This is the equivalent of "syncError" in the previous
4059 * API.</i></li>
4060 *
4061 * <li>RECONNECTING: The sync connection has dropped, but not (yet) in a way that
4062 * should be considered erroneous.
4063 * </li>
4064 *
4065 * <li>STOPPED: The client has stopped syncing with server due to stopClient
4066 * being called.
4067 * </li>
4068 * </ul>
4069 * State transition diagram:
4070 * <pre>
4071 * +---->STOPPED
4072 * |
4073 * +----->PREPARED -------> SYNCING <--+
4074 * | ^ | ^ |
4075 * | CATCHUP ----------+ | | |
4076 * | ^ V | |
4077 * null ------+ | +------- RECONNECTING |
4078 * | V V |
4079 * +------->ERROR ---------------------+
4080 *
4081 * NB: 'null' will never be emitted by this event.
4082 *
4083 * </pre>
4084 * Transitions:
4085 * <ul>
4086 *
4087 * <li><code>null -> PREPARED</code> : Occurs when the initial sync is completed
4088 * first time. This involves setting up filters and obtaining push rules.
4089 *
4090 * <li><code>null -> ERROR</code> : Occurs when the initial sync failed first time.
4091 *
4092 * <li><code>ERROR -> PREPARED</code> : Occurs when the initial sync succeeds
4093 * after previously failing.
4094 *
4095 * <li><code>PREPARED -> SYNCING</code> : Occurs immediately after transitioning
4096 * to PREPARED. Starts listening for live updates rather than catching up.
4097 *
4098 * <li><code>SYNCING -> RECONNECTING</code> : Occurs when the live update fails.
4099 *
4100 * <li><code>RECONNECTING -> RECONNECTING</code> : Can occur if the update calls
4101 * continue to fail, but the keepalive calls (to /versions) succeed.
4102 *
4103 * <li><code>RECONNECTING -> ERROR</code> : Occurs when the keepalive call also fails
4104 *
4105 * <li><code>ERROR -> SYNCING</code> : Occurs when the client has performed a
4106 * live update after having previously failed.
4107 *
4108 * <li><code>ERROR -> ERROR</code> : Occurs when the client has failed to keepalive
4109 * for a second time or more.</li>
4110 *
4111 * <li><code>SYNCING -> SYNCING</code> : Occurs when the client has performed a live
4112 * update. This is called <i>after</i> processing.</li>
4113 *
4114 * <li><code>* -> STOPPED</code> : Occurs once the client has stopped syncing or
4115 * trying to sync after stopClient has been called.</li>
4116 * </ul>
4117 *
4118 * @event module:client~MatrixClient#"sync"
4119 *
4120 * @param {string} state An enum representing the syncing state. One of "PREPARED",
4121 * "SYNCING", "ERROR", "STOPPED".
4122 *
4123 * @param {?string} prevState An enum representing the previous syncing state.
4124 * One of "PREPARED", "SYNCING", "ERROR", "STOPPED" <b>or null</b>.
4125 *
4126 * @param {?Object} data Data about this transition.
4127 *
4128 * @param {MatrixError} data.error The matrix error if <code>state=ERROR</code>.
4129 *
4130 * @param {String} data.oldSyncToken The 'since' token passed to /sync.
4131 * <code>null</code> for the first successful sync since this client was
4132 * started. Only present if <code>state=PREPARED</code> or
4133 * <code>state=SYNCING</code>.
4134 *
4135 * @param {String} data.nextSyncToken The 'next_batch' result from /sync, which
4136 * will become the 'since' token for the next call to /sync. Only present if
4137 * <code>state=PREPARED</code> or <code>state=SYNCING</code>.
4138 *
4139 * @param {boolean} data.catchingUp True if we are working our way through a
4140 * backlog of events after connecting. Only present if <code>state=SYNCING</code>.
4141 *
4142 * @example
4143 * matrixClient.on("sync", function(state, prevState, data) {
4144 * switch (state) {
4145 * case "ERROR":
4146 * // update UI to say "Connection Lost"
4147 * break;
4148 * case "SYNCING":
4149 * // update UI to remove any "Connection Lost" message
4150 * break;
4151 * case "PREPARED":
4152 * // the client instance is ready to be queried.
4153 * var rooms = matrixClient.getRooms();
4154 * break;
4155 * }
4156 * });
4157 */
4158
4159 /**
4160 * Fires whenever the sdk learns about a new group. <strong>This event
4161 * is experimental and may change.</strong>
4162 * @event module:client~MatrixClient#"Group"
4163 * @param {Group} group The newly created, fully populated group.
4164 * @example
4165 * matrixClient.on("Group", function(group){
4166 * var groupId = group.groupId;
4167 * });
4168 */
4169
4170 /**
4171 * Fires whenever a new Room is added. This will fire when you are invited to a
4172 * room, as well as when you join a room. <strong>This event is experimental and
4173 * may change.</strong>
4174 * @event module:client~MatrixClient#"Room"
4175 * @param {Room} room The newly created, fully populated room.
4176 * @example
4177 * matrixClient.on("Room", function(room){
4178 * var roomId = room.roomId;
4179 * });
4180 */
4181
4182 /**
4183 * Fires whenever a Room is removed. This will fire when you forget a room.
4184 * <strong>This event is experimental and may change.</strong>
4185 * @event module:client~MatrixClient#"deleteRoom"
4186 * @param {string} roomId The deleted room ID.
4187 * @example
4188 * matrixClient.on("deleteRoom", function(roomId){
4189 * // update UI from getRooms()
4190 * });
4191 */
4192
4193/**
4194 * Fires whenever an incoming call arrives.
4195 * @event module:client~MatrixClient#"Call.incoming"
4196 * @param {module:webrtc/call~MatrixCall} call The incoming call.
4197 * @example
4198 * matrixClient.on("Call.incoming", function(call){
4199 * call.answer(); // auto-answer
4200 * });
4201 */
4202
4203/**
4204 * Fires whenever the login session the JS SDK is using is no
4205 * longer valid and the user must log in again.
4206 * NB. This only fires when action is required from the user, not
4207 * when then login session can be renewed by using a refresh token.
4208 * @event module:client~MatrixClient#"Session.logged_out"
4209 * @example
4210 * matrixClient.on("Session.logged_out", function(call){
4211 * // show the login screen
4212 * });
4213 */
4214
4215/**
4216 * Fires when the JS SDK receives a M_CONSENT_NOT_GIVEN error in response
4217 * to a HTTP request.
4218 * @event module:client~MatrixClient#"no_consent"
4219 * @example
4220 * matrixClient.on("no_consent", function(message, contentUri) {
4221 * console.info(message + ' Go to ' + contentUri);
4222 * });
4223 */
4224
4225/**
4226 * Fires when a device is marked as verified/unverified/blocked/unblocked by
4227 * {@link module:client~MatrixClient#setDeviceVerified|MatrixClient.setDeviceVerified} or
4228 * {@link module:client~MatrixClient#setDeviceBlocked|MatrixClient.setDeviceBlocked}.
4229 *
4230 * @event module:client~MatrixClient#"deviceVerificationChanged"
4231 * @param {string} userId the owner of the verified device
4232 * @param {string} deviceId the id of the verified device
4233 * @param {module:crypto/deviceinfo} deviceInfo updated device information
4234 */
4235
4236/**
4237 * Fires whenever new user-scoped account_data is added.
4238 * @event module:client~MatrixClient#"accountData"
4239 * @param {MatrixEvent} event The event describing the account_data just added
4240 * @example
4241 * matrixClient.on("accountData", function(event){
4242 * myAccountData[event.type] = event.content;
4243 * });
4244 */
4245
4246/**
4247 * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled()
4248 * @event module:client~MatrixClient#"crypto.keyBackupStatus"
4249 * @param {bool} enabled true if key backup has been enabled, otherwise false
4250 * @example
4251 * matrixClient.on("crypto.keyBackupStatus", function(enabled){
4252 * if (enabled) {
4253 * [...]
4254 * }
4255 * });
4256 */
4257
4258/**
4259 * Fires when we want to suggest to the user that they restore their megolm keys
4260 * from backup or by cross-signing the device.
4261 *
4262 * @event module:client~MatrixClient#"crypto.suggestKeyRestore"
4263 */
4264
4265/**
4266 * Fires when a key verification is requested.
4267 * @event module:client~MatrixClient#"crypto.verification.request"
4268 * @param {object} data
4269 * @param {MatrixEvent} data.event the original verification request message
4270 * @param {Array} data.methods the verification methods that can be used
4271 * @param {Function} data.beginKeyVerification a function to call if a key
4272 * verification should be performed. The function takes one argument: the
4273 * name of the key verification method (taken from data.methods) to use.
4274 * @param {Function} data.cancel a function to call if the key verification is
4275 * rejected.
4276 */
4277
4278/**
4279 * Fires when a key verification is requested with an unknown method.
4280 * @event module:client~MatrixClient#"crypto.verification.request.unknown"
4281 * @param {string} userId the user ID who requested the key verification
4282 * @param {Function} cancel a function that will send a cancellation message to
4283 * reject the key verification.
4284 */
4285
4286/**
4287 * Fires when a key verification started message is received.
4288 * @event module:client~MatrixClient#"crypto.verification.start"
4289 * @param {module:crypto/verification/Base} verifier a verifier object to
4290 * perform the key verification
4291 */
4292
4293// EventEmitter JSDocs
4294
4295/**
4296 * The {@link https://nodejs.org/api/events.html|EventEmitter} class.
4297 * @external EventEmitter
4298 * @see {@link https://nodejs.org/api/events.html}
4299 */
4300
4301/**
4302 * Adds a listener to the end of the listeners array for the specified event.
4303 * No checks are made to see if the listener has already been added. Multiple
4304 * calls passing the same combination of event and listener will result in the
4305 * listener being added multiple times.
4306 * @function external:EventEmitter#on
4307 * @param {string} event The event to listen for.
4308 * @param {Function} listener The function to invoke.
4309 * @return {EventEmitter} for call chaining.
4310 */
4311
4312/**
4313 * Alias for {@link external:EventEmitter#on}.
4314 * @function external:EventEmitter#addListener
4315 * @param {string} event The event to listen for.
4316 * @param {Function} listener The function to invoke.
4317 * @return {EventEmitter} for call chaining.
4318 */
4319
4320/**
4321 * Adds a <b>one time</b> listener for the event. This listener is invoked only
4322 * the next time the event is fired, after which it is removed.
4323 * @function external:EventEmitter#once
4324 * @param {string} event The event to listen for.
4325 * @param {Function} listener The function to invoke.
4326 * @return {EventEmitter} for call chaining.
4327 */
4328
4329/**
4330 * Remove a listener from the listener array for the specified event.
4331 * <b>Caution:</b> changes array indices in the listener array behind the
4332 * listener.
4333 * @function external:EventEmitter#removeListener
4334 * @param {string} event The event to listen for.
4335 * @param {Function} listener The function to invoke.
4336 * @return {EventEmitter} for call chaining.
4337 */
4338
4339/**
4340 * Removes all listeners, or those of the specified event. It's not a good idea
4341 * to remove listeners that were added elsewhere in the code, especially when
4342 * it's on an emitter that you didn't create (e.g. sockets or file streams).
4343 * @function external:EventEmitter#removeAllListeners
4344 * @param {string} event Optional. The event to remove listeners for.
4345 * @return {EventEmitter} for call chaining.
4346 */
4347
4348/**
4349 * Execute each of the listeners in order with the supplied arguments.
4350 * @function external:EventEmitter#emit
4351 * @param {string} event The event to emit.
4352 * @param {Function} listener The function to invoke.
4353 * @return {boolean} true if event had listeners, false otherwise.
4354 */
4355
4356/**
4357 * By default EventEmitters will print a warning if more than 10 listeners are
4358 * added for a particular event. This is a useful default which helps finding
4359 * memory leaks. Obviously not all Emitters should be limited to 10. This
4360 * function allows that to be increased. Set to zero for unlimited.
4361 * @function external:EventEmitter#setMaxListeners
4362 * @param {Number} n The max number of listeners.
4363 * @return {EventEmitter} for call chaining.
4364 */
4365
4366// MatrixClient Callback JSDocs
4367
4368/**
4369 * The standard MatrixClient callback interface. Functions which accept this
4370 * will specify 2 return arguments. These arguments map to the 2 parameters
4371 * specified in this callback.
4372 * @callback module:client.callback
4373 * @param {Object} err The error value, the "rejected" value or null.
4374 * @param {Object} data The data returned, the "resolved" value.
4375 */
4376
4377 /**
4378 * {@link https://github.com/kriskowal/q|A promise implementation (Q)}. Functions
4379 * which return this will specify 2 return arguments. These arguments map to the
4380 * "onFulfilled" and "onRejected" values of the Promise.
4381 * @typedef {Object} Promise
4382 * @static
4383 * @property {Function} then promise.then(onFulfilled, onRejected, onProgress)
4384 * @property {Function} catch promise.catch(onRejected)
4385 * @property {Function} finally promise.finally(callback)
4386 * @property {Function} done promise.done(onFulfilled, onRejected, onProgress)
4387 */