1 | /*
|
2 | Copyright 2015, 2016 OpenMarket Ltd
|
3 | Copyright 2017 Vector Creations Ltd
|
4 | Copyright 2018-2019 New Vector Ltd
|
5 |
|
6 | Licensed under the Apache License, Version 2.0 (the "License");
|
7 | you may not use this file except in compliance with the License.
|
8 | You may obtain a copy of the License at
|
9 |
|
10 | http://www.apache.org/licenses/LICENSE-2.0
|
11 |
|
12 | Unless required by applicable law or agreed to in writing, software
|
13 | distributed under the License is distributed on an "AS IS" BASIS,
|
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15 | See the License for the specific language governing permissions and
|
16 | limitations under the License.
|
17 | */
|
18 | ;
|
19 |
|
20 | const PushProcessor = require('./pushprocessor');
|
21 |
|
22 | /**
|
23 | * This is an internal module. See {@link MatrixClient} for the public class.
|
24 | * @module client
|
25 | */
|
26 | const EventEmitter = require("events").EventEmitter;
|
27 | import Promise from 'bluebird';
|
28 | const url = require('url');
|
29 |
|
30 | const httpApi = require("./http-api");
|
31 | const MatrixEvent = require("./models/event").MatrixEvent;
|
32 | const EventStatus = require("./models/event").EventStatus;
|
33 | const EventTimeline = require("./models/event-timeline");
|
34 | const SearchResult = require("./models/search-result");
|
35 | const StubStore = require("./store/stub");
|
36 | const webRtcCall = require("./webrtc/call");
|
37 | const utils = require("./utils");
|
38 | const contentRepo = require("./content-repo");
|
39 | const Filter = require("./filter");
|
40 | const SyncApi = require("./sync");
|
41 | const MatrixBaseApis = require("./base-apis");
|
42 | const MatrixError = httpApi.MatrixError;
|
43 | const ContentHelpers = require("./content-helpers");
|
44 | const olmlib = require("./crypto/olmlib");
|
45 |
|
46 | import ReEmitter from './ReEmitter';
|
47 | import RoomList from './crypto/RoomList';
|
48 |
|
49 | import Crypto from './crypto';
|
50 | import { isCryptoAvailable } from './crypto';
|
51 | import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey';
|
52 | import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password';
|
53 | import { 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.
|
57 | Promise.config({warnings: false});
|
58 |
|
59 |
|
60 | const SCROLLBACK_DELAY_MS = 3000;
|
61 | const CRYPTO_ENABLED = isCryptoAvailable();
|
62 | const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
|
63 |
|
64 | function 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 |
|
79 | function 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 | */
|
152 | function 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 | }
|
232 | utils.inherits(MatrixClient, EventEmitter);
|
233 | utils.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 | */
|
240 | MatrixClient.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 | */
|
259 | MatrixClient.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 | */
|
270 | MatrixClient.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 | */
|
281 | MatrixClient.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 | */
|
292 | MatrixClient.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 | */
|
301 | MatrixClient.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 | */
|
311 | MatrixClient.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 | */
|
320 | MatrixClient.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 | */
|
335 | MatrixClient.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 | */
|
346 | MatrixClient.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 | */
|
354 | MatrixClient.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 | */
|
363 | MatrixClient.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 | */
|
376 | MatrixClient.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 | */
|
385 | MatrixClient.prototype.getNotifTimelineSet = function() {
|
386 | return this._notifTimelineSet;
|
387 | };
|
388 |
|
389 | /**
|
390 | * Set the global notification EventTimelineSet
|
391 | *
|
392 | * @param {EventTimelineSet} notifTimelineSet
|
393 | */
|
394 | MatrixClient.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 | */
|
404 | MatrixClient.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 | */
|
438 | MatrixClient.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 | */
|
511 | MatrixClient.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 | */
|
522 | MatrixClient.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 | */
|
533 | MatrixClient.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 | */
|
550 | MatrixClient.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 | */
|
564 | MatrixClient.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 | */
|
579 | MatrixClient.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 | */
|
599 | MatrixClient.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 | */
|
627 | MatrixClient.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 | */
|
647 | MatrixClient.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 |
|
654 | async 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 | */
|
678 | MatrixClient.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 | */
|
694 | MatrixClient.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 | */
|
710 | MatrixClient.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 | */
|
720 | MatrixClient.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 | */
|
734 | MatrixClient.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 | */
|
750 | MatrixClient.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 | */
|
764 | MatrixClient.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 | */
|
774 | MatrixClient.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 | */
|
786 | MatrixClient.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 | */
|
815 | MatrixClient.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 | */
|
830 | MatrixClient.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 | */
|
845 | MatrixClient.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 | */
|
861 | MatrixClient.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 | */
|
869 | MatrixClient.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 | */
|
902 | MatrixClient.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 | */
|
910 | MatrixClient.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 | */
|
923 | MatrixClient.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 | */
|
943 | MatrixClient.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 | */
|
966 | MatrixClient.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 | */
|
1003 | MatrixClient.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 |
|
1026 | MatrixClient.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 |
|
1047 | MatrixClient.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 | */
|
1077 | MatrixClient.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 | */
|
1092 | MatrixClient.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 |
|
1100 | MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
|
1101 | try {
|
1102 | decodeRecoveryKey(recoveryKey);
|
1103 | return true;
|
1104 | } catch (e) {
|
1105 | return false;
|
1106 | }
|
1107 | };
|
1108 |
|
1109 | MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
|
1110 |
|
1111 | MatrixClient.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 |
|
1120 | MatrixClient.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 |
|
1129 | MatrixClient.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 |
|
1201 | MatrixClient.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 | */
|
1224 | MatrixClient.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 | */
|
1232 | MatrixClient.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 | */
|
1241 | MatrixClient.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 | */
|
1258 | MatrixClient.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 | */
|
1266 | MatrixClient.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 | */
|
1277 | MatrixClient.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 | */
|
1307 | MatrixClient.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 | */
|
1315 | MatrixClient.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 | */
|
1330 | MatrixClient.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 | */
|
1345 | MatrixClient.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 | */
|
1353 | MatrixClient.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 | */
|
1366 | MatrixClient.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 | */
|
1377 | MatrixClient.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 | */
|
1399 | MatrixClient.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 | */
|
1467 | MatrixClient.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 | */
|
1478 | MatrixClient.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 | */
|
1501 | MatrixClient.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 | */
|
1513 | MatrixClient.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 | */
|
1524 | MatrixClient.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 | */
|
1542 | MatrixClient.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 | */
|
1560 | MatrixClient.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 | */
|
1579 | MatrixClient.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 | */
|
1601 | MatrixClient.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 | */
|
1629 | MatrixClient.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
|
1676 | function _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 |
|
1757 | function _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 |
|
1780 | function _updatePendingEventStatus(room, event, newStatus) {
|
1781 | if (room) {
|
1782 | room.updatePendingEvent(event, newStatus);
|
1783 | } else {
|
1784 | event.status = newStatus;
|
1785 | }
|
1786 | }
|
1787 |
|
1788 | function _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 | */
|
1830 | MatrixClient.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 | */
|
1847 | MatrixClient.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 | */
|
1860 | MatrixClient.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 | */
|
1873 | MatrixClient.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 | */
|
1887 | MatrixClient.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 | */
|
1912 | MatrixClient.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 | */
|
1937 | MatrixClient.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 | */
|
1950 | MatrixClient.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 | */
|
1963 | MatrixClient.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 | */
|
1976 | MatrixClient.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 | */
|
2004 | MatrixClient.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 | */
|
2020 | MatrixClient.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 | */
|
2051 | MatrixClient.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 | */
|
2079 | MatrixClient.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 | */
|
2106 | MatrixClient.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 | */
|
2119 | MatrixClient.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 | */
|
2134 | MatrixClient.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 | */
|
2161 | MatrixClient.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 | */
|
2174 | MatrixClient.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 | */
|
2187 | MatrixClient.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 | */
|
2211 | MatrixClient.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 | */
|
2236 | MatrixClient.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 | */
|
2253 | function _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 | */
|
2281 | function _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 | */
|
2304 | MatrixClient.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 | */
|
2321 | MatrixClient.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 | */
|
2337 | MatrixClient.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 | */
|
2349 | MatrixClient.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 | */
|
2368 | MatrixClient.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 | */
|
2382 | MatrixClient.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 | */
|
2410 | MatrixClient.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 |
|
2428 | function _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 | */
|
2441 | MatrixClient.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 | */
|
2452 | MatrixClient.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 | **/
|
2464 | MatrixClient.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 | */
|
2486 | MatrixClient.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 | */
|
2565 | MatrixClient.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 | */
|
2649 | MatrixClient.prototype._createMessagesRequest =
|
2650 | function(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 | */
|
2694 | MatrixClient.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 | */
|
2811 | MatrixClient.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 | */
|
2847 | MatrixClient.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 | */
|
2858 | MatrixClient.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 | */
|
2878 | MatrixClient.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 | */
|
2915 | MatrixClient.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 | */
|
2941 | MatrixClient.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 | */
|
2974 | MatrixClient.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 | */
|
3001 | MatrixClient.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 | */
|
3034 | MatrixClient.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 | */
|
3060 | MatrixClient.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 | */
|
3082 | MatrixClient.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 | */
|
3107 | MatrixClient.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 | */
|
3133 | MatrixClient.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 | */
|
3213 | MatrixClient.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 | */
|
3251 | MatrixClient.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 | */
|
3287 | MatrixClient.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 | */
|
3324 | MatrixClient.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 | */
|
3358 | MatrixClient.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 | */
|
3390 | MatrixClient.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 | */
|
3416 | MatrixClient.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 | */
|
3447 | MatrixClient.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 | */
|
3515 | MatrixClient.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 | */
|
3534 | MatrixClient.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 | */
|
3542 | MatrixClient.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 | */
|
3594 | MatrixClient.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 */
|
3642 | MatrixClient.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 | */
|
3659 | MatrixClient.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 | */
|
3681 | MatrixClient.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 | */
|
3703 | MatrixClient.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 | */
|
3716 | MatrixClient.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 | */
|
3724 | MatrixClient.prototype.getCanResetTimelineCallback = function() {
|
3725 | return this._canResetTimelineCallback;
|
3726 | };
|
3727 |
|
3728 | function 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 |
|
3924 | function 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 |
|
3958 | function _reject(callback, defer, err) {
|
3959 | if (callback) {
|
3960 | callback(err);
|
3961 | }
|
3962 | defer.reject(err);
|
3963 | }
|
3964 |
|
3965 | function _resolve(callback, defer, res) {
|
3966 | if (callback) {
|
3967 | callback(null, res);
|
3968 | }
|
3969 | defer.resolve(res);
|
3970 | }
|
3971 |
|
3972 | function _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 | */
|
3989 | MatrixClient.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 | */
|
4001 | MatrixClient.prototype.generateClientSecret = function() {
|
4002 | return randomString(32);
|
4003 | };
|
4004 |
|
4005 | /** */
|
4006 | module.exports.MatrixClient = MatrixClient;
|
4007 | /** */
|
4008 | module.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 | */
|