From 9329a4c32b2b012a778eaacc820a11079fa44270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20L=C3=B8vbr=C3=B8tte=20Olsen?= Date: Tue, 12 Jun 2018 16:07:40 +0200 Subject: [PATCH] Working v1 --- browser-matrix.js | 40448 ++++++++++++++++++++++++++++++++++++++++ browser-matrix.min.js | 15 + index.html | 67 + main.js | 195 + 4 files changed, 40725 insertions(+) create mode 100644 browser-matrix.js create mode 100644 browser-matrix.min.js create mode 100644 index.html create mode 100644 main.js diff --git a/browser-matrix.js b/browser-matrix.js new file mode 100644 index 0000000..8de573f --- /dev/null +++ b/browser-matrix.js @@ -0,0 +1,40448 @@ +(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + (_target = this.target).emit.apply(_target, [eventName].concat(args)); + } + }, { + key: "reEmit", + value: function reEmit(source, eventNames) { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)(eventNames), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var eventName = _step.value; + + if (this.boundHandlers[eventName] === undefined) { + this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName); + } + var boundHandler = this.boundHandlers[eventName]; + + source.on(eventName, boundHandler); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + } + }]); + return Reemitter; +}(); + +exports.default = Reemitter; + +},{"babel-runtime/core-js/get-iterator":53,"babel-runtime/helpers/classCallCheck":66,"babel-runtime/helpers/createClass":67}],3:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** + * This is an internal module. MatrixBaseApis is currently only meant to be used + * by {@link client~MatrixClient}. + * + * @module base-apis + */ + +var _typeof2 = require("babel-runtime/helpers/typeof"); + +var _typeof3 = _interopRequireDefault(_typeof2); + +var _keys = require("babel-runtime/core-js/object/keys"); + +var _keys2 = _interopRequireDefault(_keys); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var httpApi = require("./http-api"); +var utils = require("./utils"); + +/** + * Low-level wrappers for the Matrix APIs + * + * @constructor + * + * @param {Object} opts Configuration options + * + * @param {string} opts.baseUrl Required. The base URL to the client-server + * HTTP API. + * + * @param {string} opts.idBaseUrl Optional. The base identity server URL for + * identity server requests. + * + * @param {Function} opts.request Required. The function to invoke for HTTP + * requests. The value of this property is typically require("request") + * as it returns a function which meets the required interface. See + * {@link requestFunction} for more information. + * + * @param {string} opts.accessToken The access_token for this user. + * + * @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of + * time to wait before timing out HTTP requests. If not specified, there is no + * timeout. + * + * @param {Object} opts.queryParams Optional. Extra query parameters to append + * to all requests with this client. Useful for application services which require + * ?user_id=. + * + * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use + * Authorization header instead of query param to send the access token to the server. + */ +function MatrixBaseApis(opts) { + utils.checkObjectHasKeys(opts, ["baseUrl", "request"]); + + this.baseUrl = opts.baseUrl; + this.idBaseUrl = opts.idBaseUrl; + + var httpOpts = { + baseUrl: opts.baseUrl, + idBaseUrl: opts.idBaseUrl, + accessToken: opts.accessToken, + request: opts.request, + prefix: httpApi.PREFIX_R0, + onlyData: true, + extraParams: opts.queryParams, + localTimeoutMs: opts.localTimeoutMs, + useAuthorizationHeader: opts.useAuthorizationHeader + }; + this._http = new httpApi.MatrixHttpApi(this, httpOpts); + + this._txnCtr = 0; +} + +/** + * Get the Homeserver URL of this client + * @return {string} Homeserver URL of this client + */ +MatrixBaseApis.prototype.getHomeserverUrl = function () { + return this.baseUrl; +}; + +/** + * Get the Identity Server URL of this client + * @param {boolean} stripProto whether or not to strip the protocol from the URL + * @return {string} Identity Server URL of this client + */ +MatrixBaseApis.prototype.getIdentityServerUrl = function () { + var stripProto = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; + + if (stripProto && (this.idBaseUrl.startsWith("http://") || this.idBaseUrl.startsWith("https://"))) { + return this.idBaseUrl.split("://")[1]; + } + return this.idBaseUrl; +}; + +/** + * Get the access token associated with this account. + * @return {?String} The access_token or null + */ +MatrixBaseApis.prototype.getAccessToken = function () { + return this._http.opts.accessToken || null; +}; + +/** + * @return {boolean} true if there is a valid access_token for this client. + */ +MatrixBaseApis.prototype.isLoggedIn = function () { + return this._http.opts.accessToken !== undefined; +}; + +/** + * Make up a new transaction id + * + * @return {string} a new, unique, transaction id + */ +MatrixBaseApis.prototype.makeTxnId = function () { + return "m" + new Date().getTime() + "." + this._txnCtr++; +}; + +// Registration/Login operations +// ============================= + +/** + * Check whether a username is available prior to registration. An error response + * indicates an invalid/unavailable username. + * @param {string} username The username to check the availability of. + * @return {module:client.Promise} Resolves: to `true`. + */ +MatrixBaseApis.prototype.isUsernameAvailable = function (username) { + return this._http.authedRequest(undefined, "GET", '/register/available', { username: username }).then(function (response) { + return response.available; + }); +}; + +/** + * @param {string} username + * @param {string} password + * @param {string} sessionId + * @param {Object} auth + * @param {Object} bindThreepids Set key 'email' to true to bind any email + * threepid uses during registration in the ID server. Set 'msisdn' to + * true to bind msisdn. + * @param {string} guestAccessToken + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.register = function (username, password, sessionId, auth, bindThreepids, guestAccessToken, callback) { + // backwards compat + if (bindThreepids === true) { + bindThreepids = { email: true }; + } else if (bindThreepids === null || bindThreepids === undefined) { + bindThreepids = {}; + } + + if (auth === undefined || auth === null) { + auth = {}; + } + if (sessionId) { + auth.session = sessionId; + } + + var params = { + auth: auth + }; + if (username !== undefined && username !== null) { + params.username = username; + } + if (password !== undefined && password !== null) { + params.password = password; + } + if (bindThreepids.email) { + params.bind_email = true; + } + if (bindThreepids.msisdn) { + params.bind_msisdn = true; + } + if (guestAccessToken !== undefined && guestAccessToken !== null) { + params.guest_access_token = guestAccessToken; + } + // Temporary parameter added to make the register endpoint advertise + // msisdn flows. This exists because there are clients that break + // when given stages they don't recognise. This parameter will cease + // to be necessary once these old clients are gone. + // Only send it if we send any params at all (the password param is + // mandatory, so if we send any params, we'll send the password param) + if (password !== undefined && password !== null) { + params.x_show_msisdn = true; + } + + return this.registerRequest(params, undefined, callback); +}; + +/** + * Register a guest account. + * @param {Object=} opts Registration options + * @param {Object} opts.body JSON HTTP body to provide. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.registerGuest = function (opts, callback) { + opts = opts || {}; + opts.body = opts.body || {}; + return this.registerRequest(opts.body, "guest", callback); +}; + +/** + * @param {Object} data parameters for registration request + * @param {string=} kind type of user to register. may be "guest" + * @param {module:client.callback=} callback + * @return {module:client.Promise} Resolves: to the /register response + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.registerRequest = function (data, kind, callback) { + var params = {}; + if (kind) { + params.kind = kind; + } + + return this._http.request(callback, "POST", "/register", params, data); +}; + +/** + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.loginFlows = function (callback) { + return this._http.request(callback, "GET", "/login"); +}; + +/** + * @param {string} loginType + * @param {Object} data + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.login = function (loginType, data, callback) { + var login_data = { + type: loginType + }; + + // merge data into login_data + utils.extend(login_data, data); + + return this._http.authedRequest(callback, "POST", "/login", undefined, login_data); +}; + +/** + * @param {string} user + * @param {string} password + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.loginWithPassword = function (user, password, callback) { + return this.login("m.login.password", { + user: user, + password: password + }, callback); +}; + +/** + * @param {string} relayState URL Callback after SAML2 Authentication + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.loginWithSAML2 = function (relayState, callback) { + return this.login("m.login.saml2", { + relay_state: relayState + }, callback); +}; + +/** + * @param {string} redirectUrl The URL to redirect to after the HS + * authenticates with CAS. + * @return {string} The HS URL to hit to begin the CAS login process. + */ +MatrixBaseApis.prototype.getCasLoginUrl = function (redirectUrl) { + return this._http.getUrl("/login/cas/redirect", { + "redirectUrl": redirectUrl + }, httpApi.PREFIX_UNSTABLE); +}; + +/** + * @param {string} token Login token previously received from homeserver + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.loginWithToken = function (token, callback) { + return this.login("m.login.token", { + token: token + }, callback); +}; + +/** + * Logs out the current session. + * Obviously, further calls that require authorisation should fail after this + * method is called. The state of the MatrixClient object is not affected: + * it is up to the caller to either reset or destroy the MatrixClient after + * this method succeeds. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: On success, the empty object + */ +MatrixBaseApis.prototype.logout = function (callback) { + return this._http.authedRequest(callback, "POST", '/logout'); +}; + +/** + * Deactivates the logged-in account. + * Obviously, further calls that require authorisation should fail after this + * method is called. The state of the MatrixClient object is not affected: + * it is up to the caller to either reset or destroy the MatrixClient after + * this method succeeds. + * @param {object} auth Optional. Auth data to supply for User-Interactive auth. + * @param {boolean} erase Optional. If set, send as `erase` attribute in the + * JSON request body, indicating whether the account should be erased. Defaults + * to false. + * @return {module:client.Promise} Resolves: On success, the empty object + */ +MatrixBaseApis.prototype.deactivateAccount = function (auth, erase) { + if (typeof erase === 'function') { + throw new Error('deactivateAccount no longer accepts a callback parameter'); + } + + var body = {}; + if (auth) { + body.auth = auth; + } + if (erase !== undefined) { + body.erase = erase; + } + + return this._http.authedRequestWithPrefix(undefined, "POST", '/account/deactivate', undefined, body, httpApi.PREFIX_R0); +}; + +/** + * Get the fallback URL to use for unknown interactive-auth stages. + * + * @param {string} loginType the type of stage being attempted + * @param {string} authSessionId the auth session ID provided by the homeserver + * + * @return {string} HS URL to hit to for the fallback interface + */ +MatrixBaseApis.prototype.getFallbackAuthUrl = function (loginType, authSessionId) { + var path = utils.encodeUri("/auth/$loginType/fallback/web", { + $loginType: loginType + }); + + return this._http.getUrl(path, { + session: authSessionId + }, httpApi.PREFIX_R0); +}; + +// Room operations +// =============== + +/** + * Create a new room. + * @param {Object} options a list of options to pass to the /createRoom API. + * @param {string} options.room_alias_name The alias localpart to assign to + * this room. + * @param {string} options.visibility Either 'public' or 'private'. + * @param {string[]} options.invite A list of user IDs to invite to this room. + * @param {string} options.name The name to give this room. + * @param {string} options.topic The topic to give this room. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: {room_id: {string}, + * room_alias: {string(opt)}} + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.createRoom = function (options, callback) { + // valid options include: room_alias_name, visibility, invite + return this._http.authedRequest(callback, "POST", "/createRoom", undefined, options); +}; + +/** + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.roomState = function (roomId, callback) { + var path = utils.encodeUri("/rooms/$roomId/state", { $roomId: roomId }); + return this._http.authedRequest(callback, "GET", path); +}; + +/** + * @param {string} groupId + * @return {module:client.Promise} Resolves: Group summary object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getGroupSummary = function (groupId) { + var path = utils.encodeUri("/groups/$groupId/summary", { $groupId: groupId }); + return this._http.authedRequest(undefined, "GET", path); +}; + +/** + * @param {string} groupId + * @return {module:client.Promise} Resolves: Group profile object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getGroupProfile = function (groupId) { + var path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId }); + return this._http.authedRequest(undefined, "GET", path); +}; + +/** + * @param {string} groupId + * @param {Object} profile The group profile object + * @param {string=} profile.name Name of the group + * @param {string=} profile.avatar_url MXC avatar URL + * @param {string=} profile.short_description A short description of the room + * @param {string=} profile.long_description A longer HTML description of the room + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.setGroupProfile = function (groupId, profile) { + var path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId }); + return this._http.authedRequest(undefined, "POST", path, undefined, profile); +}; + +/** + * @param {string} groupId + * @param {object} policy The join policy for the group. Must include at + * least a 'type' field which is 'open' if anyone can join the group + * the group without prior approval, or 'invite' if an invite is + * required to join. + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.setGroupJoinPolicy = function (groupId, policy) { + var path = utils.encodeUri("/groups/$groupId/settings/m.join_policy", { $groupId: groupId }); + return this._http.authedRequest(undefined, "PUT", path, undefined, { + 'm.join_policy': policy + }); +}; + +/** + * @param {string} groupId + * @return {module:client.Promise} Resolves: Group users list object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getGroupUsers = function (groupId) { + var path = utils.encodeUri("/groups/$groupId/users", { $groupId: groupId }); + return this._http.authedRequest(undefined, "GET", path); +}; + +/** + * @param {string} groupId + * @return {module:client.Promise} Resolves: Group users list object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getGroupInvitedUsers = function (groupId) { + var path = utils.encodeUri("/groups/$groupId/invited_users", { $groupId: groupId }); + return this._http.authedRequest(undefined, "GET", path); +}; + +/** + * @param {string} groupId + * @return {module:client.Promise} Resolves: Group rooms list object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getGroupRooms = function (groupId) { + var path = utils.encodeUri("/groups/$groupId/rooms", { $groupId: groupId }); + return this._http.authedRequest(undefined, "GET", path); +}; + +/** + * @param {string} groupId + * @param {string} userId + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.inviteUserToGroup = function (groupId, userId) { + var path = utils.encodeUri("/groups/$groupId/admin/users/invite/$userId", { $groupId: groupId, $userId: userId }); + return this._http.authedRequest(undefined, "PUT", path, undefined, {}); +}; + +/** + * @param {string} groupId + * @param {string} userId + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.removeUserFromGroup = function (groupId, userId) { + var path = utils.encodeUri("/groups/$groupId/admin/users/remove/$userId", { $groupId: groupId, $userId: userId }); + return this._http.authedRequest(undefined, "PUT", path, undefined, {}); +}; + +/** + * @param {string} groupId + * @param {string} userId + * @param {string} roleId Optional. + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.addUserToGroupSummary = function (groupId, userId, roleId) { + var path = utils.encodeUri(roleId ? "/groups/$groupId/summary/$roleId/users/$userId" : "/groups/$groupId/summary/users/$userId", { $groupId: groupId, $roleId: roleId, $userId: userId }); + return this._http.authedRequest(undefined, "PUT", path, undefined, {}); +}; + +/** + * @param {string} groupId + * @param {string} userId + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.removeUserFromGroupSummary = function (groupId, userId) { + var path = utils.encodeUri("/groups/$groupId/summary/users/$userId", { $groupId: groupId, $userId: userId }); + return this._http.authedRequest(undefined, "DELETE", path, undefined, {}); +}; + +/** + * @param {string} groupId + * @param {string} roomId + * @param {string} categoryId Optional. + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.addRoomToGroupSummary = function (groupId, roomId, categoryId) { + var path = utils.encodeUri(categoryId ? "/groups/$groupId/summary/$categoryId/rooms/$roomId" : "/groups/$groupId/summary/rooms/$roomId", { $groupId: groupId, $categoryId: categoryId, $roomId: roomId }); + return this._http.authedRequest(undefined, "PUT", path, undefined, {}); +}; + +/** + * @param {string} groupId + * @param {string} roomId + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.removeRoomFromGroupSummary = function (groupId, roomId) { + var path = utils.encodeUri("/groups/$groupId/summary/rooms/$roomId", { $groupId: groupId, $roomId: roomId }); + return this._http.authedRequest(undefined, "DELETE", path, undefined, {}); +}; + +/** + * @param {string} groupId + * @param {string} roomId + * @param {bool} isPublic Whether the room-group association is visible to non-members + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.addRoomToGroup = function (groupId, roomId, isPublic) { + if (isPublic === undefined) { + isPublic = true; + } + var path = utils.encodeUri("/groups/$groupId/admin/rooms/$roomId", { $groupId: groupId, $roomId: roomId }); + return this._http.authedRequest(undefined, "PUT", path, undefined, { "m.visibility": { type: isPublic ? "public" : "private" } }); +}; + +/** + * Configure the visibility of a room-group association. + * @param {string} groupId + * @param {string} roomId + * @param {bool} isPublic Whether the room-group association is visible to non-members + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.updateGroupRoomVisibility = function (groupId, roomId, isPublic) { + // NB: The /config API is generic but there's not much point in exposing this yet as synapse + // is the only server to implement this. In future we should consider an API that allows + // arbitrary configuration, i.e. "config/$configKey". + + var path = utils.encodeUri("/groups/$groupId/admin/rooms/$roomId/config/m.visibility", { $groupId: groupId, $roomId: roomId }); + return this._http.authedRequest(undefined, "PUT", path, undefined, { type: isPublic ? "public" : "private" }); +}; + +/** + * @param {string} groupId + * @param {string} roomId + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.removeRoomFromGroup = function (groupId, roomId) { + var path = utils.encodeUri("/groups/$groupId/admin/rooms/$roomId", { $groupId: groupId, $roomId: roomId }); + return this._http.authedRequest(undefined, "DELETE", path, undefined, {}); +}; + +/** + * @param {string} groupId + * @param {Object} opts Additional options to send alongside the acceptance. + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.acceptGroupInvite = function (groupId) { + var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + + var path = utils.encodeUri("/groups/$groupId/self/accept_invite", { $groupId: groupId }); + return this._http.authedRequest(undefined, "PUT", path, undefined, opts || {}); +}; + +/** + * @param {string} groupId + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.joinGroup = function (groupId) { + var path = utils.encodeUri("/groups/$groupId/self/join", { $groupId: groupId }); + return this._http.authedRequest(undefined, "PUT", path, undefined, {}); +}; + +/** + * @param {string} groupId + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.leaveGroup = function (groupId) { + var path = utils.encodeUri("/groups/$groupId/self/leave", { $groupId: groupId }); + return this._http.authedRequest(undefined, "PUT", path, undefined, {}); +}; + +/** + * @return {module:client.Promise} Resolves: The groups to which the user is joined + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getJoinedGroups = function () { + var path = utils.encodeUri("/joined_groups"); + return this._http.authedRequest(undefined, "GET", path); +}; + +/** + * @param {Object} content Request content + * @param {string} content.localpart The local part of the desired group ID + * @param {Object} content.profile Group profile object + * @return {module:client.Promise} Resolves: Object with key group_id: id of the created group + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.createGroup = function (content) { + var path = utils.encodeUri("/create_group"); + return this._http.authedRequest(undefined, "POST", path, undefined, content); +}; + +/** + * @param {string[]} userIds List of user IDs + * @return {module:client.Promise} Resolves: Object as exmaple below + * + * { + * "users": { + * "@bob:example.com": { + * "+example:example.com" + * } + * } + * } + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getPublicisedGroups = function (userIds) { + var path = utils.encodeUri("/publicised_groups"); + return this._http.authedRequest(undefined, "POST", path, undefined, { user_ids: userIds }); +}; + +/** + * @param {string} groupId + * @param {bool} isPublic Whether the user's membership of this group is made public + * @return {module:client.Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.setGroupPublicity = function (groupId, isPublic) { + var path = utils.encodeUri("/groups/$groupId/self/update_publicity", { $groupId: groupId }); + return this._http.authedRequest(undefined, "PUT", path, undefined, { + publicise: isPublic + }); +}; + +/** + * Retrieve a state event. + * @param {string} roomId + * @param {string} eventType + * @param {string} stateKey + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getStateEvent = function (roomId, eventType, stateKey, callback) { + var pathParams = { + $roomId: roomId, + $eventType: eventType, + $stateKey: stateKey + }; + var path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams); + if (stateKey !== undefined) { + path = utils.encodeUri(path + "/$stateKey", pathParams); + } + return this._http.authedRequest(callback, "GET", path); +}; + +/** + * @param {string} roomId + * @param {string} eventType + * @param {Object} content + * @param {string} stateKey + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.sendStateEvent = function (roomId, eventType, content, stateKey, callback) { + var pathParams = { + $roomId: roomId, + $eventType: eventType, + $stateKey: stateKey + }; + var path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams); + if (stateKey !== undefined) { + path = utils.encodeUri(path + "/$stateKey", pathParams); + } + return this._http.authedRequest(callback, "PUT", path, undefined, content); +}; + +/** + * @param {string} roomId + * @param {string} eventId + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.redactEvent = function (roomId, eventId, callback) { + var path = utils.encodeUri("/rooms/$roomId/redact/$eventId", { + $roomId: roomId, + $eventId: eventId + }); + return this._http.authedRequest(callback, "POST", path, undefined, {}); +}; + +/** + * @param {string} roomId + * @param {Number} limit + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.roomInitialSync = function (roomId, limit, callback) { + if (utils.isFunction(limit)) { + callback = limit;limit = undefined; + } + var path = utils.encodeUri("/rooms/$roomId/initialSync", { $roomId: roomId }); + if (!limit) { + limit = 30; + } + return this._http.authedRequest(callback, "GET", path, { limit: limit }); +}; + +/** + * Set a marker to indicate the point in a room before which the user has read every + * event. This can be retrieved from room account data (the event type is `m.fully_read`) + * and displayed as a horizontal line in the timeline that is visually distinct to the + * position of the user's own read receipt. + * @param {string} roomId ID of the room that has been read + * @param {string} rmEventId ID of the event that has been read + * @param {string} rrEventId ID of the event tracked by the read receipt. This is here + * for convenience because the RR and the RM are commonly updated at the same time as + * each other. Optional. + * @return {module:client.Promise} Resolves: the empty object, {}. + */ +MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest = function (roomId, rmEventId, rrEventId) { + var path = utils.encodeUri("/rooms/$roomId/read_markers", { + $roomId: roomId + }); + + var content = { + "m.fully_read": rmEventId, + "m.read": rrEventId + }; + + return this._http.authedRequest(undefined, "POST", path, undefined, content); +}; + +// Room Directory operations +// ========================= + +/** + * @param {Object} options Options for this request + * @param {string} options.server The remote server to query for the room list. + * Optional. If unspecified, get the local home + * server's public room list. + * @param {number} options.limit Maximum number of entries to return + * @param {string} options.since Token to paginate from + * @param {object} options.filter Filter parameters + * @param {string} options.filter.generic_search_term String to search for + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.publicRooms = function (options, callback) { + if (typeof options == 'function') { + callback = options; + options = {}; + } + if (options === undefined) { + options = {}; + } + + var query_params = {}; + if (options.server) { + query_params.server = options.server; + delete options.server; + } + + if ((0, _keys2.default)(options).length === 0 && (0, _keys2.default)(query_params).length === 0) { + return this._http.authedRequest(callback, "GET", "/publicRooms"); + } else { + return this._http.authedRequest(callback, "POST", "/publicRooms", query_params, options); + } +}; + +/** + * Create an alias to room ID mapping. + * @param {string} alias The room alias to create. + * @param {string} roomId The room ID to link the alias to. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.createAlias = function (alias, roomId, callback) { + var path = utils.encodeUri("/directory/room/$alias", { + $alias: alias + }); + var data = { + room_id: roomId + }; + return this._http.authedRequest(callback, "PUT", path, undefined, data); +}; + +/** + * Delete an alias to room ID mapping. This alias must be on your local server + * and you must have sufficient access to do this operation. + * @param {string} alias The room alias to delete. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.deleteAlias = function (alias, callback) { + var path = utils.encodeUri("/directory/room/$alias", { + $alias: alias + }); + return this._http.authedRequest(callback, "DELETE", path, undefined, undefined); +}; + +/** + * Get room info for the given alias. + * @param {string} alias The room alias to resolve. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: Object with room_id and servers. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getRoomIdForAlias = function (alias, callback) { + // TODO: deprecate this or resolveRoomAlias + var path = utils.encodeUri("/directory/room/$alias", { + $alias: alias + }); + return this._http.authedRequest(callback, "GET", path); +}; + +/** + * @param {string} roomAlias + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.resolveRoomAlias = function (roomAlias, callback) { + // TODO: deprecate this or getRoomIdForAlias + var path = utils.encodeUri("/directory/room/$alias", { $alias: roomAlias }); + return this._http.request(callback, "GET", path); +}; + +/** + * Get the visibility of a room in the current HS's room directory + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getRoomDirectoryVisibility = function (roomId, callback) { + var path = utils.encodeUri("/directory/list/room/$roomId", { + $roomId: roomId + }); + return this._http.authedRequest(callback, "GET", path); +}; + +/** + * Set the visbility of a room in the current HS's room directory + * @param {string} roomId + * @param {string} visibility "public" to make the room visible + * in the public directory, or "private" to make + * it invisible. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.setRoomDirectoryVisibility = function (roomId, visibility, callback) { + var path = utils.encodeUri("/directory/list/room/$roomId", { + $roomId: roomId + }); + return this._http.authedRequest(callback, "PUT", path, undefined, { "visibility": visibility }); +}; + +/** + * Set the visbility of a room bridged to a 3rd party network in + * the current HS's room directory. + * @param {string} networkId the network ID of the 3rd party + * instance under which this room is published under. + * @param {string} roomId + * @param {string} visibility "public" to make the room visible + * in the public directory, or "private" to make + * it invisible. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService = function (networkId, roomId, visibility, callback) { + var path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", { + $networkId: networkId, + $roomId: roomId + }); + return this._http.authedRequest(callback, "PUT", path, undefined, { "visibility": visibility }); +}; + +// User Directory Operations +// ========================= + +/** + * Query the user directory with a term matching user IDs, display names and domains. + * @param {object} opts options + * @param {string} opts.term the term with which to search. + * @param {number} opts.limit the maximum number of results to return. The server will + * apply a limit if unspecified. + * @return {module:client.Promise} Resolves: an array of results. + */ +MatrixBaseApis.prototype.searchUserDirectory = function (opts) { + var body = { + search_term: opts.term + }; + + if (opts.limit !== undefined) { + body.limit = opts.limit; + } + + return this._http.authedRequest(undefined, "POST", "/user_directory/search", undefined, body); +}; + +// Media operations +// ================ + +/** + * Upload a file to the media repository on the home server. + * + * @param {object} file The object to upload. On a browser, something that + * can be sent to XMLHttpRequest.send (typically a File). Under node.js, + * a a Buffer, String or ReadStream. + * + * @param {object} opts options object + * + * @param {string=} opts.name Name to give the file on the server. Defaults + * to file.name. + * + * @param {string=} opts.type Content-type for the upload. Defaults to + * file.type, or applicaton/octet-stream. + * + * @param {boolean=} opts.rawResponse Return the raw body, rather than + * parsing the JSON. Defaults to false (except on node.js, where it + * defaults to true for backwards compatibility). + * + * @param {boolean=} opts.onlyContentUri Just return the content URI, + * rather than the whole body. Defaults to false (except on browsers, + * where it defaults to true for backwards compatibility). Ignored if + * opts.rawResponse is true. + * + * @param {Function=} opts.callback Deprecated. Optional. The callback to + * invoke on success/failure. See the promise return values for more + * information. + * + * @param {Function=} opts.progressHandler Optional. Called when a chunk of + * data has been uploaded, with an object containing the fields `loaded` + * (number of bytes transferred) and `total` (total size, if known). + * + * @return {module:client.Promise} Resolves to response object, as + * determined by this.opts.onlyData, opts.rawResponse, and + * opts.onlyContentUri. Rejects with an error (usually a MatrixError). + */ +MatrixBaseApis.prototype.uploadContent = function (file, opts) { + return this._http.uploadContent(file, opts); +}; + +/** + * Cancel a file upload in progress + * @param {module:client.Promise} promise The promise returned from uploadContent + * @return {boolean} true if canceled, otherwise false + */ +MatrixBaseApis.prototype.cancelUpload = function (promise) { + return this._http.cancelUpload(promise); +}; + +/** + * Get a list of all file uploads in progress + * @return {array} Array of objects representing current uploads. + * Currently in progress is element 0. Keys: + * - promise: The promise associated with the upload + * - loaded: Number of bytes uploaded + * - total: Total number of bytes to upload + */ +MatrixBaseApis.prototype.getCurrentUploads = function () { + return this._http.getCurrentUploads(); +}; + +// Profile operations +// ================== + +/** + * @param {string} userId + * @param {string} info The kind of info to retrieve (e.g. 'displayname', + * 'avatar_url'). + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getProfileInfo = function (userId, info, callback) { + if (utils.isFunction(info)) { + callback = info;info = undefined; + } + + var path = info ? utils.encodeUri("/profile/$userId/$info", { $userId: userId, $info: info }) : utils.encodeUri("/profile/$userId", { $userId: userId }); + return this._http.authedRequest(callback, "GET", path); +}; + +// Account operations +// ================== + +/** + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getThreePids = function (callback) { + var path = "/account/3pid"; + return this._http.authedRequest(callback, "GET", path, undefined, undefined); +}; + +/** + * @param {Object} creds + * @param {boolean} bind + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.addThreePid = function (creds, bind, callback) { + var path = "/account/3pid"; + var data = { + 'threePidCreds': creds, + 'bind': bind + }; + return this._http.authedRequest(callback, "POST", path, null, data); +}; + +/** + * @param {string} medium The threepid medium (eg. 'email') + * @param {string} address The threepid address (eg. 'bob@example.com') + * this must be as returned by getThreePids. + * @return {module:client.Promise} Resolves: The server response on success + * (generally the empty JSON object) + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.deleteThreePid = function (medium, address) { + var path = "/account/3pid/delete"; + var data = { + 'medium': medium, + 'address': address + }; + return this._http.authedRequestWithPrefix(undefined, "POST", path, null, data, httpApi.PREFIX_UNSTABLE); +}; + +/** + * Make a request to change your password. + * @param {Object} authDict + * @param {string} newPassword The new desired password. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.setPassword = function (authDict, newPassword, callback) { + var path = "/account/password"; + var data = { + 'auth': authDict, + 'new_password': newPassword + }; + + return this._http.authedRequest(callback, "POST", path, null, data); +}; + +// Device operations +// ================= + +/** + * Gets all devices recorded for the logged-in user + * @return {module:client.Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getDevices = function () { + var path = "/devices"; + return this._http.authedRequestWithPrefix(undefined, "GET", path, undefined, undefined, httpApi.PREFIX_UNSTABLE); +}; + +/** + * Update the given device + * + * @param {string} device_id device to update + * @param {Object} body body of request + * @return {module:client.Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.setDeviceDetails = function (device_id, body) { + var path = utils.encodeUri("/devices/$device_id", { + $device_id: device_id + }); + + return this._http.authedRequestWithPrefix(undefined, "PUT", path, undefined, body, httpApi.PREFIX_UNSTABLE); +}; + +/** + * Delete the given device + * + * @param {string} device_id device to delete + * @param {object} auth Optional. Auth data to supply for User-Interactive auth. + * @return {module:client.Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.deleteDevice = function (device_id, auth) { + var path = utils.encodeUri("/devices/$device_id", { + $device_id: device_id + }); + + var body = {}; + + if (auth) { + body.auth = auth; + } + + return this._http.authedRequestWithPrefix(undefined, "DELETE", path, undefined, body, httpApi.PREFIX_UNSTABLE); +}; + +/** + * Delete multiple device + * + * @param {string[]} devices IDs of the devices to delete + * @param {object} auth Optional. Auth data to supply for User-Interactive auth. + * @return {module:client.Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.deleteMultipleDevices = function (devices, auth) { + var body = { devices: devices }; + + if (auth) { + body.auth = auth; + } + + return this._http.authedRequestWithPrefix(undefined, "POST", "/delete_devices", undefined, body, httpApi.PREFIX_UNSTABLE); +}; + +// Push operations +// =============== + +/** + * Gets all pushers registered for the logged-in user + * + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: Array of objects representing pushers + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getPushers = function (callback) { + var path = "/pushers"; + return this._http.authedRequest(callback, "GET", path, undefined, undefined); +}; + +/** + * Adds a new pusher or updates an existing pusher + * + * @param {Object} pusher Object representing a pusher + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: Empty json object on success + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.setPusher = function (pusher, callback) { + var path = "/pushers/set"; + return this._http.authedRequest(callback, "POST", path, null, pusher); +}; + +/** + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getPushRules = function (callback) { + return this._http.authedRequest(callback, "GET", "/pushrules/"); +}; + +/** + * @param {string} scope + * @param {string} kind + * @param {string} ruleId + * @param {Object} body + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.addPushRule = function (scope, kind, ruleId, body, callback) { + // NB. Scope not uri encoded because devices need the '/' + var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { + $kind: kind, + $ruleId: ruleId + }); + return this._http.authedRequest(callback, "PUT", path, undefined, body); +}; + +/** + * @param {string} scope + * @param {string} kind + * @param {string} ruleId + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.deletePushRule = function (scope, kind, ruleId, callback) { + // NB. Scope not uri encoded because devices need the '/' + var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { + $kind: kind, + $ruleId: ruleId + }); + return this._http.authedRequest(callback, "DELETE", path); +}; + +/** + * Enable or disable a push notification rule. + * @param {string} scope + * @param {string} kind + * @param {string} ruleId + * @param {boolean} enabled + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.setPushRuleEnabled = function (scope, kind, ruleId, enabled, callback) { + var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", { + $kind: kind, + $ruleId: ruleId + }); + return this._http.authedRequest(callback, "PUT", path, undefined, { "enabled": enabled }); +}; + +/** + * Set the actions for a push notification rule. + * @param {string} scope + * @param {string} kind + * @param {string} ruleId + * @param {array} actions + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.setPushRuleActions = function (scope, kind, ruleId, actions, callback) { + var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", { + $kind: kind, + $ruleId: ruleId + }); + return this._http.authedRequest(callback, "PUT", path, undefined, { "actions": actions }); +}; + +// Search +// ====== + +/** + * Perform a server-side search. + * @param {Object} opts + * @param {string} opts.next_batch the batch token to pass in the query string + * @param {Object} opts.body the JSON object to pass to the request body. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.search = function (opts, callback) { + var queryparams = {}; + if (opts.next_batch) { + queryparams.next_batch = opts.next_batch; + } + return this._http.authedRequest(callback, "POST", "/search", queryparams, opts.body); +}; + +// Crypto +// ====== + +/** + * Upload keys + * + * @param {Object} content body of upload request + * + * @param {Object=} opts + * + * @param {string=} opts.device_id explicit device_id to use for upload + * (default is to use the same as that used during auth). + * + * @param {module:client.callback=} callback + * + * @return {module:client.Promise} Resolves: result object. Rejects: with + * an error response ({@link module:http-api.MatrixError}). + */ +MatrixBaseApis.prototype.uploadKeysRequest = function (content, opts, callback) { + opts = opts || {}; + var deviceId = opts.device_id; + var path = void 0; + if (deviceId) { + path = utils.encodeUri("/keys/upload/$deviceId", { + $deviceId: deviceId + }); + } else { + path = "/keys/upload"; + } + return this._http.authedRequestWithPrefix(callback, "POST", path, undefined, content, httpApi.PREFIX_UNSTABLE); +}; + +/** + * Download device keys + * + * @param {string[]} userIds list of users to get keys for + * + * @param {Object=} opts + * + * @param {string=} opts.token sync token to pass in the query request, to help + * the HS give the most recent results + * + * @return {module:client.Promise} Resolves: result object. Rejects: with + * an error response ({@link module:http-api.MatrixError}). + */ +MatrixBaseApis.prototype.downloadKeysForUsers = function (userIds, opts) { + if (utils.isFunction(opts)) { + // opts used to be 'callback'. + throw new Error('downloadKeysForUsers no longer accepts a callback parameter'); + } + opts = opts || {}; + + var content = { + device_keys: {} + }; + if ('token' in opts) { + content.token = opts.token; + } + userIds.forEach(function (u) { + content.device_keys[u] = {}; + }); + + return this._http.authedRequestWithPrefix(undefined, "POST", "/keys/query", undefined, content, httpApi.PREFIX_UNSTABLE); +}; + +/** + * Claim one-time keys + * + * @param {string[]} devices a list of [userId, deviceId] pairs + * + * @param {string} [key_algorithm = signed_curve25519] desired key type + * + * @return {module:client.Promise} Resolves: result object. Rejects: with + * an error response ({@link module:http-api.MatrixError}). + */ +MatrixBaseApis.prototype.claimOneTimeKeys = function (devices, key_algorithm) { + var queries = {}; + + if (key_algorithm === undefined) { + key_algorithm = "signed_curve25519"; + } + + for (var i = 0; i < devices.length; ++i) { + var userId = devices[i][0]; + var deviceId = devices[i][1]; + var query = queries[userId] || {}; + queries[userId] = query; + query[deviceId] = key_algorithm; + } + var content = { one_time_keys: queries }; + return this._http.authedRequestWithPrefix(undefined, "POST", "/keys/claim", undefined, content, httpApi.PREFIX_UNSTABLE); +}; + +/** + * Ask the server for a list of users who have changed their device lists + * between a pair of sync tokens + * + * @param {string} oldToken + * @param {string} newToken + * + * @return {module:client.Promise} Resolves: result object. Rejects: with + * an error response ({@link module:http-api.MatrixError}). + */ +MatrixBaseApis.prototype.getKeyChanges = function (oldToken, newToken) { + var qps = { + from: oldToken, + to: newToken + }; + + return this._http.authedRequestWithPrefix(undefined, "GET", "/keys/changes", qps, undefined, httpApi.PREFIX_UNSTABLE); +}; + +// Identity Server Operations +// ========================== + +/** + * Requests an email verification token directly from an Identity Server. + * + * Note that the Home Server offers APIs to proxy this API for specific + * situations, allowing for better feedback to the user. + * + * @param {string} email The email address to request a token for + * @param {string} clientSecret A secret binary string generated by the client. + * It is recommended this be around 16 ASCII characters. + * @param {number} sendAttempt If an identity server sees a duplicate request + * with the same sendAttempt, it will not send another email. + * To request another email to be sent, use a larger value for + * the sendAttempt param as was used in the previous request. + * @param {string} nextLink Optional If specified, the client will be redirected + * to this link after validation. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + * @throws Error if No ID server is set + */ +MatrixBaseApis.prototype.requestEmailToken = function (email, clientSecret, sendAttempt, nextLink, callback) { + var params = { + client_secret: clientSecret, + email: email, + send_attempt: sendAttempt, + next_link: nextLink + }; + return this._http.idServerRequest(callback, "POST", "/validate/email/requestToken", params, httpApi.PREFIX_IDENTITY_V1); +}; + +/** + * Submits an MSISDN token to the identity server + * + * This is used when submitting the code sent by SMS to a phone number. + * The ID server has an equivalent API for email but the js-sdk does + * not expose this, since email is normally validated by the user clicking + * a link rather than entering a code. + * + * @param {string} sid The sid given in the response to requestToken + * @param {string} clientSecret A secret binary string generated by the client. + * This must be the same value submitted in the requestToken call. + * @param {string} token The token, as enetered by the user. + * + * @return {module:client.Promise} Resolves: Object, currently with no parameters. + * @return {module:http-api.MatrixError} Rejects: with an error response. + * @throws Error if No ID server is set + */ +MatrixBaseApis.prototype.submitMsisdnToken = function (sid, clientSecret, token) { + var params = { + sid: sid, + client_secret: clientSecret, + token: token + }; + return this._http.idServerRequest(undefined, "POST", "/validate/msisdn/submitToken", params, httpApi.PREFIX_IDENTITY_V1); +}; + +/** + * Looks up the public Matrix ID mapping for a given 3rd party + * identifier from the Identity Server + * @param {string} medium The medium of the threepid, eg. 'email' + * @param {string} address The textual address of the threepid + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: A threepid mapping + * object or the empty object if no mapping + * exists + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.lookupThreePid = function (medium, address, callback) { + var params = { + medium: medium, + address: address + }; + return this._http.idServerRequest(callback, "GET", "/lookup", params, httpApi.PREFIX_IDENTITY_V1); +}; + +// Direct-to-device messaging +// ========================== + +/** + * Send an event to a specific list of devices + * + * @param {string} eventType type of event to send + * @param {Object.>} contentMap + * content to send. Map from user_id to device_id to content object. + * @param {string=} txnId transaction id. One will be made up if not + * supplied. + * @return {module:client.Promise} Resolves to the result object + */ +MatrixBaseApis.prototype.sendToDevice = function (eventType, contentMap, txnId) { + var path = utils.encodeUri("/sendToDevice/$eventType/$txnId", { + $eventType: eventType, + $txnId: txnId ? txnId : this.makeTxnId() + }); + + var body = { + messages: contentMap + }; + + return this._http.authedRequestWithPrefix(undefined, "PUT", path, undefined, body, httpApi.PREFIX_UNSTABLE); +}; + +// Third party Lookup API +// ====================== + +/** + * Get the third party protocols that can be reached using + * this HS + * @return {module:client.Promise} Resolves to the result object + */ +MatrixBaseApis.prototype.getThirdpartyProtocols = function () { + return this._http.authedRequestWithPrefix(undefined, "GET", "/thirdparty/protocols", undefined, undefined, httpApi.PREFIX_UNSTABLE).then(function (response) { + // sanity check + if (!response || (typeof response === "undefined" ? "undefined" : (0, _typeof3.default)(response)) !== 'object') { + throw new Error("/thirdparty/protocols did not return an object: " + response); + } + return response; + }); +}; + +/** + * Get information on how a specific place on a third party protocol + * may be reached. + * @param {string} protocol The protocol given in getThirdpartyProtocols() + * @param {object} params Protocol-specific parameters, as given in th + * response to getThirdpartyProtocols() + * @return {module:client.Promise} Resolves to the result object + */ +MatrixBaseApis.prototype.getThirdpartyLocation = function (protocol, params) { + var path = utils.encodeUri("/thirdparty/location/$protocol", { + $protocol: protocol + }); + + return this._http.authedRequestWithPrefix(undefined, "GET", path, params, undefined, httpApi.PREFIX_UNSTABLE); +}; + +/** + * MatrixBaseApis object + */ +module.exports = MatrixBaseApis; + +},{"./http-api":24,"./utils":50,"babel-runtime/core-js/object/keys":61,"babel-runtime/helpers/typeof":72}],4:[function(require,module,exports){ +(function (global){ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +var _assign = require("babel-runtime/core-js/object/assign"); + +var _assign2 = _interopRequireDefault(_assign); + +var _stringify = require("babel-runtime/core-js/json/stringify"); + +var _stringify2 = _interopRequireDefault(_stringify); + +var _keys = require("babel-runtime/core-js/object/keys"); + +var _keys2 = _interopRequireDefault(_keys); + +var _bluebird = require("bluebird"); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +var _regenerator = require("babel-runtime/regenerator"); + +var _regenerator2 = _interopRequireDefault(_regenerator); + +var _setDeviceVerification = function () { + var _ref4 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee2(client, userId, deviceId, verified, blocked, known) { + var dev; + return _regenerator2.default.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + if (client._crypto) { + _context2.next = 2; + break; + } + + throw new Error("End-to-End encryption disabled"); + + case 2: + _context2.next = 4; + return (0, _bluebird.resolve)(client._crypto.setDeviceVerification(userId, deviceId, verified, blocked, known)); + + case 4: + dev = _context2.sent; + + client.emit("deviceVerificationChanged", userId, deviceId, dev); + + case 6: + case "end": + return _context2.stop(); + } + } + }, _callee2, this); + })); + + return function _setDeviceVerification(_x4, _x5, _x6, _x7, _x8, _x9) { + return _ref4.apply(this, arguments); + }; +}(); + +/** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. This provides the default for rooms which + * do not specify a value. + * + * @param {boolean} value whether to blacklist all unverified devices by default + */ + + +var _ReEmitter = require("./ReEmitter"); + +var _ReEmitter2 = _interopRequireDefault(_ReEmitter); + +var _RoomList = require("./crypto/RoomList"); + +var _RoomList2 = _interopRequireDefault(_RoomList); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var PushProcessor = require('./pushprocessor'); + +/** + * This is an internal module. See {@link MatrixClient} for the public class. + * @module client + */ +var EventEmitter = require("events").EventEmitter; + +var url = require('url'); + +var httpApi = require("./http-api"); +var MatrixEvent = require("./models/event").MatrixEvent; +var EventStatus = require("./models/event").EventStatus; +var EventTimeline = require("./models/event-timeline"); +var SearchResult = require("./models/search-result"); +var StubStore = require("./store/stub"); +var webRtcCall = require("./webrtc/call"); +var utils = require("./utils"); +var contentRepo = require("./content-repo"); +var Filter = require("./filter"); +var SyncApi = require("./sync"); +var MatrixBaseApis = require("./base-apis"); +var MatrixError = httpApi.MatrixError; +var ContentHelpers = require("./content-helpers"); + +var SCROLLBACK_DELAY_MS = 3000; +var CRYPTO_ENABLED = false; + +try { + var Crypto = require("./crypto"); + CRYPTO_ENABLED = true; +} catch (e) { + console.warn("Unable to load crypto module: crypto will be disabled: " + e); +} + +/** + * Construct a Matrix Client. Only directly construct this if you want to use + * custom modules. Normally, {@link createClient} should be used + * as it specifies 'sensible' defaults for these modules. + * @constructor + * @extends {external:EventEmitter} + * @extends {module:base-apis~MatrixBaseApis} + * + * @param {Object} opts The configuration options for this client. + * @param {string} opts.baseUrl Required. The base URL to the client-server + * HTTP API. + * @param {string} opts.idBaseUrl Optional. The base identity server URL for + * identity server requests. + * @param {Function} opts.request Required. The function to invoke for HTTP + * requests. The value of this property is typically require("request") + * as it returns a function which meets the required interface. See + * {@link requestFunction} for more information. + * + * @param {string} opts.accessToken The access_token for this user. + * + * @param {string} opts.userId The user ID for this user. + * + * @param {Object=} opts.store The data store to use. If not specified, + * this client will not store any HTTP responses. + * + * @param {string=} opts.deviceId A unique identifier for this device; used for + * tracking things like crypto keys and access tokens. If not specified, + * end-to-end crypto will be disabled. + * + * @param {Object=} opts.sessionStore A store to be used for end-to-end crypto + * session data. This should be a {@link + * module:store/session/webstorage~WebStorageSessionStore|WebStorageSessionStore}, + * or an object implementing the same interface. If not specified, + * end-to-end crypto will be disabled. + * + * @param {Object} opts.scheduler Optional. The scheduler to use. If not + * specified, this client will not retry requests on failure. This client + * will supply its own processing function to + * {@link module:scheduler~MatrixScheduler#setProcessFunction}. + * + * @param {Object} opts.queryParams Optional. Extra query parameters to append + * to all requests with this client. Useful for application services which require + * ?user_id=. + * + * @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of + * time to wait before timing out HTTP requests. If not specified, there is no timeout. + * + * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use + * Authorization header instead of query param to send the access token to the server. + * + * @param {boolean} [opts.timelineSupport = false] Set to true to enable + * improved timeline support ({@link + * module:client~MatrixClient#getEventTimeline getEventTimeline}). It is + * disabled by default for compatibility with older clients - in particular to + * maintain support for back-paginating the live timeline after a '/sync' + * result with a gap. + * + * @param {module:crypto.store.base~CryptoStore} opts.cryptoStore + * crypto store implementation. + */ +function MatrixClient(opts) { + // Allow trailing slash in HS url + if (opts.baseUrl && opts.baseUrl.endsWith("/")) { + opts.baseUrl = opts.baseUrl.substr(0, opts.baseUrl.length - 1); + } + + // Allow trailing slash in IS url + if (opts.idBaseUrl && opts.idBaseUrl.endsWith("/")) { + opts.idBaseUrl = opts.idBaseUrl.substr(0, opts.idBaseUrl.length - 1); + } + + MatrixBaseApis.call(this, opts); + + this.reEmitter = new _ReEmitter2.default(this); + + this.store = opts.store || new StubStore(); + + this.deviceId = opts.deviceId || null; + + var userId = opts.userId || null; + this.credentials = { + userId: userId + }; + + this.scheduler = opts.scheduler; + if (this.scheduler) { + var self = this; + this.scheduler.setProcessFunction(function (eventToSend) { + var room = self.getRoom(eventToSend.getRoomId()); + if (eventToSend.status !== EventStatus.SENDING) { + _updatePendingEventStatus(room, eventToSend, EventStatus.SENDING); + } + return _sendEventHttpRequest(self, eventToSend); + }); + } + this.clientRunning = false; + + this.callList = { + // callId: MatrixCall + }; + + // try constructing a MatrixCall to see if we are running in an environment + // which has WebRTC. If we are, listen for and handle m.call.* events. + var call = webRtcCall.createNewMatrixCall(this); + this._supportsVoip = false; + if (call) { + setupCallEventHandler(this); + this._supportsVoip = true; + } + this._syncingRetry = null; + this._syncApi = null; + this._peekSync = null; + this._isGuest = false; + this._ongoingScrollbacks = {}; + this.timelineSupport = Boolean(opts.timelineSupport); + this.urlPreviewCache = {}; + this._notifTimelineSet = null; + + this._crypto = null; + this._cryptoStore = opts.cryptoStore; + this._sessionStore = opts.sessionStore; + + this._forceTURN = opts.forceTURN || false; + + if (CRYPTO_ENABLED) { + this.olmVersion = Crypto.getOlmVersion(); + } + + // List of which rooms have encryption enabled: separate from crypto because + // we still want to know which rooms are encrypted even if crypto is disabled: + // we don't want to start sending unencrypted events to them. + this._roomList = new _RoomList2.default(this._cryptoStore, this._sessionStore); + + // The pushprocessor caches useful things, so keep one and re-use it + this._pushProcessor = new PushProcessor(this); +} +utils.inherits(MatrixClient, EventEmitter); +utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype); + +/** + * Clear any data out of the persistent stores used by the client. + * + * @returns {Promise} Promise which resolves when the stores have been cleared. + */ +MatrixClient.prototype.clearStores = function () { + if (this._clientRunning) { + throw new Error("Cannot clear stores while client is running"); + } + + var promises = []; + + promises.push(this.store.deleteAllData()); + if (this._cryptoStore) { + promises.push(this._cryptoStore.deleteAllData()); + } + return _bluebird2.default.all(promises); +}; + +/** + * Get the user-id of the logged-in user + * + * @return {?string} MXID for the logged-in user, or null if not logged in + */ +MatrixClient.prototype.getUserId = function () { + if (this.credentials && this.credentials.userId) { + return this.credentials.userId; + } + return null; +}; + +/** + * Get the domain for this client's MXID + * @return {?string} Domain of this MXID + */ +MatrixClient.prototype.getDomain = function () { + if (this.credentials && this.credentials.userId) { + return this.credentials.userId.replace(/^.*?:/, ''); + } + return null; +}; + +/** + * Get the local part of the current user ID e.g. "foo" in "@foo:bar". + * @return {?string} The user ID localpart or null. + */ +MatrixClient.prototype.getUserIdLocalpart = function () { + if (this.credentials && this.credentials.userId) { + return this.credentials.userId.split(":")[0].substring(1); + } + return null; +}; + +/** + * Get the device ID of this client + * @return {?string} device ID + */ +MatrixClient.prototype.getDeviceId = function () { + return this.deviceId; +}; + +/** + * Check if the runtime environment supports VoIP calling. + * @return {boolean} True if VoIP is supported. + */ +MatrixClient.prototype.supportsVoip = function () { + return this._supportsVoip; +}; + +/** + * Set whether VoIP calls are forced to use only TURN + * candidates. This is the same as the forceTURN option + * when creating the client. + * @param {bool} forceTURN True to force use of TURN servers + */ +MatrixClient.prototype.setForceTURN = function (forceTURN) { + this._forceTURN = forceTURN; +}; + +/** + * Get the current sync state. + * @return {?string} the sync state, which may be null. + * @see module:client~MatrixClient#event:"sync" + */ +MatrixClient.prototype.getSyncState = function () { + if (!this._syncApi) { + return null; + } + return this._syncApi.getSyncState(); +}; + +/** + * Return whether the client is configured for a guest account. + * @return {boolean} True if this is a guest access_token (or no token is supplied). + */ +MatrixClient.prototype.isGuest = function () { + return this._isGuest; +}; + +/** + * Return the provided scheduler, if any. + * @return {?module:scheduler~MatrixScheduler} The scheduler or null + */ +MatrixClient.prototype.getScheduler = function () { + return this.scheduler; +}; + +/** + * Set whether this client is a guest account. This method is experimental + * and may change without warning. + * @param {boolean} isGuest True if this is a guest account. + */ +MatrixClient.prototype.setGuest = function (isGuest) { + // EXPERIMENTAL: + // If the token is a macaroon, it should be encoded in it that it is a 'guest' + // access token, which means that the SDK can determine this entirely without + // the dev manually flipping this flag. + this._isGuest = isGuest; +}; + +/** + * Retry a backed off syncing request immediately. This should only be used when + * the user explicitly attempts to retry their lost connection. + * @return {boolean} True if this resulted in a request being retried. + */ +MatrixClient.prototype.retryImmediately = function () { + return this._syncApi.retryImmediately(); +}; + +/** + * Return the global notification EventTimelineSet, if any + * + * @return {EventTimelineSet} the globl notification EventTimelineSet + */ +MatrixClient.prototype.getNotifTimelineSet = function () { + return this._notifTimelineSet; +}; + +/** + * Set the global notification EventTimelineSet + * + * @param {EventTimelineSet} notifTimelineSet + */ +MatrixClient.prototype.setNotifTimelineSet = function (notifTimelineSet) { + this._notifTimelineSet = notifTimelineSet; +}; + +// Crypto bits +// =========== + +/** + * Initialise support for end-to-end encryption in this client + * + * You should call this method after creating the matrixclient, but *before* + * calling `startClient`, if you want to support end-to-end encryption. + * + * It will return a Promise which will resolve when the crypto layer has been + * successfully initialised. + */ +MatrixClient.prototype.initCrypto = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee() { + var userId, crypto; + return _regenerator2.default.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + if (!this._crypto) { + _context.next = 3; + break; + } + + console.warn("Attempt to re-initialise e2e encryption on MatrixClient"); + return _context.abrupt("return"); + + case 3: + if (this._sessionStore) { + _context.next = 5; + break; + } + + throw new Error("Cannot enable encryption: no sessionStore provided"); + + case 5: + if (this._cryptoStore) { + _context.next = 7; + break; + } + + throw new Error("Cannot enable encryption: no cryptoStore provided"); + + case 7: + _context.next = 9; + return (0, _bluebird.resolve)(this._roomList.init()); + + case 9: + if (CRYPTO_ENABLED) { + _context.next = 11; + break; + } + + throw new Error("End-to-end encryption not supported in this js-sdk build: did " + "you remember to load the olm library?"); + + case 11: + userId = this.getUserId(); + + if (!(userId === null)) { + _context.next = 14; + break; + } + + throw new Error("Cannot enable encryption on MatrixClient with unknown userId: " + "ensure userId is passed in createClient()."); + + case 14: + if (!(this.deviceId === null)) { + _context.next = 16; + break; + } + + throw new Error("Cannot enable encryption on MatrixClient with unknown deviceId: " + "ensure deviceId is passed in createClient()."); + + case 16: + crypto = new Crypto(this, this._sessionStore, userId, this.deviceId, this.store, this._cryptoStore, this._roomList); + + + this.reEmitter.reEmit(crypto, ["crypto.roomKeyRequest", "crypto.roomKeyRequestCancellation", "crypto.warning"]); + + _context.next = 20; + return (0, _bluebird.resolve)(crypto.init()); + + case 20: + + // if crypto initialisation was successful, tell it to attach its event + // handlers. + crypto.registerEventHandlers(this); + this._crypto = crypto; + + case 22: + case "end": + return _context.stop(); + } + } + }, _callee, this); +})); + +/** + * Is end-to-end crypto enabled for this client. + * @return {boolean} True if end-to-end is enabled. + */ +MatrixClient.prototype.isCryptoEnabled = function () { + return this._crypto !== null; +}; + +/** + * Get the Ed25519 key for this device + * + * @return {?string} base64-encoded ed25519 key. Null if crypto is + * disabled. + */ +MatrixClient.prototype.getDeviceEd25519Key = function () { + if (!this._crypto) { + return null; + } + return this._crypto.getDeviceEd25519Key(); +}; + +/** + * Upload the device keys to the homeserver. + * @return {object} A promise that will resolve when the keys are uploaded. + */ +MatrixClient.prototype.uploadKeys = function () { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.uploadDeviceKeys(); +}; + +/** + * Download the keys for a list of users and stores the keys in the session + * store. + * @param {Array} userIds The users to fetch. + * @param {bool} forceDownload Always download the keys even if cached. + * + * @return {Promise} A promise which resolves to a map userId->deviceId->{@link + * module:crypto~DeviceInfo|DeviceInfo}. + */ +MatrixClient.prototype.downloadKeys = function (userIds, forceDownload) { + if (this._crypto === null) { + return _bluebird2.default.reject(new Error("End-to-end encryption disabled")); + } + return this._crypto.downloadKeys(userIds, forceDownload); +}; + +/** + * Get the stored device keys for a user id + * + * @param {string} userId the user to list keys for. + * + * @return {Promise} list of devices + */ +MatrixClient.prototype.getStoredDevicesForUser = function () { + var _ref2 = (0, _bluebird.method)(function (userId) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + return this._crypto.getStoredDevicesForUser(userId) || []; + }); + + return function (_x) { + return _ref2.apply(this, arguments); + }; +}(); + +/** + * Get the stored device key for a user id and device id + * + * @param {string} userId the user to list keys for. + * @param {string} deviceId unique identifier for the device + * + * @return {Promise} device or null + */ +MatrixClient.prototype.getStoredDevice = function () { + var _ref3 = (0, _bluebird.method)(function (userId, deviceId) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + return this._crypto.getStoredDevice(userId, deviceId) || null; + }); + + return function (_x2, _x3) { + return _ref3.apply(this, arguments); + }; +}(); + +/** + * Mark the given device as verified + * + * @param {string} userId owner of the device + * @param {string} deviceId unique identifier for the device + * + * @param {boolean=} verified whether to mark the device as verified. defaults + * to 'true'. + * + * @returns {Promise} + * + * @fires module:client~event:MatrixClient"deviceVerificationChanged" + */ +MatrixClient.prototype.setDeviceVerified = function (userId, deviceId, verified) { + if (verified === undefined) { + verified = true; + } + return _setDeviceVerification(this, userId, deviceId, verified, null); +}; + +/** + * Mark the given device as blocked/unblocked + * + * @param {string} userId owner of the device + * @param {string} deviceId unique identifier for the device + * + * @param {boolean=} blocked whether to mark the device as blocked. defaults + * to 'true'. + * + * @returns {Promise} + * + * @fires module:client~event:MatrixClient"deviceVerificationChanged" + */ +MatrixClient.prototype.setDeviceBlocked = function (userId, deviceId, blocked) { + if (blocked === undefined) { + blocked = true; + } + return _setDeviceVerification(this, userId, deviceId, null, blocked); +}; + +/** + * Mark the given device as known/unknown + * + * @param {string} userId owner of the device + * @param {string} deviceId unique identifier for the device + * + * @param {boolean=} known whether to mark the device as known. defaults + * to 'true'. + * + * @returns {Promise} + * + * @fires module:client~event:MatrixClient"deviceVerificationChanged" + */ +MatrixClient.prototype.setDeviceKnown = function (userId, deviceId, known) { + if (known === undefined) { + known = true; + } + return _setDeviceVerification(this, userId, deviceId, null, null, known); +}; + +MatrixClient.prototype.setGlobalBlacklistUnverifiedDevices = function (value) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + this._crypto.setGlobalBlacklistUnverifiedDevices(value); +}; + +/** + * @return {boolean} whether to blacklist all unverified devices by default + */ +MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function () { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + return this._crypto.getGlobalBlacklistUnverifiedDevices(); +}; + +/** + * Get e2e information on the device that sent an event + * + * @param {MatrixEvent} event event to be checked + * + * @return {Promise} + */ +MatrixClient.prototype.getEventSenderDeviceInfo = function () { + var _ref5 = (0, _bluebird.method)(function (event) { + if (!this._crypto) { + return null; + } + + return this._crypto.getEventSenderDeviceInfo(event); + }); + + return function (_x10) { + return _ref5.apply(this, arguments); + }; +}(); + +/** + * Check if the sender of an event is verified + * + * @param {MatrixEvent} event event to be checked + * + * @return {boolean} true if the sender of this event has been verified using + * {@link module:client~MatrixClient#setDeviceVerified|setDeviceVerified}. + */ +MatrixClient.prototype.isEventSenderVerified = function () { + var _ref6 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee3(event) { + var device; + return _regenerator2.default.wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + _context3.next = 2; + return (0, _bluebird.resolve)(this.getEventSenderDeviceInfo(event)); + + case 2: + device = _context3.sent; + + if (device) { + _context3.next = 5; + break; + } + + return _context3.abrupt("return", false); + + case 5: + return _context3.abrupt("return", device.isVerified()); + + case 6: + case "end": + return _context3.stop(); + } + } + }, _callee3, this); + })); + + return function (_x11) { + return _ref6.apply(this, arguments); + }; +}(); + +/** + * Cancel a room key request for this event if one is ongoing and resend the + * request. + * @param {MatrxEvent} event event of which to cancel and resend the room + * key request. + */ +MatrixClient.prototype.cancelAndResendEventRoomKeyRequest = function (event) { + event.cancelAndResendKeyRequest(this._crypto); +}; + +/** + * Enable end-to-end encryption for a room. + * @param {string} roomId The room ID to enable encryption in. + * @param {object} config The encryption config for the room. + * @return {Promise} A promise that will resolve when encryption is set up. + */ +MatrixClient.prototype.setRoomEncryption = function (roomId, config) { + if (!this._crypto) { + throw new Error("End-to-End encryption disabled"); + } + return this._crypto.setRoomEncryption(roomId, config); +}; + +/** + * Whether encryption is enabled for a room. + * @param {string} roomId the room id to query. + * @return {bool} whether encryption is enabled. + */ +MatrixClient.prototype.isRoomEncrypted = function (roomId) { + var room = this.getRoom(roomId); + if (!room) { + // we don't know about this room, so can't determine if it should be + // encrypted. Let's assume not. + return false; + } + + // if there is an 'm.room.encryption' event in this room, it should be + // encrypted (independently of whether we actually support encryption) + var ev = room.currentState.getStateEvents("m.room.encryption", ""); + if (ev) { + return true; + } + + // we don't have an m.room.encrypted event, but that might be because + // the server is hiding it from us. Check the store to see if it was + // previously encrypted. + return this._roomList.isRoomEncrypted(roomId); +}; + +/** + * Get a list containing all of the room keys + * + * This should be encrypted before returning it to the user. + * + * @return {module:client.Promise} a promise which resolves to a list of + * session export objects + */ +MatrixClient.prototype.exportRoomKeys = function () { + if (!this._crypto) { + return _bluebird2.default.reject(new Error("End-to-end encryption disabled")); + } + return this._crypto.exportRoomKeys(); +}; + +/** + * Import a list of room keys previously exported by exportRoomKeys + * + * @param {Object[]} keys a list of session export objects + * + * @return {module:client.Promise} a promise which resolves when the keys + * have been imported + */ +MatrixClient.prototype.importRoomKeys = function (keys) { + if (!this._crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this._crypto.importRoomKeys(keys); +}; + +// Group ops +// ========= +// Operations on groups that come down the sync stream (ie. ones the +// user is a member of or invited to) + +/** + * Get the group for the given group ID. + * This function will return a valid group for any group for which a Group event + * has been emitted. + * @param {string} groupId The group ID + * @return {Group} The Group or null if the group is not known or there is no data store. + */ +MatrixClient.prototype.getGroup = function (groupId) { + return this.store.getGroup(groupId); +}; + +/** + * Retrieve all known groups. + * @return {Groups[]} A list of groups, or an empty list if there is no data store. + */ +MatrixClient.prototype.getGroups = function () { + return this.store.getGroups(); +}; + +// Room ops +// ======== + +/** + * Get the room for the given room ID. + * This function will return a valid room for any room for which a Room event + * has been emitted. Note in particular that other events, eg. RoomState.members + * will be emitted for a room before this function will return the given room. + * @param {string} roomId The room ID + * @return {Room} The Room or null if it doesn't exist or there is no data store. + */ +MatrixClient.prototype.getRoom = function (roomId) { + return this.store.getRoom(roomId); +}; + +/** + * Retrieve all known rooms. + * @return {Room[]} A list of rooms, or an empty list if there is no data store. + */ +MatrixClient.prototype.getRooms = function () { + return this.store.getRooms(); +}; + +/** + * Retrieve a user. + * @param {string} userId The user ID to retrieve. + * @return {?User} A user or null if there is no data store or the user does + * not exist. + */ +MatrixClient.prototype.getUser = function (userId) { + return this.store.getUser(userId); +}; + +/** + * Retrieve all known users. + * @return {User[]} A list of users, or an empty list if there is no data store. + */ +MatrixClient.prototype.getUsers = function () { + return this.store.getUsers(); +}; + +// User Account Data operations +// ============================ + +/** + * Set account data event for the current user. + * @param {string} eventType The event type + * @param {Object} contents the contents object for the event + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.setAccountData = function (eventType, contents, callback) { + var path = utils.encodeUri("/user/$userId/account_data/$type", { + $userId: this.credentials.userId, + $type: eventType + }); + return this._http.authedRequest(callback, "PUT", path, undefined, contents); +}; + +/** + * Get account data event of given type for the current user. + * @param {string} eventType The event type + * @return {?object} The contents of the given account data event + */ +MatrixClient.prototype.getAccountData = function (eventType) { + return this.store.getAccountData(eventType); +}; + +/** + * Gets the users that are ignored by this client + * @returns {string[]} The array of users that are ignored (empty if none) + */ +MatrixClient.prototype.getIgnoredUsers = function () { + var event = this.getAccountData("m.ignored_user_list"); + if (!event || !event.getContent() || !event.getContent()["ignored_users"]) return []; + return (0, _keys2.default)(event.getContent()["ignored_users"]); +}; + +/** + * Sets the users that the current user should ignore. + * @param {string[]} userIds the user IDs to ignore + * @param {module:client.callback} [callback] Optional. + * @return {module:client.Promise} Resolves: Account data event + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.setIgnoredUsers = function (userIds, callback) { + var content = { ignored_users: {} }; + userIds.map(function (u) { + return content.ignored_users[u] = {}; + }); + return this.setAccountData("m.ignored_user_list", content, callback); +}; + +/** + * Gets whether or not a specific user is being ignored by this client. + * @param {string} userId the user ID to check + * @returns {boolean} true if the user is ignored, false otherwise + */ +MatrixClient.prototype.isUserIgnored = function (userId) { + return this.getIgnoredUsers().indexOf(userId) !== -1; +}; + +// Room operations +// =============== + +/** + * Join a room. If you have already joined the room, this will no-op. + * @param {string} roomIdOrAlias The room ID or room alias to join. + * @param {Object} opts Options when joining the room. + * @param {boolean} opts.syncRoom True to do a room initial sync on the resulting + * room. If false, the returned Room object will have no current state. + * Default: true. + * @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite, + * the signing URL is passed in this parameter. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: Room object. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.joinRoom = function (roomIdOrAlias, opts, callback) { + // to help people when upgrading.. + if (utils.isFunction(opts)) { + throw new Error("Expected 'opts' object, got function."); + } + opts = opts || {}; + if (opts.syncRoom === undefined) { + opts.syncRoom = true; + } + + var room = this.getRoom(roomIdOrAlias); + if (room && room.hasMembershipState(this.credentials.userId, "join")) { + return _bluebird2.default.resolve(room); + } + + var sign_promise = _bluebird2.default.resolve(); + + if (opts.inviteSignUrl) { + sign_promise = this._http.requestOtherUrl(undefined, 'POST', opts.inviteSignUrl, { mxid: this.credentials.userId }); + } + + var defer = _bluebird2.default.defer(); + + var self = this; + sign_promise.then(function (signed_invite_object) { + var data = {}; + if (signed_invite_object) { + data.third_party_signed = signed_invite_object; + } + + var path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias }); + return self._http.authedRequest(undefined, "POST", path, undefined, data); + }).then(function (res) { + var roomId = res.room_id; + var syncApi = new SyncApi(self, self._clientOpts); + var room = syncApi.createRoom(roomId); + if (opts.syncRoom) { + // v2 will do this for us + // return syncApi.syncRoom(room); + } + return _bluebird2.default.resolve(room); + }).done(function (room) { + _resolve(callback, defer, room); + }, function (err) { + _reject(callback, defer, err); + }); + return defer.promise; +}; + +/** + * Resend an event. + * @param {MatrixEvent} event The event to resend. + * @param {Room} room Optional. The room the event is in. Will update the + * timeline entry if provided. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.resendEvent = function (event, room) { + _updatePendingEventStatus(room, event, EventStatus.SENDING); + return _sendEvent(this, room, event); +}; + +/** + * Cancel a queued or unsent event. + * + * @param {MatrixEvent} event Event to cancel + * @throws Error if the event is not in QUEUED or NOT_SENT state + */ +MatrixClient.prototype.cancelPendingEvent = function (event) { + if ([EventStatus.QUEUED, EventStatus.NOT_SENT].indexOf(event.status) < 0) { + throw new Error("cannot cancel an event with status " + event.status); + } + + // first tell the scheduler to forget about it, if it's queued + if (this.scheduler) { + this.scheduler.removeEventFromQueue(event); + } + + // then tell the room about the change of state, which will remove it + // from the room's list of pending events. + var room = this.getRoom(event.getRoomId()); + _updatePendingEventStatus(room, event, EventStatus.CANCELLED); +}; + +/** + * @param {string} roomId + * @param {string} name + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.setRoomName = function (roomId, name, callback) { + return this.sendStateEvent(roomId, "m.room.name", { name: name }, undefined, callback); +}; + +/** + * @param {string} roomId + * @param {string} topic + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.setRoomTopic = function (roomId, topic, callback) { + return this.sendStateEvent(roomId, "m.room.topic", { topic: topic }, undefined, callback); +}; + +/** + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.getRoomTags = function (roomId, callback) { + var path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/", { + $userId: this.credentials.userId, + $roomId: roomId + }); + return this._http.authedRequest(callback, "GET", path, undefined); +}; + +/** + * @param {string} roomId + * @param {string} tagName name of room tag to be set + * @param {object} metadata associated with that tag to be stored + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.setRoomTag = function (roomId, tagName, metadata, callback) { + var path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { + $userId: this.credentials.userId, + $roomId: roomId, + $tag: tagName + }); + return this._http.authedRequest(callback, "PUT", path, undefined, metadata); +}; + +/** + * @param {string} roomId + * @param {string} tagName name of room tag to be removed + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.deleteRoomTag = function (roomId, tagName, callback) { + var path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { + $userId: this.credentials.userId, + $roomId: roomId, + $tag: tagName + }); + return this._http.authedRequest(callback, "DELETE", path, undefined, undefined); +}; + +/** + * @param {string} roomId + * @param {string} eventType event type to be set + * @param {object} content event content + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.setRoomAccountData = function (roomId, eventType, content, callback) { + var path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", { + $userId: this.credentials.userId, + $roomId: roomId, + $type: eventType + }); + return this._http.authedRequest(callback, "PUT", path, undefined, content); +}; + +/** + * Set a user's power level. + * @param {string} roomId + * @param {string} userId + * @param {Number} powerLevel + * @param {MatrixEvent} event + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.setPowerLevel = function (roomId, userId, powerLevel, event, callback) { + var content = { + users: {} + }; + if (event && event.getType() === "m.room.power_levels") { + // take a copy of the content to ensure we don't corrupt + // existing client state with a failed power level change + content = utils.deepCopy(event.getContent()); + } + content.users[userId] = powerLevel; + var path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", { + $roomId: roomId + }); + return this._http.authedRequest(callback, "PUT", path, undefined, content); +}; + +/** + * @param {string} roomId + * @param {string} eventType + * @param {Object} content + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendEvent = function (roomId, eventType, content, txnId, callback) { + if (utils.isFunction(txnId)) { + callback = txnId;txnId = undefined; + } + + if (!txnId) { + txnId = this.makeTxnId(); + } + + console.log("sendEvent of type " + eventType + " in " + roomId + " with txnId " + txnId); + + // we always construct a MatrixEvent when sending because the store and + // scheduler use them. We'll extract the params back out if it turns out + // the client has no scheduler or store. + var room = this.getRoom(roomId); + var localEvent = new MatrixEvent({ + event_id: "~" + roomId + ":" + txnId, + user_id: this.credentials.userId, + room_id: roomId, + type: eventType, + origin_server_ts: new Date().getTime(), + content: content + }); + localEvent._txnId = txnId; + localEvent.status = EventStatus.SENDING; + + // add this event immediately to the local store as 'sending'. + if (room) { + room.addPendingEvent(localEvent, txnId); + } + + return _sendEvent(this, room, localEvent, callback); +}; + +// encrypts the event if necessary +// adds the event to the queue, or sends it +// marks the event as sent/unsent +// returns a promise which resolves with the result of the send request +function _sendEvent(client, room, event, callback) { + // Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections, + // so that we can handle synchronous and asynchronous exceptions with the + // same code path. + return _bluebird2.default.resolve().then(function () { + var encryptionPromise = _encryptEventIfNeeded(client, event, room); + + if (!encryptionPromise) { + return null; + } + + _updatePendingEventStatus(room, event, EventStatus.ENCRYPTING); + return encryptionPromise.then(function () { + _updatePendingEventStatus(room, event, EventStatus.SENDING); + }); + }).then(function () { + var promise = void 0; + // this event may be queued + if (client.scheduler) { + // if this returns a promsie then the scheduler has control now and will + // resolve/reject when it is done. Internally, the scheduler will invoke + // processFn which is set to this._sendEventHttpRequest so the same code + // path is executed regardless. + promise = client.scheduler.queueEvent(event); + if (promise && client.scheduler.getQueueForEvent(event).length > 1) { + // event is processed FIFO so if the length is 2 or more we know + // this event is stuck behind an earlier event. + _updatePendingEventStatus(room, event, EventStatus.QUEUED); + } + } + + if (!promise) { + promise = _sendEventHttpRequest(client, event); + } + return promise; + }).then(function (res) { + // the request was sent OK + if (room) { + room.updatePendingEvent(event, EventStatus.SENT, res.event_id); + } + if (callback) { + callback(null, res); + } + return res; + }, function (err) { + // the request failed to send. + console.error("Error sending event", err.stack || err); + + try { + // set the error on the event before we update the status: + // updating the status emits the event, so the state should be + // consistent at that point. + event.error = err; + _updatePendingEventStatus(room, event, EventStatus.NOT_SENT); + // also put the event object on the error: the caller will need this + // to resend or cancel the event + err.event = event; + + if (callback) { + callback(err); + } + } catch (err2) { + console.error("Exception in error handler!", err2.stack || err); + } + throw err; + }); +} + +/** + * Encrypt an event according to the configuration of the room, if necessary. + * + * @param {MatrixClient} client + * + * @param {module:models/event.MatrixEvent} event event to be sent + * + * @param {module:models/room?} room destination room. Null if the destination + * is not a room we have seen over the sync pipe. + * + * @return {module:client.Promise?} Promise which resolves when the event has been + * encrypted, or null if nothing was needed + */ + +function _encryptEventIfNeeded(client, event, room) { + if (event.isEncrypted()) { + // this event has already been encrypted; this happens if the + // encryption step succeeded, but the send step failed on the first + // attempt. + return null; + } + + if (!client.isRoomEncrypted(event.getRoomId())) { + // looks like this room isn't encrypted. + return null; + } + + if (!client._crypto) { + throw new Error("This room is configured to use encryption, but your client does " + "not support encryption."); + } + + return client._crypto.encryptEvent(event, room); +} + +function _updatePendingEventStatus(room, event, newStatus) { + if (room) { + room.updatePendingEvent(event, newStatus); + } else { + event.status = newStatus; + } +} + +function _sendEventHttpRequest(client, event) { + var txnId = event._txnId ? event._txnId : client.makeTxnId(); + + var pathParams = { + $roomId: event.getRoomId(), + $eventType: event.getWireType(), + $stateKey: event.getStateKey(), + $txnId: txnId + }; + + var path = void 0; + + if (event.isState()) { + var pathTemplate = "/rooms/$roomId/state/$eventType"; + if (event.getStateKey() && event.getStateKey().length > 0) { + pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey"; + } + path = utils.encodeUri(pathTemplate, pathParams); + } else { + path = utils.encodeUri("/rooms/$roomId/send/$eventType/$txnId", pathParams); + } + + return client._http.authedRequest(undefined, "PUT", path, undefined, event.getWireContent()).then(function (res) { + console.log("Event sent to " + event.getRoomId() + " with event id " + res.event_id); + return res; + }); +} + +/** + * @param {string} roomId + * @param {Object} content + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendMessage = function (roomId, content, txnId, callback) { + if (utils.isFunction(txnId)) { + callback = txnId;txnId = undefined; + } + return this.sendEvent(roomId, "m.room.message", content, txnId, callback); +}; + +/** + * @param {string} roomId + * @param {string} body + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendTextMessage = function (roomId, body, txnId, callback) { + var content = ContentHelpers.makeTextMessage(body); + return this.sendMessage(roomId, content, txnId, callback); +}; + +/** + * @param {string} roomId + * @param {string} body + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendNotice = function (roomId, body, txnId, callback) { + var content = ContentHelpers.makeNotice(body); + return this.sendMessage(roomId, content, txnId, callback); +}; + +/** + * @param {string} roomId + * @param {string} body + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendEmoteMessage = function (roomId, body, txnId, callback) { + var content = ContentHelpers.makeEmoteMessage(body); + return this.sendMessage(roomId, content, txnId, callback); +}; + +/** + * @param {string} roomId + * @param {string} url + * @param {Object} info + * @param {string} text + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendImageMessage = function (roomId, url, info, text, callback) { + if (utils.isFunction(text)) { + callback = text;text = undefined; + } + if (!text) { + text = "Image"; + } + var content = { + msgtype: "m.image", + url: url, + info: info, + body: text + }; + return this.sendMessage(roomId, content, callback); +}; + +/** + * @param {string} roomId + * @param {string} url + * @param {Object} info + * @param {string} text + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendStickerMessage = function (roomId, url, info, text, callback) { + if (utils.isFunction(text)) { + callback = text;text = undefined; + } + if (!text) { + text = "Sticker"; + } + var content = { + url: url, + info: info, + body: text + }; + return this.sendEvent(roomId, "m.sticker", content, callback, undefined); +}; + +/** + * @param {string} roomId + * @param {string} body + * @param {string} htmlBody + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendHtmlMessage = function (roomId, body, htmlBody, callback) { + var content = ContentHelpers.makeHtmlMessage(body, htmlBody); + return this.sendMessage(roomId, content, callback); +}; + +/** + * @param {string} roomId + * @param {string} body + * @param {string} htmlBody + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendHtmlNotice = function (roomId, body, htmlBody, callback) { + var content = ContentHelpers.makeHtmlNotice(body, htmlBody); + return this.sendMessage(roomId, content, callback); +}; + +/** + * @param {string} roomId + * @param {string} body + * @param {string} htmlBody + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendHtmlEmote = function (roomId, body, htmlBody, callback) { + var content = ContentHelpers.makeHtmlEmote(body, htmlBody); + return this.sendMessage(roomId, content, callback); +}; + +/** + * Send a receipt. + * @param {Event} event The event being acknowledged + * @param {string} receiptType The kind of receipt e.g. "m.read" + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendReceipt = function (event, receiptType, callback) { + if (this.isGuest()) { + return _bluebird2.default.resolve({}); // guests cannot send receipts so don't bother. + } + + var path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", { + $roomId: event.getRoomId(), + $receiptType: receiptType, + $eventId: event.getId() + }); + var promise = this._http.authedRequest(callback, "POST", path, undefined, {}); + + var room = this.getRoom(event.getRoomId()); + if (room) { + room._addLocalEchoReceipt(this.credentials.userId, event, receiptType); + } + return promise; +}; + +/** + * Send a read receipt. + * @param {Event} event The event that has been read. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendReadReceipt = function (event, callback) { + return this.sendReceipt(event, "m.read", callback); +}; + +/** + * Set a marker to indicate the point in a room before which the user has read every + * event. This can be retrieved from room account data (the event type is `m.fully_read`) + * and displayed as a horizontal line in the timeline that is visually distinct to the + * position of the user's own read receipt. + * @param {string} roomId ID of the room that has been read + * @param {string} eventId ID of the event that has been read + * @param {string} rrEvent the event tracked by the read receipt. This is here for + * convenience because the RR and the RM are commonly updated at the same time as each + * other. The local echo of this receipt will be done if set. Optional. + * @return {module:client.Promise} Resolves: the empty object, {}. + */ +MatrixClient.prototype.setRoomReadMarkers = function (roomId, eventId, rrEvent) { + var rmEventId = eventId; + var rrEventId = void 0; + + // Add the optional RR update, do local echo like `sendReceipt` + if (rrEvent) { + rrEventId = rrEvent.getId(); + var room = this.getRoom(roomId); + if (room) { + room._addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read"); + } + } + + return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId); +}; + +/** + * Get a preview of the given URL as of (roughly) the given point in time, + * described as an object with OpenGraph keys and associated values. + * Attributes may be synthesized where actual OG metadata is lacking. + * Caches results to prevent hammering the server. + * @param {string} url The URL to get preview data for + * @param {Number} ts The preferred point in time that the preview should + * describe (ms since epoch). The preview returned will either be the most + * recent one preceding this timestamp if available, or failing that the next + * most recent available preview. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: Object of OG metadata. + * @return {module:http-api.MatrixError} Rejects: with an error response. + * May return synthesized attributes if the URL lacked OG meta. + */ +MatrixClient.prototype.getUrlPreview = function (url, ts, callback) { + var key = ts + "_" + url; + var og = this.urlPreviewCache[key]; + if (og) { + return _bluebird2.default.resolve(og); + } + + var self = this; + return this._http.authedRequestWithPrefix(callback, "GET", "/preview_url", { + url: url, + ts: ts + }, undefined, httpApi.PREFIX_MEDIA_R0).then(function (response) { + // TODO: expire cache occasionally + self.urlPreviewCache[key] = response; + return response; + }); +}; + +/** + * @param {string} roomId + * @param {boolean} isTyping + * @param {Number} timeoutMs + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendTyping = function (roomId, isTyping, timeoutMs, callback) { + if (this.isGuest()) { + return _bluebird2.default.resolve({}); // guests cannot send typing notifications so don't bother. + } + + var path = utils.encodeUri("/rooms/$roomId/typing/$userId", { + $roomId: roomId, + $userId: this.credentials.userId + }); + var data = { + typing: isTyping + }; + if (isTyping) { + data.timeout = timeoutMs ? timeoutMs : 20000; + } + return this._http.authedRequest(callback, "PUT", path, undefined, data); +}; + +/** + * @param {string} roomId + * @param {string} userId + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.invite = function (roomId, userId, callback) { + return _membershipChange(this, roomId, userId, "invite", undefined, callback); +}; + +/** + * Invite a user to a room based on their email address. + * @param {string} roomId The room to invite the user to. + * @param {string} email The email address to invite. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.inviteByEmail = function (roomId, email, callback) { + return this.inviteByThreePid(roomId, "email", email, callback); +}; + +/** + * Invite a user to a room based on a third-party identifier. + * @param {string} roomId The room to invite the user to. + * @param {string} medium The medium to invite the user e.g. "email". + * @param {string} address The address for the specified medium. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.inviteByThreePid = function (roomId, medium, address, callback) { + var path = utils.encodeUri("/rooms/$roomId/invite", { $roomId: roomId }); + + var identityServerUrl = this.getIdentityServerUrl(true); + if (!identityServerUrl) { + return _bluebird2.default.reject(new MatrixError({ + error: "No supplied identity server URL", + errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM" + })); + } + + return this._http.authedRequest(callback, "POST", path, undefined, { + id_server: identityServerUrl, + medium: medium, + address: address + }); +}; + +/** + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.leave = function (roomId, callback) { + return _membershipChange(this, roomId, undefined, "leave", undefined, callback); +}; + +/** + * @param {string} roomId + * @param {string} userId + * @param {string} reason Optional. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.ban = function (roomId, userId, reason, callback) { + return _membershipChange(this, roomId, userId, "ban", reason, callback); +}; + +/** + * @param {string} roomId + * @param {boolean} deleteRoom True to delete the room from the store on success. + * Default: true. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.forget = function (roomId, deleteRoom, callback) { + if (deleteRoom === undefined) { + deleteRoom = true; + } + var promise = _membershipChange(this, roomId, undefined, "forget", undefined, callback); + if (!deleteRoom) { + return promise; + } + var self = this; + return promise.then(function (response) { + self.store.removeRoom(roomId); + self.emit("deleteRoom", roomId); + return response; + }); +}; + +/** + * @param {string} roomId + * @param {string} userId + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: Object (currently empty) + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.unban = function (roomId, userId, callback) { + // unbanning != set their state to leave: this used to be + // the case, but was then changed so that leaving was always + // a revoking of priviledge, otherwise two people racing to + // kick / ban someone could end up banning and then un-banning + // them. + var path = utils.encodeUri("/rooms/$roomId/unban", { + $roomId: roomId + }); + var data = { + user_id: userId + }; + return this._http.authedRequest(callback, "POST", path, undefined, data); +}; + +/** + * @param {string} roomId + * @param {string} userId + * @param {string} reason Optional. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.kick = function (roomId, userId, reason, callback) { + return _setMembershipState(this, roomId, userId, "leave", reason, callback); +}; + +/** + * This is an internal method. + * @param {MatrixClient} client + * @param {string} roomId + * @param {string} userId + * @param {string} membershipValue + * @param {string} reason + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +function _setMembershipState(client, roomId, userId, membershipValue, reason, callback) { + if (utils.isFunction(reason)) { + callback = reason;reason = undefined; + } + + var path = utils.encodeUri("/rooms/$roomId/state/m.room.member/$userId", { $roomId: roomId, $userId: userId }); + + return client._http.authedRequest(callback, "PUT", path, undefined, { + membership: membershipValue, + reason: reason + }); +} + +/** + * This is an internal method. + * @param {MatrixClient} client + * @param {string} roomId + * @param {string} userId + * @param {string} membership + * @param {string} reason + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +function _membershipChange(client, roomId, userId, membership, reason, callback) { + if (utils.isFunction(reason)) { + callback = reason;reason = undefined; + } + + var path = utils.encodeUri("/rooms/$room_id/$membership", { + $room_id: roomId, + $membership: membership + }); + return client._http.authedRequest(callback, "POST", path, undefined, { + user_id: userId, // may be undefined e.g. on leave + reason: reason + }); +} + +/** + * Obtain a dict of actions which should be performed for this event according + * to the push rules for this user. Caches the dict on the event. + * @param {MatrixEvent} event The event to get push actions for. + * @return {module:pushprocessor~PushAction} A dict of actions to perform. + */ +MatrixClient.prototype.getPushActionsForEvent = function (event) { + if (!event.getPushActions()) { + event.setPushActions(this._pushProcessor.actionsForEvent(event)); + } + return event.getPushActions(); +}; + +// Profile operations +// ================== + +/** + * @param {string} info The kind of info to set (e.g. 'avatar_url') + * @param {Object} data The JSON object to set. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.setProfileInfo = function (info, data, callback) { + var path = utils.encodeUri("/profile/$userId/$info", { + $userId: this.credentials.userId, + $info: info + }); + return this._http.authedRequest(callback, "PUT", path, undefined, data); +}; + +/** + * @param {string} name + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.setDisplayName = function (name, callback) { + return this.setProfileInfo("displayname", { displayname: name }, callback); +}; + +/** + * @param {string} url + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.setAvatarUrl = function (url, callback) { + return this.setProfileInfo("avatar_url", { avatar_url: url }, callback); +}; + +/** + * Turn an MXC URL into an HTTP one. This method is experimental and + * may change. + * @param {string} mxcUrl The MXC URL + * @param {Number} width The desired width of the thumbnail. + * @param {Number} height The desired height of the thumbnail. + * @param {string} resizeMethod The thumbnail resize method to use, either + * "crop" or "scale". + * @param {Boolean} allowDirectLinks If true, return any non-mxc URLs + * directly. Fetching such URLs will leak information about the user to + * anyone they share a room with. If false, will return null for such URLs. + * @return {?string} the avatar URL or null. + */ +MatrixClient.prototype.mxcUrlToHttp = function (mxcUrl, width, height, resizeMethod, allowDirectLinks) { + return contentRepo.getHttpUriForMxc(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks); +}; + +/** + * @param {Object} opts Options to apply + * @param {string} opts.presence One of "online", "offline" or "unavailable" + * @param {string} opts.status_msg The status message to attach. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + * @throws If 'presence' isn't a valid presence enum value. + */ +MatrixClient.prototype.setPresence = function (opts, callback) { + var path = utils.encodeUri("/presence/$userId/status", { + $userId: this.credentials.userId + }); + + if (typeof opts === "string") { + opts = { presence: opts }; + } + + var validStates = ["offline", "online", "unavailable"]; + if (validStates.indexOf(opts.presence) == -1) { + throw new Error("Bad presence value: " + opts.presence); + } + return this._http.authedRequest(callback, "PUT", path, undefined, opts); +}; + +function _presenceList(callback, client, opts, method) { + var path = utils.encodeUri("/presence/list/$userId", { + $userId: client.credentials.userId + }); + return client._http.authedRequest(callback, method, path, undefined, opts); +} + +/** +* Retrieve current user presence list. +* @param {module:client.callback} callback Optional. +* @return {module:client.Promise} Resolves: TODO +* @return {module:http-api.MatrixError} Rejects: with an error response. +*/ +MatrixClient.prototype.getPresenceList = function (callback) { + return _presenceList(callback, this, undefined, "GET"); +}; + +/** +* Add users to the current user presence list. +* @param {module:client.callback} callback Optional. +* @param {string[]} userIds +* @return {module:client.Promise} Resolves: TODO +* @return {module:http-api.MatrixError} Rejects: with an error response. +*/ +MatrixClient.prototype.inviteToPresenceList = function (callback, userIds) { + var opts = { "invite": userIds }; + return _presenceList(callback, this, opts, "POST"); +}; + +/** +* Drop users from the current user presence list. +* @param {module:client.callback} callback Optional. +* @param {string[]} userIds +* @return {module:client.Promise} Resolves: TODO +* @return {module:http-api.MatrixError} Rejects: with an error response. +**/ +MatrixClient.prototype.dropFromPresenceList = function (callback, userIds) { + var opts = { "drop": userIds }; + return _presenceList(callback, this, opts, "POST"); +}; + +/** + * Retrieve older messages from the given room and put them in the timeline. + * + * If this is called multiple times whilst a request is ongoing, the same + * Promise will be returned. If there was a problem requesting scrollback, there + * will be a small delay before another request can be made (to prevent tight-looping + * when there is no connection). + * + * @param {Room} room The room to get older messages in. + * @param {Integer} limit Optional. The maximum number of previous events to + * pull in. Default: 30. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: Room. If you are at the beginning + * of the timeline, Room.oldState.paginationToken will be + * null. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.scrollback = function (room, limit, callback) { + if (utils.isFunction(limit)) { + callback = limit;limit = undefined; + } + limit = limit || 30; + var timeToWaitMs = 0; + + var info = this._ongoingScrollbacks[room.roomId] || {}; + if (info.promise) { + return info.promise; + } else if (info.errorTs) { + var timeWaitedMs = Date.now() - info.errorTs; + timeToWaitMs = Math.max(SCROLLBACK_DELAY_MS - timeWaitedMs, 0); + } + + if (room.oldState.paginationToken === null) { + return _bluebird2.default.resolve(room); // already at the start. + } + // attempt to grab more events from the store first + var numAdded = this.store.scrollback(room, limit).length; + if (numAdded === limit) { + // store contained everything we needed. + return _bluebird2.default.resolve(room); + } + // reduce the required number of events appropriately + limit = limit - numAdded; + + var path = utils.encodeUri("/rooms/$roomId/messages", { $roomId: room.roomId }); + var params = { + from: room.oldState.paginationToken, + limit: limit, + dir: 'b' + }; + var defer = _bluebird2.default.defer(); + info = { + promise: defer.promise, + errorTs: null + }; + var self = this; + // wait for a time before doing this request + // (which may be 0 in order not to special case the code paths) + _bluebird2.default.delay(timeToWaitMs).then(function () { + return self._http.authedRequest(callback, "GET", path, params); + }).done(function (res) { + var matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self)); + room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline()); + room.oldState.paginationToken = res.end; + if (res.chunk.length === 0) { + room.oldState.paginationToken = null; + } + self.store.storeEvents(room, matrixEvents, res.end, true); + self._ongoingScrollbacks[room.roomId] = null; + _resolve(callback, defer, room); + }, function (err) { + self._ongoingScrollbacks[room.roomId] = { + errorTs: Date.now() + }; + _reject(callback, defer, err); + }); + this._ongoingScrollbacks[room.roomId] = info; + return defer.promise; +}; + +/** + * Take an EventContext, and back/forward-fill results. + * + * @param {module:models/event-context.EventContext} eventContext context + * object to be updated + * @param {Object} opts + * @param {boolean} opts.backwards true to fill backwards, false to go forwards + * @param {boolean} opts.limit number of events to request + * + * @return {module:client.Promise} Resolves: updated EventContext object + * @return {Error} Rejects: with an error response. + */ +MatrixClient.prototype.paginateEventContext = function (eventContext, opts) { + // TODO: we should implement a backoff (as per scrollback()) to deal more + // nicely with HTTP errors. + opts = opts || {}; + var backwards = opts.backwards || false; + + var token = eventContext.getPaginateToken(backwards); + if (!token) { + // no more results. + return _bluebird2.default.reject(new Error("No paginate token")); + } + + var dir = backwards ? 'b' : 'f'; + var pendingRequest = eventContext._paginateRequests[dir]; + + if (pendingRequest) { + // already a request in progress - return the existing promise + return pendingRequest; + } + + var path = utils.encodeUri("/rooms/$roomId/messages", { $roomId: eventContext.getEvent().getRoomId() }); + var params = { + from: token, + limit: 'limit' in opts ? opts.limit : 30, + dir: dir + }; + + var self = this; + var promise = self._http.authedRequest(undefined, "GET", path, params).then(function (res) { + var token = res.end; + if (res.chunk.length === 0) { + token = null; + } else { + var matrixEvents = utils.map(res.chunk, self.getEventMapper()); + if (backwards) { + // eventContext expects the events in timeline order, but + // back-pagination returns them in reverse order. + matrixEvents.reverse(); + } + eventContext.addEvents(matrixEvents, backwards); + } + eventContext.setPaginateToken(token, backwards); + return eventContext; + }).finally(function () { + eventContext._paginateRequests[dir] = null; + }); + eventContext._paginateRequests[dir] = promise; + + return promise; +}; + +/** + * Get an EventTimeline for the given event + * + *

If the EventTimelineSet object already has the given event in its store, the + * corresponding timeline will be returned. Otherwise, a /context request is + * made, and used to construct an EventTimeline. + * + * @param {EventTimelineSet} timelineSet The timelineSet to look for the event in + * @param {string} eventId The ID of the event to look for + * + * @return {module:client.Promise} Resolves: + * {@link module:models/event-timeline~EventTimeline} including the given + * event + */ +MatrixClient.prototype.getEventTimeline = function (timelineSet, eventId) { + // don't allow any timeline support unless it's been enabled. + if (!this.timelineSupport) { + throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable" + " it."); + } + + if (timelineSet.getTimelineForEvent(eventId)) { + return _bluebird2.default.resolve(timelineSet.getTimelineForEvent(eventId)); + } + + var path = utils.encodeUri("/rooms/$roomId/context/$eventId", { + $roomId: timelineSet.room.roomId, + $eventId: eventId + }); + + // TODO: we should implement a backoff (as per scrollback()) to deal more + // nicely with HTTP errors. + var self = this; + var promise = self._http.authedRequest(undefined, "GET", path).then(function (res) { + if (!res.event) { + throw new Error("'event' not in '/context' result - homeserver too old?"); + } + + // by the time the request completes, the event might have ended up in + // the timeline. + if (timelineSet.getTimelineForEvent(eventId)) { + return timelineSet.getTimelineForEvent(eventId); + } + + // we start with the last event, since that's the point at which we + // have known state. + // events_after is already backwards; events_before is forwards. + res.events_after.reverse(); + var events = res.events_after.concat([res.event]).concat(res.events_before); + var matrixEvents = utils.map(events, self.getEventMapper()); + + var timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId()); + if (!timeline) { + timeline = timelineSet.addTimeline(); + timeline.initialiseState(utils.map(res.state, self.getEventMapper())); + timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end; + } + timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start); + + // there is no guarantee that the event ended up in "timeline" (we + // might have switched to a neighbouring timeline) - so check the + // room's index again. On the other hand, there's no guarantee the + // event ended up anywhere, if it was later redacted, so we just + // return the timeline we first thought of. + var tl = timelineSet.getTimelineForEvent(eventId) || timeline; + return tl; + }); + return promise; +}; + +/** + * Take an EventTimeline, and back/forward-fill results. + * + * @param {module:models/event-timeline~EventTimeline} eventTimeline timeline + * object to be updated + * @param {Object} [opts] + * @param {bool} [opts.backwards = false] true to fill backwards, + * false to go forwards + * @param {number} [opts.limit = 30] number of events to request + * + * @return {module:client.Promise} Resolves to a boolean: false if there are no + * events and we reached either end of the timeline; else true. + */ +MatrixClient.prototype.paginateEventTimeline = function (eventTimeline, opts) { + var isNotifTimeline = eventTimeline.getTimelineSet() === this._notifTimelineSet; + + // TODO: we should implement a backoff (as per scrollback()) to deal more + // nicely with HTTP errors. + opts = opts || {}; + var backwards = opts.backwards || false; + + if (isNotifTimeline) { + if (!backwards) { + throw new Error("paginateNotifTimeline can only paginate backwards"); + } + } + + var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; + + var token = eventTimeline.getPaginationToken(dir); + if (!token) { + // no token - no results. + return _bluebird2.default.resolve(false); + } + + var pendingRequest = eventTimeline._paginationRequests[dir]; + + if (pendingRequest) { + // already a request in progress - return the existing promise + return pendingRequest; + } + + var path = void 0, + params = void 0, + promise = void 0; + var self = this; + + if (isNotifTimeline) { + path = "/notifications"; + params = { + limit: 'limit' in opts ? opts.limit : 30, + only: 'highlight' + }; + + if (token && token !== "end") { + params.from = token; + } + + promise = this._http.authedRequestWithPrefix(undefined, "GET", path, params, undefined, httpApi.PREFIX_UNSTABLE).then(function (res) { + var token = res.next_token; + var matrixEvents = []; + + for (var i = 0; i < res.notifications.length; i++) { + var notification = res.notifications[i]; + var event = self.getEventMapper()(notification.event); + event.setPushActions(PushProcessor.actionListToActionsObject(notification.actions)); + event.event.room_id = notification.room_id; // XXX: gutwrenching + matrixEvents[i] = event; + } + + eventTimeline.getTimelineSet().addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); + + // if we've hit the end of the timeline, we need to stop trying to + // paginate. We need to keep the 'forwards' token though, to make sure + // we can recover from gappy syncs. + if (backwards && !res.next_token) { + eventTimeline.setPaginationToken(null, dir); + } + return res.next_token ? true : false; + }).finally(function () { + eventTimeline._paginationRequests[dir] = null; + }); + eventTimeline._paginationRequests[dir] = promise; + } else { + var room = this.getRoom(eventTimeline.getRoomId()); + if (!room) { + throw new Error("Unknown room " + eventTimeline.getRoomId()); + } + + path = utils.encodeUri("/rooms/$roomId/messages", { $roomId: eventTimeline.getRoomId() }); + params = { + from: token, + limit: 'limit' in opts ? opts.limit : 30, + dir: dir + }; + + var filter = eventTimeline.getFilter(); + if (filter) { + // XXX: it's horrific that /messages' filter parameter doesn't match + // /sync's one - see https://matrix.org/jira/browse/SPEC-451 + params.filter = (0, _stringify2.default)(filter.getRoomTimelineFilterComponent()); + } + + promise = this._http.authedRequest(undefined, "GET", path, params).then(function (res) { + var token = res.end; + var matrixEvents = utils.map(res.chunk, self.getEventMapper()); + eventTimeline.getTimelineSet().addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); + + // if we've hit the end of the timeline, we need to stop trying to + // paginate. We need to keep the 'forwards' token though, to make sure + // we can recover from gappy syncs. + if (backwards && res.end == res.start) { + eventTimeline.setPaginationToken(null, dir); + } + return res.end != res.start; + }).finally(function () { + eventTimeline._paginationRequests[dir] = null; + }); + eventTimeline._paginationRequests[dir] = promise; + } + + return promise; +}; + +/** + * Reset the notifTimelineSet entirely, paginating in some historical notifs as + * a starting point for subsequent pagination. + */ +MatrixClient.prototype.resetNotifTimelineSet = function () { + if (!this._notifTimelineSet) { + return; + } + + // FIXME: This thing is a total hack, and results in duplicate events being + // added to the timeline both from /sync and /notifications, and lots of + // slow and wasteful processing and pagination. The correct solution is to + // extend /messages or /search or something to filter on notifications. + + // use the fictitious token 'end'. in practice we would ideally give it + // the oldest backwards pagination token from /sync, but /sync doesn't + // know about /notifications, so we have no choice but to start paginating + // from the current point in time. This may well overlap with historical + // notifs which are then inserted into the timeline by /sync responses. + this._notifTimelineSet.resetLiveTimeline('end', null); + + // we could try to paginate a single event at this point in order to get + // a more valid pagination token, but it just ends up with an out of order + // timeline. given what a mess this is and given we're going to have duplicate + // events anyway, just leave it with the dummy token for now. + /* + this.paginateNotifTimeline(this._notifTimelineSet.getLiveTimeline(), { + backwards: true, + limit: 1 + }); + */ +}; + +/** + * Peek into a room and receive updates about the room. This only works if the + * history visibility for the room is world_readable. + * @param {String} roomId The room to attempt to peek into. + * @return {module:client.Promise} Resolves: Room object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.peekInRoom = function (roomId) { + if (this._peekSync) { + this._peekSync.stopPeeking(); + } + this._peekSync = new SyncApi(this, this._clientOpts); + return this._peekSync.peek(roomId); +}; + +/** + * Stop any ongoing room peeking. + */ +MatrixClient.prototype.stopPeeking = function () { + if (this._peekSync) { + this._peekSync.stopPeeking(); + this._peekSync = null; + } +}; + +/** + * Set r/w flags for guest access in a room. + * @param {string} roomId The room to configure guest access in. + * @param {Object} opts Options + * @param {boolean} opts.allowJoin True to allow guests to join this room. This + * implicitly gives guests write access. If false or not given, guests are + * explicitly forbidden from joining the room. + * @param {boolean} opts.allowRead True to set history visibility to + * be world_readable. This gives guests read access *from this point forward*. + * If false or not given, history visibility is not modified. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.setGuestAccess = function (roomId, opts) { + var writePromise = this.sendStateEvent(roomId, "m.room.guest_access", { + guest_access: opts.allowJoin ? "can_join" : "forbidden" + }); + + var readPromise = _bluebird2.default.resolve(); + if (opts.allowRead) { + readPromise = this.sendStateEvent(roomId, "m.room.history_visibility", { + history_visibility: "world_readable" + }); + } + + return _bluebird2.default.all([readPromise, writePromise]); +}; + +// Registration/Login operations +// ============================= + +/** + * Requests an email verification token for the purposes of registration. + * This API proxies the Identity Server /validate/email/requestToken API, + * adding registration-specific behaviour. Specifically, if an account with + * the given email address already exists, it will either send an email + * to the address informing them of this or return M_THREEPID_IN_USE + * (which one is up to the Home Server). + * + * requestEmailToken calls the equivalent API directly on the ID server, + * therefore bypassing the registration-specific logic. + * + * Parameters and return value are as for requestEmailToken + + * @param {string} email As requestEmailToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {module:client.Promise} Resolves: As requestEmailToken + */ +MatrixClient.prototype.requestRegisterEmailToken = function (email, clientSecret, sendAttempt, nextLink) { + return this._requestTokenFromEndpoint("/register/email/requestToken", { + email: email, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink + }); +}; + +/** + * Requests a text message verification token for the purposes of registration. + * This API proxies the Identity Server /validate/msisdn/requestToken API, + * adding registration-specific behaviour, as with requestRegisterEmailToken. + * + * @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in which + * phoneNumber should be parsed relative to. + * @param {string} phoneNumber The phone number, in national or international format + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {module:client.Promise} Resolves: As requestEmailToken + */ +MatrixClient.prototype.requestRegisterMsisdnToken = function (phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink) { + return this._requestTokenFromEndpoint("/register/msisdn/requestToken", { + country: phoneCountry, + phone_number: phoneNumber, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink + }); +}; + +/** + * Requests an email verification token for the purposes of adding a + * third party identifier to an account. + * This API proxies the Identity Server /validate/email/requestToken API, + * adding specific behaviour for the addition of email addresses to an + * account. Specifically, if an account with + * the given email address already exists, it will either send an email + * to the address informing them of this or return M_THREEPID_IN_USE + * (which one is up to the Home Server). + * + * requestEmailToken calls the equivalent API directly on the ID server, + * therefore bypassing the email addition specific logic. + * + * @param {string} email As requestEmailToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {module:client.Promise} Resolves: As requestEmailToken + */ +MatrixClient.prototype.requestAdd3pidEmailToken = function (email, clientSecret, sendAttempt, nextLink) { + return this._requestTokenFromEndpoint("/account/3pid/email/requestToken", { + email: email, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink + }); +}; + +/** + * Requests a text message verification token for the purposes of adding a + * third party identifier to an account. + * This API proxies the Identity Server /validate/email/requestToken API, + * adding specific behaviour for the addition of phone numbers to an + * account, as requestAdd3pidEmailToken. + * + * @param {string} phoneCountry As requestRegisterMsisdnToken + * @param {string} phoneNumber As requestRegisterMsisdnToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {module:client.Promise} Resolves: As requestEmailToken + */ +MatrixClient.prototype.requestAdd3pidMsisdnToken = function (phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink) { + return this._requestTokenFromEndpoint("/account/3pid/msisdn/requestToken", { + country: phoneCountry, + phone_number: phoneNumber, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink + }); +}; + +/** + * Requests an email verification token for the purposes of resetting + * the password on an account. + * This API proxies the Identity Server /validate/email/requestToken API, + * adding specific behaviour for the password resetting. Specifically, + * if no account with the given email address exists, it may either + * return M_THREEPID_NOT_FOUND or send an email + * to the address informing them of this (which one is up to the Home Server). + * + * requestEmailToken calls the equivalent API directly on the ID server, + * therefore bypassing the password reset specific logic. + * + * @param {string} email As requestEmailToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @param {module:client.callback} callback Optional. As requestEmailToken + * @return {module:client.Promise} Resolves: As requestEmailToken + */ +MatrixClient.prototype.requestPasswordEmailToken = function (email, clientSecret, sendAttempt, nextLink) { + return this._requestTokenFromEndpoint("/account/password/email/requestToken", { + email: email, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink + }); +}; + +/** + * Requests a text message verification token for the purposes of resetting + * the password on an account. + * This API proxies the Identity Server /validate/email/requestToken API, + * adding specific behaviour for the password resetting, as requestPasswordEmailToken. + * + * @param {string} phoneCountry As requestRegisterMsisdnToken + * @param {string} phoneNumber As requestRegisterMsisdnToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {module:client.Promise} Resolves: As requestEmailToken + */ +MatrixClient.prototype.requestPasswordMsisdnToken = function (phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink) { + return this._requestTokenFromEndpoint("/account/password/msisdn/requestToken", { + country: phoneCountry, + phone_number: phoneNumber, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink + }); +}; + +/** + * Internal utility function for requesting validation tokens from usage-specific + * requestToken endpoints. + * + * @param {string} endpoint The endpoint to send the request to + * @param {object} params Parameters for the POST request + * @return {module:client.Promise} Resolves: As requestEmailToken + */ +MatrixClient.prototype._requestTokenFromEndpoint = function (endpoint, params) { + var id_server_url = url.parse(this.idBaseUrl); + if (id_server_url.host === null) { + throw new Error("Invalid ID server URL: " + this.idBaseUrl); + } + + var postParams = (0, _assign2.default)({}, params, { + id_server: id_server_url.host + }); + return this._http.request(undefined, "POST", endpoint, undefined, postParams); +}; + +// Push operations +// =============== + +/** + * Get the room-kind push rule associated with a room. + * @param {string} scope "global" or device-specific. + * @param {string} roomId the id of the room. + * @return {object} the rule or undefined. + */ +MatrixClient.prototype.getRoomPushRule = function (scope, roomId) { + // There can be only room-kind push rule per room + // and its id is the room id. + if (this.pushRules) { + for (var i = 0; i < this.pushRules[scope].room.length; i++) { + var rule = this.pushRules[scope].room[i]; + if (rule.rule_id === roomId) { + return rule; + } + } + } else { + throw new Error("SyncApi.sync() must be done before accessing to push rules."); + } +}; + +/** + * Set a room-kind muting push rule in a room. + * The operation also updates MatrixClient.pushRules at the end. + * @param {string} scope "global" or device-specific. + * @param {string} roomId the id of the room. + * @param {string} mute the mute state. + * @return {module:client.Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.setRoomMutePushRule = function (scope, roomId, mute) { + var self = this; + var deferred = void 0, + hasDontNotifyRule = void 0; + + // Get the existing room-kind push rule if any + var roomPushRule = this.getRoomPushRule(scope, roomId); + if (roomPushRule) { + if (0 <= roomPushRule.actions.indexOf("dont_notify")) { + hasDontNotifyRule = true; + } + } + + if (!mute) { + // Remove the rule only if it is a muting rule + if (hasDontNotifyRule) { + deferred = this.deletePushRule(scope, "room", roomPushRule.rule_id); + } + } else { + if (!roomPushRule) { + deferred = this.addPushRule(scope, "room", roomId, { + actions: ["dont_notify"] + }); + } else if (!hasDontNotifyRule) { + // Remove the existing one before setting the mute push rule + // This is a workaround to SYN-590 (Push rule update fails) + deferred = _bluebird2.default.defer(); + this.deletePushRule(scope, "room", roomPushRule.rule_id).done(function () { + self.addPushRule(scope, "room", roomId, { + actions: ["dont_notify"] + }).done(function () { + deferred.resolve(); + }, function (err) { + deferred.reject(err); + }); + }, function (err) { + deferred.reject(err); + }); + + deferred = deferred.promise; + } + } + + if (deferred) { + // Update this.pushRules when the operation completes + var ruleRefreshDeferred = _bluebird2.default.defer(); + deferred.done(function () { + self.getPushRules().done(function (result) { + self.pushRules = result; + ruleRefreshDeferred.resolve(); + }, function (err) { + ruleRefreshDeferred.reject(err); + }); + }, function (err) { + // Update it even if the previous operation fails. This can help the + // app to recover when push settings has been modifed from another client + self.getPushRules().done(function (result) { + self.pushRules = result; + ruleRefreshDeferred.reject(err); + }, function (err2) { + ruleRefreshDeferred.reject(err); + }); + }); + return ruleRefreshDeferred.promise; + } +}; + +// Search +// ====== + +/** + * Perform a server-side search for messages containing the given text. + * @param {Object} opts Options for the search. + * @param {string} opts.query The text to query. + * @param {string=} opts.keys The keys to search on. Defaults to all keys. One + * of "content.body", "content.name", "content.topic". + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.searchMessageText = function (opts, callback) { + var roomEvents = { + search_term: opts.query + }; + + if ('keys' in opts) { + roomEvents.keys = opts.keys; + } + + return this.search({ + body: { + search_categories: { + room_events: roomEvents + } + } + }, callback); +}; + +/** + * Perform a server-side search for room events. + * + * The returned promise resolves to an object containing the fields: + * + * * {number} count: estimate of the number of results + * * {string} next_batch: token for back-pagination; if undefined, there are + * no more results + * * {Array} highlights: a list of words to highlight from the stemming + * algorithm + * * {Array} results: a list of results + * + * Each entry in the results list is a {module:models/search-result.SearchResult}. + * + * @param {Object} opts + * @param {string} opts.term the term to search for + * @param {Object} opts.filter a JSON filter object to pass in the request + * @return {module:client.Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.searchRoomEvents = function (opts) { + // TODO: support groups + + var body = { + search_categories: { + room_events: { + search_term: opts.term, + filter: opts.filter, + order_by: "recent", + event_context: { + before_limit: 1, + after_limit: 1, + include_profile: true + } + } + } + }; + + var searchResults = { + _query: body, + results: [], + highlights: [] + }; + + return this.search({ body: body }).then(this._processRoomEventsSearch.bind(this, searchResults)); +}; + +/** + * Take a result from an earlier searchRoomEvents call, and backfill results. + * + * @param {object} searchResults the results object to be updated + * @return {module:client.Promise} Resolves: updated result object + * @return {Error} Rejects: with an error response. + */ +MatrixClient.prototype.backPaginateRoomEventsSearch = function (searchResults) { + // TODO: we should implement a backoff (as per scrollback()) to deal more + // nicely with HTTP errors. + + if (!searchResults.next_batch) { + return _bluebird2.default.reject(new Error("Cannot backpaginate event search any further")); + } + + if (searchResults.pendingRequest) { + // already a request in progress - return the existing promise + return searchResults.pendingRequest; + } + + var searchOpts = { + body: searchResults._query, + next_batch: searchResults.next_batch + }; + + var promise = this.search(searchOpts).then(this._processRoomEventsSearch.bind(this, searchResults)).finally(function () { + searchResults.pendingRequest = null; + }); + searchResults.pendingRequest = promise; + + return promise; +}; + +/** + * helper for searchRoomEvents and backPaginateRoomEventsSearch. Processes the + * response from the API call and updates the searchResults + * + * @param {Object} searchResults + * @param {Object} response + * @return {Object} searchResults + * @private + */ +MatrixClient.prototype._processRoomEventsSearch = function (searchResults, response) { + var room_events = response.search_categories.room_events; + + searchResults.count = room_events.count; + searchResults.next_batch = room_events.next_batch; + + // combine the highlight list with our existing list; build an object + // to avoid O(N^2) fail + var highlights = {}; + room_events.highlights.forEach(function (hl) { + highlights[hl] = 1; + }); + searchResults.highlights.forEach(function (hl) { + highlights[hl] = 1; + }); + + // turn it back into a list. + searchResults.highlights = (0, _keys2.default)(highlights); + + // append the new results to our existing results + for (var i = 0; i < room_events.results.length; i++) { + var sr = SearchResult.fromJson(room_events.results[i], this.getEventMapper()); + searchResults.results.push(sr); + } + return searchResults; +}; + +/** + * Populate the store with rooms the user has left. + * @return {module:client.Promise} Resolves: TODO - Resolved when the rooms have + * been added to the data store. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.syncLeftRooms = function () { + // Guard against multiple calls whilst ongoing and multiple calls post success + if (this._syncedLeftRooms) { + return _bluebird2.default.resolve([]); // don't call syncRooms again if it succeeded. + } + if (this._syncLeftRoomsPromise) { + return this._syncLeftRoomsPromise; // return the ongoing request + } + var self = this; + var syncApi = new SyncApi(this, this._clientOpts); + this._syncLeftRoomsPromise = syncApi.syncLeftRooms(); + + // cleanup locks + this._syncLeftRoomsPromise.then(function (res) { + console.log("Marking success of sync left room request"); + self._syncedLeftRooms = true; // flip the bit on success + }).finally(function () { + self._syncLeftRoomsPromise = null; // cleanup ongoing request state + }); + + return this._syncLeftRoomsPromise; +}; + +// Filters +// ======= + +/** + * Create a new filter. + * @param {Object} content The HTTP body for the request + * @return {Filter} Resolves to a Filter object. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.createFilter = function (content) { + var self = this; + var path = utils.encodeUri("/user/$userId/filter", { + $userId: this.credentials.userId + }); + return this._http.authedRequest(undefined, "POST", path, undefined, content).then(function (response) { + // persist the filter + var filter = Filter.fromJson(self.credentials.userId, response.filter_id, content); + self.store.storeFilter(filter); + return filter; + }); +}; + +/** + * Retrieve a filter. + * @param {string} userId The user ID of the filter owner + * @param {string} filterId The filter ID to retrieve + * @param {boolean} allowCached True to allow cached filters to be returned. + * Default: True. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.getFilter = function (userId, filterId, allowCached) { + if (allowCached) { + var filter = this.store.getFilter(userId, filterId); + if (filter) { + return _bluebird2.default.resolve(filter); + } + } + + var self = this; + var path = utils.encodeUri("/user/$userId/filter/$filterId", { + $userId: userId, + $filterId: filterId + }); + + return this._http.authedRequest(undefined, "GET", path, undefined, undefined).then(function (response) { + // persist the filter + var filter = Filter.fromJson(userId, filterId, response); + self.store.storeFilter(filter); + return filter; + }); +}; + +/** + * @param {string} filterName + * @param {Filter} filter + * @return {Promise} Filter ID + */ +MatrixClient.prototype.getOrCreateFilter = function (filterName, filter) { + var filterId = this.store.getFilterIdByName(filterName); + var promise = _bluebird2.default.resolve(); + var self = this; + + if (filterId) { + // check that the existing filter matches our expectations + promise = self.getFilter(self.credentials.userId, filterId, true).then(function (existingFilter) { + var oldDef = existingFilter.getDefinition(); + var newDef = filter.getDefinition(); + + if (utils.deepCompare(oldDef, newDef)) { + // super, just use that. + // debuglog("Using existing filter ID %s: %s", filterId, + // JSON.stringify(oldDef)); + return _bluebird2.default.resolve(filterId); + } + // debuglog("Existing filter ID %s: %s; new filter: %s", + // filterId, JSON.stringify(oldDef), JSON.stringify(newDef)); + self.store.setFilterIdByName(filterName, undefined); + return undefined; + }, function (error) { + // Synapse currently returns the following when the filter cannot be found: + // { + // errcode: "M_UNKNOWN", + // name: "M_UNKNOWN", + // message: "No row found", + // data: Object, httpStatus: 404 + // } + if (error.httpStatus === 404 && (error.errcode === "M_UNKNOWN" || error.errcode === "M_NOT_FOUND")) { + // Clear existing filterId from localStorage + // if it no longer exists on the server + self.store.setFilterIdByName(filterName, undefined); + // Return a undefined value for existingId further down the promise chain + return undefined; + } else { + throw error; + } + }); + } + + return promise.then(function (existingId) { + if (existingId) { + return existingId; + } + + // create a new filter + return self.createFilter(filter.getDefinition()).then(function (createdFilter) { + // debuglog("Created new filter ID %s: %s", createdFilter.filterId, + // JSON.stringify(createdFilter.getDefinition())); + self.store.setFilterIdByName(filterName, createdFilter.filterId); + return createdFilter.filterId; + }); + }); +}; + +/** + * Gets a bearer token from the Home Server that the user can + * present to a third party in order to prove their ownership + * of the Matrix account they are logged into. + * @return {module:client.Promise} Resolves: Token object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.getOpenIdToken = function () { + var path = utils.encodeUri("/user/$userId/openid/request_token", { + $userId: this.credentials.userId + }); + + return this._http.authedRequest(undefined, "POST", path, undefined, {}); +}; + +// VoIP operations +// =============== + +/** + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.turnServer = function (callback) { + return this._http.authedRequest(callback, "GET", "/voip/turnServer"); +}; + +/** + * Get the TURN servers for this home server. + * @return {Array} The servers or an empty list. + */ +MatrixClient.prototype.getTurnServers = function () { + return this._turnServers || []; +}; + +// Higher level APIs +// ================= + +// TODO: stuff to handle: +// local echo +// event dup suppression? - apparently we should still be doing this +// tracking current display name / avatar per-message +// pagination +// re-sending (including persisting pending messages to be sent) +// - Need a nice way to callback the app for arbitrary events like +// displayname changes +// due to ambiguity (or should this be on a chat-specific layer)? +// reconnect after connectivity outages + + +/** + * High level helper method to begin syncing and poll for new events. To listen for these + * events, add a listener for {@link module:client~MatrixClient#event:"event"} + * via {@link module:client~MatrixClient#on}. Alternatively, listen for specific + * state change events. + * @param {Object=} opts Options to apply when syncing. + * @param {Number=} opts.initialSyncLimit The event limit= to apply + * to initial sync. Default: 8. + * @param {Boolean=} opts.includeArchivedRooms True to put archived=true + * on the /initialSync request. Default: false. + * @param {Boolean=} opts.resolveInvitesToProfiles True to do /profile requests + * on every invite event if the displayname/avatar_url is not known for this user ID. + * Default: false. + * + * @param {String=} opts.pendingEventOrdering Controls where pending messages + * appear in a room's timeline. If "chronological", messages will appear + * in the timeline when the call to sendEvent was made. If + * "detached", pending messages will appear in a separate list, + * accessbile via {@link module:models/room#getPendingEvents}. Default: + * "chronological". + * + * @param {Number=} opts.pollTimeout The number of milliseconds to wait on /sync. + * Default: 30000 (30 seconds). + * + * @param {Filter=} opts.filter The filter to apply to /sync calls. This will override + * the opts.initialSyncLimit, which would normally result in a timeline limit filter. + * + * @param {Boolean=} opts.disablePresence True to perform syncing without automatically + * updating presence. + */ +MatrixClient.prototype.startClient = function (opts) { + var _this = this; + + if (this.clientRunning) { + // client is already running. + return; + } + this.clientRunning = true; + // backwards compat for when 'opts' was 'historyLen'. + if (typeof opts === "number") { + opts = { + initialSyncLimit: opts + }; + } + + if (this._crypto) { + this._crypto.uploadDeviceKeys().done(); + this._crypto.start(); + } + + // periodically poll for turn servers if we support voip + checkTurnServers(this); + + if (this._syncApi) { + // This shouldn't happen since we thought the client was not running + console.error("Still have sync object whilst not running: stopping old one"); + this._syncApi.stop(); + } + + // shallow-copy the opts dict before modifying and storing it + opts = (0, _assign2.default)({}, opts); + + opts.crypto = this._crypto; + opts.canResetEntireTimeline = function (roomId) { + if (!_this._canResetTimelineCallback) { + return false; + } + return _this._canResetTimelineCallback(roomId); + }; + this._clientOpts = opts; + + this._syncApi = new SyncApi(this, opts); + this._syncApi.sync(); +}; + +/** + * High level helper method to stop the client from polling and allow a + * clean shutdown. + */ +MatrixClient.prototype.stopClient = function () { + console.log('stopping MatrixClient'); + + this.clientRunning = false; + // TODO: f.e. Room => self.store.storeRoom(room) ? + if (this._syncApi) { + this._syncApi.stop(); + this._syncApi = null; + } + if (this._crypto) { + this._crypto.stop(); + } + if (this._peekSync) { + this._peekSync.stopPeeking(); + } + global.clearTimeout(this._checkTurnServersTimeoutID); +}; + +/* + * Set a function which is called when /sync returns a 'limited' response. + * It is called with a room ID and returns a boolean. It should return 'true' if the SDK + * can SAFELY remove events from this room. It may not be safe to remove events if there + * are other references to the timelines for this room, e.g because the client is + * actively viewing events in this room. + * Default: returns false. + * @param {Function} cb The callback which will be invoked. + */ +MatrixClient.prototype.setCanResetTimelineCallback = function (cb) { + this._canResetTimelineCallback = cb; +}; + +/** + * Get the callback set via `setCanResetTimelineCallback`. + * @return {?Function} The callback or null + */ +MatrixClient.prototype.getCanResetTimelineCallback = function () { + return this._canResetTimelineCallback; +}; + +function setupCallEventHandler(client) { + var candidatesByCall = { + // callId: [Candidate] + }; + + // Maintain a buffer of events before the client has synced for the first time. + // This buffer will be inspected to see if we should send incoming call + // notifications. It needs to be buffered to correctly determine if an + // incoming call has had a matching answer/hangup. + var callEventBuffer = []; + var isClientPrepared = false; + client.on("sync", function (state) { + if (state === "PREPARED") { + isClientPrepared = true; + var ignoreCallIds = {}; // Set + // inspect the buffer and mark all calls which have been answered + // or hung up before passing them to the call event handler. + for (var i = callEventBuffer.length - 1; i >= 0; i--) { + var ev = callEventBuffer[i]; + if (ev.getType() === "m.call.answer" || ev.getType() === "m.call.hangup") { + ignoreCallIds[ev.getContent().call_id] = "yep"; + } + } + // now loop through the buffer chronologically and inject them + callEventBuffer.forEach(function (e) { + if (ignoreCallIds[e.getContent().call_id]) { + // This call has previously been ansered or hung up: ignore it + return; + } + callEventHandler(e); + }); + callEventBuffer = []; + } + }); + + client.on("event", onEvent); + + function onEvent(event) { + if (event.getType().indexOf("m.call.") !== 0) { + // not a call event + if (event.isBeingDecrypted() || event.isDecryptionFailure()) { + // not *yet* a call event, but might become one... + event.once("Event.decrypted", onEvent); + } + return; + } + if (!isClientPrepared) { + callEventBuffer.push(event); + return; + } + callEventHandler(event); + } + + function callEventHandler(event) { + var content = event.getContent(); + var call = content.call_id ? client.callList[content.call_id] : undefined; + var i = void 0; + //console.log("RECV %s content=%s", event.getType(), JSON.stringify(content)); + + if (event.getType() === "m.call.invite") { + if (event.getSender() === client.credentials.userId) { + return; // ignore invites you send + } + + if (event.getAge() > content.lifetime) { + return; // expired call + } + + if (call && call.state === "ended") { + return; // stale/old invite event + } + if (call) { + console.log("WARN: Already have a MatrixCall with id %s but got an " + "invite. Clobbering.", content.call_id); + } + + call = webRtcCall.createNewMatrixCall(client, event.getRoomId(), { + forceTURN: client._forceTURN + }); + if (!call) { + console.log("Incoming call ID " + content.call_id + " but this client " + "doesn't support WebRTC"); + // don't hang up the call: there could be other clients + // connected that do support WebRTC and declining the + // the call on their behalf would be really annoying. + return; + } + + call.callId = content.call_id; + call._initWithInvite(event); + client.callList[call.callId] = call; + + // if we stashed candidate events for that call ID, play them back now + if (candidatesByCall[call.callId]) { + for (i = 0; i < candidatesByCall[call.callId].length; i++) { + call._gotRemoteIceCandidate(candidatesByCall[call.callId][i]); + } + } + + // Were we trying to call that user (room)? + var existingCall = void 0; + var existingCalls = utils.values(client.callList); + for (i = 0; i < existingCalls.length; ++i) { + var thisCall = existingCalls[i]; + if (call.roomId === thisCall.roomId && thisCall.direction === 'outbound' && ["wait_local_media", "create_offer", "invite_sent"].indexOf(thisCall.state) !== -1) { + existingCall = thisCall; + break; + } + } + + if (existingCall) { + // If we've only got to wait_local_media or create_offer and + // we've got an invite, pick the incoming call because we know + // we haven't sent our invite yet otherwise, pick whichever + // call has the lowest call ID (by string comparison) + if (existingCall.state === 'wait_local_media' || existingCall.state === 'create_offer' || existingCall.callId > call.callId) { + console.log("Glare detected: answering incoming call " + call.callId + " and canceling outgoing call " + existingCall.callId); + existingCall._replacedBy(call); + call.answer(); + } else { + console.log("Glare detected: rejecting incoming call " + call.callId + " and keeping outgoing call " + existingCall.callId); + call.hangup(); + } + } else { + client.emit("Call.incoming", call); + } + } else if (event.getType() === 'm.call.answer') { + if (!call) { + return; + } + if (event.getSender() === client.credentials.userId) { + if (call.state === 'ringing') { + call._onAnsweredElsewhere(content); + } + } else { + call._receivedAnswer(content); + } + } else if (event.getType() === 'm.call.candidates') { + if (event.getSender() === client.credentials.userId) { + return; + } + if (!call) { + // store the candidates; we may get a call eventually. + if (!candidatesByCall[content.call_id]) { + candidatesByCall[content.call_id] = []; + } + candidatesByCall[content.call_id] = candidatesByCall[content.call_id].concat(content.candidates); + } else { + for (i = 0; i < content.candidates.length; i++) { + call._gotRemoteIceCandidate(content.candidates[i]); + } + } + } else if (event.getType() === 'm.call.hangup') { + // Note that we also observe our own hangups here so we can see + // if we've already rejected a call that would otherwise be valid + if (!call) { + // if not live, store the fact that the call has ended because + // we're probably getting events backwards so + // the hangup will come before the invite + call = webRtcCall.createNewMatrixCall(client, event.getRoomId()); + if (call) { + call.callId = content.call_id; + call._initWithHangup(event); + client.callList[content.call_id] = call; + } + } else { + if (call.state !== 'ended') { + call._onHangupReceived(content); + delete client.callList[content.call_id]; + } + } + } + } +} + +function checkTurnServers(client) { + if (!client._supportsVoip) { + return; + } + if (client.isGuest()) { + return; // guests can't access TURN servers + } + + client.turnServer().done(function (res) { + if (res.uris) { + console.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs"); + // map the response to a format that can be fed to + // RTCPeerConnection + var servers = { + urls: res.uris, + username: res.username, + credential: res.password + }; + client._turnServers = [servers]; + // re-fetch when we're about to reach the TTL + client._checkTurnServersTimeoutID = setTimeout(function () { + checkTurnServers(client); + }, (res.ttl || 60 * 60) * 1000 * 0.9); + } + }, function (err) { + console.error("Failed to get TURN URIs"); + client._checkTurnServersTimeoutID = setTimeout(function () { + checkTurnServers(client); + }, 60000); + }); +} + +function _reject(callback, defer, err) { + if (callback) { + callback(err); + } + defer.reject(err); +} + +function _resolve(callback, defer, res) { + if (callback) { + callback(null, res); + } + defer.resolve(res); +} + +function _PojoToMatrixEventMapper(client) { + function mapper(plainOldJsObject) { + var event = new MatrixEvent(plainOldJsObject); + if (event.isEncrypted()) { + client.reEmitter.reEmit(event, ["Event.decrypted"]); + event.attemptDecryption(client._crypto); + } + return event; + } + return mapper; +} + +/** + * @return {Function} + */ +MatrixClient.prototype.getEventMapper = function () { + return _PojoToMatrixEventMapper(this); +}; + +// Identity Server Operations +// ========================== + +/** + * Generates a random string suitable for use as a client secret. This + * method is experimental and may change. + * @return {string} A new client secret + */ +MatrixClient.prototype.generateClientSecret = function () { + var ret = ""; + var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for (var i = 0; i < 32; i++) { + ret += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return ret; +}; + +/** */ +module.exports.MatrixClient = MatrixClient; +/** */ +module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; + +// MatrixClient Event JSDocs + +/** + * Fires whenever the SDK receives a new event. + *

+ * This is only fired for live events received via /sync - it is not fired for + * events received over context, search, or pagination APIs. + * + * @event module:client~MatrixClient#"event" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @example + * matrixClient.on("event", function(event){ + * var sender = event.getSender(); + * }); + */ + +/** + * Fires whenever the SDK receives a new to-device event. + * @event module:client~MatrixClient#"toDeviceEvent" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @example + * matrixClient.on("toDeviceEvent", function(event){ + * var sender = event.getSender(); + * }); + */ + +/** + * Fires whenever the SDK's syncing state is updated. The state can be one of: + *

    + * + *
  • PREPARED: The client has synced with the server at least once and is + * ready for methods to be called on it. This will be immediately followed by + * a state of SYNCING. This is the equivalent of "syncComplete" in the + * previous API.
  • + * + *
  • SYNCING : The client is currently polling for new events from the server. + * This will be called after processing latest events from a sync.
  • + * + *
  • ERROR : The client has had a problem syncing with the server. If this is + * called before PREPARED then there was a problem performing the initial + * sync. If this is called after PREPARED then there was a problem polling + * the server for updates. This may be called multiple times even if the state is + * already ERROR. This is the equivalent of "syncError" in the previous + * API.
  • + * + *
  • RECONNECTING: The sync connection has dropped, but not (yet) in a way that + * should be considered erroneous. + *
  • + * + *
  • STOPPED: The client has stopped syncing with server due to stopClient + * being called. + *
  • + *
+ * State transition diagram: + *
+ *                                          +---->STOPPED
+ *                                          |
+ *              +----->PREPARED -------> SYNCING <--+
+ *              |        ^                |  ^      |
+ *              |        |                |  |      |
+ *              |        |                V  |      |
+ *   null ------+        |  +--------RECONNECTING   |
+ *              |        |  V                       |
+ *              +------->ERROR ---------------------+
+ *
+ * NB: 'null' will never be emitted by this event.
+ *
+ * 
+ * Transitions: + *
    + * + *
  • null -> PREPARED : Occurs when the initial sync is completed + * first time. This involves setting up filters and obtaining push rules. + * + *
  • null -> ERROR : Occurs when the initial sync failed first time. + * + *
  • ERROR -> PREPARED : Occurs when the initial sync succeeds + * after previously failing. + * + *
  • PREPARED -> SYNCING : Occurs immediately after transitioning + * to PREPARED. Starts listening for live updates rather than catching up. + * + *
  • SYNCING -> RECONNECTING : Occurs when the live update fails. + * + *
  • RECONNECTING -> RECONNECTING : Can occur if the update calls + * continue to fail, but the keepalive calls (to /versions) succeed. + * + *
  • RECONNECTING -> ERROR : Occurs when the keepalive call also fails + * + *
  • ERROR -> SYNCING : Occurs when the client has performed a + * live update after having previously failed. + * + *
  • ERROR -> ERROR : Occurs when the client has failed to keepalive + * for a second time or more.
  • + * + *
  • SYNCING -> SYNCING : Occurs when the client has performed a live + * update. This is called after processing.
  • + * + *
  • * -> STOPPED : Occurs once the client has stopped syncing or + * trying to sync after stopClient has been called.
  • + *
+ * + * @event module:client~MatrixClient#"sync" + * + * @param {string} state An enum representing the syncing state. One of "PREPARED", + * "SYNCING", "ERROR", "STOPPED". + * + * @param {?string} prevState An enum representing the previous syncing state. + * One of "PREPARED", "SYNCING", "ERROR", "STOPPED" or null. + * + * @param {?Object} data Data about this transition. + * + * @param {MatrixError} data.err The matrix error if state=ERROR. + * + * @param {String} data.oldSyncToken The 'since' token passed to /sync. + * null for the first successful sync since this client was + * started. Only present if state=PREPARED or + * state=SYNCING. + * + * @param {String} data.nextSyncToken The 'next_batch' result from /sync, which + * will become the 'since' token for the next call to /sync. Only present if + * state=PREPARED or state=SYNCING. + * + * @param {boolean} data.catchingUp True if we are working our way through a + * backlog of events after connecting. Only present if state=SYNCING. + * + * @example + * matrixClient.on("sync", function(state, prevState, data) { + * switch (state) { + * case "ERROR": + * // update UI to say "Connection Lost" + * break; + * case "SYNCING": + * // update UI to remove any "Connection Lost" message + * break; + * case "PREPARED": + * // the client instance is ready to be queried. + * var rooms = matrixClient.getRooms(); + * break; + * } + * }); + */ + +/** +* Fires whenever the sdk learns about a new group. This event +* is experimental and may change. +* @event module:client~MatrixClient#"Group" +* @param {Group} group The newly created, fully populated group. +* @example +* matrixClient.on("Group", function(group){ +* var groupId = group.groupId; +* }); +*/ + +/** +* Fires whenever a new Room is added. This will fire when you are invited to a +* room, as well as when you join a room. This event is experimental and +* may change. +* @event module:client~MatrixClient#"Room" +* @param {Room} room The newly created, fully populated room. +* @example +* matrixClient.on("Room", function(room){ +* var roomId = room.roomId; +* }); +*/ + +/** +* Fires whenever a Room is removed. This will fire when you forget a room. +* This event is experimental and may change. +* @event module:client~MatrixClient#"deleteRoom" +* @param {string} roomId The deleted room ID. +* @example +* matrixClient.on("deleteRoom", function(roomId){ +* // update UI from getRooms() +* }); +*/ + +/** + * Fires whenever an incoming call arrives. + * @event module:client~MatrixClient#"Call.incoming" + * @param {module:webrtc/call~MatrixCall} call The incoming call. + * @example + * matrixClient.on("Call.incoming", function(call){ + * call.answer(); // auto-answer + * }); + */ + +/** + * Fires whenever the login session the JS SDK is using is no + * longer valid and the user must log in again. + * NB. This only fires when action is required from the user, not + * when then login session can be renewed by using a refresh token. + * @event module:client~MatrixClient#"Session.logged_out" + * @example + * matrixClient.on("Session.logged_out", function(call){ + * // show the login screen + * }); + */ + +/** + * Fires when the JS SDK receives a M_CONSENT_NOT_GIVEN error in response + * to a HTTP request. + * @event module:client~MatrixClient#"no_consent" + * @example + * matrixClient.on("no_consent", function(message, contentUri) { + * console.info(message + ' Go to ' + contentUri); + * }); + */ + +/** + * Fires when a device is marked as verified/unverified/blocked/unblocked by + * {@link module:client~MatrixClient#setDeviceVerified|MatrixClient.setDeviceVerified} or + * {@link module:client~MatrixClient#setDeviceBlocked|MatrixClient.setDeviceBlocked}. + * + * @event module:client~MatrixClient#"deviceVerificationChanged" + * @param {string} userId the owner of the verified device + * @param {string} deviceId the id of the verified device + * @param {module:crypto/deviceinfo} deviceInfo updated device information + */ + +/** + * Fires whenever new user-scoped account_data is added. + * @event module:client~MatrixClient#"accountData" + * @param {MatrixEvent} event The event describing the account_data just added + * @example + * matrixClient.on("accountData", function(event){ + * myAccountData[event.type] = event.content; + * }); + */ + +// EventEmitter JSDocs + +/** + * The {@link https://nodejs.org/api/events.html|EventEmitter} class. + * @external EventEmitter + * @see {@link https://nodejs.org/api/events.html} + */ + +/** + * Adds a listener to the end of the listeners array for the specified event. + * No checks are made to see if the listener has already been added. Multiple + * calls passing the same combination of event and listener will result in the + * listener being added multiple times. + * @function external:EventEmitter#on + * @param {string} event The event to listen for. + * @param {Function} listener The function to invoke. + * @return {EventEmitter} for call chaining. + */ + +/** + * Alias for {@link external:EventEmitter#on}. + * @function external:EventEmitter#addListener + * @param {string} event The event to listen for. + * @param {Function} listener The function to invoke. + * @return {EventEmitter} for call chaining. + */ + +/** + * Adds a one time listener for the event. This listener is invoked only + * the next time the event is fired, after which it is removed. + * @function external:EventEmitter#once + * @param {string} event The event to listen for. + * @param {Function} listener The function to invoke. + * @return {EventEmitter} for call chaining. + */ + +/** + * Remove a listener from the listener array for the specified event. + * Caution: changes array indices in the listener array behind the + * listener. + * @function external:EventEmitter#removeListener + * @param {string} event The event to listen for. + * @param {Function} listener The function to invoke. + * @return {EventEmitter} for call chaining. + */ + +/** + * Removes all listeners, or those of the specified event. It's not a good idea + * to remove listeners that were added elsewhere in the code, especially when + * it's on an emitter that you didn't create (e.g. sockets or file streams). + * @function external:EventEmitter#removeAllListeners + * @param {string} event Optional. The event to remove listeners for. + * @return {EventEmitter} for call chaining. + */ + +/** + * Execute each of the listeners in order with the supplied arguments. + * @function external:EventEmitter#emit + * @param {string} event The event to emit. + * @param {Function} listener The function to invoke. + * @return {boolean} true if event had listeners, false otherwise. + */ + +/** + * By default EventEmitters will print a warning if more than 10 listeners are + * added for a particular event. This is a useful default which helps finding + * memory leaks. Obviously not all Emitters should be limited to 10. This + * function allows that to be increased. Set to zero for unlimited. + * @function external:EventEmitter#setMaxListeners + * @param {Number} n The max number of listeners. + * @return {EventEmitter} for call chaining. + */ + +// MatrixClient Callback JSDocs + +/** + * The standard MatrixClient callback interface. Functions which accept this + * will specify 2 return arguments. These arguments map to the 2 parameters + * specified in this callback. + * @callback module:client.callback + * @param {Object} err The error value, the "rejected" value or null. + * @param {Object} data The data returned, the "resolved" value. + */ + +/** + * {@link https://github.com/kriskowal/q|A promise implementation (Q)}. Functions + * which return this will specify 2 return arguments. These arguments map to the + * "onFulfilled" and "onRejected" values of the Promise. + * @typedef {Object} Promise + * @static + * @property {Function} then promise.then(onFulfilled, onRejected, onProgress) + * @property {Function} catch promise.catch(onRejected) + * @property {Function} finally promise.finally(callback) + * @property {Function} done promise.done(onFulfilled, onRejected, onProgress) + */ + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"./ReEmitter":2,"./base-apis":3,"./content-helpers":5,"./content-repo":6,"./crypto":16,"./crypto/RoomList":10,"./filter":23,"./http-api":24,"./models/event":30,"./models/event-timeline":29,"./models/search-result":36,"./pushprocessor":38,"./store/stub":46,"./sync":48,"./utils":50,"./webrtc/call":51,"babel-runtime/core-js/json/stringify":55,"babel-runtime/core-js/object/assign":56,"babel-runtime/core-js/object/keys":61,"babel-runtime/regenerator":73,"bluebird":74,"events":186,"url":195}],5:[function(require,module,exports){ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** @module ContentHelpers */ + +module.exports = { + /** + * Generates the content for a HTML Message event + * @param {string} body the plaintext body of the message + * @param {string} htmlBody the HTML representation of the message + * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + */ + makeHtmlMessage: function makeHtmlMessage(body, htmlBody) { + return { + msgtype: "m.text", + format: "org.matrix.custom.html", + body: body, + formatted_body: htmlBody + }; + }, + + /** + * Generates the content for a HTML Notice event + * @param {string} body the plaintext body of the notice + * @param {string} htmlBody the HTML representation of the notice + * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + */ + makeHtmlNotice: function makeHtmlNotice(body, htmlBody) { + return { + msgtype: "m.notice", + format: "org.matrix.custom.html", + body: body, + formatted_body: htmlBody + }; + }, + + /** + * Generates the content for a HTML Emote event + * @param {string} body the plaintext body of the emote + * @param {string} htmlBody the HTML representation of the emote + * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + */ + makeHtmlEmote: function makeHtmlEmote(body, htmlBody) { + return { + msgtype: "m.emote", + format: "org.matrix.custom.html", + body: body, + formatted_body: htmlBody + }; + }, + + /** + * Generates the content for a Plaintext Message event + * @param {string} body the plaintext body of the emote + * @returns {{msgtype: string, body: string}} + */ + makeTextMessage: function makeTextMessage(body) { + return { + msgtype: "m.text", + body: body + }; + }, + + /** + * Generates the content for a Plaintext Notice event + * @param {string} body the plaintext body of the notice + * @returns {{msgtype: string, body: string}} + */ + makeNotice: function makeNotice(body) { + return { + msgtype: "m.notice", + body: body + }; + }, + + /** + * Generates the content for a Plaintext Emote event + * @param {string} body the plaintext body of the emote + * @returns {{msgtype: string, body: string}} + */ + makeEmoteMessage: function makeEmoteMessage(body) { + return { + msgtype: "m.emote", + body: body + }; + } +}; + +},{}],6:[function(require,module,exports){ +"use strict"; + +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +/** + * @module content-repo + */ +var utils = require("./utils"); + +/** Content Repo utility functions */ +module.exports = { + /** + * Get the HTTP URL for an MXC URI. + * @param {string} baseUrl The base homeserver url which has a content repo. + * @param {string} mxc The mxc:// URI. + * @param {Number} width The desired width of the thumbnail. + * @param {Number} height The desired height of the thumbnail. + * @param {string} resizeMethod The thumbnail resize method to use, either + * "crop" or "scale". + * @param {Boolean} allowDirectLinks If true, return any non-mxc URLs + * directly. Fetching such URLs will leak information about the user to + * anyone they share a room with. If false, will return the emptry string + * for such URLs. + * @return {string} The complete URL to the content. + */ + getHttpUriForMxc: function getHttpUriForMxc(baseUrl, mxc, width, height, resizeMethod, allowDirectLinks) { + if (typeof mxc !== "string" || !mxc) { + return ''; + } + if (mxc.indexOf("mxc://") !== 0) { + if (allowDirectLinks) { + return mxc; + } else { + return ''; + } + } + var serverAndMediaId = mxc.slice(6); // strips mxc:// + var prefix = "/_matrix/media/v1/download/"; + var params = {}; + + if (width) { + params.width = width; + } + if (height) { + params.height = height; + } + if (resizeMethod) { + params.method = resizeMethod; + } + if (utils.keys(params).length > 0) { + // these are thumbnailing params so they probably want the + // thumbnailing API... + prefix = "/_matrix/media/v1/thumbnail/"; + } + + var fragmentOffset = serverAndMediaId.indexOf("#"); + var fragment = ""; + if (fragmentOffset >= 0) { + fragment = serverAndMediaId.substr(fragmentOffset); + serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset); + } + return baseUrl + prefix + serverAndMediaId + (utils.keys(params).length === 0 ? "" : "?" + utils.encodeParams(params)) + fragment; + }, + + /** + * Get an identicon URL from an arbitrary string. + * @param {string} baseUrl The base homeserver url which has a content repo. + * @param {string} identiconString The string to create an identicon for. + * @param {Number} width The desired width of the image in pixels. Default: 96. + * @param {Number} height The desired height of the image in pixels. Default: 96. + * @return {string} The complete URL to the identicon. + */ + getIdenticonUri: function getIdenticonUri(baseUrl, identiconString, width, height) { + if (!identiconString) { + return null; + } + if (!width) { + width = 96; + } + if (!height) { + height = 96; + } + var params = { + width: width, + height: height + }; + + var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", { + $ident: identiconString + }); + return baseUrl + path + (utils.keys(params).length === 0 ? "" : "?" + utils.encodeParams(params)); + } +}; + +},{"./utils":50}],7:[function(require,module,exports){ +/* +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** + * @module crypto/DeviceList + * + * Manages the list of other users' devices + */ + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _bluebird = require('bluebird'); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +var _regenerator = require('babel-runtime/regenerator'); + +var _regenerator2 = _interopRequireDefault(_regenerator); + +var _keys = require('babel-runtime/core-js/object/keys'); + +var _keys2 = _interopRequireDefault(_keys); + +var _getIterator2 = require('babel-runtime/core-js/get-iterator'); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +var _createClass2 = require('babel-runtime/helpers/createClass'); + +var _createClass3 = _interopRequireDefault(_createClass2); + +var _updateStoredDeviceKeysForUser = function () { + var _ref4 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee3(_olmDevice, userId, userStore, userResult) { + var updated, deviceId, _deviceId, deviceResult; + + return _regenerator2.default.wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + updated = false; + + // remove any devices in the store which aren't in the response + + _context3.t0 = _regenerator2.default.keys(userStore); + + case 2: + if ((_context3.t1 = _context3.t0()).done) { + _context3.next = 9; + break; + } + + deviceId = _context3.t1.value; + + if (userStore.hasOwnProperty(deviceId)) { + _context3.next = 6; + break; + } + + return _context3.abrupt('continue', 2); + + case 6: + + if (!(deviceId in userResult)) { + console.log("Device " + userId + ":" + deviceId + " has been removed"); + delete userStore[deviceId]; + updated = true; + } + _context3.next = 2; + break; + + case 9: + _context3.t2 = _regenerator2.default.keys(userResult); + + case 10: + if ((_context3.t3 = _context3.t2()).done) { + _context3.next = 27; + break; + } + + _deviceId = _context3.t3.value; + + if (userResult.hasOwnProperty(_deviceId)) { + _context3.next = 14; + break; + } + + return _context3.abrupt('continue', 10); + + case 14: + deviceResult = userResult[_deviceId]; + + // check that the user_id and device_id in the response object are + // correct + + if (!(deviceResult.user_id !== userId)) { + _context3.next = 18; + break; + } + + console.warn("Mismatched user_id " + deviceResult.user_id + " in keys from " + userId + ":" + _deviceId); + return _context3.abrupt('continue', 10); + + case 18: + if (!(deviceResult.device_id !== _deviceId)) { + _context3.next = 21; + break; + } + + console.warn("Mismatched device_id " + deviceResult.device_id + " in keys from " + userId + ":" + _deviceId); + return _context3.abrupt('continue', 10); + + case 21: + _context3.next = 23; + return (0, _bluebird.resolve)(_storeDeviceKeys(_olmDevice, userStore, deviceResult)); + + case 23: + if (!_context3.sent) { + _context3.next = 25; + break; + } + + updated = true; + + case 25: + _context3.next = 10; + break; + + case 27: + return _context3.abrupt('return', updated); + + case 28: + case 'end': + return _context3.stop(); + } + } + }, _callee3, this); + })); + + return function _updateStoredDeviceKeysForUser(_x4, _x5, _x6, _x7) { + return _ref4.apply(this, arguments); + }; +}(); + +/* + * Process a device in a /query response, and add it to the userStore + * + * returns (a promise for) true if a change was made, else false + */ + + +var _storeDeviceKeys = function () { + var _ref5 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee4(_olmDevice, userStore, deviceResult) { + var deviceId, userId, signKeyId, signKey, unsigned, deviceStore; + return _regenerator2.default.wrap(function _callee4$(_context4) { + while (1) { + switch (_context4.prev = _context4.next) { + case 0: + if (deviceResult.keys) { + _context4.next = 2; + break; + } + + return _context4.abrupt('return', false); + + case 2: + deviceId = deviceResult.device_id; + userId = deviceResult.user_id; + signKeyId = "ed25519:" + deviceId; + signKey = deviceResult.keys[signKeyId]; + + if (signKey) { + _context4.next = 9; + break; + } + + console.warn("Device " + userId + ":" + deviceId + " has no ed25519 key"); + return _context4.abrupt('return', false); + + case 9: + unsigned = deviceResult.unsigned || {}; + _context4.prev = 10; + _context4.next = 13; + return (0, _bluebird.resolve)(_olmlib2.default.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey)); + + case 13: + _context4.next = 19; + break; + + case 15: + _context4.prev = 15; + _context4.t0 = _context4['catch'](10); + + console.warn("Unable to verify signature on device " + userId + ":" + deviceId + ":" + _context4.t0); + return _context4.abrupt('return', false); + + case 19: + + // DeviceInfo + deviceStore = void 0; + + if (!(deviceId in userStore)) { + _context4.next = 27; + break; + } + + // already have this device. + deviceStore = userStore[deviceId]; + + if (!(deviceStore.getFingerprint() != signKey)) { + _context4.next = 25; + break; + } + + // this should only happen if the list has been MITMed; we are + // best off sticking with the original keys. + // + // Should we warn the user about it somehow? + console.warn("Ed25519 key for device " + userId + ":" + deviceId + " has changed"); + return _context4.abrupt('return', false); + + case 25: + _context4.next = 28; + break; + + case 27: + userStore[deviceId] = deviceStore = new _deviceinfo2.default(deviceId); + + case 28: + + deviceStore.keys = deviceResult.keys || {}; + deviceStore.algorithms = deviceResult.algorithms || []; + deviceStore.unsigned = unsigned; + return _context4.abrupt('return', true); + + case 32: + case 'end': + return _context4.stop(); + } + } + }, _callee4, this, [[10, 15]]); + })); + + return function _storeDeviceKeys(_x8, _x9, _x10) { + return _ref5.apply(this, arguments); + }; +}(); + +var _deviceinfo = require('./deviceinfo'); + +var _deviceinfo2 = _interopRequireDefault(_deviceinfo); + +var _olmlib = require('./olmlib'); + +var _olmlib2 = _interopRequireDefault(_olmlib); + +var _indexeddbCryptoStore = require('./store/indexeddb-crypto-store'); + +var _indexeddbCryptoStore2 = _interopRequireDefault(_indexeddbCryptoStore); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/* State transition diagram for DeviceList._deviceTrackingStatus + * + * | + * stopTrackingDeviceList V + * +---------------------> NOT_TRACKED + * | | + * +<--------------------+ | startTrackingDeviceList + * | | V + * | +-------------> PENDING_DOWNLOAD <--------------------+-+ + * | | ^ | | | + * | | restart download | | start download | | invalidateUserDeviceList + * | | client failed | | | | + * | | | V | | + * | +------------ DOWNLOAD_IN_PROGRESS -------------------+ | + * | | | | + * +<-------------------+ | download successful | + * ^ V | + * +----------------------- UP_TO_DATE ------------------------+ + */ + +// constants for DeviceList._deviceTrackingStatus +var TRACKING_STATUS_NOT_TRACKED = 0; +var TRACKING_STATUS_PENDING_DOWNLOAD = 1; +var TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2; +var TRACKING_STATUS_UP_TO_DATE = 3; + +/** + * @alias module:crypto/DeviceList + */ + +var DeviceList = function () { + function DeviceList(baseApis, cryptoStore, sessionStore, olmDevice) { + (0, _classCallCheck3.default)(this, DeviceList); + + this._cryptoStore = cryptoStore; + this._sessionStore = sessionStore; + + // userId -> { + // deviceId -> { + // [device info] + // } + // } + this._devices = {}; + + // which users we are tracking device status for. + // userId -> TRACKING_STATUS_* + this._deviceTrackingStatus = {}; // loaded from storage in load() + + // The 'next_batch' sync token at the point the data was writen, + // ie. a token representing the point immediately after the + // moment represented by the snapshot in the db. + this._syncToken = null; + + this._serialiser = new DeviceListUpdateSerialiser(baseApis, olmDevice, this); + + // userId -> promise + this._keyDownloadsInProgressByUser = {}; + + // Set whenever changes are made other than setting the sync token + this._dirty = false; + + // Promise resolved when device data is saved + this._savePromise = null; + // Function that resolves the save promise + this._resolveSavePromise = null; + // The time the save is scheduled for + this._savePromiseTime = null; + // The timer used to delay the save + this._saveTimer = null; + } + + /** + * Load the device tracking state from storage + */ + + + (0, _createClass3.default)(DeviceList, [{ + key: 'load', + value: function () { + var _ref = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee() { + var _this = this; + + var shouldDeleteSessionStore, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, u; + + return _regenerator2.default.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + shouldDeleteSessionStore = false; + _context.next = 3; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn( + // migrate from session store if there's data there and not here + 'readwrite', [_indexeddbCryptoStore2.default.STORE_DEVICE_DATA], function (txn) { + _this._cryptoStore.getEndToEndDeviceData(txn, function (deviceData) { + if (deviceData === null) { + console.log("Migrating e2e device data..."); + _this._devices = _this._sessionStore.getAllEndToEndDevices() || {}; + _this._deviceTrackingStatus = _this._sessionStore.getEndToEndDeviceTrackingStatus() || {}; + _this._syncToken = _this._sessionStore.getEndToEndDeviceSyncToken(); + _this._cryptoStore.storeEndToEndDeviceData({ + devices: _this._devices, + trackingStatus: _this._deviceTrackingStatus, + syncToken: _this._syncToken + }, txn); + shouldDeleteSessionStore = true; + } else { + _this._devices = deviceData ? deviceData.devices : {}, _this._deviceTrackingStatus = deviceData ? deviceData.trackingStatus : {}; + _this._syncToken = deviceData ? deviceData.syncToken : null; + } + }); + })); + + case 3: + + if (shouldDeleteSessionStore) { + // migrated data is now safely persisted: remove from old store + this._sessionStore.removeEndToEndDeviceData(); + } + + _iteratorNormalCompletion = true; + _didIteratorError = false; + _iteratorError = undefined; + _context.prev = 7; + for (_iterator = (0, _getIterator3.default)((0, _keys2.default)(this._deviceTrackingStatus)); !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + u = _step.value; + + // if a download was in progress when we got shut down, it isn't any more. + if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) { + this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD; + } + } + _context.next = 15; + break; + + case 11: + _context.prev = 11; + _context.t0 = _context['catch'](7); + _didIteratorError = true; + _iteratorError = _context.t0; + + case 15: + _context.prev = 15; + _context.prev = 16; + + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + + case 18: + _context.prev = 18; + + if (!_didIteratorError) { + _context.next = 21; + break; + } + + throw _iteratorError; + + case 21: + return _context.finish(18); + + case 22: + return _context.finish(15); + + case 23: + case 'end': + return _context.stop(); + } + } + }, _callee, this, [[7, 11, 15, 23], [16,, 18, 22]]); + })); + + function load() { + return _ref.apply(this, arguments); + } + + return load; + }() + + /** + * Save the device tracking state to storage, if any changes are + * pending other than updating the sync token + * + * The actual save will be delayed by a short amount of time to + * aggregate multiple writes to the database. + * + * @param {integer} delay Time in ms before which the save actually happens. + * By default, the save is delayed for a short period in order to batch + * multiple writes, but this behaviour can be disabled by passing 0. + * + * @return {Promise} true if the data was saved, false if + * it was not (eg. because no changes were pending). The promise + * will only resolve once the data is saved, so may take some time + * to resolve. + */ + + }, { + key: 'saveIfDirty', + value: function () { + var _ref2 = (0, _bluebird.method)(function (delay) { + var _this2 = this; + + if (!this._dirty) return _bluebird2.default.resolve(false); + // Delay saves for a bit so we can aggregate multiple saves that happen + // in quick succession (eg. when a whole room's devices are marked as known) + if (delay === undefined) delay = 500; + + var targetTime = Date.now + delay; + if (this._savePromiseTime && targetTime < this._savePromiseTime) { + // There's a save scheduled but for after we would like: cancel + // it & schedule one for the time we want + clearTimeout(this._saveTimer); + this._saveTimer = null; + this._savePromiseTime = null; + // (but keep the save promise since whatever called save before + // will still want to know when the save is done) + } + + var savePromise = this._savePromise; + if (savePromise === null) { + savePromise = new _bluebird2.default(function (resolve, reject) { + _this2._resolveSavePromise = resolve; + }); + this._savePromise = savePromise; + } + + if (this._saveTimer === null) { + var resolveSavePromise = this._resolveSavePromise; + this._savePromiseTime = targetTime; + this._saveTimer = setTimeout(function () { + console.log('Saving device tracking data at token ' + _this2._syncToken); + // null out savePromise now (after the delay but before the write), + // otherwise we could return the existing promise when the save has + // actually already happened. Likewise for the dirty flag. + _this2._savePromiseTime = null; + _this2._saveTimer = null; + _this2._savePromise = null; + _this2._resolveSavePromise = null; + + _this2._dirty = false; + _this2._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_DEVICE_DATA], function (txn) { + _this2._cryptoStore.storeEndToEndDeviceData({ + devices: _this2._devices, + trackingStatus: _this2._deviceTrackingStatus, + syncToken: _this2._syncToken + }, txn); + }).then(function () { + resolveSavePromise(); + }); + }, delay); + } + return savePromise; + }); + + function saveIfDirty(_x) { + return _ref2.apply(this, arguments); + } + + return saveIfDirty; + }() + + /** + * Gets the sync token last set with setSyncToken + * + * @return {string} The sync token + */ + + }, { + key: 'getSyncToken', + value: function getSyncToken() { + return this._syncToken; + } + + /** + * Sets the sync token that the app will pass as the 'since' to the /sync + * endpoint next time it syncs. + * The sync token must always be set after any changes made as a result of + * data in that sync since setting the sync token to a newer one will mean + * those changed will not be synced from the server if a new client starts + * up with that data. + * + * @param {string} st The sync token + */ + + }, { + key: 'setSyncToken', + value: function setSyncToken(st) { + this._syncToken = st; + } + + /** + * Ensures up to date keys for a list of users are stored in the session store, + * downloading and storing them if they're not (or if forceDownload is + * true). + * @param {Array} userIds The users to fetch. + * @param {bool} forceDownload Always download the keys even if cached. + * + * @return {Promise} A promise which resolves to a map userId->deviceId->{@link + * module:crypto/deviceinfo|DeviceInfo}. + */ + + }, { + key: 'downloadKeys', + value: function downloadKeys(userIds, forceDownload) { + var _this3 = this; + + var usersToDownload = []; + var promises = []; + + userIds.forEach(function (u) { + var trackingStatus = _this3._deviceTrackingStatus[u]; + if (_this3._keyDownloadsInProgressByUser[u]) { + // already a key download in progress/queued for this user; its results + // will be good enough for us. + console.log('downloadKeys: already have a download in progress for ' + (u + ': awaiting its result')); + promises.push(_this3._keyDownloadsInProgressByUser[u]); + } else if (forceDownload || trackingStatus != TRACKING_STATUS_UP_TO_DATE) { + usersToDownload.push(u); + } + }); + + if (usersToDownload.length != 0) { + console.log("downloadKeys: downloading for", usersToDownload); + var downloadPromise = this._doKeyDownload(usersToDownload); + promises.push(downloadPromise); + } + + if (promises.length === 0) { + console.log("downloadKeys: already have all necessary keys"); + } + + return _bluebird2.default.all(promises).then(function () { + return _this3._getDevicesFromStore(userIds); + }); + } + + /** + * Get the stored device keys for a list of user ids + * + * @param {string[]} userIds the list of users to list keys for. + * + * @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}. + */ + + }, { + key: '_getDevicesFromStore', + value: function _getDevicesFromStore(userIds) { + var stored = {}; + var self = this; + userIds.map(function (u) { + stored[u] = {}; + var devices = self.getStoredDevicesForUser(u) || []; + devices.map(function (dev) { + stored[u][dev.deviceId] = dev; + }); + }); + return stored; + } + + /** + * Get the stored device keys for a user id + * + * @param {string} userId the user to list keys for. + * + * @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't + * managed to get a list of devices for this user yet. + */ + + }, { + key: 'getStoredDevicesForUser', + value: function getStoredDevicesForUser(userId) { + var devs = this._devices[userId]; + if (!devs) { + return null; + } + var res = []; + for (var deviceId in devs) { + if (devs.hasOwnProperty(deviceId)) { + res.push(_deviceinfo2.default.fromStorage(devs[deviceId], deviceId)); + } + } + return res; + } + + /** + * Get the stored device data for a user, in raw object form + * + * @param {string} userId the user to get data for + * + * @return {Object} deviceId->{object} devices, or undefined if + * there is no data for this user. + */ + + }, { + key: 'getRawStoredDevicesForUser', + value: function getRawStoredDevicesForUser(userId) { + return this._devices[userId]; + } + + /** + * Get the stored keys for a single device + * + * @param {string} userId + * @param {string} deviceId + * + * @return {module:crypto/deviceinfo?} device, or undefined + * if we don't know about this device + */ + + }, { + key: 'getStoredDevice', + value: function getStoredDevice(userId, deviceId) { + var devs = this._devices[userId]; + if (!devs || !devs[deviceId]) { + return undefined; + } + return _deviceinfo2.default.fromStorage(devs[deviceId], deviceId); + } + + /** + * Find a device by curve25519 identity key + * + * @param {string} userId owner of the device + * @param {string} algorithm encryption algorithm + * @param {string} senderKey curve25519 key to match + * + * @return {module:crypto/deviceinfo?} + */ + + }, { + key: 'getDeviceByIdentityKey', + value: function getDeviceByIdentityKey(userId, algorithm, senderKey) { + if (algorithm !== _olmlib2.default.OLM_ALGORITHM && algorithm !== _olmlib2.default.MEGOLM_ALGORITHM) { + // we only deal in olm keys + return null; + } + + var devices = this._devices[userId]; + if (!devices) { + return null; + } + + for (var deviceId in devices) { + if (!devices.hasOwnProperty(deviceId)) { + continue; + } + + var device = devices[deviceId]; + for (var keyId in device.keys) { + if (!device.keys.hasOwnProperty(keyId)) { + continue; + } + if (keyId.indexOf("curve25519:") !== 0) { + continue; + } + var deviceKey = device.keys[keyId]; + if (deviceKey == senderKey) { + return _deviceinfo2.default.fromStorage(device, deviceId); + } + } + } + + // doesn't match a known device + return null; + } + + /** + * Replaces the list of devices for a user with the given device list + * + * @param {string} u The user ID + * @param {Object} devs New device info for user + */ + + }, { + key: 'storeDevicesForUser', + value: function storeDevicesForUser(u, devs) { + this._devices[u] = devs; + this._dirty = true; + } + + /** + * flag the given user for device-list tracking, if they are not already. + * + * This will mean that a subsequent call to refreshOutdatedDeviceLists() + * will download the device list for the user, and that subsequent calls to + * invalidateUserDeviceList will trigger more updates. + * + * @param {String} userId + */ + + }, { + key: 'startTrackingDeviceList', + value: function startTrackingDeviceList(userId) { + // sanity-check the userId. This is mostly paranoia, but if synapse + // can't parse the userId we give it as an mxid, it 500s the whole + // request and we can never update the device lists again (because + // the broken userId is always 'invalid' and always included in any + // refresh request). + // By checking it is at least a string, we can eliminate a class of + // silly errors. + if (typeof userId !== 'string') { + throw new Error('userId must be a string; was ' + userId); + } + if (!this._deviceTrackingStatus[userId]) { + console.log('Now tracking device list for ' + userId); + this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD; + } + // we don't yet persist the tracking status, since there may be a lot + // of calls; we save all data together once the sync is done + this._dirty = true; + } + + /** + * Mark the given user as no longer being tracked for device-list updates. + * + * This won't affect any in-progress downloads, which will still go on to + * complete; it will just mean that we don't think that we have an up-to-date + * list for future calls to downloadKeys. + * + * @param {String} userId + */ + + }, { + key: 'stopTrackingDeviceList', + value: function stopTrackingDeviceList(userId) { + if (this._deviceTrackingStatus[userId]) { + console.log('No longer tracking device list for ' + userId); + this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED; + + // we don't yet persist the tracking status, since there may be a lot + // of calls; we save all data together once the sync is done + this._dirty = true; + } + } + + /** + * Set all users we're currently tracking to untracked + * + * This will flag each user whose devices we are tracking as in need of an + * update. + */ + + }, { + key: 'stopTrackingAllDeviceLists', + value: function stopTrackingAllDeviceLists() { + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = (0, _getIterator3.default)((0, _keys2.default)(this._deviceTrackingStatus)), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var userId = _step2.value; + + this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED; + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + this._dirty = true; + } + + /** + * Mark the cached device list for the given user outdated. + * + * If we are not tracking this user's devices, we'll do nothing. Otherwise + * we flag the user as needing an update. + * + * This doesn't actually set off an update, so that several users can be + * batched together. Call refreshOutdatedDeviceLists() for that. + * + * @param {String} userId + */ + + }, { + key: 'invalidateUserDeviceList', + value: function invalidateUserDeviceList(userId) { + if (this._deviceTrackingStatus[userId]) { + console.log("Marking device list outdated for", userId); + this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD; + + // we don't yet persist the tracking status, since there may be a lot + // of calls; we save all data together once the sync is done + this._dirty = true; + } + } + + /** + * If we have users who have outdated device lists, start key downloads for them + * + * @returns {Promise} which completes when the download completes; normally there + * is no need to wait for this (it's mostly for the unit tests). + */ + + }, { + key: 'refreshOutdatedDeviceLists', + value: function refreshOutdatedDeviceLists() { + this.saveIfDirty(); + + var usersToDownload = []; + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; + + try { + for (var _iterator3 = (0, _getIterator3.default)((0, _keys2.default)(this._deviceTrackingStatus)), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var userId = _step3.value; + + var stat = this._deviceTrackingStatus[userId]; + if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) { + usersToDownload.push(userId); + } + } + } catch (err) { + _didIteratorError3 = true; + _iteratorError3 = err; + } finally { + try { + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); + } + } finally { + if (_didIteratorError3) { + throw _iteratorError3; + } + } + } + + return this._doKeyDownload(usersToDownload); + } + + /** + * Set the stored device data for a user, in raw object form + * Used only by internal class DeviceListUpdateSerialiser + * + * @param {string} userId the user to get data for + * + * @param {Object} devices deviceId->{object} the new devices + */ + + }, { + key: '_setRawStoredDevicesForUser', + value: function _setRawStoredDevicesForUser(userId, devices) { + this._devices[userId] = devices; + } + + /** + * Fire off download update requests for the given users, and update the + * device list tracking status for them, and the + * _keyDownloadsInProgressByUser map for them. + * + * @param {String[]} users list of userIds + * + * @return {module:client.Promise} resolves when all the users listed have + * been updated. rejects if there was a problem updating any of the + * users. + */ + + }, { + key: '_doKeyDownload', + value: function _doKeyDownload(users) { + var _this4 = this; + + if (users.length === 0) { + // nothing to do + return _bluebird2.default.resolve(); + } + + var prom = this._serialiser.updateDevicesForUsers(users, this._syncToken).then(function () { + finished(true); + }, function (e) { + console.error('Error downloading keys for ' + users + ":", e); + finished(false); + throw e; + }); + + users.forEach(function (u) { + _this4._keyDownloadsInProgressByUser[u] = prom; + var stat = _this4._deviceTrackingStatus[u]; + if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) { + _this4._deviceTrackingStatus[u] = TRACKING_STATUS_DOWNLOAD_IN_PROGRESS; + } + }); + + var finished = function finished(success) { + users.forEach(function (u) { + _this4._dirty = true; + + // we may have queued up another download request for this user + // since we started this request. If that happens, we should + // ignore the completion of the first one. + if (_this4._keyDownloadsInProgressByUser[u] !== prom) { + console.log('Another update in the queue for', u, '- not marking up-to-date'); + return; + } + delete _this4._keyDownloadsInProgressByUser[u]; + var stat = _this4._deviceTrackingStatus[u]; + if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) { + if (success) { + // we didn't get any new invalidations since this download started: + // this user's device list is now up to date. + _this4._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE; + console.log("Device list for", u, "now up to date"); + } else { + _this4._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD; + } + } + }); + _this4.saveIfDirty(); + }; + + return prom; + } + }]); + return DeviceList; +}(); + +/** + * Serialises updates to device lists + * + * Ensures that results from /keys/query are not overwritten if a second call + * completes *before* an earlier one. + * + * It currently does this by ensuring only one call to /keys/query happens at a + * time (and queuing other requests up). + */ + + +exports.default = DeviceList; + +var DeviceListUpdateSerialiser = function () { + /* + * @param {object} baseApis Base API object + * @param {object} olmDevice The Olm Device + * @param {object} deviceList The device list object + */ + function DeviceListUpdateSerialiser(baseApis, olmDevice, deviceList) { + (0, _classCallCheck3.default)(this, DeviceListUpdateSerialiser); + + this._baseApis = baseApis; + this._olmDevice = olmDevice; + this._deviceList = deviceList; // the device list to be updated + + this._downloadInProgress = false; + + // users which are queued for download + // userId -> true + this._keyDownloadsQueuedByUser = {}; + + // deferred which is resolved when the queued users are downloaded. + // + // non-null indicates that we have users queued for download. + this._queuedQueryDeferred = null; + + this._syncToken = null; // The sync token we send with the requests + } + + /** + * Make a key query request for the given users + * + * @param {String[]} users list of user ids + * + * @param {String} syncToken sync token to pass in the query request, to + * help the HS give the most recent results + * + * @return {module:client.Promise} resolves when all the users listed have + * been updated. rejects if there was a problem updating any of the + * users. + */ + + + (0, _createClass3.default)(DeviceListUpdateSerialiser, [{ + key: 'updateDevicesForUsers', + value: function updateDevicesForUsers(users, syncToken) { + var _this5 = this; + + users.forEach(function (u) { + _this5._keyDownloadsQueuedByUser[u] = true; + }); + + if (!this._queuedQueryDeferred) { + this._queuedQueryDeferred = _bluebird2.default.defer(); + } + + // We always take the new sync token and just use the latest one we've + // been given, since it just needs to be at least as recent as the + // sync response the device invalidation message arrived in + this._syncToken = syncToken; + + if (this._downloadInProgress) { + // just queue up these users + console.log('Queued key download for', users); + return this._queuedQueryDeferred.promise; + } + + // start a new download. + return this._doQueuedQueries(); + } + }, { + key: '_doQueuedQueries', + value: function _doQueuedQueries() { + var _this6 = this; + + if (this._downloadInProgress) { + throw new Error("DeviceListUpdateSerialiser._doQueuedQueries called with request active"); + } + + var downloadUsers = (0, _keys2.default)(this._keyDownloadsQueuedByUser); + this._keyDownloadsQueuedByUser = {}; + var deferred = this._queuedQueryDeferred; + this._queuedQueryDeferred = null; + + console.log('Starting key download for', downloadUsers); + this._downloadInProgress = true; + + var opts = {}; + if (this._syncToken) { + opts.token = this._syncToken; + } + + this._baseApis.downloadKeysForUsers(downloadUsers, opts).then(function (res) { + var dk = res.device_keys || {}; + + // do each user in a separate promise, to avoid wedging the CPU + // (https://github.com/vector-im/riot-web/issues/3158) + // + // of course we ought to do this in a web worker or similar, but + // this serves as an easy solution for now. + var prom = _bluebird2.default.resolve(); + var _iteratorNormalCompletion4 = true; + var _didIteratorError4 = false; + var _iteratorError4 = undefined; + + try { + var _loop = function _loop() { + var userId = _step4.value; + + prom = prom.delay(5).then(function () { + return _this6._processQueryResponseForUser(userId, dk[userId]); + }); + }; + + for (var _iterator4 = (0, _getIterator3.default)(downloadUsers), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { + _loop(); + } + } catch (err) { + _didIteratorError4 = true; + _iteratorError4 = err; + } finally { + try { + if (!_iteratorNormalCompletion4 && _iterator4.return) { + _iterator4.return(); + } + } finally { + if (_didIteratorError4) { + throw _iteratorError4; + } + } + } + + return prom; + }).done(function () { + console.log('Completed key download for ' + downloadUsers); + + _this6._downloadInProgress = false; + deferred.resolve(); + + // if we have queued users, fire off another request. + if (_this6._queuedQueryDeferred) { + _this6._doQueuedQueries(); + } + }, function (e) { + console.warn('Error downloading keys for ' + downloadUsers + ':', e); + _this6._downloadInProgress = false; + deferred.reject(e); + }); + + return deferred.promise; + } + }, { + key: '_processQueryResponseForUser', + value: function () { + var _ref3 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee2(userId, response) { + var userStore, devs, storage; + return _regenerator2.default.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + console.log('got keys for ' + userId + ':', response); + + // map from deviceid -> deviceinfo for this user + userStore = {}; + devs = this._deviceList.getRawStoredDevicesForUser(userId); + + if (devs) { + (0, _keys2.default)(devs).forEach(function (deviceId) { + var d = _deviceinfo2.default.fromStorage(devs[deviceId], deviceId); + userStore[deviceId] = d; + }); + } + + _context2.next = 6; + return (0, _bluebird.resolve)(_updateStoredDeviceKeysForUser(this._olmDevice, userId, userStore, response || {})); + + case 6: + + // put the updates into thr object that will be returned as our results + storage = {}; + + (0, _keys2.default)(userStore).forEach(function (deviceId) { + storage[deviceId] = userStore[deviceId].toStorage(); + }); + + this._deviceList._setRawStoredDevicesForUser(userId, storage); + + case 9: + case 'end': + return _context2.stop(); + } + } + }, _callee2, this); + })); + + function _processQueryResponseForUser(_x2, _x3) { + return _ref3.apply(this, arguments); + } + + return _processQueryResponseForUser; + }() + }]); + return DeviceListUpdateSerialiser; +}(); + +},{"./deviceinfo":15,"./olmlib":17,"./store/indexeddb-crypto-store":19,"babel-runtime/core-js/get-iterator":53,"babel-runtime/core-js/object/keys":61,"babel-runtime/helpers/classCallCheck":66,"babel-runtime/helpers/createClass":67,"babel-runtime/regenerator":73,"bluebird":74}],8:[function(require,module,exports){ +(function (global){ +"use strict"; + +var _getIterator2 = require("babel-runtime/core-js/get-iterator"); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +var _keys = require("babel-runtime/core-js/object/keys"); + +var _keys2 = _interopRequireDefault(_keys); + +var _regenerator = require("babel-runtime/regenerator"); + +var _regenerator2 = _interopRequireDefault(_regenerator); + +var _bluebird = require("bluebird"); + +var _initialiseAccount = function () { + var _ref2 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee2(sessionStore, cryptoStore, pickleKey, account) { + return _regenerator2.default.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + _context2.next = 2; + return (0, _bluebird.resolve)(cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_ACCOUNT], function (txn) { + cryptoStore.getAccount(txn, function (pickledAccount) { + if (pickledAccount !== null) { + account.unpickle(pickleKey, pickledAccount); + } else { + account.create(); + pickledAccount = account.pickle(pickleKey); + cryptoStore.storeAccount(txn, pickledAccount); + } + }); + })); + + case 2: + case "end": + return _context2.stop(); + } + } + }, _callee2, this); + })); + + return function _initialiseAccount(_x, _x2, _x3, _x4) { + return _ref2.apply(this, arguments); + }; +}(); + +/** + * @return {array} The version of Olm. + */ + + +var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store"); + +var _indexeddbCryptoStore2 = _interopRequireDefault(_indexeddbCryptoStore); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * olm.js wrapper + * + * @module crypto/OlmDevice + */ +var Olm = global.Olm; /* + Copyright 2016 OpenMarket Ltd + Copyright 2017 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +if (!Olm) { + throw new Error("global.Olm is not defined"); +} + +// The maximum size of an event is 65K, and we base64 the content, so this is a +// reasonable approximation to the biggest plaintext we can encrypt. +var MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4; + +function checkPayloadLength(payloadString) { + if (payloadString === undefined) { + throw new Error("payloadString undefined"); + } + + if (payloadString.length > MAX_PLAINTEXT_LENGTH) { + // might as well fail early here rather than letting the olm library throw + // a cryptic memory allocation error. + // + // Note that even if we manage to do the encryption, the message send may fail, + // because by the time we've wrapped the ciphertext in the event object, it may + // exceed 65K. But at least we won't just fail with "abort()" in that case. + throw new Error("Message too long (" + payloadString.length + " bytes). " + "The maximum for an encrypted message is " + MAX_PLAINTEXT_LENGTH + " bytes."); + } +} + +/** + * The type of object we use for importing and exporting megolm session data. + * + * @typedef {Object} module:crypto/OlmDevice.MegolmSessionData + * @property {String} sender_key Sender's Curve25519 device key + * @property {String[]} forwarding_curve25519_key_chain Devices which forwarded + * this session to us (normally empty). + * @property {Object} sender_claimed_keys Other keys the sender claims. + * @property {String} room_id Room this session is used in + * @property {String} session_id Unique id for the session + * @property {String} session_key Base64'ed key data + */ + +/** + * Manages the olm cryptography functions. Each OlmDevice has a single + * OlmAccount and a number of OlmSessions. + * + * Accounts and sessions are kept pickled in a sessionStore. + * + * @constructor + * @alias module:crypto/OlmDevice + * + * @param {Object} sessionStore A store to be used for data in end-to-end + * crypto. This is deprecated and being replaced by cryptoStore. + * @param {Object} cryptoStore A store for crypto data + * + * @property {string} deviceCurve25519Key Curve25519 key for the account + * @property {string} deviceEd25519Key Ed25519 key for the account + */ +function OlmDevice(sessionStore, cryptoStore) { + this._sessionStore = sessionStore; + this._cryptoStore = cryptoStore; + this._pickleKey = "DEFAULT_KEY"; + + // don't know these until we load the account from storage in init() + this.deviceCurve25519Key = null; + this.deviceEd25519Key = null; + this._maxOneTimeKeys = null; + + // we don't bother stashing outboundgroupsessions in the sessionstore - + // instead we keep them here. + this._outboundGroupSessionStore = {}; + + // Store a set of decrypted message indexes for each group session. + // This partially mitigates a replay attack where a MITM resends a group + // message into the room. + // + // When we decrypt a message and the message index matches a previously + // decrypted message, one possible cause of that is that we are decrypting + // the same event, and may not indicate an actual replay attack. For + // example, this could happen if we receive events, forget about them, and + // then re-fetch them when we backfill. So we store the event ID and + // timestamp corresponding to each message index when we first decrypt it, + // and compare these against the event ID and timestamp every time we use + // that same index. If they match, then we're probably decrypting the same + // event and we don't consider it a replay attack. + // + // Keys are strings of form "||" + // Values are objects of the form "{id: , timestamp: }" + this._inboundGroupSessionMessageIndexes = {}; +} + +/** + * Initialise the OlmAccount. This must be called before any other operations + * on the OlmDevice. + * + * Attempts to load the OlmAccount from the crypto store, or creates one if none is + * found. + * + * Reads the device keys from the OlmAccount object. + */ +OlmDevice.prototype.init = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee() { + var e2eKeys, account; + return _regenerator2.default.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + _context.next = 2; + return (0, _bluebird.resolve)(this._migrateFromSessionStore()); + + case 2: + e2eKeys = void 0; + account = new Olm.Account(); + _context.prev = 4; + _context.next = 7; + return (0, _bluebird.resolve)(_initialiseAccount(this._sessionStore, this._cryptoStore, this._pickleKey, account)); + + case 7: + e2eKeys = JSON.parse(account.identity_keys()); + + this._maxOneTimeKeys = account.max_number_of_one_time_keys(); + + case 9: + _context.prev = 9; + + account.free(); + return _context.finish(9); + + case 12: + + this.deviceCurve25519Key = e2eKeys.curve25519; + this.deviceEd25519Key = e2eKeys.ed25519; + + case 14: + case "end": + return _context.stop(); + } + } + }, _callee, this, [[4,, 9, 12]]); +})); + +OlmDevice.getOlmVersion = function () { + return Olm.get_library_version(); +}; + +OlmDevice.prototype._migrateFromSessionStore = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee3() { + var _this = this; + + var sessions, ibGroupSessions, numIbSessions; + return _regenerator2.default.wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + _context3.next = 2; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_ACCOUNT], function (txn) { + _this._cryptoStore.getAccount(txn, function (pickledAccount) { + if (pickledAccount === null) { + // Migrate from sessionStore + pickledAccount = _this._sessionStore.getEndToEndAccount(); + if (pickledAccount !== null) { + console.log("Migrating account from session store"); + _this._cryptoStore.storeAccount(txn, pickledAccount); + } + } + }); + })); + + case 2: + + // remove the old account now the transaction has completed. Either we've + // migrated it or decided not to, either way we want to blow away the old data. + this._sessionStore.removeEndToEndAccount(); + + // sessions + sessions = this._sessionStore.getAllEndToEndSessions(); + + if (!((0, _keys2.default)(sessions).length > 0)) { + _context3.next = 8; + break; + } + + _context3.next = 7; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_SESSIONS], function (txn) { + // Don't migrate sessions from localstorage if we already have sessions + // in indexeddb, since this means we've already migrated and an old version + // has run against the same localstorage and created some spurious sessions. + _this._cryptoStore.countEndToEndSessions(txn, function (count) { + if (count) { + console.log("Crypto store already has sessions: not migrating"); + return; + } + var numSessions = 0; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)((0, _keys2.default)(sessions)), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var deviceKey = _step.value; + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = (0, _getIterator3.default)((0, _keys2.default)(sessions[deviceKey])), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var sessionId = _step2.value; + + numSessions++; + _this._cryptoStore.storeEndToEndSession(deviceKey, sessionId, sessions[deviceKey][sessionId], txn); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + console.log("Migrating " + numSessions + " sessions from session store"); + }); + })); + + case 7: + + this._sessionStore.removeAllEndToEndSessions(); + + case 8: + + // inbound group sessions + ibGroupSessions = this._sessionStore.getAllEndToEndInboundGroupSessionKeys(); + + if (!((0, _keys2.default)(ibGroupSessions).length > 0)) { + _context3.next = 14; + break; + } + + numIbSessions = 0; + _context3.next = 13; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_INBOUND_GROUP_SESSIONS], function (txn) { + // We always migrate inbound group sessions, even if we already have some + // in the new store. They should be be safe to migrate. + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; + + try { + for (var _iterator3 = (0, _getIterator3.default)(ibGroupSessions), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var s = _step3.value; + + try { + _this._cryptoStore.addEndToEndInboundGroupSession(s.senderKey, s.sessionId, JSON.parse(_this._sessionStore.getEndToEndInboundGroupSession(s.senderKey, s.sessionId)), txn); + } catch (e) { + console.warn("Failed to migrate session " + s.senderKey + "/" + s.sessionId + ": " + e.stack || e); + } + ++numIbSessions; + } + } catch (err) { + _didIteratorError3 = true; + _iteratorError3 = err; + } finally { + try { + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); + } + } finally { + if (_didIteratorError3) { + throw _iteratorError3; + } + } + } + + console.log("Migrated " + numIbSessions + " inbound group sessions from session store"); + })); + + case 13: + this._sessionStore.removeAllEndToEndInboundGroupSessions(); + + case 14: + case "end": + return _context3.stop(); + } + } + }, _callee3, this); +})); + +/** + * extract our OlmAccount from the crypto store and call the given function + * with the account object + * The `account` object is useable only within the callback passed to this + * function and will be freed as soon the callback returns. It is *not* + * useable for the rest of the lifetime of the transaction. + * This function requires a live transaction object from cryptoStore.doTxn() + * and therefore may only be called in a doTxn() callback. + * + * @param {*} txn Opaque transaction object from cryptoStore.doTxn() + * @param {function} func + * @private + */ +OlmDevice.prototype._getAccount = function (txn, func) { + var _this2 = this; + + this._cryptoStore.getAccount(txn, function (pickledAccount) { + var account = new Olm.Account(); + try { + account.unpickle(_this2._pickleKey, pickledAccount); + func(account); + } finally { + account.free(); + } + }); +}; + +/* + * Saves an account to the crypto store. + * This function requires a live transaction object from cryptoStore.doTxn() + * and therefore may only be called in a doTxn() callback. + * + * @param {*} txn Opaque transaction object from cryptoStore.doTxn() + * @param {object} Olm.Account object + * @private + */ +OlmDevice.prototype._storeAccount = function (txn, account) { + this._cryptoStore.storeAccount(txn, account.pickle(this._pickleKey)); +}; + +/** + * extract an OlmSession from the session store and call the given function + * The session is useable only within the callback passed to this + * function and will be freed as soon the callback returns. It is *not* + * useable for the rest of the lifetime of the transaction. + * + * @param {string} deviceKey + * @param {string} sessionId + * @param {*} txn Opaque transaction object from cryptoStore.doTxn() + * @param {function} func + * @private + */ +OlmDevice.prototype._getSession = function (deviceKey, sessionId, txn, func) { + var _this3 = this; + + this._cryptoStore.getEndToEndSession(deviceKey, sessionId, txn, function (pickledSession) { + _this3._unpickleSession(pickledSession, func); + }); +}; + +/** + * Creates a session object from a session pickle and executes the given + * function with it. The session object is destroyed once the function + * returns. + * + * @param {string} pickledSession + * @param {function} func + * @private + */ +OlmDevice.prototype._unpickleSession = function (pickledSession, func) { + var session = new Olm.Session(); + try { + session.unpickle(this._pickleKey, pickledSession); + func(session); + } finally { + session.free(); + } +}; + +/** + * store our OlmSession in the session store + * + * @param {string} deviceKey + * @param {OlmSession} session + * @param {*} txn Opaque transaction object from cryptoStore.doTxn() + * @private + */ +OlmDevice.prototype._saveSession = function (deviceKey, session, txn) { + var pickledSession = session.pickle(this._pickleKey); + this._cryptoStore.storeEndToEndSession(deviceKey, session.session_id(), pickledSession, txn); +}; + +/** + * get an OlmUtility and call the given function + * + * @param {function} func + * @return {object} result of func + * @private + */ +OlmDevice.prototype._getUtility = function (func) { + var utility = new Olm.Utility(); + try { + return func(utility); + } finally { + utility.free(); + } +}; + +/** + * Signs a message with the ed25519 key for this account. + * + * @param {string} message message to be signed + * @return {Promise} base64-encoded signature + */ +OlmDevice.prototype.sign = function () { + var _ref4 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee4(message) { + var _this4 = this; + + var result; + return _regenerator2.default.wrap(function _callee4$(_context4) { + while (1) { + switch (_context4.prev = _context4.next) { + case 0: + result = void 0; + _context4.next = 3; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore2.default.STORE_ACCOUNT], function (txn) { + _this4._getAccount(txn, function (account) { + result = account.sign(message); + }); + })); + + case 3: + return _context4.abrupt("return", result); + + case 4: + case "end": + return _context4.stop(); + } + } + }, _callee4, this); + })); + + return function (_x5) { + return _ref4.apply(this, arguments); + }; +}(); + +/** + * Get the current (unused, unpublished) one-time keys for this account. + * + * @return {object} one time keys; an object with the single property + * curve25519, which is itself an object mapping key id to Curve25519 + * key. + */ +OlmDevice.prototype.getOneTimeKeys = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee5() { + var _this5 = this; + + var result; + return _regenerator2.default.wrap(function _callee5$(_context5) { + while (1) { + switch (_context5.prev = _context5.next) { + case 0: + result = void 0; + _context5.next = 3; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore2.default.STORE_ACCOUNT], function (txn) { + _this5._getAccount(txn, function (account) { + result = JSON.parse(account.one_time_keys()); + }); + })); + + case 3: + return _context5.abrupt("return", result); + + case 4: + case "end": + return _context5.stop(); + } + } + }, _callee5, this); +})); + +/** + * Get the maximum number of one-time keys we can store. + * + * @return {number} number of keys + */ +OlmDevice.prototype.maxNumberOfOneTimeKeys = function () { + return this._maxOneTimeKeys; +}; + +/** + * Marks all of the one-time keys as published. + */ +OlmDevice.prototype.markKeysAsPublished = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee6() { + var _this6 = this; + + return _regenerator2.default.wrap(function _callee6$(_context6) { + while (1) { + switch (_context6.prev = _context6.next) { + case 0: + _context6.next = 2; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_ACCOUNT], function (txn) { + _this6._getAccount(txn, function (account) { + account.mark_keys_as_published(); + _this6._storeAccount(txn, account); + }); + })); + + case 2: + case "end": + return _context6.stop(); + } + } + }, _callee6, this); +})); + +/** + * Generate some new one-time keys + * + * @param {number} numKeys number of keys to generate + * @return {Promise} Resolved once the account is saved back having generated the keys + */ +OlmDevice.prototype.generateOneTimeKeys = function (numKeys) { + var _this7 = this; + + return this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_ACCOUNT], function (txn) { + _this7._getAccount(txn, function (account) { + account.generate_one_time_keys(numKeys); + _this7._storeAccount(txn, account); + }); + }); +}; + +/** + * Generate a new outbound session + * + * The new session will be stored in the cryptoStore. + * + * @param {string} theirIdentityKey remote user's Curve25519 identity key + * @param {string} theirOneTimeKey remote user's one-time Curve25519 key + * @return {string} sessionId for the outbound session. + */ +OlmDevice.prototype.createOutboundSession = function () { + var _ref7 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee7(theirIdentityKey, theirOneTimeKey) { + var _this8 = this; + + var newSessionId; + return _regenerator2.default.wrap(function _callee7$(_context7) { + while (1) { + switch (_context7.prev = _context7.next) { + case 0: + newSessionId = void 0; + _context7.next = 3; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_ACCOUNT, _indexeddbCryptoStore2.default.STORE_SESSIONS], function (txn) { + _this8._getAccount(txn, function (account) { + var session = new Olm.Session(); + try { + session.create_outbound(account, theirIdentityKey, theirOneTimeKey); + newSessionId = session.session_id(); + _this8._storeAccount(txn, account); + _this8._saveSession(theirIdentityKey, session, txn); + } finally { + session.free(); + } + }); + })); + + case 3: + return _context7.abrupt("return", newSessionId); + + case 4: + case "end": + return _context7.stop(); + } + } + }, _callee7, this); + })); + + return function (_x6, _x7) { + return _ref7.apply(this, arguments); + }; +}(); + +/** + * Generate a new inbound session, given an incoming message + * + * @param {string} theirDeviceIdentityKey remote user's Curve25519 identity key + * @param {number} messageType messageType field from the received message (must be 0) + * @param {string} ciphertext base64-encoded body from the received message + * + * @return {{payload: string, session_id: string}} decrypted payload, and + * session id of new session + * + * @raises {Error} if the received message was not valid (for instance, it + * didn't use a valid one-time key). + */ +OlmDevice.prototype.createInboundSession = function () { + var _ref8 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee8(theirDeviceIdentityKey, messageType, ciphertext) { + var _this9 = this; + + var result; + return _regenerator2.default.wrap(function _callee8$(_context8) { + while (1) { + switch (_context8.prev = _context8.next) { + case 0: + if (!(messageType !== 0)) { + _context8.next = 2; + break; + } + + throw new Error("Need messageType == 0 to create inbound session"); + + case 2: + result = void 0; + _context8.next = 5; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_ACCOUNT, _indexeddbCryptoStore2.default.STORE_SESSIONS], function (txn) { + _this9._getAccount(txn, function (account) { + var session = new Olm.Session(); + try { + session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext); + account.remove_one_time_keys(session); + _this9._storeAccount(txn, account); + + var payloadString = session.decrypt(messageType, ciphertext); + + _this9._saveSession(theirDeviceIdentityKey, session, txn); + + result = { + payload: payloadString, + session_id: session.session_id() + }; + } finally { + session.free(); + } + }); + })); + + case 5: + return _context8.abrupt("return", result); + + case 6: + case "end": + return _context8.stop(); + } + } + }, _callee8, this); + })); + + return function (_x8, _x9, _x10) { + return _ref8.apply(this, arguments); + }; +}(); + +/** + * Get a list of known session IDs for the given device + * + * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * remote device + * @return {Promise} a list of known session ids for the device + */ +OlmDevice.prototype.getSessionIdsForDevice = function () { + var _ref9 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee9(theirDeviceIdentityKey) { + var _this10 = this; + + var sessionIds; + return _regenerator2.default.wrap(function _callee9$(_context9) { + while (1) { + switch (_context9.prev = _context9.next) { + case 0: + sessionIds = void 0; + _context9.next = 3; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore2.default.STORE_SESSIONS], function (txn) { + _this10._cryptoStore.getEndToEndSessions(theirDeviceIdentityKey, txn, function (sessions) { + sessionIds = (0, _keys2.default)(sessions); + }); + })); + + case 3: + return _context9.abrupt("return", sessionIds); + + case 4: + case "end": + return _context9.stop(); + } + } + }, _callee9, this); + })); + + return function (_x11) { + return _ref9.apply(this, arguments); + }; +}(); + +/** + * Get the right olm session id for encrypting messages to the given identity key + * + * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * remote device + * @return {Promise} session id, or null if no established session + */ +OlmDevice.prototype.getSessionIdForDevice = function () { + var _ref10 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee10(theirDeviceIdentityKey) { + var sessionIds; + return _regenerator2.default.wrap(function _callee10$(_context10) { + while (1) { + switch (_context10.prev = _context10.next) { + case 0: + _context10.next = 2; + return (0, _bluebird.resolve)(this.getSessionIdsForDevice(theirDeviceIdentityKey)); + + case 2: + sessionIds = _context10.sent; + + if (!(sessionIds.length === 0)) { + _context10.next = 5; + break; + } + + return _context10.abrupt("return", null); + + case 5: + // Use the session with the lowest ID. + sessionIds.sort(); + return _context10.abrupt("return", sessionIds[0]); + + case 7: + case "end": + return _context10.stop(); + } + } + }, _callee10, this); + })); + + return function (_x12) { + return _ref10.apply(this, arguments); + }; +}(); + +/** + * Get information on the active Olm sessions for a device. + *

+ * Returns an array, with an entry for each active session. The first entry in + * the result will be the one used for outgoing messages. Each entry contains + * the keys 'hasReceivedMessage' (true if the session has received an incoming + * message and is therefore past the pre-key stage), and 'sessionId'. + * + * @param {string} deviceIdentityKey Curve25519 identity key for the device + * @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>} + */ +OlmDevice.prototype.getSessionInfoForDevice = function () { + var _ref11 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee11(deviceIdentityKey) { + var _this11 = this; + + var info; + return _regenerator2.default.wrap(function _callee11$(_context11) { + while (1) { + switch (_context11.prev = _context11.next) { + case 0: + info = []; + _context11.next = 3; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore2.default.STORE_SESSIONS], function (txn) { + _this11._cryptoStore.getEndToEndSessions(deviceIdentityKey, txn, function (sessions) { + var sessionIds = (0, _keys2.default)(sessions).sort(); + var _iteratorNormalCompletion4 = true; + var _didIteratorError4 = false; + var _iteratorError4 = undefined; + + try { + var _loop = function _loop() { + var sessionId = _step4.value; + + _this11._unpickleSession(sessions[sessionId], function (session) { + info.push({ + hasReceivedMessage: session.has_received_message(), + sessionId: sessionId + }); + }); + }; + + for (var _iterator4 = (0, _getIterator3.default)(sessionIds), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { + _loop(); + } + } catch (err) { + _didIteratorError4 = true; + _iteratorError4 = err; + } finally { + try { + if (!_iteratorNormalCompletion4 && _iterator4.return) { + _iterator4.return(); + } + } finally { + if (_didIteratorError4) { + throw _iteratorError4; + } + } + } + }); + })); + + case 3: + return _context11.abrupt("return", info); + + case 4: + case "end": + return _context11.stop(); + } + } + }, _callee11, this); + })); + + return function (_x13) { + return _ref11.apply(this, arguments); + }; +}(); + +/** + * Encrypt an outgoing message using an existing session + * + * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * remote device + * @param {string} sessionId the id of the active session + * @param {string} payloadString payload to be encrypted and sent + * + * @return {Promise} ciphertext + */ +OlmDevice.prototype.encryptMessage = function () { + var _ref12 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee12(theirDeviceIdentityKey, sessionId, payloadString) { + var _this12 = this; + + var res; + return _regenerator2.default.wrap(function _callee12$(_context12) { + while (1) { + switch (_context12.prev = _context12.next) { + case 0: + checkPayloadLength(payloadString); + + res = void 0; + _context12.next = 4; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_SESSIONS], function (txn) { + _this12._getSession(theirDeviceIdentityKey, sessionId, txn, function (session) { + res = session.encrypt(payloadString); + _this12._saveSession(theirDeviceIdentityKey, session, txn); + }); + })); + + case 4: + return _context12.abrupt("return", res); + + case 5: + case "end": + return _context12.stop(); + } + } + }, _callee12, this); + })); + + return function (_x14, _x15, _x16) { + return _ref12.apply(this, arguments); + }; +}(); + +/** + * Decrypt an incoming message using an existing session + * + * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * remote device + * @param {string} sessionId the id of the active session + * @param {number} messageType messageType field from the received message + * @param {string} ciphertext base64-encoded body from the received message + * + * @return {Promise} decrypted payload. + */ +OlmDevice.prototype.decryptMessage = function () { + var _ref13 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee13(theirDeviceIdentityKey, sessionId, messageType, ciphertext) { + var _this13 = this; + + var payloadString; + return _regenerator2.default.wrap(function _callee13$(_context13) { + while (1) { + switch (_context13.prev = _context13.next) { + case 0: + payloadString = void 0; + _context13.next = 3; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_SESSIONS], function (txn) { + _this13._getSession(theirDeviceIdentityKey, sessionId, txn, function (session) { + payloadString = session.decrypt(messageType, ciphertext); + _this13._saveSession(theirDeviceIdentityKey, session, txn); + }); + })); + + case 3: + return _context13.abrupt("return", payloadString); + + case 4: + case "end": + return _context13.stop(); + } + } + }, _callee13, this); + })); + + return function (_x17, _x18, _x19, _x20) { + return _ref13.apply(this, arguments); + }; +}(); + +/** + * Determine if an incoming messages is a prekey message matching an existing session + * + * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * remote device + * @param {string} sessionId the id of the active session + * @param {number} messageType messageType field from the received message + * @param {string} ciphertext base64-encoded body from the received message + * + * @return {Promise} true if the received message is a prekey message which matches + * the given session. + */ +OlmDevice.prototype.matchesSession = function () { + var _ref14 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee14(theirDeviceIdentityKey, sessionId, messageType, ciphertext) { + var _this14 = this; + + var matches; + return _regenerator2.default.wrap(function _callee14$(_context14) { + while (1) { + switch (_context14.prev = _context14.next) { + case 0: + if (!(messageType !== 0)) { + _context14.next = 2; + break; + } + + return _context14.abrupt("return", false); + + case 2: + matches = void 0; + _context14.next = 5; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore2.default.STORE_SESSIONS], function (txn) { + _this14._getSession(theirDeviceIdentityKey, sessionId, txn, function (session) { + matches = session.matches_inbound(ciphertext); + }); + })); + + case 5: + return _context14.abrupt("return", matches); + + case 6: + case "end": + return _context14.stop(); + } + } + }, _callee14, this); + })); + + return function (_x21, _x22, _x23, _x24) { + return _ref14.apply(this, arguments); + }; +}(); + +// Outbound group session +// ====================== + +/** + * store an OutboundGroupSession in _outboundGroupSessionStore + * + * @param {Olm.OutboundGroupSession} session + * @private + */ +OlmDevice.prototype._saveOutboundGroupSession = function (session) { + var pickledSession = session.pickle(this._pickleKey); + this._outboundGroupSessionStore[session.session_id()] = pickledSession; +}; + +/** + * extract an OutboundGroupSession from _outboundGroupSessionStore and call the + * given function + * + * @param {string} sessionId + * @param {function} func + * @return {object} result of func + * @private + */ +OlmDevice.prototype._getOutboundGroupSession = function (sessionId, func) { + var pickled = this._outboundGroupSessionStore[sessionId]; + if (pickled === null) { + throw new Error("Unknown outbound group session " + sessionId); + } + + var session = new Olm.OutboundGroupSession(); + try { + session.unpickle(this._pickleKey, pickled); + return func(session); + } finally { + session.free(); + } +}; + +/** + * Generate a new outbound group session + * + * @return {string} sessionId for the outbound session. + */ +OlmDevice.prototype.createOutboundGroupSession = function () { + var session = new Olm.OutboundGroupSession(); + try { + session.create(); + this._saveOutboundGroupSession(session); + return session.session_id(); + } finally { + session.free(); + } +}; + +/** + * Encrypt an outgoing message with an outbound group session + * + * @param {string} sessionId the id of the outboundgroupsession + * @param {string} payloadString payload to be encrypted and sent + * + * @return {string} ciphertext + */ +OlmDevice.prototype.encryptGroupMessage = function (sessionId, payloadString) { + var self = this; + + checkPayloadLength(payloadString); + + return this._getOutboundGroupSession(sessionId, function (session) { + var res = session.encrypt(payloadString); + self._saveOutboundGroupSession(session); + return res; + }); +}; + +/** + * Get the session keys for an outbound group session + * + * @param {string} sessionId the id of the outbound group session + * + * @return {{chain_index: number, key: string}} current chain index, and + * base64-encoded secret key. + */ +OlmDevice.prototype.getOutboundGroupSessionKey = function (sessionId) { + return this._getOutboundGroupSession(sessionId, function (session) { + return { + chain_index: session.message_index(), + key: session.session_key() + }; + }); +}; + +// Inbound group session +// ===================== + +/** + * data stored in the session store about an inbound group session + * + * @typedef {Object} InboundGroupSessionData + * @property {string} room_Id + * @property {string} session pickled Olm.InboundGroupSession + * @property {Object} keysClaimed + * @property {Array} forwardingCurve25519KeyChain Devices involved in forwarding + * this session to us (normally empty). + */ + +/** + * Unpickle a session from a sessionData object and invoke the given function. + * The session is valid only until func returns. + * + * @param {Object} sessionData Object describing the session. + * @param {function(Olm.InboundGroupSession)} func Invoked with the unpickled session + * @return {*} result of func + */ +OlmDevice.prototype._unpickleInboundGroupSession = function (sessionData, func) { + var session = new Olm.InboundGroupSession(); + try { + session.unpickle(this._pickleKey, sessionData.session); + return func(session); + } finally { + session.free(); + } +}; + +/** + * extract an InboundGroupSession from the crypto store and call the given function + * + * @param {string} roomId The room ID to extract the session for, or null to fetch + * sessions for any room. + * @param {string} senderKey + * @param {string} sessionId + * @param {*} txn Opaque transaction object from cryptoStore.doTxn() + * @param {function(Olm.InboundGroupSession, InboundGroupSessionData)} func + * function to call. + * + * @private + */ +OlmDevice.prototype._getInboundGroupSession = function (roomId, senderKey, sessionId, txn, func) { + var _this15 = this; + + this._cryptoStore.getEndToEndInboundGroupSession(senderKey, sessionId, txn, function (sessionData) { + if (sessionData === null) { + func(null); + return; + } + + // if we were given a room ID, check that the it matches the original one for the session. This stops + // the HS pretending a message was targeting a different room. + if (roomId !== null && roomId !== sessionData.room_id) { + throw new Error("Mismatched room_id for inbound group session (expected " + sessionData.room_id + ", was " + roomId + ")"); + } + + _this15._unpickleInboundGroupSession(sessionData, function (session) { + func(session, sessionData); + }); + }); +}; + +/** + * Add an inbound group session to the session store + * + * @param {string} roomId room in which this session will be used + * @param {string} senderKey base64-encoded curve25519 key of the sender + * @param {Array} forwardingCurve25519KeyChain Devices involved in forwarding + * this session to us. + * @param {string} sessionId session identifier + * @param {string} sessionKey base64-encoded secret key + * @param {Object} keysClaimed Other keys the sender claims. + * @param {boolean} exportFormat true if the megolm keys are in export format + * (ie, they lack an ed25519 signature) + */ +OlmDevice.prototype.addInboundGroupSession = function () { + var _ref15 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee15(roomId, senderKey, forwardingCurve25519KeyChain, sessionId, sessionKey, keysClaimed, exportFormat) { + var _this16 = this; + + return _regenerator2.default.wrap(function _callee15$(_context15) { + while (1) { + switch (_context15.prev = _context15.next) { + case 0: + _context15.next = 2; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_INBOUND_GROUP_SESSIONS], function (txn) { + /* if we already have this session, consider updating it */ + _this16._getInboundGroupSession(roomId, senderKey, sessionId, txn, function (existingSession, existingSessionData) { + if (existingSession) { + console.log("Update for megolm session " + senderKey + "/" + sessionId); + // for now we just ignore updates. TODO: implement something here + return; + } + + // new session. + var session = new Olm.InboundGroupSession(); + try { + if (exportFormat) { + session.import_session(sessionKey); + } else { + session.create(sessionKey); + } + if (sessionId != session.session_id()) { + throw new Error("Mismatched group session ID from senderKey: " + senderKey); + } + + var sessionData = { + room_id: roomId, + session: session.pickle(_this16._pickleKey), + keysClaimed: keysClaimed, + forwardingCurve25519KeyChain: forwardingCurve25519KeyChain + }; + + _this16._cryptoStore.addEndToEndInboundGroupSession(senderKey, sessionId, sessionData, txn); + } finally { + session.free(); + } + }); + })); + + case 2: + case "end": + return _context15.stop(); + } + } + }, _callee15, this); + })); + + return function (_x25, _x26, _x27, _x28, _x29, _x30, _x31) { + return _ref15.apply(this, arguments); + }; +}(); + +/** + * Decrypt a received message with an inbound group session + * + * @param {string} roomId room in which the message was received + * @param {string} senderKey base64-encoded curve25519 key of the sender + * @param {string} sessionId session identifier + * @param {string} body base64-encoded body of the encrypted message + * @param {string} eventId ID of the event being decrypted + * @param {Number} timestamp timestamp of the event being decrypted + * + * @return {null} the sessionId is unknown + * + * @return {Promise<{result: string, senderKey: string, + * forwardingCurve25519KeyChain: Array, + * keysClaimed: Object}>} + */ +OlmDevice.prototype.decryptGroupMessage = function () { + var _ref16 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee16(roomId, senderKey, sessionId, body, eventId, timestamp) { + var _this17 = this; + + var result; + return _regenerator2.default.wrap(function _callee16$(_context16) { + while (1) { + switch (_context16.prev = _context16.next) { + case 0: + result = void 0; + _context16.next = 3; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_INBOUND_GROUP_SESSIONS], function (txn) { + _this17._getInboundGroupSession(roomId, senderKey, sessionId, txn, function (session, sessionData) { + if (session === null) { + result = null; + return; + } + var res = session.decrypt(body); + + var plaintext = res.plaintext; + if (plaintext === undefined) { + // Compatibility for older olm versions. + plaintext = res; + } else { + // Check if we have seen this message index before to detect replay attacks. + // If the event ID and timestamp are specified, and the match the event ID + // and timestamp from the last time we used this message index, then we + // don't consider it a replay attack. + var messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index; + if (messageIndexKey in _this17._inboundGroupSessionMessageIndexes) { + var msgInfo = _this17._inboundGroupSessionMessageIndexes[messageIndexKey]; + if (msgInfo.id !== eventId || msgInfo.timestamp !== timestamp) { + throw new Error("Duplicate message index, possible replay attack: " + messageIndexKey); + } + } + _this17._inboundGroupSessionMessageIndexes[messageIndexKey] = { + id: eventId, + timestamp: timestamp + }; + } + + sessionData.session = session.pickle(_this17._pickleKey); + _this17._cryptoStore.storeEndToEndInboundGroupSession(senderKey, sessionId, sessionData, txn); + result = { + result: plaintext, + keysClaimed: sessionData.keysClaimed || {}, + senderKey: senderKey, + forwardingCurve25519KeyChain: sessionData.forwardingCurve25519KeyChain || [] + }; + }); + })); + + case 3: + return _context16.abrupt("return", result); + + case 4: + case "end": + return _context16.stop(); + } + } + }, _callee16, this); + })); + + return function (_x32, _x33, _x34, _x35, _x36, _x37) { + return _ref16.apply(this, arguments); + }; +}(); + +/** + * Determine if we have the keys for a given megolm session + * + * @param {string} roomId room in which the message was received + * @param {string} senderKey base64-encoded curve25519 key of the sender + * @param {sring} sessionId session identifier + * + * @returns {Promise} true if we have the keys to this session + */ +OlmDevice.prototype.hasInboundSessionKeys = function () { + var _ref17 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee17(roomId, senderKey, sessionId) { + var _this18 = this; + + var result; + return _regenerator2.default.wrap(function _callee17$(_context17) { + while (1) { + switch (_context17.prev = _context17.next) { + case 0: + result = void 0; + _context17.next = 3; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore2.default.STORE_INBOUND_GROUP_SESSIONS], function (txn) { + _this18._cryptoStore.getEndToEndInboundGroupSession(senderKey, sessionId, txn, function (sessionData) { + if (sessionData === null) { + result = false; + return; + } + + if (roomId !== sessionData.room_id) { + console.warn("requested keys for inbound group session " + senderKey + "|" + (sessionId + ", with incorrect room_id ") + ("(expected " + sessionData.room_id + ", ") + ("was " + roomId + ")")); + result = false; + } else { + result = true; + } + }); + })); + + case 3: + return _context17.abrupt("return", result); + + case 4: + case "end": + return _context17.stop(); + } + } + }, _callee17, this); + })); + + return function (_x38, _x39, _x40) { + return _ref17.apply(this, arguments); + }; +}(); + +/** + * Extract the keys to a given megolm session, for sharing + * + * @param {string} roomId room in which the message was received + * @param {string} senderKey base64-encoded curve25519 key of the sender + * @param {string} sessionId session identifier + * + * @returns {Promise<{chain_index: number, key: string, + * forwarding_curve25519_key_chain: Array, + * sender_claimed_ed25519_key: string + * }>} + * details of the session key. The key is a base64-encoded megolm key in + * export format. + */ +OlmDevice.prototype.getInboundGroupSessionKey = function () { + var _ref18 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee18(roomId, senderKey, sessionId) { + var _this19 = this; + + var result; + return _regenerator2.default.wrap(function _callee18$(_context18) { + while (1) { + switch (_context18.prev = _context18.next) { + case 0: + result = void 0; + _context18.next = 3; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore2.default.STORE_INBOUND_GROUP_SESSIONS], function (txn) { + _this19._getInboundGroupSession(roomId, senderKey, sessionId, txn, function (session, sessionData) { + if (session === null) { + result = null; + return; + } + var messageIndex = session.first_known_index(); + + var claimedKeys = sessionData.keysClaimed || {}; + var senderEd25519Key = claimedKeys.ed25519 || null; + + result = { + "chain_index": messageIndex, + "key": session.export_session(messageIndex), + "forwarding_curve25519_key_chain": sessionData.forwardingCurve25519KeyChain || [], + "sender_claimed_ed25519_key": senderEd25519Key + }; + }); + })); + + case 3: + return _context18.abrupt("return", result); + + case 4: + case "end": + return _context18.stop(); + } + } + }, _callee18, this); + })); + + return function (_x41, _x42, _x43) { + return _ref18.apply(this, arguments); + }; +}(); + +/** + * Export an inbound group session + * + * @param {string} senderKey base64-encoded curve25519 key of the sender + * @param {string} sessionId session identifier + * @param {string} sessionData The session object from the store + * @return {module:crypto/OlmDevice.MegolmSessionData} exported session data + */ +OlmDevice.prototype.exportInboundGroupSession = function (senderKey, sessionId, sessionData) { + return this._unpickleInboundGroupSession(sessionData, function (session) { + var messageIndex = session.first_known_index(); + + return { + "sender_key": senderKey, + "sender_claimed_keys": sessionData.keysClaimed, + "room_id": sessionData.room_id, + "session_id": sessionId, + "session_key": session.export_session(messageIndex), + "forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [] + }; + }); +}; + +// Utilities +// ========= + +/** + * Verify an ed25519 signature. + * + * @param {string} key ed25519 key + * @param {string} message message which was signed + * @param {string} signature base64-encoded signature to be checked + * + * @raises {Error} if there is a problem with the verification. If the key was + * too small then the message will be "OLM.INVALID_BASE64". If the signature + * was invalid then the message will be "OLM.BAD_MESSAGE_MAC". + */ +OlmDevice.prototype.verifySignature = function (key, message, signature) { + this._getUtility(function (util) { + util.ed25519_verify(key, message, signature); + }); +}; + +/** */ +module.exports = OlmDevice; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"./store/indexeddb-crypto-store":19,"babel-runtime/core-js/get-iterator":53,"babel-runtime/core-js/object/keys":61,"babel-runtime/regenerator":73,"bluebird":74}],9:[function(require,module,exports){ +(function (global){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _getIterator2 = require('babel-runtime/core-js/get-iterator'); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +var _createClass2 = require('babel-runtime/helpers/createClass'); + +var _createClass3 = _interopRequireDefault(_createClass2); + +var _bluebird = require('bluebird'); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +var _utils = require('../utils'); + +var _utils2 = _interopRequireDefault(_utils); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Internal module. Management of outgoing room key requests. + * + * See https://docs.google.com/document/d/1m4gQkcnJkxNuBmb5NoFCIadIY-DyqqNAS3lloE73BlQ + * for draft documentation on what we're supposed to be implementing here. + * + * @module + */ + +// delay between deciding we want some keys, and sending out the request, to +// allow for (a) it turning up anyway, (b) grouping requests together +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var SEND_KEY_REQUESTS_DELAY_MS = 500; + +/** possible states for a room key request + * + * The state machine looks like: + * + * | (cancellation sent) + * | .-------------------------------------------------. + * | | | + * V V (cancellation requested) | + * UNSENT -----------------------------+ | + * | | | + * | | | + * | (send successful) | CANCELLATION_PENDING_AND_WILL_RESEND + * V | Λ + * SENT | | + * |-------------------------------- | --------------' + * | | (cancellation requested with intent + * | | to resend the original request) + * | | + * | (cancellation requested) | + * V | + * CANCELLATION_PENDING | + * | | + * | (cancellation sent) | + * V | + * (deleted) <---------------------------+ + * + * @enum {number} + */ +var ROOM_KEY_REQUEST_STATES = { + /** request not yet sent */ + UNSENT: 0, + + /** request sent, awaiting reply */ + SENT: 1, + + /** reply received, cancellation not yet sent */ + CANCELLATION_PENDING: 2, + + /** + * Cancellation not yet sent and will transition to UNSENT instead of + * being deleted once the cancellation has been sent. + */ + CANCELLATION_PENDING_AND_WILL_RESEND: 3 +}; + +var OutgoingRoomKeyRequestManager = function () { + function OutgoingRoomKeyRequestManager(baseApis, deviceId, cryptoStore) { + (0, _classCallCheck3.default)(this, OutgoingRoomKeyRequestManager); + + this._baseApis = baseApis; + this._deviceId = deviceId; + this._cryptoStore = cryptoStore; + + // handle for the delayed call to _sendOutgoingRoomKeyRequests. Non-null + // if the callback has been set, or if it is still running. + this._sendOutgoingRoomKeyRequestsTimer = null; + + // sanity check to ensure that we don't end up with two concurrent runs + // of _sendOutgoingRoomKeyRequests + this._sendOutgoingRoomKeyRequestsRunning = false; + + this._clientRunning = false; + } + + /** + * Called when the client is started. Sets background processes running. + */ + + + (0, _createClass3.default)(OutgoingRoomKeyRequestManager, [{ + key: 'start', + value: function start() { + this._clientRunning = true; + + // set the timer going, to handle any requests which didn't get sent + // on the previous run of the client. + this._startTimer(); + } + + /** + * Called when the client is stopped. Stops any running background processes. + */ + + }, { + key: 'stop', + value: function stop() { + console.log('stopping OutgoingRoomKeyRequestManager'); + // stop the timer on the next run + this._clientRunning = false; + } + + /** + * Send off a room key request, if we haven't already done so. + * + * The `requestBody` is compared (with a deep-equality check) against + * previous queued or sent requests and if it matches, no change is made. + * Otherwise, a request is added to the pending list, and a job is started + * in the background to send it. + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * @param {Array<{userId: string, deviceId: string}>} recipients + * + * @returns {Promise} resolves when the request has been added to the + * pending list (or we have established that a similar request already + * exists) + */ + + }, { + key: 'sendRoomKeyRequest', + value: function sendRoomKeyRequest(requestBody, recipients) { + var _this = this; + + return this._cryptoStore.getOrAddOutgoingRoomKeyRequest({ + requestBody: requestBody, + recipients: recipients, + requestId: this._baseApis.makeTxnId(), + state: ROOM_KEY_REQUEST_STATES.UNSENT + }).then(function (req) { + if (req.state === ROOM_KEY_REQUEST_STATES.UNSENT) { + _this._startTimer(); + } + }); + } + + /** + * Cancel room key requests, if any match the given requestBody + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * @param {boolean} andResend if true, transition to UNSENT instead of + * deleting after sending cancellation. + * + * @returns {Promise} resolves when the request has been updated in our + * pending list. + */ + + }, { + key: 'cancelRoomKeyRequest', + value: function cancelRoomKeyRequest(requestBody) { + var _this2 = this; + + var andResend = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + + return this._cryptoStore.getOutgoingRoomKeyRequest(requestBody).then(function (req) { + if (!req) { + // no request was made for this key + return; + } + switch (req.state) { + case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: + case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND: + // nothing to do here + return; + + case ROOM_KEY_REQUEST_STATES.UNSENT: + // just delete it + + // FIXME: ghahah we may have attempted to send it, and + // not yet got a successful response. So the server + // may have seen it, so we still need to send a cancellation + // in that case :/ + + console.log('deleting unnecessary room key request for ' + stringifyRequestBody(requestBody)); + return _this2._cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT); + + case ROOM_KEY_REQUEST_STATES.SENT: + { + // If `andResend` is set, transition to UNSENT once the + // cancellation has successfully been sent. + var state = andResend ? ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND : ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING; + // send a cancellation. + return _this2._cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, ROOM_KEY_REQUEST_STATES.SENT, { + state: state, + cancellationTxnId: _this2._baseApis.makeTxnId() + }).then(function (updatedReq) { + if (!updatedReq) { + // updateOutgoingRoomKeyRequest couldn't find the + // request in state ROOM_KEY_REQUEST_STATES.SENT, + // so we must have raced with another tab to mark + // the request cancelled. There is no point in + // sending another cancellation since the other tab + // will do it. + console.log('Tried to cancel room key request for ' + stringifyRequestBody(requestBody) + ' but it was already cancelled in another tab'); + return; + } + + // We don't want to wait for the timer, so we send it + // immediately. (We might actually end up racing with the timer, + // but that's ok: even if we make the request twice, we'll do it + // with the same transaction_id, so only one message will get + // sent). + // + // (We also don't want to wait for the response from the server + // here, as it will slow down processing of received keys if we + // do.) + _this2._sendOutgoingRoomKeyRequestCancellation(updatedReq, andResend).catch(function (e) { + console.error("Error sending room key request cancellation;" + " will retry later.", e); + _this2._startTimer(); + }).then(function () { + if (!andResend) return; + // The request has transitioned from + // CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We + // still need to resend the request which is now UNSENT, so + // start the timer if it isn't already started. + _this2._startTimer(); + }); + }); + } + default: + throw new Error('unhandled state: ' + req.state); + } + }); + } + + // start the background timer to send queued requests, if the timer isn't + // already running + + }, { + key: '_startTimer', + value: function _startTimer() { + var _this3 = this; + + if (this._sendOutgoingRoomKeyRequestsTimer) { + return; + } + + var startSendingOutgoingRoomKeyRequests = function startSendingOutgoingRoomKeyRequests() { + if (_this3._sendOutgoingRoomKeyRequestsRunning) { + throw new Error("RoomKeyRequestSend already in progress!"); + } + _this3._sendOutgoingRoomKeyRequestsRunning = true; + + _this3._sendOutgoingRoomKeyRequests().finally(function () { + _this3._sendOutgoingRoomKeyRequestsRunning = false; + }).catch(function (e) { + // this should only happen if there is an indexeddb error, + // in which case we're a bit stuffed anyway. + console.warn('error in OutgoingRoomKeyRequestManager: ' + e); + }).done(); + }; + + this._sendOutgoingRoomKeyRequestsTimer = global.setTimeout(startSendingOutgoingRoomKeyRequests, SEND_KEY_REQUESTS_DELAY_MS); + } + + // look for and send any queued requests. Runs itself recursively until + // there are no more requests, or there is an error (in which case, the + // timer will be restarted before the promise resolves). + + }, { + key: '_sendOutgoingRoomKeyRequests', + value: function _sendOutgoingRoomKeyRequests() { + var _this4 = this; + + if (!this._clientRunning) { + this._sendOutgoingRoomKeyRequestsTimer = null; + return _bluebird2.default.resolve(); + } + + console.log("Looking for queued outgoing room key requests"); + + return this._cryptoStore.getOutgoingRoomKeyRequestByState([ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND, ROOM_KEY_REQUEST_STATES.UNSENT]).then(function (req) { + if (!req) { + console.log("No more outgoing room key requests"); + _this4._sendOutgoingRoomKeyRequestsTimer = null; + return; + } + + var prom = void 0; + switch (req.state) { + case ROOM_KEY_REQUEST_STATES.UNSENT: + prom = _this4._sendOutgoingRoomKeyRequest(req); + break; + case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: + prom = _this4._sendOutgoingRoomKeyRequestCancellation(req); + break; + case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND: + prom = _this4._sendOutgoingRoomKeyRequestCancellation(req, true); + break; + } + + return prom.then(function () { + // go around the loop again + return _this4._sendOutgoingRoomKeyRequests(); + }).catch(function (e) { + console.error("Error sending room key request; will retry later.", e); + _this4._sendOutgoingRoomKeyRequestsTimer = null; + _this4._startTimer(); + }).done(); + }); + } + + // given a RoomKeyRequest, send it and update the request record + + }, { + key: '_sendOutgoingRoomKeyRequest', + value: function _sendOutgoingRoomKeyRequest(req) { + var _this5 = this; + + console.log('Requesting keys for ' + stringifyRequestBody(req.requestBody) + (' from ' + stringifyRecipientList(req.recipients)) + ('(id ' + req.requestId + ')')); + + var requestMessage = { + action: "request", + requesting_device_id: this._deviceId, + request_id: req.requestId, + body: req.requestBody + }; + + return this._sendMessageToDevices(requestMessage, req.recipients, req.requestId).then(function () { + return _this5._cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT, { state: ROOM_KEY_REQUEST_STATES.SENT }); + }); + } + + // Given a RoomKeyRequest, cancel it and delete the request record unless + // andResend is set, in which case transition to UNSENT. + + }, { + key: '_sendOutgoingRoomKeyRequestCancellation', + value: function _sendOutgoingRoomKeyRequestCancellation(req, andResend) { + var _this6 = this; + + console.log('Sending cancellation for key request for ' + (stringifyRequestBody(req.requestBody) + ' to ') + (stringifyRecipientList(req.recipients) + ' ') + ('(cancellation id ' + req.cancellationTxnId + ')')); + + var requestMessage = { + action: "request_cancellation", + requesting_device_id: this._deviceId, + request_id: req.requestId + }; + + return this._sendMessageToDevices(requestMessage, req.recipients, req.cancellationTxnId).then(function () { + if (andResend) { + // We want to resend, so transition to UNSENT + return _this6._cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND, { state: ROOM_KEY_REQUEST_STATES.UNSENT }); + } + return _this6._cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING); + }); + } + + // send a RoomKeyRequest to a list of recipients + + }, { + key: '_sendMessageToDevices', + value: function _sendMessageToDevices(message, recipients, txnId) { + var contentMap = {}; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)(recipients), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var recip = _step.value; + + if (!contentMap[recip.userId]) { + contentMap[recip.userId] = {}; + } + contentMap[recip.userId][recip.deviceId] = message; + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return this._baseApis.sendToDevice('m.room_key_request', contentMap, txnId); + } + }]); + return OutgoingRoomKeyRequestManager; +}(); + +exports.default = OutgoingRoomKeyRequestManager; + + +function stringifyRequestBody(requestBody) { + // we assume that the request is for megolm keys, which are identified by + // room id and session id + return requestBody.room_id + " / " + requestBody.session_id; +} + +function stringifyRecipientList(recipients) { + return '[' + _utils2.default.map(recipients, function (r) { + return r.userId + ':' + r.deviceId; + }).join(",") + ']'; +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"../utils":50,"babel-runtime/core-js/get-iterator":53,"babel-runtime/helpers/classCallCheck":66,"babel-runtime/helpers/createClass":67,"bluebird":74}],10:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _regenerator = require('babel-runtime/regenerator'); + +var _regenerator2 = _interopRequireDefault(_regenerator); + +var _getIterator2 = require('babel-runtime/core-js/get-iterator'); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +var _keys = require('babel-runtime/core-js/object/keys'); + +var _keys2 = _interopRequireDefault(_keys); + +var _bluebird = require('bluebird'); + +var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +var _createClass2 = require('babel-runtime/helpers/createClass'); + +var _createClass3 = _interopRequireDefault(_createClass2); + +var _indexeddbCryptoStore = require('./store/indexeddb-crypto-store'); + +var _indexeddbCryptoStore2 = _interopRequireDefault(_indexeddbCryptoStore); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * @alias module:crypto/RoomList + */ +var RoomList = function () { + function RoomList(cryptoStore, sessionStore) { + (0, _classCallCheck3.default)(this, RoomList); + + this._cryptoStore = cryptoStore; + this._sessionStore = sessionStore; + + // Object of roomId -> room e2e info object (body of the m.room.encryption event) + this._roomEncryption = {}; + } + + (0, _createClass3.default)(RoomList, [{ + key: 'init', + value: function () { + var _ref = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee() { + var _this = this; + + var removeSessionStoreRooms; + return _regenerator2.default.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + removeSessionStoreRooms = false; + _context.next = 3; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_ROOMS], function (txn) { + _this._cryptoStore.getEndToEndRooms(txn, function (result) { + if (result === null || (0, _keys2.default)(result).length === 0) { + // migrate from session store, if there's data there + var sessStoreRooms = _this._sessionStore.getAllEndToEndRooms(); + if (sessStoreRooms !== null) { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)((0, _keys2.default)(sessStoreRooms)), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var roomId = _step.value; + + _this._cryptoStore.storeEndToEndRoom(roomId, sessStoreRooms[roomId], txn); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + } + _this._roomEncryption = sessStoreRooms; + removeSessionStoreRooms = true; + } else { + _this._roomEncryption = result; + } + }); + })); + + case 3: + if (removeSessionStoreRooms) { + this._sessionStore.removeAllEndToEndRooms(); + } + + case 4: + case 'end': + return _context.stop(); + } + } + }, _callee, this); + })); + + function init() { + return _ref.apply(this, arguments); + } + + return init; + }() + }, { + key: 'getRoomEncryption', + value: function getRoomEncryption(roomId) { + return this._roomEncryption[roomId] || null; + } + }, { + key: 'isRoomEncrypted', + value: function isRoomEncrypted(roomId) { + return Boolean(this.getRoomEncryption(roomId)); + } + }, { + key: 'setRoomEncryption', + value: function () { + var _ref2 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee2(roomId, roomInfo) { + var _this2 = this; + + return _regenerator2.default.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + this._roomEncryption[roomId] = roomInfo; + _context2.next = 3; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_ROOMS], function (txn) { + _this2._cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn); + })); + + case 3: + case 'end': + return _context2.stop(); + } + } + }, _callee2, this); + })); + + function setRoomEncryption(_x, _x2) { + return _ref2.apply(this, arguments); + } + + return setRoomEncryption; + }() + }]); + return RoomList; +}(); /* + Copyright 2018 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +/** + * @module crypto/RoomList + * + * Manages the list of encrypted rooms + */ + +exports.default = RoomList; + +},{"./store/indexeddb-crypto-store":19,"babel-runtime/core-js/get-iterator":53,"babel-runtime/core-js/object/keys":61,"babel-runtime/helpers/classCallCheck":66,"babel-runtime/helpers/createClass":67,"babel-runtime/regenerator":73,"bluebird":74}],11:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.UnknownDeviceError = exports.DecryptionError = exports.DecryptionAlgorithm = exports.EncryptionAlgorithm = exports.DECRYPTION_CLASSES = exports.ENCRYPTION_CLASSES = undefined; + +var _keys = require('babel-runtime/core-js/object/keys'); + +var _keys2 = _interopRequireDefault(_keys); + +var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of'); + +var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf); + +var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn'); + +var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2); + +var _inherits2 = require('babel-runtime/helpers/inherits'); + +var _inherits3 = _interopRequireDefault(_inherits2); + +var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +var _createClass2 = require('babel-runtime/helpers/createClass'); + +var _createClass3 = _interopRequireDefault(_createClass2); + +exports.registerAlgorithm = registerAlgorithm; + +var _bluebird = require('bluebird'); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * map of registered encryption algorithm classes. A map from string to {@link + * module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class + * + * @type {Object.} + */ +var ENCRYPTION_CLASSES = exports.ENCRYPTION_CLASSES = {}; + +/** + * map of registered encryption algorithm classes. Map from string to {@link + * module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} class + * + * @type {Object.} + */ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Internal module. Defines the base classes of the encryption implementations + * + * @module + */ + +var DECRYPTION_CLASSES = exports.DECRYPTION_CLASSES = {}; + +/** + * base type for encryption implementations + * + * @alias module:crypto/algorithms/base.EncryptionAlgorithm + * + * @param {object} params parameters + * @param {string} params.userId The UserID for the local user + * @param {string} params.deviceId The identifier for this device. + * @param {module:crypto} params.crypto crypto core + * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper + * @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface + * @param {string} params.roomId The ID of the room we will be sending to + * @param {object} params.config The body of the m.room.encryption event + */ + +var EncryptionAlgorithm = function () { + function EncryptionAlgorithm(params) { + (0, _classCallCheck3.default)(this, EncryptionAlgorithm); + + this._userId = params.userId; + this._deviceId = params.deviceId; + this._crypto = params.crypto; + this._olmDevice = params.olmDevice; + this._baseApis = params.baseApis; + this._roomId = params.roomId; + } + + /** + * Encrypt a message event + * + * @method module:crypto/algorithms/base.EncryptionAlgorithm.encryptMessage + * @abstract + * + * @param {module:models/room} room + * @param {string} eventType + * @param {object} plaintext event content + * + * @return {module:client.Promise} Promise which resolves to the new event body + */ + + /** + * Called when the membership of a member of the room changes. + * + * @param {module:models/event.MatrixEvent} event event causing the change + * @param {module:models/room-member} member user whose membership changed + * @param {string=} oldMembership previous membership + * @public + */ + + + (0, _createClass3.default)(EncryptionAlgorithm, [{ + key: 'onRoomMembership', + value: function onRoomMembership(event, member, oldMembership) {} + }]); + return EncryptionAlgorithm; +}(); + +exports.EncryptionAlgorithm = EncryptionAlgorithm; // https://github.com/jsdoc3/jsdoc/issues/1272 + +/** + * base type for decryption implementations + * + * @alias module:crypto/algorithms/base.DecryptionAlgorithm + * @param {object} params parameters + * @param {string} params.userId The UserID for the local user + * @param {module:crypto} params.crypto crypto core + * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper + * @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface + * @param {string=} params.roomId The ID of the room we will be receiving + * from. Null for to-device events. + */ + +var DecryptionAlgorithm = function () { + function DecryptionAlgorithm(params) { + (0, _classCallCheck3.default)(this, DecryptionAlgorithm); + + this._userId = params.userId; + this._crypto = params.crypto; + this._olmDevice = params.olmDevice; + this._baseApis = params.baseApis; + this._roomId = params.roomId; + } + + /** + * Decrypt an event + * + * @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent + * @abstract + * + * @param {MatrixEvent} event undecrypted event + * + * @return {Promise} promise which + * resolves once we have finished decrypting. Rejects with an + * `algorithms.DecryptionError` if there is a problem decrypting the event. + */ + + /** + * Handle a key event + * + * @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent + * + * @param {module:models/event.MatrixEvent} params event key event + */ + + + (0, _createClass3.default)(DecryptionAlgorithm, [{ + key: 'onRoomKeyEvent', + value: function onRoomKeyEvent(params) {} + // ignore by default + + + /** + * Import a room key + * + * @param {module:crypto/OlmDevice.MegolmSessionData} session + */ + + }, { + key: 'importRoomKey', + value: function importRoomKey(session) {} + // ignore by default + + + /** + * Determine if we have the keys necessary to respond to a room key request + * + * @param {module:crypto~IncomingRoomKeyRequest} keyRequest + * @return {Promise} true if we have the keys and could (theoretically) share + * them; else false. + */ + + }, { + key: 'hasKeysForKeyRequest', + value: function hasKeysForKeyRequest(keyRequest) { + return _bluebird2.default.resolve(false); + } + + /** + * Send the response to a room key request + * + * @param {module:crypto~IncomingRoomKeyRequest} keyRequest + */ + + }, { + key: 'shareKeysWithDevice', + value: function shareKeysWithDevice(keyRequest) { + throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm"); + } + }]); + return DecryptionAlgorithm; +}(); + +exports.DecryptionAlgorithm = DecryptionAlgorithm; // https://github.com/jsdoc3/jsdoc/issues/1272 + +/** + * Exception thrown when decryption fails + * + * @alias module:crypto/algorithms/base.DecryptionError + * @param {string} msg user-visible message describing the problem + * + * @param {Object=} details key/value pairs reported in the logs but not shown + * to the user. + * + * @extends Error + */ + +var DecryptionError = function (_Error) { + (0, _inherits3.default)(DecryptionError, _Error); + + function DecryptionError(msg, details) { + (0, _classCallCheck3.default)(this, DecryptionError); + + var _this = (0, _possibleConstructorReturn3.default)(this, (DecryptionError.__proto__ || (0, _getPrototypeOf2.default)(DecryptionError)).call(this, msg)); + + _this.name = 'DecryptionError'; + _this.detailedString = _detailedStringForDecryptionError(_this, details); + return _this; + } + + return DecryptionError; +}(Error); + +exports.DecryptionError = DecryptionError; // https://github.com/jsdoc3/jsdoc/issues/1272 + +function _detailedStringForDecryptionError(err, details) { + var result = err.name + '[msg: ' + err.message; + + if (details) { + result += ', ' + (0, _keys2.default)(details).map(function (k) { + return k + ': ' + details[k]; + }).join(', '); + } + + result += ']'; + + return result; +} + +/** + * Exception thrown specifically when we want to warn the user to consider + * the security of their conversation before continuing + * + * @param {string} msg message describing the problem + * @param {Object} devices userId -> {deviceId -> object} + * set of unknown devices per user we're warning about + * @extends Error + */ + +var UnknownDeviceError = exports.UnknownDeviceError = function (_Error2) { + (0, _inherits3.default)(UnknownDeviceError, _Error2); + + function UnknownDeviceError(msg, devices) { + (0, _classCallCheck3.default)(this, UnknownDeviceError); + + var _this2 = (0, _possibleConstructorReturn3.default)(this, (UnknownDeviceError.__proto__ || (0, _getPrototypeOf2.default)(UnknownDeviceError)).call(this, msg)); + + _this2.name = "UnknownDeviceError"; + _this2.devices = devices; + return _this2; + } + + return UnknownDeviceError; +}(Error); + +/** + * Registers an encryption/decryption class for a particular algorithm + * + * @param {string} algorithm algorithm tag to register for + * + * @param {class} encryptor {@link + * module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} + * implementation + * + * @param {class} decryptor {@link + * module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} + * implementation + */ + + +function registerAlgorithm(algorithm, encryptor, decryptor) { + ENCRYPTION_CLASSES[algorithm] = encryptor; + DECRYPTION_CLASSES[algorithm] = decryptor; +} + +},{"babel-runtime/core-js/object/get-prototype-of":60,"babel-runtime/core-js/object/keys":61,"babel-runtime/helpers/classCallCheck":66,"babel-runtime/helpers/createClass":67,"babel-runtime/helpers/inherits":69,"babel-runtime/helpers/possibleConstructorReturn":70,"bluebird":74}],12:[function(require,module,exports){ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** + * @module crypto/algorithms + */ + +var base = require("./base"); + +require("./olm"); +require("./megolm"); + +/** + * @see module:crypto/algorithms/base.ENCRYPTION_CLASSES + */ +module.exports.ENCRYPTION_CLASSES = base.ENCRYPTION_CLASSES; + +/** + * @see module:crypto/algorithms/base.DECRYPTION_CLASSES + */ +module.exports.DECRYPTION_CLASSES = base.DECRYPTION_CLASSES; + +/** + * @see module:crypto/algorithms/base.DecryptionError + */ +module.exports.DecryptionError = base.DecryptionError; + +},{"./base":11,"./megolm":13,"./olm":14}],13:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** + * Defines m.olm encryption/decryption + * + * @module crypto/algorithms/megolm + */ + +var _defineProperty2 = require("babel-runtime/helpers/defineProperty"); + +var _defineProperty3 = _interopRequireDefault(_defineProperty2); + +var _set = require("babel-runtime/core-js/set"); + +var _set2 = _interopRequireDefault(_set); + +var _stringify = require("babel-runtime/core-js/json/stringify"); + +var _stringify2 = _interopRequireDefault(_stringify); + +var _keys = require("babel-runtime/core-js/object/keys"); + +var _keys2 = _interopRequireDefault(_keys); + +var _getIterator2 = require("babel-runtime/core-js/get-iterator"); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +var _regenerator = require("babel-runtime/regenerator"); + +var _regenerator2 = _interopRequireDefault(_regenerator); + +var _bluebird = require("bluebird"); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var utils = require("../../utils"); +var olmlib = require("../olmlib"); +var base = require("./base"); + +/** + * @private + * @constructor + * + * @param {string} sessionId + * + * @property {string} sessionId + * @property {Number} useCount number of times this session has been used + * @property {Number} creationTime when the session was created (ms since the epoch) + * + * @property {object} sharedWithDevices + * devices with which we have shared the session key + * userId -> {deviceId -> msgindex} + */ +function OutboundSessionInfo(sessionId) { + this.sessionId = sessionId; + this.useCount = 0; + this.creationTime = new Date().getTime(); + this.sharedWithDevices = {}; +} + +/** + * Check if it's time to rotate the session + * + * @param {Number} rotationPeriodMsgs + * @param {Number} rotationPeriodMs + * @return {Boolean} + */ +OutboundSessionInfo.prototype.needsRotation = function (rotationPeriodMsgs, rotationPeriodMs) { + var sessionLifetime = new Date().getTime() - this.creationTime; + + if (this.useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) { + console.log("Rotating megolm session after " + this.useCount + " messages, " + sessionLifetime + "ms"); + return true; + } + + return false; +}; + +OutboundSessionInfo.prototype.markSharedWithDevice = function (userId, deviceId, chainIndex) { + if (!this.sharedWithDevices[userId]) { + this.sharedWithDevices[userId] = {}; + } + this.sharedWithDevices[userId][deviceId] = chainIndex; +}; + +/** + * Determine if this session has been shared with devices which it shouldn't + * have been. + * + * @param {Object} devicesInRoom userId -> {deviceId -> object} + * devices we should shared the session with. + * + * @return {Boolean} true if we have shared the session with devices which aren't + * in devicesInRoom. + */ +OutboundSessionInfo.prototype.sharedWithTooManyDevices = function (devicesInRoom) { + for (var userId in this.sharedWithDevices) { + if (!this.sharedWithDevices.hasOwnProperty(userId)) { + continue; + } + + if (!devicesInRoom.hasOwnProperty(userId)) { + console.log("Starting new session because we shared with " + userId); + return true; + } + + for (var deviceId in this.sharedWithDevices[userId]) { + if (!this.sharedWithDevices[userId].hasOwnProperty(deviceId)) { + continue; + } + + if (!devicesInRoom[userId].hasOwnProperty(deviceId)) { + console.log("Starting new session because we shared with " + userId + ":" + deviceId); + return true; + } + } + } +}; + +/** + * Megolm encryption implementation + * + * @constructor + * @extends {module:crypto/algorithms/base.EncryptionAlgorithm} + * + * @param {object} params parameters, as per + * {@link module:crypto/algorithms/base.EncryptionAlgorithm} + */ +function MegolmEncryption(params) { + base.EncryptionAlgorithm.call(this, params); + + // the most recent attempt to set up a session. This is used to serialise + // the session setups, so that we have a race-free view of which session we + // are using, and which devices we have shared the keys with. It resolves + // with an OutboundSessionInfo (or undefined, for the first message in the + // room). + this._setupPromise = _bluebird2.default.resolve(); + + // default rotation periods + this._sessionRotationPeriodMsgs = 100; + this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000; + + if (params.config.rotation_period_ms !== undefined) { + this._sessionRotationPeriodMs = params.config.rotation_period_ms; + } + + if (params.config.rotation_period_msgs !== undefined) { + this._sessionRotationPeriodMsgs = params.config.rotation_period_msgs; + } +} +utils.inherits(MegolmEncryption, base.EncryptionAlgorithm); + +/** + * @private + * + * @param {Object} devicesInRoom The devices in this room, indexed by user ID + * + * @return {module:client.Promise} Promise which resolves to the + * OutboundSessionInfo when setup is complete. + */ +MegolmEncryption.prototype._ensureOutboundSession = function (devicesInRoom) { + + // takes the previous OutboundSessionInfo, and considers whether to create + // a new one. Also shares the key with any (new) devices in the room. + // Updates `session` to hold the final OutboundSessionInfo. + // + // returns a promise which resolves once the keyshare is successful. + var prepareSession = function () { + var _ref = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee(oldSession) { + var shareMap, userId, userDevices, deviceId, deviceInfo, key; + return _regenerator2.default.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + session = oldSession; + + // need to make a brand new session? + if (session && session.needsRotation(self._sessionRotationPeriodMsgs, self._sessionRotationPeriodMs)) { + console.log("Starting new megolm session because we need to rotate."); + session = null; + } + + // determine if we have shared with anyone we shouldn't have + if (session && session.sharedWithTooManyDevices(devicesInRoom)) { + session = null; + } + + if (session) { + _context.next = 8; + break; + } + + console.log("Starting new megolm session for room " + self._roomId); + _context.next = 7; + return (0, _bluebird.resolve)(self._prepareNewSession()); + + case 7: + session = _context.sent; + + case 8: + + // now check if we need to share with any devices + shareMap = {}; + _context.t0 = _regenerator2.default.keys(devicesInRoom); + + case 10: + if ((_context.t1 = _context.t0()).done) { + _context.next = 29; + break; + } + + userId = _context.t1.value; + + if (devicesInRoom.hasOwnProperty(userId)) { + _context.next = 14; + break; + } + + return _context.abrupt("continue", 10); + + case 14: + userDevices = devicesInRoom[userId]; + _context.t2 = _regenerator2.default.keys(userDevices); + + case 16: + if ((_context.t3 = _context.t2()).done) { + _context.next = 27; + break; + } + + deviceId = _context.t3.value; + + if (userDevices.hasOwnProperty(deviceId)) { + _context.next = 20; + break; + } + + return _context.abrupt("continue", 16); + + case 20: + deviceInfo = userDevices[deviceId]; + key = deviceInfo.getIdentityKey(); + + if (!(key == self._olmDevice.deviceCurve25519Key)) { + _context.next = 24; + break; + } + + return _context.abrupt("continue", 16); + + case 24: + + if (!session.sharedWithDevices[userId] || session.sharedWithDevices[userId][deviceId] === undefined) { + shareMap[userId] = shareMap[userId] || []; + shareMap[userId].push(deviceInfo); + } + _context.next = 16; + break; + + case 27: + _context.next = 10; + break; + + case 29: + return _context.abrupt("return", self._shareKeyWithDevices(session, shareMap)); + + case 30: + case "end": + return _context.stop(); + } + } + }, _callee, this); + })); + + return function prepareSession(_x) { + return _ref.apply(this, arguments); + }; + }(); + + // helper which returns the session prepared by prepareSession + + + var self = this; + + var session = void 0;function returnSession() { + return session; + } + + // first wait for the previous share to complete + var prom = this._setupPromise.then(prepareSession); + + // _setupPromise resolves to `session` whether or not the share succeeds + this._setupPromise = prom.then(returnSession, returnSession); + + // but we return a promise which only resolves if the share was successful. + return prom.then(returnSession); +}; + +/** + * @private + * + * @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session + */ +MegolmEncryption.prototype._prepareNewSession = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee2() { + var sessionId, key; + return _regenerator2.default.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + sessionId = this._olmDevice.createOutboundGroupSession(); + key = this._olmDevice.getOutboundGroupSessionKey(sessionId); + _context2.next = 4; + return (0, _bluebird.resolve)(this._olmDevice.addInboundGroupSession(this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId, key.key, { ed25519: this._olmDevice.deviceEd25519Key })); + + case 4: + return _context2.abrupt("return", new OutboundSessionInfo(sessionId)); + + case 5: + case "end": + return _context2.stop(); + } + } + }, _callee2, this); +})); + +/** + * @private + * + * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session + * + * @param {number} chainIndex current chain index + * + * @param {object} devicemap + * mapping from userId to deviceId to {@link module:crypto~OlmSessionResult} + * + * @param {object} devicesByUser + * map from userid to list of devices + * + * @return {array>} + */ +MegolmEncryption.prototype._splitUserDeviceMap = function (session, chainIndex, devicemap, devicesByUser) { + var maxToDeviceMessagesPerRequest = 20; + + // use an array where the slices of a content map gets stored + var mapSlices = []; + var currentSliceId = 0; // start inserting in the first slice + var entriesInCurrentSlice = 0; + + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)((0, _keys2.default)(devicesByUser)), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var userId = _step.value; + + var devicesToShareWith = devicesByUser[userId]; + var sessionResults = devicemap[userId]; + + for (var i = 0; i < devicesToShareWith.length; i++) { + var deviceInfo = devicesToShareWith[i]; + var deviceId = deviceInfo.deviceId; + + var sessionResult = sessionResults[deviceId]; + if (!sessionResult.sessionId) { + // no session with this device, probably because there + // were no one-time keys. + // + // we could send them a to_device message anyway, as a + // signal that they have missed out on the key sharing + // message because of the lack of keys, but there's not + // much point in that really; it will mostly serve to clog + // up to_device inboxes. + + // mark this device as "handled" because we don't want to try + // to claim a one-time-key for dead devices on every message. + session.markSharedWithDevice(userId, deviceId, chainIndex); + + // ensureOlmSessionsForUsers has already done the logging, + // so just skip it. + continue; + } + + console.log("share keys with device " + userId + ":" + deviceId); + + if (entriesInCurrentSlice > maxToDeviceMessagesPerRequest) { + // the current slice is filled up. Start inserting into the next slice + entriesInCurrentSlice = 0; + currentSliceId++; + } + if (!mapSlices[currentSliceId]) { + mapSlices[currentSliceId] = []; + } + + mapSlices[currentSliceId].push({ + userId: userId, + deviceInfo: deviceInfo + }); + + entriesInCurrentSlice++; + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return mapSlices; +}; + +/** + * @private + * + * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session + * + * @param {number} chainIndex current chain index + * + * @param {object} userDeviceMap + * mapping from userId to deviceInfo + * + * @param {object} payload fields to include in the encrypted payload + * + * @return {module:client.Promise} Promise which resolves once the key sharing + * for the given userDeviceMap is generated and has been sent. + */ +MegolmEncryption.prototype._encryptAndSendKeysToDevices = function (session, chainIndex, userDeviceMap, payload) { + var _this = this; + + var encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + ciphertext: {} + }; + var contentMap = {}; + + var promises = []; + for (var i = 0; i < userDeviceMap.length; i++) { + var val = userDeviceMap[i]; + var userId = val.userId; + var deviceInfo = val.deviceInfo; + var deviceId = deviceInfo.deviceId; + + if (!contentMap[userId]) { + contentMap[userId] = {}; + } + contentMap[userId][deviceId] = encryptedContent; + + promises.push(olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this._userId, this._deviceId, this._olmDevice, userId, deviceInfo, payload)); + } + + return _bluebird2.default.all(promises).then(function () { + return _this._baseApis.sendToDevice("m.room.encrypted", contentMap).then(function () { + // store that we successfully uploaded the keys of the current slice + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = (0, _getIterator3.default)((0, _keys2.default)(contentMap)), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var _userId = _step2.value; + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; + + try { + for (var _iterator3 = (0, _getIterator3.default)((0, _keys2.default)(contentMap[_userId])), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var _deviceId = _step3.value; + + session.markSharedWithDevice(_userId, _deviceId, chainIndex); + } + } catch (err) { + _didIteratorError3 = true; + _iteratorError3 = err; + } finally { + try { + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); + } + } finally { + if (_didIteratorError3) { + throw _iteratorError3; + } + } + } + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + }); + }); +}; + +/** + * @private + * + * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session + * + * @param {object} devicesByUser + * map from userid to list of devices + */ +MegolmEncryption.prototype._shareKeyWithDevices = function () { + var _ref3 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee3(session, devicesByUser) { + var key, payload, devicemap, userDeviceMaps, i; + return _regenerator2.default.wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId); + payload = { + type: "m.room_key", + content: { + algorithm: olmlib.MEGOLM_ALGORITHM, + room_id: this._roomId, + session_id: session.sessionId, + session_key: key.key, + chain_index: key.chain_index + } + }; + _context3.next = 4; + return (0, _bluebird.resolve)(olmlib.ensureOlmSessionsForDevices(this._olmDevice, this._baseApis, devicesByUser)); + + case 4: + devicemap = _context3.sent; + userDeviceMaps = this._splitUserDeviceMap(session, key.chain_index, devicemap, devicesByUser); + i = 0; + + case 7: + if (!(i < userDeviceMaps.length)) { + _context3.next = 21; + break; + } + + _context3.prev = 8; + _context3.next = 11; + return (0, _bluebird.resolve)(this._encryptAndSendKeysToDevices(session, key.chain_index, userDeviceMaps[i], payload)); + + case 11: + console.log("Completed megolm keyshare in " + this._roomId + " " + ("(slice " + (i + 1) + "/" + userDeviceMaps.length + ")")); + _context3.next = 18; + break; + + case 14: + _context3.prev = 14; + _context3.t0 = _context3["catch"](8); + + console.log("megolm keyshare in " + this._roomId + " " + ("(slice " + (i + 1) + "/" + userDeviceMaps.length + ") failed")); + + throw _context3.t0; + + case 18: + i++; + _context3.next = 7; + break; + + case 21: + case "end": + return _context3.stop(); + } + } + }, _callee3, this, [[8, 14]]); + })); + + return function (_x2, _x3) { + return _ref3.apply(this, arguments); + }; +}(); + +/** + * @inheritdoc + * + * @param {module:models/room} room + * @param {string} eventType + * @param {object} content plaintext event content + * + * @return {module:client.Promise} Promise which resolves to the new event body + */ +MegolmEncryption.prototype.encryptMessage = function (room, eventType, content) { + var self = this; + console.log("Starting to encrypt event for " + this._roomId); + + return this._getDevicesInRoom(room).then(function (devicesInRoom) { + // check if any of these devices are not yet known to the user. + // if so, warn the user so they can verify or ignore. + self._checkForUnknownDevices(devicesInRoom); + + return self._ensureOutboundSession(devicesInRoom); + }).then(function (session) { + var payloadJson = { + room_id: self._roomId, + type: eventType, + content: content + }; + + var ciphertext = self._olmDevice.encryptGroupMessage(session.sessionId, (0, _stringify2.default)(payloadJson)); + + var encryptedContent = { + algorithm: olmlib.MEGOLM_ALGORITHM, + sender_key: self._olmDevice.deviceCurve25519Key, + ciphertext: ciphertext, + session_id: session.sessionId, + // Include our device ID so that recipients can send us a + // m.new_device message if they don't have our session key. + device_id: self._deviceId + }; + + session.useCount++; + return encryptedContent; + }); +}; + +/** + * Checks the devices we're about to send to and see if any are entirely + * unknown to the user. If so, warn the user, and mark them as known to + * give the user a chance to go verify them before re-sending this message. + * + * @param {Object} devicesInRoom userId -> {deviceId -> object} + * devices we should shared the session with. + */ +MegolmEncryption.prototype._checkForUnknownDevices = function (devicesInRoom) { + var unknownDevices = {}; + + (0, _keys2.default)(devicesInRoom).forEach(function (userId) { + (0, _keys2.default)(devicesInRoom[userId]).forEach(function (deviceId) { + var device = devicesInRoom[userId][deviceId]; + if (device.isUnverified() && !device.isKnown()) { + if (!unknownDevices[userId]) { + unknownDevices[userId] = {}; + } + unknownDevices[userId][deviceId] = device; + } + }); + }); + + if ((0, _keys2.default)(unknownDevices).length) { + // it'd be kind to pass unknownDevices up to the user in this error + throw new base.UnknownDeviceError("This room contains unknown devices which have not been verified. " + "We strongly recommend you verify them before continuing.", unknownDevices); + } +}; + +/** + * Get the list of unblocked devices for all users in the room + * + * @param {module:models/room} room + * + * @return {module:client.Promise} Promise which resolves to a map + * from userId to deviceId to deviceInfo + */ +MegolmEncryption.prototype._getDevicesInRoom = function (room) { + // XXX what about rooms where invitees can see the content? + var roomMembers = utils.map(room.getJoinedMembers(), function (u) { + return u.userId; + }); + + // The global value is treated as a default for when rooms don't specify a value. + var isBlacklisting = this._crypto.getGlobalBlacklistUnverifiedDevices(); + if (typeof room.getBlacklistUnverifiedDevices() === 'boolean') { + isBlacklisting = room.getBlacklistUnverifiedDevices(); + } + + // We are happy to use a cached version here: we assume that if we already + // have a list of the user's devices, then we already share an e2e room + // with them, which means that they will have announced any new devices via + // an m.new_device. + // + // XXX: what if the cache is stale, and the user left the room we had in + // common and then added new devices before joining this one? --Matthew + // + // yup, see https://github.com/vector-im/riot-web/issues/2305 --richvdh + return this._crypto.downloadKeys(roomMembers, false).then(function (devices) { + // remove any blocked devices + for (var userId in devices) { + if (!devices.hasOwnProperty(userId)) { + continue; + } + + var userDevices = devices[userId]; + for (var deviceId in userDevices) { + if (!userDevices.hasOwnProperty(deviceId)) { + continue; + } + + if (userDevices[deviceId].isBlocked() || userDevices[deviceId].isUnverified() && isBlacklisting) { + delete userDevices[deviceId]; + } + } + } + + return devices; + }); +}; + +/** + * Megolm decryption implementation + * + * @constructor + * @extends {module:crypto/algorithms/base.DecryptionAlgorithm} + * + * @param {object} params parameters, as per + * {@link module:crypto/algorithms/base.DecryptionAlgorithm} + */ +function MegolmDecryption(params) { + base.DecryptionAlgorithm.call(this, params); + + // events which we couldn't decrypt due to unknown sessions / indexes: map from + // senderKey|sessionId to Set of MatrixEvents + this._pendingEvents = {}; + + // this gets stubbed out by the unit tests. + this.olmlib = olmlib; +} +utils.inherits(MegolmDecryption, base.DecryptionAlgorithm); + +/** + * @inheritdoc + * + * @param {MatrixEvent} event + * + * returns a promise which resolves to a + * {@link module:crypto~EventDecryptionResult} once we have finished + * decrypting, or rejects with an `algorithms.DecryptionError` if there is a + * problem decrypting the event. + */ +MegolmDecryption.prototype.decryptEvent = function () { + var _ref4 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee4(event) { + var content, res, payload; + return _regenerator2.default.wrap(function _callee4$(_context4) { + while (1) { + switch (_context4.prev = _context4.next) { + case 0: + content = event.getWireContent(); + + if (!(!content.sender_key || !content.session_id || !content.ciphertext)) { + _context4.next = 3; + break; + } + + throw new base.DecryptionError("Missing fields in input"); + + case 3: + + // we add the event to the pending list *before* we start decryption. + // + // then, if the key turns up while decryption is in progress (and + // decryption fails), we will schedule a retry. + // (fixes https://github.com/vector-im/riot-web/issues/5001) + this._addEventToPendingList(event); + + res = void 0; + _context4.prev = 5; + _context4.next = 8; + return (0, _bluebird.resolve)(this._olmDevice.decryptGroupMessage(event.getRoomId(), content.sender_key, content.session_id, content.ciphertext, event.getId(), event.getTs())); + + case 8: + res = _context4.sent; + _context4.next = 15; + break; + + case 11: + _context4.prev = 11; + _context4.t0 = _context4["catch"](5); + + if (_context4.t0.message === 'OLM.UNKNOWN_MESSAGE_INDEX') { + this._requestKeysForEvent(event); + } + throw new base.DecryptionError(_context4.t0.toString(), { + session: content.sender_key + '|' + content.session_id + }); + + case 15: + if (!(res === null)) { + _context4.next = 18; + break; + } + + // We've got a message for a session we don't have. + // + // (XXX: We might actually have received this key since we started + // decrypting, in which case we'll have scheduled a retry, and this + // request will be redundant. We could probably check to see if the + // event is still in the pending list; if not, a retry will have been + // scheduled, so we needn't send out the request here.) + this._requestKeysForEvent(event); + throw new base.DecryptionError("The sender's device has not sent us the keys for this message.", { + session: content.sender_key + '|' + content.session_id + }); + + case 18: + + // success. We can remove the event from the pending list, if that hasn't + // already happened. + this._removeEventFromPendingList(event); + + payload = JSON.parse(res.result); + + // belt-and-braces check that the room id matches that indicated by the HS + // (this is somewhat redundant, since the megolm session is scoped to the + // room, so neither the sender nor a MITM can lie about the room_id). + + if (!(payload.room_id !== event.getRoomId())) { + _context4.next = 22; + break; + } + + throw new base.DecryptionError("Message intended for room " + payload.room_id); + + case 22: + return _context4.abrupt("return", { + clearEvent: payload, + senderCurve25519Key: res.senderKey, + claimedEd25519Key: res.keysClaimed.ed25519, + forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain + }); + + case 23: + case "end": + return _context4.stop(); + } + } + }, _callee4, this, [[5, 11]]); + })); + + return function (_x4) { + return _ref4.apply(this, arguments); + }; +}(); + +MegolmDecryption.prototype._requestKeysForEvent = function (event) { + var sender = event.getSender(); + var wireContent = event.getWireContent(); + + // send the request to all of our own devices, and the + // original sending device if it wasn't us. + var recipients = [{ + userId: this._userId, deviceId: '*' + }]; + if (sender != this._userId) { + recipients.push({ + userId: sender, deviceId: wireContent.device_id + }); + } + + this._crypto.requestRoomKey({ + room_id: event.getRoomId(), + algorithm: wireContent.algorithm, + sender_key: wireContent.sender_key, + session_id: wireContent.session_id + }, recipients); +}; + +/** + * Add an event to the list of those awaiting their session keys. + * + * @private + * + * @param {module:models/event.MatrixEvent} event + */ +MegolmDecryption.prototype._addEventToPendingList = function (event) { + var content = event.getWireContent(); + var k = content.sender_key + "|" + content.session_id; + if (!this._pendingEvents[k]) { + this._pendingEvents[k] = new _set2.default(); + } + this._pendingEvents[k].add(event); +}; + +/** + * Remove an event from the list of those awaiting their session keys. + * + * @private + * + * @param {module:models/event.MatrixEvent} event + */ +MegolmDecryption.prototype._removeEventFromPendingList = function (event) { + var content = event.getWireContent(); + var k = content.sender_key + "|" + content.session_id; + if (!this._pendingEvents[k]) { + return; + } + + this._pendingEvents[k].delete(event); + if (this._pendingEvents[k].size === 0) { + delete this._pendingEvents[k]; + } +}; + +/** + * @inheritdoc + * + * @param {module:models/event.MatrixEvent} event key event + */ +MegolmDecryption.prototype.onRoomKeyEvent = function (event) { + var _this2 = this; + + var content = event.getContent(); + var sessionId = content.session_id; + var senderKey = event.getSenderKey(); + var forwardingKeyChain = []; + var exportFormat = false; + var keysClaimed = void 0; + + if (!content.room_id || !sessionId || !content.session_key) { + console.error("key event is missing fields"); + return; + } + + if (!senderKey) { + console.error("key event has no sender key (not encrypted?)"); + return; + } + + if (event.getType() == "m.forwarded_room_key") { + exportFormat = true; + forwardingKeyChain = content.forwarding_curve25519_key_chain; + if (!utils.isArray(forwardingKeyChain)) { + forwardingKeyChain = []; + } + + // copy content before we modify it + forwardingKeyChain = forwardingKeyChain.slice(); + forwardingKeyChain.push(senderKey); + + senderKey = content.sender_key; + if (!senderKey) { + console.error("forwarded_room_key event is missing sender_key field"); + return; + } + + var ed25519Key = content.sender_claimed_ed25519_key; + if (!ed25519Key) { + console.error("forwarded_room_key_event is missing sender_claimed_ed25519_key field"); + return; + } + + keysClaimed = { + ed25519: ed25519Key + }; + } else { + keysClaimed = event.getKeysClaimed(); + } + + console.log("Adding key for megolm session " + senderKey + "|" + sessionId); + this._olmDevice.addInboundGroupSession(content.room_id, senderKey, forwardingKeyChain, sessionId, content.session_key, keysClaimed, exportFormat).then(function () { + // cancel any outstanding room key requests for this session + _this2._crypto.cancelRoomKeyRequest({ + algorithm: content.algorithm, + room_id: content.room_id, + session_id: content.session_id, + sender_key: senderKey + }); + + // have another go at decrypting events sent with this session. + _this2._retryDecryption(senderKey, sessionId); + }).catch(function (e) { + console.error("Error handling m.room_key_event: " + e); + }); +}; + +/** + * @inheritdoc + */ +MegolmDecryption.prototype.hasKeysForKeyRequest = function (keyRequest) { + var body = keyRequest.requestBody; + + return this._olmDevice.hasInboundSessionKeys(body.room_id, body.sender_key, body.session_id + // TODO: ratchet index + ); +}; + +/** + * @inheritdoc + */ +MegolmDecryption.prototype.shareKeysWithDevice = function (keyRequest) { + var _this3 = this; + + var userId = keyRequest.userId; + var deviceId = keyRequest.deviceId; + var deviceInfo = this._crypto.getStoredDevice(userId, deviceId); + var body = keyRequest.requestBody; + + this.olmlib.ensureOlmSessionsForDevices(this._olmDevice, this._baseApis, (0, _defineProperty3.default)({}, userId, [deviceInfo])).then(function (devicemap) { + var olmSessionResult = devicemap[userId][deviceId]; + if (!olmSessionResult.sessionId) { + // no session with this device, probably because there + // were no one-time keys. + // + // ensureOlmSessionsForUsers has already done the logging, + // so just skip it. + return null; + } + + console.log("sharing keys for session " + body.sender_key + "|" + body.session_id + " with device " + userId + ":" + deviceId); + + return _this3._buildKeyForwardingMessage(body.room_id, body.sender_key, body.session_id); + }).then(function (payload) { + var encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: _this3._olmDevice.deviceCurve25519Key, + ciphertext: {} + }; + + return _this3.olmlib.encryptMessageForDevice(encryptedContent.ciphertext, _this3._userId, _this3._deviceId, _this3._olmDevice, userId, deviceInfo, payload).then(function () { + var contentMap = (0, _defineProperty3.default)({}, userId, (0, _defineProperty3.default)({}, deviceId, encryptedContent)); + + // TODO: retries + return _this3._baseApis.sendToDevice("m.room.encrypted", contentMap); + }); + }).done(); +}; + +MegolmDecryption.prototype._buildKeyForwardingMessage = function () { + var _ref5 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee5(roomId, senderKey, sessionId) { + var key; + return _regenerator2.default.wrap(function _callee5$(_context5) { + while (1) { + switch (_context5.prev = _context5.next) { + case 0: + _context5.next = 2; + return (0, _bluebird.resolve)(this._olmDevice.getInboundGroupSessionKey(roomId, senderKey, sessionId)); + + case 2: + key = _context5.sent; + return _context5.abrupt("return", { + type: "m.forwarded_room_key", + content: { + algorithm: olmlib.MEGOLM_ALGORITHM, + room_id: roomId, + sender_key: senderKey, + sender_claimed_ed25519_key: key.sender_claimed_ed25519_key, + session_id: sessionId, + session_key: key.key, + chain_index: key.chain_index, + forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain + } + }); + + case 4: + case "end": + return _context5.stop(); + } + } + }, _callee5, this); + })); + + return function (_x5, _x6, _x7) { + return _ref5.apply(this, arguments); + }; +}(); + +/** + * @inheritdoc + * + * @param {module:crypto/OlmDevice.MegolmSessionData} session + */ +MegolmDecryption.prototype.importRoomKey = function (session) { + var _this4 = this; + + return this._olmDevice.addInboundGroupSession(session.room_id, session.sender_key, session.forwarding_curve25519_key_chain, session.session_id, session.session_key, session.sender_claimed_keys, true).then(function () { + // have another go at decrypting events sent with this session. + _this4._retryDecryption(session.sender_key, session.session_id); + }); +}; + +/** + * Have another go at decrypting events after we receive a key + * + * @private + * @param {String} senderKey + * @param {String} sessionId + */ +MegolmDecryption.prototype._retryDecryption = function (senderKey, sessionId) { + var k = senderKey + "|" + sessionId; + var pending = this._pendingEvents[k]; + if (!pending) { + return; + } + + delete this._pendingEvents[k]; + + var _iteratorNormalCompletion4 = true; + var _didIteratorError4 = false; + var _iteratorError4 = undefined; + + try { + for (var _iterator4 = (0, _getIterator3.default)(pending), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { + var ev = _step4.value; + + ev.attemptDecryption(this._crypto); + } + } catch (err) { + _didIteratorError4 = true; + _iteratorError4 = err; + } finally { + try { + if (!_iteratorNormalCompletion4 && _iterator4.return) { + _iterator4.return(); + } + } finally { + if (_didIteratorError4) { + throw _iteratorError4; + } + } + } +}; + +base.registerAlgorithm(olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption); + +},{"../../utils":50,"../olmlib":17,"./base":11,"babel-runtime/core-js/get-iterator":53,"babel-runtime/core-js/json/stringify":55,"babel-runtime/core-js/object/keys":61,"babel-runtime/core-js/set":63,"babel-runtime/helpers/defineProperty":68,"babel-runtime/regenerator":73,"bluebird":74}],14:[function(require,module,exports){ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** + * Defines m.olm encryption/decryption + * + * @module crypto/algorithms/olm + */ + +var _stringify = require("babel-runtime/core-js/json/stringify"); + +var _stringify2 = _interopRequireDefault(_stringify); + +var _regenerator = require("babel-runtime/regenerator"); + +var _regenerator2 = _interopRequireDefault(_regenerator); + +var _bluebird = require("bluebird"); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var utils = require("../../utils"); +var olmlib = require("../olmlib"); +var DeviceInfo = require("../deviceinfo"); +var DeviceVerification = DeviceInfo.DeviceVerification; + +var base = require("./base"); + +/** + * Olm encryption implementation + * + * @constructor + * @extends {module:crypto/algorithms/base.EncryptionAlgorithm} + * + * @param {object} params parameters, as per + * {@link module:crypto/algorithms/base.EncryptionAlgorithm} + */ +function OlmEncryption(params) { + base.EncryptionAlgorithm.call(this, params); + this._sessionPrepared = false; + this._prepPromise = null; +} +utils.inherits(OlmEncryption, base.EncryptionAlgorithm); + +/** + * @private + + * @param {string[]} roomMembers list of currently-joined users in the room + * @return {module:client.Promise} Promise which resolves when setup is complete + */ +OlmEncryption.prototype._ensureSession = function (roomMembers) { + if (this._prepPromise) { + // prep already in progress + return this._prepPromise; + } + + if (this._sessionPrepared) { + // prep already done + return _bluebird2.default.resolve(); + } + + var self = this; + this._prepPromise = self._crypto.downloadKeys(roomMembers).then(function (res) { + return self._crypto.ensureOlmSessionsForUsers(roomMembers); + }).then(function () { + self._sessionPrepared = true; + }).finally(function () { + self._prepPromise = null; + }); + return this._prepPromise; +}; + +/** + * @inheritdoc + * + * @param {module:models/room} room + * @param {string} eventType + * @param {object} content plaintext event content + * + * @return {module:client.Promise} Promise which resolves to the new event body + */ +OlmEncryption.prototype.encryptMessage = function (room, eventType, content) { + // pick the list of recipients based on the membership list. + // + // TODO: there is a race condition here! What if a new user turns up + // just as you are sending a secret message? + + var users = utils.map(room.getJoinedMembers(), function (u) { + return u.userId; + }); + + var self = this; + return this._ensureSession(users).then(function () { + var payloadFields = { + room_id: room.roomId, + type: eventType, + content: content + }; + + var encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: self._olmDevice.deviceCurve25519Key, + ciphertext: {} + }; + + var promises = []; + + for (var i = 0; i < users.length; ++i) { + var userId = users[i]; + var devices = self._crypto.getStoredDevicesForUser(userId); + + for (var j = 0; j < devices.length; ++j) { + var deviceInfo = devices[j]; + var key = deviceInfo.getIdentityKey(); + if (key == self._olmDevice.deviceCurve25519Key) { + // don't bother sending to ourself + continue; + } + if (deviceInfo.verified == DeviceVerification.BLOCKED) { + // don't bother setting up sessions with blocked users + continue; + } + + promises.push(olmlib.encryptMessageForDevice(encryptedContent.ciphertext, self._userId, self._deviceId, self._olmDevice, userId, deviceInfo, payloadFields)); + } + } + + return _bluebird2.default.all(promises).return(encryptedContent); + }); +}; + +/** + * Olm decryption implementation + * + * @constructor + * @extends {module:crypto/algorithms/base.DecryptionAlgorithm} + * @param {object} params parameters, as per + * {@link module:crypto/algorithms/base.DecryptionAlgorithm} + */ +function OlmDecryption(params) { + base.DecryptionAlgorithm.call(this, params); +} +utils.inherits(OlmDecryption, base.DecryptionAlgorithm); + +/** + * @inheritdoc + * + * @param {MatrixEvent} event + * + * returns a promise which resolves to a + * {@link module:crypto~EventDecryptionResult} once we have finished + * decrypting. Rejects with an `algorithms.DecryptionError` if there is a + * problem decrypting the event. + */ +OlmDecryption.prototype.decryptEvent = function () { + var _ref = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee(event) { + var content, deviceKey, ciphertext, message, payloadString, payload, claimedKeys; + return _regenerator2.default.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + content = event.getWireContent(); + deviceKey = content.sender_key; + ciphertext = content.ciphertext; + + if (ciphertext) { + _context.next = 5; + break; + } + + throw new base.DecryptionError("Missing ciphertext"); + + case 5: + if (this._olmDevice.deviceCurve25519Key in ciphertext) { + _context.next = 7; + break; + } + + throw new base.DecryptionError("Not included in recipients"); + + case 7: + message = ciphertext[this._olmDevice.deviceCurve25519Key]; + payloadString = void 0; + _context.prev = 9; + _context.next = 12; + return (0, _bluebird.resolve)(this._decryptMessage(deviceKey, message)); + + case 12: + payloadString = _context.sent; + _context.next = 18; + break; + + case 15: + _context.prev = 15; + _context.t0 = _context["catch"](9); + throw new base.DecryptionError("Bad Encrypted Message", { + sender: deviceKey, + err: _context.t0 + }); + + case 18: + payload = JSON.parse(payloadString); + + // check that we were the intended recipient, to avoid unknown-key attack + // https://github.com/vector-im/vector-web/issues/2483 + + if (!(payload.recipient != this._userId)) { + _context.next = 21; + break; + } + + throw new base.DecryptionError("Message was intented for " + payload.recipient); + + case 21: + if (!(payload.recipient_keys.ed25519 != this._olmDevice.deviceEd25519Key)) { + _context.next = 23; + break; + } + + throw new base.DecryptionError("Message not intended for this device", { + intended: payload.recipient_keys.ed25519, + our_key: this._olmDevice.deviceEd25519Key + }); + + case 23: + if (!(payload.sender != event.getSender())) { + _context.next = 25; + break; + } + + throw new base.DecryptionError("Message forwarded from " + payload.sender, { + reported_sender: event.getSender() + }); + + case 25: + if (!(payload.room_id !== event.getRoomId())) { + _context.next = 27; + break; + } + + throw new base.DecryptionError("Message intended for room " + payload.room_id, { + reported_room: event.room_id + }); + + case 27: + claimedKeys = payload.keys || {}; + return _context.abrupt("return", { + clearEvent: payload, + senderCurve25519Key: deviceKey, + claimedEd25519Key: claimedKeys.ed25519 || null + }); + + case 29: + case "end": + return _context.stop(); + } + } + }, _callee, this, [[9, 15]]); + })); + + return function (_x) { + return _ref.apply(this, arguments); + }; +}(); + +/** + * Attempt to decrypt an Olm message + * + * @param {string} theirDeviceIdentityKey Curve25519 identity key of the sender + * @param {object} message message object, with 'type' and 'body' fields + * + * @return {string} payload, if decrypted successfully. + */ +OlmDecryption.prototype._decryptMessage = function () { + var _ref2 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee2(theirDeviceIdentityKey, message) { + var sessionIds, decryptionErrors, i, sessionId, payload, foundSession, res; + return _regenerator2.default.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + _context2.next = 2; + return (0, _bluebird.resolve)(this._olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey)); + + case 2: + sessionIds = _context2.sent; + + + // try each session in turn. + decryptionErrors = {}; + i = 0; + + case 5: + if (!(i < sessionIds.length)) { + _context2.next = 26; + break; + } + + sessionId = sessionIds[i]; + _context2.prev = 7; + _context2.next = 10; + return (0, _bluebird.resolve)(this._olmDevice.decryptMessage(theirDeviceIdentityKey, sessionId, message.type, message.body)); + + case 10: + payload = _context2.sent; + + console.log("Decrypted Olm message from " + theirDeviceIdentityKey + " with session " + sessionId); + return _context2.abrupt("return", payload); + + case 15: + _context2.prev = 15; + _context2.t0 = _context2["catch"](7); + _context2.next = 19; + return (0, _bluebird.resolve)(this._olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, message.type, message.body)); + + case 19: + foundSession = _context2.sent; + + if (!foundSession) { + _context2.next = 22; + break; + } + + throw new Error("Error decrypting prekey message with existing session id " + sessionId + ": " + _context2.t0.message); + + case 22: + + // otherwise it's probably a message for another session; carry on, but + // keep a record of the error + decryptionErrors[sessionId] = _context2.t0.message; + + case 23: + i++; + _context2.next = 5; + break; + + case 26: + if (!(message.type !== 0)) { + _context2.next = 30; + break; + } + + if (!(sessionIds.length === 0)) { + _context2.next = 29; + break; + } + + throw new Error("No existing sessions"); + + case 29: + throw new Error("Error decrypting non-prekey message with existing sessions: " + (0, _stringify2.default)(decryptionErrors)); + + case 30: + + // prekey message which doesn't match any existing sessions: make a new + // session. + + res = void 0; + _context2.prev = 31; + _context2.next = 34; + return (0, _bluebird.resolve)(this._olmDevice.createInboundSession(theirDeviceIdentityKey, message.type, message.body)); + + case 34: + res = _context2.sent; + _context2.next = 41; + break; + + case 37: + _context2.prev = 37; + _context2.t1 = _context2["catch"](31); + + decryptionErrors["(new)"] = _context2.t1.message; + throw new Error("Error decrypting prekey message: " + (0, _stringify2.default)(decryptionErrors)); + + case 41: + + console.log("created new inbound Olm session ID " + res.session_id + " with " + theirDeviceIdentityKey); + return _context2.abrupt("return", res.payload); + + case 43: + case "end": + return _context2.stop(); + } + } + }, _callee2, this, [[7, 15], [31, 37]]); + })); + + return function (_x2, _x3) { + return _ref2.apply(this, arguments); + }; +}(); + +base.registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption); + +},{"../../utils":50,"../deviceinfo":15,"../olmlib":17,"./base":11,"babel-runtime/core-js/json/stringify":55,"babel-runtime/regenerator":73,"bluebird":74}],15:[function(require,module,exports){ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** + * @module crypto/deviceinfo + */ + +/** + * Information about a user's device + * + * @constructor + * @alias module:crypto/deviceinfo + * + * @property {string} deviceId the ID of this device + * + * @property {string[]} algorithms list of algorithms supported by this device + * + * @property {Object.} keys a map from + * <key type>:<id> -> <base64-encoded key>> + * + * @property {module:crypto/deviceinfo.DeviceVerification} verified + * whether the device has been verified/blocked by the user + * + * @property {boolean} known + * whether the user knows of this device's existence (useful when warning + * the user that a user has added new devices) + * + * @property {Object} unsigned additional data from the homeserver + * + * @param {string} deviceId id of the device + */ + +function DeviceInfo(deviceId) { + // you can't change the deviceId + Object.defineProperty(this, 'deviceId', { + enumerable: true, + value: deviceId + }); + + this.algorithms = []; + this.keys = {}; + this.verified = DeviceVerification.UNVERIFIED; + this.known = false; + this.unsigned = {}; +} + +/** + * rehydrate a DeviceInfo from the session store + * + * @param {object} obj raw object from session store + * @param {string} deviceId id of the device + * + * @return {module:crypto~DeviceInfo} new DeviceInfo + */ +DeviceInfo.fromStorage = function (obj, deviceId) { + var res = new DeviceInfo(deviceId); + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + res[prop] = obj[prop]; + } + } + return res; +}; + +/** + * Prepare a DeviceInfo for JSON serialisation in the session store + * + * @return {object} deviceinfo with non-serialised members removed + */ +DeviceInfo.prototype.toStorage = function () { + return { + algorithms: this.algorithms, + keys: this.keys, + verified: this.verified, + known: this.known, + unsigned: this.unsigned + }; +}; + +/** + * Get the fingerprint for this device (ie, the Ed25519 key) + * + * @return {string} base64-encoded fingerprint of this device + */ +DeviceInfo.prototype.getFingerprint = function () { + return this.keys["ed25519:" + this.deviceId]; +}; + +/** + * Get the identity key for this device (ie, the Curve25519 key) + * + * @return {string} base64-encoded identity key of this device + */ +DeviceInfo.prototype.getIdentityKey = function () { + return this.keys["curve25519:" + this.deviceId]; +}; + +/** + * Get the configured display name for this device, if any + * + * @return {string?} displayname + */ +DeviceInfo.prototype.getDisplayName = function () { + return this.unsigned.device_display_name || null; +}; + +/** + * Returns true if this device is blocked + * + * @return {Boolean} true if blocked + */ +DeviceInfo.prototype.isBlocked = function () { + return this.verified == DeviceVerification.BLOCKED; +}; + +/** + * Returns true if this device is verified + * + * @return {Boolean} true if verified + */ +DeviceInfo.prototype.isVerified = function () { + return this.verified == DeviceVerification.VERIFIED; +}; + +/** + * Returns true if this device is unverified + * + * @return {Boolean} true if unverified + */ +DeviceInfo.prototype.isUnverified = function () { + return this.verified == DeviceVerification.UNVERIFIED; +}; + +/** + * Returns true if the user knows about this device's existence + * + * @return {Boolean} true if known + */ +DeviceInfo.prototype.isKnown = function () { + return this.known == true; +}; + +/** + * @enum + */ +DeviceInfo.DeviceVerification = { + VERIFIED: 1, + UNVERIFIED: 0, + BLOCKED: -1 +}; + +var DeviceVerification = DeviceInfo.DeviceVerification; + +/** */ +module.exports = DeviceInfo; + +},{}],16:[function(require,module,exports){ +/* +Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** + * @module crypto + */ + +var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +var _getIterator2 = require('babel-runtime/core-js/get-iterator'); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +var _set = require('babel-runtime/core-js/set'); + +var _set2 = _interopRequireDefault(_set); + +var _assign = require('babel-runtime/core-js/object/assign'); + +var _assign2 = _interopRequireDefault(_assign); + +var _stringify = require('babel-runtime/core-js/json/stringify'); + +var _stringify2 = _interopRequireDefault(_stringify); + +var _bluebird = require('bluebird'); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +var _regenerator = require('babel-runtime/regenerator'); + +var _regenerator2 = _interopRequireDefault(_regenerator); + +// returns a promise which resolves to the response +var _uploadOneTimeKeys = function () { + var _ref2 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee2(crypto) { + var oneTimeKeys, oneTimeJson, promises, keyId, k, res; + return _regenerator2.default.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + _context2.next = 2; + return (0, _bluebird.resolve)(crypto._olmDevice.getOneTimeKeys()); + + case 2: + oneTimeKeys = _context2.sent; + oneTimeJson = {}; + promises = []; + + + for (keyId in oneTimeKeys.curve25519) { + if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) { + k = { + key: oneTimeKeys.curve25519[keyId] + }; + + oneTimeJson["signed_curve25519:" + keyId] = k; + promises.push(crypto._signObject(k)); + } + } + + _context2.next = 8; + return (0, _bluebird.resolve)(_bluebird2.default.all(promises)); + + case 8: + _context2.next = 10; + return (0, _bluebird.resolve)(crypto._baseApis.uploadKeysRequest({ + one_time_keys: oneTimeJson + }, { + // for now, we set the device id explicitly, as we may not be using the + // same one as used in login. + device_id: crypto._deviceId + })); + + case 10: + res = _context2.sent; + _context2.next = 13; + return (0, _bluebird.resolve)(crypto._olmDevice.markKeysAsPublished()); + + case 13: + return _context2.abrupt('return', res); + + case 14: + case 'end': + return _context2.stop(); + } + } + }, _callee2, this); + })); + + return function _uploadOneTimeKeys(_x) { + return _ref2.apply(this, arguments); + }; +}(); + +/** + * Download the keys for a list of users and stores the keys in the session + * store. + * @param {Array} userIds The users to fetch. + * @param {bool} forceDownload Always download the keys even if cached. + * + * @return {Promise} A promise which resolves to a map userId->deviceId->{@link + * module:crypto/deviceinfo|DeviceInfo}. + */ + + +var _events = require('events'); + +var _OutgoingRoomKeyRequestManager = require('./OutgoingRoomKeyRequestManager'); + +var _OutgoingRoomKeyRequestManager2 = _interopRequireDefault(_OutgoingRoomKeyRequestManager); + +var _indexeddbCryptoStore = require('./store/indexeddb-crypto-store'); + +var _indexeddbCryptoStore2 = _interopRequireDefault(_indexeddbCryptoStore); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var anotherjson = require('another-json'); + + +var utils = require("../utils"); +var OlmDevice = require("./OlmDevice"); +var olmlib = require("./olmlib"); +var algorithms = require("./algorithms"); +var DeviceInfo = require("./deviceinfo"); +var DeviceVerification = DeviceInfo.DeviceVerification; +var DeviceList = require('./DeviceList').default; + +/** + * Cryptography bits + * + * This module is internal to the js-sdk; the public API is via MatrixClient. + * + * @constructor + * @alias module:crypto + * + * @internal + * + * @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface + * + * @param {module:store/session/webstorage~WebStorageSessionStore} sessionStore + * Store to be used for end-to-end crypto session data + * + * @param {string} userId The user ID for the local user + * + * @param {string} deviceId The identifier for this device. + * + * @param {Object} clientStore the MatrixClient data store. + * + * @param {module:crypto/store/base~CryptoStore} cryptoStore + * storage for the crypto layer. + * + * @param {RoomList} roomList An initialised RoomList object + */ +function Crypto(baseApis, sessionStore, userId, deviceId, clientStore, cryptoStore, roomList) { + this._baseApis = baseApis; + this._sessionStore = sessionStore; + this._userId = userId; + this._deviceId = deviceId; + this._clientStore = clientStore; + this._cryptoStore = cryptoStore; + this._roomList = roomList; + + this._olmDevice = new OlmDevice(sessionStore, cryptoStore); + this._deviceList = new DeviceList(baseApis, cryptoStore, sessionStore, this._olmDevice); + + // the last time we did a check for the number of one-time-keys on the + // server. + this._lastOneTimeKeyCheck = null; + this._oneTimeKeyCheckInProgress = false; + + // EncryptionAlgorithm instance for each room + this._roomEncryptors = {}; + + // map from algorithm to DecryptionAlgorithm instance, for each room + this._roomDecryptors = {}; + + this._supportedAlgorithms = utils.keys(algorithms.DECRYPTION_CLASSES); + + this._deviceKeys = {}; + + this._globalBlacklistUnverifiedDevices = false; + + this._outgoingRoomKeyRequestManager = new _OutgoingRoomKeyRequestManager2.default(baseApis, this._deviceId, this._cryptoStore); + + // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations + // we received in the current sync. + this._receivedRoomKeyRequests = []; + this._receivedRoomKeyRequestCancellations = []; + // true if we are currently processing received room key requests + this._processingRoomKeyRequests = false; +} +utils.inherits(Crypto, _events.EventEmitter); + +/** + * Initialise the crypto module so that it is ready for use + * + * Returns a promise which resolves once the crypto module is ready for use. + */ +Crypto.prototype.init = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee() { + var _this = this; + + var sessionStoreHasAccount, cryptoStoreHasAccount, myDevices, deviceInfo; + return _regenerator2.default.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + sessionStoreHasAccount = Boolean(this._sessionStore.getEndToEndAccount()); + cryptoStoreHasAccount = void 0; + _context.next = 4; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore2.default.STORE_ACCOUNT], function (txn) { + _this._cryptoStore.getAccount(txn, function (pickledAccount) { + cryptoStoreHasAccount = Boolean(pickledAccount); + }); + })); + + case 4: + if (sessionStoreHasAccount && !cryptoStoreHasAccount) { + // we're about to migrate to the crypto store + this.emit("crypto.warning", 'CRYPTO_WARNING_ACCOUNT_MIGRATED'); + } else if (sessionStoreHasAccount && cryptoStoreHasAccount) { + // There's an account in both stores: an old version of + // the code has been run against this store. + this.emit("crypto.warning", 'CRYPTO_WARNING_OLD_VERSION_DETECTED'); + } + + _context.next = 7; + return (0, _bluebird.resolve)(this._olmDevice.init()); + + case 7: + _context.next = 9; + return (0, _bluebird.resolve)(this._deviceList.load()); + + case 9: + + // build our device keys: these will later be uploaded + this._deviceKeys["ed25519:" + this._deviceId] = this._olmDevice.deviceEd25519Key; + this._deviceKeys["curve25519:" + this._deviceId] = this._olmDevice.deviceCurve25519Key; + + myDevices = this._deviceList.getRawStoredDevicesForUser(this._userId); + + + if (!myDevices) { + myDevices = {}; + } + + if (!myDevices[this._deviceId]) { + // add our own deviceinfo to the sessionstore + deviceInfo = { + keys: this._deviceKeys, + algorithms: this._supportedAlgorithms, + verified: DeviceVerification.VERIFIED, + known: true + }; + + + myDevices[this._deviceId] = deviceInfo; + this._deviceList.storeDevicesForUser(this._userId, myDevices); + this._deviceList.saveIfDirty(); + } + + case 14: + case 'end': + return _context.stop(); + } + } + }, _callee, this); +})); + +/** + * Tell the crypto module to register for MatrixClient events which it needs to + * listen for + * + * @param {external:EventEmitter} eventEmitter event source where we can register + * for event notifications + */ +Crypto.prototype.registerEventHandlers = function (eventEmitter) { + var crypto = this; + + eventEmitter.on("RoomMember.membership", function (event, member, oldMembership) { + try { + crypto._onRoomMembership(event, member, oldMembership); + } catch (e) { + console.error("Error handling membership change:", e); + } + }); + + eventEmitter.on("toDeviceEvent", function (event) { + crypto._onToDeviceEvent(event); + }); +}; + +/** Start background processes related to crypto */ +Crypto.prototype.start = function () { + this._outgoingRoomKeyRequestManager.start(); +}; + +/** Stop background processes related to crypto */ +Crypto.prototype.stop = function () { + this._outgoingRoomKeyRequestManager.stop(); +}; + +/** + * @return {string} The version of Olm. + */ +Crypto.getOlmVersion = function () { + return OlmDevice.getOlmVersion(); +}; + +/** + * Get the Ed25519 key for this device + * + * @return {string} base64-encoded ed25519 key. + */ +Crypto.prototype.getDeviceEd25519Key = function () { + return this._olmDevice.deviceEd25519Key; +}; + +/** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. This provides the default for rooms which + * do not specify a value. + * + * @param {boolean} value whether to blacklist all unverified devices by default + */ +Crypto.prototype.setGlobalBlacklistUnverifiedDevices = function (value) { + this._globalBlacklistUnverifiedDevices = value; +}; + +/** + * @return {boolean} whether to blacklist all unverified devices by default + */ +Crypto.prototype.getGlobalBlacklistUnverifiedDevices = function () { + return this._globalBlacklistUnverifiedDevices; +}; + +/** + * Upload the device keys to the homeserver. + * @return {object} A promise that will resolve when the keys are uploaded. + */ +Crypto.prototype.uploadDeviceKeys = function () { + var crypto = this; + var userId = crypto._userId; + var deviceId = crypto._deviceId; + + var deviceKeys = { + algorithms: crypto._supportedAlgorithms, + device_id: deviceId, + keys: crypto._deviceKeys, + user_id: userId + }; + + return crypto._signObject(deviceKeys).then(function () { + crypto._baseApis.uploadKeysRequest({ + device_keys: deviceKeys + }, { + // for now, we set the device id explicitly, as we may not be using the + // same one as used in login. + device_id: deviceId + }); + }); +}; + +/** + * Stores the current one_time_key count which will be handled later (in a call of + * onSyncCompleted). The count is e.g. coming from a /sync response. + * + * @param {Number} currentCount The current count of one_time_keys to be stored + */ +Crypto.prototype.updateOneTimeKeyCount = function (currentCount) { + if (isFinite(currentCount)) { + this._oneTimeKeyCount = currentCount; + } else { + throw new TypeError("Parameter for updateOneTimeKeyCount has to be a number"); + } +}; + +// check if it's time to upload one-time keys, and do so if so. +function _maybeUploadOneTimeKeys(crypto) { + // frequency with which to check & upload one-time keys + var uploadPeriod = 1000 * 60; // one minute + + // max number of keys to upload at once + // Creating keys can be an expensive operation so we limit the + // number we generate in one go to avoid blocking the application + // for too long. + var maxKeysPerCycle = 5; + + if (crypto._oneTimeKeyCheckInProgress) { + return; + } + + var now = Date.now(); + if (crypto._lastOneTimeKeyCheck !== null && now - crypto._lastOneTimeKeyCheck < uploadPeriod) { + // we've done a key upload recently. + return; + } + + crypto._lastOneTimeKeyCheck = now; + + // We need to keep a pool of one time public keys on the server so that + // other devices can start conversations with us. But we can only store + // a finite number of private keys in the olm Account object. + // To complicate things further then can be a delay between a device + // claiming a public one time key from the server and it sending us a + // message. We need to keep the corresponding private key locally until + // we receive the message. + // But that message might never arrive leaving us stuck with duff + // private keys clogging up our local storage. + // So we need some kind of enginering compromise to balance all of + // these factors. + + // Check how many keys we can store in the Account object. + var maxOneTimeKeys = crypto._olmDevice.maxNumberOfOneTimeKeys(); + // Try to keep at most half that number on the server. This leaves the + // rest of the slots free to hold keys that have been claimed from the + // server but we haven't recevied a message for. + // If we run out of slots when generating new keys then olm will + // discard the oldest private keys first. This will eventually clean + // out stale private keys that won't receive a message. + var keyLimit = Math.floor(maxOneTimeKeys / 2); + + function uploadLoop(keyCount) { + if (keyLimit <= keyCount) { + // If we don't need to generate any more keys then we are done. + return _bluebird2.default.resolve(); + } + + var keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle); + + // Ask olm to generate new one time keys, then upload them to synapse. + return crypto._olmDevice.generateOneTimeKeys(keysThisLoop).then(function () { + return _uploadOneTimeKeys(crypto); + }).then(function (res) { + if (res.one_time_key_counts && res.one_time_key_counts.signed_curve25519) { + // if the response contains a more up to date value use this + // for the next loop + return uploadLoop(res.one_time_key_counts.signed_curve25519); + } else { + throw new Error("response for uploading keys does not contain " + "one_time_key_counts.signed_curve25519"); + } + }); + } + + crypto._oneTimeKeyCheckInProgress = true; + _bluebird2.default.resolve().then(function () { + if (crypto._oneTimeKeyCount !== undefined) { + // We already have the current one_time_key count from a /sync response. + // Use this value instead of asking the server for the current key count. + return _bluebird2.default.resolve(crypto._oneTimeKeyCount); + } + // ask the server how many keys we have + return crypto._baseApis.uploadKeysRequest({}, { + device_id: crypto._deviceId + }).then(function (res) { + return res.one_time_key_counts.signed_curve25519 || 0; + }); + }).then(function (keyCount) { + // Start the uploadLoop with the current keyCount. The function checks if + // we need to upload new keys or not. + // If there are too many keys on the server then we don't need to + // create any more keys. + return uploadLoop(keyCount); + }).catch(function (e) { + console.error("Error uploading one-time keys", e.stack || e); + }).finally(function () { + // reset _oneTimeKeyCount to prevent start uploading based on old data. + // it will be set again on the next /sync-response + crypto._oneTimeKeyCount = undefined; + crypto._oneTimeKeyCheckInProgress = false; + }).done(); +}Crypto.prototype.downloadKeys = function (userIds, forceDownload) { + return this._deviceList.downloadKeys(userIds, forceDownload); +}; + +/** + * Get the stored device keys for a user id + * + * @param {string} userId the user to list keys for. + * + * @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't + * managed to get a list of devices for this user yet. + */ +Crypto.prototype.getStoredDevicesForUser = function (userId) { + return this._deviceList.getStoredDevicesForUser(userId); +}; + +/** + * Get the stored keys for a single device + * + * @param {string} userId + * @param {string} deviceId + * + * @return {module:crypto/deviceinfo?} device, or undefined + * if we don't know about this device + */ +Crypto.prototype.getStoredDevice = function (userId, deviceId) { + return this._deviceList.getStoredDevice(userId, deviceId); +}; + +/** + * Save the device list, if necessary + * + * @param {integer} delay Time in ms before which the save actually happens. + * By default, the save is delayed for a short period in order to batch + * multiple writes, but this behaviour can be disabled by passing 0. + * + * @return {Promise} true if the data was saved, false if + * it was not (eg. because no changes were pending). The promise + * will only resolve once the data is saved, so may take some time + * to resolve. + */ +Crypto.prototype.saveDeviceList = function (delay) { + return this._deviceList.saveIfDirty(delay); +}; + +/** + * Update the blocked/verified state of the given device + * + * @param {string} userId owner of the device + * @param {string} deviceId unique identifier for the device + * + * @param {?boolean} verified whether to mark the device as verified. Null to + * leave unchanged. + * + * @param {?boolean} blocked whether to mark the device as blocked. Null to + * leave unchanged. + * + * @param {?boolean} known whether to mark that the user has been made aware of + * the existence of this device. Null to leave unchanged + * + * @return {Promise} updated DeviceInfo + */ +Crypto.prototype.setDeviceVerification = function () { + var _ref3 = (0, _bluebird.method)(function (userId, deviceId, verified, blocked, known) { + var devices = this._deviceList.getRawStoredDevicesForUser(userId); + if (!devices || !devices[deviceId]) { + throw new Error("Unknown device " + userId + ":" + deviceId); + } + + var dev = devices[deviceId]; + var verificationStatus = dev.verified; + + if (verified) { + verificationStatus = DeviceVerification.VERIFIED; + } else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) { + verificationStatus = DeviceVerification.UNVERIFIED; + } + + if (blocked) { + verificationStatus = DeviceVerification.BLOCKED; + } else if (blocked !== null && verificationStatus == DeviceVerification.BLOCKED) { + verificationStatus = DeviceVerification.UNVERIFIED; + } + + var knownStatus = dev.known; + if (known !== null && known !== undefined) { + knownStatus = known; + } + + if (dev.verified !== verificationStatus || dev.known !== knownStatus) { + dev.verified = verificationStatus; + dev.known = knownStatus; + this._deviceList.storeDevicesForUser(userId, devices); + this._deviceList.saveIfDirty(); + } + return DeviceInfo.fromStorage(dev, deviceId); + }); + + return function (_x2, _x3, _x4, _x5, _x6) { + return _ref3.apply(this, arguments); + }; +}(); + +/** + * Get information on the active olm sessions with a user + *

+ * Returns a map from device id to an object with keys 'deviceIdKey' (the + * device's curve25519 identity key) and 'sessions' (an array of objects in the + * same format as that returned by + * {@link module:crypto/OlmDevice#getSessionInfoForDevice}). + *

+ * This method is provided for debugging purposes. + * + * @param {string} userId id of user to inspect + * + * @return {Promise>} + */ +Crypto.prototype.getOlmSessionsForUser = function () { + var _ref4 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee3(userId) { + var devices, result, j, device, deviceKey, sessions; + return _regenerator2.default.wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + devices = this.getStoredDevicesForUser(userId) || []; + result = {}; + j = 0; + + case 3: + if (!(j < devices.length)) { + _context3.next = 13; + break; + } + + device = devices[j]; + deviceKey = device.getIdentityKey(); + _context3.next = 8; + return (0, _bluebird.resolve)(this._olmDevice.getSessionInfoForDevice(deviceKey)); + + case 8: + sessions = _context3.sent; + + + result[device.deviceId] = { + deviceIdKey: deviceKey, + sessions: sessions + }; + + case 10: + ++j; + _context3.next = 3; + break; + + case 13: + return _context3.abrupt('return', result); + + case 14: + case 'end': + return _context3.stop(); + } + } + }, _callee3, this); + })); + + return function (_x7) { + return _ref4.apply(this, arguments); + }; +}(); + +/** + * Get the device which sent an event + * + * @param {module:models/event.MatrixEvent} event event to be checked + * + * @return {module:crypto/deviceinfo?} + */ +Crypto.prototype.getEventSenderDeviceInfo = function (event) { + var senderKey = event.getSenderKey(); + var algorithm = event.getWireContent().algorithm; + + if (!senderKey || !algorithm) { + return null; + } + + var forwardingChain = event.getForwardingCurve25519KeyChain(); + if (forwardingChain.length > 0) { + // we got this event from somewhere else + // TODO: check if we can trust the forwarders. + return null; + } + + // senderKey is the Curve25519 identity key of the device which the event + // was sent from. In the case of Megolm, it's actually the Curve25519 + // identity key of the device which set up the Megolm session. + + var device = this._deviceList.getDeviceByIdentityKey(event.getSender(), algorithm, senderKey); + + if (device === null) { + // we haven't downloaded the details of this device yet. + return null; + } + + // so far so good, but now we need to check that the sender of this event + // hadn't advertised someone else's Curve25519 key as their own. We do that + // by checking the Ed25519 claimed by the event (or, in the case of megolm, + // the event which set up the megolm session), to check that it matches the + // fingerprint of the purported sending device. + // + // (see https://github.com/vector-im/vector-web/issues/2215) + + var claimedKey = event.getClaimedEd25519Key(); + if (!claimedKey) { + console.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device"); + return null; + } + + if (claimedKey !== device.getFingerprint()) { + console.warn("Event " + event.getId() + " claims ed25519 key " + claimedKey + "but sender device has key " + device.getFingerprint()); + return null; + } + + return device; +}; + +/** + * Configure a room to use encryption (ie, save a flag in the sessionstore). + * + * @param {string} roomId The room ID to enable encryption in. + * + * @param {object} config The encryption config for the room. + * + * @param {boolean=} inhibitDeviceQuery true to suppress device list query for + * users in the room (for now) + */ +Crypto.prototype.setRoomEncryption = function () { + var _ref5 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee4(roomId, config, inhibitDeviceQuery) { + var _this2 = this; + + var existingConfig, AlgClass, alg, room, members; + return _regenerator2.default.wrap(function _callee4$(_context4) { + while (1) { + switch (_context4.prev = _context4.next) { + case 0: + // if we already have encryption in this room, we should ignore this event + // (for now at least. maybe we should alert the user somehow?) + existingConfig = this._roomList.getRoomEncryption(roomId); + + if (!(existingConfig && (0, _stringify2.default)(existingConfig) != (0, _stringify2.default)(config))) { + _context4.next = 4; + break; + } + + console.error("Ignoring m.room.encryption event which requests " + "a change of config in " + roomId); + return _context4.abrupt('return'); + + case 4: + AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm]; + + if (AlgClass) { + _context4.next = 7; + break; + } + + throw new Error("Unable to encrypt with " + config.algorithm); + + case 7: + _context4.next = 9; + return (0, _bluebird.resolve)(this._roomList.setRoomEncryption(roomId, config)); + + case 9: + alg = new AlgClass({ + userId: this._userId, + deviceId: this._deviceId, + crypto: this, + olmDevice: this._olmDevice, + baseApis: this._baseApis, + roomId: roomId, + config: config + }); + + this._roomEncryptors[roomId] = alg; + + // make sure we are tracking the device lists for all users in this room. + console.log("Enabling encryption in " + roomId + "; " + "starting to track device lists for all users therein"); + room = this._clientStore.getRoom(roomId); + + if (room) { + _context4.next = 15; + break; + } + + throw new Error('Unable to enable encryption in unknown room ' + roomId); + + case 15: + members = room.getJoinedMembers(); + + members.forEach(function (m) { + _this2._deviceList.startTrackingDeviceList(m.userId); + }); + if (!inhibitDeviceQuery) { + this._deviceList.refreshOutdatedDeviceLists(); + } + + case 18: + case 'end': + return _context4.stop(); + } + } + }, _callee4, this); + })); + + return function (_x8, _x9, _x10) { + return _ref5.apply(this, arguments); + }; +}(); + +/** + * @typedef {Object} module:crypto~OlmSessionResult + * @property {module:crypto/deviceinfo} device device info + * @property {string?} sessionId base64 olm session id; null if no session + * could be established + */ + +/** + * Try to make sure we have established olm sessions for all known devices for + * the given users. + * + * @param {string[]} users list of user ids + * + * @return {module:client.Promise} resolves once the sessions are complete, to + * an Object mapping from userId to deviceId to + * {@link module:crypto~OlmSessionResult} + */ +Crypto.prototype.ensureOlmSessionsForUsers = function (users) { + var devicesByUser = {}; + + for (var i = 0; i < users.length; ++i) { + var userId = users[i]; + devicesByUser[userId] = []; + + var devices = this.getStoredDevicesForUser(userId) || []; + for (var j = 0; j < devices.length; ++j) { + var deviceInfo = devices[j]; + + var key = deviceInfo.getIdentityKey(); + if (key == this._olmDevice.deviceCurve25519Key) { + // don't bother setting up session to ourself + continue; + } + if (deviceInfo.verified == DeviceVerification.BLOCKED) { + // don't bother setting up sessions with blocked users + continue; + } + + devicesByUser[userId].push(deviceInfo); + } + } + + return olmlib.ensureOlmSessionsForDevices(this._olmDevice, this._baseApis, devicesByUser); +}; + +/** + * Get a list containing all of the room keys + * + * @return {module:crypto/OlmDevice.MegolmSessionData[]} a list of session export objects + */ +Crypto.prototype.exportRoomKeys = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee5() { + var _this3 = this; + + var exportedSessions; + return _regenerator2.default.wrap(function _callee5$(_context5) { + while (1) { + switch (_context5.prev = _context5.next) { + case 0: + exportedSessions = []; + _context5.next = 3; + return (0, _bluebird.resolve)(this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore2.default.STORE_INBOUND_GROUP_SESSIONS], function (txn) { + _this3._cryptoStore.getAllEndToEndInboundGroupSessions(txn, function (s) { + if (s === null) return; + + var sess = _this3._olmDevice.exportInboundGroupSession(s.senderKey, s.sessionId, s.sessionData); + sess.algorithm = olmlib.MEGOLM_ALGORITHM; + exportedSessions.push(sess); + }); + })); + + case 3: + return _context5.abrupt('return', exportedSessions); + + case 4: + case 'end': + return _context5.stop(); + } + } + }, _callee5, this); +})); + +/** + * Import a list of room keys previously exported by exportRoomKeys + * + * @param {Object[]} keys a list of session export objects + * @return {module:client.Promise} a promise which resolves once the keys have been imported + */ +Crypto.prototype.importRoomKeys = function (keys) { + var _this4 = this; + + return _bluebird2.default.map(keys, function (key) { + if (!key.room_id || !key.algorithm) { + console.warn("ignoring room key entry with missing fields", key); + return null; + } + + var alg = _this4._getRoomDecryptor(key.room_id, key.algorithm); + return alg.importRoomKey(key); + }); +}; + +/** + * Encrypt an event according to the configuration of the room. + * + * @param {module:models/event.MatrixEvent} event event to be sent + * + * @param {module:models/room} room destination room. + * + * @return {module:client.Promise?} Promise which resolves when the event has been + * encrypted, or null if nothing was needed + */ +Crypto.prototype.encryptEvent = function (event, room) { + var _this5 = this; + + if (!room) { + throw new Error("Cannot send encrypted messages in unknown rooms"); + } + + var roomId = event.getRoomId(); + + var alg = this._roomEncryptors[roomId]; + if (!alg) { + // MatrixClient has already checked that this room should be encrypted, + // so this is an unexpected situation. + throw new Error("Room was previously configured to use encryption, but is " + "no longer. Perhaps the homeserver is hiding the " + "configuration event."); + } + + var content = event.getContent(); + // If event has an m.relates_to then we need + // to put this on the wrapping event instead + var mRelatesTo = content['m.relates_to']; + if (mRelatesTo) { + // Clone content here so we don't remove `m.relates_to` from the local-echo + content = (0, _assign2.default)({}, content); + delete content['m.relates_to']; + } + + return alg.encryptMessage(room, event.getType(), content).then(function (encryptedContent) { + if (mRelatesTo) { + encryptedContent['m.relates_to'] = mRelatesTo; + } + + event.makeEncrypted("m.room.encrypted", encryptedContent, _this5._olmDevice.deviceCurve25519Key, _this5._olmDevice.deviceEd25519Key); + }); +}; + +/** + * Decrypt a received event + * + * @param {MatrixEvent} event + * + * @return {Promise} resolves once we have + * finished decrypting. Rejects with an `algorithms.DecryptionError` if there + * is a problem decrypting the event. + */ +Crypto.prototype.decryptEvent = function (event) { + if (event.isRedacted()) { + return _bluebird2.default.resolve({ + clearEvent: { + room_id: event.getRoomId(), + type: "m.room.message", + content: {} + } + }); + } + var content = event.getWireContent(); + var alg = this._getRoomDecryptor(event.getRoomId(), content.algorithm); + return alg.decryptEvent(event); +}; + +/** + * Handle the notification from /sync or /keys/changes that device lists have + * been changed. + * + * @param {Object} syncData Object containing sync tokens associated with this sync + * @param {Object} syncDeviceLists device_lists field from /sync, or response from + * /keys/changes + */ +Crypto.prototype.handleDeviceListChanges = function () { + var _ref7 = (0, _bluebird.method)(function (syncData, syncDeviceLists) { + // Initial syncs don't have device change lists. We'll either get the complete list + // of changes for the interval or will have invalidated everything in willProcessSync + if (!syncData.oldSyncToken) return; + + // Here, we're relying on the fact that we only ever save the sync data after + // sucessfully saving the device list data, so we're guaranteed that the device + // list store is at least as fresh as the sync token from the sync store, ie. + // any device changes received in sync tokens prior to the 'next' token here + // have been processed and are reflected in the current device list. + // If we didn't make this assumption, we'd have to use the /keys/changes API + // to get key changes between the sync token in the device list and the 'old' + // sync token used here to make sure we didn't miss any. + this._evalDeviceListChanges(syncDeviceLists); + }); + + return function (_x11, _x12) { + return _ref7.apply(this, arguments); + }; +}(); + +/** + * Send a request for some room keys, if we have not already done so + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * @param {Array<{userId: string, deviceId: string}>} recipients + */ +Crypto.prototype.requestRoomKey = function (requestBody, recipients) { + this._outgoingRoomKeyRequestManager.sendRoomKeyRequest(requestBody, recipients).catch(function (e) { + // this normally means we couldn't talk to the store + console.error('Error requesting key for event', e); + }).done(); +}; + +/** + * Cancel any earlier room key request + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * parameters to match for cancellation + * @param {boolean} andResend + * if true, resend the key request after cancelling. + */ +Crypto.prototype.cancelRoomKeyRequest = function (requestBody, andResend) { + this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody, andResend).catch(function (e) { + console.warn("Error clearing pending room key requests", e); + }).done(); +}; + +/** + * handle an m.room.encryption event + * + * @param {module:models/event.MatrixEvent} event encryption event + */ +Crypto.prototype.onCryptoEvent = function () { + var _ref8 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee6(event) { + var roomId, content; + return _regenerator2.default.wrap(function _callee6$(_context6) { + while (1) { + switch (_context6.prev = _context6.next) { + case 0: + roomId = event.getRoomId(); + content = event.getContent(); + _context6.prev = 2; + _context6.next = 5; + return (0, _bluebird.resolve)(this.setRoomEncryption(roomId, content, true)); + + case 5: + _context6.next = 10; + break; + + case 7: + _context6.prev = 7; + _context6.t0 = _context6['catch'](2); + + console.error("Error configuring encryption in room " + roomId + ":", _context6.t0); + + case 10: + case 'end': + return _context6.stop(); + } + } + }, _callee6, this, [[2, 7]]); + })); + + return function (_x13) { + return _ref8.apply(this, arguments); + }; +}(); + +/** + * Called before the result of a sync is procesed + * + * @param {Object} syncData the data from the 'MatrixClient.sync' event + */ +Crypto.prototype.onSyncWillProcess = function () { + var _ref9 = (0, _bluebird.method)(function (syncData) { + if (!syncData.oldSyncToken) { + // If there is no old sync token, we start all our tracking from + // scratch, so mark everything as untracked. onCryptoEvent will + // be called for all e2e rooms during the processing of the sync, + // at which point we'll start tracking all the users of that room. + console.log("Initial sync performed - resetting device tracking state"); + this._deviceList.stopTrackingAllDeviceLists(); + } + }); + + return function (_x14) { + return _ref9.apply(this, arguments); + }; +}(); + +/** + * handle the completion of a /sync + * + * This is called after the processing of each successful /sync response. + * It is an opportunity to do a batch process on the information received. + * + * @param {Object} syncData the data from the 'MatrixClient.sync' event + */ +Crypto.prototype.onSyncCompleted = function () { + var _ref10 = (0, _bluebird.method)(function (syncData) { + var nextSyncToken = syncData.nextSyncToken; + + this._deviceList.setSyncToken(syncData.nextSyncToken); + this._deviceList.saveIfDirty(); + + // catch up on any new devices we got told about during the sync. + this._deviceList.lastKnownSyncToken = nextSyncToken; + this._deviceList.refreshOutdatedDeviceLists(); + + // we don't start uploading one-time keys until we've caught up with + // to-device messages, to help us avoid throwing away one-time-keys that we + // are about to receive messages for + // (https://github.com/vector-im/riot-web/issues/2782). + if (!syncData.catchingUp) { + _maybeUploadOneTimeKeys(this); + this._processReceivedRoomKeyRequests(); + } + }); + + return function (_x15) { + return _ref10.apply(this, arguments); + }; +}(); + +/** + * Trigger the appropriate invalidations and removes for a given + * device list + * + * @param {Object} deviceLists device_lists field from /sync, or response from + * /keys/changes + */ +Crypto.prototype._evalDeviceListChanges = function () { + var _ref11 = (0, _bluebird.method)(function (deviceLists) { + var _this6 = this; + + if (deviceLists.changed && Array.isArray(deviceLists.changed)) { + deviceLists.changed.forEach(function (u) { + _this6._deviceList.invalidateUserDeviceList(u); + }); + } + + if (deviceLists.left && Array.isArray(deviceLists.left)) { + // Check we really don't share any rooms with these users + // any more: the server isn't required to give us the + // exact correct set. + var e2eUserIds = new _set2.default(this._getE2eUsers()); + + deviceLists.left.forEach(function (u) { + if (!e2eUserIds.has(u)) { + _this6._deviceList.stopTrackingDeviceList(u); + } + }); + } + }); + + return function (_x16) { + return _ref11.apply(this, arguments); + }; +}(); + +/** + * Get a list of all the IDs of users we share an e2e room with + * + * @returns {string[]} List of user IDs + */ +Crypto.prototype._getE2eUsers = function () { + var e2eUserIds = []; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)(this._getE2eRooms()), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var room = _step.value; + + var members = room.getJoinedMembers(); + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = (0, _getIterator3.default)(members), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var member = _step2.value; + + e2eUserIds.push(member.userId); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return e2eUserIds; +}; + +/** + * Get a list of the e2e-enabled rooms we are members of + * + * @returns {module:models.Room[]} + */ +Crypto.prototype._getE2eRooms = function () { + var _this7 = this; + + return this._clientStore.getRooms().filter(function (room) { + // check for rooms with encryption enabled + var alg = _this7._roomEncryptors[room.roomId]; + if (!alg) { + return false; + } + + // ignore any rooms which we have left + var me = room.getMember(_this7._userId); + if (!me || me.membership !== "join" && me.membership !== "invite") { + return false; + } + + return true; + }); +}; + +Crypto.prototype._onToDeviceEvent = function (event) { + var _this8 = this; + + try { + if (event.getType() == "m.room_key" || event.getType() == "m.forwarded_room_key") { + this._onRoomKeyEvent(event); + } else if (event.getType() == "m.room_key_request") { + this._onRoomKeyRequestEvent(event); + } else if (event.isBeingDecrypted()) { + // once the event has been decrypted, try again + event.once('Event.decrypted', function (ev) { + _this8._onToDeviceEvent(ev); + }); + } + } catch (e) { + console.error("Error handling toDeviceEvent:", e); + } +}; + +/** + * Handle a key event + * + * @private + * @param {module:models/event.MatrixEvent} event key event + */ +Crypto.prototype._onRoomKeyEvent = function (event) { + var content = event.getContent(); + + if (!content.room_id || !content.algorithm) { + console.error("key event is missing fields"); + return; + } + + var alg = this._getRoomDecryptor(content.room_id, content.algorithm); + alg.onRoomKeyEvent(event); +}; + +/** + * Handle a change in the membership state of a member of a room + * + * @private + * @param {module:models/event.MatrixEvent} event event causing the change + * @param {module:models/room-member} member user whose membership changed + * @param {string=} oldMembership previous membership + */ +Crypto.prototype._onRoomMembership = function (event, member, oldMembership) { + // this event handler is registered on the *client* (as opposed to the room + // member itself), which means it is only called on changes to the *live* + // membership state (ie, it is not called when we back-paginate, nor when + // we load the state in the initialsync). + // + // Further, it is automatically registered and called when new members + // arrive in the room. + + var roomId = member.roomId; + + var alg = this._roomEncryptors[roomId]; + if (!alg) { + // not encrypting in this room + return; + } + + if (member.membership == 'join') { + console.log('Join event for ' + member.userId + ' in ' + roomId); + // make sure we are tracking the deviceList for this user + this._deviceList.startTrackingDeviceList(member.userId); + } + + alg.onRoomMembership(event, member, oldMembership); +}; + +/** + * Called when we get an m.room_key_request event. + * + * @private + * @param {module:models/event.MatrixEvent} event key request event + */ +Crypto.prototype._onRoomKeyRequestEvent = function (event) { + var content = event.getContent(); + if (content.action === "request") { + // Queue it up for now, because they tend to arrive before the room state + // events at initial sync, and we want to see if we know anything about the + // room before passing them on to the app. + var req = new IncomingRoomKeyRequest(event); + this._receivedRoomKeyRequests.push(req); + } else if (content.action === "request_cancellation") { + var _req = new IncomingRoomKeyRequestCancellation(event); + this._receivedRoomKeyRequestCancellations.push(_req); + } +}; + +/** + * Process any m.room_key_request events which were queued up during the + * current sync. + * + * @private + */ +Crypto.prototype._processReceivedRoomKeyRequests = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee7() { + var _this9 = this; + + var requests, cancellations; + return _regenerator2.default.wrap(function _callee7$(_context7) { + while (1) { + switch (_context7.prev = _context7.next) { + case 0: + if (!this._processingRoomKeyRequests) { + _context7.next = 2; + break; + } + + return _context7.abrupt('return'); + + case 2: + this._processingRoomKeyRequests = true; + + _context7.prev = 3; + + // we need to grab and clear the queues in the synchronous bit of this method, + // so that we don't end up racing with the next /sync. + requests = this._receivedRoomKeyRequests; + + this._receivedRoomKeyRequests = []; + cancellations = this._receivedRoomKeyRequestCancellations; + + this._receivedRoomKeyRequestCancellations = []; + + // Process all of the requests, *then* all of the cancellations. + // + // This makes sure that if we get a request and its cancellation in the + // same /sync result, then we process the request before the + // cancellation (and end up with a cancelled request), rather than the + // cancellation before the request (and end up with an outstanding + // request which should have been cancelled.) + _context7.next = 10; + return (0, _bluebird.resolve)(_bluebird2.default.map(requests, function (req) { + return _this9._processReceivedRoomKeyRequest(req); + })); + + case 10: + _context7.next = 12; + return (0, _bluebird.resolve)(_bluebird2.default.map(cancellations, function (cancellation) { + return _this9._processReceivedRoomKeyRequestCancellation(cancellation); + })); + + case 12: + _context7.next = 17; + break; + + case 14: + _context7.prev = 14; + _context7.t0 = _context7['catch'](3); + + console.error('Error processing room key requsts: ' + _context7.t0); + + case 17: + _context7.prev = 17; + + this._processingRoomKeyRequests = false; + return _context7.finish(17); + + case 20: + case 'end': + return _context7.stop(); + } + } + }, _callee7, this, [[3, 14, 17, 20]]); +})); + +/** + * Helper for processReceivedRoomKeyRequests + * + * @param {IncomingRoomKeyRequest} req + */ +Crypto.prototype._processReceivedRoomKeyRequest = function () { + var _ref13 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee8(req) { + var userId, deviceId, body, roomId, alg, decryptor, device; + return _regenerator2.default.wrap(function _callee8$(_context8) { + while (1) { + switch (_context8.prev = _context8.next) { + case 0: + userId = req.userId; + deviceId = req.deviceId; + body = req.requestBody; + roomId = body.room_id; + alg = body.algorithm; + + + console.log('m.room_key_request from ' + userId + ':' + deviceId + (' for ' + roomId + ' / ' + body.session_id + ' (id ' + req.requestId + ')')); + + if (!(userId !== this._userId)) { + _context8.next = 9; + break; + } + + // TODO: determine if we sent this device the keys already: in + // which case we can do so again. + console.log("Ignoring room key request from other user for now"); + return _context8.abrupt('return'); + + case 9: + if (this._roomDecryptors[roomId]) { + _context8.next = 12; + break; + } + + console.log('room key request for unencrypted room ' + roomId); + return _context8.abrupt('return'); + + case 12: + decryptor = this._roomDecryptors[roomId][alg]; + + if (decryptor) { + _context8.next = 16; + break; + } + + console.log('room key request for unknown alg ' + alg + ' in room ' + roomId); + return _context8.abrupt('return'); + + case 16: + _context8.next = 18; + return (0, _bluebird.resolve)(decryptor.hasKeysForKeyRequest(req)); + + case 18: + if (_context8.sent) { + _context8.next = 21; + break; + } + + console.log('room key request for unknown session ' + roomId + ' / ' + body.session_id); + return _context8.abrupt('return'); + + case 21: + + req.share = function () { + decryptor.shareKeysWithDevice(req); + }; + + // if the device is is verified already, share the keys + device = this._deviceList.getStoredDevice(userId, deviceId); + + if (!(device && device.isVerified())) { + _context8.next = 27; + break; + } + + console.log('device is already verified: sharing keys'); + req.share(); + return _context8.abrupt('return'); + + case 27: + + this.emit("crypto.roomKeyRequest", req); + + case 28: + case 'end': + return _context8.stop(); + } + } + }, _callee8, this); + })); + + return function (_x17) { + return _ref13.apply(this, arguments); + }; +}(); + +/** + * Helper for processReceivedRoomKeyRequests + * + * @param {IncomingRoomKeyRequestCancellation} cancellation + */ +Crypto.prototype._processReceivedRoomKeyRequestCancellation = function () { + var _ref14 = (0, _bluebird.method)(function (cancellation) { + console.log('m.room_key_request cancellation for ' + cancellation.userId + ':' + (cancellation.deviceId + ' (id ' + cancellation.requestId + ')')); + + // we should probably only notify the app of cancellations we told it + // about, but we don't currently have a record of that, so we just pass + // everything through. + this.emit("crypto.roomKeyRequestCancellation", cancellation); + }); + + return function (_x18) { + return _ref14.apply(this, arguments); + }; +}(); + +/** + * Get a decryptor for a given room and algorithm. + * + * If we already have a decryptor for the given room and algorithm, return + * it. Otherwise try to instantiate it. + * + * @private + * + * @param {string?} roomId room id for decryptor. If undefined, a temporary + * decryptor is instantiated. + * + * @param {string} algorithm crypto algorithm + * + * @return {module:crypto.algorithms.base.DecryptionAlgorithm} + * + * @raises {module:crypto.algorithms.DecryptionError} if the algorithm is + * unknown + */ +Crypto.prototype._getRoomDecryptor = function (roomId, algorithm) { + var decryptors = void 0; + var alg = void 0; + + roomId = roomId || null; + if (roomId) { + decryptors = this._roomDecryptors[roomId]; + if (!decryptors) { + this._roomDecryptors[roomId] = decryptors = {}; + } + + alg = decryptors[algorithm]; + if (alg) { + return alg; + } + } + + var AlgClass = algorithms.DECRYPTION_CLASSES[algorithm]; + if (!AlgClass) { + throw new algorithms.DecryptionError('Unknown encryption algorithm "' + algorithm + '".'); + } + alg = new AlgClass({ + userId: this._userId, + crypto: this, + olmDevice: this._olmDevice, + baseApis: this._baseApis, + roomId: roomId + }); + + if (decryptors) { + decryptors[algorithm] = alg; + } + return alg; +}; + +/** + * sign the given object with our ed25519 key + * + * @param {Object} obj Object to which we will add a 'signatures' property + */ +Crypto.prototype._signObject = function () { + var _ref15 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee9(obj) { + var sigs; + return _regenerator2.default.wrap(function _callee9$(_context9) { + while (1) { + switch (_context9.prev = _context9.next) { + case 0: + sigs = {}; + + sigs[this._userId] = {}; + _context9.next = 4; + return (0, _bluebird.resolve)(this._olmDevice.sign(anotherjson.stringify(obj))); + + case 4: + sigs[this._userId]["ed25519:" + this._deviceId] = _context9.sent; + + obj.signatures = sigs; + + case 6: + case 'end': + return _context9.stop(); + } + } + }, _callee9, this); + })); + + return function (_x19) { + return _ref15.apply(this, arguments); + }; +}(); + +/** + * The parameters of a room key request. The details of the request may + * vary with the crypto algorithm, but the management and storage layers for + * outgoing requests expect it to have 'room_id' and 'session_id' properties. + * + * @typedef {Object} RoomKeyRequestBody + */ + +/** + * Represents a received m.room_key_request event + * + * @property {string} userId user requesting the key + * @property {string} deviceId device requesting the key + * @property {string} requestId unique id for the request + * @property {module:crypto~RoomKeyRequestBody} requestBody + * @property {function()} share callback which, when called, will ask + * the relevant crypto algorithm implementation to share the keys for + * this request. + */ + +var IncomingRoomKeyRequest = function IncomingRoomKeyRequest(event) { + (0, _classCallCheck3.default)(this, IncomingRoomKeyRequest); + + var content = event.getContent(); + + this.userId = event.getSender(); + this.deviceId = content.requesting_device_id; + this.requestId = content.request_id; + this.requestBody = content.body || {}; + this.share = function () { + throw new Error("don't know how to share keys for this request yet"); + }; +}; + +/** + * Represents a received m.room_key_request cancellation + * + * @property {string} userId user requesting the cancellation + * @property {string} deviceId device requesting the cancellation + * @property {string} requestId unique id for the request to be cancelled + */ + + +var IncomingRoomKeyRequestCancellation = function IncomingRoomKeyRequestCancellation(event) { + (0, _classCallCheck3.default)(this, IncomingRoomKeyRequestCancellation); + + var content = event.getContent(); + + this.userId = event.getSender(); + this.deviceId = content.requesting_device_id; + this.requestId = content.request_id; +}; + +/** + * The result of a (successful) call to decryptEvent. + * + * @typedef {Object} EventDecryptionResult + * + * @property {Object} clearEvent The plaintext payload for the event + * (typically containing type and content fields). + * + * @property {?string} senderCurve25519Key Key owned by the sender of this + * event. See {@link module:models/event.MatrixEvent#getSenderKey}. + * + * @property {?string} claimedEd25519Key ed25519 key claimed by the sender of + * this event. See + * {@link module:models/event.MatrixEvent#getClaimedEd25519Key}. + * + * @property {?Array} forwardingCurve25519KeyChain list of curve25519 + * keys involved in telling us about the senderCurve25519Key and + * claimedEd25519Key. See + * {@link module:models/event.MatrixEvent#getForwardingCurve25519KeyChain}. + */ + +/** + * Fires when we receive a room key request + * + * @event module:client~MatrixClient#"crypto.roomKeyRequest" + * @param {module:crypto~IncomingRoomKeyRequest} req request details + */ + +/** + * Fires when we receive a room key request cancellation + * + * @event module:client~MatrixClient#"crypto.roomKeyRequestCancellation" + * @param {module:crypto~IncomingRoomKeyRequestCancellation} req + */ + +/** + * Fires when the app may wish to warn the user about something related + * the end-to-end crypto. + * + * Comes with a type which is one of: + * * CRYPTO_WARNING_ACCOUNT_MIGRATED: Account data has been migrated from an older + * version of the store in such a way that older clients will no longer be + * able to read it. The app may wish to warn the user against going back to + * an older version of the app. + * * CRYPTO_WARNING_OLD_VERSION_DETECTED: js-sdk has detected that an older version + * of js-sdk has been run against the same store after a migration has been + * performed. This is likely have caused unexpected behaviour in the old + * version. For example, the old version and the new version may have two + * different identity keys. + * + * @event module:client~MatrixClient#"crypto.warning" + * @param {string} type One of the strings listed above + */ + +/** */ + + +module.exports = Crypto; + +},{"../utils":50,"./DeviceList":7,"./OlmDevice":8,"./OutgoingRoomKeyRequestManager":9,"./algorithms":12,"./deviceinfo":15,"./olmlib":17,"./store/indexeddb-crypto-store":19,"another-json":52,"babel-runtime/core-js/get-iterator":53,"babel-runtime/core-js/json/stringify":55,"babel-runtime/core-js/object/assign":56,"babel-runtime/core-js/set":63,"babel-runtime/helpers/classCallCheck":66,"babel-runtime/regenerator":73,"bluebird":74,"events":186}],17:[function(require,module,exports){ +'use strict'; + +var _bluebird = require('bluebird'); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +var _regenerator = require('babel-runtime/regenerator'); + +var _regenerator2 = _interopRequireDefault(_regenerator); + +var _stringify = require('babel-runtime/core-js/json/stringify'); + +var _stringify2 = _interopRequireDefault(_stringify); + +var _verifyKeyAndStartSession = function () { + var _ref3 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee3(olmDevice, oneTimeKey, userId, deviceInfo) { + var deviceId, sid; + return _regenerator2.default.wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + deviceId = deviceInfo.deviceId; + _context3.prev = 1; + _context3.next = 4; + return (0, _bluebird.resolve)(_verifySignature(olmDevice, oneTimeKey, userId, deviceId, deviceInfo.getFingerprint())); + + case 4: + _context3.next = 10; + break; + + case 6: + _context3.prev = 6; + _context3.t0 = _context3['catch'](1); + + console.error("Unable to verify signature on one-time key for device " + userId + ":" + deviceId + ":", _context3.t0); + return _context3.abrupt('return', null); + + case 10: + sid = void 0; + _context3.prev = 11; + _context3.next = 14; + return (0, _bluebird.resolve)(olmDevice.createOutboundSession(deviceInfo.getIdentityKey(), oneTimeKey.key)); + + case 14: + sid = _context3.sent; + _context3.next = 21; + break; + + case 17: + _context3.prev = 17; + _context3.t1 = _context3['catch'](11); + + // possibly a bad key + console.error("Error starting session with device " + userId + ":" + deviceId + ": " + _context3.t1); + return _context3.abrupt('return', null); + + case 21: + + console.log("Started new sessionid " + sid + " for device " + userId + ":" + deviceId); + return _context3.abrupt('return', sid); + + case 23: + case 'end': + return _context3.stop(); + } + } + }, _callee3, this, [[1, 6], [11, 17]]); + })); + + return function _verifyKeyAndStartSession(_x11, _x12, _x13, _x14) { + return _ref3.apply(this, arguments); + }; +}(); + +/** + * Verify the signature on an object + * + * @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op + * + * @param {Object} obj object to check signature on. Note that this will be + * stripped of its 'signatures' and 'unsigned' properties. + * + * @param {string} signingUserId ID of the user whose signature should be checked + * + * @param {string} signingDeviceId ID of the device whose signature should be checked + * + * @param {string} signingKey base64-ed ed25519 public key + * + * Returns a promise which resolves (to undefined) if the the signature is good, + * or rejects with an Error if it is bad. + */ + + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var anotherjson = require('another-json'); /* + Copyright 2016 OpenMarket Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +/** + * @module olmlib + * + * Utilities common to olm encryption algorithms + */ + +var utils = require("../utils"); + +/** + * matrix algorithm tag for olm + */ +module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2"; + +/** + * matrix algorithm tag for megolm + */ +module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2"; + +/** + * Encrypt an event payload for an Olm device + * + * @param {Object} resultsObject The `ciphertext` property + * of the m.room.encrypted event to which to add our result + * + * @param {string} ourUserId + * @param {string} ourDeviceId + * @param {module:crypto/OlmDevice} olmDevice olm.js wrapper + * @param {string} recipientUserId + * @param {module:crypto/deviceinfo} recipientDevice + * @param {object} payloadFields fields to include in the encrypted payload + * + * Returns a promise which resolves (to undefined) when the payload + * has been encrypted into `resultsObject` + */ +module.exports.encryptMessageForDevice = function () { + var _ref = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee(resultsObject, ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice, payloadFields) { + var deviceKey, sessionId, payload; + return _regenerator2.default.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + deviceKey = recipientDevice.getIdentityKey(); + _context.next = 3; + return (0, _bluebird.resolve)(olmDevice.getSessionIdForDevice(deviceKey)); + + case 3: + sessionId = _context.sent; + + if (!(sessionId === null)) { + _context.next = 6; + break; + } + + return _context.abrupt('return'); + + case 6: + + console.log("Using sessionid " + sessionId + " for device " + recipientUserId + ":" + recipientDevice.deviceId); + + payload = { + sender: ourUserId, + sender_device: ourDeviceId, + + // Include the Ed25519 key so that the recipient knows what + // device this message came from. + // We don't need to include the curve25519 key since the + // recipient will already know this from the olm headers. + // When combined with the device keys retrieved from the + // homeserver signed by the ed25519 key this proves that + // the curve25519 key and the ed25519 key are owned by + // the same device. + keys: { + "ed25519": olmDevice.deviceEd25519Key + }, + + // include the recipient device details in the payload, + // to avoid unknown key attacks, per + // https://github.com/vector-im/vector-web/issues/2483 + recipient: recipientUserId, + recipient_keys: { + "ed25519": recipientDevice.getFingerprint() + } + }; + + // TODO: technically, a bunch of that stuff only needs to be included for + // pre-key messages: after that, both sides know exactly which devices are + // involved in the session. If we're looking to reduce data transfer in the + // future, we could elide them for subsequent messages. + + utils.extend(payload, payloadFields); + + _context.next = 11; + return (0, _bluebird.resolve)(olmDevice.encryptMessage(deviceKey, sessionId, (0, _stringify2.default)(payload))); + + case 11: + resultsObject[deviceKey] = _context.sent; + + case 12: + case 'end': + return _context.stop(); + } + } + }, _callee, this); + })); + + return function (_x, _x2, _x3, _x4, _x5, _x6, _x7) { + return _ref.apply(this, arguments); + }; +}(); + +/** + * Try to make sure we have established olm sessions for the given devices. + * + * @param {module:crypto/OlmDevice} olmDevice + * + * @param {module:base-apis~MatrixBaseApis} baseApis + * + * @param {object} devicesByUser + * map from userid to list of devices + * + * @return {module:client.Promise} resolves once the sessions are complete, to + * an Object mapping from userId to deviceId to + * {@link module:crypto~OlmSessionResult} + */ +module.exports.ensureOlmSessionsForDevices = function () { + var _ref2 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee2(olmDevice, baseApis, devicesByUser) { + var devicesWithoutSession, result, userId, devices, j, deviceInfo, deviceId, key, sessionId, oneTimeKeyAlgorithm, res, otk_res, promises, _loop, _userId, _ret; + + return _regenerator2.default.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + devicesWithoutSession = [ + // [userId, deviceId], ... + ]; + result = {}; + _context2.t0 = _regenerator2.default.keys(devicesByUser); + + case 3: + if ((_context2.t1 = _context2.t0()).done) { + _context2.next = 24; + break; + } + + userId = _context2.t1.value; + + if (devicesByUser.hasOwnProperty(userId)) { + _context2.next = 7; + break; + } + + return _context2.abrupt('continue', 3); + + case 7: + result[userId] = {}; + devices = devicesByUser[userId]; + j = 0; + + case 10: + if (!(j < devices.length)) { + _context2.next = 22; + break; + } + + deviceInfo = devices[j]; + deviceId = deviceInfo.deviceId; + key = deviceInfo.getIdentityKey(); + _context2.next = 16; + return (0, _bluebird.resolve)(olmDevice.getSessionIdForDevice(key)); + + case 16: + sessionId = _context2.sent; + + if (sessionId === null) { + devicesWithoutSession.push([userId, deviceId]); + } + result[userId][deviceId] = { + device: deviceInfo, + sessionId: sessionId + }; + + case 19: + j++; + _context2.next = 10; + break; + + case 22: + _context2.next = 3; + break; + + case 24: + if (!(devicesWithoutSession.length === 0)) { + _context2.next = 26; + break; + } + + return _context2.abrupt('return', result); + + case 26: + + // TODO: this has a race condition - if we try to send another message + // while we are claiming a key, we will end up claiming two and setting up + // two sessions. + // + // That should eventually resolve itself, but it's poor form. + + oneTimeKeyAlgorithm = "signed_curve25519"; + _context2.next = 29; + return (0, _bluebird.resolve)(baseApis.claimOneTimeKeys(devicesWithoutSession, oneTimeKeyAlgorithm)); + + case 29: + res = _context2.sent; + otk_res = res.one_time_keys || {}; + promises = []; + + _loop = function _loop(_userId) { + if (!devicesByUser.hasOwnProperty(_userId)) { + return 'continue'; + } + var userRes = otk_res[_userId] || {}; + var devices = devicesByUser[_userId]; + + var _loop2 = function _loop2(_j) { + var deviceInfo = devices[_j]; + var deviceId = deviceInfo.deviceId; + if (result[_userId][deviceId].sessionId) { + // we already have a result for this device + return 'continue'; + } + + var deviceRes = userRes[deviceId] || {}; + var oneTimeKey = null; + for (var keyId in deviceRes) { + if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) { + oneTimeKey = deviceRes[keyId]; + } + } + + if (!oneTimeKey) { + console.warn("No one-time keys (alg=" + oneTimeKeyAlgorithm + ") for device " + _userId + ":" + deviceId); + return 'continue'; + } + + promises.push(_verifyKeyAndStartSession(olmDevice, oneTimeKey, _userId, deviceInfo).then(function (sid) { + result[_userId][deviceId].sessionId = sid; + })); + }; + + for (var _j = 0; _j < devices.length; _j++) { + var _ret2 = _loop2(_j); + + if (_ret2 === 'continue') continue; + } + }; + + _context2.t2 = _regenerator2.default.keys(devicesByUser); + + case 34: + if ((_context2.t3 = _context2.t2()).done) { + _context2.next = 41; + break; + } + + _userId = _context2.t3.value; + _ret = _loop(_userId); + + if (!(_ret === 'continue')) { + _context2.next = 39; + break; + } + + return _context2.abrupt('continue', 34); + + case 39: + _context2.next = 34; + break; + + case 41: + _context2.next = 43; + return (0, _bluebird.resolve)(_bluebird2.default.all(promises)); + + case 43: + return _context2.abrupt('return', result); + + case 44: + case 'end': + return _context2.stop(); + } + } + }, _callee2, this); + })); + + return function (_x8, _x9, _x10) { + return _ref2.apply(this, arguments); + }; +}(); + +var _verifySignature = module.exports.verifySignature = function () { + var _ref4 = (0, _bluebird.method)(function (olmDevice, obj, signingUserId, signingDeviceId, signingKey) { + var signKeyId = "ed25519:" + signingDeviceId; + var signatures = obj.signatures || {}; + var userSigs = signatures[signingUserId] || {}; + var signature = userSigs[signKeyId]; + if (!signature) { + throw Error("No signature"); + } + + // prepare the canonical json: remove unsigned and signatures, and stringify with + // anotherjson + delete obj.unsigned; + delete obj.signatures; + var json = anotherjson.stringify(obj); + + olmDevice.verifySignature(signingKey, json, signature); + }); + + return function (_x15, _x16, _x17, _x18, _x19) { + return _ref4.apply(this, arguments); + }; +}(); + +},{"../utils":50,"another-json":52,"babel-runtime/core-js/json/stringify":55,"babel-runtime/regenerator":73,"bluebird":74}],18:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.Backend = exports.VERSION = undefined; + +var _assign = require('babel-runtime/core-js/object/assign'); + +var _assign2 = _interopRequireDefault(_assign); + +var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +var _createClass2 = require('babel-runtime/helpers/createClass'); + +var _createClass3 = _interopRequireDefault(_createClass2); + +exports.upgradeDatabase = upgradeDatabase; + +var _bluebird = require('bluebird'); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +var _utils = require('../../utils'); + +var _utils2 = _interopRequireDefault(_utils); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/* +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var VERSION = exports.VERSION = 6; + +/** + * Implementation of a CryptoStore which is backed by an existing + * IndexedDB connection. Generally you want IndexedDBCryptoStore + * which connects to the database and defers to one of these. + * + * @implements {module:crypto/store/base~CryptoStore} + */ + +var Backend = exports.Backend = function () { + /** + * @param {IDBDatabase} db + */ + function Backend(db) { + var _this = this; + + (0, _classCallCheck3.default)(this, Backend); + + this._db = db; + + // make sure we close the db on `onversionchange` - otherwise + // attempts to delete the database will block (and subsequent + // attempts to re-create it will also block). + db.onversionchange = function (ev) { + console.log('versionchange for indexeddb ' + _this._dbName + ': closing'); + db.close(); + }; + } + + /** + * Look for an existing outgoing room key request, and if none is found, + * add a new one + * + * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * same instance as passed in, or the existing one. + */ + + + (0, _createClass3.default)(Backend, [{ + key: 'getOrAddOutgoingRoomKeyRequest', + value: function getOrAddOutgoingRoomKeyRequest(request) { + var requestBody = request.requestBody; + + var deferred = _bluebird2.default.defer(); + var txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite"); + txn.onerror = deferred.reject; + + // first see if we already have an entry for this request. + this._getOutgoingRoomKeyRequest(txn, requestBody, function (existing) { + if (existing) { + // this entry matches the request - return it. + console.log('already have key request outstanding for ' + (requestBody.room_id + ' / ' + requestBody.session_id + ': ') + 'not sending another'); + deferred.resolve(existing); + return; + } + + // we got to the end of the list without finding a match + // - add the new request. + console.log('enqueueing key request for ' + requestBody.room_id + ' / ' + requestBody.session_id); + txn.oncomplete = function () { + deferred.resolve(request); + }; + var store = txn.objectStore("outgoingRoomKeyRequests"); + store.add(request); + }); + + return deferred.promise; + } + + /** + * Look for an existing room key request + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * existing request to look for + * + * @return {Promise} resolves to the matching + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * not found + */ + + }, { + key: 'getOutgoingRoomKeyRequest', + value: function getOutgoingRoomKeyRequest(requestBody) { + var deferred = _bluebird2.default.defer(); + + var txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); + txn.onerror = deferred.reject; + + this._getOutgoingRoomKeyRequest(txn, requestBody, function (existing) { + deferred.resolve(existing); + }); + return deferred.promise; + } + + /** + * look for an existing room key request in the db + * + * @private + * @param {IDBTransaction} txn database transaction + * @param {module:crypto~RoomKeyRequestBody} requestBody + * existing request to look for + * @param {Function} callback function to call with the results of the + * search. Either passed a matching + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * not found. + */ + + }, { + key: '_getOutgoingRoomKeyRequest', + value: function _getOutgoingRoomKeyRequest(txn, requestBody, callback) { + var store = txn.objectStore("outgoingRoomKeyRequests"); + + var idx = store.index("session"); + var cursorReq = idx.openCursor([requestBody.room_id, requestBody.session_id]); + + cursorReq.onsuccess = function (ev) { + var cursor = ev.target.result; + if (!cursor) { + // no match found + callback(null); + return; + } + + var existing = cursor.value; + + if (_utils2.default.deepCompare(existing.requestBody, requestBody)) { + // got a match + callback(existing); + return; + } + + // look at the next entry in the index + cursor.continue(); + }; + } + + /** + * Look for room key requests by state + * + * @param {Array} wantedStates list of acceptable states + * + * @return {Promise} resolves to the a + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * there are no pending requests in those states. If there are multiple + * requests in those states, an arbitrary one is chosen. + */ + + }, { + key: 'getOutgoingRoomKeyRequestByState', + value: function getOutgoingRoomKeyRequestByState(wantedStates) { + if (wantedStates.length === 0) { + return _bluebird2.default.resolve(null); + } + + // this is a bit tortuous because we need to make sure we do the lookup + // in a single transaction, to avoid having a race with the insertion + // code. + + // index into the wantedStates array + var stateIndex = 0; + var result = void 0; + + function onsuccess(ev) { + var cursor = ev.target.result; + if (cursor) { + // got a match + result = cursor.value; + return; + } + + // try the next state in the list + stateIndex++; + if (stateIndex >= wantedStates.length) { + // no matches + return; + } + + var wantedState = wantedStates[stateIndex]; + var cursorReq = ev.target.source.openCursor(wantedState); + cursorReq.onsuccess = onsuccess; + } + + var txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); + var store = txn.objectStore("outgoingRoomKeyRequests"); + + var wantedState = wantedStates[stateIndex]; + var cursorReq = store.index("state").openCursor(wantedState); + cursorReq.onsuccess = onsuccess; + + return promiseifyTxn(txn).then(function () { + return result; + }); + } + + /** + * Look for an existing room key request by id and state, and update it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * @param {Object} updates name/value map of updates to apply + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * updated request, or null if no matching row was found + */ + + }, { + key: 'updateOutgoingRoomKeyRequest', + value: function updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { + var result = null; + + function onsuccess(ev) { + var cursor = ev.target.result; + if (!cursor) { + return; + } + var data = cursor.value; + if (data.state != expectedState) { + console.warn('Cannot update room key request from ' + expectedState + ' ' + ('as it was already updated to ' + data.state)); + return; + } + (0, _assign2.default)(data, updates); + cursor.update(data); + result = data; + } + + var txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite"); + var cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId); + cursorReq.onsuccess = onsuccess; + return promiseifyTxn(txn).then(function () { + return result; + }); + } + + /** + * Look for an existing room key request by id and state, and delete it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * + * @returns {Promise} resolves once the operation is completed + */ + + }, { + key: 'deleteOutgoingRoomKeyRequest', + value: function deleteOutgoingRoomKeyRequest(requestId, expectedState) { + var txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite"); + var cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId); + cursorReq.onsuccess = function (ev) { + var cursor = ev.target.result; + if (!cursor) { + return; + } + var data = cursor.value; + if (data.state != expectedState) { + console.warn('Cannot delete room key request in state ' + data.state + ' ' + ('(expected ' + expectedState + ')')); + return; + } + cursor.delete(); + }; + return promiseifyTxn(txn); + } + + // Olm Account + + }, { + key: 'getAccount', + value: function getAccount(txn, func) { + var objectStore = txn.objectStore("account"); + var getReq = objectStore.get("-"); + getReq.onsuccess = function () { + try { + func(getReq.result || null); + } catch (e) { + abortWithException(txn, e); + } + }; + } + }, { + key: 'storeAccount', + value: function storeAccount(txn, newData) { + var objectStore = txn.objectStore("account"); + objectStore.put(newData, "-"); + } + + // Olm Sessions + + }, { + key: 'countEndToEndSessions', + value: function countEndToEndSessions(txn, func) { + var objectStore = txn.objectStore("sessions"); + var countReq = objectStore.count(); + countReq.onsuccess = function () { + func(countReq.result); + }; + } + }, { + key: 'getEndToEndSessions', + value: function getEndToEndSessions(deviceKey, txn, func) { + var objectStore = txn.objectStore("sessions"); + var idx = objectStore.index("deviceKey"); + var getReq = idx.openCursor(deviceKey); + var results = {}; + getReq.onsuccess = function () { + var cursor = getReq.result; + if (cursor) { + results[cursor.value.sessionId] = cursor.value.session; + cursor.continue(); + } else { + try { + func(results); + } catch (e) { + abortWithException(txn, e); + } + } + }; + } + }, { + key: 'getEndToEndSession', + value: function getEndToEndSession(deviceKey, sessionId, txn, func) { + var objectStore = txn.objectStore("sessions"); + var getReq = objectStore.get([deviceKey, sessionId]); + getReq.onsuccess = function () { + try { + if (getReq.result) { + func(getReq.result.session); + } else { + func(null); + } + } catch (e) { + abortWithException(txn, e); + } + }; + } + }, { + key: 'storeEndToEndSession', + value: function storeEndToEndSession(deviceKey, sessionId, session, txn) { + var objectStore = txn.objectStore("sessions"); + objectStore.put({ deviceKey: deviceKey, sessionId: sessionId, session: session }); + } + + // Inbound group sessions + + }, { + key: 'getEndToEndInboundGroupSession', + value: function getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + var objectStore = txn.objectStore("inbound_group_sessions"); + var getReq = objectStore.get([senderCurve25519Key, sessionId]); + getReq.onsuccess = function () { + try { + if (getReq.result) { + func(getReq.result.session); + } else { + func(null); + } + } catch (e) { + abortWithException(txn, e); + } + }; + } + }, { + key: 'getAllEndToEndInboundGroupSessions', + value: function getAllEndToEndInboundGroupSessions(txn, func) { + var objectStore = txn.objectStore("inbound_group_sessions"); + var getReq = objectStore.openCursor(); + getReq.onsuccess = function () { + var cursor = getReq.result; + if (cursor) { + try { + func({ + senderKey: cursor.value.senderCurve25519Key, + sessionId: cursor.value.sessionId, + sessionData: cursor.value.session + }); + } catch (e) { + abortWithException(txn, e); + } + cursor.continue(); + } else { + try { + func(null); + } catch (e) { + abortWithException(txn, e); + } + } + }; + } + }, { + key: 'addEndToEndInboundGroupSession', + value: function addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + var objectStore = txn.objectStore("inbound_group_sessions"); + var addReq = objectStore.add({ + senderCurve25519Key: senderCurve25519Key, sessionId: sessionId, session: sessionData + }); + addReq.onerror = function (ev) { + if (addReq.error.name === 'ConstraintError') { + // This stops the error from triggering the txn's onerror + ev.stopPropagation(); + // ...and this stops it from aborting the transaction + ev.preventDefault(); + console.log("Ignoring duplicate inbound group session: " + senderCurve25519Key + " / " + sessionId); + } else { + abortWithException(txn, new Error("Failed to add inbound group session: " + addReq.error)); + } + }; + } + }, { + key: 'storeEndToEndInboundGroupSession', + value: function storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + var objectStore = txn.objectStore("inbound_group_sessions"); + objectStore.put({ + senderCurve25519Key: senderCurve25519Key, sessionId: sessionId, session: sessionData + }); + } + }, { + key: 'getEndToEndDeviceData', + value: function getEndToEndDeviceData(txn, func) { + var objectStore = txn.objectStore("device_data"); + var getReq = objectStore.get("-"); + getReq.onsuccess = function () { + try { + func(getReq.result || null); + } catch (e) { + abortWithException(txn, e); + } + }; + } + }, { + key: 'storeEndToEndDeviceData', + value: function storeEndToEndDeviceData(deviceData, txn) { + var objectStore = txn.objectStore("device_data"); + objectStore.put(deviceData, "-"); + } + }, { + key: 'storeEndToEndRoom', + value: function storeEndToEndRoom(roomId, roomInfo, txn) { + var objectStore = txn.objectStore("rooms"); + objectStore.put(roomInfo, roomId); + } + }, { + key: 'getEndToEndRooms', + value: function getEndToEndRooms(txn, func) { + var rooms = {}; + var objectStore = txn.objectStore("rooms"); + var getReq = objectStore.openCursor(); + getReq.onsuccess = function () { + var cursor = getReq.result; + if (cursor) { + rooms[cursor.key] = cursor.value; + cursor.continue(); + } else { + try { + func(rooms); + } catch (e) { + abortWithException(txn, e); + } + } + }; + } + }, { + key: 'doTxn', + value: function doTxn(mode, stores, func) { + var txn = this._db.transaction(stores, mode); + var promise = promiseifyTxn(txn); + var result = func(txn); + return promise.then(function () { + return result; + }); + } + }]); + return Backend; +}(); + +function upgradeDatabase(db, oldVersion) { + console.log('Upgrading IndexedDBCryptoStore from version ' + oldVersion + (' to ' + VERSION)); + if (oldVersion < 1) { + // The database did not previously exist. + createDatabase(db); + } + if (oldVersion < 2) { + db.createObjectStore("account"); + } + if (oldVersion < 3) { + var sessionsStore = db.createObjectStore("sessions", { + keyPath: ["deviceKey", "sessionId"] + }); + sessionsStore.createIndex("deviceKey", "deviceKey"); + } + if (oldVersion < 4) { + db.createObjectStore("inbound_group_sessions", { + keyPath: ["senderCurve25519Key", "sessionId"] + }); + } + if (oldVersion < 5) { + db.createObjectStore("device_data"); + } + if (oldVersion < 6) { + db.createObjectStore("rooms"); + } + // Expand as needed. +} + +function createDatabase(db) { + var outgoingRoomKeyRequestsStore = db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" }); + + // we assume that the RoomKeyRequestBody will have room_id and session_id + // properties, to make the index efficient. + outgoingRoomKeyRequestsStore.createIndex("session", ["requestBody.room_id", "requestBody.session_id"]); + + outgoingRoomKeyRequestsStore.createIndex("state", "state"); +} + +/* + * Aborts a transaction with a given exception + * The transaction promise will be rejected with this exception. + */ +function abortWithException(txn, e) { + // We cheekily stick our exception onto the transaction object here + // We could alternatively make the thing we pass back to the app + // an object containing the transaction and exception. + txn._mx_abortexception = e; + try { + txn.abort(); + } catch (e) { + // sometimes we won't be able to abort the transaction + // (ie. if it's aborted or completed) + } +} + +function promiseifyTxn(txn) { + return new _bluebird2.default(function (resolve, reject) { + txn.oncomplete = function () { + if (txn._mx_abortexception !== undefined) { + reject(txn._mx_abortexception); + } + resolve(); + }; + txn.onerror = function () { + if (txn._mx_abortexception !== undefined) { + reject(txn._mx_abortexception); + } + reject(); + }; + txn.onabort = function () { + return reject(txn._mx_abortexception); + }; + }); +} + +},{"../../utils":50,"babel-runtime/core-js/object/assign":56,"babel-runtime/helpers/classCallCheck":66,"babel-runtime/helpers/createClass":67,"bluebird":74}],19:[function(require,module,exports){ +(function (global){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +var _createClass2 = require('babel-runtime/helpers/createClass'); + +var _createClass3 = _interopRequireDefault(_createClass2); + +var _bluebird = require('bluebird'); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +var _localStorageCryptoStore = require('./localStorage-crypto-store'); + +var _localStorageCryptoStore2 = _interopRequireDefault(_localStorageCryptoStore); + +var _memoryCryptoStore = require('./memory-crypto-store'); + +var _memoryCryptoStore2 = _interopRequireDefault(_memoryCryptoStore); + +var _indexeddbCryptoStoreBackend = require('./indexeddb-crypto-store-backend'); + +var IndexedDBCryptoStoreBackend = _interopRequireWildcard(_indexeddbCryptoStoreBackend); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Internal module. indexeddb storage for e2e. + * + * @module + */ + +/** + * An implementation of CryptoStore, which is normally backed by an indexeddb, + * but with fallback to MemoryCryptoStore. + * + * @implements {module:crypto/store/base~CryptoStore} + */ +/* +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var IndexedDBCryptoStore = function () { + /** + * Create a new IndexedDBCryptoStore + * + * @param {IDBFactory} indexedDB global indexedDB instance + * @param {string} dbName name of db to connect to + */ + function IndexedDBCryptoStore(indexedDB, dbName) { + (0, _classCallCheck3.default)(this, IndexedDBCryptoStore); + + this._indexedDB = indexedDB; + this._dbName = dbName; + this._backendPromise = null; + } + + /** + * Ensure the database exists and is up-to-date, or fall back to + * a local storage or in-memory store. + * + * @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend, + * or a MemoryCryptoStore + */ + + + (0, _createClass3.default)(IndexedDBCryptoStore, [{ + key: '_connect', + value: function _connect() { + var _this = this; + + if (this._backendPromise) { + return this._backendPromise; + } + + this._backendPromise = new _bluebird2.default(function (resolve, reject) { + if (!_this._indexedDB) { + reject(new Error('no indexeddb support available')); + return; + } + + console.log('connecting to indexeddb ' + _this._dbName); + + var req = _this._indexedDB.open(_this._dbName, IndexedDBCryptoStoreBackend.VERSION); + + req.onupgradeneeded = function (ev) { + var db = ev.target.result; + var oldVersion = ev.oldVersion; + IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion); + }; + + req.onblocked = function () { + console.log('can\'t yet open IndexedDBCryptoStore because it is open elsewhere'); + }; + + req.onerror = function (ev) { + reject(ev.target.error); + }; + + req.onsuccess = function (r) { + var db = r.target.result; + + console.log('connected to indexeddb ' + _this._dbName); + resolve(new IndexedDBCryptoStoreBackend.Backend(db)); + }; + }).catch(function (e) { + console.warn('unable to connect to indexeddb ' + _this._dbName + (': falling back to localStorage store: ' + e)); + return new _localStorageCryptoStore2.default(global.localStorage); + }).catch(function (e) { + console.warn('unable to open localStorage: falling back to in-memory store: ' + e); + return new _memoryCryptoStore2.default(); + }); + + return this._backendPromise; + } + + /** + * Delete all data from this store. + * + * @returns {Promise} resolves when the store has been cleared. + */ + + }, { + key: 'deleteAllData', + value: function deleteAllData() { + var _this2 = this; + + return new _bluebird2.default(function (resolve, reject) { + if (!_this2._indexedDB) { + reject(new Error('no indexeddb support available')); + return; + } + + console.log('Removing indexeddb instance: ' + _this2._dbName); + var req = _this2._indexedDB.deleteDatabase(_this2._dbName); + + req.onblocked = function () { + console.log('can\'t yet delete IndexedDBCryptoStore because it is open elsewhere'); + }; + + req.onerror = function (ev) { + reject(ev.target.error); + }; + + req.onsuccess = function () { + console.log('Removed indexeddb instance: ' + _this2._dbName); + resolve(); + }; + }).catch(function (e) { + // in firefox, with indexedDB disabled, this fails with a + // DOMError. We treat this as non-fatal, so that people can + // still use the app. + console.warn('unable to delete IndexedDBCryptoStore: ' + e); + }); + } + + /** + * Look for an existing outgoing room key request, and if none is found, + * add a new one + * + * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * same instance as passed in, or the existing one. + */ + + }, { + key: 'getOrAddOutgoingRoomKeyRequest', + value: function getOrAddOutgoingRoomKeyRequest(request) { + return this._connect().then(function (backend) { + return backend.getOrAddOutgoingRoomKeyRequest(request); + }); + } + + /** + * Look for an existing room key request + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * existing request to look for + * + * @return {Promise} resolves to the matching + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * not found + */ + + }, { + key: 'getOutgoingRoomKeyRequest', + value: function getOutgoingRoomKeyRequest(requestBody) { + return this._connect().then(function (backend) { + return backend.getOutgoingRoomKeyRequest(requestBody); + }); + } + + /** + * Look for room key requests by state + * + * @param {Array} wantedStates list of acceptable states + * + * @return {Promise} resolves to the a + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * there are no pending requests in those states. If there are multiple + * requests in those states, an arbitrary one is chosen. + */ + + }, { + key: 'getOutgoingRoomKeyRequestByState', + value: function getOutgoingRoomKeyRequestByState(wantedStates) { + return this._connect().then(function (backend) { + return backend.getOutgoingRoomKeyRequestByState(wantedStates); + }); + } + + /** + * Look for an existing room key request by id and state, and update it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * @param {Object} updates name/value map of updates to apply + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * updated request, or null if no matching row was found + */ + + }, { + key: 'updateOutgoingRoomKeyRequest', + value: function updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { + return this._connect().then(function (backend) { + return backend.updateOutgoingRoomKeyRequest(requestId, expectedState, updates); + }); + } + + /** + * Look for an existing room key request by id and state, and delete it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * + * @returns {Promise} resolves once the operation is completed + */ + + }, { + key: 'deleteOutgoingRoomKeyRequest', + value: function deleteOutgoingRoomKeyRequest(requestId, expectedState) { + return this._connect().then(function (backend) { + return backend.deleteOutgoingRoomKeyRequest(requestId, expectedState); + }); + } + + // Olm Account + + /* + * Get the account pickle from the store. + * This requires an active transaction. See doTxn(). + * + * @param {*} txn An active transaction. See doTxn(). + * @param {function(string)} func Called with the account pickle + */ + + }, { + key: 'getAccount', + value: function getAccount(txn, func) { + this._backendPromise.value().getAccount(txn, func); + } + + /* + * Write the account pickle to the store. + * This requires an active transaction. See doTxn(). + * + * @param {*} txn An active transaction. See doTxn(). + * @param {string} newData The new account pickle to store. + */ + + }, { + key: 'storeAccount', + value: function storeAccount(txn, newData) { + this._backendPromise.value().storeAccount(txn, newData); + } + + // Olm sessions + + /** + * Returns the number of end-to-end sessions in the store + * @param {*} txn An active transaction. See doTxn(). + * @param {function(int)} func Called with the count of sessions + */ + + }, { + key: 'countEndToEndSessions', + value: function countEndToEndSessions(txn, func) { + this._backendPromise.value().countEndToEndSessions(txn, func); + } + + /** + * Retrieve a specific end-to-end session between the logged-in user + * and another device. + * @param {string} deviceKey The public key of the other device. + * @param {string} sessionId The ID of the session to retrieve + * @param {*} txn An active transaction. See doTxn(). + * @param {function(object)} func Called with A map from sessionId + * to Base64 end-to-end session. + */ + + }, { + key: 'getEndToEndSession', + value: function getEndToEndSession(deviceKey, sessionId, txn, func) { + this._backendPromise.value().getEndToEndSession(deviceKey, sessionId, txn, func); + } + + /** + * Retrieve the end-to-end sessions between the logged-in user and another + * device. + * @param {string} deviceKey The public key of the other device. + * @param {*} txn An active transaction. See doTxn(). + * @param {function(object)} func Called with A map from sessionId + * to Base64 end-to-end session. + */ + + }, { + key: 'getEndToEndSessions', + value: function getEndToEndSessions(deviceKey, txn, func) { + this._backendPromise.value().getEndToEndSessions(deviceKey, txn, func); + } + + /** + * Store a session between the logged-in user and another device + * @param {string} deviceKey The public key of the other device. + * @param {string} sessionId The ID for this end-to-end session. + * @param {string} session Base64 encoded end-to-end session. + * @param {*} txn An active transaction. See doTxn(). + */ + + }, { + key: 'storeEndToEndSession', + value: function storeEndToEndSession(deviceKey, sessionId, session, txn) { + this._backendPromise.value().storeEndToEndSession(deviceKey, sessionId, session, txn); + } + + // Inbound group saessions + + /** + * Retrieve the end-to-end inbound group session for a given + * server key and session ID + * @param {string} senderCurve25519Key The sender's curve 25519 key + * @param {string} sessionId The ID of the session + * @param {*} txn An active transaction. See doTxn(). + * @param {function(object)} func Called with A map from sessionId + * to Base64 end-to-end session. + */ + + }, { + key: 'getEndToEndInboundGroupSession', + value: function getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + this._backendPromise.value().getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func); + } + + /** + * Fetches all inbound group sessions in the store + * @param {*} txn An active transaction. See doTxn(). + * @param {function(object)} func Called once for each group session + * in the store with an object having keys {senderKey, sessionId, + * sessionData}, then once with null to indicate the end of the list. + */ + + }, { + key: 'getAllEndToEndInboundGroupSessions', + value: function getAllEndToEndInboundGroupSessions(txn, func) { + this._backendPromise.value().getAllEndToEndInboundGroupSessions(txn, func); + } + + /** + * Adds an end-to-end inbound group session to the store. + * If there already exists an inbound group session with the same + * senderCurve25519Key and sessionID, the session will not be added. + * @param {string} senderCurve25519Key The sender's curve 25519 key + * @param {string} sessionId The ID of the session + * @param {object} sessionData The session data structure + * @param {*} txn An active transaction. See doTxn(). + */ + + }, { + key: 'addEndToEndInboundGroupSession', + value: function addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + this._backendPromise.value().addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); + } + + /** + * Writes an end-to-end inbound group session to the store. + * If there already exists an inbound group session with the same + * senderCurve25519Key and sessionID, it will be overwritten. + * @param {string} senderCurve25519Key The sender's curve 25519 key + * @param {string} sessionId The ID of the session + * @param {object} sessionData The session data structure + * @param {*} txn An active transaction. See doTxn(). + */ + + }, { + key: 'storeEndToEndInboundGroupSession', + value: function storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + this._backendPromise.value().storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); + } + + // End-to-end device tracking + + /** + * Store the state of all tracked devices + * This contains devices for each user, a tracking state for each user + * and a sync token matching the point in time the snapshot represents. + * These all need to be written out in full each time such that the snapshot + * is always consistent, so they are stored in one object. + * + * @param {Object} deviceData + * @param {*} txn An active transaction. See doTxn(). + */ + + }, { + key: 'storeEndToEndDeviceData', + value: function storeEndToEndDeviceData(deviceData, txn) { + this._backendPromise.value().storeEndToEndDeviceData(deviceData, txn); + } + + /** + * Get the state of all tracked devices + * + * @param {*} txn An active transaction. See doTxn(). + * @param {function(Object)} func Function called with the + * device data + */ + + }, { + key: 'getEndToEndDeviceData', + value: function getEndToEndDeviceData(txn, func) { + this._backendPromise.value().getEndToEndDeviceData(txn, func); + } + + // End to End Rooms + + /** + * Store the end-to-end state for a room. + * @param {string} roomId The room's ID. + * @param {object} roomInfo The end-to-end info for the room. + * @param {*} txn An active transaction. See doTxn(). + */ + + }, { + key: 'storeEndToEndRoom', + value: function storeEndToEndRoom(roomId, roomInfo, txn) { + this._backendPromise.value().storeEndToEndRoom(roomId, roomInfo, txn); + } + + /** + * Get an object of roomId->roomInfo for all e2e rooms in the store + * @param {*} txn An active transaction. See doTxn(). + * @param {function(Object)} func Function called with the end to end encrypted rooms + */ + + }, { + key: 'getEndToEndRooms', + value: function getEndToEndRooms(txn, func) { + this._backendPromise.value().getEndToEndRooms(txn, func); + } + + /** + * Perform a transaction on the crypto store. Any store methods + * that require a transaction (txn) object to be passed in may + * only be called within a callback of either this function or + * one of the store functions operating on the same transaction. + * + * @param {string} mode 'readwrite' if you need to call setter + * functions with this transaction. Otherwise, 'readonly'. + * @param {string[]} stores List IndexedDBCryptoStore.STORE_* + * options representing all types of object that will be + * accessed or written to with this transaction. + * @param {function(*)} func Function called with the + * transaction object: an opaque object that should be passed + * to store functions. + * @return {Promise} Promise that resolves with the result of the `func` + * when the transaction is complete. If the backend is + * async (ie. the indexeddb backend) any of the callback + * functions throwing an exception will cause this promise to + * reject with that exception. On synchronous backends, the + * exception will propagate to the caller of the getFoo method. + */ + + }, { + key: 'doTxn', + value: function doTxn(mode, stores, func) { + return this._connect().then(function (backend) { + return backend.doTxn(mode, stores, func); + }); + } + }]); + return IndexedDBCryptoStore; +}(); + +exports.default = IndexedDBCryptoStore; + + +IndexedDBCryptoStore.STORE_ACCOUNT = 'account'; +IndexedDBCryptoStore.STORE_SESSIONS = 'sessions'; +IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; +IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data'; +IndexedDBCryptoStore.STORE_ROOMS = 'rooms'; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"./indexeddb-crypto-store-backend":18,"./localStorage-crypto-store":20,"./memory-crypto-store":21,"babel-runtime/helpers/classCallCheck":66,"babel-runtime/helpers/createClass":67,"bluebird":74}],20:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _stringify = require('babel-runtime/core-js/json/stringify'); + +var _stringify2 = _interopRequireDefault(_stringify); + +var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of'); + +var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf); + +var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +var _createClass2 = require('babel-runtime/helpers/createClass'); + +var _createClass3 = _interopRequireDefault(_createClass2); + +var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn'); + +var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2); + +var _inherits2 = require('babel-runtime/helpers/inherits'); + +var _inherits3 = _interopRequireDefault(_inherits2); + +var _bluebird = require('bluebird'); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +var _memoryCryptoStore = require('./memory-crypto-store.js'); + +var _memoryCryptoStore2 = _interopRequireDefault(_memoryCryptoStore); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Internal module. Partial localStorage backed storage for e2e. + * This is not a full crypto store, just the in-memory store with + * some things backed by localStorage. It exists because indexedDB + * is broken in Firefox private mode or set to, "will not remember + * history". + * + * @module + */ + +/* +Copyright 2017, 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var E2E_PREFIX = "crypto."; +var KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; +var KEY_DEVICE_DATA = E2E_PREFIX + "device_data"; +var KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/"; +var KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/"; + +function keyEndToEndSessions(deviceKey) { + return E2E_PREFIX + "sessions/" + deviceKey; +} + +function keyEndToEndInboundGroupSession(senderKey, sessionId) { + return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId; +} + +function keyEndToEndRoomsPrefix(roomId) { + return KEY_ROOMS_PREFIX + roomId; +} + +/** + * @implements {module:crypto/store/base~CryptoStore} + */ + +var LocalStorageCryptoStore = function (_MemoryCryptoStore) { + (0, _inherits3.default)(LocalStorageCryptoStore, _MemoryCryptoStore); + + function LocalStorageCryptoStore(webStore) { + (0, _classCallCheck3.default)(this, LocalStorageCryptoStore); + + var _this = (0, _possibleConstructorReturn3.default)(this, (LocalStorageCryptoStore.__proto__ || (0, _getPrototypeOf2.default)(LocalStorageCryptoStore)).call(this)); + + _this.store = webStore; + return _this; + } + + // Olm Sessions + + (0, _createClass3.default)(LocalStorageCryptoStore, [{ + key: 'countEndToEndSessions', + value: function countEndToEndSessions(txn, func) { + var count = 0; + for (var i = 0; i < this.store.length; ++i) { + if (this.store.key(i).startsWith(keyEndToEndSessions(''))) ++count; + } + func(count); + } + }, { + key: '_getEndToEndSessions', + value: function _getEndToEndSessions(deviceKey, txn, func) { + return getJsonItem(this.store, keyEndToEndSessions(deviceKey)); + } + }, { + key: 'getEndToEndSession', + value: function getEndToEndSession(deviceKey, sessionId, txn, func) { + var sessions = this._getEndToEndSessions(deviceKey); + func(sessions[sessionId] || {}); + } + }, { + key: 'getEndToEndSessions', + value: function getEndToEndSessions(deviceKey, txn, func) { + func(this._getEndToEndSessions(deviceKey) || {}); + } + }, { + key: 'storeEndToEndSession', + value: function storeEndToEndSession(deviceKey, sessionId, session, txn) { + var sessions = this._getEndToEndSessions(deviceKey) || {}; + sessions[sessionId] = session; + setJsonItem(this.store, keyEndToEndSessions(deviceKey), sessions); + } + + // Inbound Group Sessions + + }, { + key: 'getEndToEndInboundGroupSession', + value: function getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + func(getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId))); + } + }, { + key: 'getAllEndToEndInboundGroupSessions', + value: function getAllEndToEndInboundGroupSessions(txn, func) { + for (var i = 0; i < this.store.length; ++i) { + var key = this.store.key(i); + if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) { + // we can't use split, as the components we are trying to split out + // might themselves contain '/' characters. We rely on the + // senderKey being a (32-byte) curve25519 key, base64-encoded + // (hence 43 characters long). + + func({ + senderKey: key.substr(KEY_INBOUND_SESSION_PREFIX.length, 43), + sessionId: key.substr(KEY_INBOUND_SESSION_PREFIX.length + 44), + sessionData: getJsonItem(this.store, key) + }); + } + } + func(null); + } + }, { + key: 'addEndToEndInboundGroupSession', + value: function addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + var existing = getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId)); + if (!existing) { + this.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); + } + } + }, { + key: 'storeEndToEndInboundGroupSession', + value: function storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + setJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), sessionData); + } + }, { + key: 'getEndToEndDeviceData', + value: function getEndToEndDeviceData(txn, func) { + func(getJsonItem(this.store, KEY_DEVICE_DATA)); + } + }, { + key: 'storeEndToEndDeviceData', + value: function storeEndToEndDeviceData(deviceData, txn) { + setJsonItem(this.store, KEY_DEVICE_DATA, deviceData); + } + }, { + key: 'storeEndToEndRoom', + value: function storeEndToEndRoom(roomId, roomInfo, txn) { + setJsonItem(this.store, keyEndToEndRoomsPrefix(roomId), roomInfo); + } + }, { + key: 'getEndToEndRooms', + value: function getEndToEndRooms(txn, func) { + var result = {}; + var prefix = keyEndToEndRoomsPrefix(''); + + for (var i = 0; i < this.store.length; ++i) { + var key = this.store.key(i); + if (key.startsWith(prefix)) { + var roomId = key.substr(prefix.length); + result[roomId] = getJsonItem(this.store, key); + } + } + func(result); + } + + /** + * Delete all data from this store. + * + * @returns {Promise} Promise which resolves when the store has been cleared. + */ + + }, { + key: 'deleteAllData', + value: function deleteAllData() { + this.store.removeItem(KEY_END_TO_END_ACCOUNT); + return _bluebird2.default.resolve(); + } + + // Olm account + + }, { + key: 'getAccount', + value: function getAccount(txn, func) { + var account = getJsonItem(this.store, KEY_END_TO_END_ACCOUNT); + func(account); + } + }, { + key: 'storeAccount', + value: function storeAccount(txn, newData) { + setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, newData); + } + }, { + key: 'doTxn', + value: function doTxn(mode, stores, func) { + return _bluebird2.default.resolve(func(null)); + } + }]); + return LocalStorageCryptoStore; +}(_memoryCryptoStore2.default); + +exports.default = LocalStorageCryptoStore; + + +function getJsonItem(store, key) { + try { + // if the key is absent, store.getItem() returns null, and + // JSON.parse(null) === null, so this returns null. + return JSON.parse(store.getItem(key)); + } catch (e) { + console.log("Error: Failed to get key %s: %s", key, e.stack || e); + console.log(e.stack); + } + return null; +} + +function setJsonItem(store, key, val) { + store.setItem(key, (0, _stringify2.default)(val)); +} + +},{"./memory-crypto-store.js":21,"babel-runtime/core-js/json/stringify":55,"babel-runtime/core-js/object/get-prototype-of":60,"babel-runtime/helpers/classCallCheck":66,"babel-runtime/helpers/createClass":67,"babel-runtime/helpers/inherits":69,"babel-runtime/helpers/possibleConstructorReturn":70,"bluebird":74}],21:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _keys = require('babel-runtime/core-js/object/keys'); + +var _keys2 = _interopRequireDefault(_keys); + +var _assign = require('babel-runtime/core-js/object/assign'); + +var _assign2 = _interopRequireDefault(_assign); + +var _getIterator2 = require('babel-runtime/core-js/get-iterator'); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +var _createClass2 = require('babel-runtime/helpers/createClass'); + +var _createClass3 = _interopRequireDefault(_createClass2); + +var _bluebird = require('bluebird'); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +var _utils = require('../../utils'); + +var _utils2 = _interopRequireDefault(_utils); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Internal module. in-memory storage for e2e. + * + * @module + */ + +/** + * @implements {module:crypto/store/base~CryptoStore} + */ +/* +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var MemoryCryptoStore = function () { + function MemoryCryptoStore() { + (0, _classCallCheck3.default)(this, MemoryCryptoStore); + + this._outgoingRoomKeyRequests = []; + this._account = null; + + // Map of {devicekey -> {sessionId -> session pickle}} + this._sessions = {}; + // Map of {senderCurve25519Key+'/'+sessionId -> session data object} + this._inboundGroupSessions = {}; + // Opaque device data object + this._deviceData = null; + // roomId -> Opaque roomInfo object + this._rooms = {}; + } + + /** + * Delete all data from this store. + * + * @returns {Promise} Promise which resolves when the store has been cleared. + */ + + + (0, _createClass3.default)(MemoryCryptoStore, [{ + key: 'deleteAllData', + value: function deleteAllData() { + return _bluebird2.default.resolve(); + } + + /** + * Look for an existing outgoing room key request, and if none is found, + * add a new one + * + * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * same instance as passed in, or the existing one. + */ + + }, { + key: 'getOrAddOutgoingRoomKeyRequest', + value: function getOrAddOutgoingRoomKeyRequest(request) { + var _this = this; + + var requestBody = request.requestBody; + + return _bluebird2.default.try(function () { + // first see if we already have an entry for this request. + var existing = _this._getOutgoingRoomKeyRequest(requestBody); + + if (existing) { + // this entry matches the request - return it. + console.log('already have key request outstanding for ' + (requestBody.room_id + ' / ' + requestBody.session_id + ': ') + 'not sending another'); + return existing; + } + + // we got to the end of the list without finding a match + // - add the new request. + console.log('enqueueing key request for ' + requestBody.room_id + ' / ' + requestBody.session_id); + _this._outgoingRoomKeyRequests.push(request); + return request; + }); + } + + /** + * Look for an existing room key request + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * existing request to look for + * + * @return {Promise} resolves to the matching + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * not found + */ + + }, { + key: 'getOutgoingRoomKeyRequest', + value: function getOutgoingRoomKeyRequest(requestBody) { + return _bluebird2.default.resolve(this._getOutgoingRoomKeyRequest(requestBody)); + } + + /** + * Looks for existing room key request, and returns the result synchronously. + * + * @internal + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * existing request to look for + * + * @return {module:crypto/store/base~OutgoingRoomKeyRequest?} + * the matching request, or null if not found + */ + + }, { + key: '_getOutgoingRoomKeyRequest', + value: function _getOutgoingRoomKeyRequest(requestBody) { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)(this._outgoingRoomKeyRequests), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var existing = _step.value; + + if (_utils2.default.deepCompare(existing.requestBody, requestBody)) { + return existing; + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return null; + } + + /** + * Look for room key requests by state + * + * @param {Array} wantedStates list of acceptable states + * + * @return {Promise} resolves to the a + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * there are no pending requests in those states + */ + + }, { + key: 'getOutgoingRoomKeyRequestByState', + value: function getOutgoingRoomKeyRequestByState(wantedStates) { + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = (0, _getIterator3.default)(this._outgoingRoomKeyRequests), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var req = _step2.value; + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; + + try { + for (var _iterator3 = (0, _getIterator3.default)(wantedStates), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var state = _step3.value; + + if (req.state === state) { + return _bluebird2.default.resolve(req); + } + } + } catch (err) { + _didIteratorError3 = true; + _iteratorError3 = err; + } finally { + try { + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); + } + } finally { + if (_didIteratorError3) { + throw _iteratorError3; + } + } + } + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + return _bluebird2.default.resolve(null); + } + + /** + * Look for an existing room key request by id and state, and update it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * @param {Object} updates name/value map of updates to apply + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * updated request, or null if no matching row was found + */ + + }, { + key: 'updateOutgoingRoomKeyRequest', + value: function updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { + var _iteratorNormalCompletion4 = true; + var _didIteratorError4 = false; + var _iteratorError4 = undefined; + + try { + for (var _iterator4 = (0, _getIterator3.default)(this._outgoingRoomKeyRequests), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { + var req = _step4.value; + + if (req.requestId !== requestId) { + continue; + } + + if (req.state != expectedState) { + console.warn('Cannot update room key request from ' + expectedState + ' ' + ('as it was already updated to ' + req.state)); + return _bluebird2.default.resolve(null); + } + (0, _assign2.default)(req, updates); + return _bluebird2.default.resolve(req); + } + } catch (err) { + _didIteratorError4 = true; + _iteratorError4 = err; + } finally { + try { + if (!_iteratorNormalCompletion4 && _iterator4.return) { + _iterator4.return(); + } + } finally { + if (_didIteratorError4) { + throw _iteratorError4; + } + } + } + + return _bluebird2.default.resolve(null); + } + + /** + * Look for an existing room key request by id and state, and delete it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * + * @returns {Promise} resolves once the operation is completed + */ + + }, { + key: 'deleteOutgoingRoomKeyRequest', + value: function deleteOutgoingRoomKeyRequest(requestId, expectedState) { + for (var i = 0; i < this._outgoingRoomKeyRequests.length; i++) { + var req = this._outgoingRoomKeyRequests[i]; + + if (req.requestId !== requestId) { + continue; + } + + if (req.state != expectedState) { + console.warn('Cannot delete room key request in state ' + req.state + ' ' + ('(expected ' + expectedState + ')')); + return _bluebird2.default.resolve(null); + } + + this._outgoingRoomKeyRequests.splice(i, 1); + return _bluebird2.default.resolve(req); + } + + return _bluebird2.default.resolve(null); + } + + // Olm Account + + }, { + key: 'getAccount', + value: function getAccount(txn, func) { + func(this._account); + } + }, { + key: 'storeAccount', + value: function storeAccount(txn, newData) { + this._account = newData; + } + + // Olm Sessions + + }, { + key: 'countEndToEndSessions', + value: function countEndToEndSessions(txn, func) { + return (0, _keys2.default)(this._sessions).length; + } + }, { + key: 'getEndToEndSession', + value: function getEndToEndSession(deviceKey, sessionId, txn, func) { + var deviceSessions = this._sessions[deviceKey] || {}; + func(deviceSessions[sessionId] || null); + } + }, { + key: 'getEndToEndSessions', + value: function getEndToEndSessions(deviceKey, txn, func) { + func(this._sessions[deviceKey] || {}); + } + }, { + key: 'storeEndToEndSession', + value: function storeEndToEndSession(deviceKey, sessionId, session, txn) { + var deviceSessions = this._sessions[deviceKey]; + if (deviceSessions === undefined) { + deviceSessions = {}; + this._sessions[deviceKey] = deviceSessions; + } + deviceSessions[sessionId] = session; + } + + // Inbound Group Sessions + + }, { + key: 'getEndToEndInboundGroupSession', + value: function getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + func(this._inboundGroupSessions[senderCurve25519Key + '/' + sessionId] || null); + } + }, { + key: 'getAllEndToEndInboundGroupSessions', + value: function getAllEndToEndInboundGroupSessions(txn, func) { + var _iteratorNormalCompletion5 = true; + var _didIteratorError5 = false; + var _iteratorError5 = undefined; + + try { + for (var _iterator5 = (0, _getIterator3.default)((0, _keys2.default)(this._inboundGroupSessions)), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { + var key = _step5.value; + + // we can't use split, as the components we are trying to split out + // might themselves contain '/' characters. We rely on the + // senderKey being a (32-byte) curve25519 key, base64-encoded + // (hence 43 characters long). + + func({ + senderKey: key.substr(0, 43), + sessionId: key.substr(44), + sessionData: this._inboundGroupSessions[key] + }); + } + } catch (err) { + _didIteratorError5 = true; + _iteratorError5 = err; + } finally { + try { + if (!_iteratorNormalCompletion5 && _iterator5.return) { + _iterator5.return(); + } + } finally { + if (_didIteratorError5) { + throw _iteratorError5; + } + } + } + + func(null); + } + }, { + key: 'addEndToEndInboundGroupSession', + value: function addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + var k = senderCurve25519Key + '/' + sessionId; + if (this._inboundGroupSessions[k] === undefined) { + this._inboundGroupSessions[k] = sessionData; + } + } + }, { + key: 'storeEndToEndInboundGroupSession', + value: function storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + this._inboundGroupSessions[senderCurve25519Key + '/' + sessionId] = sessionData; + } + + // Device Data + + }, { + key: 'getEndToEndDeviceData', + value: function getEndToEndDeviceData(txn, func) { + func(this._deviceData); + } + }, { + key: 'storeEndToEndDeviceData', + value: function storeEndToEndDeviceData(deviceData, txn) { + this._deviceData = deviceData; + } + + // E2E rooms + + }, { + key: 'storeEndToEndRoom', + value: function storeEndToEndRoom(roomId, roomInfo, txn) { + this._rooms[roomId] = roomInfo; + } + }, { + key: 'getEndToEndRooms', + value: function getEndToEndRooms(txn, func) { + func(this._rooms); + } + }, { + key: 'doTxn', + value: function doTxn(mode, stores, func) { + return _bluebird2.default.resolve(func(null)); + } + }]); + return MemoryCryptoStore; +}(); + +exports.default = MemoryCryptoStore; + +},{"../../utils":50,"babel-runtime/core-js/get-iterator":53,"babel-runtime/core-js/object/assign":56,"babel-runtime/core-js/object/keys":61,"babel-runtime/helpers/classCallCheck":66,"babel-runtime/helpers/createClass":67,"bluebird":74}],22:[function(require,module,exports){ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; +/** + * @module filter-component + */ + +/** + * Checks if a value matches a given field value, which may be a * terminated + * wildcard pattern. + * @param {String} actual_value The value to be compared + * @param {String} filter_value The filter pattern to be compared + * @return {bool} true if the actual_value matches the filter_value + */ + +var _keys = require("babel-runtime/core-js/object/keys"); + +var _keys2 = _interopRequireDefault(_keys); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _matches_wildcard(actual_value, filter_value) { + if (filter_value.endsWith("*")) { + var type_prefix = filter_value.slice(0, -1); + return actual_value.substr(0, type_prefix.length) === type_prefix; + } else { + return actual_value === filter_value; + } +} + +/** + * FilterComponent is a section of a Filter definition which defines the + * types, rooms, senders filters etc to be applied to a particular type of resource. + * This is all ported over from synapse's Filter object. + * + * N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as + * 'Filters' are referred to as 'FilterCollections'. + * + * @constructor + * @param {Object} filter_json the definition of this filter JSON, e.g. { 'contains_url': true } + */ +function FilterComponent(filter_json) { + this.filter_json = filter_json; + + this.types = filter_json.types || null; + this.not_types = filter_json.not_types || []; + + this.rooms = filter_json.rooms || null; + this.not_rooms = filter_json.not_rooms || []; + + this.senders = filter_json.senders || null; + this.not_senders = filter_json.not_senders || []; + + this.contains_url = filter_json.contains_url || null; +} + +/** + * Checks with the filter component matches the given event + * @param {MatrixEvent} event event to be checked against the filter + * @return {bool} true if the event matches the filter + */ +FilterComponent.prototype.check = function (event) { + return this._checkFields(event.getRoomId(), event.getSender(), event.getType(), event.getContent() ? event.getContent().url !== undefined : false); +}; + +/** + * Checks whether the filter component matches the given event fields. + * @param {String} room_id the room_id for the event being checked + * @param {String} sender the sender of the event being checked + * @param {String} event_type the type of the event being checked + * @param {String} contains_url whether the event contains a content.url field + * @return {bool} true if the event fields match the filter + */ +FilterComponent.prototype._checkFields = function (room_id, sender, event_type, contains_url) { + var literal_keys = { + "rooms": function rooms(v) { + return room_id === v; + }, + "senders": function senders(v) { + return sender === v; + }, + "types": function types(v) { + return _matches_wildcard(event_type, v); + } + }; + + var self = this; + for (var n = 0; n < (0, _keys2.default)(literal_keys).length; n++) { + var name = (0, _keys2.default)(literal_keys)[n]; + var match_func = literal_keys[name]; + var not_name = "not_" + name; + var disallowed_values = self[not_name]; + if (disallowed_values.filter(match_func).length > 0) { + return false; + } + + var allowed_values = self[name]; + if (allowed_values) { + if (!allowed_values.map(match_func)) { + return false; + } + } + } + + var contains_url_filter = this.filter_json.contains_url; + if (contains_url_filter !== undefined) { + if (contains_url_filter !== contains_url) { + return false; + } + } + + return true; +}; + +/** + * Filters a list of events down to those which match this filter component + * @param {MatrixEvent[]} events Events to be checked againt the filter component + * @return {MatrixEvent[]} events which matched the filter component + */ +FilterComponent.prototype.filter = function (events) { + return events.filter(this.check, this); +}; + +/** + * Returns the limit field for a given filter component, providing a default of + * 10 if none is otherwise specified. Cargo-culted from Synapse. + * @return {Number} the limit for this filter component. + */ +FilterComponent.prototype.limit = function () { + return this.filter_json.limit !== undefined ? this.filter_json.limit : 10; +}; + +/** The FilterComponent class */ +module.exports = FilterComponent; + +},{"babel-runtime/core-js/object/keys":61}],23:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; +/** + * @module filter + */ + +var FilterComponent = require("./filter-component"); + +/** + * @param {Object} obj + * @param {string} keyNesting + * @param {*} val + */ +function setProp(obj, keyNesting, val) { + var nestedKeys = keyNesting.split("."); + var currentObj = obj; + for (var i = 0; i < nestedKeys.length - 1; i++) { + if (!currentObj[nestedKeys[i]]) { + currentObj[nestedKeys[i]] = {}; + } + currentObj = currentObj[nestedKeys[i]]; + } + currentObj[nestedKeys[nestedKeys.length - 1]] = val; +} + +/** + * Construct a new Filter. + * @constructor + * @param {string} userId The user ID for this filter. + * @param {string=} filterId The filter ID if known. + * @prop {string} userId The user ID of the filter + * @prop {?string} filterId The filter ID + */ +function Filter(userId, filterId) { + this.userId = userId; + this.filterId = filterId; + this.definition = {}; +} + +/** + * Get the ID of this filter on your homeserver (if known) + * @return {?Number} The filter ID + */ +Filter.prototype.getFilterId = function () { + return this.filterId; +}; + +/** + * Get the JSON body of the filter. + * @return {Object} The filter definition + */ +Filter.prototype.getDefinition = function () { + return this.definition; +}; + +/** + * Set the JSON body of the filter + * @param {Object} definition The filter definition + */ +Filter.prototype.setDefinition = function (definition) { + this.definition = definition; + + // This is all ported from synapse's FilterCollection() + + // definitions look something like: + // { + // "room": { + // "rooms": ["!abcde:example.com"], + // "not_rooms": ["!123456:example.com"], + // "state": { + // "types": ["m.room.*"], + // "not_rooms": ["!726s6s6q:example.com"], + // }, + // "timeline": { + // "limit": 10, + // "types": ["m.room.message"], + // "not_rooms": ["!726s6s6q:example.com"], + // "not_senders": ["@spam:example.com"] + // "contains_url": true + // }, + // "ephemeral": { + // "types": ["m.receipt", "m.typing"], + // "not_rooms": ["!726s6s6q:example.com"], + // "not_senders": ["@spam:example.com"] + // } + // }, + // "presence": { + // "types": ["m.presence"], + // "not_senders": ["@alice:example.com"] + // }, + // "event_format": "client", + // "event_fields": ["type", "content", "sender"] + // } + + var room_filter_json = definition.room; + + // consider the top level rooms/not_rooms filter + var room_filter_fields = {}; + if (room_filter_json) { + if (room_filter_json.rooms) { + room_filter_fields.rooms = room_filter_json.rooms; + } + if (room_filter_json.rooms) { + room_filter_fields.not_rooms = room_filter_json.not_rooms; + } + + this._include_leave = room_filter_json.include_leave || false; + } + + this._room_filter = new FilterComponent(room_filter_fields); + this._room_timeline_filter = new FilterComponent(room_filter_json ? room_filter_json.timeline || {} : {}); + + // don't bother porting this from synapse yet: + // this._room_state_filter = + // new FilterComponent(room_filter_json.state || {}); + // this._room_ephemeral_filter = + // new FilterComponent(room_filter_json.ephemeral || {}); + // this._room_account_data_filter = + // new FilterComponent(room_filter_json.account_data || {}); + // this._presence_filter = + // new FilterComponent(definition.presence || {}); + // this._account_data_filter = + // new FilterComponent(definition.account_data || {}); +}; + +/** + * Get the room.timeline filter component of the filter + * @return {FilterComponent} room timeline filter component + */ +Filter.prototype.getRoomTimelineFilterComponent = function () { + return this._room_timeline_filter; +}; + +/** + * Filter the list of events based on whether they are allowed in a timeline + * based on this filter + * @param {MatrixEvent[]} events the list of events being filtered + * @return {MatrixEvent[]} the list of events which match the filter + */ +Filter.prototype.filterRoomTimeline = function (events) { + return this._room_timeline_filter.filter(this._room_filter.filter(events)); +}; + +/** + * Set the max number of events to return for each room's timeline. + * @param {Number} limit The max number of events to return for each room. + */ +Filter.prototype.setTimelineLimit = function (limit) { + setProp(this.definition, "room.timeline.limit", limit); +}; + +/** + * Control whether left rooms should be included in responses. + * @param {boolean} includeLeave True to make rooms the user has left appear + * in responses. + */ +Filter.prototype.setIncludeLeaveRooms = function (includeLeave) { + setProp(this.definition, "room.include_leave", includeLeave); +}; + +/** + * Create a filter from existing data. + * @static + * @param {string} userId + * @param {string} filterId + * @param {Object} jsonObj + * @return {Filter} + */ +Filter.fromJson = function (userId, filterId, jsonObj) { + var filter = new Filter(userId, filterId); + filter.setDefinition(jsonObj); + return filter; +}; + +/** The Filter class */ +module.exports = Filter; + +},{"./filter-component":22}],24:[function(require,module,exports){ +(function (global){ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; +/** + * This is an internal module. See {@link MatrixHttpApi} for the public class. + * @module http-api + */ + +var _create = require('babel-runtime/core-js/object/create'); + +var _create2 = _interopRequireDefault(_create); + +var _stringify = require('babel-runtime/core-js/json/stringify'); + +var _stringify2 = _interopRequireDefault(_stringify); + +var _typeof2 = require('babel-runtime/helpers/typeof'); + +var _typeof3 = _interopRequireDefault(_typeof2); + +var _bluebird = require('bluebird'); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var parseContentType = require('content-type').parse; + +var utils = require("./utils"); + +// we use our own implementation of setTimeout, so that if we get suspended in +// the middle of a /sync, we cancel the sync as soon as we awake, rather than +// waiting for the delay to elapse. +var callbacks = require("./realtime-callbacks"); + +/* +TODO: +- CS: complete register function (doing stages) +- Identity server: linkEmail, authEmail, bindEmail, lookup3pid +*/ + +/** + * A constant representing the URI path for release 0 of the Client-Server HTTP API. + */ +module.exports.PREFIX_R0 = "/_matrix/client/r0"; + +/** + * A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs. + */ +module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable"; + +/** + * URI path for the identity API + */ +module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1"; + +/** + * URI path for the media repo API + */ +module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0"; + +/** + * Construct a MatrixHttpApi. + * @constructor + * @param {EventEmitter} event_emitter The event emitter to use for emitting events + * @param {Object} opts The options to use for this HTTP API. + * @param {string} opts.baseUrl Required. The base client-server URL e.g. + * 'http://localhost:8008'. + * @param {Function} opts.request Required. The function to call for HTTP + * requests. This function must look like function(opts, callback){ ... }. + * @param {string} opts.prefix Required. The matrix client prefix to use, e.g. + * '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants. + * + * @param {boolean} opts.onlyData True to return only the 'data' component of the + * response (e.g. the parsed HTTP body). If false, requests will return an + * object with the properties code, headers and data. + * + * @param {string} opts.accessToken The access_token to send with requests. Can be + * null to not send an access token. + * @param {Object=} opts.extraParams Optional. Extra query parameters to send on + * requests. + * @param {Number=} opts.localTimeoutMs The default maximum amount of time to wait + * before timing out the request. If not specified, there is no timeout. + * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use + * Authorization header instead of query param to send the access token to the server. + */ +module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) { + utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]); + opts.onlyData = opts.onlyData || false; + this.event_emitter = event_emitter; + this.opts = opts; + this.useAuthorizationHeader = Boolean(opts.useAuthorizationHeader); + this.uploads = []; +}; + +module.exports.MatrixHttpApi.prototype = { + + /** + * Get the content repository url with query parameters. + * @return {Object} An object with a 'base', 'path' and 'params' for base URL, + * path and query parameters respectively. + */ + getContentUri: function getContentUri() { + var params = { + access_token: this.opts.accessToken + }; + return { + base: this.opts.baseUrl, + path: "/_matrix/media/v1/upload", + params: params + }; + }, + + /** + * Upload content to the Home Server + * + * @param {object} file The object to upload. On a browser, something that + * can be sent to XMLHttpRequest.send (typically a File). Under node.js, + * a Buffer, String or ReadStream. + * + * @param {object} opts options object + * + * @param {string=} opts.name Name to give the file on the server. Defaults + * to file.name. + * + * @param {string=} opts.type Content-type for the upload. Defaults to + * file.type, or applicaton/octet-stream. + * + * @param {boolean=} opts.rawResponse Return the raw body, rather than + * parsing the JSON. Defaults to false (except on node.js, where it + * defaults to true for backwards compatibility). + * + * @param {boolean=} opts.onlyContentUri Just return the content URI, + * rather than the whole body. Defaults to false (except on browsers, + * where it defaults to true for backwards compatibility). Ignored if + * opts.rawResponse is true. + * + * @param {Function=} opts.callback Deprecated. Optional. The callback to + * invoke on success/failure. See the promise return values for more + * information. + * + * @param {Function=} opts.progressHandler Optional. Called when a chunk of + * data has been uploaded, with an object containing the fields `loaded` + * (number of bytes transferred) and `total` (total size, if known). + * + * @return {module:client.Promise} Resolves to response object, as + * determined by this.opts.onlyData, opts.rawResponse, and + * opts.onlyContentUri. Rejects with an error (usually a MatrixError). + */ + uploadContent: function uploadContent(file, opts) { + if (utils.isFunction(opts)) { + // opts used to be callback + opts = { + callback: opts + }; + } else if (opts === undefined) { + opts = {}; + } + + // if the file doesn't have a mime type, use a default since + // the HS errors if we don't supply one. + var contentType = opts.type || file.type || 'application/octet-stream'; + var fileName = opts.name || file.name; + + // we used to recommend setting file.stream to the thing to upload on + // nodejs. + var body = file.stream ? file.stream : file; + + // backwards-compatibility hacks where we used to do different things + // between browser and node. + var rawResponse = opts.rawResponse; + if (rawResponse === undefined) { + if (global.XMLHttpRequest) { + rawResponse = false; + } else { + console.warn("Returning the raw JSON from uploadContent(). Future " + "versions of the js-sdk will change this default, to " + "return the parsed object. Set opts.rawResponse=false " + "to change this behaviour now."); + rawResponse = true; + } + } + + var onlyContentUri = opts.onlyContentUri; + if (!rawResponse && onlyContentUri === undefined) { + if (global.XMLHttpRequest) { + console.warn("Returning only the content-uri from uploadContent(). " + "Future versions of the js-sdk will change this " + "default, to return the whole response object. Set " + "opts.onlyContentUri=false to change this behaviour now."); + onlyContentUri = true; + } else { + onlyContentUri = false; + } + } + + // browser-request doesn't support File objects because it deep-copies + // the options using JSON.parse(JSON.stringify(options)). Instead of + // loading the whole file into memory as a string and letting + // browser-request base64 encode and then decode it again, we just + // use XMLHttpRequest directly. + // (browser-request doesn't support progress either, which is also kind + // of important here) + + var upload = { loaded: 0, total: 0 }; + var promise = void 0; + + // XMLHttpRequest doesn't parse JSON for us. request normally does, but + // we're setting opts.json=false so that it doesn't JSON-encode the + // request, which also means it doesn't JSON-decode the response. Either + // way, we have to JSON-parse the response ourselves. + var bodyParser = null; + if (!rawResponse) { + bodyParser = function bodyParser(rawBody) { + var body = JSON.parse(rawBody); + if (onlyContentUri) { + body = body.content_uri; + if (body === undefined) { + throw Error('Bad response'); + } + } + return body; + }; + } + + if (global.XMLHttpRequest) { + var defer = _bluebird2.default.defer(); + var xhr = new global.XMLHttpRequest(); + upload.xhr = xhr; + var cb = requestCallback(defer, opts.callback, this.opts.onlyData); + + var timeout_fn = function timeout_fn() { + xhr.abort(); + cb(new Error('Timeout')); + }; + + // set an initial timeout of 30s; we'll advance it each time we get + // a progress notification + xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000); + + xhr.onreadystatechange = function () { + switch (xhr.readyState) { + case global.XMLHttpRequest.DONE: + callbacks.clearTimeout(xhr.timeout_timer); + var resp; + try { + if (!xhr.responseText) { + throw new Error('No response body.'); + } + resp = xhr.responseText; + if (bodyParser) { + resp = bodyParser(resp); + } + } catch (err) { + err.http_status = xhr.status; + cb(err); + return; + } + cb(undefined, xhr, resp); + break; + } + }; + xhr.upload.addEventListener("progress", function (ev) { + callbacks.clearTimeout(xhr.timeout_timer); + upload.loaded = ev.loaded; + upload.total = ev.total; + xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000); + if (opts.progressHandler) { + opts.progressHandler({ + loaded: ev.loaded, + total: ev.total + }); + } + }); + var url = this.opts.baseUrl + "/_matrix/media/v1/upload"; + url += "?access_token=" + encodeURIComponent(this.opts.accessToken); + url += "&filename=" + encodeURIComponent(fileName); + + xhr.open("POST", url); + xhr.setRequestHeader("Content-Type", contentType); + xhr.send(body); + promise = defer.promise; + + // dirty hack (as per _request) to allow the upload to be cancelled. + promise.abort = xhr.abort.bind(xhr); + } else { + var queryParams = { + filename: fileName + }; + + promise = this.authedRequest(opts.callback, "POST", "/upload", queryParams, body, { + prefix: "/_matrix/media/v1", + headers: { "Content-Type": contentType }, + json: false, + bodyParser: bodyParser + }); + } + + var self = this; + + // remove the upload from the list on completion + var promise0 = promise.finally(function () { + for (var i = 0; i < self.uploads.length; ++i) { + if (self.uploads[i] === upload) { + self.uploads.splice(i, 1); + return; + } + } + }); + + // copy our dirty abort() method to the new promise + promise0.abort = promise.abort; + + upload.promise = promise0; + this.uploads.push(upload); + + return promise0; + }, + + cancelUpload: function cancelUpload(promise) { + if (promise.abort) { + promise.abort(); + return true; + } + return false; + }, + + getCurrentUploads: function getCurrentUploads() { + return this.uploads; + }, + + idServerRequest: function idServerRequest(callback, method, path, params, prefix) { + var fullUri = this.opts.idBaseUrl + prefix + path; + + if (callback !== undefined && !utils.isFunction(callback)) { + throw Error("Expected callback to be a function but got " + (typeof callback === 'undefined' ? 'undefined' : (0, _typeof3.default)(callback))); + } + + var opts = { + uri: fullUri, + method: method, + withCredentials: false, + json: false, + _matrix_opts: this.opts + }; + if (method == 'GET') { + opts.qs = params; + } else { + opts.form = params; + } + + var defer = _bluebird2.default.defer(); + this.opts.request(opts, requestCallback(defer, callback, this.opts.onlyData)); + // ID server does not always take JSON, so we can't use requests' 'json' + // option as we do with the home server, but it does return JSON, so + // parse it manually + return defer.promise.then(function (response) { + return JSON.parse(response); + }); + }, + + /** + * Perform an authorised request to the homeserver. + * @param {Function} callback Optional. The callback to invoke on + * success/failure. See the promise return values for more information. + * @param {string} method The HTTP method e.g. "GET". + * @param {string} path The HTTP path after the supplied prefix e.g. + * "/createRoom". + * + * @param {Object=} queryParams A dict of query params (these will NOT be + * urlencoded). If unspecified, there will be no query params. + * + * @param {Object} data The HTTP JSON body. + * + * @param {Object|Number=} opts additional options. If a number is specified, + * this is treated as `opts.localTimeoutMs`. + * + * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before + * timing out the request. If not specified, there is no timeout. + * + * @param {sting=} opts.prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. + * + * @param {Object=} opts.headers map of additional request headers + * + * @return {module:client.Promise} Resolves to {data: {Object}, + * headers: {Object}, code: {Number}}. + * If onlyData is set, this will resolve to the data + * object only. + * @return {module:http-api.MatrixError} Rejects with an error if a problem + * occurred. This includes network problems and Matrix-specific error JSON. + */ + authedRequest: function authedRequest(callback, method, path, queryParams, data, opts) { + if (!queryParams) { + queryParams = {}; + } + if (this.useAuthorizationHeader) { + if (isFinite(opts)) { + // opts used to be localTimeoutMs + opts = { + localTimeoutMs: opts + }; + } + if (!opts) { + opts = {}; + } + if (!opts.headers) { + opts.headers = {}; + } + if (!opts.headers.Authorization) { + opts.headers.Authorization = "Bearer " + this.opts.accessToken; + } + if (queryParams.access_token) { + delete queryParams.access_token; + } + } else { + if (!queryParams.access_token) { + queryParams.access_token = this.opts.accessToken; + } + } + + var requestPromise = this.request(callback, method, path, queryParams, data, opts); + + var self = this; + requestPromise.catch(function (err) { + if (err.errcode == 'M_UNKNOWN_TOKEN') { + self.event_emitter.emit("Session.logged_out"); + } else if (err.errcode == 'M_CONSENT_NOT_GIVEN') { + self.event_emitter.emit("no_consent", err.message, err.data.consent_uri); + } + }); + + // return the original promise, otherwise tests break due to it having to + // go around the event loop one more time to process the result of the request + return requestPromise; + }, + + /** + * Perform a request to the homeserver without any credentials. + * @param {Function} callback Optional. The callback to invoke on + * success/failure. See the promise return values for more information. + * @param {string} method The HTTP method e.g. "GET". + * @param {string} path The HTTP path after the supplied prefix e.g. + * "/createRoom". + * + * @param {Object=} queryParams A dict of query params (these will NOT be + * urlencoded). If unspecified, there will be no query params. + * + * @param {Object} data The HTTP JSON body. + * + * @param {Object=} opts additional options + * + * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before + * timing out the request. If not specified, there is no timeout. + * + * @param {sting=} opts.prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. + * + * @param {Object=} opts.headers map of additional request headers + * + * @return {module:client.Promise} Resolves to {data: {Object}, + * headers: {Object}, code: {Number}}. + * If onlyData is set, this will resolve to the data + * object only. + * @return {module:http-api.MatrixError} Rejects with an error if a problem + * occurred. This includes network problems and Matrix-specific error JSON. + */ + request: function request(callback, method, path, queryParams, data, opts) { + opts = opts || {}; + var prefix = opts.prefix !== undefined ? opts.prefix : this.opts.prefix; + var fullUri = this.opts.baseUrl + prefix + path; + + return this.requestOtherUrl(callback, method, fullUri, queryParams, data, opts); + }, + + /** + * Perform an authorised request to the homeserver with a specific path + * prefix which overrides the default for this call only. Useful for hitting + * different Matrix Client-Server versions. + * @param {Function} callback Optional. The callback to invoke on + * success/failure. See the promise return values for more information. + * @param {string} method The HTTP method e.g. "GET". + * @param {string} path The HTTP path after the supplied prefix e.g. + * "/createRoom". + * @param {Object} queryParams A dict of query params (these will NOT be + * urlencoded). + * @param {Object} data The HTTP JSON body. + * @param {string} prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". + * @param {Number=} localTimeoutMs The maximum amount of time to wait before + * timing out the request. If not specified, there is no timeout. + * @return {module:client.Promise} Resolves to {data: {Object}, + * headers: {Object}, code: {Number}}. + * If onlyData is set, this will resolve to the data + * object only. + * @return {module:http-api.MatrixError} Rejects with an error if a problem + * occurred. This includes network problems and Matrix-specific error JSON. + * + * @deprecated prefer authedRequest with opts.prefix + */ + authedRequestWithPrefix: function authedRequestWithPrefix(callback, method, path, queryParams, data, prefix, localTimeoutMs) { + return this.authedRequest(callback, method, path, queryParams, data, { + localTimeoutMs: localTimeoutMs, + prefix: prefix + }); + }, + + /** + * Perform a request to the homeserver without any credentials but with a + * specific path prefix which overrides the default for this call only. + * Useful for hitting different Matrix Client-Server versions. + * @param {Function} callback Optional. The callback to invoke on + * success/failure. See the promise return values for more information. + * @param {string} method The HTTP method e.g. "GET". + * @param {string} path The HTTP path after the supplied prefix e.g. + * "/createRoom". + * @param {Object} queryParams A dict of query params (these will NOT be + * urlencoded). + * @param {Object} data The HTTP JSON body. + * @param {string} prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". + * @param {Number=} localTimeoutMs The maximum amount of time to wait before + * timing out the request. If not specified, there is no timeout. + * @return {module:client.Promise} Resolves to {data: {Object}, + * headers: {Object}, code: {Number}}. + * If onlyData is set, this will resolve to the data + * object only. + * @return {module:http-api.MatrixError} Rejects with an error if a problem + * occurred. This includes network problems and Matrix-specific error JSON. + * + * @deprecated prefer request with opts.prefix + */ + requestWithPrefix: function requestWithPrefix(callback, method, path, queryParams, data, prefix, localTimeoutMs) { + return this.request(callback, method, path, queryParams, data, { + localTimeoutMs: localTimeoutMs, + prefix: prefix + }); + }, + + /** + * Perform a request to an arbitrary URL. + * @param {Function} callback Optional. The callback to invoke on + * success/failure. See the promise return values for more information. + * @param {string} method The HTTP method e.g. "GET". + * @param {string} uri The HTTP URI + * + * @param {Object=} queryParams A dict of query params (these will NOT be + * urlencoded). If unspecified, there will be no query params. + * + * @param {Object} data The HTTP JSON body. + * + * @param {Object=} opts additional options + * + * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before + * timing out the request. If not specified, there is no timeout. + * + * @param {sting=} opts.prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. + * + * @param {Object=} opts.headers map of additional request headers + * + * @return {module:client.Promise} Resolves to {data: {Object}, + * headers: {Object}, code: {Number}}. + * If onlyData is set, this will resolve to the data + * object only. + * @return {module:http-api.MatrixError} Rejects with an error if a problem + * occurred. This includes network problems and Matrix-specific error JSON. + */ + requestOtherUrl: function requestOtherUrl(callback, method, uri, queryParams, data, opts) { + if (opts === undefined || opts === null) { + opts = {}; + } else if (isFinite(opts)) { + // opts used to be localTimeoutMs + opts = { + localTimeoutMs: opts + }; + } + + return this._request(callback, method, uri, queryParams, data, opts); + }, + + /** + * Form and return a homeserver request URL based on the given path + * params and prefix. + * @param {string} path The HTTP path after the supplied prefix e.g. + * "/createRoom". + * @param {Object} queryParams A dict of query params (these will NOT be + * urlencoded). + * @param {string} prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". + * @return {string} URL + */ + getUrl: function getUrl(path, queryParams, prefix) { + var queryString = ""; + if (queryParams) { + queryString = "?" + utils.encodeParams(queryParams); + } + return this.opts.baseUrl + prefix + path + queryString; + }, + + /** + * @private + * + * @param {function} callback + * @param {string} method + * @param {string} uri + * @param {object} queryParams + * @param {object|string} data + * @param {object=} opts + * + * @param {boolean} [opts.json =true] Json-encode data before sending, and + * decode response on receipt. (We will still json-decode error + * responses, even if this is false.) + * + * @param {object=} opts.headers extra request headers + * + * @param {number=} opts.localTimeoutMs client-side timeout for the + * request. Default timeout if falsy. + * + * @param {function=} opts.bodyParser function to parse the body of the + * response before passing it to the promise and callback. + * + * @return {module:client.Promise} a promise which resolves to either the + * response object (if this.opts.onlyData is truthy), or the parsed + * body. Rejects + */ + _request: function _request(callback, method, uri, queryParams, data, opts) { + if (callback !== undefined && !utils.isFunction(callback)) { + throw Error("Expected callback to be a function but got " + (typeof callback === 'undefined' ? 'undefined' : (0, _typeof3.default)(callback))); + } + opts = opts || {}; + + var self = this; + if (this.opts.extraParams) { + for (var key in this.opts.extraParams) { + if (!this.opts.extraParams.hasOwnProperty(key)) { + continue; + } + queryParams[key] = this.opts.extraParams[key]; + } + } + + var headers = utils.extend({}, opts.headers || {}); + var json = opts.json === undefined ? true : opts.json; + var bodyParser = opts.bodyParser; + + // we handle the json encoding/decoding here, because request and + // browser-request make a mess of it. Specifically, they attempt to + // json-decode plain-text error responses, which in turn means that the + // actual error gets swallowed by a SyntaxError. + + if (json) { + if (data) { + data = (0, _stringify2.default)(data); + headers['content-type'] = 'application/json'; + } + + if (!headers['accept']) { + headers['accept'] = 'application/json'; + } + + if (bodyParser === undefined) { + bodyParser = function bodyParser(rawBody) { + return JSON.parse(rawBody); + }; + } + } + + var defer = _bluebird2.default.defer(); + + var timeoutId = void 0; + var timedOut = false; + var req = void 0; + var localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs; + + var resetTimeout = function resetTimeout() { + if (localTimeoutMs) { + if (timeoutId) { + callbacks.clearTimeout(timeoutId); + } + timeoutId = callbacks.setTimeout(function () { + timedOut = true; + if (req && req.abort) { + req.abort(); + } + defer.reject(new module.exports.MatrixError({ + error: "Locally timed out waiting for a response", + errcode: "ORG.MATRIX.JSSDK_TIMEOUT", + timeout: localTimeoutMs + })); + }, localTimeoutMs); + } + }; + resetTimeout(); + + var reqPromise = defer.promise; + + try { + req = this.opts.request({ + uri: uri, + method: method, + withCredentials: false, + qs: queryParams, + body: data, + json: false, + timeout: localTimeoutMs, + headers: opts.headers || {}, + _matrix_opts: this.opts + }, function (err, response, body) { + if (localTimeoutMs) { + callbacks.clearTimeout(timeoutId); + if (timedOut) { + return; // already rejected promise + } + } + + var handlerFn = requestCallback(defer, callback, self.opts.onlyData, bodyParser); + handlerFn(err, response, body); + }); + if (req) { + // This will only work in a browser, where opts.request is the + // `browser-request` import. Currently `request` does not support progress + // updates - see https://github.com/request/request/pull/2346. + // `browser-request` returns an XHRHttpRequest which exposes `onprogress` + if ('onprogress' in req) { + req.onprogress = function (e) { + // Prevent the timeout from rejecting the deferred promise if progress is + // seen with the request + resetTimeout(); + }; + } + + // FIXME: This is EVIL, but I can't think of a better way to expose + // abort() operations on underlying HTTP requests :( + if (req.abort) reqPromise.abort = req.abort.bind(req); + } + } catch (ex) { + defer.reject(ex); + if (callback) { + callback(ex); + } + } + return reqPromise; + } +}; + +/* + * Returns a callback that can be invoked by an HTTP request on completion, + * that will either resolve or reject the given defer as well as invoke the + * given userDefinedCallback (if any). + * + * HTTP errors are transformed into javascript errors and the deferred is rejected. + * + * If bodyParser is given, it is used to transform the body of the successful + * responses before passing to the defer/callback. + * + * If onlyData is true, the defer/callback is invoked with the body of the + * response, otherwise the result object (with `code` and `data` fields) + * + */ +var requestCallback = function requestCallback(defer, userDefinedCallback, onlyData, bodyParser) { + userDefinedCallback = userDefinedCallback || function () {}; + + return function (err, response, body) { + if (!err) { + try { + if (response.statusCode >= 400) { + err = parseErrorResponse(response, body); + } else if (bodyParser) { + body = bodyParser(body); + } + } catch (e) { + err = new Error('Error parsing server response: ' + e); + } + } + + if (err) { + defer.reject(err); + userDefinedCallback(err); + } else { + var res = { + code: response.statusCode, + + // XXX: why do we bother with this? it doesn't work for + // XMLHttpRequest, so clearly we don't use it. + headers: response.headers, + data: body + }; + defer.resolve(onlyData ? body : res); + userDefinedCallback(null, onlyData ? body : res); + } + }; +}; + +/** + * Attempt to turn an HTTP error response into a Javascript Error. + * + * If it is a JSON response, we will parse it into a MatrixError. Otherwise + * we return a generic Error. + * + * @param {XMLHttpRequest|http.IncomingMessage} response response object + * @param {String} body raw body of the response + * @returns {Error} + */ +function parseErrorResponse(response, body) { + var httpStatus = response.statusCode; + var contentType = getResponseContentType(response); + + var err = void 0; + if (contentType) { + if (contentType.type === 'application/json') { + err = new module.exports.MatrixError(JSON.parse(body)); + } else if (contentType.type === 'text/plain') { + err = new Error('Server returned ' + httpStatus + ' error: ' + body); + } + } + + if (!err) { + err = new Error('Server returned ' + httpStatus + ' error'); + } + err.httpStatus = httpStatus; + return err; +} + +/** + * extract the Content-Type header from the response object, and + * parse it to a `{type, parameters}` object. + * + * returns null if no content-type header could be found. + * + * @param {XMLHttpRequest|http.IncomingMessage} response response object + * @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found + */ +function getResponseContentType(response) { + var contentType = void 0; + if (response.getResponseHeader) { + // XMLHttpRequest provides getResponseHeader + contentType = response.getResponseHeader("Content-Type"); + } else if (response.headers) { + // request provides http.IncomingMessage which has a message.headers map + contentType = response.headers['content-type'] || null; + } + + if (!contentType) { + return null; + } + + try { + return parseContentType(contentType); + } catch (e) { + throw new Error('Error parsing Content-Type \'' + contentType + '\': ' + e); + } +} + +/** + * Construct a Matrix error. This is a JavaScript Error with additional + * information specific to the standard Matrix error response. + * @constructor + * @param {Object} errorJson The Matrix error JSON returned from the homeserver. + * @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN". + * @prop {string} name Same as MatrixError.errcode but with a default unknown string. + * @prop {string} message The Matrix 'error' value, e.g. "Missing token." + * @prop {Object} data The raw Matrix error JSON used to construct this object. + * @prop {integer} httpStatus The numeric HTTP status code given + */ +module.exports.MatrixError = function MatrixError(errorJson) { + errorJson = errorJson || {}; + this.errcode = errorJson.errcode; + this.name = errorJson.errcode || "Unknown error code"; + this.message = errorJson.error || "Unknown message"; + this.data = errorJson; +}; +module.exports.MatrixError.prototype = (0, _create2.default)(Error.prototype); +/** */ +module.exports.MatrixError.prototype.constructor = module.exports.MatrixError; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"./realtime-callbacks":39,"./utils":50,"babel-runtime/core-js/json/stringify":55,"babel-runtime/core-js/object/create":57,"babel-runtime/helpers/typeof":72,"bluebird":74,"content-type":76}],25:[function(require,module,exports){ +/* +Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** @module interactive-auth */ + +var _getIterator2 = require("babel-runtime/core-js/get-iterator"); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +var _stringify = require("babel-runtime/core-js/json/stringify"); + +var _stringify2 = _interopRequireDefault(_stringify); + +var _bluebird = require("bluebird"); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var url = require("url"); + +var utils = require("./utils"); + +var EMAIL_STAGE_TYPE = "m.login.email.identity"; +var MSISDN_STAGE_TYPE = "m.login.msisdn"; + +/** + * Abstracts the logic used to drive the interactive auth process. + * + *

Components implementing an interactive auth flow should instantiate one of + * these, passing in the necessary callbacks to the constructor. They should + * then call attemptAuth, which will return a promise which will resolve or + * reject when the interactive-auth process completes. + * + *

Meanwhile, calls will be made to the startAuthStage and doRequest + * callbacks, and information gathered from the user can be submitted with + * submitAuthDict. + * + * @constructor + * @alias module:interactive-auth + * + * @param {object} opts options object + * + * @param {object} opts.matrixClient A matrix client to use for the auth process + * + * @param {object?} opts.authData error response from the last request. If + * null, a request will be made with no auth before starting. + * + * @param {function(object?, bool?): module:client.Promise} opts.doRequest + * called with the new auth dict to submit the request and a flag set + * to true if this request is a background request. Should return a + * promise which resolves to the successful response or rejects with a + * MatrixError. + * + * @param {function(string, object?)} opts.stateUpdated + * called when the status of the UI auth changes, ie. when the state of + * an auth stage changes of when the auth flow moves to a new stage. + * The arguments are: the login type (eg m.login.password); and an object + * which is either an error or an informational object specific to the + * login type. If the 'errcode' key is defined, the object is an error, + * and has keys: + * errcode: string, the textual error code, eg. M_UNKNOWN + * error: string, human readable string describing the error + * + * The login type specific objects are as follows: + * m.login.email.identity: + * * emailSid: string, the sid of the active email auth session + * + * @param {object?} opts.inputs Inputs provided by the user and used by different + * stages of the auto process. The inputs provided will affect what flow is chosen. + * + * @param {string?} opts.inputs.emailAddress An email address. If supplied, a flow + * using email verification will be chosen. + * + * @param {string?} opts.inputs.phoneCountry An ISO two letter country code. Gives + * the country that opts.phoneNumber should be resolved relative to. + * + * @param {string?} opts.inputs.phoneNumber A phone number. If supplied, a flow + * using phone number validation will be chosen. + * + * @param {string?} opts.sessionId If resuming an existing interactive auth session, + * the sessionId of that session. + * + * @param {string?} opts.clientSecret If resuming an existing interactive auth session, + * the client secret for that session + * + * @param {string?} opts.emailSid If returning from having completed m.login.email.identity + * auth, the sid for the email verification session. + * + */ +function InteractiveAuth(opts) { + this._matrixClient = opts.matrixClient; + this._data = opts.authData || {}; + this._requestCallback = opts.doRequest; + // startAuthStage included for backwards compat + this._stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage; + this._completionDeferred = null; + this._inputs = opts.inputs || {}; + + if (opts.sessionId) this._data.session = opts.sessionId; + this._clientSecret = opts.clientSecret || this._matrixClient.generateClientSecret(); + this._emailSid = opts.emailSid; + if (this._emailSid === undefined) this._emailSid = null; + + this._currentStage = null; +} + +InteractiveAuth.prototype = { + /** + * begin the authentication process. + * + * @return {module:client.Promise} which resolves to the response on success, + * or rejects with the error on failure. Rejects with NoAuthFlowFoundError if + * no suitable authentication flow can be found + */ + attemptAuth: function attemptAuth() { + var _this = this; + + this._completionDeferred = _bluebird2.default.defer(); + + // wrap in a promise so that if _startNextAuthStage + // throws, it rejects the promise in a consistent way + return _bluebird2.default.resolve().then(function () { + // if we have no flows, try a request (we'll have + // just a session ID in _data if resuming) + if (!_this._data.flows) { + _this._doRequest(_this._data); + } else { + _this._startNextAuthStage(); + } + return _this._completionDeferred.promise; + }); + }, + + /** + * Poll to check if the auth session or current stage has been + * completed out-of-band. If so, the attemptAuth promise will + * be resolved. + */ + poll: function poll() { + if (!this._data.session) return; + + var authDict = {}; + if (this._currentStage == EMAIL_STAGE_TYPE) { + // The email can be validated out-of-band, but we need to provide the + // creds so the HS can go & check it. + if (this._emailSid) { + var idServerParsedUrl = url.parse(this._matrixClient.getIdentityServerUrl()); + authDict = { + type: EMAIL_STAGE_TYPE, + threepid_creds: { + sid: this._emailSid, + client_secret: this._clientSecret, + id_server: idServerParsedUrl.host + } + }; + } + } + + this.submitAuthDict(authDict, true); + }, + + /** + * get the auth session ID + * + * @return {string} session id + */ + getSessionId: function getSessionId() { + return this._data ? this._data.session : undefined; + }, + + /** + * get the client secret used for validation sessions + * with the ID server. + * + * @return {string} client secret + */ + getClientSecret: function getClientSecret() { + return this._clientSecret; + }, + + /** + * get the server params for a given stage + * + * @param {string} loginType login type for the stage + * @return {object?} any parameters from the server for this stage + */ + getStageParams: function getStageParams(loginType) { + var params = {}; + if (this._data && this._data.params) { + params = this._data.params; + } + return params[loginType]; + }, + + /** + * submit a new auth dict and fire off the request. This will either + * make attemptAuth resolve/reject, or cause the startAuthStage callback + * to be called for a new stage. + * + * @param {object} authData new auth dict to send to the server. Should + * include a `type` propterty denoting the login type, as well as any + * other params for that stage. + * @param {bool} background If true, this request failing will not result + * in the attemptAuth promise being rejected. This can be set to true + * for requests that just poll to see if auth has been completed elsewhere. + */ + submitAuthDict: function submitAuthDict(authData, background) { + if (!this._completionDeferred) { + throw new Error("submitAuthDict() called before attemptAuth()"); + } + + // use the sessionid from the last request. + var auth = { + session: this._data.session + }; + utils.extend(auth, authData); + + this._doRequest(auth, background); + }, + + /** + * Gets the sid for the email validation session + * Specific to m.login.email.identity + * + * @returns {string} The sid of the email auth session + */ + getEmailSid: function getEmailSid() { + return this._emailSid; + }, + + /** + * Sets the sid for the email validation session + * This must be set in order to successfully poll for completion + * of the email validation. + * Specific to m.login.email.identity + * + * @param {string} sid The sid for the email validation session + */ + setEmailSid: function setEmailSid(sid) { + this._emailSid = sid; + }, + + /** + * Fire off a request, and either resolve the promise, or call + * startAuthStage. + * + * @private + * @param {object?} auth new auth dict, including session id + * @param {bool?} background If true, this request is a background poll, so it + * failing will not result in the attemptAuth promise being rejected. + * This can be set to true for requests that just poll to see if auth has + * been completed elsewhere. + */ + _doRequest: function _doRequest(auth, background) { + var _this2 = this; + + var self = this; + + // hackery to make sure that synchronous exceptions end up in the catch + // handler (without the additional event loop entailed by q.fcall or an + // extra Promise.resolve().then) + var prom = void 0; + try { + prom = this._requestCallback(auth, background); + } catch (e) { + prom = _bluebird2.default.reject(e); + } + + prom = prom.then(function (result) { + console.log("result from request: ", result); + self._completionDeferred.resolve(result); + }, function (error) { + // sometimes UI auth errors don't come with flows + var errorFlows = error.data ? error.data.flows : null; + var haveFlows = Boolean(self._data.flows) || Boolean(errorFlows); + if (error.httpStatus !== 401 || !error.data || !haveFlows) { + // doesn't look like an interactive-auth failure. fail the whole lot. + throw error; + } + // if the error didn't come with flows, completed flows or session ID, + // copy over the ones we have. Synapse sometimes sends responses without + // any UI auth data (eg. when polling for email validation, if the email + // has not yet been validated). This appears to be a Synapse bug, which + // we workaround here. + if (!error.data.flows && !error.data.completed && !error.data.session) { + error.data.flows = self._data.flows; + error.data.completed = self._data.completed; + error.data.session = self._data.session; + } + self._data = error.data; + self._startNextAuthStage(); + }); + if (!background) { + prom = prom.catch(function (e) { + _this2._completionDeferred.reject(e); + }); + } else { + // We ignore all failures here (even non-UI auth related ones) + // since we don't want to suddenly fail if the internet connection + // had a blip whilst we were polling + prom = prom.catch(function (error) { + console.log("Ignoring error from UI auth: " + error); + }); + } + prom.done(); + }, + + /** + * Pick the next stage and call the callback + * + * @private + * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + */ + _startNextAuthStage: function _startNextAuthStage() { + var nextStage = this._chooseStage(); + if (!nextStage) { + throw new Error("No incomplete flows from the server"); + } + this._currentStage = nextStage; + + if (nextStage == 'm.login.dummy') { + this.submitAuthDict({ + type: 'm.login.dummy' + }); + return; + } + + if (this._data.errcode || this._data.error) { + this._stateUpdatedCallback(nextStage, { + errcode: this._data.errcode || "", + error: this._data.error || "" + }); + return; + } + + var stageStatus = {}; + if (nextStage == EMAIL_STAGE_TYPE) { + stageStatus.emailSid = this._emailSid; + } + this._stateUpdatedCallback(nextStage, stageStatus); + }, + + /** + * Pick the next auth stage + * + * @private + * @return {string?} login type + * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + */ + _chooseStage: function _chooseStage() { + var flow = this._chooseFlow(); + console.log("Active flow => %s", (0, _stringify2.default)(flow)); + var nextStage = this._firstUncompletedStage(flow); + console.log("Next stage: %s", nextStage); + return nextStage; + }, + + /** + * Pick one of the flows from the returned list + * If a flow using all of the inputs is found, it will + * be returned, otherwise, null will be returned. + * + * Only flows using all given inputs are chosen because it + * is likley to be surprising if the user provides a + * credential and it is not used. For example, for registration, + * this could result in the email not being used which would leave + * the account with no means to reset a password. + * + * @private + * @return {object} flow + * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + */ + _chooseFlow: function _chooseFlow() { + var flows = this._data.flows || []; + + // we've been given an email or we've already done an email part + var haveEmail = Boolean(this._inputs.emailAddress) || Boolean(this._emailSid); + var haveMsisdn = Boolean(this._inputs.phoneCountry) && Boolean(this._inputs.phoneNumber); + + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)(flows), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var flow = _step.value; + + var flowHasEmail = false; + var flowHasMsisdn = false; + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = (0, _getIterator3.default)(flow.stages), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var stage = _step2.value; + + if (stage === EMAIL_STAGE_TYPE) { + flowHasEmail = true; + } else if (stage == MSISDN_STAGE_TYPE) { + flowHasMsisdn = true; + } + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + if (flowHasEmail == haveEmail && flowHasMsisdn == haveMsisdn) { + return flow; + } + } + // Throw an error with a fairly generic description, but with more + // information such that the app can give a better one if so desired. + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + var err = new Error("No appropriate authentication flow found"); + err.name = 'NoAuthFlowFoundError'; + err.required_stages = []; + if (haveEmail) err.required_stages.push(EMAIL_STAGE_TYPE); + if (haveMsisdn) err.required_stages.push(MSISDN_STAGE_TYPE); + err.available_flows = flows; + throw err; + }, + + /** + * Get the first uncompleted stage in the given flow + * + * @private + * @param {object} flow + * @return {string} login type + */ + _firstUncompletedStage: function _firstUncompletedStage(flow) { + var completed = (this._data || {}).completed || []; + for (var i = 0; i < flow.stages.length; ++i) { + var stageType = flow.stages[i]; + if (completed.indexOf(stageType) === -1) { + return stageType; + } + } + } +}; + +/** */ +module.exports = InteractiveAuth; + +},{"./utils":50,"babel-runtime/core-js/get-iterator":53,"babel-runtime/core-js/json/stringify":55,"bluebird":74,"url":195}],26:[function(require,module,exports){ +(function (global){ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** The {@link module:ContentHelpers} object */ + +module.exports.ContentHelpers = require("./content-helpers"); +/** The {@link module:models/event.MatrixEvent|MatrixEvent} class. */ +module.exports.MatrixEvent = require("./models/event").MatrixEvent; +/** The {@link module:models/event.EventStatus|EventStatus} enum. */ +module.exports.EventStatus = require("./models/event").EventStatus; +/** The {@link module:store/memory.MatrixInMemoryStore|MatrixInMemoryStore} class. */ +module.exports.MatrixInMemoryStore = require("./store/memory").MatrixInMemoryStore; +/** The {@link module:store/indexeddb.IndexedDBStore|IndexedDBStore} class. */ +module.exports.IndexedDBStore = require("./store/indexeddb").IndexedDBStore; +/** The {@link module:store/indexeddb.IndexedDBStoreBackend|IndexedDBStoreBackend} class. */ +module.exports.IndexedDBStoreBackend = require("./store/indexeddb").IndexedDBStoreBackend; +/** The {@link module:sync-accumulator.SyncAccumulator|SyncAccumulator} class. */ +module.exports.SyncAccumulator = require("./sync-accumulator"); +/** The {@link module:http-api.MatrixHttpApi|MatrixHttpApi} class. */ +module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi; +/** The {@link module:http-api.MatrixError|MatrixError} class. */ +module.exports.MatrixError = require("./http-api").MatrixError; +/** The {@link module:client.MatrixClient|MatrixClient} class. */ +module.exports.MatrixClient = require("./client").MatrixClient; +/** The {@link module:models/room|Room} class. */ +module.exports.Room = require("./models/room"); +/** The {@link module:models/event-timeline~EventTimeline} class. */ +module.exports.EventTimeline = require("./models/event-timeline"); +/** The {@link module:models/event-timeline-set~EventTimelineSet} class. */ +module.exports.EventTimelineSet = require("./models/event-timeline-set"); +/** The {@link module:models/room-member|RoomMember} class. */ +module.exports.RoomMember = require("./models/room-member"); +/** The {@link module:models/room-state~RoomState|RoomState} class. */ +module.exports.RoomState = require("./models/room-state"); +/** The {@link module:models/user~User|User} class. */ +module.exports.User = require("./models/user"); +/** The {@link module:scheduler~MatrixScheduler|MatrixScheduler} class. */ +module.exports.MatrixScheduler = require("./scheduler"); +/** The {@link module:store/session/webstorage~WebStorageSessionStore| + * WebStorageSessionStore} class. Work in progress; unstable. */ +module.exports.WebStorageSessionStore = require("./store/session/webstorage"); +/** True if crypto libraries are being used on this client. */ +module.exports.CRYPTO_ENABLED = require("./client").CRYPTO_ENABLED; +/** {@link module:content-repo|ContentRepo} utility functions. */ +module.exports.ContentRepo = require("./content-repo"); +/** The {@link module:filter~Filter|Filter} class. */ +module.exports.Filter = require("./filter"); +/** The {@link module:timeline-window~TimelineWindow} class. */ +module.exports.TimelineWindow = require("./timeline-window").TimelineWindow; +/** The {@link module:interactive-auth} class. */ +module.exports.InteractiveAuth = require("./interactive-auth"); + +module.exports.MemoryCryptoStore = require("./crypto/store/memory-crypto-store").default; +module.exports.IndexedDBCryptoStore = require("./crypto/store/indexeddb-crypto-store").default; + +/** + * Create a new Matrix Call. + * @function + * @param {module:client.MatrixClient} client The MatrixClient instance to use. + * @param {string} roomId The room the call is in. + * @return {module:webrtc/call~MatrixCall} The Matrix call or null if the browser + * does not support WebRTC. + */ +module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCall; + +/** + * Set an audio input device to use for MatrixCalls + * @function + * @param {string=} deviceId the identifier for the device + * undefined treated as unset + */ +module.exports.setMatrixCallAudioInput = require('./webrtc/call').setAudioInput; +/** + * Set a video input device to use for MatrixCalls + * @function + * @param {string=} deviceId the identifier for the device + * undefined treated as unset + */ +module.exports.setMatrixCallVideoInput = require('./webrtc/call').setVideoInput; + +// expose the underlying request object so different environments can use +// different request libs (e.g. request or browser-request) +var request = void 0; +/** + * The function used to perform HTTP requests. Only use this if you want to + * use a different HTTP library, e.g. Angular's $http. This should + * be set prior to calling {@link createClient}. + * @param {requestFunction} r The request function to use. + */ +module.exports.request = function (r) { + request = r; +}; + +/** + * Return the currently-set request function. + * @return {requestFunction} The current request function. + */ +module.exports.getRequest = function () { + return request; +}; + +/** + * Apply wrapping code around the request function. The wrapper function is + * installed as the new request handler, and when invoked it is passed the + * previous value, along with the options and callback arguments. + * @param {requestWrapperFunction} wrapper The wrapping function. + */ +module.exports.wrapRequest = function (wrapper) { + var origRequest = request; + request = function request(options, callback) { + return wrapper(origRequest, options, callback); + }; +}; + +var cryptoStoreFactory = function cryptoStoreFactory() { + return new module.exports.MemoryCryptoStore(); +}; + +/** + * Configure a different factory to be used for creating crypto stores + * + * @param {Function} fac a function which will return a new + * {@link module:crypto.store.base~CryptoStore}. + */ +module.exports.setCryptoStoreFactory = function (fac) { + cryptoStoreFactory = fac; +}; + +/** + * Construct a Matrix Client. Similar to {@link module:client~MatrixClient} + * except that the 'request', 'store' and 'scheduler' dependencies are satisfied. + * @param {(Object|string)} opts The configuration options for this client. If + * this is a string, it is assumed to be the base URL. These configuration + * options will be passed directly to {@link module:client~MatrixClient}. + * @param {Object} opts.store If not set, defaults to + * {@link module:store/memory.MatrixInMemoryStore}. + * @param {Object} opts.scheduler If not set, defaults to + * {@link module:scheduler~MatrixScheduler}. + * @param {requestFunction} opts.request If not set, defaults to the function + * supplied to {@link request} which defaults to the request module from NPM. + * + * @param {module:crypto.store.base~CryptoStore=} opts.cryptoStore + * crypto store implementation. Calls the factory supplied to + * {@link setCryptoStoreFactory} if unspecified; or if no factory has been + * specified, uses a default implementation (indexeddb in the browser, + * in-memory otherwise). + * + * @return {MatrixClient} A new matrix client. + * @see {@link module:client~MatrixClient} for the full list of options for + * opts. + */ +module.exports.createClient = function (opts) { + if (typeof opts === "string") { + opts = { + "baseUrl": opts + }; + } + opts.request = opts.request || request; + opts.store = opts.store || new module.exports.MatrixInMemoryStore({ + localStorage: global.localStorage + }); + opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler(); + opts.cryptoStore = opts.cryptoStore || cryptoStoreFactory(); + return new module.exports.MatrixClient(opts); +}; + +/** + * The request function interface for performing HTTP requests. This matches the + * API for the {@link https://github.com/request/request#requestoptions-callback| + * request NPM module}. The SDK will attempt to call this function in order to + * perform an HTTP request. + * @callback requestFunction + * @param {Object} opts The options for this HTTP request. + * @param {string} opts.uri The complete URI. + * @param {string} opts.method The HTTP method. + * @param {Object} opts.qs The query parameters to append to the URI. + * @param {Object} opts.body The JSON-serializable object. + * @param {boolean} opts.json True if this is a JSON request. + * @param {Object} opts._matrix_opts The underlying options set for + * {@link MatrixHttpApi}. + * @param {requestCallback} callback The request callback. + */ + +/** + * A wrapper for the request function interface. + * @callback requestWrapperFunction + * @param {requestFunction} origRequest The underlying request function being + * wrapped + * @param {Object} opts The options for this HTTP request, given in the same + * form as {@link requestFunction}. + * @param {requestCallback} callback The request callback. + */ + +/** + * The request callback interface for performing HTTP requests. This matches the + * API for the {@link https://github.com/request/request#requestoptions-callback| + * request NPM module}. The SDK will implement a callback which meets this + * interface in order to handle the HTTP response. + * @callback requestCallback + * @param {Error} err The error if one occurred, else falsey. + * @param {Object} response The HTTP response which consists of + * {statusCode: {Number}, headers: {Object}} + * @param {Object} body The parsed HTTP response body. + */ + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"./client":4,"./content-helpers":5,"./content-repo":6,"./crypto/store/indexeddb-crypto-store":19,"./crypto/store/memory-crypto-store":21,"./filter":23,"./http-api":24,"./interactive-auth":25,"./models/event":30,"./models/event-timeline":29,"./models/event-timeline-set":28,"./models/room":35,"./models/room-member":32,"./models/room-state":33,"./models/user":37,"./scheduler":40,"./store/indexeddb":43,"./store/memory":44,"./store/session/webstorage":45,"./sync-accumulator":47,"./timeline-window":49,"./webrtc/call":51}],27:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** + * @module models/event-context + */ + +/** + * Construct a new EventContext + * + * An eventcontext is used for circumstances such as search results, when we + * have a particular event of interest, and a bunch of events before and after + * it. + * + * It also stores pagination tokens for going backwards and forwards in the + * timeline. + * + * @param {MatrixEvent} ourEvent the event at the centre of this context + * + * @constructor + */ + +function EventContext(ourEvent) { + this._timeline = [ourEvent]; + this._ourEventIndex = 0; + this._paginateTokens = { b: null, f: null }; + + // this is used by MatrixClient to keep track of active requests + this._paginateRequests = { b: null, f: null }; +} + +/** + * Get the main event of interest + * + * This is a convenience function for getTimeline()[getOurEventIndex()]. + * + * @return {MatrixEvent} The event at the centre of this context. + */ +EventContext.prototype.getEvent = function () { + return this._timeline[this._ourEventIndex]; +}; + +/** + * Get the list of events in this context + * + * @return {Array} An array of MatrixEvents + */ +EventContext.prototype.getTimeline = function () { + return this._timeline; +}; + +/** + * Get the index in the timeline of our event + * + * @return {Number} + */ +EventContext.prototype.getOurEventIndex = function () { + return this._ourEventIndex; +}; + +/** + * Get a pagination token. + * + * @param {boolean} backwards true to get the pagination token for going + * backwards in time + * @return {string} + */ +EventContext.prototype.getPaginateToken = function (backwards) { + return this._paginateTokens[backwards ? 'b' : 'f']; +}; + +/** + * Set a pagination token. + * + * Generally this will be used only by the matrix js sdk. + * + * @param {string} token pagination token + * @param {boolean} backwards true to set the pagination token for going + * backwards in time + */ +EventContext.prototype.setPaginateToken = function (token, backwards) { + this._paginateTokens[backwards ? 'b' : 'f'] = token; +}; + +/** + * Add more events to the timeline + * + * @param {Array} events new events, in timeline order + * @param {boolean} atStart true to insert new events at the start + */ +EventContext.prototype.addEvents = function (events, atStart) { + // TODO: should we share logic with Room.addEventsToTimeline? + // Should Room even use EventContext? + + if (atStart) { + this._timeline = events.concat(this._timeline); + this._ourEventIndex += events.length; + } else { + this._timeline = this._timeline.concat(events); + } +}; + +/** + * The EventContext class + */ +module.exports = EventContext; + +},{}],28:[function(require,module,exports){ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; +/** + * @module models/event-timeline-set + */ + +var EventEmitter = require("events").EventEmitter; +var utils = require("../utils"); +var EventTimeline = require("./event-timeline"); + +// var DEBUG = false; +var DEBUG = true; + +var debuglog = void 0; +if (DEBUG) { + // using bind means that we get to keep useful line numbers in the console + debuglog = console.log.bind(console); +} else { + debuglog = function debuglog() {}; +} + +/** + * Construct a set of EventTimeline objects, typically on behalf of a given + * room. A room may have multiple EventTimelineSets for different levels + * of filtering. The global notification list is also an EventTimelineSet, but + * lacks a room. + * + *

This is an ordered sequence of timelines, which may or may not + * be continuous. Each timeline lists a series of events, as well as tracking + * the room state at the start and the end of the timeline (if appropriate). + * It also tracks forward and backward pagination tokens, as well as containing + * links to the next timeline in the sequence. + * + *

There is one special timeline - the 'live' timeline, which represents the + * timeline to which events are being added in real-time as they are received + * from the /sync API. Note that you should not retain references to this + * timeline - even if it is the current timeline right now, it may not remain + * so if the server gives us a timeline gap in /sync. + * + *

In order that we can find events from their ids later, we also maintain a + * map from event_id to timeline and index. + * + * @constructor + * @param {?Room} room the optional room for this timelineSet + * @param {Object} opts hash of options inherited from Room. + * opts.timelineSupport gives whether timeline support is enabled + * opts.filter is the filter object, if any, for this timelineSet. + */ +function EventTimelineSet(room, opts) { + this.room = room; + + this._timelineSupport = Boolean(opts.timelineSupport); + this._liveTimeline = new EventTimeline(this); + + // just a list - *not* ordered. + this._timelines = [this._liveTimeline]; + this._eventIdToTimeline = {}; + + this._filter = opts.filter || null; +} +utils.inherits(EventTimelineSet, EventEmitter); + +/** + * Get the filter object this timeline set is filtered on, if any + * @return {?Filter} the optional filter for this timelineSet + */ +EventTimelineSet.prototype.getFilter = function () { + return this._filter; +}; + +/** + * Set the filter object this timeline set is filtered on + * (passed to the server when paginating via /messages). + * @param {Filter} filter the filter for this timelineSet + */ +EventTimelineSet.prototype.setFilter = function (filter) { + this._filter = filter; +}; + +/** + * Get the list of pending sent events for this timelineSet's room, filtered + * by the timelineSet's filter if appropriate. + * + * @return {module:models/event.MatrixEvent[]} A list of the sent events + * waiting for remote echo. + * + * @throws If opts.pendingEventOrdering was not 'detached' + */ +EventTimelineSet.prototype.getPendingEvents = function () { + if (!this.room) { + return []; + } + + if (this._filter) { + return this._filter.filterRoomTimeline(this.room.getPendingEvents()); + } else { + return this.room.getPendingEvents(); + } +}; + +/** + * Get the live timeline for this room. + * + * @return {module:models/event-timeline~EventTimeline} live timeline + */ +EventTimelineSet.prototype.getLiveTimeline = function () { + return this._liveTimeline; +}; + +/** + * Return the timeline (if any) this event is in. + * @param {String} eventId the eventId being sought + * @return {module:models/event-timeline~EventTimeline} timeline + */ +EventTimelineSet.prototype.eventIdToTimeline = function (eventId) { + return this._eventIdToTimeline[eventId]; +}; + +/** + * Track a new event as if it were in the same timeline as an old event, + * replacing it. + * @param {String} oldEventId event ID of the original event + * @param {String} newEventId event ID of the replacement event + */ +EventTimelineSet.prototype.replaceEventId = function (oldEventId, newEventId) { + var existingTimeline = this._eventIdToTimeline[oldEventId]; + if (existingTimeline) { + delete this._eventIdToTimeline[oldEventId]; + this._eventIdToTimeline[newEventId] = existingTimeline; + } +}; + +/** + * Reset the live timeline, and start a new one. + * + *

This is used when /sync returns a 'limited' timeline. + * + * @param {string=} backPaginationToken token for back-paginating the new timeline + * @param {string=} forwardPaginationToken token for forward-paginating the old live timeline, + * if absent or null, all timelines are reset. + * + * @fires module:client~MatrixClient#event:"Room.timelineReset" + */ +EventTimelineSet.prototype.resetLiveTimeline = function (backPaginationToken, forwardPaginationToken) { + // Each EventTimeline has RoomState objects tracking the state at the start + // and end of that timeline. The copies at the end of the live timeline are + // special because they will have listeners attached to monitor changes to + // the current room state, so we move this RoomState from the end of the + // current live timeline to the end of the new one and, if necessary, + // replace it with a newly created one. We also make a copy for the start + // of the new timeline. + + // if timeline support is disabled, forget about the old timelines + var resetAllTimelines = !this._timelineSupport || !forwardPaginationToken; + + var newTimeline = void 0; + if (resetAllTimelines) { + newTimeline = new EventTimeline(this); + this._timelines = [newTimeline]; + this._eventIdToTimeline = {}; + } else { + newTimeline = this.addTimeline(); + } + + var oldTimeline = this._liveTimeline; + + // Collect the state events from the old timeline + var evMap = oldTimeline.getState(EventTimeline.FORWARDS).events; + var events = []; + for (var evtype in evMap) { + if (!evMap.hasOwnProperty(evtype)) { + continue; + } + for (var stateKey in evMap[evtype]) { + if (!evMap[evtype].hasOwnProperty(stateKey)) { + continue; + } + events.push(evMap[evtype][stateKey]); + } + } + + // Use those events to initialise the state of the new live timeline + newTimeline.initialiseState(events); + + var freshEndState = newTimeline._endState; + // Now clobber the end state of the new live timeline with that from the + // previous live timeline. It will be identical except that we'll keep + // using the same RoomMember objects for the 'live' set of members with any + // listeners still attached + newTimeline._endState = oldTimeline._endState; + + // If we're not resetting all timelines, we need to fix up the old live timeline + if (!resetAllTimelines) { + // Firstly, we just stole the old timeline's end state, so it needs a new one. + // Just swap them around and give it the one we just generated for the + // new live timeline. + oldTimeline._endState = freshEndState; + + // Now set the forward pagination token on the old live timeline + // so it can be forward-paginated. + oldTimeline.setPaginationToken(forwardPaginationToken, EventTimeline.FORWARDS); + } + + // make sure we set the pagination token before firing timelineReset, + // otherwise clients which start back-paginating will fail, and then get + // stuck without realising that they *can* back-paginate. + newTimeline.setPaginationToken(backPaginationToken, EventTimeline.BACKWARDS); + + // Now we can swap the live timeline to the new one. + this._liveTimeline = newTimeline; + this.emit("Room.timelineReset", this.room, this, resetAllTimelines); +}; + +/** + * Get the timeline which contains the given event, if any + * + * @param {string} eventId event ID to look for + * @return {?module:models/event-timeline~EventTimeline} timeline containing + * the given event, or null if unknown + */ +EventTimelineSet.prototype.getTimelineForEvent = function (eventId) { + var res = this._eventIdToTimeline[eventId]; + return res === undefined ? null : res; +}; + +/** + * Get an event which is stored in our timelines + * + * @param {string} eventId event ID to look for + * @return {?module:models/event~MatrixEvent} the given event, or undefined if unknown + */ +EventTimelineSet.prototype.findEventById = function (eventId) { + var tl = this.getTimelineForEvent(eventId); + if (!tl) { + return undefined; + } + return utils.findElement(tl.getEvents(), function (ev) { + return ev.getId() == eventId; + }); +}; + +/** + * Add a new timeline to this timeline list + * + * @return {module:models/event-timeline~EventTimeline} newly-created timeline + */ +EventTimelineSet.prototype.addTimeline = function () { + if (!this._timelineSupport) { + throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable" + " it."); + } + + var timeline = new EventTimeline(this); + this._timelines.push(timeline); + return timeline; +}; + +/** + * Add events to a timeline + * + *

Will fire "Room.timeline" for each event added. + * + * @param {MatrixEvent[]} events A list of events to add. + * + * @param {boolean} toStartOfTimeline True to add these events to the start + * (oldest) instead of the end (newest) of the timeline. If true, the oldest + * event will be the last element of 'events'. + * + * @param {module:models/event-timeline~EventTimeline} timeline timeline to + * add events to. + * + * @param {string=} paginationToken token for the next batch of events + * + * @fires module:client~MatrixClient#event:"Room.timeline" + * + */ +EventTimelineSet.prototype.addEventsToTimeline = function (events, toStartOfTimeline, timeline, paginationToken) { + if (!timeline) { + throw new Error("'timeline' not specified for EventTimelineSet.addEventsToTimeline"); + } + + if (!toStartOfTimeline && timeline == this._liveTimeline) { + throw new Error("EventTimelineSet.addEventsToTimeline cannot be used for adding events to " + "the live timeline - use Room.addLiveEvents instead"); + } + + if (this._filter) { + events = this._filter.filterRoomTimeline(events); + if (!events.length) { + return; + } + } + + var direction = toStartOfTimeline ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; + var inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS; + + // Adding events to timelines can be quite complicated. The following + // illustrates some of the corner-cases. + // + // Let's say we start by knowing about four timelines. timeline3 and + // timeline4 are neighbours: + // + // timeline1 timeline2 timeline3 timeline4 + // [M] [P] [S] <------> [T] + // + // Now we paginate timeline1, and get the following events from the server: + // [M, N, P, R, S, T, U]. + // + // 1. First, we ignore event M, since we already know about it. + // + // 2. Next, we append N to timeline 1. + // + // 3. Next, we don't add event P, since we already know about it, + // but we do link together the timelines. We now have: + // + // timeline1 timeline2 timeline3 timeline4 + // [M, N] <---> [P] [S] <------> [T] + // + // 4. Now we add event R to timeline2: + // + // timeline1 timeline2 timeline3 timeline4 + // [M, N] <---> [P, R] [S] <------> [T] + // + // Note that we have switched the timeline we are working on from + // timeline1 to timeline2. + // + // 5. We ignore event S, but again join the timelines: + // + // timeline1 timeline2 timeline3 timeline4 + // [M, N] <---> [P, R] <---> [S] <------> [T] + // + // 6. We ignore event T, and the timelines are already joined, so there + // is nothing to do. + // + // 7. Finally, we add event U to timeline4: + // + // timeline1 timeline2 timeline3 timeline4 + // [M, N] <---> [P, R] <---> [S] <------> [T, U] + // + // The important thing to note in the above is what happened when we + // already knew about a given event: + // + // - if it was appropriate, we joined up the timelines (steps 3, 5). + // - in any case, we started adding further events to the timeline which + // contained the event we knew about (steps 3, 5, 6). + // + // + // So much for adding events to the timeline. But what do we want to do + // with the pagination token? + // + // In the case above, we will be given a pagination token which tells us how to + // get events beyond 'U' - in this case, it makes sense to store this + // against timeline4. But what if timeline4 already had 'U' and beyond? in + // that case, our best bet is to throw away the pagination token we were + // given and stick with whatever token timeline4 had previously. In short, + // we want to only store the pagination token if the last event we receive + // is one we didn't previously know about. + // + // We make an exception for this if it turns out that we already knew about + // *all* of the events, and we weren't able to join up any timelines. When + // that happens, it means our existing pagination token is faulty, since it + // is only telling us what we already know. Rather than repeatedly + // paginating with the same token, we might as well use the new pagination + // token in the hope that we eventually work our way out of the mess. + + var didUpdate = false; + var lastEventWasNew = false; + for (var i = 0; i < events.length; i++) { + var event = events[i]; + var eventId = event.getId(); + + var existingTimeline = this._eventIdToTimeline[eventId]; + + if (!existingTimeline) { + // we don't know about this event yet. Just add it to the timeline. + this.addEventToTimeline(event, timeline, toStartOfTimeline); + lastEventWasNew = true; + didUpdate = true; + continue; + } + + lastEventWasNew = false; + + if (existingTimeline == timeline) { + debuglog("Event " + eventId + " already in timeline " + timeline); + continue; + } + + var neighbour = timeline.getNeighbouringTimeline(direction); + if (neighbour) { + // this timeline already has a neighbour in the relevant direction; + // let's assume the timelines are already correctly linked up, and + // skip over to it. + // + // there's probably some edge-case here where we end up with an + // event which is in a timeline a way down the chain, and there is + // a break in the chain somewhere. But I can't really imagine how + // that would happen, so I'm going to ignore it for now. + // + if (existingTimeline == neighbour) { + debuglog("Event " + eventId + " in neighbouring timeline - " + "switching to " + existingTimeline); + } else { + debuglog("Event " + eventId + " already in a different " + "timeline " + existingTimeline); + } + timeline = existingTimeline; + continue; + } + + // time to join the timelines. + console.info("Already have timeline for " + eventId + " - joining timeline " + timeline + " to " + existingTimeline); + timeline.setNeighbouringTimeline(existingTimeline, direction); + existingTimeline.setNeighbouringTimeline(timeline, inverseDirection); + timeline = existingTimeline; + didUpdate = true; + } + + // see above - if the last event was new to us, or if we didn't find any + // new information, we update the pagination token for whatever + // timeline we ended up on. + if (lastEventWasNew || !didUpdate) { + timeline.setPaginationToken(paginationToken, direction); + } +}; + +/** + * Add an event to the end of this live timeline. + * + * @param {MatrixEvent} event Event to be added + * @param {string?} duplicateStrategy 'ignore' or 'replace' + */ +EventTimelineSet.prototype.addLiveEvent = function (event, duplicateStrategy) { + if (this._filter) { + var events = this._filter.filterRoomTimeline([event]); + if (!events.length) { + return; + } + } + + var timeline = this._eventIdToTimeline[event.getId()]; + if (timeline) { + if (duplicateStrategy === "replace") { + debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId()); + var tlEvents = timeline.getEvents(); + for (var j = 0; j < tlEvents.length; j++) { + if (tlEvents[j].getId() === event.getId()) { + // still need to set the right metadata on this event + EventTimeline.setEventMetadata(event, timeline.getState(EventTimeline.FORWARDS), false); + + if (!tlEvents[j].encryptedType) { + tlEvents[j] = event; + } + + // XXX: we need to fire an event when this happens. + break; + } + } + } else { + debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " + event.getId()); + } + return; + } + + this.addEventToTimeline(event, this._liveTimeline, false); +}; + +/** + * Add event to the given timeline, and emit Room.timeline. Assumes + * we have already checked we don't know about this event. + * + * Will fire "Room.timeline" for each event added. + * + * @param {MatrixEvent} event + * @param {EventTimeline} timeline + * @param {boolean} toStartOfTimeline + * + * @fires module:client~MatrixClient#event:"Room.timeline" + */ +EventTimelineSet.prototype.addEventToTimeline = function (event, timeline, toStartOfTimeline) { + var eventId = event.getId(); + timeline.addEvent(event, toStartOfTimeline); + this._eventIdToTimeline[eventId] = timeline; + + var data = { + timeline: timeline, + liveEvent: !toStartOfTimeline && timeline == this._liveTimeline + }; + this.emit("Room.timeline", event, this.room, Boolean(toStartOfTimeline), false, data); +}; + +/** + * Replaces event with ID oldEventId with one with newEventId, if oldEventId is + * recognised. Otherwise, add to the live timeline. Used to handle remote echos. + * + * @param {MatrixEvent} localEvent the new event to be added to the timeline + * @param {String} oldEventId the ID of the original event + * @param {boolean} newEventId the ID of the replacement event + * + * @fires module:client~MatrixClient#event:"Room.timeline" + */ +EventTimelineSet.prototype.handleRemoteEcho = function (localEvent, oldEventId, newEventId) { + // XXX: why don't we infer newEventId from localEvent? + var existingTimeline = this._eventIdToTimeline[oldEventId]; + if (existingTimeline) { + delete this._eventIdToTimeline[oldEventId]; + this._eventIdToTimeline[newEventId] = existingTimeline; + } else { + if (this._filter) { + if (this._filter.filterRoomTimeline([localEvent]).length) { + this.addEventToTimeline(localEvent, this._liveTimeline, false); + } + } else { + this.addEventToTimeline(localEvent, this._liveTimeline, false); + } + } +}; + +/** + * Removes a single event from this room. + * + * @param {String} eventId The id of the event to remove + * + * @return {?MatrixEvent} the removed event, or null if the event was not found + * in this room. + */ +EventTimelineSet.prototype.removeEvent = function (eventId) { + var timeline = this._eventIdToTimeline[eventId]; + if (!timeline) { + return null; + } + + var removed = timeline.removeEvent(eventId); + if (removed) { + delete this._eventIdToTimeline[eventId]; + var data = { + timeline: timeline + }; + this.emit("Room.timeline", removed, this.room, undefined, true, data); + } + return removed; +}; + +/** + * Determine where two events appear in the timeline relative to one another + * + * @param {string} eventId1 The id of the first event + * @param {string} eventId2 The id of the second event + + * @return {?number} a number less than zero if eventId1 precedes eventId2, and + * greater than zero if eventId1 succeeds eventId2. zero if they are the + * same event; null if we can't tell (either because we don't know about one + * of the events, or because they are in separate timelines which don't join + * up). + */ +EventTimelineSet.prototype.compareEventOrdering = function (eventId1, eventId2) { + if (eventId1 == eventId2) { + // optimise this case + return 0; + } + + var timeline1 = this._eventIdToTimeline[eventId1]; + var timeline2 = this._eventIdToTimeline[eventId2]; + + if (timeline1 === undefined) { + return null; + } + if (timeline2 === undefined) { + return null; + } + + if (timeline1 === timeline2) { + // both events are in the same timeline - figure out their + // relative indices + var idx1 = void 0, + idx2 = void 0; + var events = timeline1.getEvents(); + for (var idx = 0; idx < events.length && (idx1 === undefined || idx2 === undefined); idx++) { + var evId = events[idx].getId(); + if (evId == eventId1) { + idx1 = idx; + } + if (evId == eventId2) { + idx2 = idx; + } + } + return idx1 - idx2; + } + + // the events are in different timelines. Iterate through the + // linkedlist to see which comes first. + + // first work forwards from timeline1 + var tl = timeline1; + while (tl) { + if (tl === timeline2) { + // timeline1 is before timeline2 + return -1; + } + tl = tl.getNeighbouringTimeline(EventTimeline.FORWARDS); + } + + // now try backwards from timeline1 + tl = timeline1; + while (tl) { + if (tl === timeline2) { + // timeline2 is before timeline1 + return 1; + } + tl = tl.getNeighbouringTimeline(EventTimeline.BACKWARDS); + } + + // the timelines are not contiguous. + return null; +}; + +/** + * The EventTimelineSet class. + */ +module.exports = EventTimelineSet; + +/** + * Fires whenever the timeline in a room is updated. + * @event module:client~MatrixClient#"Room.timeline" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {?Room} room The room, if any, whose timeline was updated. + * @param {boolean} toStartOfTimeline True if this event was added to the start + * @param {boolean} removed True if this event has just been removed from the timeline + * (beginning; oldest) of the timeline e.g. due to pagination. + * + * @param {object} data more data about the event + * + * @param {module:event-timeline.EventTimeline} data.timeline the timeline the + * event was added to/removed from + * + * @param {boolean} data.liveEvent true if the event was a real-time event + * added to the end of the live timeline + * + * @example + * matrixClient.on("Room.timeline", + * function(event, room, toStartOfTimeline, removed, data) { + * if (!toStartOfTimeline && data.liveEvent) { + * var messageToAppend = room.timeline.[room.timeline.length - 1]; + * } + * }); + */ + +/** + * Fires whenever the live timeline in a room is reset. + * + * When we get a 'limited' sync (for example, after a network outage), we reset + * the live timeline to be empty before adding the recent events to the new + * timeline. This event is fired after the timeline is reset, and before the + * new events are added. + * + * @event module:client~MatrixClient#"Room.timelineReset" + * @param {Room} room The room whose live timeline was reset, if any + * @param {EventTimelineSet} timelineSet timelineSet room whose live timeline was reset + * @param {boolean} resetAllTimelines True if all timelines were reset. + */ + +},{"../utils":50,"./event-timeline":29,"events":186}],29:[function(require,module,exports){ +/* +Copyright 2016, 2017 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** + * @module models/event-timeline + */ + +var _freeze = require("babel-runtime/core-js/object/freeze"); + +var _freeze2 = _interopRequireDefault(_freeze); + +var _getIterator2 = require("babel-runtime/core-js/get-iterator"); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var RoomState = require("./room-state"); + +/** + * Construct a new EventTimeline + * + *

An EventTimeline represents a contiguous sequence of events in a room. + * + *

As well as keeping track of the events themselves, it stores the state of + * the room at the beginning and end of the timeline, and pagination tokens for + * going backwards and forwards in the timeline. + * + *

In order that clients can meaningfully maintain an index into a timeline, + * the EventTimeline object tracks a 'baseIndex'. This starts at zero, but is + * incremented when events are prepended to the timeline. The index of an event + * relative to baseIndex therefore remains constant. + * + *

Once a timeline joins up with its neighbour, they are linked together into a + * doubly-linked list. + * + * @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of + * @constructor + */ +function EventTimeline(eventTimelineSet) { + this._eventTimelineSet = eventTimelineSet; + this._roomId = eventTimelineSet.room ? eventTimelineSet.room.roomId : null; + this._events = []; + this._baseIndex = 0; + this._startState = new RoomState(this._roomId); + this._startState.paginationToken = null; + this._endState = new RoomState(this._roomId); + this._endState.paginationToken = null; + + this._prevTimeline = null; + this._nextTimeline = null; + + // this is used by client.js + this._paginationRequests = { 'b': null, 'f': null }; + + this._name = this._roomId + ":" + new Date().toISOString(); +} + +/** + * Symbolic constant for methods which take a 'direction' argument: + * refers to the start of the timeline, or backwards in time. + */ +EventTimeline.BACKWARDS = "b"; + +/** + * Symbolic constant for methods which take a 'direction' argument: + * refers to the end of the timeline, or forwards in time. + */ +EventTimeline.FORWARDS = "f"; + +/** + * Initialise the start and end state with the given events + * + *

This can only be called before any events are added. + * + * @param {MatrixEvent[]} stateEvents list of state events to initialise the + * state with. + * @throws {Error} if an attempt is made to call this after addEvent is called. + */ +EventTimeline.prototype.initialiseState = function (stateEvents) { + if (this._events.length > 0) { + throw new Error("Cannot initialise state after events are added"); + } + + // We previously deep copied events here and used different copies in + // the oldState and state events: this decision seems to date back + // quite a way and was apparently made to fix a bug where modifications + // made to the start state leaked through to the end state. + // This really shouldn't be possible though: the events themselves should + // not change. Duplicating the events uses a lot of extra memory, + // so we now no longer do it. To assert that they really do never change, + // freeze them! Note that we can't do this for events in general: + // although it looks like the only things preventing us are the + // 'status' flag, forwardLooking (which is only set once when adding to the + // timeline) and possibly the sender (which seems like it should never be + // reset but in practice causes a lot of the tests to break). + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)(stateEvents), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var e = _step.value; + + (0, _freeze2.default)(e); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + this._startState.setStateEvents(stateEvents); + this._endState.setStateEvents(stateEvents); +}; + +/** + * Get the ID of the room for this timeline + * @return {string} room ID + */ +EventTimeline.prototype.getRoomId = function () { + return this._roomId; +}; + +/** + * Get the filter for this timeline's timelineSet (if any) + * @return {Filter} filter + */ +EventTimeline.prototype.getFilter = function () { + return this._eventTimelineSet.getFilter(); +}; + +/** + * Get the timelineSet for this timeline + * @return {EventTimelineSet} timelineSet + */ +EventTimeline.prototype.getTimelineSet = function () { + return this._eventTimelineSet; +}; + +/** + * Get the base index. + * + *

This is an index which is incremented when events are prepended to the + * timeline. An individual event therefore stays at the same index in the array + * relative to the base index (although note that a given event's index may + * well be less than the base index, thus giving that event a negative relative + * index). + * + * @return {number} + */ +EventTimeline.prototype.getBaseIndex = function () { + return this._baseIndex; +}; + +/** + * Get the list of events in this context + * + * @return {MatrixEvent[]} An array of MatrixEvents + */ +EventTimeline.prototype.getEvents = function () { + return this._events; +}; + +/** + * Get the room state at the start/end of the timeline + * + * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * start of the timeline; EventTimeline.FORWARDS to get the state at the end + * of the timeline. + * + * @return {RoomState} state at the start/end of the timeline + */ +EventTimeline.prototype.getState = function (direction) { + if (direction == EventTimeline.BACKWARDS) { + return this._startState; + } else if (direction == EventTimeline.FORWARDS) { + return this._endState; + } else { + throw new Error("Invalid direction '" + direction + "'"); + } +}; + +/** + * Get a pagination token + * + * @param {string} direction EventTimeline.BACKWARDS to get the pagination + * token for going backwards in time; EventTimeline.FORWARDS to get the + * pagination token for going forwards in time. + * + * @return {?string} pagination token + */ +EventTimeline.prototype.getPaginationToken = function (direction) { + return this.getState(direction).paginationToken; +}; + +/** + * Set a pagination token + * + * @param {?string} token pagination token + * + * @param {string} direction EventTimeline.BACKWARDS to set the pagination + * token for going backwards in time; EventTimeline.FORWARDS to set the + * pagination token for going forwards in time. + */ +EventTimeline.prototype.setPaginationToken = function (token, direction) { + this.getState(direction).paginationToken = token; +}; + +/** + * Get the next timeline in the series + * + * @param {string} direction EventTimeline.BACKWARDS to get the previous + * timeline; EventTimeline.FORWARDS to get the next timeline. + * + * @return {?EventTimeline} previous or following timeline, if they have been + * joined up. + */ +EventTimeline.prototype.getNeighbouringTimeline = function (direction) { + if (direction == EventTimeline.BACKWARDS) { + return this._prevTimeline; + } else if (direction == EventTimeline.FORWARDS) { + return this._nextTimeline; + } else { + throw new Error("Invalid direction '" + direction + "'"); + } +}; + +/** + * Set the next timeline in the series + * + * @param {EventTimeline} neighbour previous/following timeline + * + * @param {string} direction EventTimeline.BACKWARDS to set the previous + * timeline; EventTimeline.FORWARDS to set the next timeline. + * + * @throws {Error} if an attempt is made to set the neighbouring timeline when + * it is already set. + */ +EventTimeline.prototype.setNeighbouringTimeline = function (neighbour, direction) { + if (this.getNeighbouringTimeline(direction)) { + throw new Error("timeline already has a neighbouring timeline - " + "cannot reset neighbour"); + } + + if (direction == EventTimeline.BACKWARDS) { + this._prevTimeline = neighbour; + } else if (direction == EventTimeline.FORWARDS) { + this._nextTimeline = neighbour; + } else { + throw new Error("Invalid direction '" + direction + "'"); + } + + // make sure we don't try to paginate this timeline + this.setPaginationToken(null, direction); +}; + +/** + * Add a new event to the timeline, and update the state + * + * @param {MatrixEvent} event new event + * @param {boolean} atStart true to insert new event at the start + */ +EventTimeline.prototype.addEvent = function (event, atStart) { + var stateContext = atStart ? this._startState : this._endState; + + // only call setEventMetadata on the unfiltered timelineSets + var timelineSet = this.getTimelineSet(); + if (timelineSet.room && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) { + EventTimeline.setEventMetadata(event, stateContext, atStart); + + // modify state + if (event.isState()) { + stateContext.setStateEvents([event]); + // it is possible that the act of setting the state event means we + // can set more metadata (specifically sender/target props), so try + // it again if the prop wasn't previously set. It may also mean that + // the sender/target is updated (if the event set was a room member event) + // so we want to use the *updated* member (new avatar/name) instead. + // + // However, we do NOT want to do this on member events if we're going + // back in time, else we'll set the .sender value for BEFORE the given + // member event, whereas we want to set the .sender value for the ACTUAL + // member event itself. + if (!event.sender || event.getType() === "m.room.member" && !atStart) { + EventTimeline.setEventMetadata(event, stateContext, atStart); + } + } + } + + var insertIndex = void 0; + + if (atStart) { + insertIndex = 0; + } else { + insertIndex = this._events.length; + } + + this._events.splice(insertIndex, 0, event); // insert element + if (atStart) { + this._baseIndex++; + } +}; + +/** + * Static helper method to set sender and target properties + * + * @param {MatrixEvent} event the event whose metadata is to be set + * @param {RoomState} stateContext the room state to be queried + * @param {bool} toStartOfTimeline if true the event's forwardLooking flag is set false + */ +EventTimeline.setEventMetadata = function (event, stateContext, toStartOfTimeline) { + // set sender and target properties + event.sender = stateContext.getSentinelMember(event.getSender()); + if (event.getType() === "m.room.member") { + event.target = stateContext.getSentinelMember(event.getStateKey()); + } + if (event.isState()) { + // room state has no concept of 'old' or 'current', but we want the + // room state to regress back to previous values if toStartOfTimeline + // is set, which means inspecting prev_content if it exists. This + // is done by toggling the forwardLooking flag. + if (toStartOfTimeline) { + event.forwardLooking = false; + } + } +}; + +/** + * Remove an event from the timeline + * + * @param {string} eventId ID of event to be removed + * @return {?MatrixEvent} removed event, or null if not found + */ +EventTimeline.prototype.removeEvent = function (eventId) { + for (var i = this._events.length - 1; i >= 0; i--) { + var ev = this._events[i]; + if (ev.getId() == eventId) { + this._events.splice(i, 1); + if (i < this._baseIndex) { + this._baseIndex--; + } + return ev; + } + } + return null; +}; + +/** + * Return a string to identify this timeline, for debugging + * + * @return {string} name for this timeline + */ +EventTimeline.prototype.toString = function () { + return this._name; +}; + +/** + * The EventTimeline class + */ +module.exports = EventTimeline; + +},{"./room-state":33,"babel-runtime/core-js/get-iterator":53,"babel-runtime/core-js/object/freeze":59}],30:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** + * This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for + * the public classes. + * @module models/event + */ + +var _regenerator = require('babel-runtime/regenerator'); + +var _regenerator2 = _interopRequireDefault(_regenerator); + +var _bluebird = require('bluebird'); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +var _events = require('events'); + +var _utils = require('../utils.js'); + +var _utils2 = _interopRequireDefault(_utils); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Enum for event statuses. + * @readonly + * @enum {string} + */ +module.exports.EventStatus = { + /** The event was not sent and will no longer be retried. */ + NOT_SENT: "not_sent", + + /** The message is being encrypted */ + ENCRYPTING: "encrypting", + + /** The event is in the process of being sent. */ + SENDING: "sending", + /** The event is in a queue waiting to be sent. */ + QUEUED: "queued", + /** The event has been sent to the server, but we have not yet received the + * echo. */ + SENT: "sent", + + /** The event was cancelled before it was successfully sent. */ + CANCELLED: "cancelled" +}; + +var interns = {}; + +/** + * Construct a Matrix Event object + * @constructor + * + * @param {Object} event The raw event to be wrapped in this DAO + * + * @prop {Object} event The raw (possibly encrypted) event. Do not access + * this property directly unless you absolutely have to. Prefer the getter + * methods defined on this class. Using the getter methods shields your app + * from changes to event JSON between Matrix versions. + * + * @prop {RoomMember} sender The room member who sent this event, or null e.g. + * this is a presence event. This is only guaranteed to be set for events that + * appear in a timeline, ie. do not guarantee that it will be set on state + * events. + * @prop {RoomMember} target The room member who is the target of this event, e.g. + * the invitee, the person being banned, etc. + * @prop {EventStatus} status The sending status of the event. + * @prop {Error} error most recent error associated with sending the event, if any + * @prop {boolean} forwardLooking True if this event is 'forward looking', meaning + * that getDirectionalContent() will return event.content and not event.prev_content. + * Default: true. This property is experimental and may change. + */ +module.exports.MatrixEvent = function MatrixEvent(event) { + // intern the values of matrix events to force share strings and reduce the + // amount of needless string duplication. This can save moderate amounts of + // memory (~10% on a 350MB heap). + // 'membership' at the event level (rather than the content level) is a legacy + // field that Riot never otherwise looks at, but it will still take up a lot + // of space if we don't intern it. + ["state_key", "type", "sender", "room_id", "membership"].forEach(function (prop) { + if (!event[prop]) { + return; + } + if (!interns[event[prop]]) { + interns[event[prop]] = event[prop]; + } + event[prop] = interns[event[prop]]; + }); + + ["membership", "avatar_url", "displayname"].forEach(function (prop) { + if (!event.content || !event.content[prop]) { + return; + } + if (!interns[event.content[prop]]) { + interns[event.content[prop]] = event.content[prop]; + } + event.content[prop] = interns[event.content[prop]]; + }); + + this.event = event || {}; + + this.sender = null; + this.target = null; + this.status = null; + this.error = null; + this.forwardLooking = true; + this._pushActions = null; + + this._clearEvent = {}; + + /* curve25519 key which we believe belongs to the sender of the event. See + * getSenderKey() + */ + this._senderCurve25519Key = null; + + /* ed25519 key which the sender of this event (for olm) or the creator of + * the megolm session (for megolm) claims to own. See getClaimedEd25519Key() + */ + this._claimedEd25519Key = null; + + /* curve25519 keys of devices involved in telling us about the + * _senderCurve25519Key and _claimedEd25519Key. + * See getForwardingCurve25519KeyChain(). + */ + this._forwardingCurve25519KeyChain = []; + + /* if we have a process decrypting this event, a Promise which resolves + * when it is finished. Normally null. + */ + this._decryptionPromise = null; + + /* flag to indicate if we should retry decrypting this event after the + * first attempt (eg, we have received new data which means that a second + * attempt may succeed) + */ + this._retryDecryption = false; +}; +_utils2.default.inherits(module.exports.MatrixEvent, _events.EventEmitter); + +_utils2.default.extend(module.exports.MatrixEvent.prototype, { + + /** + * Get the event_id for this event. + * @return {string} The event ID, e.g. $143350589368169JsLZx:localhost + * + */ + getId: function getId() { + return this.event.event_id; + }, + + /** + * Get the user_id for this event. + * @return {string} The user ID, e.g. @alice:matrix.org + */ + getSender: function getSender() { + return this.event.sender || this.event.user_id; // v2 / v1 + }, + + /** + * Get the (decrypted, if necessary) type of event. + * + * @return {string} The event type, e.g. m.room.message + */ + getType: function getType() { + return this._clearEvent.type || this.event.type; + }, + + /** + * Get the (possibly encrypted) type of the event that will be sent to the + * homeserver. + * + * @return {string} The event type. + */ + getWireType: function getWireType() { + return this.event.type; + }, + + /** + * Get the room_id for this event. This will return undefined + * for m.presence events. + * @return {string} The room ID, e.g. !cURbafjkfsMDVwdRDQ:matrix.org + * + */ + getRoomId: function getRoomId() { + return this.event.room_id; + }, + + /** + * Get the timestamp of this event. + * @return {Number} The event timestamp, e.g. 1433502692297 + */ + getTs: function getTs() { + return this.event.origin_server_ts; + }, + + /** + * Get the timestamp of this event, as a Date object. + * @return {Date} The event date, e.g. new Date(1433502692297) + */ + getDate: function getDate() { + return this.event.origin_server_ts ? new Date(this.event.origin_server_ts) : null; + }, + + /** + * Get the (decrypted, if necessary) event content JSON. + * + * @return {Object} The event content JSON, or an empty object. + */ + getContent: function getContent() { + return this._clearEvent.content || this.event.content || {}; + }, + + /** + * Get the (possibly encrypted) event content JSON that will be sent to the + * homeserver. + * + * @return {Object} The event content JSON, or an empty object. + */ + getWireContent: function getWireContent() { + return this.event.content || {}; + }, + + /** + * Get the previous event content JSON. This will only return something for + * state events which exist in the timeline. + * @return {Object} The previous event content JSON, or an empty object. + */ + getPrevContent: function getPrevContent() { + // v2 then v1 then default + return this.getUnsigned().prev_content || this.event.prev_content || {}; + }, + + /** + * Get either 'content' or 'prev_content' depending on if this event is + * 'forward-looking' or not. This can be modified via event.forwardLooking. + * In practice, this means we get the chronologically earlier content value + * for this event (this method should surely be called getEarlierContent) + * This method is experimental and may change. + * @return {Object} event.content if this event is forward-looking, else + * event.prev_content. + */ + getDirectionalContent: function getDirectionalContent() { + return this.forwardLooking ? this.getContent() : this.getPrevContent(); + }, + + /** + * Get the age of this event. This represents the age of the event when the + * event arrived at the device, and not the age of the event when this + * function was called. + * @return {Number} The age of this event in milliseconds. + */ + getAge: function getAge() { + return this.getUnsigned().age || this.event.age; // v2 / v1 + }, + + /** + * Get the event state_key if it has one. This will return undefined + * for message events. + * @return {string} The event's state_key. + */ + getStateKey: function getStateKey() { + return this.event.state_key; + }, + + /** + * Check if this event is a state event. + * @return {boolean} True if this is a state event. + */ + isState: function isState() { + return this.event.state_key !== undefined; + }, + + /** + * Replace the content of this event with encrypted versions. + * (This is used when sending an event; it should not be used by applications). + * + * @internal + * + * @param {string} crypto_type type of the encrypted event - typically + * "m.room.encrypted" + * + * @param {object} crypto_content raw 'content' for the encrypted event. + * + * @param {string} senderCurve25519Key curve25519 key to record for the + * sender of this event. + * See {@link module:models/event.MatrixEvent#getSenderKey}. + * + * @param {string} claimedEd25519Key claimed ed25519 key to record for the + * sender if this event. + * See {@link module:models/event.MatrixEvent#getClaimedEd25519Key} + */ + makeEncrypted: function makeEncrypted(crypto_type, crypto_content, senderCurve25519Key, claimedEd25519Key) { + // keep the plain-text data for 'view source' + this._clearEvent = { + type: this.event.type, + content: this.event.content + }; + this.event.type = crypto_type; + this.event.content = crypto_content; + this._senderCurve25519Key = senderCurve25519Key; + this._claimedEd25519Key = claimedEd25519Key; + }, + + /** + * Check if this event is currently being decrypted. + * + * @return {boolean} True if this event is currently being decrypted, else false. + */ + isBeingDecrypted: function isBeingDecrypted() { + return this._decryptionPromise != null; + }, + + /** + * Check if this event is an encrypted event which we failed to decrypt + * + * (This implies that we might retry decryption at some point in the future) + * + * @return {boolean} True if this event is an encrypted event which we + * couldn't decrypt. + */ + isDecryptionFailure: function isDecryptionFailure() { + return this._clearEvent && this._clearEvent.content && this._clearEvent.content.msgtype === "m.bad.encrypted"; + }, + + /** + * Start the process of trying to decrypt this event. + * + * (This is used within the SDK: it isn't intended for use by applications) + * + * @internal + * + * @param {module:crypto} crypto crypto module + * + * @returns {Promise} promise which resolves (to undefined) when the decryption + * attempt is completed. + */ + attemptDecryption: function () { + var _ref = (0, _bluebird.method)(function (crypto) { + // start with a couple of sanity checks. + if (!this.isEncrypted()) { + throw new Error("Attempt to decrypt event which isn't encrypted"); + } + + if (this._clearEvent && this._clearEvent.content && this._clearEvent.content.msgtype !== "m.bad.encrypted") { + // we may want to just ignore this? let's start with rejecting it. + throw new Error("Attempt to decrypt event which has already been encrypted"); + } + + // if we already have a decryption attempt in progress, then it may + // fail because it was using outdated info. We now have reason to + // succeed where it failed before, but we don't want to have multiple + // attempts going at the same time, so just set a flag that says we have + // new info. + // + if (this._decryptionPromise) { + console.log('Event ' + this.getId() + ' already being decrypted; queueing a retry'); + this._retryDecryption = true; + return this._decryptionPromise; + } + + this._decryptionPromise = this._decryptionLoop(crypto); + return this._decryptionPromise; + }); + + function attemptDecryption(_x) { + return _ref.apply(this, arguments); + } + + return attemptDecryption; + }(), + + /** + * Cancel any room key request for this event and resend another. + * + * @param {module:crypto} crypto crypto module + */ + cancelAndResendKeyRequest: function cancelAndResendKeyRequest(crypto) { + var wireContent = this.getWireContent(); + crypto.cancelRoomKeyRequest({ + algorithm: wireContent.algorithm, + room_id: this.getRoomId(), + session_id: wireContent.session_id, + sender_key: wireContent.sender_key + }, true); + }, + + _decryptionLoop: function () { + var _ref2 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee(crypto) { + var res; + return _regenerator2.default.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + _context.next = 2; + return (0, _bluebird.resolve)(_bluebird2.default.resolve()); + + case 2: + if (!true) { + _context.next = 33; + break; + } + + this._retryDecryption = false; + + res = void 0; + _context.prev = 5; + + if (crypto) { + _context.next = 10; + break; + } + + res = this._badEncryptedMessage("Encryption not enabled"); + _context.next = 13; + break; + + case 10: + _context.next = 12; + return (0, _bluebird.resolve)(crypto.decryptEvent(this)); + + case 12: + res = _context.sent; + + case 13: + _context.next = 27; + break; + + case 15: + _context.prev = 15; + _context.t0 = _context['catch'](5); + + if (!(_context.t0.name !== "DecryptionError")) { + _context.next = 22; + break; + } + + // not a decryption error: log the whole exception as an error + // (and don't bother with a retry) + console.error('Error decrypting event (id=' + this.getId() + '): ' + (_context.t0.stack || _context.t0)); + this._decryptionPromise = null; + this._retryDecryption = false; + return _context.abrupt('return'); + + case 22: + if (!this._retryDecryption) { + _context.next = 25; + break; + } + + // decryption error, but we have a retry queued. + console.log('Got error decrypting event (id=' + this.getId() + ': ' + (_context.t0 + '), but retrying')); + return _context.abrupt('continue', 2); + + case 25: + + // decryption error, no retries queued. Warn about the error and + // set it to m.bad.encrypted. + console.warn('Error decrypting event (id=' + this.getId() + '): ' + _context.t0.detailedString); + + res = this._badEncryptedMessage(_context.t0.message); + + case 27: + + // at this point, we've either successfully decrypted the event, or have given up + // (and set res to a 'badEncryptedMessage'). Either way, we can now set the + // cleartext of the event and raise Event.decrypted. + // + // make sure we clear '_decryptionPromise' before sending the 'Event.decrypted' event, + // otherwise the app will be confused to see `isBeingDecrypted` still set when + // there isn't an `Event.decrypted` on the way. + // + // see also notes on _retryDecryption above. + // + this._decryptionPromise = null; + this._retryDecryption = false; + this._setClearData(res); + return _context.abrupt('return'); + + case 33: + case 'end': + return _context.stop(); + } + } + }, _callee, this, [[5, 15]]); + })); + + function _decryptionLoop(_x2) { + return _ref2.apply(this, arguments); + } + + return _decryptionLoop; + }(), + + _badEncryptedMessage: function _badEncryptedMessage(reason) { + return { + clearEvent: { + type: "m.room.message", + content: { + msgtype: "m.bad.encrypted", + body: "** Unable to decrypt: " + reason + " **" + } + } + }; + }, + + /** + * Update the cleartext data on this event. + * + * (This is used after decrypting an event; it should not be used by applications). + * + * @internal + * + * @fires module:models/event.MatrixEvent#"Event.decrypted" + * + * @param {module:crypto~EventDecryptionResult} decryptionResult + * the decryption result, including the plaintext and some key info + */ + _setClearData: function _setClearData(decryptionResult) { + this._clearEvent = decryptionResult.clearEvent; + this._senderCurve25519Key = decryptionResult.senderCurve25519Key || null; + this._claimedEd25519Key = decryptionResult.claimedEd25519Key || null; + this._forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain || []; + this.emit("Event.decrypted", this); + }, + + /** + * Check if the event is encrypted. + * @return {boolean} True if this event is encrypted. + */ + isEncrypted: function isEncrypted() { + return this.event.type === "m.room.encrypted"; + }, + + /** + * The curve25519 key for the device that we think sent this event + * + * For an Olm-encrypted event, this is inferred directly from the DH + * exchange at the start of the session: the curve25519 key is involved in + * the DH exchange, so only a device which holds the private part of that + * key can establish such a session. + * + * For a megolm-encrypted event, it is inferred from the Olm message which + * established the megolm session + * + * @return {string} + */ + getSenderKey: function getSenderKey() { + return this._senderCurve25519Key; + }, + + /** + * The additional keys the sender of this encrypted event claims to possess. + * + * Just a wrapper for #getClaimedEd25519Key (q.v.) + * + * @return {Object} + */ + getKeysClaimed: function getKeysClaimed() { + return { + ed25519: this._claimedEd25519Key + }; + }, + + /** + * Get the ed25519 the sender of this event claims to own. + * + * For Olm messages, this claim is encoded directly in the plaintext of the + * event itself. For megolm messages, it is implied by the m.room_key event + * which established the megolm session. + * + * Until we download the device list of the sender, it's just a claim: the + * device list gives a proof that the owner of the curve25519 key used for + * this event (and returned by #getSenderKey) also owns the ed25519 key by + * signing the public curve25519 key with the ed25519 key. + * + * In general, applications should not use this method directly, but should + * instead use MatrixClient.getEventSenderDeviceInfo. + * + * @return {string} + */ + getClaimedEd25519Key: function getClaimedEd25519Key() { + return this._claimedEd25519Key; + }, + + /** + * Get the curve25519 keys of the devices which were involved in telling us + * about the claimedEd25519Key and sender curve25519 key. + * + * Normally this will be empty, but in the case of a forwarded megolm + * session, the sender keys are sent to us by another device (the forwarding + * device), which we need to trust to do this. In that case, the result will + * be a list consisting of one entry. + * + * If the device that sent us the key (A) got it from another device which + * it wasn't prepared to vouch for (B), the result will be [A, B]. And so on. + * + * @return {string[]} base64-encoded curve25519 keys, from oldest to newest. + */ + getForwardingCurve25519KeyChain: function getForwardingCurve25519KeyChain() { + return this._forwardingCurve25519KeyChain; + }, + + getUnsigned: function getUnsigned() { + return this.event.unsigned || {}; + }, + + /** + * Update the content of an event in the same way it would be by the server + * if it were redacted before it was sent to us + * + * @param {module:models/event.MatrixEvent} redaction_event + * event causing the redaction + */ + makeRedacted: function makeRedacted(redaction_event) { + // quick sanity-check + if (!redaction_event.event) { + throw new Error("invalid redaction_event in makeRedacted"); + } + + // we attempt to replicate what we would see from the server if + // the event had been redacted before we saw it. + // + // The server removes (most of) the content of the event, and adds a + // "redacted_because" key to the unsigned section containing the + // redacted event. + if (!this.event.unsigned) { + this.event.unsigned = {}; + } + this.event.unsigned.redacted_because = redaction_event.event; + + var key = void 0; + for (key in this.event) { + if (!this.event.hasOwnProperty(key)) { + continue; + } + if (!_REDACT_KEEP_KEY_MAP[key]) { + delete this.event[key]; + } + } + + var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {}; + var content = this.getContent(); + for (key in content) { + if (!content.hasOwnProperty(key)) { + continue; + } + if (!keeps[key]) { + delete content[key]; + } + } + }, + + /** + * Check if this event has been redacted + * + * @return {boolean} True if this event has been redacted + */ + isRedacted: function isRedacted() { + return Boolean(this.getUnsigned().redacted_because); + }, + + /** + * Get the push actions, if known, for this event + * + * @return {?Object} push actions + */ + getPushActions: function getPushActions() { + return this._pushActions; + }, + + /** + * Set the push actions for this event. + * + * @param {Object} pushActions push actions + */ + setPushActions: function setPushActions(pushActions) { + this._pushActions = pushActions; + }, + + /** + * Replace the `event` property and recalculate any properties based on it. + * @param {Object} event the object to assign to the `event` property + */ + handleRemoteEcho: function handleRemoteEcho(event) { + this.event = event; + // successfully sent. + this.status = null; + } +}); + +/* _REDACT_KEEP_KEY_MAP gives the keys we keep when an event is redacted + * + * This is specified here: + * http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#redactions + * + * Also: + * - We keep 'unsigned' since that is created by the local server + * - We keep user_id for backwards-compat with v1 + */ +var _REDACT_KEEP_KEY_MAP = ['event_id', 'type', 'room_id', 'user_id', 'sender', 'state_key', 'prev_state', 'content', 'unsigned', 'origin_server_ts'].reduce(function (ret, val) { + ret[val] = 1;return ret; +}, {}); + +// a map from event type to the .content keys we keep when an event is redacted +var _REDACT_KEEP_CONTENT_MAP = { + 'm.room.member': { 'membership': 1 }, + 'm.room.create': { 'creator': 1 }, + 'm.room.join_rules': { 'join_rule': 1 }, + 'm.room.power_levels': { 'ban': 1, 'events': 1, 'events_default': 1, + 'kick': 1, 'redact': 1, 'state_default': 1, + 'users': 1, 'users_default': 1 + }, + 'm.room.aliases': { 'aliases': 1 } +}; + +/** + * Fires when an event is decrypted + * + * @event module:models/event.MatrixEvent#"Event.decrypted" + * + * @param {module:models/event.MatrixEvent} event + * The matrix event which has been decrypted + */ + +},{"../utils.js":50,"babel-runtime/regenerator":73,"bluebird":74,"events":186}],31:[function(require,module,exports){ +"use strict"; + +/* +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module models/group + */ +var EventEmitter = require("events").EventEmitter; + +var utils = require("../utils"); + +/** + * Construct a new Group. + * + * @param {string} groupId The ID of this group. + * + * @prop {string} groupId The ID of this group. + * @prop {string} name The human-readable display name for this group. + * @prop {string} avatarUrl The mxc URL for this group's avatar. + * @prop {string} myMembership The logged in user's membership of this group + * @prop {Object} inviter Infomation about the user who invited the logged in user + * to the group, if myMembership is 'invite'. + * @prop {string} inviter.userId The user ID of the inviter + */ +function Group(groupId) { + this.groupId = groupId; + this.name = null; + this.avatarUrl = null; + this.myMembership = null; + this.inviter = null; +} +utils.inherits(Group, EventEmitter); + +Group.prototype.setProfile = function (name, avatarUrl) { + if (this.name === name && this.avatarUrl === avatarUrl) return; + + this.name = name || this.groupId; + this.avatarUrl = avatarUrl; + + this.emit("Group.profile", this); +}; + +Group.prototype.setMyMembership = function (membership) { + if (this.myMembership === membership) return; + + this.myMembership = membership; + + this.emit("Group.myMembership", this); +}; + +/** + * Sets the 'inviter' property. This does not emit an event (the inviter + * will only change when the user is revited / reinvited to a room), + * so set this before setting myMembership. + * @param {Object} inviter Infomation about who invited us to the room + */ +Group.prototype.setInviter = function (inviter) { + this.inviter = inviter; +}; + +module.exports = Group; + +/** + * Fires whenever a group's profile information is updated. + * This means the 'name' and 'avatarUrl' properties. + * @event module:client~MatrixClient#"Group.profile" + * @param {Group} group The group whose profile was updated. + * @example + * matrixClient.on("Group.profile", function(group){ + * var name = group.name; + * }); + */ + +/** + * Fires whenever the logged in user's membership status of + * the group is updated. + * @event module:client~MatrixClient#"Group.myMembership" + * @param {Group} group The group in which the user's membership changed + * @example + * matrixClient.on("Group.myMembership", function(group){ + * var myMembership = group.myMembership; + * }); + */ + +},{"../utils":50,"events":186}],32:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; +/** + * @module models/room-member + */ + +var EventEmitter = require("events").EventEmitter; +var ContentRepo = require("../content-repo"); + +var utils = require("../utils"); + +/** + * Construct a new room member. + * + * @constructor + * @alias module:models/room-member + * + * @param {string} roomId The room ID of the member. + * @param {string} userId The user ID of the member. + * @prop {string} roomId The room ID for this member. + * @prop {string} userId The user ID of this member. + * @prop {boolean} typing True if the room member is currently typing. + * @prop {string} name The human-readable name for this room member. This will be + * disambiguated with a suffix of " (@user_id:matrix.org)" if another member shares the + * same displayname. + * @prop {string} rawDisplayName The ambiguous displayname of this room member. + * @prop {Number} powerLevel The power level for this room member. + * @prop {Number} powerLevelNorm The normalised power level (0-100) for this + * room member. + * @prop {User} user The User object for this room member, if one exists. + * @prop {string} membership The membership state for this room member e.g. 'join'. + * @prop {Object} events The events describing this RoomMember. + * @prop {MatrixEvent} events.member The m.room.member event for this RoomMember. + */ +function RoomMember(roomId, userId) { + this.roomId = roomId; + this.userId = userId; + this.typing = false; + this.name = userId; + this.rawDisplayName = userId; + this.powerLevel = 0; + this.powerLevelNorm = 0; + this.user = null; + this.membership = null; + this.events = { + member: null + }; + this._updateModifiedTime(); +} +utils.inherits(RoomMember, EventEmitter); + +/** + * Update this room member's membership event. May fire "RoomMember.name" if + * this event updates this member's name. + * @param {MatrixEvent} event The m.room.member event + * @param {RoomState} roomState Optional. The room state to take into account + * when calculating (e.g. for disambiguating users with the same name). + * @fires module:client~MatrixClient#event:"RoomMember.name" + * @fires module:client~MatrixClient#event:"RoomMember.membership" + */ +RoomMember.prototype.setMembershipEvent = function (event, roomState) { + if (event.getType() !== "m.room.member") { + return; + } + this.events.member = event; + + var oldMembership = this.membership; + this.membership = event.getDirectionalContent().membership; + + var oldName = this.name; + this.name = calculateDisplayName(this, event, roomState); + this.rawDisplayName = event.getDirectionalContent().displayname || this.userId; + if (oldMembership !== this.membership) { + this._updateModifiedTime(); + this.emit("RoomMember.membership", event, this, oldMembership); + } + if (oldName !== this.name) { + this._updateModifiedTime(); + this.emit("RoomMember.name", event, this, oldName); + } +}; + +/** + * Update this room member's power level event. May fire + * "RoomMember.powerLevel" if this event updates this member's power levels. + * @param {MatrixEvent} powerLevelEvent The m.room.power_levels + * event + * @fires module:client~MatrixClient#event:"RoomMember.powerLevel" + */ +RoomMember.prototype.setPowerLevelEvent = function (powerLevelEvent) { + if (powerLevelEvent.getType() !== "m.room.power_levels") { + return; + } + + var evContent = powerLevelEvent.getDirectionalContent(); + + var maxLevel = evContent.users_default || 0; + utils.forEach(utils.values(evContent.users), function (lvl) { + maxLevel = Math.max(maxLevel, lvl); + }); + var oldPowerLevel = this.powerLevel; + var oldPowerLevelNorm = this.powerLevelNorm; + + if (evContent.users && evContent.users[this.userId] !== undefined) { + this.powerLevel = evContent.users[this.userId]; + } else if (evContent.users_default !== undefined) { + this.powerLevel = evContent.users_default; + } else { + this.powerLevel = 0; + } + this.powerLevelNorm = 0; + if (maxLevel > 0) { + this.powerLevelNorm = this.powerLevel * 100 / maxLevel; + } + + // emit for changes in powerLevelNorm as well (since the app will need to + // redraw everyone's level if the max has changed) + if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) { + this._updateModifiedTime(); + this.emit("RoomMember.powerLevel", powerLevelEvent, this); + } +}; + +/** + * Update this room member's typing event. May fire "RoomMember.typing" if + * this event changes this member's typing state. + * @param {MatrixEvent} event The typing event + * @fires module:client~MatrixClient#event:"RoomMember.typing" + */ +RoomMember.prototype.setTypingEvent = function (event) { + if (event.getType() !== "m.typing") { + return; + } + var oldTyping = this.typing; + this.typing = false; + var typingList = event.getContent().user_ids; + if (!utils.isArray(typingList)) { + // malformed event :/ bail early. TODO: whine? + return; + } + if (typingList.indexOf(this.userId) !== -1) { + this.typing = true; + } + if (oldTyping !== this.typing) { + this._updateModifiedTime(); + this.emit("RoomMember.typing", event, this); + } +}; + +/** + * Update the last modified time to the current time. + */ +RoomMember.prototype._updateModifiedTime = function () { + this._modified = Date.now(); +}; + +/** + * Get the timestamp when this RoomMember was last updated. This timestamp is + * updated when properties on this RoomMember are updated. + * It is updated before firing events. + * @return {number} The timestamp + */ +RoomMember.prototype.getLastModifiedTime = function () { + return this._modified; +}; + +/** + * Get the avatar URL for a room member. + * @param {string} baseUrl The base homeserver URL See + * {@link module:client~MatrixClient#getHomeserverUrl}. + * @param {Number} width The desired width of the thumbnail. + * @param {Number} height The desired height of the thumbnail. + * @param {string} resizeMethod The thumbnail resize method to use, either + * "crop" or "scale". + * @param {Boolean} allowDefault (optional) Passing false causes this method to + * return null if the user has no avatar image. Otherwise, a default image URL + * will be returned. Default: true. + * @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be + * returned even if it is a direct hyperlink rather than a matrix content URL. + * If false, any non-matrix content URLs will be ignored. Setting this option to + * true will expose URLs that, if fetched, will leak information about the user + * to anyone who they share a room with. + * @return {?string} the avatar URL or null. + */ +RoomMember.prototype.getAvatarUrl = function (baseUrl, width, height, resizeMethod, allowDefault, allowDirectLinks) { + if (allowDefault === undefined) { + allowDefault = true; + } + if (!this.events.member && !allowDefault) { + return null; + } + var rawUrl = this.events.member ? this.events.member.getContent().avatar_url : null; + var httpUrl = ContentRepo.getHttpUriForMxc(baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks); + if (httpUrl) { + return httpUrl; + } else if (allowDefault) { + return ContentRepo.getIdenticonUri(baseUrl, this.userId, width, height); + } + return null; +}; + +function calculateDisplayName(member, event, roomState) { + var displayName = event.getDirectionalContent().displayname; + var selfUserId = member.userId; + + if (!displayName) { + return selfUserId; + } + + if (!roomState) { + return displayName; + } + + // Check if the name contains something that look like a mxid + // If it does, it may be someone trying to impersonate someone else + // Show full mxid in this case + // Also show mxid if there are other people with the same displayname + var disambiguate = /@.+:.+/.test(displayName); + if (!disambiguate) { + var userIds = roomState.getUserIdsWithDisplayName(displayName); + var otherUsers = userIds.filter(function (u) { + return u !== selfUserId; + }); + disambiguate = otherUsers.length > 0; + } + + if (disambiguate) { + return displayName + " (" + selfUserId + ")"; + } + return displayName; +} + +/** + * The RoomMember class. + */ +module.exports = RoomMember; + +/** + * Fires whenever any room member's name changes. + * @event module:client~MatrixClient#"RoomMember.name" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {RoomMember} member The member whose RoomMember.name changed. + * @param {string?} oldName The previous name. Null if the member didn't have a + * name previously. + * @example + * matrixClient.on("RoomMember.name", function(event, member){ + * var newName = member.name; + * }); + */ + +/** + * Fires whenever any room member's membership state changes. + * @event module:client~MatrixClient#"RoomMember.membership" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {RoomMember} member The member whose RoomMember.membership changed. + * @param {string?} oldMembership The previous membership state. Null if it's a + * new member. + * @example + * matrixClient.on("RoomMember.membership", function(event, member, oldMembership){ + * var newState = member.membership; + * }); + */ + +/** + * Fires whenever any room member's typing state changes. + * @event module:client~MatrixClient#"RoomMember.typing" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {RoomMember} member The member whose RoomMember.typing changed. + * @example + * matrixClient.on("RoomMember.typing", function(event, member){ + * var isTyping = member.typing; + * }); + */ + +/** + * Fires whenever any room member's power level changes. + * @event module:client~MatrixClient#"RoomMember.powerLevel" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {RoomMember} member The member whose RoomMember.powerLevel changed. + * @example + * matrixClient.on("RoomMember.powerLevel", function(event, member){ + * var newPowerLevel = member.powerLevel; + * var newNormPowerLevel = member.powerLevelNorm; + * }); + */ + +},{"../content-repo":6,"../utils":50,"events":186}],33:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; +/** + * @module models/room-state + */ + +var EventEmitter = require("events").EventEmitter; + +var utils = require("../utils"); +var RoomMember = require("./room-member"); + +/** + * Construct room state. + * + * Room State represents the state of the room at a given point. + * It can be mutated by adding state events to it. + * There are two types of room member associated with a state event: + * normal member objects (accessed via getMember/getMembers) which mutate + * with the state to represent the current state of that room/user, eg. + * the object returned by getMember('@bob:example.com') will mutate to + * get a different display name if Bob later changes his display name + * in the room. + * There are also 'sentinel' members (accessed via getSentinelMember). + * These also represent the state of room members at the point in time + * represented by the RoomState object, but unlike objects from getMember, + * sentinel objects will always represent the room state as at the time + * getSentinelMember was called, so if Bob subsequently changes his display + * name, a room member object previously acquired with getSentinelMember + * will still have his old display name. Calling getSentinelMember again + * after the display name change will return a new RoomMember object + * with Bob's new display name. + * + * @constructor + * @param {?string} roomId Optional. The ID of the room which has this state. + * If none is specified it just tracks paginationTokens, useful for notifTimelineSet + * @prop {Object.} members The room member dictionary, keyed + * on the user's ID. + * @prop {Object.>} events The state + * events dictionary, keyed on the event type and then the state_key value. + * @prop {string} paginationToken The pagination token for this state. + */ +function RoomState(roomId) { + this.roomId = roomId; + this.members = { + // userId: RoomMember + }; + this.events = { + // eventType: { stateKey: MatrixEvent } + }; + this.paginationToken = null; + + this._sentinels = { + // userId: RoomMember + }; + this._updateModifiedTime(); + this._displayNameToUserIds = {}; + this._userIdsToDisplayNames = {}; + this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite + this._joinedMemberCount = null; // cache of the number of joined members +} +utils.inherits(RoomState, EventEmitter); + +/** + * Returns the number of joined members in this room + * This method caches the result. + * @return {integer} The number of members in this room whose membership is 'join' + */ +RoomState.prototype.getJoinedMemberCount = function () { + if (this._joinedMemberCount === null) { + this._joinedMemberCount = this.getMembers().filter(function (m) { + return m.membership === 'join'; + }).length; + } + return this._joinedMemberCount; +}; + +/** + * Get all RoomMembers in this room. + * @return {Array} A list of RoomMembers. + */ +RoomState.prototype.getMembers = function () { + return utils.values(this.members); +}; + +/** + * Get a room member by their user ID. + * @param {string} userId The room member's user ID. + * @return {RoomMember} The member or null if they do not exist. + */ +RoomState.prototype.getMember = function (userId) { + return this.members[userId] || null; +}; + +/** + * Get a room member whose properties will not change with this room state. You + * typically want this if you want to attach a RoomMember to a MatrixEvent which + * may no longer be represented correctly by Room.currentState or Room.oldState. + * The term 'sentinel' refers to the fact that this RoomMember is an unchanging + * guardian for state at this particular point in time. + * @param {string} userId The room member's user ID. + * @return {RoomMember} The member or null if they do not exist. + */ +RoomState.prototype.getSentinelMember = function (userId) { + if (!userId) return null; + var sentinel = this._sentinels[userId]; + + if (sentinel === undefined) { + sentinel = new RoomMember(this.roomId, userId); + var membershipEvent = this.getStateEvents("m.room.member", userId); + if (!membershipEvent) return null; + sentinel.setMembershipEvent(membershipEvent, this); + var pwrLvlEvent = this.getStateEvents("m.room.power_levels", ""); + if (pwrLvlEvent) { + sentinel.setPowerLevelEvent(pwrLvlEvent); + } + this._sentinels[userId] = sentinel; + } + return sentinel; +}; + +/** + * Get state events from the state of the room. + * @param {string} eventType The event type of the state event. + * @param {string} stateKey Optional. The state_key of the state event. If + * this is undefined then all matching state events will be + * returned. + * @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was + * undefined, else a single event (or null if no match found). + */ +RoomState.prototype.getStateEvents = function (eventType, stateKey) { + if (!this.events[eventType]) { + // no match + return stateKey === undefined ? [] : null; + } + if (stateKey === undefined) { + // return all values + return utils.values(this.events[eventType]); + } + var event = this.events[eventType][stateKey]; + return event ? event : null; +}; + +/** + * Add an array of one or more state MatrixEvents, overwriting + * any existing state with the same {type, stateKey} tuple. Will fire + * "RoomState.events" for every event added. May fire "RoomState.members" + * if there are m.room.member events. + * @param {MatrixEvent[]} stateEvents a list of state events for this room. + * @fires module:client~MatrixClient#event:"RoomState.members" + * @fires module:client~MatrixClient#event:"RoomState.newMember" + * @fires module:client~MatrixClient#event:"RoomState.events" + */ +RoomState.prototype.setStateEvents = function (stateEvents) { + var self = this; + this._updateModifiedTime(); + + // update the core event dict + utils.forEach(stateEvents, function (event) { + if (event.getRoomId() !== self.roomId) { + return; + } + if (!event.isState()) { + return; + } + + if (self.events[event.getType()] === undefined) { + self.events[event.getType()] = {}; + } + self.events[event.getType()][event.getStateKey()] = event; + if (event.getType() === "m.room.member") { + _updateDisplayNameCache(self, event.getStateKey(), event.getContent().displayname); + _updateThirdPartyTokenCache(self, event); + } + self.emit("RoomState.events", event, self); + }); + + // update higher level data structures. This needs to be done AFTER the + // core event dict as these structures may depend on other state events in + // the given array (e.g. disambiguating display names in one go to do both + // clashing names rather than progressively which only catches 1 of them). + utils.forEach(stateEvents, function (event) { + if (event.getRoomId() !== self.roomId) { + return; + } + if (!event.isState()) { + return; + } + + if (event.getType() === "m.room.member") { + var userId = event.getStateKey(); + + // leave events apparently elide the displayname or avatar_url, + // so let's fake one up so that we don't leak user ids + // into the timeline + if (event.getContent().membership === "leave" || event.getContent().membership === "ban") { + event.getContent().avatar_url = event.getContent().avatar_url || event.getPrevContent().avatar_url; + event.getContent().displayname = event.getContent().displayname || event.getPrevContent().displayname; + } + + var member = self.members[userId]; + if (!member) { + member = new RoomMember(event.getRoomId(), userId); + self.emit("RoomState.newMember", event, self, member); + } + + member.setMembershipEvent(event, self); + // this member may have a power level already, so set it. + var pwrLvlEvent = self.getStateEvents("m.room.power_levels", ""); + if (pwrLvlEvent) { + member.setPowerLevelEvent(pwrLvlEvent); + } + + // blow away the sentinel which is now outdated + delete self._sentinels[userId]; + + self.members[userId] = member; + self._joinedMemberCount = null; + self.emit("RoomState.members", event, self, member); + } else if (event.getType() === "m.room.power_levels") { + var members = utils.values(self.members); + utils.forEach(members, function (member) { + member.setPowerLevelEvent(event); + self.emit("RoomState.members", event, self, member); + }); + + // assume all our sentinels are now out-of-date + self._sentinels = {}; + } + }); +}; + +/** + * Set the current typing event for this room. + * @param {MatrixEvent} event The typing event + */ +RoomState.prototype.setTypingEvent = function (event) { + utils.forEach(utils.values(this.members), function (member) { + member.setTypingEvent(event); + }); +}; + +/** + * Get the m.room.member event which has the given third party invite token. + * + * @param {string} token The token + * @return {?MatrixEvent} The m.room.member event or null + */ +RoomState.prototype.getInviteForThreePidToken = function (token) { + return this._tokenToInvite[token] || null; +}; + +/** + * Update the last modified time to the current time. + */ +RoomState.prototype._updateModifiedTime = function () { + this._modified = Date.now(); +}; + +/** + * Get the timestamp when this room state was last updated. This timestamp is + * updated when this object has received new state events. + * @return {number} The timestamp + */ +RoomState.prototype.getLastModifiedTime = function () { + return this._modified; +}; + +/** + * Get user IDs with the specified display name. + * @param {string} displayName The display name to get user IDs from. + * @return {string[]} An array of user IDs or an empty array. + */ +RoomState.prototype.getUserIdsWithDisplayName = function (displayName) { + return this._displayNameToUserIds[displayName] || []; +}; + +/** + * Returns true if userId is in room, event is not redacted and either sender of + * mxEvent or has power level sufficient to redact events other than their own. + * @param {MatrixEvent} mxEvent The event to test permission for + * @param {string} userId The user ID of the user to test permission for + * @return {boolean} true if the given used ID can redact given event + */ +RoomState.prototype.maySendRedactionForEvent = function (mxEvent, userId) { + var member = this.getMember(userId); + if (!member || member.membership === 'leave') return false; + + if (mxEvent.status || mxEvent.isRedacted()) return false; + + // The user may have been the sender, but they can't redact their own message + // if redactions are blocked. + var canRedact = this.maySendEvent("m.room.redaction", userId); + if (mxEvent.getSender() === userId) return canRedact; + + return this._hasSufficientPowerLevelFor('redact', member.powerLevel); +}; + +/** + * Returns true if the given power level is sufficient for action + * @param {string} action The type of power level to check + * @param {number} powerLevel The power level of the member + * @return {boolean} true if the given power level is sufficient + */ +RoomState.prototype._hasSufficientPowerLevelFor = function (action, powerLevel) { + var powerLevelsEvent = this.getStateEvents('m.room.power_levels', ''); + + var powerLevels = {}; + if (powerLevelsEvent) { + powerLevels = powerLevelsEvent.getContent(); + } + + var requiredLevel = 50; + if (powerLevels[action] !== undefined) { + requiredLevel = powerLevels[action]; + } + + return powerLevel >= requiredLevel; +}; + +/** + * Short-form for maySendEvent('m.room.message', userId) + * @param {string} userId The user ID of the user to test permission for + * @return {boolean} true if the given user ID should be permitted to send + * message events into the given room. + */ +RoomState.prototype.maySendMessage = function (userId) { + return this._maySendEventOfType('m.room.message', userId, false); +}; + +/** + * Returns true if the given user ID has permission to send a normal + * event of type `eventType` into this room. + * @param {string} eventType The type of event to test + * @param {string} userId The user ID of the user to test permission for + * @return {boolean} true if the given user ID should be permitted to send + * the given type of event into this room, + * according to the room's state. + */ +RoomState.prototype.maySendEvent = function (eventType, userId) { + return this._maySendEventOfType(eventType, userId, false); +}; + +/** + * Returns true if the given MatrixClient has permission to send a state + * event of type `stateEventType` into this room. + * @param {string} stateEventType The type of state events to test + * @param {MatrixClient} cli The client to test permission for + * @return {boolean} true if the given client should be permitted to send + * the given type of state event into this room, + * according to the room's state. + */ +RoomState.prototype.mayClientSendStateEvent = function (stateEventType, cli) { + if (cli.isGuest()) { + return false; + } + return this.maySendStateEvent(stateEventType, cli.credentials.userId); +}; + +/** + * Returns true if the given user ID has permission to send a state + * event of type `stateEventType` into this room. + * @param {string} stateEventType The type of state events to test + * @param {string} userId The user ID of the user to test permission for + * @return {boolean} true if the given user ID should be permitted to send + * the given type of state event into this room, + * according to the room's state. + */ +RoomState.prototype.maySendStateEvent = function (stateEventType, userId) { + return this._maySendEventOfType(stateEventType, userId, true); +}; + +/** + * Returns true if the given user ID has permission to send a normal or state + * event of type `eventType` into this room. + * @param {string} eventType The type of event to test + * @param {string} userId The user ID of the user to test permission for + * @param {boolean} state If true, tests if the user may send a state + event of this type. Otherwise tests whether + they may send a regular event. + * @return {boolean} true if the given user ID should be permitted to send + * the given type of event into this room, + * according to the room's state. + */ +RoomState.prototype._maySendEventOfType = function (eventType, userId, state) { + var member = this.getMember(userId); + if (!member || member.membership == 'leave') { + return false; + } + + var power_levels_event = this.getStateEvents('m.room.power_levels', ''); + + var power_levels = void 0; + var events_levels = {}; + + var state_default = 0; + var events_default = 0; + if (power_levels_event) { + power_levels = power_levels_event.getContent(); + events_levels = power_levels.events || {}; + + if (power_levels.state_default !== undefined) { + state_default = power_levels.state_default; + } else { + state_default = 50; + } + if (power_levels.events_default !== undefined) { + events_default = power_levels.events_default; + } + } + + var required_level = state ? state_default : events_default; + if (events_levels[eventType] !== undefined) { + required_level = events_levels[eventType]; + } + return member.powerLevel >= required_level; +}; + +/** + * Returns true if the given user ID has permission to trigger notification + * of type `notifLevelKey` + * @param {string} notifLevelKey The level of notification to test (eg. 'room') + * @param {string} userId The user ID of the user to test permission for + * @return {boolean} true if the given user ID has permission to trigger a + * notification of this type. + */ +RoomState.prototype.mayTriggerNotifOfType = function (notifLevelKey, userId) { + var member = this.getMember(userId); + if (!member) { + return false; + } + + var powerLevelsEvent = this.getStateEvents('m.room.power_levels', ''); + + var notifLevel = 50; + if (powerLevelsEvent && powerLevelsEvent.getContent() && powerLevelsEvent.getContent().notifications && powerLevelsEvent.getContent().notifications[notifLevelKey]) { + notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey]; + } + + return member.powerLevel >= notifLevel; +}; + +/** + * The RoomState class. + */ +module.exports = RoomState; + +function _updateThirdPartyTokenCache(roomState, memberEvent) { + if (!memberEvent.getContent().third_party_invite) { + return; + } + var token = (memberEvent.getContent().third_party_invite.signed || {}).token; + if (!token) { + return; + } + var threePidInvite = roomState.getStateEvents("m.room.third_party_invite", token); + if (!threePidInvite) { + return; + } + roomState._tokenToInvite[token] = memberEvent; +} + +function _updateDisplayNameCache(roomState, userId, displayName) { + var oldName = roomState._userIdsToDisplayNames[userId]; + delete roomState._userIdsToDisplayNames[userId]; + if (oldName) { + // Remove the old name from the cache. + // We clobber the user_id > name lookup but the name -> [user_id] lookup + // means we need to remove that user ID from that array rather than nuking + // the lot. + var existingUserIds = roomState._displayNameToUserIds[oldName] || []; + for (var i = 0; i < existingUserIds.length; i++) { + if (existingUserIds[i] === userId) { + // remove this user ID from this array + existingUserIds.splice(i, 1); + i--; + } + } + roomState._displayNameToUserIds[oldName] = existingUserIds; + } + + roomState._userIdsToDisplayNames[userId] = displayName; + if (!roomState._displayNameToUserIds[displayName]) { + roomState._displayNameToUserIds[displayName] = []; + } + roomState._displayNameToUserIds[displayName].push(userId); +} + +/** + * Fires whenever the event dictionary in room state is updated. + * @event module:client~MatrixClient#"RoomState.events" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {RoomState} state The room state whose RoomState.events dictionary + * was updated. + * @example + * matrixClient.on("RoomState.events", function(event, state){ + * var newStateEvent = event; + * }); + */ + +/** + * Fires whenever a member in the members dictionary is updated in any way. + * @event module:client~MatrixClient#"RoomState.members" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {RoomState} state The room state whose RoomState.members dictionary + * was updated. + * @param {RoomMember} member The room member that was updated. + * @example + * matrixClient.on("RoomState.members", function(event, state, member){ + * var newMembershipState = member.membership; + * }); + */ + +/** +* Fires whenever a member is added to the members dictionary. The RoomMember +* will not be fully populated yet (e.g. no membership state). +* @event module:client~MatrixClient#"RoomState.newMember" +* @param {MatrixEvent} event The matrix event which caused this event to fire. +* @param {RoomState} state The room state whose RoomState.members dictionary +* was updated with a new entry. +* @param {RoomMember} member The room member that was added. +* @example +* matrixClient.on("RoomState.newMember", function(event, state, member){ +* // add event listeners on 'member' +* }); +*/ + +},{"../utils":50,"./room-member":32,"events":186}],34:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; +/** + * @module models/room-summary + */ + +/** + * Construct a new Room Summary. A summary can be used for display on a recent + * list, without having to load the entire room list into memory. + * @constructor + * @param {string} roomId Required. The ID of this room. + * @param {Object} info Optional. The summary info. Additional keys are supported. + * @param {string} info.title The title of the room (e.g. m.room.name) + * @param {string} info.desc The description of the room (e.g. + * m.room.topic) + * @param {Number} info.numMembers The number of joined users. + * @param {string[]} info.aliases The list of aliases for this room. + * @param {Number} info.timestamp The timestamp for this room. + */ + +function RoomSummary(roomId, info) { + this.roomId = roomId; + this.info = info; +} + +/** + * The RoomSummary class. + */ +module.exports = RoomSummary; + +},{}],35:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; +/** + * @module models/room + */ + +var _assign = require("babel-runtime/core-js/object/assign"); + +var _assign2 = _interopRequireDefault(_assign); + +var _ReEmitter = require("../ReEmitter"); + +var _ReEmitter2 = _interopRequireDefault(_ReEmitter); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var EventEmitter = require("events").EventEmitter; + +var EventStatus = require("./event").EventStatus; +var RoomSummary = require("./room-summary"); +var MatrixEvent = require("./event").MatrixEvent; +var utils = require("../utils"); +var ContentRepo = require("../content-repo"); +var EventTimeline = require("./event-timeline"); +var EventTimelineSet = require("./event-timeline-set"); + +function synthesizeReceipt(userId, event, receiptType) { + // console.log("synthesizing receipt for "+event.getId()); + // This is really ugly because JS has no way to express an object literal + // where the name of a key comes from an expression + var fakeReceipt = { + content: {}, + type: "m.receipt", + room_id: event.getRoomId() + }; + fakeReceipt.content[event.getId()] = {}; + fakeReceipt.content[event.getId()][receiptType] = {}; + fakeReceipt.content[event.getId()][receiptType][userId] = { + ts: event.getTs() + }; + return new MatrixEvent(fakeReceipt); +} + +/** + * Construct a new Room. + * + *

For a room, we store an ordered sequence of timelines, which may or may not + * be continuous. Each timeline lists a series of events, as well as tracking + * the room state at the start and the end of the timeline. It also tracks + * forward and backward pagination tokens, as well as containing links to the + * next timeline in the sequence. + * + *

There is one special timeline - the 'live' timeline, which represents the + * timeline to which events are being added in real-time as they are received + * from the /sync API. Note that you should not retain references to this + * timeline - even if it is the current timeline right now, it may not remain + * so if the server gives us a timeline gap in /sync. + * + *

In order that we can find events from their ids later, we also maintain a + * map from event_id to timeline and index. + * + * @constructor + * @alias module:models/room + * @param {string} roomId Required. The ID of this room. + * @param {Object=} opts Configuration options + * @param {*} opts.storageToken Optional. The token which a data store can use + * to remember the state of the room. What this means is dependent on the store + * implementation. + * + * @param {String=} opts.pendingEventOrdering Controls where pending messages + * appear in a room's timeline. If "chronological", messages will appear + * in the timeline when the call to sendEvent was made. If + * "detached", pending messages will appear in a separate list, + * accessbile via {@link module:models/room#getPendingEvents}. Default: + * "chronological". + * + * @param {boolean} [opts.timelineSupport = false] Set to true to enable improved + * timeline support. + * + * @prop {string} roomId The ID of this room. + * @prop {string} name The human-readable display name for this room. + * @prop {Array} timeline The live event timeline for this room, + * with the oldest event at index 0. Present for backwards compatibility - + * prefer getLiveTimeline().getEvents(). + * @prop {object} tags Dict of room tags; the keys are the tag name and the values + * are any metadata associated with the tag - e.g. { "fav" : { order: 1 } } + * @prop {object} accountData Dict of per-room account_data events; the keys are the + * event type and the values are the events. + * @prop {RoomState} oldState The state of the room at the time of the oldest + * event in the live timeline. Present for backwards compatibility - + * prefer getLiveTimeline().getState(true). + * @prop {RoomState} currentState The state of the room at the time of the + * newest event in the timeline. Present for backwards compatibility - + * prefer getLiveTimeline().getState(false). + * @prop {RoomSummary} summary The room summary. + * @prop {*} storageToken A token which a data store can use to remember + * the state of the room. + */ +function Room(roomId, opts) { + opts = opts || {}; + opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological"; + + this.reEmitter = new _ReEmitter2.default(this); + + if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) { + throw new Error("opts.pendingEventOrdering MUST be either 'chronological' or " + "'detached'. Got: '" + opts.pendingEventOrdering + "'"); + } + + this.roomId = roomId; + this.name = roomId; + this.tags = { + // $tagName: { $metadata: $value }, + // $tagName: { $metadata: $value }, + }; + this.accountData = { + // $eventType: $event + }; + this.summary = null; + this.storageToken = opts.storageToken; + this._opts = opts; + this._txnToEvent = {}; // Pending in-flight requests { string: MatrixEvent } + // receipts should clobber based on receipt_type and user_id pairs hence + // the form of this structure. This is sub-optimal for the exposed APIs + // which pass in an event ID and get back some receipts, so we also store + // a pre-cached list for this purpose. + this._receipts = { + // receipt_type: { + // user_id: { + // eventId: , + // data: + // } + // } + }; + this._receiptCacheByEventId = { + // $event_id: [{ + // type: $type, + // userId: $user_id, + // data: + // }] + }; + // only receipts that came from the server, not synthesized ones + this._realReceipts = {}; + + this._notificationCounts = {}; + + // all our per-room timeline sets. the first one is the unfiltered ones; + // the subsequent ones are the filtered ones in no particular order. + this._timelineSets = [new EventTimelineSet(this, opts)]; + this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), ["Room.timeline", "Room.timelineReset"]); + + this._fixUpLegacyTimelineFields(); + + // any filtered timeline sets we're maintaining for this room + this._filteredTimelineSets = { + // filter_id: timelineSet + }; + + if (this._opts.pendingEventOrdering == "detached") { + this._pendingEventList = []; + } + + // read by megolm; boolean value - null indicates "use global value" + this._blacklistUnverifiedDevices = null; +} +utils.inherits(Room, EventEmitter); + +/** + * Get the list of pending sent events for this room + * + * @return {module:models/event.MatrixEvent[]} A list of the sent events + * waiting for remote echo. + * + * @throws If opts.pendingEventOrdering was not 'detached' + */ +Room.prototype.getPendingEvents = function () { + if (this._opts.pendingEventOrdering !== "detached") { + throw new Error("Cannot call getPendingEventList with pendingEventOrdering == " + this._opts.pendingEventOrdering); + } + + return this._pendingEventList; +}; + +/** + * Get the live unfiltered timeline for this room. + * + * @return {module:models/event-timeline~EventTimeline} live timeline + */ +Room.prototype.getLiveTimeline = function () { + return this.getUnfilteredTimelineSet().getLiveTimeline(); +}; + +/** + * Reset the live timeline of all timelineSets, and start new ones. + * + *

This is used when /sync returns a 'limited' timeline. + * + * @param {string=} backPaginationToken token for back-paginating the new timeline + * @param {string=} forwardPaginationToken token for forward-paginating the old live timeline, + * if absent or null, all timelines are reset, removing old ones (including the previous live + * timeline which would otherwise be unable to paginate forwards without this token). + * Removing just the old live timeline whilst preserving previous ones is not supported. + */ +Room.prototype.resetLiveTimeline = function (backPaginationToken, forwardPaginationToken) { + for (var i = 0; i < this._timelineSets.length; i++) { + this._timelineSets[i].resetLiveTimeline(backPaginationToken, forwardPaginationToken); + } + + this._fixUpLegacyTimelineFields(); +}; + +/** + * Fix up this.timeline, this.oldState and this.currentState + * + * @private + */ +Room.prototype._fixUpLegacyTimelineFields = function () { + // maintain this.timeline as a reference to the live timeline, + // and this.oldState and this.currentState as references to the + // state at the start and end of that timeline. These are more + // for backwards-compatibility than anything else. + this.timeline = this.getLiveTimeline().getEvents(); + this.oldState = this.getLiveTimeline().getState(EventTimeline.BACKWARDS); + this.currentState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); +}; + +/** + * Return the timeline sets for this room. + * @return {EventTimelineSet[]} array of timeline sets for this room + */ +Room.prototype.getTimelineSets = function () { + return this._timelineSets; +}; + +/** + * Helper to return the main unfiltered timeline set for this room + * @return {EventTimelineSet} room's unfiltered timeline set + */ +Room.prototype.getUnfilteredTimelineSet = function () { + return this._timelineSets[0]; +}; + +/** + * Get the timeline which contains the given event from the unfiltered set, if any + * + * @param {string} eventId event ID to look for + * @return {?module:models/event-timeline~EventTimeline} timeline containing + * the given event, or null if unknown + */ +Room.prototype.getTimelineForEvent = function (eventId) { + return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId); +}; + +/** + * Add a new timeline to this room's unfiltered timeline set + * + * @return {module:models/event-timeline~EventTimeline} newly-created timeline + */ +Room.prototype.addTimeline = function () { + return this.getUnfilteredTimelineSet().addTimeline(); +}; + +/** + * Get an event which is stored in our unfiltered timeline set + * + * @param {string} eventId event ID to look for + * @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown + */ +Room.prototype.findEventById = function (eventId) { + return this.getUnfilteredTimelineSet().findEventById(eventId); +}; + +/** + * Get one of the notification counts for this room + * @param {String} type The type of notification count to get. default: 'total' + * @return {Number} The notification count, or undefined if there is no count + * for this type. + */ +Room.prototype.getUnreadNotificationCount = function (type) { + type = type || 'total'; + return this._notificationCounts[type]; +}; + +/** + * Set one of the notification counts for this room + * @param {String} type The type of notification count to set. + * @param {Number} count The new count + */ +Room.prototype.setUnreadNotificationCount = function (type, count) { + this._notificationCounts[type] = count; +}; + +/** + * Whether to send encrypted messages to devices within this room. + * @param {Boolean} value true to blacklist unverified devices, null + * to use the global value for this room. + */ +Room.prototype.setBlacklistUnverifiedDevices = function (value) { + this._blacklistUnverifiedDevices = value; +}; + +/** + * Whether to send encrypted messages to devices within this room. + * @return {Boolean} true if blacklisting unverified devices, null + * if the global value should be used for this room. + */ +Room.prototype.getBlacklistUnverifiedDevices = function () { + return this._blacklistUnverifiedDevices; +}; + +/** + * Get the avatar URL for a room if one was set. + * @param {String} baseUrl The homeserver base URL. See + * {@link module:client~MatrixClient#getHomeserverUrl}. + * @param {Number} width The desired width of the thumbnail. + * @param {Number} height The desired height of the thumbnail. + * @param {string} resizeMethod The thumbnail resize method to use, either + * "crop" or "scale". + * @param {boolean} allowDefault True to allow an identicon for this room if an + * avatar URL wasn't explicitly set. Default: true. + * @return {?string} the avatar URL or null. + */ +Room.prototype.getAvatarUrl = function (baseUrl, width, height, resizeMethod, allowDefault) { + var roomAvatarEvent = this.currentState.getStateEvents("m.room.avatar", ""); + if (allowDefault === undefined) { + allowDefault = true; + } + if (!roomAvatarEvent && !allowDefault) { + return null; + } + + var mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null; + if (mainUrl) { + return ContentRepo.getHttpUriForMxc(baseUrl, mainUrl, width, height, resizeMethod); + } else if (allowDefault) { + return ContentRepo.getIdenticonUri(baseUrl, this.roomId, width, height); + } + + return null; +}; + +/** + * Get the aliases this room has according to the room's state + * The aliases returned by this function may not necessarily + * still point to this room. + * @return {array} The room's alias as an array of strings + */ +Room.prototype.getAliases = function () { + var alias_strings = []; + + var alias_events = this.currentState.getStateEvents("m.room.aliases"); + if (alias_events) { + for (var i = 0; i < alias_events.length; ++i) { + var alias_event = alias_events[i]; + if (utils.isArray(alias_event.getContent().aliases)) { + Array.prototype.push.apply(alias_strings, alias_event.getContent().aliases); + } + } + } + return alias_strings; +}; + +/** + * Get this room's canonical alias + * The alias returned by this function may not necessarily + * still point to this room. + * @return {?string} The room's canonical alias, or null if there is none + */ +Room.prototype.getCanonicalAlias = function () { + var canonicalAlias = this.currentState.getStateEvents("m.room.canonical_alias", ""); + if (canonicalAlias) { + return canonicalAlias.getContent().alias; + } + return null; +}; + +/** + * Add events to a timeline + * + *

Will fire "Room.timeline" for each event added. + * + * @param {MatrixEvent[]} events A list of events to add. + * + * @param {boolean} toStartOfTimeline True to add these events to the start + * (oldest) instead of the end (newest) of the timeline. If true, the oldest + * event will be the last element of 'events'. + * + * @param {module:models/event-timeline~EventTimeline} timeline timeline to + * add events to. + * + * @param {string=} paginationToken token for the next batch of events + * + * @fires module:client~MatrixClient#event:"Room.timeline" + * + */ +Room.prototype.addEventsToTimeline = function (events, toStartOfTimeline, timeline, paginationToken) { + timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken); +}; + +/** + * Get a member from the current room state. + * @param {string} userId The user ID of the member. + * @return {RoomMember} The member or null. + */ +Room.prototype.getMember = function (userId) { + var member = this.currentState.members[userId]; + if (!member) { + return null; + } + return member; +}; + +/** + * Get a list of members whose membership state is "join". + * @return {RoomMember[]} A list of currently joined members. + */ +Room.prototype.getJoinedMembers = function () { + return this.getMembersWithMembership("join"); +}; + +/** + * Get a list of members with given membership state. + * @param {string} membership The membership state. + * @return {RoomMember[]} A list of members with the given membership state. + */ +Room.prototype.getMembersWithMembership = function (membership) { + return utils.filter(this.currentState.getMembers(), function (m) { + return m.membership === membership; + }); +}; + +/** + * Get the default room name (i.e. what a given user would see if the + * room had no m.room.name) + * @param {string} userId The userId from whose perspective we want + * to calculate the default name + * @return {string} The default room name + */ +Room.prototype.getDefaultRoomName = function (userId) { + return calculateRoomName(this, userId, true); +}; + +/** +* Check if the given user_id has the given membership state. +* @param {string} userId The user ID to check. +* @param {string} membership The membership e.g. 'join' +* @return {boolean} True if this user_id has the given membership state. +*/ +Room.prototype.hasMembershipState = function (userId, membership) { + var member = this.getMember(userId); + if (!member) { + return false; + } + return member.membership === membership; +}; + +/** + * Add a timelineSet for this room with the given filter + * @param {Filter} filter The filter to be applied to this timelineSet + * @return {EventTimelineSet} The timelineSet + */ +Room.prototype.getOrCreateFilteredTimelineSet = function (filter) { + if (this._filteredTimelineSets[filter.filterId]) { + return this._filteredTimelineSets[filter.filterId]; + } + var opts = (0, _assign2.default)({ filter: filter }, this._opts); + var timelineSet = new EventTimelineSet(this, opts); + this.reEmitter.reEmit(timelineSet, ["Room.timeline", "Room.timelineReset"]); + this._filteredTimelineSets[filter.filterId] = timelineSet; + this._timelineSets.push(timelineSet); + + // populate up the new timelineSet with filtered events from our live + // unfiltered timeline. + // + // XXX: This is risky as our timeline + // may have grown huge and so take a long time to filter. + // see https://github.com/vector-im/vector-web/issues/2109 + + var unfilteredLiveTimeline = this.getLiveTimeline(); + + unfilteredLiveTimeline.getEvents().forEach(function (event) { + timelineSet.addLiveEvent(event); + }); + + // find the earliest unfiltered timeline + var timeline = unfilteredLiveTimeline; + while (timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)) { + timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS); + } + + timelineSet.getLiveTimeline().setPaginationToken(timeline.getPaginationToken(EventTimeline.BACKWARDS), EventTimeline.BACKWARDS); + + // alternatively, we could try to do something like this to try and re-paginate + // in the filtered events from nothing, but Mark says it's an abuse of the API + // to do so: + // + // timelineSet.resetLiveTimeline( + // unfilteredLiveTimeline.getPaginationToken(EventTimeline.FORWARDS) + // ); + + return timelineSet; +}; + +/** + * Forget the timelineSet for this room with the given filter + * + * @param {Filter} filter the filter whose timelineSet is to be forgotten + */ +Room.prototype.removeFilteredTimelineSet = function (filter) { + var timelineSet = this._filteredTimelineSets[filter.filterId]; + delete this._filteredTimelineSets[filter.filterId]; + var i = this._timelineSets.indexOf(timelineSet); + if (i > -1) { + this._timelineSets.splice(i, 1); + } +}; + +/** + * Add an event to the end of this room's live timelines. Will fire + * "Room.timeline". + * + * @param {MatrixEvent} event Event to be added + * @param {string?} duplicateStrategy 'ignore' or 'replace' + * @fires module:client~MatrixClient#event:"Room.timeline" + * @private + */ +Room.prototype._addLiveEvent = function (event, duplicateStrategy) { + var i = void 0; + if (event.getType() === "m.room.redaction") { + var redactId = event.event.redacts; + + // if we know about this event, redact its contents now. + var redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId); + if (redactedEvent) { + redactedEvent.makeRedacted(event); + this.emit("Room.redaction", event, this); + + // TODO: we stash user displaynames (among other things) in + // RoomMember objects which are then attached to other events + // (in the sender and target fields). We should get those + // RoomMember objects to update themselves when the events that + // they are based on are changed. + } + + // FIXME: apply redactions to notification list + + // NB: We continue to add the redaction event to the timeline so + // clients can say "so and so redacted an event" if they wish to. Also + // this may be needed to trigger an update. + } + + if (event.getUnsigned().transaction_id) { + var existingEvent = this._txnToEvent[event.getUnsigned().transaction_id]; + if (existingEvent) { + // remote echo of an event we sent earlier + this._handleRemoteEcho(event, existingEvent); + return; + } + } + + // add to our timeline sets + for (i = 0; i < this._timelineSets.length; i++) { + this._timelineSets[i].addLiveEvent(event, duplicateStrategy); + } + + // synthesize and inject implicit read receipts + // Done after adding the event because otherwise the app would get a read receipt + // pointing to an event that wasn't yet in the timeline + // Don't synthesize RR for m.room.redaction as this causes the RR to go missing. + if (event.sender && event.getType() !== "m.room.redaction") { + this.addReceipt(synthesizeReceipt(event.sender.userId, event, "m.read"), true); + + // Any live events from a user could be taken as implicit + // presence information: evidence that they are currently active. + // ...except in a world where we use 'user.currentlyActive' to reduce + // presence spam, this isn't very useful - we'll get a transition when + // they are no longer currently active anyway. So don't bother to + // reset the lastActiveAgo and lastPresenceTs from the RoomState's user. + } +}; + +/** + * Add a pending outgoing event to this room. + * + *

The event is added to either the pendingEventList, or the live timeline, + * depending on the setting of opts.pendingEventOrdering. + * + *

This is an internal method, intended for use by MatrixClient. + * + * @param {module:models/event.MatrixEvent} event The event to add. + * + * @param {string} txnId Transaction id for this outgoing event + * + * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" + * + * @throws if the event doesn't have status SENDING, or we aren't given a + * unique transaction id. + */ +Room.prototype.addPendingEvent = function (event, txnId) { + if (event.status !== EventStatus.SENDING) { + throw new Error("addPendingEvent called on an event with status " + event.status); + } + + if (this._txnToEvent[txnId]) { + throw new Error("addPendingEvent called on an event with known txnId " + txnId); + } + + // call setEventMetadata to set up event.sender etc + // as event is shared over all timelineSets, we set up its metadata based + // on the unfiltered timelineSet. + EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false); + + this._txnToEvent[txnId] = event; + + if (this._opts.pendingEventOrdering == "detached") { + this._pendingEventList.push(event); + } else { + for (var i = 0; i < this._timelineSets.length; i++) { + var timelineSet = this._timelineSets[i]; + if (timelineSet.getFilter()) { + if (this._filter.filterRoomTimeline([event]).length) { + timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), false); + } + } else { + timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), false); + } + } + } + + this.emit("Room.localEchoUpdated", event, this, null, null); +}; + +/** + * Deal with the echo of a message we sent. + * + *

We move the event to the live timeline if it isn't there already, and + * update it. + * + * @param {module:models/event.MatrixEvent} remoteEvent The event received from + * /sync + * @param {module:models/event.MatrixEvent} localEvent The local echo, which + * should be either in the _pendingEventList or the timeline. + * + * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" + * @private + */ +Room.prototype._handleRemoteEcho = function (remoteEvent, localEvent) { + var oldEventId = localEvent.getId(); + var newEventId = remoteEvent.getId(); + var oldStatus = localEvent.status; + + // no longer pending + delete this._txnToEvent[remoteEvent.transaction_id]; + + // if it's in the pending list, remove it + if (this._pendingEventList) { + utils.removeElement(this._pendingEventList, function (ev) { + return ev.getId() == oldEventId; + }, false); + } + + // replace the event source (this will preserve the plaintext payload if + // any, which is good, because we don't want to try decoding it again). + localEvent.handleRemoteEcho(remoteEvent.event); + + for (var i = 0; i < this._timelineSets.length; i++) { + var timelineSet = this._timelineSets[i]; + + // if it's already in the timeline, update the timeline map. If it's not, add it. + timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId); + } + + this.emit("Room.localEchoUpdated", localEvent, this, oldEventId, oldStatus); +}; + +/* a map from current event status to a list of allowed next statuses + */ +var ALLOWED_TRANSITIONS = {}; + +ALLOWED_TRANSITIONS[EventStatus.ENCRYPTING] = [EventStatus.SENDING, EventStatus.NOT_SENT]; + +ALLOWED_TRANSITIONS[EventStatus.SENDING] = [EventStatus.ENCRYPTING, EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.SENT]; + +ALLOWED_TRANSITIONS[EventStatus.QUEUED] = [EventStatus.SENDING, EventStatus.CANCELLED]; + +ALLOWED_TRANSITIONS[EventStatus.SENT] = []; + +ALLOWED_TRANSITIONS[EventStatus.NOT_SENT] = [EventStatus.SENDING, EventStatus.QUEUED, EventStatus.CANCELLED]; + +ALLOWED_TRANSITIONS[EventStatus.CANCELLED] = []; + +/** + * Update the status / event id on a pending event, to reflect its transmission + * progress. + * + *

This is an internal method. + * + * @param {MatrixEvent} event local echo event + * @param {EventStatus} newStatus status to assign + * @param {string} newEventId new event id to assign. Ignored unless + * newStatus == EventStatus.SENT. + * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" + */ +Room.prototype.updatePendingEvent = function (event, newStatus, newEventId) { + console.log("setting pendingEvent status to " + newStatus + " in " + event.getRoomId()); + + // if the message was sent, we expect an event id + if (newStatus == EventStatus.SENT && !newEventId) { + throw new Error("updatePendingEvent called with status=SENT, " + "but no new event id"); + } + + // SENT races against /sync, so we have to special-case it. + if (newStatus == EventStatus.SENT) { + var timeline = this.getUnfilteredTimelineSet().eventIdToTimeline(newEventId); + if (timeline) { + // we've already received the event via the event stream. + // nothing more to do here. + return; + } + } + + var oldStatus = event.status; + var oldEventId = event.getId(); + + if (!oldStatus) { + throw new Error("updatePendingEventStatus called on an event which is " + "not a local echo."); + } + + var allowed = ALLOWED_TRANSITIONS[oldStatus]; + if (!allowed || allowed.indexOf(newStatus) < 0) { + throw new Error("Invalid EventStatus transition " + oldStatus + "->" + newStatus); + } + + event.status = newStatus; + + if (newStatus == EventStatus.SENT) { + // update the event id + event.event.event_id = newEventId; + + // if the event was already in the timeline (which will be the case if + // opts.pendingEventOrdering==chronological), we need to update the + // timeline map. + for (var i = 0; i < this._timelineSets.length; i++) { + this._timelineSets[i].replaceEventId(oldEventId, newEventId); + } + } else if (newStatus == EventStatus.CANCELLED) { + // remove it from the pending event list, or the timeline. + if (this._pendingEventList) { + utils.removeElement(this._pendingEventList, function (ev) { + return ev.getId() == oldEventId; + }, false); + } + this.removeEvent(oldEventId); + } + + this.emit("Room.localEchoUpdated", event, this, event.getId(), oldStatus); +}; + +/** + * Add some events to this room. This can include state events, message + * events and typing notifications. These events are treated as "live" so + * they will go to the end of the timeline. + * + * @param {MatrixEvent[]} events A list of events to add. + * + * @param {string} duplicateStrategy Optional. Applies to events in the + * timeline only. If this is 'replace' then if a duplicate is encountered, the + * event passed to this function will replace the existing event in the + * timeline. If this is not specified, or is 'ignore', then the event passed to + * this function will be ignored entirely, preserving the existing event in the + * timeline. Events are identical based on their event ID only. + * + * @throws If duplicateStrategy is not falsey, 'replace' or 'ignore'. + */ +Room.prototype.addLiveEvents = function (events, duplicateStrategy) { + var i = void 0; + if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) { + throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'"); + } + + // sanity check that the live timeline is still live + for (i = 0; i < this._timelineSets.length; i++) { + var liveTimeline = this._timelineSets[i].getLiveTimeline(); + if (liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) { + throw new Error("live timeline " + i + " is no longer live - it has a pagination token " + "(" + liveTimeline.getPaginationToken(EventTimeline.FORWARDS) + ")"); + } + if (liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) { + throw new Error("live timeline " + i + " is no longer live - " + "it has a neighbouring timeline"); + } + } + + for (i = 0; i < events.length; i++) { + if (events[i].getType() === "m.typing") { + this.currentState.setTypingEvent(events[i]); + } else if (events[i].getType() === "m.receipt") { + this.addReceipt(events[i]); + } + // N.B. account_data is added directly by /sync to avoid + // having to maintain an event.isAccountData() here + else { + // TODO: We should have a filter to say "only add state event + // types X Y Z to the timeline". + this._addLiveEvent(events[i], duplicateStrategy); + } + } +}; + +/** + * Removes events from this room. + * @param {String[]} event_ids A list of event_ids to remove. + */ +Room.prototype.removeEvents = function (event_ids) { + for (var i = 0; i < event_ids.length; ++i) { + this.removeEvent(event_ids[i]); + } +}; + +/** + * Removes a single event from this room. + * + * @param {String} eventId The id of the event to remove + * + * @return {bool} true if the event was removed from any of the room's timeline sets + */ +Room.prototype.removeEvent = function (eventId) { + var removedAny = false; + for (var i = 0; i < this._timelineSets.length; i++) { + var removed = this._timelineSets[i].removeEvent(eventId); + if (removed) { + removedAny = true; + } + } + return removedAny; +}; + +/** + * Recalculate various aspects of the room, including the room name and + * room summary. Call this any time the room's current state is modified. + * May fire "Room.name" if the room name is updated. + * @param {string} userId The client's user ID. + * @fires module:client~MatrixClient#event:"Room.name" + */ +Room.prototype.recalculate = function (userId) { + // set fake stripped state events if this is an invite room so logic remains + // consistent elsewhere. + var self = this; + var membershipEvent = this.currentState.getStateEvents("m.room.member", userId); + if (membershipEvent && membershipEvent.getContent().membership === "invite") { + var strippedStateEvents = membershipEvent.event.invite_room_state || []; + utils.forEach(strippedStateEvents, function (strippedEvent) { + var existingEvent = self.currentState.getStateEvents(strippedEvent.type, strippedEvent.state_key); + if (!existingEvent) { + // set the fake stripped event instead + self.currentState.setStateEvents([new MatrixEvent({ + type: strippedEvent.type, + state_key: strippedEvent.state_key, + content: strippedEvent.content, + event_id: "$fake" + Date.now(), + room_id: self.roomId, + user_id: userId // technically a lie + })]); + } + }); + } + + var oldName = this.name; + this.name = calculateRoomName(this, userId); + this.summary = new RoomSummary(this.roomId, { + title: this.name + }); + + if (oldName !== this.name) { + this.emit("Room.name", this); + } +}; + +/** + * Get a list of user IDs who have read up to the given event. + * @param {MatrixEvent} event the event to get read receipts for. + * @return {String[]} A list of user IDs. + */ +Room.prototype.getUsersReadUpTo = function (event) { + return this.getReceiptsForEvent(event).filter(function (receipt) { + return receipt.type === "m.read"; + }).map(function (receipt) { + return receipt.userId; + }); +}; + +/** + * Get the ID of the event that a given user has read up to, or null if we + * have received no read receipts from them. + * @param {String} userId The user ID to get read receipt event ID for + * @param {Boolean} ignoreSynthesized If true, return only receipts that have been + * sent by the server, not implicit ones generated + * by the JS SDK. + * @return {String} ID of the latest event that the given user has read, or null. + */ +Room.prototype.getEventReadUpTo = function (userId, ignoreSynthesized) { + var receipts = this._receipts; + if (ignoreSynthesized) { + receipts = this._realReceipts; + } + + if (receipts["m.read"] === undefined || receipts["m.read"][userId] === undefined) { + return null; + } + + return receipts["m.read"][userId].eventId; +}; + +/** + * Get a list of receipts for the given event. + * @param {MatrixEvent} event the event to get receipts for + * @return {Object[]} A list of receipts with a userId, type and data keys or + * an empty list. + */ +Room.prototype.getReceiptsForEvent = function (event) { + return this._receiptCacheByEventId[event.getId()] || []; +}; + +/** + * Add a receipt event to the room. + * @param {MatrixEvent} event The m.receipt event. + * @param {Boolean} fake True if this event is implicit + */ +Room.prototype.addReceipt = function (event, fake) { + // event content looks like: + // content: { + // $event_id: { + // $receipt_type: { + // $user_id: { + // ts: $timestamp + // } + // } + // } + // } + if (fake === undefined) { + fake = false; + } + if (!fake) { + this._addReceiptsToStructure(event, this._realReceipts); + // we don't bother caching real receipts by event ID + // as there's nothing that would read it. + } + this._addReceiptsToStructure(event, this._receipts); + this._receiptCacheByEventId = this._buildReceiptCache(this._receipts); + + // send events after we've regenerated the cache, otherwise things that + // listened for the event would read from a stale cache + this.emit("Room.receipt", event, this); +}; + +/** + * Add a receipt event to the room. + * @param {MatrixEvent} event The m.receipt event. + * @param {Object} receipts The object to add receipts to + */ +Room.prototype._addReceiptsToStructure = function (event, receipts) { + var self = this; + utils.keys(event.getContent()).forEach(function (eventId) { + utils.keys(event.getContent()[eventId]).forEach(function (receiptType) { + utils.keys(event.getContent()[eventId][receiptType]).forEach(function (userId) { + var receipt = event.getContent()[eventId][receiptType][userId]; + + if (!receipts[receiptType]) { + receipts[receiptType] = {}; + } + + var existingReceipt = receipts[receiptType][userId]; + + if (!existingReceipt) { + receipts[receiptType][userId] = {}; + } else { + // we only want to add this receipt if we think it is later + // than the one we already have. (This is managed + // server-side, but because we synthesize RRs locally we + // have to do it here too.) + var ordering = self.getUnfilteredTimelineSet().compareEventOrdering(existingReceipt.eventId, eventId); + if (ordering !== null && ordering >= 0) { + return; + } + } + + receipts[receiptType][userId] = { + eventId: eventId, + data: receipt + }; + }); + }); + }); +}; + +/** + * Build and return a map of receipts by event ID + * @param {Object} receipts A map of receipts + * @return {Object} Map of receipts by event ID + */ +Room.prototype._buildReceiptCache = function (receipts) { + var receiptCacheByEventId = {}; + utils.keys(receipts).forEach(function (receiptType) { + utils.keys(receipts[receiptType]).forEach(function (userId) { + var receipt = receipts[receiptType][userId]; + if (!receiptCacheByEventId[receipt.eventId]) { + receiptCacheByEventId[receipt.eventId] = []; + } + receiptCacheByEventId[receipt.eventId].push({ + userId: userId, + type: receiptType, + data: receipt.data + }); + }); + }); + return receiptCacheByEventId; +}; + +/** + * Add a temporary local-echo receipt to the room to reflect in the + * client the fact that we've sent one. + * @param {string} userId The user ID if the receipt sender + * @param {MatrixEvent} e The event that is to be acknowledged + * @param {string} receiptType The type of receipt + */ +Room.prototype._addLocalEchoReceipt = function (userId, e, receiptType) { + this.addReceipt(synthesizeReceipt(userId, e, receiptType), true); +}; + +/** + * Update the room-tag event for the room. The previous one is overwritten. + * @param {MatrixEvent} event the m.tag event + */ +Room.prototype.addTags = function (event) { + // event content looks like: + // content: { + // tags: { + // $tagName: { $metadata: $value }, + // $tagName: { $metadata: $value }, + // } + // } + + // XXX: do we need to deep copy here? + this.tags = event.getContent().tags; + + // XXX: we could do a deep-comparison to see if the tags have really + // changed - but do we want to bother? + this.emit("Room.tags", event, this); +}; + +/** + * Update the account_data events for this room, overwriting events of the same type. + * @param {Array} events an array of account_data events to add + */ +Room.prototype.addAccountData = function (events) { + for (var i = 0; i < events.length; i++) { + var event = events[i]; + if (event.getType() === "m.tag") { + this.addTags(event); + } + this.accountData[event.getType()] = event; + this.emit("Room.accountData", event, this); + } +}; + +/** + * Access account_data event of given event type for this room + * @param {string} type the type of account_data event to be accessed + * @return {?MatrixEvent} the account_data event in question + */ +Room.prototype.getAccountData = function (type) { + return this.accountData[type]; +}; + +/** + * This is an internal method. Calculates the name of the room from the current + * room state. + * @param {Room} room The matrix room. + * @param {string} userId The client's user ID. Used to filter room members + * correctly. + * @param {bool} ignoreRoomNameEvent Return the implicit room name that we'd see if there + * was no m.room.name event. + * @return {string} The calculated room name. + */ +function calculateRoomName(room, userId, ignoreRoomNameEvent) { + if (!ignoreRoomNameEvent) { + // check for an alias, if any. for now, assume first alias is the + // official one. + var mRoomName = room.currentState.getStateEvents("m.room.name", ""); + if (mRoomName && mRoomName.getContent() && mRoomName.getContent().name) { + return mRoomName.getContent().name; + } + } + + var alias = room.getCanonicalAlias(); + + if (!alias) { + var aliases = room.getAliases(); + + if (aliases.length) { + alias = aliases[0]; + } + } + if (alias) { + return alias; + } + + // get members that are NOT ourselves and are actually in the room. + var otherMembers = utils.filter(room.currentState.getMembers(), function (m) { + return m.userId !== userId && m.membership !== "leave" && m.membership !== "ban"; + }); + var allMembers = utils.filter(room.currentState.getMembers(), function (m) { + return m.membership !== "leave"; + }); + var myMemberEventArray = utils.filter(room.currentState.getMembers(), function (m) { + return m.userId == userId; + }); + var myMemberEvent = myMemberEventArray.length && myMemberEventArray[0].events ? myMemberEventArray[0].events.member.event : undefined; + + // TODO: Localisation + if (myMemberEvent && myMemberEvent.content.membership == "invite") { + if (room.currentState.getMember(myMemberEvent.sender)) { + // extract who invited us to the room + return room.currentState.getMember(myMemberEvent.sender).name; + } else if (allMembers[0].events.member) { + // use the sender field from the invite event, although this only + // gets us the mxid + return myMemberEvent.sender; + } else { + return "Room Invite"; + } + } + + if (otherMembers.length === 0) { + var leftMembers = utils.filter(room.currentState.getMembers(), function (m) { + return m.userId !== userId && m.membership === "leave"; + }); + if (allMembers.length === 1) { + // self-chat, peeked room with 1 participant, + // or inbound invite, or outbound 3PID invite. + if (allMembers[0].userId === userId) { + var thirdPartyInvites = room.currentState.getStateEvents("m.room.third_party_invite"); + if (thirdPartyInvites && thirdPartyInvites.length > 0) { + var name = "Inviting " + thirdPartyInvites[0].getContent().display_name; + if (thirdPartyInvites.length > 1) { + if (thirdPartyInvites.length == 2) { + name += " and " + thirdPartyInvites[1].getContent().display_name; + } else { + name += " and " + thirdPartyInvites.length + " others"; + } + } + return name; + } else if (leftMembers.length === 1) { + // if it was a chat with one person who's now left, it's still + // notionally a chat with them + return leftMembers[0].name; + } else { + return "Empty room"; + } + } else { + return allMembers[0].name; + } + } else { + // there really isn't anyone in this room... + return "Empty room"; + } + } else if (otherMembers.length === 1) { + return otherMembers[0].name; + } else if (otherMembers.length === 2) { + return otherMembers[0].name + " and " + otherMembers[1].name; + } else { + return otherMembers[0].name + " and " + (otherMembers.length - 1) + " others"; + } +} + +/** + * The Room class. + */ +module.exports = Room; + +/** + * Fires when an event we had previously received is redacted. + * + * (Note this is *not* fired when the redaction happens before we receive the + * event). + * + * @event module:client~MatrixClient#"Room.redaction" + * @param {MatrixEvent} event The matrix event which was redacted + * @param {Room} room The room containing the redacted event + */ + +/** + * Fires whenever the name of a room is updated. + * @event module:client~MatrixClient#"Room.name" + * @param {Room} room The room whose Room.name was updated. + * @example + * matrixClient.on("Room.name", function(room){ + * var newName = room.name; + * }); + */ + +/** + * Fires whenever a receipt is received for a room + * @event module:client~MatrixClient#"Room.receipt" + * @param {event} event The receipt event + * @param {Room} room The room whose receipts was updated. + * @example + * matrixClient.on("Room.receipt", function(event, room){ + * var receiptContent = event.getContent(); + * }); + */ + +/** + * Fires whenever a room's tags are updated. + * @event module:client~MatrixClient#"Room.tags" + * @param {event} event The tags event + * @param {Room} room The room whose Room.tags was updated. + * @example + * matrixClient.on("Room.tags", function(event, room){ + * var newTags = event.getContent().tags; + * if (newTags["favourite"]) showStar(room); + * }); + */ + +/** + * Fires whenever a room's account_data is updated. + * @event module:client~MatrixClient#"Room.accountData" + * @param {event} event The account_data event + * @param {Room} room The room whose account_data was updated. + * @example + * matrixClient.on("Room.accountData", function(event, room){ + * if (event.getType() === "m.room.colorscheme") { + * applyColorScheme(event.getContents()); + * } + * }); + */ + +/** + * Fires when the status of a transmitted event is updated. + * + *

When an event is first transmitted, a temporary copy of the event is + * inserted into the timeline, with a temporary event id, and a status of + * 'SENDING'. + * + *

Once the echo comes back from the server, the content of the event + * (MatrixEvent.event) is replaced by the complete event from the homeserver, + * thus updating its event id, as well as server-generated fields such as the + * timestamp. Its status is set to null. + * + *

Once the /send request completes, if the remote echo has not already + * arrived, the event is updated with a new event id and the status is set to + * 'SENT'. The server-generated fields are of course not updated yet. + * + *

If the /send fails, In this case, the event's status is set to + * 'NOT_SENT'. If it is later resent, the process starts again, setting the + * status to 'SENDING'. Alternatively, the message may be cancelled, which + * removes the event from the room, and sets the status to 'CANCELLED'. + * + *

This event is raised to reflect each of the transitions above. + * + * @event module:client~MatrixClient#"Room.localEchoUpdated" + * + * @param {MatrixEvent} event The matrix event which has been updated + * + * @param {Room} room The room containing the redacted event + * + * @param {string} oldEventId The previous event id (the temporary event id, + * except when updating a successfully-sent event when its echo arrives) + * + * @param {EventStatus} oldStatus The previous event status. + */ + +},{"../ReEmitter":2,"../content-repo":6,"../utils":50,"./event":30,"./event-timeline":29,"./event-timeline-set":28,"./room-summary":34,"babel-runtime/core-js/object/assign":56,"events":186}],36:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** + * @module models/search-result + */ + +var EventContext = require("./event-context"); +var utils = require("../utils"); + +/** + * Construct a new SearchResult + * + * @param {number} rank where this SearchResult ranks in the results + * @param {event-context.EventContext} eventContext the matching event and its + * context + * + * @constructor + */ +function SearchResult(rank, eventContext) { + this.rank = rank; + this.context = eventContext; +} + +/** + * Create a SearchResponse from the response to /search + * @static + * @param {Object} jsonObj + * @param {function} eventMapper + * @return {SearchResult} + */ + +SearchResult.fromJson = function (jsonObj, eventMapper) { + var jsonContext = jsonObj.context || {}; + var events_before = jsonContext.events_before || []; + var events_after = jsonContext.events_after || []; + + var context = new EventContext(eventMapper(jsonObj.result)); + + context.setPaginateToken(jsonContext.start, true); + context.addEvents(utils.map(events_before, eventMapper), true); + context.addEvents(utils.map(events_after, eventMapper), false); + context.setPaginateToken(jsonContext.end, false); + + return new SearchResult(jsonObj.rank, context); +}; + +/** + * The SearchResult class + */ +module.exports = SearchResult; + +},{"../utils":50,"./event-context":27}],37:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; +/** + * @module models/user + */ + +var EventEmitter = require("events").EventEmitter; +var utils = require("../utils"); + +/** + * Construct a new User. A User must have an ID and can optionally have extra + * information associated with it. + * @constructor + * @param {string} userId Required. The ID of this user. + * @prop {string} userId The ID of the user. + * @prop {Object} info The info object supplied in the constructor. + * @prop {string} displayName The 'displayname' of the user if known. + * @prop {string} avatarUrl The 'avatar_url' of the user if known. + * @prop {string} presence The presence enum if known. + * @prop {string} presenceStatusMsg The presence status message if known. + * @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted + * proactively with the server, or we saw a message from the user + * @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last + * received presence data for this user. We can subtract + * lastActiveAgo from this to approximate an absolute value for + * when a user was last active. + * @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be + * an approximation and that the user should be seen as active 'now' + * @prop {Object} events The events describing this user. + * @prop {MatrixEvent} events.presence The m.presence event for this user. + */ +function User(userId) { + this.userId = userId; + this.presence = "offline"; + this.presenceStatusMsg = null; + this.displayName = userId; + this.rawDisplayName = userId; + this.avatarUrl = null; + this.lastActiveAgo = 0; + this.lastPresenceTs = 0; + this.currentlyActive = false; + this.events = { + presence: null, + profile: null + }; + this._updateModifiedTime(); +} +utils.inherits(User, EventEmitter); + +/** + * Update this User with the given presence event. May fire "User.presence", + * "User.avatarUrl" and/or "User.displayName" if this event updates this user's + * properties. + * @param {MatrixEvent} event The m.presence event. + * @fires module:client~MatrixClient#event:"User.presence" + * @fires module:client~MatrixClient#event:"User.displayName" + * @fires module:client~MatrixClient#event:"User.avatarUrl" + */ +User.prototype.setPresenceEvent = function (event) { + if (event.getType() !== "m.presence") { + return; + } + var firstFire = this.events.presence === null; + this.events.presence = event; + + var eventsToFire = []; + if (event.getContent().presence !== this.presence || firstFire) { + eventsToFire.push("User.presence"); + } + if (event.getContent().avatar_url && event.getContent().avatar_url !== this.avatarUrl) { + eventsToFire.push("User.avatarUrl"); + } + if (event.getContent().displayname && event.getContent().displayname !== this.displayName) { + eventsToFire.push("User.displayName"); + } + if (event.getContent().currently_active !== undefined && event.getContent().currently_active !== this.currentlyActive) { + eventsToFire.push("User.currentlyActive"); + } + + this.presence = event.getContent().presence; + eventsToFire.push("User.lastPresenceTs"); + + if (event.getContent().status_msg) { + this.presenceStatusMsg = event.getContent().status_msg; + } + if (event.getContent().displayname) { + this.displayName = event.getContent().displayname; + } + if (event.getContent().avatar_url) { + this.avatarUrl = event.getContent().avatar_url; + } + this.lastActiveAgo = event.getContent().last_active_ago; + this.lastPresenceTs = Date.now(); + this.currentlyActive = event.getContent().currently_active; + + this._updateModifiedTime(); + + for (var i = 0; i < eventsToFire.length; i++) { + this.emit(eventsToFire[i], event, this); + } +}; + +/** + * Manually set this user's display name. No event is emitted in response to this + * as there is no underlying MatrixEvent to emit with. + * @param {string} name The new display name. + */ +User.prototype.setDisplayName = function (name) { + var oldName = this.displayName; + this.displayName = name; + if (name !== oldName) { + this._updateModifiedTime(); + } +}; + +/** + * Manually set this user's non-disambiguated display name. No event is emitted + * in response to this as there is no underlying MatrixEvent to emit with. + * @param {string} name The new display name. + */ +User.prototype.setRawDisplayName = function (name) { + this.rawDisplayName = name; +}; + +/** + * Manually set this user's avatar URL. No event is emitted in response to this + * as there is no underlying MatrixEvent to emit with. + * @param {string} url The new avatar URL. + */ +User.prototype.setAvatarUrl = function (url) { + var oldUrl = this.avatarUrl; + this.avatarUrl = url; + if (url !== oldUrl) { + this._updateModifiedTime(); + } +}; + +/** + * Update the last modified time to the current time. + */ +User.prototype._updateModifiedTime = function () { + this._modified = Date.now(); +}; + +/** + * Get the timestamp when this User was last updated. This timestamp is + * updated when this User receives a new Presence event which has updated a + * property on this object. It is updated before firing events. + * @return {number} The timestamp + */ +User.prototype.getLastModifiedTime = function () { + return this._modified; +}; + +/** + * Get the absolute timestamp when this User was last known active on the server. + * It is *NOT* accurate if this.currentlyActive is true. + * @return {number} The timestamp + */ +User.prototype.getLastActiveTs = function () { + return this.lastPresenceTs - this.lastActiveAgo; +}; + +/** + * The User class. + */ +module.exports = User; + +/** + * Fires whenever any user's lastPresenceTs changes, + * ie. whenever any presence event is received for a user. + * @event module:client~MatrixClient#"User.lastPresenceTs" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {User} user The user whose User.lastPresenceTs changed. + * @example + * matrixClient.on("User.lastPresenceTs", function(event, user){ + * var newlastPresenceTs = user.lastPresenceTs; + * }); + */ + +/** + * Fires whenever any user's presence changes. + * @event module:client~MatrixClient#"User.presence" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {User} user The user whose User.presence changed. + * @example + * matrixClient.on("User.presence", function(event, user){ + * var newPresence = user.presence; + * }); + */ + +/** + * Fires whenever any user's currentlyActive changes. + * @event module:client~MatrixClient#"User.currentlyActive" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {User} user The user whose User.currentlyActive changed. + * @example + * matrixClient.on("User.currentlyActive", function(event, user){ + * var newCurrentlyActive = user.currentlyActive; + * }); + */ + +/** + * Fires whenever any user's display name changes. + * @event module:client~MatrixClient#"User.displayName" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {User} user The user whose User.displayName changed. + * @example + * matrixClient.on("User.displayName", function(event, user){ + * var newName = user.displayName; + * }); + */ + +/** + * Fires whenever any user's avatar URL changes. + * @event module:client~MatrixClient#"User.avatarUrl" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {User} user The user whose User.avatarUrl changed. + * @example + * matrixClient.on("User.avatarUrl", function(event, user){ + * var newUrl = user.avatarUrl; + * }); + */ + +},{"../utils":50,"events":186}],38:[function(require,module,exports){ +'use strict'; + +var _typeof2 = require('babel-runtime/helpers/typeof'); + +var _typeof3 = _interopRequireDefault(_typeof2); + +var _getIterator2 = require('babel-runtime/core-js/get-iterator'); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +var _keys = require('babel-runtime/core-js/object/keys'); + +var _keys2 = _interopRequireDefault(_keys); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +/** + * @module pushprocessor + */ + +var RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride']; + +/** + * Construct a Push Processor. + * @constructor + * @param {Object} client The Matrix client object to use + */ +function PushProcessor(client) { + var _this = this; + + var escapeRegExp = function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + }; + + var cachedGlobToRegex = { + // $glob: RegExp, + }; + + var matchingRuleFromKindSet = function matchingRuleFromKindSet(ev, kindset, device) { + for (var ruleKindIndex = 0; ruleKindIndex < RULEKINDS_IN_ORDER.length; ++ruleKindIndex) { + var kind = RULEKINDS_IN_ORDER[ruleKindIndex]; + var ruleset = kindset[kind]; + + for (var ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) { + var rule = ruleset[ruleIndex]; + if (!rule.enabled) { + continue; + } + + var rawrule = templateRuleToRaw(kind, rule, device); + if (!rawrule) { + continue; + } + + if (_this.ruleMatchesEvent(rawrule, ev)) { + rule.kind = kind; + return rule; + } + } + } + return null; + }; + + var templateRuleToRaw = function templateRuleToRaw(kind, tprule, device) { + var rawrule = { + 'rule_id': tprule.rule_id, + 'actions': tprule.actions, + 'conditions': [] + }; + switch (kind) { + case 'underride': + case 'override': + rawrule.conditions = tprule.conditions; + break; + case 'room': + if (!tprule.rule_id) { + return null; + } + rawrule.conditions.push({ + 'kind': 'event_match', + 'key': 'room_id', + 'value': tprule.rule_id + }); + break; + case 'sender': + if (!tprule.rule_id) { + return null; + } + rawrule.conditions.push({ + 'kind': 'event_match', + 'key': 'user_id', + 'value': tprule.rule_id + }); + break; + case 'content': + if (!tprule.pattern) { + return null; + } + rawrule.conditions.push({ + 'kind': 'event_match', + 'key': 'content.body', + 'pattern': tprule.pattern + }); + break; + } + if (device) { + rawrule.conditions.push({ + 'kind': 'device', + 'profile_tag': device + }); + } + return rawrule; + }; + + var eventFulfillsCondition = function eventFulfillsCondition(cond, ev) { + var condition_functions = { + "event_match": eventFulfillsEventMatchCondition, + "device": eventFulfillsDeviceCondition, + "contains_display_name": eventFulfillsDisplayNameCondition, + "room_member_count": eventFulfillsRoomMemberCountCondition, + "sender_notification_permission": eventFulfillsSenderNotifPermCondition + }; + if (condition_functions[cond.kind]) { + return condition_functions[cond.kind](cond, ev); + } + // unknown conditions: we previously matched all unknown conditions, + // but given that rules can be added to the base rules on a server, + // it's probably better to not match unknown conditions. + return false; + }; + + var eventFulfillsSenderNotifPermCondition = function eventFulfillsSenderNotifPermCondition(cond, ev) { + var notifLevelKey = cond['key']; + if (!notifLevelKey) { + return false; + } + + var room = client.getRoom(ev.getRoomId()); + if (!room || !room.currentState) { + return false; + } + + // Note that this should not be the current state of the room but the state at + // the point the event is in the DAG. Unfortunately the js-sdk does not store + // this. + return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender()); + }; + + var eventFulfillsRoomMemberCountCondition = function eventFulfillsRoomMemberCountCondition(cond, ev) { + if (!cond.is) { + return false; + } + + var room = client.getRoom(ev.getRoomId()); + if (!room || !room.currentState || !room.currentState.members) { + return false; + } + + var memberCount = room.currentState.getJoinedMemberCount(); + + var m = cond.is.match(/^([=<>]*)([0-9]*)$/); + if (!m) { + return false; + } + var ineq = m[1]; + var rhs = parseInt(m[2]); + if (isNaN(rhs)) { + return false; + } + switch (ineq) { + case '': + case '==': + return memberCount == rhs; + case '<': + return memberCount < rhs; + case '>': + return memberCount > rhs; + case '<=': + return memberCount <= rhs; + case '>=': + return memberCount >= rhs; + default: + return false; + } + }; + + var eventFulfillsDisplayNameCondition = function eventFulfillsDisplayNameCondition(cond, ev) { + var content = ev.getContent(); + if (!content || !content.body || typeof content.body != 'string') { + return false; + } + + var room = client.getRoom(ev.getRoomId()); + if (!room || !room.currentState || !room.currentState.members || !room.currentState.getMember(client.credentials.userId)) { + return false; + } + + var displayName = room.currentState.getMember(client.credentials.userId).name; + + // N.B. we can't use \b as it chokes on unicode. however \W seems to be okay + // as shorthand for [^0-9A-Za-z_]. + var pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i'); + return content.body.search(pat) > -1; + }; + + var eventFulfillsDeviceCondition = function eventFulfillsDeviceCondition(cond, ev) { + return false; // XXX: Allow a profile tag to be set for the web client instance + }; + + var eventFulfillsEventMatchCondition = function eventFulfillsEventMatchCondition(cond, ev) { + if (!cond.key) { + return false; + } + + var val = valueForDottedKey(cond.key, ev); + if (!val || typeof val != 'string') { + return false; + } + + if (cond.value) { + return cond.value === val; + } + + var regex = void 0; + + if (cond.key == 'content.body') { + regex = createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)'); + } else { + regex = createCachedRegex('^', cond.pattern, '$'); + } + + return !!val.match(regex); + }; + + var createCachedRegex = function createCachedRegex(prefix, glob, suffix) { + if (cachedGlobToRegex[glob]) { + return cachedGlobToRegex[glob]; + } + cachedGlobToRegex[glob] = new RegExp(prefix + globToRegexp(glob) + suffix, 'i'); + return cachedGlobToRegex[glob]; + }; + + var globToRegexp = function globToRegexp(glob) { + // From + // https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132 + // Because micromatch is about 130KB with dependencies, + // and minimatch is not much better. + var pat = escapeRegExp(glob); + pat = pat.replace(/\\\*/g, '.*'); + pat = pat.replace(/\?/g, '.'); + pat = pat.replace(/\\\[(!|)(.*)\\]/g, function (match, p1, p2, offset, string) { + var first = p1 && '^' || ''; + var second = p2.replace(/\\\-/, '-'); + return '[' + first + second + ']'; + }); + return pat; + }; + + var valueForDottedKey = function valueForDottedKey(key, ev) { + var parts = key.split('.'); + var val = void 0; + + // special-case the first component to deal with encrypted messages + var firstPart = parts[0]; + if (firstPart == 'content') { + val = ev.getContent(); + parts.shift(); + } else if (firstPart == 'type') { + val = ev.getType(); + parts.shift(); + } else { + // use the raw event for any other fields + val = ev.event; + } + + while (parts.length > 0) { + var thispart = parts.shift(); + if (!val[thispart]) { + return null; + } + val = val[thispart]; + } + return val; + }; + + var matchingRuleForEventWithRulesets = function matchingRuleForEventWithRulesets(ev, rulesets) { + if (!rulesets || !rulesets.device) { + return null; + } + if (ev.getSender() == client.credentials.userId) { + return null; + } + + var allDevNames = (0, _keys2.default)(rulesets.device); + for (var i = 0; i < allDevNames.length; ++i) { + var devname = allDevNames[i]; + var devrules = rulesets.device[devname]; + + var matchingRule = matchingRuleFromKindSet(devrules, devname); + if (matchingRule) { + return matchingRule; + } + } + return matchingRuleFromKindSet(ev, rulesets.global); + }; + + var pushActionsForEventAndRulesets = function pushActionsForEventAndRulesets(ev, rulesets) { + var rule = matchingRuleForEventWithRulesets(ev, rulesets); + if (!rule) { + return {}; + } + + var actionObj = PushProcessor.actionListToActionsObject(rule.actions); + + // Some actions are implicit in some situations: we add those here + if (actionObj.tweaks.highlight === undefined) { + // if it isn't specified, highlight if it's a content + // rule but otherwise not + actionObj.tweaks.highlight = rule.kind == 'content'; + } + + return actionObj; + }; + + this.ruleMatchesEvent = function (rule, ev) { + var ret = true; + for (var i = 0; i < rule.conditions.length; ++i) { + var cond = rule.conditions[i]; + ret &= eventFulfillsCondition(cond, ev); + } + //console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match")); + return ret; + }; + + /** + * Get the user's push actions for the given event + * + * @param {module:models/event.MatrixEvent} ev + * + * @return {PushAction} + */ + this.actionsForEvent = function (ev) { + return pushActionsForEventAndRulesets(ev, client.pushRules); + }; + + /** + * Get one of the users push rules by its ID + * + * @param {string} ruleId The ID of the rule to search for + * @return {object} The push rule, or null if no such rule was found + */ + this.getPushRuleById = function (ruleId) { + var _arr = ['device', 'global']; + + for (var _i = 0; _i < _arr.length; _i++) { + var scope = _arr[_i]; + if (client.pushRules[scope] === undefined) continue; + + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)(RULEKINDS_IN_ORDER), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var kind = _step.value; + + if (client.pushRules[scope][kind] === undefined) continue; + + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = (0, _getIterator3.default)(client.pushRules[scope][kind]), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var rule = _step2.value; + + if (rule.rule_id === ruleId) return rule; + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + } + return null; + }; +} + +/** + * Convert a list of actions into a object with the actions as keys and their values + * eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ] + * becomes { notify: true, tweaks: { sound: 'default' } } + * @param {array} actionlist The actions list + * + * @return {object} A object with key 'notify' (true or false) and an object of actions + */ +PushProcessor.actionListToActionsObject = function (actionlist) { + var actionobj = { 'notify': false, 'tweaks': {} }; + for (var i = 0; i < actionlist.length; ++i) { + var action = actionlist[i]; + if (action === 'notify') { + actionobj.notify = true; + } else if ((typeof action === 'undefined' ? 'undefined' : (0, _typeof3.default)(action)) === 'object') { + if (action.value === undefined) { + action.value = true; + } + actionobj.tweaks[action.set_tweak] = action.value; + } + } + return actionobj; +}; + +/** + * @typedef {Object} PushAction + * @type {Object} + * @property {boolean} notify Whether this event should notify the user or not. + * @property {Object} tweaks How this event should be notified. + * @property {boolean} tweaks.highlight Whether this event should be highlighted + * on the UI. + * @property {boolean} tweaks.sound Whether this notification should produce a + * noise. + */ + +/** The PushProcessor class. */ +module.exports = PushProcessor; + +},{"babel-runtime/core-js/get-iterator":53,"babel-runtime/core-js/object/keys":61,"babel-runtime/helpers/typeof":72}],39:[function(require,module,exports){ +(function (global){ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* A re-implementation of the javascript callback functions (setTimeout, + * clearTimeout; setInterval and clearInterval are not yet implemented) which + * try to improve handling of large clock jumps (as seen when + * suspending/resuming the system). + * + * In particular, if a timeout would have fired while the system was suspended, + * it will instead fire as soon as possible after resume. + */ + +"use strict"; + +// we schedule a callback at least this often, to check if we've missed out on +// some wall-clock time due to being suspended. + +var TIMER_CHECK_PERIOD_MS = 1000; + +// counter, for making up ids to return from setTimeout +var _count = 0; + +// the key for our callback with the real global.setTimeout +var _realCallbackKey = void 0; + +// a sorted list of the callbacks to be run. +// each is an object with keys [runAt, func, params, key]. +var _callbackList = []; + +// var debuglog = console.log.bind(console); +var debuglog = function debuglog() {}; + +/** + * Replace the function used by this module to get the current time. + * + * Intended for use by the unit tests. + * + * @param {function} f function which should return a millisecond counter + * + * @internal + */ +module.exports.setNow = function (f) { + _now = f || Date.now; +}; +var _now = Date.now; + +/** + * reimplementation of window.setTimeout, which will call the callback if + * the wallclock time goes past the deadline. + * + * @param {function} func callback to be called after a delay + * @param {Number} delayMs number of milliseconds to delay by + * + * @return {Number} an identifier for this callback, which may be passed into + * clearTimeout later. + */ +module.exports.setTimeout = function (func, delayMs) { + delayMs = delayMs || 0; + if (delayMs < 0) { + delayMs = 0; + } + + var params = Array.prototype.slice.call(arguments, 2); + var runAt = _now() + delayMs; + var key = _count++; + debuglog("setTimeout: scheduling cb", key, "at", runAt, "(delay", delayMs, ")"); + var data = { + runAt: runAt, + func: func, + params: params, + key: key + }; + + // figure out where it goes in the list + var idx = binarySearch(_callbackList, function (el) { + return el.runAt - runAt; + }); + + _callbackList.splice(idx, 0, data); + _scheduleRealCallback(); + + return key; +}; + +/** + * reimplementation of window.clearTimeout, which mirrors setTimeout + * + * @param {Number} key result from an earlier setTimeout call + */ +module.exports.clearTimeout = function (key) { + if (_callbackList.length === 0) { + return; + } + + // remove the element from the list + var i = void 0; + for (i = 0; i < _callbackList.length; i++) { + var cb = _callbackList[i]; + if (cb.key == key) { + _callbackList.splice(i, 1); + break; + } + } + + // iff it was the first one in the list, reschedule our callback. + if (i === 0) { + _scheduleRealCallback(); + } +}; + +// use the real global.setTimeout to schedule a callback to _runCallbacks. +function _scheduleRealCallback() { + if (_realCallbackKey) { + global.clearTimeout(_realCallbackKey); + } + + var first = _callbackList[0]; + + if (!first) { + debuglog("_scheduleRealCallback: no more callbacks, not rescheduling"); + return; + } + + var now = _now(); + var delayMs = Math.min(first.runAt - now, TIMER_CHECK_PERIOD_MS); + + debuglog("_scheduleRealCallback: now:", now, "delay:", delayMs); + _realCallbackKey = global.setTimeout(_runCallbacks, delayMs); +} + +function _runCallbacks() { + var cb = void 0; + var now = _now(); + debuglog("_runCallbacks: now:", now); + + // get the list of things to call + var callbacksToRun = []; + while (true) { + var first = _callbackList[0]; + if (!first || first.runAt > now) { + break; + } + cb = _callbackList.shift(); + debuglog("_runCallbacks: popping", cb.key); + callbacksToRun.push(cb); + } + + // reschedule the real callback before running our functions, to + // keep the codepaths the same whether or not our functions + // register their own setTimeouts. + _scheduleRealCallback(); + + for (var i = 0; i < callbacksToRun.length; i++) { + cb = callbacksToRun[i]; + try { + cb.func.apply(global, cb.params); + } catch (e) { + console.error("Uncaught exception in callback function", e.stack || e); + } + } +} + +/* search in a sorted array. + * + * returns the index of the last element for which func returns + * greater than zero, or array.length if no such element exists. + */ +function binarySearch(array, func) { + // min is inclusive, max exclusive. + var min = 0, + max = array.length; + + while (min < max) { + var mid = min + max >> 1; + var res = func(array[mid]); + if (res > 0) { + // the element at 'mid' is too big; set it as the new max. + max = mid; + } else { + // the element at 'mid' is too small. 'min' is inclusive, so +1. + min = mid + 1; + } + } + // presumably, min==max now. + return min; +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{}],40:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; +/** + * This is an internal module which manages queuing, scheduling and retrying + * of requests. + * @module scheduler + */ + +var _bluebird = require("bluebird"); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var utils = require("./utils"); + + +var DEBUG = false; // set true to enable console logging. + +/** + * Construct a scheduler for Matrix. Requires + * {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided + * with a way of processing events. + * @constructor + * @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry + * algorithm to apply when determining when to try to send an event again. + * Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}. + * @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing + * algorithm to apply when determining which events should be sent before the + * given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}. + */ +function MatrixScheduler(retryAlgorithm, queueAlgorithm) { + this.retryAlgorithm = retryAlgorithm || MatrixScheduler.RETRY_BACKOFF_RATELIMIT; + this.queueAlgorithm = queueAlgorithm || MatrixScheduler.QUEUE_MESSAGES; + this._queues = { + // queueName: [{ + // event: MatrixEvent, // event to send + // defer: Deferred, // defer to resolve/reject at the END of the retries + // attempts: Number // number of times we've called processFn + // }, ...] + }; + this._activeQueues = []; + this._procFn = null; +} + +/** + * Retrieve a queue based on an event. The event provided does not need to be in + * the queue. + * @param {MatrixEvent} event An event to get the queue for. + * @return {?Array} A shallow copy of events in the queue or null. + * Modifying this array will not modify the list itself. Modifying events in + * this array will modify the underlying event in the queue. + * @see MatrixScheduler.removeEventFromQueue To remove an event from the queue. + */ +MatrixScheduler.prototype.getQueueForEvent = function (event) { + var name = this.queueAlgorithm(event); + if (!name || !this._queues[name]) { + return null; + } + return utils.map(this._queues[name], function (obj) { + return obj.event; + }); +}; + +/** + * Remove this event from the queue. The event is equal to another event if they + * have the same ID returned from event.getId(). + * @param {MatrixEvent} event The event to remove. + * @return {boolean} True if this event was removed. + */ +MatrixScheduler.prototype.removeEventFromQueue = function (event) { + var name = this.queueAlgorithm(event); + if (!name || !this._queues[name]) { + return false; + } + var removed = false; + utils.removeElement(this._queues[name], function (element) { + if (element.event.getId() === event.getId()) { + // XXX we should probably reject the promise? + // https://github.com/matrix-org/matrix-js-sdk/issues/496 + removed = true; + return true; + } + }); + return removed; +}; + +/** + * Set the process function. Required for events in the queue to be processed. + * If set after events have been added to the queue, this will immediately start + * processing them. + * @param {module:scheduler~processFn} fn The function that can process events + * in the queue. + */ +MatrixScheduler.prototype.setProcessFunction = function (fn) { + this._procFn = fn; + _startProcessingQueues(this); +}; + +/** + * Queue an event if it is required and start processing queues. + * @param {MatrixEvent} event The event that may be queued. + * @return {?Promise} A promise if the event was queued, which will be + * resolved or rejected in due time, else null. + */ +MatrixScheduler.prototype.queueEvent = function (event) { + var queueName = this.queueAlgorithm(event); + if (!queueName) { + return null; + } + // add the event to the queue and make a deferred for it. + if (!this._queues[queueName]) { + this._queues[queueName] = []; + } + var defer = _bluebird2.default.defer(); + this._queues[queueName].push({ + event: event, + defer: defer, + attempts: 0 + }); + debuglog("Queue algorithm dumped event %s into queue '%s'", event.getId(), queueName); + _startProcessingQueues(this); + return defer.promise; +}; + +/** + * Retries events up to 4 times using exponential backoff. This produces wait + * times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the + * failure was due to a rate limited request, the time specified in the error is + * waited before being retried. + * @param {MatrixEvent} event + * @param {Number} attempts + * @param {MatrixError} err + * @return {Number} + * @see module:scheduler~retryAlgorithm + */ +MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function (event, attempts, err) { + if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) { + // client error; no amount of retrying with save you now. + return -1; + } + // we ship with browser-request which returns { cors: rejected } when trying + // with no connection, so if we match that, give up since they have no conn. + if (err.cors === "rejected") { + return -1; + } + + if (err.name === "M_LIMIT_EXCEEDED") { + var waitTime = err.data.retry_after_ms; + if (waitTime) { + return waitTime; + } + } + if (attempts > 4) { + return -1; // give up + } + return 1000 * Math.pow(2, attempts); +}; + +/** + * Queues m.room.message events and lets other events continue + * concurrently. + * @param {MatrixEvent} event + * @return {string} + * @see module:scheduler~queueAlgorithm + */ +MatrixScheduler.QUEUE_MESSAGES = function (event) { + if (event.getType() === "m.room.message") { + // put these events in the 'message' queue. + return "message"; + } + // allow all other events continue concurrently. + return null; +}; + +function _startProcessingQueues(scheduler) { + if (!scheduler._procFn) { + return; + } + // for each inactive queue with events in them + utils.forEach(utils.filter(utils.keys(scheduler._queues), function (queueName) { + return scheduler._activeQueues.indexOf(queueName) === -1 && scheduler._queues[queueName].length > 0; + }), function (queueName) { + // mark the queue as active + scheduler._activeQueues.push(queueName); + // begin processing the head of the queue + debuglog("Spinning up queue: '%s'", queueName); + _processQueue(scheduler, queueName); + }); +} + +function _processQueue(scheduler, queueName) { + // get head of queue + var obj = _peekNextEvent(scheduler, queueName); + if (!obj) { + // queue is empty. Mark as inactive and stop recursing. + var index = scheduler._activeQueues.indexOf(queueName); + if (index >= 0) { + scheduler._activeQueues.splice(index, 1); + } + debuglog("Stopping queue '%s' as it is now empty", queueName); + return; + } + debuglog("Queue '%s' has %s pending events", queueName, scheduler._queues[queueName].length); + // fire the process function and if it resolves, resolve the deferred. Else + // invoke the retry algorithm. + scheduler._procFn(obj.event).done(function (res) { + // remove this from the queue + _removeNextEvent(scheduler, queueName); + debuglog("Queue '%s' sent event %s", queueName, obj.event.getId()); + obj.defer.resolve(res); + // keep processing + _processQueue(scheduler, queueName); + }, function (err) { + obj.attempts += 1; + // ask the retry algorithm when/if we should try again + var waitTimeMs = scheduler.retryAlgorithm(obj.event, obj.attempts, err); + debuglog("retry(%s) err=%s event_id=%s waitTime=%s", obj.attempts, err, obj.event.getId(), waitTimeMs); + if (waitTimeMs === -1) { + // give up (you quitter!) + debuglog("Queue '%s' giving up on event %s", queueName, obj.event.getId()); + // remove this from the queue + _removeNextEvent(scheduler, queueName); + obj.defer.reject(err); + // process next event + _processQueue(scheduler, queueName); + } else { + setTimeout(function () { + _processQueue(scheduler, queueName); + }, waitTimeMs); + } + }); +} + +function _peekNextEvent(scheduler, queueName) { + var queue = scheduler._queues[queueName]; + if (!utils.isArray(queue)) { + return null; + } + return queue[0]; +} + +function _removeNextEvent(scheduler, queueName) { + var queue = scheduler._queues[queueName]; + if (!utils.isArray(queue)) { + return null; + } + return queue.shift(); +} + +function debuglog() { + if (DEBUG) { + var _console; + + (_console = console).log.apply(_console, arguments); + } +} + +/** + * The retry algorithm to apply when retrying events. To stop retrying, return + * -1. If this event was part of a queue, it will be removed from + * the queue. + * @callback retryAlgorithm + * @param {MatrixEvent} event The event being retried. + * @param {Number} attempts The number of failed attempts. This will always be + * >= 1. + * @param {MatrixError} err The most recent error message received when trying + * to send this event. + * @return {Number} The number of milliseconds to wait before trying again. If + * this is 0, the request will be immediately retried. If this is + * -1, the event will be marked as + * {@link module:models/event.EventStatus.NOT_SENT} and will not be retried. + */ + +/** + * The queuing algorithm to apply to events. This function must be idempotent as + * it may be called multiple times with the same event. All queues created are + * serviced in a FIFO manner. To send the event ASAP, return null + * which will not put this event in a queue. Events that fail to send that form + * part of a queue will be removed from the queue and the next event in the + * queue will be sent. + * @callback queueAlgorithm + * @param {MatrixEvent} event The event to be sent. + * @return {string} The name of the queue to put the event into. If a queue with + * this name does not exist, it will be created. If this is null, + * the event is not put into a queue and will be sent concurrently. + */ + +/** +* The function to invoke to process (send) events in the queue. +* @callback processFn +* @param {MatrixEvent} event The event to send. +* @return {Promise} Resolved/rejected depending on the outcome of the request. +*/ + +/** + * The MatrixScheduler class. + */ +module.exports = MatrixScheduler; + +},{"./utils":50,"bluebird":74}],41:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _getIterator2 = require("babel-runtime/core-js/get-iterator"); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +var _slicedToArray2 = require("babel-runtime/helpers/slicedToArray"); + +var _slicedToArray3 = _interopRequireDefault(_slicedToArray2); + +var _bluebird = require("bluebird"); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +var _syncAccumulator = require("../sync-accumulator"); + +var _syncAccumulator2 = _interopRequireDefault(_syncAccumulator); + +var _utils = require("../utils"); + +var _utils2 = _interopRequireDefault(_utils); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var VERSION = 1; /* + Copyright 2017 Vector Creations Ltd + Copyright 2018 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +function createDatabase(db) { + // Make user store, clobber based on user ID. (userId property of User objects) + db.createObjectStore("users", { keyPath: ["userId"] }); + + // Make account data store, clobber based on event type. + // (event.type property of MatrixEvent objects) + db.createObjectStore("accountData", { keyPath: ["type"] }); + + // Make /sync store (sync tokens, room data, etc), always clobber (const key). + db.createObjectStore("sync", { keyPath: ["clobber"] }); +} + +/** + * Helper method to collect results from a Cursor and promiseify it. + * @param {ObjectStore|Index} store The store to perform openCursor on. + * @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor. + * @param {Function} resultMapper A function which is repeatedly called with a + * Cursor. + * Return the data you want to keep. + * @return {Promise} Resolves to an array of whatever you returned from + * resultMapper. + */ +function selectQuery(store, keyRange, resultMapper) { + var query = store.openCursor(keyRange); + return new _bluebird2.default(function (resolve, reject) { + var results = []; + query.onerror = function (event) { + reject(new Error("Query failed: " + event.target.errorCode)); + }; + // collect results + query.onsuccess = function (event) { + var cursor = event.target.result; + if (!cursor) { + resolve(results); + return; // end of results + } + results.push(resultMapper(cursor)); + cursor.continue(); + }; + }); +} + +function promiseifyTxn(txn) { + return new _bluebird2.default(function (resolve, reject) { + txn.oncomplete = function (event) { + resolve(event); + }; + txn.onerror = function (event) { + reject(event); + }; + }); +} + +function promiseifyRequest(req) { + return new _bluebird2.default(function (resolve, reject) { + req.onsuccess = function (event) { + resolve(event); + }; + req.onerror = function (event) { + reject(event); + }; + }); +} + +/** + * Does the actual reading from and writing to the indexeddb + * + * Construct a new Indexed Database store backend. This requires a call to + * connect() before this store can be used. + * @constructor + * @param {Object} indexedDBInterface The Indexed DB interface e.g + * window.indexedDB + * @param {string=} dbName Optional database name. The same name must be used + * to open the same database. + */ +var LocalIndexedDBStoreBackend = function LocalIndexedDBStoreBackend(indexedDBInterface, dbName) { + this.indexedDB = indexedDBInterface; + this._dbName = "matrix-js-sdk:" + (dbName || "default"); + this.db = null; + this._disconnected = true; + this._syncAccumulator = new _syncAccumulator2.default(); +}; + +LocalIndexedDBStoreBackend.prototype = { + /** + * Attempt to connect to the database. This can fail if the user does not + * grant permission. + * @return {Promise} Resolves if successfully connected. + */ + connect: function connect() { + var _this = this; + + if (!this._disconnected) { + console.log("LocalIndexedDBStoreBackend.connect: already connected or connecting"); + return _bluebird2.default.resolve(); + } + + this._disconnected = false; + + console.log("LocalIndexedDBStoreBackend.connect: connecting..."); + var req = this.indexedDB.open(this._dbName, VERSION); + req.onupgradeneeded = function (ev) { + var db = ev.target.result; + var oldVersion = ev.oldVersion; + console.log("LocalIndexedDBStoreBackend.connect: upgrading from " + oldVersion); + if (oldVersion < 1) { + // The database did not previously exist. + createDatabase(db); + } + // Expand as needed. + }; + + req.onblocked = function () { + console.log("can't yet open LocalIndexedDBStoreBackend because it is open elsewhere"); + }; + + console.log("LocalIndexedDBStoreBackend.connect: awaiting connection..."); + return promiseifyRequest(req).then(function (ev) { + console.log("LocalIndexedDBStoreBackend.connect: connected"); + _this.db = ev.target.result; + + // add a poorly-named listener for when deleteDatabase is called + // so we can close our db connections. + _this.db.onversionchange = function () { + _this.db.close(); + }; + + return _this._init(); + }); + }, + + /** + * Having connected, load initial data from the database and prepare for use + * @return {Promise} Resolves on success + */ + _init: function _init() { + var _this2 = this; + + return _bluebird2.default.all([this._loadAccountData(), this._loadSyncData()]).then(function (_ref) { + var _ref2 = (0, _slicedToArray3.default)(_ref, 2), + accountData = _ref2[0], + syncData = _ref2[1]; + + console.log("LocalIndexedDBStoreBackend: loaded initial data"); + _this2._syncAccumulator.accumulate({ + next_batch: syncData.nextBatch, + rooms: syncData.roomsData, + groups: syncData.groupsData, + account_data: { + events: accountData + } + }); + }); + }, + + /** + * Clear the entire database. This should be used when logging out of a client + * to prevent mixing data between accounts. + * @return {Promise} Resolved when the database is cleared. + */ + clearDatabase: function clearDatabase() { + var _this3 = this; + + return new _bluebird2.default(function (resolve, reject) { + console.log("Removing indexeddb instance: " + _this3._dbName); + var req = _this3.indexedDB.deleteDatabase(_this3._dbName); + + req.onblocked = function () { + console.log("can't yet delete indexeddb " + _this3._dbName + " because it is open elsewhere"); + }; + + req.onerror = function (ev) { + // in firefox, with indexedDB disabled, this fails with a + // DOMError. We treat this as non-fatal, so that we can still + // use the app. + console.warn("unable to delete js-sdk store indexeddb: " + ev.target.error); + resolve(); + }; + + req.onsuccess = function () { + console.log("Removed indexeddb instance: " + _this3._dbName); + resolve(); + }; + }); + }, + + /** + * @param {boolean=} copy If false, the data returned is from internal + * buffers and must not be mutated. Otherwise, a copy is made before + * returning such that the data can be safely mutated. Default: true. + * + * @return {Promise} Resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + getSavedSync: function getSavedSync(copy) { + if (copy === undefined) copy = true; + + var data = this._syncAccumulator.getJSON(); + if (!data.nextBatch) return _bluebird2.default.resolve(null); + if (copy) { + // We must deep copy the stored data so that the /sync processing code doesn't + // corrupt the internal state of the sync accumulator (it adds non-clonable keys) + return _bluebird2.default.resolve(_utils2.default.deepCopy(data)); + } else { + return _bluebird2.default.resolve(data); + } + }, + + getNextBatchToken: function getNextBatchToken() { + return _bluebird2.default.resolve(this._syncAccumulator.getNextBatchToken()); + }, + + setSyncData: function setSyncData(syncData) { + var _this4 = this; + + return _bluebird2.default.resolve().then(function () { + _this4._syncAccumulator.accumulate(syncData); + }); + }, + + syncToDatabase: function syncToDatabase(userTuples) { + var syncData = this._syncAccumulator.getJSON(); + + return _bluebird2.default.all([this._persistUserPresenceEvents(userTuples), this._persistAccountData(syncData.accountData), this._persistSyncData(syncData.nextBatch, syncData.roomsData, syncData.groupsData)]); + }, + + /** + * Persist rooms /sync data along with the next batch token. + * @param {string} nextBatch The next_batch /sync value. + * @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator + * @param {Object} groupsData The 'groups' /sync data from a SyncAccumulator + * @return {Promise} Resolves if the data was persisted. + */ + _persistSyncData: function _persistSyncData(nextBatch, roomsData, groupsData) { + var _this5 = this; + + console.log("Persisting sync data up to ", nextBatch); + return _bluebird2.default.try(function () { + var txn = _this5.db.transaction(["sync"], "readwrite"); + var store = txn.objectStore("sync"); + store.put({ + clobber: "-", // constant key so will always clobber + nextBatch: nextBatch, + roomsData: roomsData, + groupsData: groupsData + }); // put == UPSERT + return promiseifyTxn(txn); + }); + }, + + /** + * Persist a list of account data events. Events with the same 'type' will + * be replaced. + * @param {Object[]} accountData An array of raw user-scoped account data events + * @return {Promise} Resolves if the events were persisted. + */ + _persistAccountData: function _persistAccountData(accountData) { + var _this6 = this; + + return _bluebird2.default.try(function () { + var txn = _this6.db.transaction(["accountData"], "readwrite"); + var store = txn.objectStore("accountData"); + for (var i = 0; i < accountData.length; i++) { + store.put(accountData[i]); // put == UPSERT + } + return promiseifyTxn(txn); + }); + }, + + /** + * Persist a list of [user id, presence event] they are for. + * Users with the same 'userId' will be replaced. + * Presence events should be the event in its raw form (not the Event + * object) + * @param {Object[]} tuples An array of [userid, event] tuples + * @return {Promise} Resolves if the users were persisted. + */ + _persistUserPresenceEvents: function _persistUserPresenceEvents(tuples) { + var _this7 = this; + + return _bluebird2.default.try(function () { + var txn = _this7.db.transaction(["users"], "readwrite"); + var store = txn.objectStore("users"); + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)(tuples), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var tuple = _step.value; + + store.put({ + userId: tuple[0], + event: tuple[1] + }); // put == UPSERT + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return promiseifyTxn(txn); + }); + }, + + /** + * Load all user presence events from the database. This is not cached. + * FIXME: It would probably be more sensible to store the events in the + * sync. + * @return {Promise} A list of presence events in their raw form. + */ + getUserPresenceEvents: function getUserPresenceEvents() { + var _this8 = this; + + return _bluebird2.default.try(function () { + var txn = _this8.db.transaction(["users"], "readonly"); + var store = txn.objectStore("users"); + return selectQuery(store, undefined, function (cursor) { + return [cursor.value.userId, cursor.value.event]; + }); + }); + }, + + /** + * Load all the account data events from the database. This is not cached. + * @return {Promise} A list of raw global account events. + */ + _loadAccountData: function _loadAccountData() { + var _this9 = this; + + console.log("LocalIndexedDBStoreBackend: loading account data..."); + return _bluebird2.default.try(function () { + var txn = _this9.db.transaction(["accountData"], "readonly"); + var store = txn.objectStore("accountData"); + return selectQuery(store, undefined, function (cursor) { + return cursor.value; + }).then(function (result) { + console.log("LocalIndexedDBStoreBackend: loaded account data"); + return result; + }); + }); + }, + + /** + * Load the sync data from the database. + * @return {Promise} An object with "roomsData" and "nextBatch" keys. + */ + _loadSyncData: function _loadSyncData() { + var _this10 = this; + + console.log("LocalIndexedDBStoreBackend: loading sync data..."); + return _bluebird2.default.try(function () { + var txn = _this10.db.transaction(["sync"], "readonly"); + var store = txn.objectStore("sync"); + return selectQuery(store, undefined, function (cursor) { + return cursor.value; + }).then(function (results) { + console.log("LocalIndexedDBStoreBackend: loaded sync data"); + if (results.length > 1) { + console.warn("loadSyncData: More than 1 sync row found."); + } + return results.length > 0 ? results[0] : {}; + }); + }); + } +}; + +exports.default = LocalIndexedDBStoreBackend; + +},{"../sync-accumulator":47,"../utils":50,"babel-runtime/core-js/get-iterator":53,"babel-runtime/helpers/slicedToArray":71,"bluebird":74}],42:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _bluebird = require('bluebird'); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * An IndexedDB store backend where the actual backend sits in a web + * worker. + * + * Construct a new Indexed Database store backend. This requires a call to + * connect() before this store can be used. + * @constructor + * @param {string} workerScript URL to the worker script + * @param {string=} dbName Optional database name. The same name must be used + * to open the same database. + * @param {Object} workerApi The web worker compatible interface object + */ +var RemoteIndexedDBStoreBackend = function RemoteIndexedDBStoreBackend(workerScript, dbName, workerApi) { + this._workerScript = workerScript; + this._dbName = dbName; + this._workerApi = workerApi; + this._worker = null; + this._nextSeq = 0; + // The currently in-flight requests to the actual backend + this._inFlight = { + // seq: promise, + }; + // Once we start connecting, we keep the promise and re-use it + // if we try to connect again + this._startPromise = null; +}; /* + Copyright 2017 Vector Creations Ltd + Copyright 2018 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +RemoteIndexedDBStoreBackend.prototype = { + /** + * Attempt to connect to the database. This can fail if the user does not + * grant permission. + * @return {Promise} Resolves if successfully connected. + */ + connect: function connect() { + var _this = this; + + return this._ensureStarted().then(function () { + return _this._doCmd('connect'); + }); + }, + + /** + * Clear the entire database. This should be used when logging out of a client + * to prevent mixing data between accounts. + * @return {Promise} Resolved when the database is cleared. + */ + clearDatabase: function clearDatabase() { + var _this2 = this; + + return this._ensureStarted().then(function () { + return _this2._doCmd('clearDatabase'); + }); + }, + + /** + * @return {Promise} Resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + getSavedSync: function getSavedSync() { + return this._doCmd('getSavedSync'); + }, + + getNextBatchToken: function getNextBatchToken() { + return this._doCmd('getNextBatchToken'); + }, + + setSyncData: function setSyncData(syncData) { + return this._doCmd('setSyncData', [syncData]); + }, + + syncToDatabase: function syncToDatabase(users) { + return this._doCmd('syncToDatabase', [users]); + }, + + /** + * Load all user presence events from the database. This is not cached. + * @return {Promise} A list of presence events in their raw form. + */ + getUserPresenceEvents: function getUserPresenceEvents() { + return this._doCmd('getUserPresenceEvents'); + }, + + _ensureStarted: function _ensureStarted() { + if (this._startPromise === null) { + this._worker = new this._workerApi(this._workerScript); + this._worker.onmessage = this._onWorkerMessage.bind(this); + + // tell the worker the db name. + this._startPromise = this._doCmd('_setupWorker', [this._dbName]).then(function () { + console.log("IndexedDB worker is ready"); + }); + } + return this._startPromise; + }, + + _doCmd: function _doCmd(cmd, args) { + var _this3 = this; + + // wrap in a q so if the postMessage throws, + // the promise automatically gets rejected + return _bluebird2.default.resolve().then(function () { + var seq = _this3._nextSeq++; + var def = _bluebird2.default.defer(); + + _this3._inFlight[seq] = def; + + _this3._worker.postMessage({ + command: cmd, + seq: seq, + args: args + }); + + return def.promise; + }); + }, + + _onWorkerMessage: function _onWorkerMessage(ev) { + var msg = ev.data; + + if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') { + if (msg.seq === undefined) { + console.error("Got reply from worker with no seq"); + return; + } + + var def = this._inFlight[msg.seq]; + if (def === undefined) { + console.error("Got reply for unknown seq " + msg.seq); + return; + } + delete this._inFlight[msg.seq]; + + if (msg.command == 'cmd_success') { + def.resolve(msg.result); + } else { + def.reject(msg.error); + } + } else { + console.warn("Unrecognised message from worker: " + msg); + } + } +}; + +exports.default = RemoteIndexedDBStoreBackend; + +},{"bluebird":74}],43:[function(require,module,exports){ +(function (global){ +"use strict"; + +var _getIterator2 = require("babel-runtime/core-js/get-iterator"); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +var _slicedToArray2 = require("babel-runtime/helpers/slicedToArray"); + +var _slicedToArray3 = _interopRequireDefault(_slicedToArray2); + +var _bluebird = require("bluebird"); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +var _memory = require("./memory"); + +var _utils = require("../utils"); + +var _utils2 = _interopRequireDefault(_utils); + +var _indexeddbLocalBackend = require("./indexeddb-local-backend.js"); + +var _indexeddbLocalBackend2 = _interopRequireDefault(_indexeddbLocalBackend); + +var _indexeddbRemoteBackend = require("./indexeddb-remote-backend.js"); + +var _indexeddbRemoteBackend2 = _interopRequireDefault(_indexeddbRemoteBackend); + +var _user = require("../models/user"); + +var _user2 = _interopRequireDefault(_user); + +var _event = require("../models/event"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * This is an internal module. See {@link IndexedDBStore} for the public class. + * @module store/indexeddb + */ + +// If this value is too small we'll be writing very often which will cause +// noticable stop-the-world pauses. If this value is too big we'll be writing +// so infrequently that the /sync size gets bigger on reload. Writing more +// often does not affect the length of the pause since the entire /sync +// response is persisted each time. +var WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes + + +/** + * Construct a new Indexed Database store, which extends MatrixInMemoryStore. + * + * This store functions like a MatrixInMemoryStore except it periodically persists + * the contents of the store to an IndexedDB backend. + * + * All data is still kept in-memory but can be loaded from disk by calling + * startup(). This can make startup times quicker as a complete + * sync from the server is not required. This does not reduce memory usage as all + * the data is eagerly fetched when startup() is called. + *
+ * let opts = { localStorage: window.localStorage };
+ * let store = new IndexedDBStore();
+ * await store.startup(); // load from indexed db
+ * let client = sdk.createClient({
+ *     store: store,
+ * });
+ * client.startClient();
+ * client.on("sync", function(state, prevState, data) {
+ *     if (state === "PREPARED") {
+ *         console.log("Started up, now with go faster stripes!");
+ *     }
+ * });
+ * 
+ * + * @constructor + * @extends MatrixInMemoryStore + * @param {Object} opts Options object. + * @param {Object} opts.indexedDB The Indexed DB interface e.g. + * window.indexedDB + * @param {string=} opts.dbName Optional database name. The same name must be used + * to open the same database. + * @param {string=} opts.workerScript Optional URL to a script to invoke a web + * worker with to run IndexedDB queries on the web worker. The IndexedDbStoreWorker + * class is provided for this purpose and requires the application to provide a + * trivial wrapper script around it. + * @param {Object=} opts.workerApi The webWorker API object. If omitted, the global Worker + * object will be used if it exists. + * @prop {IndexedDBStoreBackend} backend The backend instance. Call through to + * this API if you need to perform specific indexeddb actions like deleting the + * database. + */ +/* +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var IndexedDBStore = function IndexedDBStore(opts) { + _memory.MatrixInMemoryStore.call(this, opts); + + if (!opts.indexedDB) { + throw new Error('Missing required option: indexedDB'); + } + + if (opts.workerScript) { + // try & find a webworker-compatible API + var workerApi = opts.workerApi; + if (!workerApi) { + // default to the global Worker object (which is where it in a browser) + workerApi = global.Worker; + } + this.backend = new _indexeddbRemoteBackend2.default(opts.workerScript, opts.dbName, workerApi); + } else { + this.backend = new _indexeddbLocalBackend2.default(opts.indexedDB, opts.dbName); + } + + this.startedUp = false; + this._syncTs = 0; + + // Records the last-modified-time of each user at the last point we saved + // the database, such that we can derive the set if users that have been + // modified since we last saved. + this._userModifiedMap = { + // user_id : timestamp + }; +}; +_utils2.default.inherits(IndexedDBStore, _memory.MatrixInMemoryStore); + +/** + * @return {Promise} Resolved when loaded from indexed db. + */ +IndexedDBStore.prototype.startup = function () { + var _this = this; + + if (this.startedUp) { + console.log("IndexedDBStore.startup: already started"); + return _bluebird2.default.resolve(); + } + + console.log("IndexedDBStore.startup: connecting to backend"); + return this.backend.connect().then(function () { + console.log("IndexedDBStore.startup: loading presence events"); + return _this.backend.getUserPresenceEvents(); + }).then(function (userPresenceEvents) { + console.log("IndexedDBStore.startup: processing presence events"); + userPresenceEvents.forEach(function (_ref) { + var _ref2 = (0, _slicedToArray3.default)(_ref, 2), + userId = _ref2[0], + rawEvent = _ref2[1]; + + var u = new _user2.default(userId); + if (rawEvent) { + u.setPresenceEvent(new _event.MatrixEvent(rawEvent)); + } + _this._userModifiedMap[u.userId] = u.getLastModifiedTime(); + _this.storeUser(u); + }); + }); +}; + +/** + * @return {Promise} Resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ +IndexedDBStore.prototype.getSavedSync = function () { + return this.backend.getSavedSync(); +}; + +/** + * @return {Promise} If there is a saved sync, the nextBatch token + * for this sync, otherwise null. + */ +IndexedDBStore.prototype.getSavedSyncToken = function () { + return this.backend.getNextBatchToken(); +}, + +/** + * Delete all data from this store. + * @return {Promise} Resolves if the data was deleted from the database. + */ +IndexedDBStore.prototype.deleteAllData = function () { + _memory.MatrixInMemoryStore.prototype.deleteAllData.call(this); + return this.backend.clearDatabase().then(function () { + console.log("Deleted indexeddb data."); + }, function (err) { + console.error("Failed to delete indexeddb data: " + err); + throw err; + }); +}; + +/** + * Whether this store would like to save its data + * Note that obviously whether the store wants to save or + * not could change between calling this function and calling + * save(). + * + * @return {boolean} True if calling save() will actually save + * (at the time this function is called). + */ +IndexedDBStore.prototype.wantsSave = function () { + var now = Date.now(); + return now - this._syncTs > WRITE_DELAY_MS; +}; + +/** + * Possibly write data to the database. + * @return {Promise} Promise resolves after the write completes + * (or immediately if no write is performed) + */ +IndexedDBStore.prototype.save = function () { + if (this.wantsSave()) { + return this._reallySave(); + } + return _bluebird2.default.resolve(); +}; + +IndexedDBStore.prototype._reallySave = function () { + this._syncTs = Date.now(); // set now to guard against multi-writes + + // work out changed users (this doesn't handle deletions but you + // can't 'delete' users as they are just presence events). + var userTuples = []; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)(this.getUsers()), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var u = _step.value; + + if (this._userModifiedMap[u.userId] === u.getLastModifiedTime()) continue; + if (!u.events.presence) continue; + + userTuples.push([u.userId, u.events.presence.event]); + + // note that we've saved this version of the user + this._userModifiedMap[u.userId] = u.getLastModifiedTime(); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return this.backend.syncToDatabase(userTuples).catch(function (err) { + console.error("sync fail:", err); + }); +}; + +IndexedDBStore.prototype.setSyncData = function (syncData) { + return this.backend.setSyncData(syncData); +}; + +module.exports.IndexedDBStore = IndexedDBStore; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"../models/event":30,"../models/user":37,"../utils":50,"./indexeddb-local-backend.js":41,"./indexeddb-remote-backend.js":42,"./memory":44,"babel-runtime/core-js/get-iterator":53,"babel-runtime/helpers/slicedToArray":71,"bluebird":74}],44:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; +/** + * This is an internal module. See {@link MatrixInMemoryStore} for the public class. + * @module store/memory + */ + +var _bluebird = require("bluebird"); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var utils = require("../utils"); +var User = require("../models/user"); + + +/** + * Construct a new in-memory data store for the Matrix Client. + * @constructor + * @param {Object=} opts Config options + * @param {LocalStorage} opts.localStorage The local storage instance to persist + * some forms of data such as tokens. Rooms will NOT be stored. + */ +module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) { + opts = opts || {}; + this.rooms = { + // roomId: Room + }; + this.groups = { + // groupId: Group + }; + this.users = { + // userId: User + }; + this.syncToken = null; + this.filters = { + // userId: { + // filterId: Filter + // } + }; + this.accountData = { + // type : content + }; + this.localStorage = opts.localStorage; +}; + +module.exports.MatrixInMemoryStore.prototype = { + + /** + * Retrieve the token to stream from. + * @return {string} The token or null. + */ + getSyncToken: function getSyncToken() { + return this.syncToken; + }, + + /** + * Set the token to stream from. + * @param {string} token The token to stream from. + */ + setSyncToken: function setSyncToken(token) { + this.syncToken = token; + }, + + /** + * Store the given room. + * @param {Group} group The group to be stored + */ + storeGroup: function storeGroup(group) { + this.groups[group.groupId] = group; + }, + + /** + * Retrieve a group by its group ID. + * @param {string} groupId The group ID. + * @return {Group} The group or null. + */ + getGroup: function getGroup(groupId) { + return this.groups[groupId] || null; + }, + + /** + * Retrieve all known groups. + * @return {Group[]} A list of groups, which may be empty. + */ + getGroups: function getGroups() { + return utils.values(this.groups); + }, + + /** + * Store the given room. + * @param {Room} room The room to be stored. All properties must be stored. + */ + storeRoom: function storeRoom(room) { + this.rooms[room.roomId] = room; + // add listeners for room member changes so we can keep the room member + // map up-to-date. + room.currentState.on("RoomState.members", this._onRoomMember.bind(this)); + // add existing members + var self = this; + room.currentState.getMembers().forEach(function (m) { + self._onRoomMember(null, room.currentState, m); + }); + }, + + /** + * Called when a room member in a room being tracked by this store has been + * updated. + * @param {MatrixEvent} event + * @param {RoomState} state + * @param {RoomMember} member + */ + _onRoomMember: function _onRoomMember(event, state, member) { + if (member.membership === "invite") { + // We do NOT add invited members because people love to typo user IDs + // which would then show up in these lists (!) + return; + } + + var user = this.users[member.userId] || new User(member.userId); + if (member.name) { + user.setDisplayName(member.name); + if (member.events.member) { + user.setRawDisplayName(member.events.member.getDirectionalContent().displayname); + } + } + if (member.events.member && member.events.member.getContent().avatar_url) { + user.setAvatarUrl(member.events.member.getContent().avatar_url); + } + this.users[user.userId] = user; + }, + + /** + * Retrieve a room by its' room ID. + * @param {string} roomId The room ID. + * @return {Room} The room or null. + */ + getRoom: function getRoom(roomId) { + return this.rooms[roomId] || null; + }, + + /** + * Retrieve all known rooms. + * @return {Room[]} A list of rooms, which may be empty. + */ + getRooms: function getRooms() { + return utils.values(this.rooms); + }, + + /** + * Permanently delete a room. + * @param {string} roomId + */ + removeRoom: function removeRoom(roomId) { + if (this.rooms[roomId]) { + this.rooms[roomId].removeListener("RoomState.members", this._onRoomMember); + } + delete this.rooms[roomId]; + }, + + /** + * Retrieve a summary of all the rooms. + * @return {RoomSummary[]} A summary of each room. + */ + getRoomSummaries: function getRoomSummaries() { + return utils.map(utils.values(this.rooms), function (room) { + return room.summary; + }); + }, + + /** + * Store a User. + * @param {User} user The user to store. + */ + storeUser: function storeUser(user) { + this.users[user.userId] = user; + }, + + /** + * Retrieve a User by its' user ID. + * @param {string} userId The user ID. + * @return {User} The user or null. + */ + getUser: function getUser(userId) { + return this.users[userId] || null; + }, + + /** + * Retrieve all known users. + * @return {User[]} A list of users, which may be empty. + */ + getUsers: function getUsers() { + return utils.values(this.users); + }, + + /** + * Retrieve scrollback for this room. + * @param {Room} room The matrix room + * @param {integer} limit The max number of old events to retrieve. + * @return {Array} An array of objects which will be at most 'limit' + * length and at least 0. The objects are the raw event JSON. + */ + scrollback: function scrollback(room, limit) { + return []; + }, + + /** + * Store events for a room. The events have already been added to the timeline + * @param {Room} room The room to store events for. + * @param {Array} events The events to store. + * @param {string} token The token associated with these events. + * @param {boolean} toStart True if these are paginated results. + */ + storeEvents: function storeEvents(room, events, token, toStart) { + // no-op because they've already been added to the room instance. + }, + + /** + * Store a filter. + * @param {Filter} filter + */ + storeFilter: function storeFilter(filter) { + if (!filter) { + return; + } + if (!this.filters[filter.userId]) { + this.filters[filter.userId] = {}; + } + this.filters[filter.userId][filter.filterId] = filter; + }, + + /** + * Retrieve a filter. + * @param {string} userId + * @param {string} filterId + * @return {?Filter} A filter or null. + */ + getFilter: function getFilter(userId, filterId) { + if (!this.filters[userId] || !this.filters[userId][filterId]) { + return null; + } + return this.filters[userId][filterId]; + }, + + /** + * Retrieve a filter ID with the given name. + * @param {string} filterName The filter name. + * @return {?string} The filter ID or null. + */ + getFilterIdByName: function getFilterIdByName(filterName) { + if (!this.localStorage) { + return null; + } + try { + return this.localStorage.getItem("mxjssdk_memory_filter_" + filterName); + } catch (e) {} + return null; + }, + + /** + * Set a filter name to ID mapping. + * @param {string} filterName + * @param {string} filterId + */ + setFilterIdByName: function setFilterIdByName(filterName, filterId) { + if (!this.localStorage) { + return; + } + try { + this.localStorage.setItem("mxjssdk_memory_filter_" + filterName, filterId); + } catch (e) {} + }, + + /** + * Store user-scoped account data events. + * N.B. that account data only allows a single event per type, so multiple + * events with the same type will replace each other. + * @param {Array} events The events to store. + */ + storeAccountDataEvents: function storeAccountDataEvents(events) { + var self = this; + events.forEach(function (event) { + self.accountData[event.getType()] = event; + }); + }, + + /** + * Get account data event by event type + * @param {string} eventType The event type being queried + * @return {?MatrixEvent} the user account_data event of given type, if any + */ + getAccountData: function getAccountData(eventType) { + return this.accountData[eventType]; + }, + + /** + * setSyncData does nothing as there is no backing data store. + * + * @param {Object} syncData The sync data + * @return {Promise} An immediately resolved promise. + */ + setSyncData: function setSyncData(syncData) { + return _bluebird2.default.resolve(); + }, + + /** + * We never want to save becase we have nothing to save to. + * + * @return {boolean} If the store wants to save + */ + wantsSave: function wantsSave() { + return false; + }, + + /** + * Save does nothing as there is no backing data store. + */ + save: function save() {}, + + /** + * Startup does nothing as this store doesn't require starting up. + * @return {Promise} An immediately resolved promise. + */ + startup: function startup() { + return _bluebird2.default.resolve(); + }, + + /** + * @return {Promise} Resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + getSavedSync: function getSavedSync() { + return _bluebird2.default.resolve(null); + }, + + /** + * @return {Promise} If there is a saved sync, the nextBatch token + * for this sync, otherwise null. + */ + getSavedSyncToken: function getSavedSyncToken() { + return _bluebird2.default.resolve(null); + }, + + /** + * Delete all data from this store. + * @return {Promise} An immediately resolved promise. + */ + deleteAllData: function deleteAllData() { + this.rooms = { + // roomId: Room + }; + this.users = { + // userId: User + }; + this.syncToken = null; + this.filters = { + // userId: { + // filterId: Filter + // } + }; + this.accountData = { + // type : content + }; + return _bluebird2.default.resolve(); + } +}; + +},{"../models/user":37,"../utils":50,"bluebird":74}],45:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** + * @module store/session/webstorage + */ + +var _getIterator2 = require("babel-runtime/core-js/get-iterator"); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var utils = require("../../utils"); + +var DEBUG = false; // set true to enable console logging. +var E2E_PREFIX = "session.e2e."; + +/** + * Construct a web storage session store, capable of storing account keys, + * session keys and access tokens. + * @constructor + * @param {WebStorage} webStore A web storage implementation, e.g. + * 'window.localStorage' or 'window.sessionStorage' or a custom implementation. + * @throws if the supplied 'store' does not meet the Storage interface of the + * WebStorage API. + */ +function WebStorageSessionStore(webStore) { + this.store = webStore; + if (!utils.isFunction(webStore.getItem) || !utils.isFunction(webStore.setItem) || !utils.isFunction(webStore.removeItem) || !utils.isFunction(webStore.key) || typeof webStore.length !== 'number') { + throw new Error("Supplied webStore does not meet the WebStorage API interface"); + } +} + +WebStorageSessionStore.prototype = { + /** + * Remove the stored end to end account for the logged-in user. + */ + removeEndToEndAccount: function removeEndToEndAccount() { + this.store.removeItem(KEY_END_TO_END_ACCOUNT); + }, + + /** + * Load the end to end account for the logged-in user. + * Note that the end-to-end account is now stored in the + * crypto store rather than here: this remains here so + * old sessions can be migrated out of the session store. + * @return {?string} Base64 encoded account. + */ + getEndToEndAccount: function getEndToEndAccount() { + return this.store.getItem(KEY_END_TO_END_ACCOUNT); + }, + + /** + * Retrieves the known devices for all users. + * @return {object} A map from user ID to map of device ID to keys for the device. + */ + getAllEndToEndDevices: function getAllEndToEndDevices() { + var prefix = keyEndToEndDevicesForUser(''); + var devices = {}; + for (var i = 0; i < this.store.length; ++i) { + var key = this.store.key(i); + var userId = key.substr(prefix.length); + if (key.startsWith(prefix)) devices[userId] = getJsonItem(this.store, key); + } + return devices; + }, + + getEndToEndDeviceTrackingStatus: function getEndToEndDeviceTrackingStatus() { + return getJsonItem(this.store, KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS); + }, + + /** + * Get the sync token corresponding to the device list. + * + * @return {String?} token + */ + getEndToEndDeviceSyncToken: function getEndToEndDeviceSyncToken() { + return getJsonItem(this.store, KEY_END_TO_END_DEVICE_SYNC_TOKEN); + }, + + /** + * Removes all end to end device data from the store + */ + removeEndToEndDeviceData: function removeEndToEndDeviceData() { + removeByPrefix(this.store, keyEndToEndDevicesForUser('')); + removeByPrefix(this.store, KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS); + removeByPrefix(this.store, KEY_END_TO_END_DEVICE_SYNC_TOKEN); + }, + + /** + * Retrieve the end-to-end sessions between the logged-in user and another + * device. + * @param {string} deviceKey The public key of the other device. + * @return {object} A map from sessionId to Base64 end-to-end session. + */ + getEndToEndSessions: function getEndToEndSessions(deviceKey) { + return getJsonItem(this.store, keyEndToEndSessions(deviceKey)); + }, + + /** + * Retrieve all end-to-end sessions between the logged-in user and other + * devices. + * @return {object} A map of {deviceKey -> {sessionId -> session pickle}} + */ + getAllEndToEndSessions: function getAllEndToEndSessions() { + var deviceKeys = getKeysWithPrefix(this.store, keyEndToEndSessions('')); + var results = {}; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)(deviceKeys), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var k = _step.value; + + var unprefixedKey = k.substr(keyEndToEndSessions('').length); + results[unprefixedKey] = getJsonItem(this.store, k); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return results; + }, + + /** + * Remove all end-to-end sessions from the store + * This is used after migrating sessions awat from the sessions store. + */ + removeAllEndToEndSessions: function removeAllEndToEndSessions() { + removeByPrefix(this.store, keyEndToEndSessions('')); + }, + + /** + * Retrieve a list of all known inbound group sessions + * + * @return {{senderKey: string, sessionId: string}} + */ + getAllEndToEndInboundGroupSessionKeys: function getAllEndToEndInboundGroupSessionKeys() { + var prefix = E2E_PREFIX + 'inboundgroupsessions/'; + var result = []; + for (var i = 0; i < this.store.length; i++) { + var key = this.store.key(i); + if (!key.startsWith(prefix)) { + continue; + } + // we can't use split, as the components we are trying to split out + // might themselves contain '/' characters. We rely on the + // senderKey being a (32-byte) curve25519 key, base64-encoded + // (hence 43 characters long). + + result.push({ + senderKey: key.substr(prefix.length, 43), + sessionId: key.substr(prefix.length + 44) + }); + } + return result; + }, + + getEndToEndInboundGroupSession: function getEndToEndInboundGroupSession(senderKey, sessionId) { + var key = keyEndToEndInboundGroupSession(senderKey, sessionId); + return this.store.getItem(key); + }, + + removeAllEndToEndInboundGroupSessions: function removeAllEndToEndInboundGroupSessions() { + removeByPrefix(this.store, E2E_PREFIX + 'inboundgroupsessions/'); + }, + + /** + * Get the end-to-end state for all rooms + * @return {object} roomId -> object with the end-to-end info for the room. + */ + getAllEndToEndRooms: function getAllEndToEndRooms() { + var roomKeys = getKeysWithPrefix(this.store, keyEndToEndRoom('')); + var results = {}; + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = (0, _getIterator3.default)(roomKeys), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var k = _step2.value; + + var unprefixedKey = k.substr(keyEndToEndRoom('').length); + results[unprefixedKey] = getJsonItem(this.store, k); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + return results; + }, + + removeAllEndToEndRooms: function removeAllEndToEndRooms() { + removeByPrefix(this.store, keyEndToEndRoom('')); + } +}; + +var KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; +var KEY_END_TO_END_DEVICE_SYNC_TOKEN = E2E_PREFIX + "device_sync_token"; +var KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS = E2E_PREFIX + "device_tracking"; + +function keyEndToEndDevicesForUser(userId) { + return E2E_PREFIX + "devices/" + userId; +} + +function keyEndToEndSessions(deviceKey) { + return E2E_PREFIX + "sessions/" + deviceKey; +} + +function keyEndToEndInboundGroupSession(senderKey, sessionId) { + return E2E_PREFIX + "inboundgroupsessions/" + senderKey + "/" + sessionId; +} + +function keyEndToEndRoom(roomId) { + return E2E_PREFIX + "rooms/" + roomId; +} + +function getJsonItem(store, key) { + try { + // if the key is absent, store.getItem() returns null, and + // JSON.parse(null) === null, so this returns null. + return JSON.parse(store.getItem(key)); + } catch (e) { + debuglog("Failed to get key %s: %s", key, e); + debuglog(e.stack); + } + return null; +} + +function getKeysWithPrefix(store, prefix) { + var results = []; + for (var i = 0; i < store.length; ++i) { + var key = store.key(i); + if (key.startsWith(prefix)) results.push(key); + } + return results; +} + +function removeByPrefix(store, prefix) { + var toRemove = []; + for (var i = 0; i < store.length; ++i) { + var key = store.key(i); + if (key.startsWith(prefix)) toRemove.push(key); + } + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; + + try { + for (var _iterator3 = (0, _getIterator3.default)(toRemove), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var _key = _step3.value; + + store.removeItem(_key); + } + } catch (err) { + _didIteratorError3 = true; + _iteratorError3 = err; + } finally { + try { + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); + } + } finally { + if (_didIteratorError3) { + throw _iteratorError3; + } + } + } +} + +function debuglog() { + if (DEBUG) { + var _console; + + (_console = console).log.apply(_console, arguments); + } +} + +/** */ +module.exports = WebStorageSessionStore; + +},{"../../utils":50,"babel-runtime/core-js/get-iterator":53}],46:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +var _bluebird = require("bluebird"); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * This is an internal module. + * @module store/stub + */ + +/** + * Construct a stub store. This does no-ops on most store methods. + * @constructor + */ +function StubStore() { + this.fromToken = null; +} + +StubStore.prototype = { + + /** + * Get the sync token. + * @return {string} + */ + getSyncToken: function getSyncToken() { + return this.fromToken; + }, + + /** + * Set the sync token. + * @param {string} token + */ + setSyncToken: function setSyncToken(token) { + this.fromToken = token; + }, + + /** + * No-op. + * @param {Group} group + */ + storeGroup: function storeGroup(group) {}, + + /** + * No-op. + * @param {string} groupId + * @return {null} + */ + getGroup: function getGroup(groupId) { + return null; + }, + + /** + * No-op. + * @return {Array} An empty array. + */ + getGroups: function getGroups() { + return []; + }, + + /** + * No-op. + * @param {Room} room + */ + storeRoom: function storeRoom(room) {}, + + /** + * No-op. + * @param {string} roomId + * @return {null} + */ + getRoom: function getRoom(roomId) { + return null; + }, + + /** + * No-op. + * @return {Array} An empty array. + */ + getRooms: function getRooms() { + return []; + }, + + /** + * Permanently delete a room. + * @param {string} roomId + */ + removeRoom: function removeRoom(roomId) { + return; + }, + + /** + * No-op. + * @return {Array} An empty array. + */ + getRoomSummaries: function getRoomSummaries() { + return []; + }, + + /** + * No-op. + * @param {User} user + */ + storeUser: function storeUser(user) {}, + + /** + * No-op. + * @param {string} userId + * @return {null} + */ + getUser: function getUser(userId) { + return null; + }, + + /** + * No-op. + * @return {User[]} + */ + getUsers: function getUsers() { + return []; + }, + + /** + * No-op. + * @param {Room} room + * @param {integer} limit + * @return {Array} + */ + scrollback: function scrollback(room, limit) { + return []; + }, + + /** + * Store events for a room. + * @param {Room} room The room to store events for. + * @param {Array} events The events to store. + * @param {string} token The token associated with these events. + * @param {boolean} toStart True if these are paginated results. + */ + storeEvents: function storeEvents(room, events, token, toStart) {}, + + /** + * Store a filter. + * @param {Filter} filter + */ + storeFilter: function storeFilter(filter) {}, + + /** + * Retrieve a filter. + * @param {string} userId + * @param {string} filterId + * @return {?Filter} A filter or null. + */ + getFilter: function getFilter(userId, filterId) { + return null; + }, + + /** + * Retrieve a filter ID with the given name. + * @param {string} filterName The filter name. + * @return {?string} The filter ID or null. + */ + getFilterIdByName: function getFilterIdByName(filterName) { + return null; + }, + + /** + * Set a filter name to ID mapping. + * @param {string} filterName + * @param {string} filterId + */ + setFilterIdByName: function setFilterIdByName(filterName, filterId) {}, + + /** + * Store user-scoped account data events + * @param {Array} events The events to store. + */ + storeAccountDataEvents: function storeAccountDataEvents(events) {}, + + /** + * Get account data event by event type + * @param {string} eventType The event type being queried + */ + getAccountData: function getAccountData(eventType) {}, + + /** + * setSyncData does nothing as there is no backing data store. + * + * @param {Object} syncData The sync data + * @return {Promise} An immediately resolved promise. + */ + setSyncData: function setSyncData(syncData) { + return _bluebird2.default.resolve(); + }, + + /** + * We never want to save becase we have nothing to save to. + * + * @return {boolean} If the store wants to save + */ + wantsSave: function wantsSave() { + return false; + }, + + /** + * Save does nothing as there is no backing data store. + */ + save: function save() {}, + + /** + * Startup does nothing. + * @return {Promise} An immediately resolved promise. + */ + startup: function startup() { + return _bluebird2.default.resolve(); + }, + + /** + * @return {Promise} Resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + getSavedSync: function getSavedSync() { + return _bluebird2.default.resolve(null); + }, + + /** + * @return {Promise} If there is a saved sync, the nextBatch token + * for this sync, otherwise null. + */ + getSavedSyncToken: function getSavedSyncToken() { + return _bluebird2.default.resolve(null); + }, + + /** + * Delete all data from this store. Does nothing since this store + * doesn't store anything. + * @return {Promise} An immediately resolved promise. + */ + deleteAllData: function deleteAllData() { + return _bluebird2.default.resolve(); + } +}; + +/** Stub Store class. */ +module.exports = StubStore; + +},{"bluebird":74}],47:[function(require,module,exports){ +"use strict"; + +var _create = require("babel-runtime/core-js/object/create"); + +var _create2 = _interopRequireDefault(_create); + +var _keys = require("babel-runtime/core-js/object/keys"); + +var _keys2 = _interopRequireDefault(_keys); + +var _classCallCheck2 = require("babel-runtime/helpers/classCallCheck"); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +var _createClass2 = require("babel-runtime/helpers/createClass"); + +var _createClass3 = _interopRequireDefault(_createClass2); + +var _utils = require("./utils"); + +var _utils2 = _interopRequireDefault(_utils); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * The purpose of this class is to accumulate /sync responses such that a + * complete "initial" JSON response can be returned which accurately represents + * the sum total of the /sync responses accumulated to date. It only handles + * room data: that is, everything under the "rooms" top-level key. + * + * This class is used when persisting room data so a complete /sync response can + * be loaded from disk and incremental syncs can be performed on the server, + * rather than asking the server to do an initial sync on startup. + */ +var SyncAccumulator = function () { + + /** + * @param {Object} opts + * @param {Number=} opts.maxTimelineEntries The ideal maximum number of + * timeline entries to keep in the sync response. This is best-effort, as + * clients do not always have a back-pagination token for each event, so + * it's possible there may be slightly *less* than this value. There will + * never be more. This cannot be 0 or else it makes it impossible to scroll + * back in a room. Default: 50. + */ + function SyncAccumulator(opts) { + (0, _classCallCheck3.default)(this, SyncAccumulator); + + opts = opts || {}; + opts.maxTimelineEntries = opts.maxTimelineEntries || 50; + this.opts = opts; + this.accountData = { + //$event_type: Object + }; + this.inviteRooms = { + //$roomId: { ... sync 'invite' json data ... } + }; + this.joinRooms = { + //$roomId: { + // _currentState: { $event_type: { $state_key: json } }, + // _timeline: [ + // { event: $event, token: null|token }, + // { event: $event, token: null|token }, + // { event: $event, token: null|token }, + // ... + // ], + // _accountData: { $event_type: json }, + // _unreadNotifications: { ... unread_notifications JSON ... }, + // _readReceipts: { $user_id: { data: $json, eventId: $event_id }} + //} + }; + // the /sync token which corresponds to the last time rooms were + // accumulated. We remember this so that any caller can obtain a + // coherent /sync response and know at what point they should be + // streaming from without losing events. + this.nextBatch = null; + + // { ('invite'|'join'|'leave'): $groupId: { ... sync 'group' data } } + this.groups = { + invite: {}, + join: {}, + leave: {} + }; + } + + (0, _createClass3.default)(SyncAccumulator, [{ + key: "accumulate", + value: function accumulate(syncResponse) { + this._accumulateRooms(syncResponse); + this._accumulateGroups(syncResponse); + this._accumulateAccountData(syncResponse); + this.nextBatch = syncResponse.next_batch; + } + }, { + key: "_accumulateAccountData", + value: function _accumulateAccountData(syncResponse) { + var _this = this; + + if (!syncResponse.account_data || !syncResponse.account_data.events) { + return; + } + // Clobbers based on event type. + syncResponse.account_data.events.forEach(function (e) { + _this.accountData[e.type] = e; + }); + } + + /** + * Accumulate incremental /sync room data. + * @param {Object} syncResponse the complete /sync JSON + */ + + }, { + key: "_accumulateRooms", + value: function _accumulateRooms(syncResponse) { + var _this2 = this; + + if (!syncResponse.rooms) { + return; + } + if (syncResponse.rooms.invite) { + (0, _keys2.default)(syncResponse.rooms.invite).forEach(function (roomId) { + _this2._accumulateRoom(roomId, "invite", syncResponse.rooms.invite[roomId]); + }); + } + if (syncResponse.rooms.join) { + (0, _keys2.default)(syncResponse.rooms.join).forEach(function (roomId) { + _this2._accumulateRoom(roomId, "join", syncResponse.rooms.join[roomId]); + }); + } + if (syncResponse.rooms.leave) { + (0, _keys2.default)(syncResponse.rooms.leave).forEach(function (roomId) { + _this2._accumulateRoom(roomId, "leave", syncResponse.rooms.leave[roomId]); + }); + } + } + }, { + key: "_accumulateRoom", + value: function _accumulateRoom(roomId, category, data) { + // Valid /sync state transitions + // +--------+ <======+ 1: Accept an invite + // +== | INVITE | | (5) 2: Leave a room + // | +--------+ =====+ | 3: Join a public room previously + // |(1) (4) | | left (handle as if new room) + // V (2) V | 4: Reject an invite + // +------+ ========> +--------+ 5: Invite to a room previously + // | JOIN | (3) | LEAVE* | left (handle as if new room) + // +------+ <======== +--------+ + // + // * equivalent to "no state" + switch (category) { + case "invite": + // (5) + this._accumulateInviteState(roomId, data); + break; + case "join": + if (this.inviteRooms[roomId]) { + // (1) + // was previously invite, now join. We expect /sync to give + // the entire state and timeline on 'join', so delete previous + // invite state + delete this.inviteRooms[roomId]; + } + // (3) + this._accumulateJoinState(roomId, data); + break; + case "leave": + if (this.inviteRooms[roomId]) { + // (4) + delete this.inviteRooms[roomId]; + } else { + // (2) + delete this.joinRooms[roomId]; + } + break; + default: + console.error("Unknown cateogory: ", category); + } + } + }, { + key: "_accumulateInviteState", + value: function _accumulateInviteState(roomId, data) { + if (!data.invite_state || !data.invite_state.events) { + // no new data + return; + } + if (!this.inviteRooms[roomId]) { + this.inviteRooms[roomId] = { + invite_state: data.invite_state + }; + return; + } + // accumulate extra keys for invite->invite transitions + // clobber based on event type / state key + // We expect invite_state to be small, so just loop over the events + var currentData = this.inviteRooms[roomId]; + data.invite_state.events.forEach(function (e) { + var hasAdded = false; + for (var i = 0; i < currentData.invite_state.events.length; i++) { + var current = currentData.invite_state.events[i]; + if (current.type === e.type && current.state_key == e.state_key) { + currentData.invite_state.events[i] = e; // update + hasAdded = true; + } + } + if (!hasAdded) { + currentData.invite_state.events.push(e); + } + }); + } + + // Accumulate timeline and state events in a room. + + }, { + key: "_accumulateJoinState", + value: function _accumulateJoinState(roomId, data) { + // We expect this function to be called a lot (every /sync) so we want + // this to be fast. /sync stores events in an array but we often want + // to clobber based on type/state_key. Rather than convert arrays to + // maps all the time, just keep private maps which contain + // the actual current accumulated sync state, and array-ify it when + // getJSON() is called. + + // State resolution: + // The 'state' key is the delta from the previous sync (or start of time + // if no token was supplied), to the START of the timeline. To obtain + // the current state, we need to "roll forward" state by reading the + // timeline. We want to store the current state so we can drop events + // out the end of the timeline based on opts.maxTimelineEntries. + // + // 'state' 'timeline' current state + // |-------x<======================>x + // T I M E + // + // When getJSON() is called, we 'roll back' the current state by the + // number of entries in the timeline to work out what 'state' should be. + + // Back-pagination: + // On an initial /sync, the server provides a back-pagination token for + // the start of the timeline. When /sync deltas come down, they also + // include back-pagination tokens for the start of the timeline. This + // means not all events in the timeline have back-pagination tokens, as + // it is only the ones at the START of the timeline which have them. + // In order for us to have a valid timeline (and back-pagination token + // to match), we need to make sure that when we remove old timeline + // events, that we roll forward to an event which has a back-pagination + // token. This means we can't keep a strict sliding-window based on + // opts.maxTimelineEntries, and we may have a few less. We should never + // have more though, provided that the /sync limit is less than or equal + // to opts.maxTimelineEntries. + + if (!this.joinRooms[roomId]) { + // Create truly empty objects so event types of 'hasOwnProperty' and co + // don't cause this code to break. + this.joinRooms[roomId] = { + _currentState: (0, _create2.default)(null), + _timeline: [], + _accountData: (0, _create2.default)(null), + _unreadNotifications: {}, + _readReceipts: {} + }; + } + var currentData = this.joinRooms[roomId]; + + if (data.account_data && data.account_data.events) { + // clobber based on type + data.account_data.events.forEach(function (e) { + currentData._accountData[e.type] = e; + }); + } + + // these probably clobber, spec is unclear. + if (data.unread_notifications) { + currentData._unreadNotifications = data.unread_notifications; + } + + if (data.ephemeral && data.ephemeral.events) { + data.ephemeral.events.forEach(function (e) { + // We purposefully do not persist m.typing events. + // Technically you could refresh a browser before the timer on a + // typing event is up, so it'll look like you aren't typing when + // you really still are. However, the alternative is worse. If + // we do persist typing events, it will look like people are + // typing forever until someone really does start typing (which + // will prompt Synapse to send down an actual m.typing event to + // clobber the one we persisted). + if (e.type !== "m.receipt" || !e.content) { + // This means we'll drop unknown ephemeral events but that + // seems okay. + return; + } + // Handle m.receipt events. They clobber based on: + // (user_id, receipt_type) + // but they are keyed in the event as: + // content:{ $event_id: { $receipt_type: { $user_id: {json} }}} + // so store them in the former so we can accumulate receipt deltas + // quickly and efficiently (we expect a lot of them). Fold the + // receipt type into the key name since we only have 1 at the + // moment (m.read) and nested JSON objects are slower and more + // of a hassle to work with. We'll inflate this back out when + // getJSON() is called. + (0, _keys2.default)(e.content).forEach(function (eventId) { + if (!e.content[eventId]["m.read"]) { + return; + } + (0, _keys2.default)(e.content[eventId]["m.read"]).forEach(function (userId) { + // clobber on user ID + currentData._readReceipts[userId] = { + data: e.content[eventId]["m.read"][userId], + eventId: eventId + }; + }); + }); + }); + } + + // if we got a limited sync, we need to remove all timeline entries or else + // we will have gaps in the timeline. + if (data.timeline && data.timeline.limited) { + currentData._timeline = []; + } + + // Work out the current state. The deltas need to be applied in the order: + // - existing state which didn't come down /sync. + // - State events under the 'state' key. + // - State events in the 'timeline'. + if (data.state && data.state.events) { + data.state.events.forEach(function (e) { + setState(currentData._currentState, e); + }); + } + if (data.timeline && data.timeline.events) { + data.timeline.events.forEach(function (e, index) { + // this nops if 'e' isn't a state event + setState(currentData._currentState, e); + // append the event to the timeline. The back-pagination token + // corresponds to the first event in the timeline + currentData._timeline.push({ + event: e, + token: index === 0 ? data.timeline.prev_batch : null + }); + }); + } + + // attempt to prune the timeline by jumping between events which have + // pagination tokens. + if (currentData._timeline.length > this.opts.maxTimelineEntries) { + var startIndex = currentData._timeline.length - this.opts.maxTimelineEntries; + for (var i = startIndex; i < currentData._timeline.length; i++) { + if (currentData._timeline[i].token) { + // keep all events after this, including this one + currentData._timeline = currentData._timeline.slice(i, currentData._timeline.length); + break; + } + } + } + } + + /** + * Accumulate incremental /sync group data. + * @param {Object} syncResponse the complete /sync JSON + */ + + }, { + key: "_accumulateGroups", + value: function _accumulateGroups(syncResponse) { + var _this3 = this; + + if (!syncResponse.groups) { + return; + } + if (syncResponse.groups.invite) { + (0, _keys2.default)(syncResponse.groups.invite).forEach(function (groupId) { + _this3._accumulateGroup(groupId, "invite", syncResponse.groups.invite[groupId]); + }); + } + if (syncResponse.groups.join) { + (0, _keys2.default)(syncResponse.groups.join).forEach(function (groupId) { + _this3._accumulateGroup(groupId, "join", syncResponse.groups.join[groupId]); + }); + } + if (syncResponse.groups.leave) { + (0, _keys2.default)(syncResponse.groups.leave).forEach(function (groupId) { + _this3._accumulateGroup(groupId, "leave", syncResponse.groups.leave[groupId]); + }); + } + } + }, { + key: "_accumulateGroup", + value: function _accumulateGroup(groupId, category, data) { + var _arr = ['invite', 'join', 'leave']; + + for (var _i = 0; _i < _arr.length; _i++) { + var cat = _arr[_i]; + delete this.groups[cat][groupId]; + } + this.groups[category][groupId] = data; + } + + /** + * Return everything under the 'rooms' key from a /sync response which + * represents all room data that should be stored. This should be paired + * with the sync token which represents the most recent /sync response + * provided to accumulate(). + * @return {Object} An object with a "nextBatch", "roomsData" and "accountData" + * keys. + * The "nextBatch" key is a string which represents at what point in the + * /sync stream the accumulator reached. This token should be used when + * restarting a /sync stream at startup. Failure to do so can lead to missing + * events. The "roomsData" key is an Object which represents the entire + * /sync response from the 'rooms' key onwards. The "accountData" key is + * a list of raw events which represent global account data. + */ + + }, { + key: "getJSON", + value: function getJSON() { + var _this4 = this; + + var data = { + join: {}, + invite: {}, + // always empty. This is set by /sync when a room was previously + // in 'invite' or 'join'. On fresh startup, the client won't know + // about any previous room being in 'invite' or 'join' so we can + // just omit mentioning it at all, even if it has previously come + // down /sync. + // The notable exception is when a client is kicked or banned: + // we may want to hold onto that room so the client can clearly see + // why their room has disappeared. We don't persist it though because + // it is unclear *when* we can safely remove the room from the DB. + // Instead, we assume that if you're loading from the DB, you've + // refreshed the page, which means you've seen the kick/ban already. + leave: {} + }; + (0, _keys2.default)(this.inviteRooms).forEach(function (roomId) { + data.invite[roomId] = _this4.inviteRooms[roomId]; + }); + (0, _keys2.default)(this.joinRooms).forEach(function (roomId) { + var roomData = _this4.joinRooms[roomId]; + var roomJson = { + ephemeral: { events: [] }, + account_data: { events: [] }, + state: { events: [] }, + timeline: { + events: [], + prev_batch: null + }, + unread_notifications: roomData._unreadNotifications + }; + // Add account data + (0, _keys2.default)(roomData._accountData).forEach(function (evType) { + roomJson.account_data.events.push(roomData._accountData[evType]); + }); + + // Add receipt data + var receiptEvent = { + type: "m.receipt", + room_id: roomId, + content: { + // $event_id: { "m.read": { $user_id: $json } } + } + }; + (0, _keys2.default)(roomData._readReceipts).forEach(function (userId) { + var receiptData = roomData._readReceipts[userId]; + if (!receiptEvent.content[receiptData.eventId]) { + receiptEvent.content[receiptData.eventId] = { + "m.read": {} + }; + } + receiptEvent.content[receiptData.eventId]["m.read"][userId] = receiptData.data; + }); + // add only if we have some receipt data + if ((0, _keys2.default)(receiptEvent.content).length > 0) { + roomJson.ephemeral.events.push(receiptEvent); + } + + // Add timeline data + roomData._timeline.forEach(function (msgData) { + if (!roomJson.timeline.prev_batch) { + // the first event we add to the timeline MUST match up to + // the prev_batch token. + if (!msgData.token) { + return; // this shouldn't happen as we prune constantly. + } + roomJson.timeline.prev_batch = msgData.token; + } + roomJson.timeline.events.push(msgData.event); + }); + + // Add state data: roll back current state to the start of timeline, + // by "reverse clobbering" from the end of the timeline to the start. + // Convert maps back into arrays. + var rollBackState = (0, _create2.default)(null); + for (var i = roomJson.timeline.events.length - 1; i >= 0; i--) { + var timelineEvent = roomJson.timeline.events[i]; + if (timelineEvent.state_key === null || timelineEvent.state_key === undefined) { + continue; // not a state event + } + // since we're going back in time, we need to use the previous + // state value else we'll break causality. We don't have the + // complete previous state event, so we need to create one. + var prevStateEvent = _utils2.default.deepCopy(timelineEvent); + if (prevStateEvent.unsigned) { + if (prevStateEvent.unsigned.prev_content) { + prevStateEvent.content = prevStateEvent.unsigned.prev_content; + } + if (prevStateEvent.unsigned.prev_sender) { + prevStateEvent.sender = prevStateEvent.unsigned.prev_sender; + } + } + setState(rollBackState, prevStateEvent); + } + (0, _keys2.default)(roomData._currentState).forEach(function (evType) { + (0, _keys2.default)(roomData._currentState[evType]).forEach(function (stateKey) { + var ev = roomData._currentState[evType][stateKey]; + if (rollBackState[evType] && rollBackState[evType][stateKey]) { + // use the reverse clobbered event instead. + ev = rollBackState[evType][stateKey]; + } + roomJson.state.events.push(ev); + }); + }); + data.join[roomId] = roomJson; + }); + + // Add account data + var accData = []; + (0, _keys2.default)(this.accountData).forEach(function (evType) { + accData.push(_this4.accountData[evType]); + }); + + return { + nextBatch: this.nextBatch, + roomsData: data, + groupsData: this.groups, + accountData: accData + }; + } + }, { + key: "getNextBatchToken", + value: function getNextBatchToken() { + return this.nextBatch; + } + }]); + return SyncAccumulator; +}(); /* + Copyright 2017 Vector Creations Ltd + Copyright 2018 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +/** + * This is an internal module. See {@link SyncAccumulator} for the public class. + * @module sync-accumulator + */ + +function setState(eventMap, event) { + if (event.state_key === null || event.state_key === undefined || !event.type) { + return; + } + if (!eventMap[event.type]) { + eventMap[event.type] = (0, _create2.default)(null); + } + eventMap[event.type][event.state_key] = event; +} + +module.exports = SyncAccumulator; + +},{"./utils":50,"babel-runtime/core-js/object/create":57,"babel-runtime/core-js/object/keys":61,"babel-runtime/helpers/classCallCheck":66,"babel-runtime/helpers/createClass":67}],48:[function(require,module,exports){ +(function (global){ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/* + * TODO: + * This class mainly serves to take all the syncing logic out of client.js and + * into a separate file. It's all very fluid, and this class gut wrenches a lot + * of MatrixClient props (e.g. _http). Given we want to support WebSockets as + * an alternative syncing API, we may want to have a proper syncing interface + * for HTTP and WS at some point. + */ + +var _stringify = require("babel-runtime/core-js/json/stringify"); + +var _stringify2 = _interopRequireDefault(_stringify); + +var _keys = require("babel-runtime/core-js/object/keys"); + +var _keys2 = _interopRequireDefault(_keys); + +var _getIterator2 = require("babel-runtime/core-js/get-iterator"); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +var _regenerator = require("babel-runtime/regenerator"); + +var _regenerator2 = _interopRequireDefault(_regenerator); + +var _bluebird = require("bluebird"); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var User = require("./models/user"); +var Room = require("./models/room"); +var Group = require('./models/group'); +var utils = require("./utils"); +var Filter = require("./filter"); +var EventTimeline = require("./models/event-timeline"); + +var DEBUG = true; + +// /sync requests allow you to set a timeout= but the request may continue +// beyond that and wedge forever, so we need to track how long we are willing +// to keep open the connection. This constant is *ADDED* to the timeout= value +// to determine the max time we're willing to wait. +var BUFFER_PERIOD_MS = 80 * 1000; + +// Number of consecutive failed syncs that will lead to a syncState of ERROR as opposed +// to RECONNECTING. This is needed to inform the client of server issues when the +// keepAlive is successful but the server /sync fails. +var FAILED_SYNC_ERROR_THRESHOLD = 3; + +function getFilterName(userId, suffix) { + // scope this on the user ID because people may login on many accounts + // and they all need to be stored! + return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : ""); +} + +function debuglog() { + var _console; + + if (!DEBUG) { + return; + } + (_console = console).log.apply(_console, arguments); +} + +/** + * Internal class - unstable. + * Construct an entity which is able to sync with a homeserver. + * @constructor + * @param {MatrixClient} client The matrix client instance to use. + * @param {Object} opts Config options + * @param {module:crypto=} opts.crypto Crypto manager + * @param {Function=} opts.canResetEntireTimeline A function which is called + * with a room ID and returns a boolean. It should return 'true' if the SDK can + * SAFELY remove events from this room. It may not be safe to remove events if + * there are other references to the timelines for this room. + * Default: returns false. + * @param {Boolean=} opts.disablePresence True to perform syncing without automatically + * updating presence. + */ +function SyncApi(client, opts) { + this.client = client; + opts = opts || {}; + opts.initialSyncLimit = opts.initialSyncLimit === undefined ? 8 : opts.initialSyncLimit; + opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false; + opts.pollTimeout = opts.pollTimeout || 30 * 1000; + opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological"; + if (!opts.canResetEntireTimeline) { + opts.canResetEntireTimeline = function (roomId) { + return false; + }; + } + this.opts = opts; + this._peekRoomId = null; + this._currentSyncRequest = null; + this._syncState = null; + this._catchingUp = false; + this._running = false; + this._keepAliveTimer = null; + this._connectionReturnedDefer = null; + this._notifEvents = []; // accumulator of sync events in the current sync response + this._failedSyncCount = 0; // Number of consecutive failed /sync requests + + if (client.getNotifTimelineSet()) { + client.reEmitter.reEmit(client.getNotifTimelineSet(), ["Room.timeline", "Room.timelineReset"]); + } +} + +/** + * @param {string} roomId + * @return {Room} + */ +SyncApi.prototype.createRoom = function (roomId) { + var client = this.client; + var room = new Room(roomId, { + pendingEventOrdering: this.opts.pendingEventOrdering, + timelineSupport: client.timelineSupport + }); + client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", "Room.redaction", "Room.receipt", "Room.tags", "Room.timelineReset", "Room.localEchoUpdated", "Room.accountData"]); + this._registerStateListeners(room); + return room; +}; + +/** + * @param {string} groupId + * @return {Group} + */ +SyncApi.prototype.createGroup = function (groupId) { + var client = this.client; + var group = new Group(groupId); + client.reEmitter.reEmit(group, ["Group.profile", "Group.myMembership"]); + client.store.storeGroup(group); + return group; +}; + +/** + * @param {Room} room + * @private + */ +SyncApi.prototype._registerStateListeners = function (room) { + var client = this.client; + // we need to also re-emit room state and room member events, so hook it up + // to the client now. We need to add a listener for RoomState.members in + // order to hook them correctly. (TODO: find a better way?) + client.reEmitter.reEmit(room.currentState, ["RoomState.events", "RoomState.members", "RoomState.newMember"]); + room.currentState.on("RoomState.newMember", function (event, state, member) { + member.user = client.getUser(member.userId); + client.reEmitter.reEmit(member, ["RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel", "RoomMember.membership"]); + }); +}; + +/** + * @param {Room} room + * @private + */ +SyncApi.prototype._deregisterStateListeners = function (room) { + // could do with a better way of achieving this. + room.currentState.removeAllListeners("RoomState.events"); + room.currentState.removeAllListeners("RoomState.members"); + room.currentState.removeAllListeners("RoomState.newMember"); +}; + +/** + * Sync rooms the user has left. + * @return {Promise} Resolved when they've been added to the store. + */ +SyncApi.prototype.syncLeftRooms = function () { + var client = this.client; + var self = this; + + // grab a filter with limit=1 and include_leave=true + var filter = new Filter(this.client.credentials.userId); + filter.setTimelineLimit(1); + filter.setIncludeLeaveRooms(true); + + var localTimeoutMs = this.opts.pollTimeout + BUFFER_PERIOD_MS; + var qps = { + timeout: 0 // don't want to block since this is a single isolated req + }; + + return client.getOrCreateFilter(getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter).then(function (filterId) { + qps.filter = filterId; + return client._http.authedRequest(undefined, "GET", "/sync", qps, undefined, localTimeoutMs); + }).then(function (data) { + var leaveRooms = []; + if (data.rooms && data.rooms.leave) { + leaveRooms = self._mapSyncResponseToRoomArray(data.rooms.leave); + } + var rooms = []; + leaveRooms.forEach(function (leaveObj) { + var room = leaveObj.room; + rooms.push(room); + if (!leaveObj.isBrandNewRoom) { + // the intention behind syncLeftRooms is to add in rooms which were + // *omitted* from the initial /sync. Rooms the user were joined to + // but then left whilst the app is running will appear in this list + // and we do not want to bother with them since they will have the + // current state already (and may get dupe messages if we add + // yet more timeline events!), so skip them. + // NB: When we persist rooms to localStorage this will be more + // complicated... + return; + } + leaveObj.timeline = leaveObj.timeline || {}; + var timelineEvents = self._mapSyncEventsFormat(leaveObj.timeline, room); + var stateEvents = self._mapSyncEventsFormat(leaveObj.state, room); + + // set the back-pagination token. Do this *before* adding any + // events so that clients can start back-paginating. + room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS); + + self._processRoomEvents(room, stateEvents, timelineEvents); + + room.recalculate(client.credentials.userId); + client.store.storeRoom(room); + client.emit("Room", room); + + self._processEventsForNotifs(room, timelineEvents); + }); + return rooms; + }); +}; + +/** + * Peek into a room. This will result in the room in question being synced so it + * is accessible via getRooms(). Live updates for the room will be provided. + * @param {string} roomId The room ID to peek into. + * @return {Promise} A promise which resolves once the room has been added to the + * store. + */ +SyncApi.prototype.peek = function (roomId) { + var self = this; + var client = this.client; + this._peekRoomId = roomId; + return this.client.roomInitialSync(roomId, 20).then(function (response) { + // make sure things are init'd + response.messages = response.messages || {}; + response.messages.chunk = response.messages.chunk || []; + response.state = response.state || []; + + var peekRoom = self.createRoom(roomId); + + // FIXME: Mostly duplicated from _processRoomEvents but not entirely + // because "state" in this API is at the BEGINNING of the chunk + var oldStateEvents = utils.map(utils.deepCopy(response.state), client.getEventMapper()); + var stateEvents = utils.map(response.state, client.getEventMapper()); + var messages = utils.map(response.messages.chunk, client.getEventMapper()); + + // XXX: copypasted from /sync until we kill off this + // minging v1 API stuff) + // handle presence events (User objects) + if (response.presence && utils.isArray(response.presence)) { + response.presence.map(client.getEventMapper()).forEach(function (presenceEvent) { + var user = client.store.getUser(presenceEvent.getContent().user_id); + if (user) { + user.setPresenceEvent(presenceEvent); + } else { + user = createNewUser(client, presenceEvent.getContent().user_id); + user.setPresenceEvent(presenceEvent); + client.store.storeUser(user); + } + client.emit("event", presenceEvent); + }); + } + + // set the pagination token before adding the events in case people + // fire off pagination requests in response to the Room.timeline + // events. + if (response.messages.start) { + peekRoom.oldState.paginationToken = response.messages.start; + } + + // set the state of the room to as it was after the timeline executes + peekRoom.oldState.setStateEvents(oldStateEvents); + peekRoom.currentState.setStateEvents(stateEvents); + + self._resolveInvites(peekRoom); + peekRoom.recalculate(self.client.credentials.userId); + + // roll backwards to diverge old state. addEventsToTimeline + // will overwrite the pagination token, so make sure it overwrites + // it with the right thing. + peekRoom.addEventsToTimeline(messages.reverse(), true, peekRoom.getLiveTimeline(), response.messages.start); + + client.store.storeRoom(peekRoom); + client.emit("Room", peekRoom); + + self._peekPoll(peekRoom); + return peekRoom; + }); +}; + +/** + * Stop polling for updates in the peeked room. NOPs if there is no room being + * peeked. + */ +SyncApi.prototype.stopPeeking = function () { + this._peekRoomId = null; +}; + +/** + * Do a peek room poll. + * @param {Room} peekRoom + * @param {string} token from= token + */ +SyncApi.prototype._peekPoll = function (peekRoom, token) { + if (this._peekRoomId !== peekRoom.roomId) { + debuglog("Stopped peeking in room %s", peekRoom.roomId); + return; + } + + var self = this; + // FIXME: gut wrenching; hard-coded timeout values + this.client._http.authedRequest(undefined, "GET", "/events", { + room_id: peekRoom.roomId, + timeout: 30 * 1000, + from: token + }, undefined, 50 * 1000).done(function (res) { + if (self._peekRoomId !== peekRoom.roomId) { + debuglog("Stopped peeking in room %s", peekRoom.roomId); + return; + } + // We have a problem that we get presence both from /events and /sync + // however, /sync only returns presence for users in rooms + // you're actually joined to. + // in order to be sure to get presence for all of the users in the + // peeked room, we handle presence explicitly here. This may result + // in duplicate presence events firing for some users, which is a + // performance drain, but such is life. + // XXX: copypasted from /sync until we can kill this minging v1 stuff. + + res.chunk.filter(function (e) { + return e.type === "m.presence"; + }).map(self.client.getEventMapper()).forEach(function (presenceEvent) { + var user = self.client.store.getUser(presenceEvent.getContent().user_id); + if (user) { + user.setPresenceEvent(presenceEvent); + } else { + user = createNewUser(self.client, presenceEvent.getContent().user_id); + user.setPresenceEvent(presenceEvent); + self.client.store.storeUser(user); + } + self.client.emit("event", presenceEvent); + }); + + // strip out events which aren't for the given room_id (e.g presence) + var events = res.chunk.filter(function (e) { + return e.room_id === peekRoom.roomId; + }).map(self.client.getEventMapper()); + + peekRoom.addLiveEvents(events); + self._peekPoll(peekRoom, res.end); + }, function (err) { + console.error("[%s] Peek poll failed: %s", peekRoom.roomId, err); + setTimeout(function () { + self._peekPoll(peekRoom, token); + }, 30 * 1000); + }); +}; + +/** + * Returns the current state of this sync object + * @see module:client~MatrixClient#event:"sync" + * @return {?String} + */ +SyncApi.prototype.getSyncState = function () { + return this._syncState; +}; + +SyncApi.prototype.recoverFromSyncStartupError = function () { + var _ref = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee(savedSyncPromise, err) { + var keepaliveProm; + return _regenerator2.default.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + _context.next = 2; + return (0, _bluebird.resolve)(savedSyncPromise); + + case 2: + keepaliveProm = this._startKeepAlives(); + + this._updateSyncState("ERROR", { error: err }); + _context.next = 6; + return (0, _bluebird.resolve)(keepaliveProm); + + case 6: + case "end": + return _context.stop(); + } + } + }, _callee, this); + })); + + return function (_x, _x2) { + return _ref.apply(this, arguments); + }; +}(); + +/** + * Main entry point + */ +SyncApi.prototype.sync = function () { + + // We need to do one-off checks before we can begin the /sync loop. + // These are: + // 1) We need to get push rules so we can check if events should bing as we get + // them from /sync. + // 2) We need to get/create a filter which we can use for /sync. + + var getPushRules = function () { + var _ref2 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee2() { + var result; + return _regenerator2.default.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + _context2.prev = 0; + _context2.next = 3; + return (0, _bluebird.resolve)(client.getPushRules()); + + case 3: + result = _context2.sent; + + debuglog("Got push rules"); + + client.pushRules = result; + _context2.next = 14; + break; + + case 8: + _context2.prev = 8; + _context2.t0 = _context2["catch"](0); + _context2.next = 12; + return (0, _bluebird.resolve)(self.recoverFromSyncStartupError(savedSyncPromise, _context2.t0)); + + case 12: + getPushRules(); + return _context2.abrupt("return"); + + case 14: + getFilter(); // Now get the filter and start syncing + + case 15: + case "end": + return _context2.stop(); + } + } + }, _callee2, this, [[0, 8]]); + })); + + return function getPushRules() { + return _ref2.apply(this, arguments); + }; + }(); + + var getFilter = function () { + var _ref3 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee3() { + var filter, filterId; + return _regenerator2.default.wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + filter = void 0; + + if (self.opts.filter) { + filter = self.opts.filter; + } else { + filter = new Filter(client.credentials.userId); + filter.setTimelineLimit(self.opts.initialSyncLimit); + } + + filterId = void 0; + _context3.prev = 3; + _context3.next = 6; + return (0, _bluebird.resolve)(client.getOrCreateFilter(getFilterName(client.credentials.userId), filter)); + + case 6: + filterId = _context3.sent; + _context3.next = 15; + break; + + case 9: + _context3.prev = 9; + _context3.t0 = _context3["catch"](3); + _context3.next = 13; + return (0, _bluebird.resolve)(self.recoverFromSyncStartupError(savedSyncPromise, _context3.t0)); + + case 13: + getFilter(); + return _context3.abrupt("return"); + + case 15: + // reset the notifications timeline to prepare it to paginate from + // the current point in time. + // The right solution would be to tie /sync pagination tokens into + // /notifications API somehow. + client.resetNotifTimelineSet(); + + if (self._currentSyncRequest === null) { + // Send this first sync request here so we can then wait for the saved + // sync data to finish processing before we process the results of this one. + console.log("Sending first sync request..."); + self._currentSyncRequest = self._doSyncRequest({ filterId: filterId }, savedSyncToken); + } + + // Now wait for the saved sync to finish... + _context3.next = 19; + return (0, _bluebird.resolve)(savedSyncPromise); + + case 19: + self._sync({ filterId: filterId }); + + case 20: + case "end": + return _context3.stop(); + } + } + }, _callee3, this, [[3, 9]]); + })); + + return function getFilter() { + return _ref3.apply(this, arguments); + }; + }(); + + var client = this.client; + var self = this; + + this._running = true; + + if (global.document) { + this._onOnlineBound = this._onOnline.bind(this); + global.document.addEventListener("online", this._onOnlineBound, false); + } + + var savedSyncPromise = _bluebird2.default.resolve(); + var savedSyncToken = null; + + if (client.isGuest()) { + // no push rules for guests, no access to POST filter for guests. + self._sync({}); + } else { + // Pull the saved sync token out first, before the worker starts sending + // all the sync data which could take a while. This will let us send our + // first incremental sync request before we've processed our saved data. + savedSyncPromise = client.store.getSavedSyncToken().then(function (tok) { + savedSyncToken = tok; + return client.store.getSavedSync(); + }).then(function (savedSync) { + if (savedSync) { + return self._syncFromCache(savedSync); + } + }); + // Now start the first incremental sync request: this can also + // take a while so if we set it going now, we can wait for it + // to finish while we process our saved sync data. + getPushRules(); + } +}; + +/** + * Stops the sync object from syncing. + */ +SyncApi.prototype.stop = function () { + debuglog("SyncApi.stop"); + if (global.document) { + global.document.removeEventListener("online", this._onOnlineBound, false); + this._onOnlineBound = undefined; + } + this._running = false; + if (this._currentSyncRequest) { + this._currentSyncRequest.abort(); + } + if (this._keepAliveTimer) { + clearTimeout(this._keepAliveTimer); + this._keepAliveTimer = null; + } +}; + +/** + * Retry a backed off syncing request immediately. This should only be used when + * the user explicitly attempts to retry their lost connection. + * @return {boolean} True if this resulted in a request being retried. + */ +SyncApi.prototype.retryImmediately = function () { + if (!this._connectionReturnedDefer) { + return false; + } + this._startKeepAlives(0); + return true; +}; +/** + * Process a single set of cached sync data. + * @param {Object} savedSync a saved sync that was persisted by a store. This + * should have been acquired via client.store.getSavedSync(). + */ +SyncApi.prototype._syncFromCache = function () { + var _ref4 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee4(savedSync) { + var nextSyncToken, syncEventData, data; + return _regenerator2.default.wrap(function _callee4$(_context4) { + while (1) { + switch (_context4.prev = _context4.next) { + case 0: + debuglog("sync(): not doing HTTP hit, instead returning stored /sync data"); + + nextSyncToken = savedSync.nextBatch; + + // Set sync token for future incremental syncing + + this.client.store.setSyncToken(nextSyncToken); + + // No previous sync, set old token to null + syncEventData = { + oldSyncToken: null, + nextSyncToken: nextSyncToken, + catchingUp: false + }; + data = { + next_batch: nextSyncToken, + rooms: savedSync.roomsData, + groups: savedSync.groupsData, + account_data: { + events: savedSync.accountData + } + }; + _context4.prev = 5; + _context4.next = 8; + return (0, _bluebird.resolve)(this._processSyncResponse(syncEventData, data)); + + case 8: + _context4.next = 13; + break; + + case 10: + _context4.prev = 10; + _context4.t0 = _context4["catch"](5); + + console.error("Error processing cached sync", _context4.t0.stack || _context4.t0); + + case 13: + + this._updateSyncState("PREPARED", syncEventData); + + case 14: + case "end": + return _context4.stop(); + } + } + }, _callee4, this, [[5, 10]]); + })); + + return function (_x3) { + return _ref4.apply(this, arguments); + }; +}(); + +/** + * Invoke me to do /sync calls + * @param {Object} syncOptions + * @param {string} syncOptions.filterId + * @param {boolean} syncOptions.hasSyncedBefore + */ +SyncApi.prototype._sync = function () { + var _ref5 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee5(syncOptions) { + var client, syncToken, data, syncEventData; + return _regenerator2.default.wrap(function _callee5$(_context5) { + while (1) { + switch (_context5.prev = _context5.next) { + case 0: + client = this.client; + + if (this._running) { + _context5.next = 6; + break; + } + + debuglog("Sync no longer running: exiting."); + if (this._connectionReturnedDefer) { + this._connectionReturnedDefer.reject(); + this._connectionReturnedDefer = null; + } + this._updateSyncState("STOPPED"); + return _context5.abrupt("return"); + + case 6: + syncToken = client.store.getSyncToken(); + data = void 0; + _context5.prev = 8; + + //debuglog('Starting sync since=' + syncToken); + if (this._currentSyncRequest === null) { + this._currentSyncRequest = this._doSyncRequest(syncOptions, syncToken); + } + _context5.next = 12; + return (0, _bluebird.resolve)(this._currentSyncRequest); + + case 12: + data = _context5.sent; + _context5.next = 19; + break; + + case 15: + _context5.prev = 15; + _context5.t0 = _context5["catch"](8); + + this._onSyncError(_context5.t0, syncOptions); + return _context5.abrupt("return"); + + case 19: + _context5.prev = 19; + + this._currentSyncRequest = null; + return _context5.finish(19); + + case 22: + + //debuglog('Completed sync, next_batch=' + data.next_batch); + + // set the sync token NOW *before* processing the events. We do this so + // if something barfs on an event we can skip it rather than constantly + // polling with the same token. + client.store.setSyncToken(data.next_batch); + + // Reset after a successful sync + this._failedSyncCount = 0; + + _context5.next = 26; + return (0, _bluebird.resolve)(client.store.setSyncData(data)); + + case 26: + syncEventData = { + oldSyncToken: syncToken, + nextSyncToken: data.next_batch, + catchingUp: this._catchingUp + }; + + if (!this.opts.crypto) { + _context5.next = 30; + break; + } + + _context5.next = 30; + return (0, _bluebird.resolve)(this.opts.crypto.onSyncWillProcess(syncEventData)); + + case 30: + _context5.prev = 30; + _context5.next = 33; + return (0, _bluebird.resolve)(this._processSyncResponse(syncEventData, data)); + + case 33: + _context5.next = 38; + break; + + case 35: + _context5.prev = 35; + _context5.t1 = _context5["catch"](30); + + // log the exception with stack if we have it, else fall back + // to the plain description + console.error("Caught /sync error", _context5.t1.stack || _context5.t1); + + case 38: + + // update this as it may have changed + syncEventData.catchingUp = this._catchingUp; + + // emit synced events + if (!syncOptions.hasSyncedBefore) { + this._updateSyncState("PREPARED", syncEventData); + syncOptions.hasSyncedBefore = true; + } + + // tell the crypto module to do its processing. It may block (to do a + // /keys/changes request). + + if (!this.opts.crypto) { + _context5.next = 43; + break; + } + + _context5.next = 43; + return (0, _bluebird.resolve)(this.opts.crypto.onSyncCompleted(syncEventData)); + + case 43: + + // keep emitting SYNCING -> SYNCING for clients who want to do bulk updates + this._updateSyncState("SYNCING", syncEventData); + + if (!client.store.wantsSave()) { + _context5.next = 49; + break; + } + + if (!this.opts.crypto) { + _context5.next = 48; + break; + } + + _context5.next = 48; + return (0, _bluebird.resolve)(this.opts.crypto.saveDeviceList(0)); + + case 48: + + // tell databases that everything is now in a consistent state and can be saved. + client.store.save(); + + case 49: + + // Begin next sync + this._sync(syncOptions); + + case 50: + case "end": + return _context5.stop(); + } + } + }, _callee5, this, [[8, 15, 19, 22], [30, 35]]); + })); + + return function (_x4) { + return _ref5.apply(this, arguments); + }; +}(); + +SyncApi.prototype._doSyncRequest = function (syncOptions, syncToken) { + var qps = this._getSyncParams(syncOptions, syncToken); + return this.client._http.authedRequest(undefined, "GET", "/sync", qps, undefined, qps.timeout + BUFFER_PERIOD_MS); +}; + +SyncApi.prototype._getSyncParams = function (syncOptions, syncToken) { + var pollTimeout = this.opts.pollTimeout; + + if (this.getSyncState() !== 'SYNCING' || this._catchingUp) { + // unless we are happily syncing already, we want the server to return + // as quickly as possible, even if there are no events queued. This + // serves two purposes: + // + // * When the connection dies, we want to know asap when it comes back, + // so that we can hide the error from the user. (We don't want to + // have to wait for an event or a timeout). + // + // * We want to know if the server has any to_device messages queued up + // for us. We do that by calling it with a zero timeout until it + // doesn't give us any more to_device messages. + this._catchingUp = true; + pollTimeout = 0; + } + + var filterId = syncOptions.filterId; + if (this.client.isGuest() && !filterId) { + filterId = this._getGuestFilter(); + } + + var qps = { + filter: filterId, + timeout: pollTimeout + }; + + if (this.opts.disablePresence) { + qps.set_presence = "offline"; + } + + if (syncToken) { + qps.since = syncToken; + } else { + // use a cachebuster for initialsyncs, to make sure that + // we don't get a stale sync + // (https://github.com/vector-im/vector-web/issues/1354) + qps._cacheBuster = Date.now(); + } + + if (this.getSyncState() == 'ERROR' || this.getSyncState() == 'RECONNECTING') { + // we think the connection is dead. If it comes back up, we won't know + // about it till /sync returns. If the timeout= is high, this could + // be a long time. Set it to 0 when doing retries so we don't have to wait + // for an event or a timeout before emiting the SYNCING event. + qps.timeout = 0; + } + + return qps; +}; + +SyncApi.prototype._onSyncError = function (err, syncOptions) { + var _this = this; + + if (!this._running) { + debuglog("Sync no longer running: exiting"); + if (this._connectionReturnedDefer) { + this._connectionReturnedDefer.reject(); + this._connectionReturnedDefer = null; + } + this._updateSyncState("STOPPED"); + return; + } + + console.error("/sync error %s", err); + console.error(err); + + this._failedSyncCount++; + console.log('Number of consecutive failed sync requests:', this._failedSyncCount); + + debuglog("Starting keep-alive"); + // Note that we do *not* mark the sync connection as + // lost yet: we only do this if a keepalive poke + // fails, since long lived HTTP connections will + // go away sometimes and we shouldn't treat this as + // erroneous. We set the state to 'reconnecting' + // instead, so that clients can onserve this state + // if they wish. + this._startKeepAlives().then(function () { + _this._sync(syncOptions); + }); + + this._currentSyncRequest = null; + // Transition from RECONNECTING to ERROR after a given number of failed syncs + this._updateSyncState(this._failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ? "ERROR" : "RECONNECTING"); +}; + +/** + * Process data returned from a sync response and propagate it + * into the model objects + * + * @param {Object} syncEventData Object containing sync tokens associated with this sync + * @param {Object} data The response from /sync + */ +SyncApi.prototype._processSyncResponse = function () { + var _ref6 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee8(syncEventData, data) { + var client, self, events, inviteRooms, joinRooms, leaveRooms, currentCount; + return _regenerator2.default.wrap(function _callee8$(_context8) { + while (1) { + switch (_context8.prev = _context8.next) { + case 0: + client = this.client; + self = this; + + // data looks like: + // { + // next_batch: $token, + // presence: { events: [] }, + // account_data: { events: [] }, + // device_lists: { changed: ["@user:server", ... ]}, + // to_device: { events: [] }, + // device_one_time_keys_count: { signed_curve25519: 42 }, + // rooms: { + // invite: { + // $roomid: { + // invite_state: { events: [] } + // } + // }, + // join: { + // $roomid: { + // state: { events: [] }, + // timeline: { events: [], prev_batch: $token, limited: true }, + // ephemeral: { events: [] }, + // account_data: { events: [] }, + // unread_notifications: { + // highlight_count: 0, + // notification_count: 0, + // } + // } + // }, + // leave: { + // $roomid: { + // state: { events: [] }, + // timeline: { events: [], prev_batch: $token } + // } + // } + // }, + // groups: { + // invite: { + // $groupId: { + // inviter: $inviter, + // profile: { + // avatar_url: $avatarUrl, + // name: $groupName, + // }, + // }, + // }, + // join: {}, + // leave: {}, + // }, + // } + + // TODO-arch: + // - Each event we pass through needs to be emitted via 'event', can we + // do this in one place? + // - The isBrandNewRoom boilerplate is boilerplatey. + + // handle presence events (User objects) + + if (data.presence && utils.isArray(data.presence.events)) { + data.presence.events.map(client.getEventMapper()).forEach(function (presenceEvent) { + var user = client.store.getUser(presenceEvent.getSender()); + if (user) { + user.setPresenceEvent(presenceEvent); + } else { + user = createNewUser(client, presenceEvent.getSender()); + user.setPresenceEvent(presenceEvent); + client.store.storeUser(user); + } + client.emit("event", presenceEvent); + }); + } + + // handle non-room account_data + if (data.account_data && utils.isArray(data.account_data.events)) { + events = data.account_data.events.map(client.getEventMapper()); + + client.store.storeAccountDataEvents(events); + events.forEach(function (accountDataEvent) { + // Honour push rules that come down the sync stream but also + // honour push rules that were previously cached. Base rules + // will be updated when we recieve push rules via getPushRules + // (see SyncApi.prototype.sync) before syncing over the network. + if (accountDataEvent.getType() == 'm.push_rules') { + client.pushRules = accountDataEvent.getContent(); + } + client.emit("accountData", accountDataEvent); + return accountDataEvent; + }); + } + + // handle to-device events + if (data.to_device && utils.isArray(data.to_device.events) && data.to_device.events.length > 0) { + data.to_device.events.map(client.getEventMapper()).forEach(function (toDeviceEvent) { + var content = toDeviceEvent.getContent(); + if (toDeviceEvent.getType() == "m.room.message" && content.msgtype == "m.bad.encrypted") { + // the mapper already logged a warning. + console.log('Ignoring undecryptable to-device event from ' + toDeviceEvent.getSender()); + return; + } + + client.emit("toDeviceEvent", toDeviceEvent); + }); + } else { + // no more to-device events: we can stop polling with a short timeout. + this._catchingUp = false; + } + + if (data.groups) { + if (data.groups.invite) { + this._processGroupSyncEntry(data.groups.invite, 'invite'); + } + + if (data.groups.join) { + this._processGroupSyncEntry(data.groups.join, 'join'); + } + + if (data.groups.leave) { + this._processGroupSyncEntry(data.groups.leave, 'leave'); + } + } + + // the returned json structure is a bit crap, so make it into a + // nicer form (array) after applying sanity to make sure we don't fail + // on missing keys (on the off chance) + inviteRooms = []; + joinRooms = []; + leaveRooms = []; + + + if (data.rooms) { + if (data.rooms.invite) { + inviteRooms = this._mapSyncResponseToRoomArray(data.rooms.invite); + } + if (data.rooms.join) { + joinRooms = this._mapSyncResponseToRoomArray(data.rooms.join); + } + if (data.rooms.leave) { + leaveRooms = this._mapSyncResponseToRoomArray(data.rooms.leave); + } + } + + this._notifEvents = []; + + // Handle invites + inviteRooms.forEach(function (inviteObj) { + var room = inviteObj.room; + var stateEvents = self._mapSyncEventsFormat(inviteObj.invite_state, room); + self._processRoomEvents(room, stateEvents); + if (inviteObj.isBrandNewRoom) { + room.recalculate(client.credentials.userId); + client.store.storeRoom(room); + client.emit("Room", room); + } + stateEvents.forEach(function (e) { + client.emit("event", e); + }); + }); + + // Handle joins + _context8.next = 14; + return (0, _bluebird.resolve)(_bluebird2.default.mapSeries(joinRooms, function () { + var _ref7 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee7(joinObj) { + var processRoomEvent = function () { + var _ref8 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee6(e) { + return _regenerator2.default.wrap(function _callee6$(_context6) { + while (1) { + switch (_context6.prev = _context6.next) { + case 0: + client.emit("event", e); + + if (!(e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto)) { + _context6.next = 4; + break; + } + + _context6.next = 4; + return (0, _bluebird.resolve)(self.opts.crypto.onCryptoEvent(e)); + + case 4: + case "end": + return _context6.stop(); + } + } + }, _callee6, this); + })); + + return function processRoomEvent(_x8) { + return _ref8.apply(this, arguments); + }; + }(); + + var room, stateEvents, timelineEvents, ephemeralEvents, accountDataEvents, limited, i, eventId; + return _regenerator2.default.wrap(function _callee7$(_context7) { + while (1) { + switch (_context7.prev = _context7.next) { + case 0: + room = joinObj.room; + stateEvents = self._mapSyncEventsFormat(joinObj.state, room); + timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room); + ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral); + accountDataEvents = self._mapSyncEventsFormat(joinObj.account_data); + + // we do this first so it's correct when any of the events fire + + if (joinObj.unread_notifications) { + room.setUnreadNotificationCount('total', joinObj.unread_notifications.notification_count); + room.setUnreadNotificationCount('highlight', joinObj.unread_notifications.highlight_count); + } + + joinObj.timeline = joinObj.timeline || {}; + + if (!joinObj.isBrandNewRoom) { + _context7.next = 11; + break; + } + + // set the back-pagination token. Do this *before* adding any + // events so that clients can start back-paginating. + room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS); + _context7.next = 25; + break; + + case 11: + if (!joinObj.timeline.limited) { + _context7.next = 25; + break; + } + + limited = true; + + // we've got a limited sync, so we *probably* have a gap in the + // timeline, so should reset. But we might have been peeking or + // paginating and already have some of the events, in which + // case we just want to append any subsequent events to the end + // of the existing timeline. + // + // This is particularly important in the case that we already have + // *all* of the events in the timeline - in that case, if we reset + // the timeline, we'll end up with an entirely empty timeline, + // which we'll try to paginate but not get any new events (which + // will stop us linking the empty timeline into the chain). + // + + i = timelineEvents.length - 1; + + case 14: + if (!(i >= 0)) { + _context7.next = 24; + break; + } + + eventId = timelineEvents[i].getId(); + + if (!room.getTimelineForEvent(eventId)) { + _context7.next = 21; + break; + } + + debuglog("Already have event " + eventId + " in limited " + "sync - not resetting"); + limited = false; + + // we might still be missing some of the events before i; + // we don't want to be adding them to the end of the + // timeline because that would put them out of order. + timelineEvents.splice(0, i); + + // XXX: there's a problem here if the skipped part of the + // timeline modifies the state set in stateEvents, because + // we'll end up using the state from stateEvents rather + // than the later state from timelineEvents. We probably + // need to wind stateEvents forward over the events we're + // skipping. + + return _context7.abrupt("break", 24); + + case 21: + i--; + _context7.next = 14; + break; + + case 24: + + if (limited) { + self._deregisterStateListeners(room); + room.resetLiveTimeline(joinObj.timeline.prev_batch, self.opts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken); + + // We have to assume any gap in any timeline is + // reason to stop incrementally tracking notifications and + // reset the timeline. + client.resetNotifTimelineSet(); + + self._registerStateListeners(room); + } + + case 25: + + self._processRoomEvents(room, stateEvents, timelineEvents); + + // XXX: should we be adding ephemeralEvents to the timeline? + // It feels like that for symmetry with room.addAccountData() + // there should be a room.addEphemeralEvents() or similar. + room.addLiveEvents(ephemeralEvents); + + // we deliberately don't add accountData to the timeline + room.addAccountData(accountDataEvents); + + room.recalculate(client.credentials.userId); + if (joinObj.isBrandNewRoom) { + client.store.storeRoom(room); + client.emit("Room", room); + } + + self._processEventsForNotifs(room, timelineEvents); + + _context7.next = 33; + return (0, _bluebird.resolve)(_bluebird2.default.mapSeries(stateEvents, processRoomEvent)); + + case 33: + _context7.next = 35; + return (0, _bluebird.resolve)(_bluebird2.default.mapSeries(timelineEvents, processRoomEvent)); + + case 35: + ephemeralEvents.forEach(function (e) { + client.emit("event", e); + }); + accountDataEvents.forEach(function (e) { + client.emit("event", e); + }); + + case 37: + case "end": + return _context7.stop(); + } + } + }, _callee7, this); + })); + + return function (_x7) { + return _ref7.apply(this, arguments); + }; + }())); + + case 14: + + // Handle leaves (e.g. kicked rooms) + leaveRooms.forEach(function (leaveObj) { + var room = leaveObj.room; + var stateEvents = self._mapSyncEventsFormat(leaveObj.state, room); + var timelineEvents = self._mapSyncEventsFormat(leaveObj.timeline, room); + var accountDataEvents = self._mapSyncEventsFormat(leaveObj.account_data); + + self._processRoomEvents(room, stateEvents, timelineEvents); + room.addAccountData(accountDataEvents); + + room.recalculate(client.credentials.userId); + if (leaveObj.isBrandNewRoom) { + client.store.storeRoom(room); + client.emit("Room", room); + } + + self._processEventsForNotifs(room, timelineEvents); + + stateEvents.forEach(function (e) { + client.emit("event", e); + }); + timelineEvents.forEach(function (e) { + client.emit("event", e); + }); + accountDataEvents.forEach(function (e) { + client.emit("event", e); + }); + }); + + // update the notification timeline, if appropriate. + // we only do this for live events, as otherwise we can't order them sanely + // in the timeline relative to ones paginated in by /notifications. + // XXX: we could fix this by making EventTimeline support chronological + // ordering... but it doesn't, right now. + if (syncEventData.oldSyncToken && this._notifEvents.length) { + this._notifEvents.sort(function (a, b) { + return a.getTs() - b.getTs(); + }); + this._notifEvents.forEach(function (event) { + client.getNotifTimelineSet().addLiveEvent(event); + }); + } + + // Handle device list updates + + if (!data.device_lists) { + _context8.next = 22; + break; + } + + if (!this.opts.crypto) { + _context8.next = 22; + break; + } + + _context8.next = 20; + return (0, _bluebird.resolve)(this.opts.crypto.handleDeviceListChanges(syncEventData, data.device_lists)); + + case 20: + _context8.next = 22; + break; + + case 22: + + // Handle one_time_keys_count + if (this.opts.crypto && data.device_one_time_keys_count) { + currentCount = data.device_one_time_keys_count.signed_curve25519 || 0; + + this.opts.crypto.updateOneTimeKeyCount(currentCount); + } + + case 23: + case "end": + return _context8.stop(); + } + } + }, _callee8, this); + })); + + return function (_x5, _x6) { + return _ref6.apply(this, arguments); + }; +}(); + +/** + * Starts polling the connectivity check endpoint + * @param {number} delay How long to delay until the first poll. + * defaults to a short, randomised interval (to prevent + * tightlooping if /versions succeeds but /sync etc. fail). + * @return {promise} which resolves once the connection returns + */ +SyncApi.prototype._startKeepAlives = function (delay) { + if (delay === undefined) { + delay = 2000 + Math.floor(Math.random() * 5000); + } + + if (this._keepAliveTimer !== null) { + clearTimeout(this._keepAliveTimer); + } + var self = this; + if (delay > 0) { + self._keepAliveTimer = setTimeout(self._pokeKeepAlive.bind(self), delay); + } else { + self._pokeKeepAlive(); + } + if (!this._connectionReturnedDefer) { + this._connectionReturnedDefer = _bluebird2.default.defer(); + } + return this._connectionReturnedDefer.promise; +}; + +/** + * Make a dummy call to /_matrix/client/versions, to see if the HS is + * reachable. + * + * On failure, schedules a call back to itself. On success, resolves + * this._connectionReturnedDefer. + */ +SyncApi.prototype._pokeKeepAlive = function () { + var self = this; + function success() { + clearTimeout(self._keepAliveTimer); + if (self._connectionReturnedDefer) { + self._connectionReturnedDefer.resolve(); + self._connectionReturnedDefer = null; + } + } + + this.client._http.request(undefined, // callback + "GET", "/_matrix/client/versions", undefined, // queryParams + undefined, // data + { + prefix: '', + localTimeoutMs: 15 * 1000 + }).done(function () { + success(); + }, function (err) { + if (err.httpStatus == 400) { + // treat this as a success because the server probably just doesn't + // support /versions: point is, we're getting a response. + // We wait a short time though, just in case somehow the server + // is in a mode where it 400s /versions responses and sync etc. + // responses fail, this will mean we don't hammer in a loop. + self._keepAliveTimer = setTimeout(success, 2000); + } else { + self._keepAliveTimer = setTimeout(self._pokeKeepAlive.bind(self), 5000 + Math.floor(Math.random() * 5000)); + // A keepalive has failed, so we emit the + // error state (whether or not this is the + // first failure). + // Note we do this after setting the timer: + // this lets the unit tests advance the mock + // clock when the get the error. + self._updateSyncState("ERROR", { error: err }); + } + }); +}; + +/** + * @param {Object} groupsSection Groups section object, eg. response.groups.invite + * @param {string} sectionName Which section this is ('invite', 'join' or 'leave') + */ +SyncApi.prototype._processGroupSyncEntry = function (groupsSection, sectionName) { + // Processes entries from 'groups' section of the sync stream + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)((0, _keys2.default)(groupsSection)), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var groupId = _step.value; + + var groupInfo = groupsSection[groupId]; + var group = this.client.store.getGroup(groupId); + var isBrandNew = group === null; + if (group === null) { + group = this.createGroup(groupId); + } + if (groupInfo.profile) { + group.setProfile(groupInfo.profile.name, groupInfo.profile.avatar_url); + } + if (groupInfo.inviter) { + group.setInviter({ userId: groupInfo.inviter }); + } + group.setMyMembership(sectionName); + if (isBrandNew) { + // Now we've filled in all the fields, emit the Group event + this.client.emit("Group", group); + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } +}; + +/** + * @param {Object} obj + * @return {Object[]} + */ +SyncApi.prototype._mapSyncResponseToRoomArray = function (obj) { + // Maps { roomid: {stuff}, roomid: {stuff} } + // to + // [{stuff+Room+isBrandNewRoom}, {stuff+Room+isBrandNewRoom}] + var client = this.client; + var self = this; + return utils.keys(obj).map(function (roomId) { + var arrObj = obj[roomId]; + var room = client.store.getRoom(roomId); + var isBrandNewRoom = false; + if (!room) { + room = self.createRoom(roomId); + isBrandNewRoom = true; + } + arrObj.room = room; + arrObj.isBrandNewRoom = isBrandNewRoom; + return arrObj; + }); +}; + +/** + * @param {Object} obj + * @param {Room} room + * @return {MatrixEvent[]} + */ +SyncApi.prototype._mapSyncEventsFormat = function (obj, room) { + if (!obj || !utils.isArray(obj.events)) { + return []; + } + var mapper = this.client.getEventMapper(); + return obj.events.map(function (e) { + if (room) { + e.room_id = room.roomId; + } + return mapper(e); + }); +}; + +/** + * @param {Room} room + */ +SyncApi.prototype._resolveInvites = function (room) { + if (!room || !this.opts.resolveInvitesToProfiles) { + return; + } + var client = this.client; + // For each invited room member we want to give them a displayname/avatar url + // if they have one (the m.room.member invites don't contain this). + room.getMembersWithMembership("invite").forEach(function (member) { + if (member._requestedProfileInfo) { + return; + } + member._requestedProfileInfo = true; + // try to get a cached copy first. + var user = client.getUser(member.userId); + var promise = void 0; + if (user) { + promise = _bluebird2.default.resolve({ + avatar_url: user.avatarUrl, + displayname: user.displayName + }); + } else { + promise = client.getProfileInfo(member.userId); + } + promise.done(function (info) { + // slightly naughty by doctoring the invite event but this means all + // the code paths remain the same between invite/join display name stuff + // which is a worthy trade-off for some minor pollution. + var inviteEvent = member.events.member; + if (inviteEvent.getContent().membership !== "invite") { + // between resolving and now they have since joined, so don't clobber + return; + } + inviteEvent.getContent().avatar_url = info.avatar_url; + inviteEvent.getContent().displayname = info.displayname; + // fire listeners + member.setMembershipEvent(inviteEvent, room.currentState); + }, function (err) { + // OH WELL. + }); + }); +}; + +/** + * @param {Room} room + * @param {MatrixEvent[]} stateEventList A list of state events. This is the state + * at the *START* of the timeline list if it is supplied. + * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index + * is earlier in time. Higher index is later. + */ +SyncApi.prototype._processRoomEvents = function (room, stateEventList, timelineEventList) { + // If there are no events in the timeline yet, initialise it with + // the given state events + var liveTimeline = room.getLiveTimeline(); + var timelineWasEmpty = liveTimeline.getEvents().length == 0; + if (timelineWasEmpty) { + // Passing these events into initialiseState will freeze them, so we need + // to compute and cache the push actions for them now, otherwise sync dies + // with an attempt to assign to read only property. + // XXX: This is pretty horrible and is assuming all sorts of behaviour from + // these functions that it shouldn't be. We should probably either store the + // push actions cache elsewhere so we can freeze MatrixEvents, or otherwise + // find some solution where MatrixEvents are immutable but allow for a cache + // field. + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = (0, _getIterator3.default)(stateEventList), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var ev = _step2.value; + + this.client.getPushActionsForEvent(ev); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + liveTimeline.initialiseState(stateEventList); + } + + this._resolveInvites(room); + + // recalculate the room name at this point as adding events to the timeline + // may make notifications appear which should have the right name. + // XXX: This looks suspect: we'll end up recalculating the room once here + // and then again after adding events (_processSyncResponse calls it after + // calling us) even if no state events were added. It also means that if + // one of the room events in timelineEventList is something that needs + // a recalculation (like m.room.name) we won't recalculate until we've + // finished adding all the events, which will cause the notification to have + // the old room name rather than the new one. + room.recalculate(this.client.credentials.userId); + + // If the timeline wasn't empty, we process the state events here: they're + // defined as updates to the state before the start of the timeline, so this + // starts to roll the state forward. + // XXX: That's what we *should* do, but this can happen if we were previously + // peeking in a room, in which case we obviously do *not* want to add the + // state events here onto the end of the timeline. Historically, the js-sdk + // has just set these new state events on the old and new state. This seems + // very wrong because there could be events in the timeline that diverge the + // state, in which case this is going to leave things out of sync. However, + // for now I think it;s best to behave the same as the code has done previously. + if (!timelineWasEmpty) { + // XXX: As above, don't do this... + //room.addLiveEvents(stateEventList || []); + // Do this instead... + room.oldState.setStateEvents(stateEventList || []); + room.currentState.setStateEvents(stateEventList || []); + } + // execute the timeline events. This will continue to diverge the current state + // if the timeline has any state events in it. + // This also needs to be done before running push rules on the events as they need + // to be decorated with sender etc. + room.addLiveEvents(timelineEventList || []); +}; + +/** + * Takes a list of timelineEvents and adds and adds to _notifEvents + * as appropriate. + * This must be called after the room the events belong to has been stored. + * + * @param {Room} room + * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index + * is earlier in time. Higher index is later. + */ +SyncApi.prototype._processEventsForNotifs = function (room, timelineEventList) { + // gather our notifications into this._notifEvents + if (this.client.getNotifTimelineSet()) { + for (var i = 0; i < timelineEventList.length; i++) { + var pushActions = this.client.getPushActionsForEvent(timelineEventList[i]); + if (pushActions && pushActions.notify && pushActions.tweaks && pushActions.tweaks.highlight) { + this._notifEvents.push(timelineEventList[i]); + } + } + } +}; + +/** + * @return {string} + */ +SyncApi.prototype._getGuestFilter = function () { + var guestRooms = this.client._guestRooms; // FIXME: horrible gut-wrenching + if (!guestRooms) { + return "{}"; + } + // we just need to specify the filter inline if we're a guest because guests + // can't create filters. + return (0, _stringify2.default)({ + room: { + timeline: { + limit: 20 + } + } + }); +}; + +/** + * Sets the sync state and emits an event to say so + * @param {String} newState The new state string + * @param {Object} data Object of additional data to emit in the event + */ +SyncApi.prototype._updateSyncState = function (newState, data) { + var old = this._syncState; + this._syncState = newState; + this.client.emit("sync", this._syncState, old, data); +}; + +/** + * Event handler for the 'online' event + * This event is generally unreliable and precise behaviour + * varies between browsers, so we poll for connectivity too, + * but this might help us reconnect a little faster. + */ +SyncApi.prototype._onOnline = function () { + debuglog("Browser thinks we are back online"); + this._startKeepAlives(0); +}; + +function createNewUser(client, userId) { + var user = new User(userId); + client.reEmitter.reEmit(user, ["User.avatarUrl", "User.displayName", "User.presence", "User.currentlyActive", "User.lastPresenceTs"]); + return user; +} + +/** */ +module.exports = SyncApi; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"./filter":23,"./models/event-timeline":29,"./models/group":31,"./models/room":35,"./models/user":37,"./utils":50,"babel-runtime/core-js/get-iterator":53,"babel-runtime/core-js/json/stringify":55,"babel-runtime/core-js/object/keys":61,"babel-runtime/regenerator":73,"bluebird":74}],49:[function(require,module,exports){ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +/** @module timeline-window */ + +var _bluebird = require("bluebird"); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var EventTimeline = require("./models/event-timeline"); + +/** + * @private + */ +var DEBUG = false; + +/** + * @private + */ +var debuglog = DEBUG ? console.log.bind(console) : function () {}; + +/** + * the number of times we ask the server for more events before giving up + * + * @private + */ +var DEFAULT_PAGINATE_LOOP_LIMIT = 5; + +/** + * Construct a TimelineWindow. + * + *

This abstracts the separate timelines in a Matrix {@link + * module:models/room|Room} into a single iterable thing. It keeps track of + * the start and endpoints of the window, which can be advanced with the help + * of pagination requests. + * + *

Before the window is useful, it must be initialised by calling {@link + * module:timeline-window~TimelineWindow#load|load}. + * + *

Note that the window will not automatically extend itself when new events + * are received from /sync; you should arrange to call {@link + * module:timeline-window~TimelineWindow#paginate|paginate} on {@link + * module:client~MatrixClient.event:"Room.timeline"|Room.timeline} events. + * + * @param {MatrixClient} client MatrixClient to be used for context/pagination + * requests. + * + * @param {EventTimelineSet} timelineSet The timelineSet to track + * + * @param {Object} [opts] Configuration options for this window + * + * @param {number} [opts.windowLimit = 1000] maximum number of events to keep + * in the window. If more events are retrieved via pagination requests, + * excess events will be dropped from the other end of the window. + * + * @constructor + */ +function TimelineWindow(client, timelineSet, opts) { + opts = opts || {}; + this._client = client; + this._timelineSet = timelineSet; + + // these will be TimelineIndex objects; they delineate the 'start' and + // 'end' of the window. + // + // _start.index is inclusive; _end.index is exclusive. + this._start = null; + this._end = null; + + this._eventCount = 0; + this._windowLimit = opts.windowLimit || 1000; +} + +/** + * Initialise the window to point at a given event, or the live timeline + * + * @param {string} [initialEventId] If given, the window will contain the + * given event + * @param {number} [initialWindowSize = 20] Size of the initial window + * + * @return {module:client.Promise} + */ +TimelineWindow.prototype.load = function (initialEventId, initialWindowSize) { + var self = this; + initialWindowSize = initialWindowSize || 20; + + // given an EventTimeline, find the event we were looking for, and initialise our + // fields so that the event in question is in the middle of the window. + var initFields = function initFields(timeline) { + var eventIndex = void 0; + + var events = timeline.getEvents(); + + if (!initialEventId) { + // we were looking for the live timeline: initialise to the end + eventIndex = events.length; + } else { + for (var i = 0; i < events.length; i++) { + if (events[i].getId() == initialEventId) { + eventIndex = i; + break; + } + } + + if (eventIndex === undefined) { + throw new Error("getEventTimeline result didn't include requested event"); + } + } + + var endIndex = Math.min(events.length, eventIndex + Math.ceil(initialWindowSize / 2)); + var startIndex = Math.max(0, endIndex - initialWindowSize); + self._start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex()); + self._end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex()); + self._eventCount = endIndex - startIndex; + }; + + // We avoid delaying the resolution of the promise by a reactor tick if + // we already have the data we need, which is important to keep room-switching + // feeling snappy. + // + if (initialEventId) { + var prom = this._client.getEventTimeline(this._timelineSet, initialEventId); + + if (prom.isFulfilled()) { + initFields(prom.value()); + return _bluebird2.default.resolve(); + } else { + return prom.then(initFields); + } + } else { + var tl = this._timelineSet.getLiveTimeline(); + initFields(tl); + return _bluebird2.default.resolve(); + } +}; + +/** + * Check if this window can be extended + * + *

This returns true if we either have more events, or if we have a + * pagination token which means we can paginate in that direction. It does not + * necessarily mean that there are more events available in that direction at + * this time. + * + * @param {string} direction EventTimeline.BACKWARDS to check if we can + * paginate backwards; EventTimeline.FORWARDS to check if we can go forwards + * + * @return {boolean} true if we can paginate in the given direction + */ +TimelineWindow.prototype.canPaginate = function (direction) { + var tl = void 0; + if (direction == EventTimeline.BACKWARDS) { + tl = this._start; + } else if (direction == EventTimeline.FORWARDS) { + tl = this._end; + } else { + throw new Error("Invalid direction '" + direction + "'"); + } + + if (!tl) { + debuglog("TimelineWindow: no timeline yet"); + return false; + } + + if (direction == EventTimeline.BACKWARDS) { + if (tl.index > tl.minIndex()) { + return true; + } + } else { + if (tl.index < tl.maxIndex()) { + return true; + } + } + + return Boolean(tl.timeline.getNeighbouringTimeline(direction) || tl.timeline.getPaginationToken(direction)); +}; + +/** + * Attempt to extend the window + * + * @param {string} direction EventTimeline.BACKWARDS to extend the window + * backwards (towards older events); EventTimeline.FORWARDS to go forwards. + * + * @param {number} size number of events to try to extend by. If fewer than this + * number are immediately available, then we return immediately rather than + * making an API call. + * + * @param {boolean} [makeRequest = true] whether we should make API calls to + * fetch further events if we don't have any at all. (This has no effect if + * the room already knows about additional events in the relevant direction, + * even if there are fewer than 'size' of them, as we will just return those + * we already know about.) + * + * @param {number} [requestLimit = 5] limit for the number of API requests we + * should make. + * + * @return {module:client.Promise} Resolves to a boolean which is true if more events + * were successfully retrieved. + */ +TimelineWindow.prototype.paginate = function (direction, size, makeRequest, requestLimit) { + // Either wind back the message cap (if there are enough events in the + // timeline to do so), or fire off a pagination request. + + if (makeRequest === undefined) { + makeRequest = true; + } + + if (requestLimit === undefined) { + requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT; + } + + var tl = void 0; + if (direction == EventTimeline.BACKWARDS) { + tl = this._start; + } else if (direction == EventTimeline.FORWARDS) { + tl = this._end; + } else { + throw new Error("Invalid direction '" + direction + "'"); + } + + if (!tl) { + debuglog("TimelineWindow: no timeline yet"); + return _bluebird2.default.resolve(false); + } + + if (tl.pendingPaginate) { + return tl.pendingPaginate; + } + + // try moving the cap + var count = direction == EventTimeline.BACKWARDS ? tl.retreat(size) : tl.advance(size); + + if (count) { + this._eventCount += count; + debuglog("TimelineWindow: increased cap by " + count + " (now " + this._eventCount + ")"); + // remove some events from the other end, if necessary + var excess = this._eventCount - this._windowLimit; + if (excess > 0) { + this.unpaginate(excess, direction != EventTimeline.BACKWARDS); + } + return _bluebird2.default.resolve(true); + } + + if (!makeRequest || requestLimit === 0) { + // todo: should we return something different to indicate that there + // might be more events out there, but we haven't found them yet? + return _bluebird2.default.resolve(false); + } + + // try making a pagination request + var token = tl.timeline.getPaginationToken(direction); + if (!token) { + debuglog("TimelineWindow: no token"); + return _bluebird2.default.resolve(false); + } + + debuglog("TimelineWindow: starting request"); + var self = this; + + var prom = this._client.paginateEventTimeline(tl.timeline, { + backwards: direction == EventTimeline.BACKWARDS, + limit: size + }).finally(function () { + tl.pendingPaginate = null; + }).then(function (r) { + debuglog("TimelineWindow: request completed with result " + r); + if (!r) { + // end of timeline + return false; + } + + // recurse to advance the index into the results. + // + // If we don't get any new events, we want to make sure we keep asking + // the server for events for as long as we have a valid pagination + // token. In particular, we want to know if we've actually hit the + // start of the timeline, or if we just happened to know about all of + // the events thanks to https://matrix.org/jira/browse/SYN-645. + // + // On the other hand, we necessarily want to wait forever for the + // server to make its mind up about whether there are other events, + // because it gives a bad user experience + // (https://github.com/vector-im/vector-web/issues/1204). + return self.paginate(direction, size, true, requestLimit - 1); + }); + tl.pendingPaginate = prom; + return prom; +}; + +/** + * Remove `delta` events from the start or end of the timeline. + * + * @param {number} delta number of events to remove from the timeline + * @param {boolean} startOfTimeline if events should be removed from the start + * of the timeline. + */ +TimelineWindow.prototype.unpaginate = function (delta, startOfTimeline) { + var tl = startOfTimeline ? this._start : this._end; + + // sanity-check the delta + if (delta > this._eventCount || delta < 0) { + throw new Error("Attemting to unpaginate " + delta + " events, but " + "only have " + this._eventCount + " in the timeline"); + } + + while (delta > 0) { + var count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta); + if (count <= 0) { + // sadness. This shouldn't be possible. + throw new Error("Unable to unpaginate any further, but still have " + this._eventCount + " events"); + } + + delta -= count; + this._eventCount -= count; + debuglog("TimelineWindow.unpaginate: dropped " + count + " (now " + this._eventCount + ")"); + } +}; + +/** + * Get a list of the events currently in the window + * + * @return {MatrixEvent[]} the events in the window + */ +TimelineWindow.prototype.getEvents = function () { + if (!this._start) { + // not yet loaded + return []; + } + + var result = []; + + // iterate through each timeline between this._start and this._end + // (inclusive). + var timeline = this._start.timeline; + while (true) { + var events = timeline.getEvents(); + + // For the first timeline in the chain, we want to start at + // this._start.index. For the last timeline in the chain, we want to + // stop before this._end.index. Otherwise, we want to copy all of the + // events in the timeline. + // + // (Note that both this._start.index and this._end.index are relative + // to their respective timelines' BaseIndex). + // + var startIndex = 0, + endIndex = events.length; + if (timeline === this._start.timeline) { + startIndex = this._start.index + timeline.getBaseIndex(); + } + if (timeline === this._end.timeline) { + endIndex = this._end.index + timeline.getBaseIndex(); + } + + for (var i = startIndex; i < endIndex; i++) { + result.push(events[i]); + } + + // if we're not done, iterate to the next timeline. + if (timeline === this._end.timeline) { + break; + } else { + timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS); + } + } + + return result; +}; + +/** + * a thing which contains a timeline reference, and an index into it. + * + * @constructor + * @param {EventTimeline} timeline + * @param {number} index + * @private + */ +function TimelineIndex(timeline, index) { + this.timeline = timeline; + + // the indexes are relative to BaseIndex, so could well be negative. + this.index = index; +} + +/** + * @return {number} the minimum possible value for the index in the current + * timeline + */ +TimelineIndex.prototype.minIndex = function () { + return this.timeline.getBaseIndex() * -1; +}; + +/** + * @return {number} the maximum possible value for the index in the current + * timeline (exclusive - ie, it actually returns one more than the index + * of the last element). + */ +TimelineIndex.prototype.maxIndex = function () { + return this.timeline.getEvents().length - this.timeline.getBaseIndex(); +}; + +/** + * Try move the index forward, or into the neighbouring timeline + * + * @param {number} delta number of events to advance by + * @return {number} number of events successfully advanced by + */ +TimelineIndex.prototype.advance = function (delta) { + if (!delta) { + return 0; + } + + // first try moving the index in the current timeline. See if there is room + // to do so. + var cappedDelta = void 0; + if (delta < 0) { + // we want to wind the index backwards. + // + // (this.minIndex() - this.index) is a negative number whose magnitude + // is the amount of room we have to wind back the index in the current + // timeline. We cap delta to this quantity. + cappedDelta = Math.max(delta, this.minIndex() - this.index); + if (cappedDelta < 0) { + this.index += cappedDelta; + return cappedDelta; + } + } else { + // we want to wind the index forwards. + // + // (this.maxIndex() - this.index) is a (positive) number whose magnitude + // is the amount of room we have to wind forward the index in the current + // timeline. We cap delta to this quantity. + cappedDelta = Math.min(delta, this.maxIndex() - this.index); + if (cappedDelta > 0) { + this.index += cappedDelta; + return cappedDelta; + } + } + + // the index is already at the start/end of the current timeline. + // + // next see if there is a neighbouring timeline to switch to. + var neighbour = this.timeline.getNeighbouringTimeline(delta < 0 ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS); + if (neighbour) { + this.timeline = neighbour; + if (delta < 0) { + this.index = this.maxIndex(); + } else { + this.index = this.minIndex(); + } + + debuglog("paginate: switched to new neighbour"); + + // recurse, using the next timeline + return this.advance(delta); + } + + return 0; +}; + +/** + * Try move the index backwards, or into the neighbouring timeline + * + * @param {number} delta number of events to retreat by + * @return {number} number of events successfully retreated by + */ +TimelineIndex.prototype.retreat = function (delta) { + return this.advance(delta * -1) * -1; +}; + +/** + * The TimelineWindow class. + */ +module.exports.TimelineWindow = TimelineWindow; + +/** + * The TimelineIndex class. exported here for unit testing. + */ +module.exports.TimelineIndex = TimelineIndex; + +},{"./models/event-timeline":29,"bluebird":74}],50:[function(require,module,exports){ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; +/** + * This is an internal module. + * @module utils + */ + +/** + * Encode a dictionary of query parameters. + * @param {Object} params A dict of key/values to encode e.g. + * {"foo": "bar", "baz": "taz"} + * @return {string} The encoded string e.g. foo=bar&baz=taz + */ + +var _create = require("babel-runtime/core-js/object/create"); + +var _create2 = _interopRequireDefault(_create); + +var _typeof2 = require("babel-runtime/helpers/typeof"); + +var _typeof3 = _interopRequireDefault(_typeof2); + +var _stringify = require("babel-runtime/core-js/json/stringify"); + +var _stringify2 = _interopRequireDefault(_stringify); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +module.exports.encodeParams = function (params) { + var qs = ""; + for (var key in params) { + if (!params.hasOwnProperty(key)) { + continue; + } + qs += "&" + encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); + } + return qs.substring(1); +}; + +/** + * Encodes a URI according to a set of template variables. Variables will be + * passed through encodeURIComponent. + * @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'. + * @param {Object} variables The key/value pairs to replace the template + * variables with. E.g. { "$bar": "baz" }. + * @return {string} The result of replacing all template variables e.g. '/foo/baz'. + */ +module.exports.encodeUri = function (pathTemplate, variables) { + for (var key in variables) { + if (!variables.hasOwnProperty(key)) { + continue; + } + pathTemplate = pathTemplate.replace(key, encodeURIComponent(variables[key])); + } + return pathTemplate; +}; + +/** + * Applies a map function to the given array. + * @param {Array} array The array to apply the function to. + * @param {Function} fn The function that will be invoked for each element in + * the array with the signature fn(element){...} + * @return {Array} A new array with the results of the function. + */ +module.exports.map = function (array, fn) { + var results = new Array(array.length); + for (var i = 0; i < array.length; i++) { + results[i] = fn(array[i]); + } + return results; +}; + +/** + * Applies a filter function to the given array. + * @param {Array} array The array to apply the function to. + * @param {Function} fn The function that will be invoked for each element in + * the array. It should return true to keep the element. The function signature + * looks like fn(element, index, array){...}. + * @return {Array} A new array with the results of the function. + */ +module.exports.filter = function (array, fn) { + var results = []; + for (var i = 0; i < array.length; i++) { + if (fn(array[i], i, array)) { + results.push(array[i]); + } + } + return results; +}; + +/** + * Get the keys for an object. Same as Object.keys(). + * @param {Object} obj The object to get the keys for. + * @return {string[]} The keys of the object. + */ +module.exports.keys = function (obj) { + var keys = []; + for (var key in obj) { + if (!obj.hasOwnProperty(key)) { + continue; + } + keys.push(key); + } + return keys; +}; + +/** + * Get the values for an object. + * @param {Object} obj The object to get the values for. + * @return {Array<*>} The values of the object. + */ +module.exports.values = function (obj) { + var values = []; + for (var key in obj) { + if (!obj.hasOwnProperty(key)) { + continue; + } + values.push(obj[key]); + } + return values; +}; + +/** + * Invoke a function for each item in the array. + * @param {Array} array The array. + * @param {Function} fn The function to invoke for each element. Has the + * function signature fn(element, index). + */ +module.exports.forEach = function (array, fn) { + for (var i = 0; i < array.length; i++) { + fn(array[i], i); + } +}; + +/** + * The findElement() method returns a value in the array, if an element in the array + * satisfies (returns true) the provided testing function. Otherwise undefined + * is returned. + * @param {Array} array The array. + * @param {Function} fn Function to execute on each value in the array, with the + * function signature fn(element, index, array) + * @param {boolean} reverse True to search in reverse order. + * @return {*} The first value in the array which returns true for + * the given function. + */ +module.exports.findElement = function (array, fn, reverse) { + var i = void 0; + if (reverse) { + for (i = array.length - 1; i >= 0; i--) { + if (fn(array[i], i, array)) { + return array[i]; + } + } + } else { + for (i = 0; i < array.length; i++) { + if (fn(array[i], i, array)) { + return array[i]; + } + } + } +}; + +/** + * The removeElement() method removes the first element in the array that + * satisfies (returns true) the provided testing function. + * @param {Array} array The array. + * @param {Function} fn Function to execute on each value in the array, with the + * function signature fn(element, index, array). Return true to + * remove this element and break. + * @param {boolean} reverse True to search in reverse order. + * @return {boolean} True if an element was removed. + */ +module.exports.removeElement = function (array, fn, reverse) { + var i = void 0; + var removed = void 0; + if (reverse) { + for (i = array.length - 1; i >= 0; i--) { + if (fn(array[i], i, array)) { + removed = array[i]; + array.splice(i, 1); + return removed; + } + } + } else { + for (i = 0; i < array.length; i++) { + if (fn(array[i], i, array)) { + removed = array[i]; + array.splice(i, 1); + return removed; + } + } + } + return false; +}; + +/** + * Checks if the given thing is a function. + * @param {*} value The thing to check. + * @return {boolean} True if it is a function. + */ +module.exports.isFunction = function (value) { + return Object.prototype.toString.call(value) == "[object Function]"; +}; + +/** + * Checks if the given thing is an array. + * @param {*} value The thing to check. + * @return {boolean} True if it is an array. + */ +module.exports.isArray = function (value) { + return Array.isArray ? Array.isArray(value) : Boolean(value && value.constructor === Array); +}; + +/** + * Checks that the given object has the specified keys. + * @param {Object} obj The object to check. + * @param {string[]} keys The list of keys that 'obj' must have. + * @throws If the object is missing keys. + */ +module.exports.checkObjectHasKeys = function (obj, keys) { + for (var i = 0; i < keys.length; i++) { + if (!obj.hasOwnProperty(keys[i])) { + throw new Error("Missing required key: " + keys[i]); + } + } +}; + +/** + * Checks that the given object has no extra keys other than the specified ones. + * @param {Object} obj The object to check. + * @param {string[]} allowedKeys The list of allowed key names. + * @throws If there are extra keys. + */ +module.exports.checkObjectHasNoAdditionalKeys = function (obj, allowedKeys) { + for (var key in obj) { + if (!obj.hasOwnProperty(key)) { + continue; + } + if (allowedKeys.indexOf(key) === -1) { + throw new Error("Unknown key: " + key); + } + } +}; + +/** + * Deep copy the given object. The object MUST NOT have circular references and + * MUST NOT have functions. + * @param {Object} obj The object to deep copy. + * @return {Object} A copy of the object without any references to the original. + */ +module.exports.deepCopy = function (obj) { + return JSON.parse((0, _stringify2.default)(obj)); +}; + +/** + * Compare two objects for equality. The objects MUST NOT have circular references. + * + * @param {Object} x The first object to compare. + * @param {Object} y The second object to compare. + * + * @return {boolean} true if the two objects are equal + */ +var deepCompare = module.exports.deepCompare = function (x, y) { + // Inspired by + // http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249 + + // Compare primitives and functions. + // Also check if both arguments link to the same object. + if (x === y) { + return true; + } + + if ((typeof x === "undefined" ? "undefined" : (0, _typeof3.default)(x)) !== (typeof y === "undefined" ? "undefined" : (0, _typeof3.default)(y))) { + return false; + } + + // special-case NaN (since NaN !== NaN) + if (typeof x === 'number' && isNaN(x) && isNaN(y)) { + return true; + } + + // special-case null (since typeof null == 'object', but null.constructor + // throws) + if (x === null || y === null) { + return x === y; + } + + // everything else is either an unequal primitive, or an object + if (!(x instanceof Object)) { + return false; + } + + // check they are the same type of object + if (x.constructor !== y.constructor || x.prototype !== y.prototype) { + return false; + } + + // special-casing for some special types of object + if (x instanceof RegExp || x instanceof Date) { + return x.toString() === y.toString(); + } + + // the object algorithm works for Array, but it's sub-optimal. + if (x instanceof Array) { + if (x.length !== y.length) { + return false; + } + + for (var i = 0; i < x.length; i++) { + if (!deepCompare(x[i], y[i])) { + return false; + } + } + } else { + // disable jshint "The body of a for in should be wrapped in an if + // statement" + /* jshint -W089 */ + + // check that all of y's direct keys are in x + var p = void 0; + for (p in y) { + if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { + return false; + } + } + + // finally, compare each of x's keys with y + for (p in y) { + if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { + return false; + } + if (!deepCompare(x[p], y[p])) { + return false; + } + } + } + /* jshint +W089 */ + return true; +}; + +/** + * Copy properties from one object to another. + * + * All enumerable properties, included inherited ones, are copied. + * + * This is approximately equivalent to ES6's Object.assign, except + * that the latter doesn't copy inherited properties. + * + * @param {Object} target The object that will receive new properties + * @param {...Object} source Objects from which to copy properties + * + * @return {Object} target + */ +module.exports.extend = function () { + var target = arguments[0] || {}; + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + for (var propName in source) { + // eslint-disable-line guard-for-in + target[propName] = source[propName]; + } + } + return target; +}; + +/** + * Run polyfills to add Array.map and Array.filter if they are missing. + */ +module.exports.runPolyfills = function () { + // Array.prototype.filter + // ======================================================== + // SOURCE: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter + if (!Array.prototype.filter) { + Array.prototype.filter = function (fun /*, thisArg*/) { + if (this === void 0 || this === null) { + throw new TypeError(); + } + + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun !== 'function') { + throw new TypeError(); + } + + var res = []; + var thisArg = arguments.length >= 2 ? arguments[1] : void 0; + for (var i = 0; i < len; i++) { + if (i in t) { + var val = t[i]; + + // NOTE: Technically this should Object.defineProperty at + // the next index, as push can be affected by + // properties on Object.prototype and Array.prototype. + // But that method's new, and collisions should be + // rare, so use the more-compatible alternative. + if (fun.call(thisArg, val, i, t)) { + res.push(val); + } + } + } + + return res; + }; + } + + // Array.prototype.map + // ======================================================== + // SOURCE: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map + // Production steps of ECMA-262, Edition 5, 15.4.4.19 + // Reference: http://es5.github.io/#x15.4.4.19 + if (!Array.prototype.map) { + Array.prototype.map = function (callback, thisArg) { + var T = void 0, + k = void 0; + + if (this === null || this === undefined) { + throw new TypeError(' this is null or not defined'); + } + + // 1. Let O be the result of calling ToObject passing the |this| + // value as the argument. + var O = Object(this); + + // 2. Let lenValue be the result of calling the Get internal + // method of O with the argument "length". + // 3. Let len be ToUint32(lenValue). + var len = O.length >>> 0; + + // 4. If IsCallable(callback) is false, throw a TypeError exception. + // See: http://es5.github.com/#x9.11 + if (typeof callback !== 'function') { + throw new TypeError(callback + ' is not a function'); + } + + // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. + if (arguments.length > 1) { + T = thisArg; + } + + // 6. Let A be a new array created as if by the expression new Array(len) + // where Array is the standard built-in constructor with that name and + // len is the value of len. + var A = new Array(len); + + // 7. Let k be 0 + k = 0; + + // 8. Repeat, while k < len + while (k < len) { + var kValue, mappedValue; + + // a. Let Pk be ToString(k). + // This is implicit for LHS operands of the in operator + // b. Let kPresent be the result of calling the HasProperty internal + // method of O with argument Pk. + // This step can be combined with c + // c. If kPresent is true, then + if (k in O) { + // i. Let kValue be the result of calling the Get internal + // method of O with argument Pk. + kValue = O[k]; + + // ii. Let mappedValue be the result of calling the Call internal + // method of callback with T as the this value and argument + // list containing kValue, k, and O. + mappedValue = callback.call(T, kValue, k, O); + + // iii. Call the DefineOwnProperty internal method of A with arguments + // Pk, Property Descriptor + // { Value: mappedValue, + // Writable: true, + // Enumerable: true, + // Configurable: true }, + // and false. + + // In browsers that support Object.defineProperty, use the following: + // Object.defineProperty(A, k, { + // value: mappedValue, + // writable: true, + // enumerable: true, + // configurable: true + // }); + + // For best browser support, use the following: + A[k] = mappedValue; + } + // d. Increase k by 1. + k++; + } + + // 9. return A + return A; + }; + } + + // Array.prototype.forEach + // ======================================================== + // SOURCE: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach + // Production steps of ECMA-262, Edition 5, 15.4.4.18 + // Reference: http://es5.github.io/#x15.4.4.18 + if (!Array.prototype.forEach) { + Array.prototype.forEach = function (callback, thisArg) { + var T = void 0, + k = void 0; + + if (this === null || this === undefined) { + throw new TypeError(' this is null or not defined'); + } + + // 1. Let O be the result of calling ToObject passing the |this| value as the + // argument. + var O = Object(this); + + // 2. Let lenValue be the result of calling the Get internal method of O with the + // argument "length". + // 3. Let len be ToUint32(lenValue). + var len = O.length >>> 0; + + // 4. If IsCallable(callback) is false, throw a TypeError exception. + // See: http://es5.github.com/#x9.11 + if (typeof callback !== "function") { + throw new TypeError(callback + ' is not a function'); + } + + // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. + if (arguments.length > 1) { + T = thisArg; + } + + // 6. Let k be 0 + k = 0; + + // 7. Repeat, while k < len + while (k < len) { + var kValue; + + // a. Let Pk be ToString(k). + // This is implicit for LHS operands of the in operator + // b. Let kPresent be the result of calling the HasProperty internal + // method of O with + // argument Pk. + // This step can be combined with c + // c. If kPresent is true, then + if (k in O) { + // i. Let kValue be the result of calling the Get internal method of O with + // argument Pk + kValue = O[k]; + + // ii. Call the Call internal method of callback with T as the this value and + // argument list containing kValue, k, and O. + callback.call(T, kValue, k, O); + } + // d. Increase k by 1. + k++; + } + // 8. return undefined + }; + } +}; + +/** + * Inherit the prototype methods from one constructor into another. This is a + * port of the Node.js implementation with an Object.create polyfill. + * + * @param {function} ctor Constructor function which needs to inherit the + * prototype. + * @param {function} superCtor Constructor function to inherit prototype from. + */ +module.exports.inherits = function (ctor, superCtor) { + // Add Object.create polyfill for IE8 + // Source: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript + // /Reference/Global_Objects/Object/create#Polyfill + if (typeof _create2.default != 'function') { + // Production steps of ECMA-262, Edition 5, 15.2.3.5 + // Reference: http://es5.github.io/#x15.2.3.5 + Object.create = function () { + // To save on memory, use a shared constructor + function Temp() {} + + // make a safe reference to Object.prototype.hasOwnProperty + var hasOwn = Object.prototype.hasOwnProperty; + + return function (O) { + // 1. If Type(O) is not Object or Null throw a TypeError exception. + if ((typeof O === "undefined" ? "undefined" : (0, _typeof3.default)(O)) != 'object') { + throw new TypeError('Object prototype may only be an Object or null'); + } + + // 2. Let obj be the result of creating a new object as if by the + // expression new Object() where Object is the standard built-in + // constructor with that name + // 3. Set the [[Prototype]] internal property of obj to O. + Temp.prototype = O; + var obj = new Temp(); + Temp.prototype = null; // Let's not keep a stray reference to O... + + // 4. If the argument Properties is present and not undefined, add + // own properties to obj as if by calling the standard built-in + // function Object.defineProperties with arguments obj and + // Properties. + if (arguments.length > 1) { + // Object.defineProperties does ToObject on its first argument. + var Properties = Object(arguments[1]); + for (var prop in Properties) { + if (hasOwn.call(Properties, prop)) { + obj[prop] = Properties[prop]; + } + } + } + + // 5. Return obj + return obj; + }; + }(); + } + // END polyfill + + // Add util.inherits from Node.js + // Source: + // https://github.com/joyent/node/blob/master/lib/util.js + // Copyright Joyent, Inc. and other Node contributors. + // + // Permission is hereby granted, free of charge, to any person obtaining a + // copy of this software and associated documentation files (the + // "Software"), to deal in the Software without restriction, including + // without limitation the rights to use, copy, modify, merge, publish, + // distribute, sublicense, and/or sell copies of the Software, and to permit + // persons to whom the Software is furnished to do so, subject to the + // following conditions: + // + // The above copyright notice and this permission notice shall be included + // in all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + // USE OR OTHER DEALINGS IN THE SOFTWARE. + ctor.super_ = superCtor; + ctor.prototype = (0, _create2.default)(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); +}; + +},{"babel-runtime/core-js/json/stringify":55,"babel-runtime/core-js/object/create":57,"babel-runtime/helpers/typeof":72}],51:[function(require,module,exports){ +(function (global){ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; +/** + * This is an internal module. See {@link createNewMatrixCall} for the public API. + * @module webrtc/call + */ + +var _create = require("babel-runtime/core-js/object/create"); + +var _create2 = _interopRequireDefault(_create); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var utils = require("../utils"); +var EventEmitter = require("events").EventEmitter; +var DEBUG = true; // set true to enable console logging. + +// events: hangup, error(err), replaced(call), state(state, oldState) + +/** + * Fires whenever an error occurs when call.js encounters an issue with setting up the call. + *

+ * The error given will have a code equal to either `MatrixCall.ERR_LOCAL_OFFER_FAILED` or + * `MatrixCall.ERR_NO_USER_MEDIA`. `ERR_LOCAL_OFFER_FAILED` is emitted when the local client + * fails to create an offer. `ERR_NO_USER_MEDIA` is emitted when the user has denied access + * to their audio/video hardware. + * + * @event module:webrtc/call~MatrixCall#"error" + * @param {Error} err The error raised by MatrixCall. + * @example + * matrixCall.on("error", function(err){ + * console.error(err.code, err); + * }); + */ + +/** + * Construct a new Matrix Call. + * @constructor + * @param {Object} opts Config options. + * @param {string} opts.roomId The room ID for this call. + * @param {Object} opts.webRtc The WebRTC globals from the browser. + * @param {boolean} opts.forceTURN whether relay through TURN should be forced. + * @param {Object} opts.URL The URL global. + * @param {Array} opts.turnServers Optional. A list of TURN servers. + * @param {MatrixClient} opts.client The Matrix Client instance to send events to. + */ +function MatrixCall(opts) { + this.roomId = opts.roomId; + this.client = opts.client; + this.webRtc = opts.webRtc; + this.forceTURN = opts.forceTURN; + this.URL = opts.URL; + // Array of Objects with urls, username, credential keys + this.turnServers = opts.turnServers || []; + if (this.turnServers.length === 0) { + this.turnServers.push({ + urls: [MatrixCall.FALLBACK_STUN_SERVER] + }); + } + utils.forEach(this.turnServers, function (server) { + utils.checkObjectHasKeys(server, ["urls"]); + }); + + this.callId = "c" + new Date().getTime() + Math.random(); + this.state = 'fledgling'; + this.didConnect = false; + + // A queue for candidates waiting to go out. + // We try to amalgamate candidates into a single candidate message where + // possible + this.candidateSendQueue = []; + this.candidateSendTries = 0; + + // Lookup from opaque queue ID to a promise for media element operations that + // need to be serialised into a given queue. Store this per-MatrixCall on the + // assumption that multiple matrix calls will never compete for control of the + // same DOM elements. + this.mediaPromises = (0, _create2.default)(null); + + this.screenSharingStream = null; + + this._answerContent = null; +} +/** The length of time a call can be ringing for. */ +MatrixCall.CALL_TIMEOUT_MS = 60000; +/** The fallback server to use for STUN. */ +MatrixCall.FALLBACK_STUN_SERVER = 'stun:stun.l.google.com:19302'; +/** An error code when the local client failed to create an offer. */ +MatrixCall.ERR_LOCAL_OFFER_FAILED = "local_offer_failed"; +/** + * An error code when there is no local mic/camera to use. This may be because + * the hardware isn't plugged in, or the user has explicitly denied access. + */ +MatrixCall.ERR_NO_USER_MEDIA = "no_user_media"; + +/* + * Error code used when a call event failed to send + * because unknown devices were present in the room + */ +MatrixCall.ERR_UNKNOWN_DEVICES = "unknown_devices"; + +/* + * Error code usewd when we fail to send the invite + * for some reason other than there being unknown devices + */ +MatrixCall.ERR_SEND_INVITE = "send_invite"; + +/* + * Error code usewd when we fail to send the answer + * for some reason other than there being unknown devices + */ +MatrixCall.ERR_SEND_ANSWER = "send_answer"; + +utils.inherits(MatrixCall, EventEmitter); + +/** + * Place a voice call to this room. + * @throws If you have not specified a listener for 'error' events. + */ +MatrixCall.prototype.placeVoiceCall = function () { + debuglog("placeVoiceCall"); + checkForErrorListener(this); + _placeCallWithConstraints(this, _getUserMediaVideoContraints('voice')); + this.type = 'voice'; +}; + +/** + * Place a video call to this room. + * @param {Element} remoteVideoElement a <video> DOM element + * to render video to. + * @param {Element} localVideoElement a <video> DOM element + * to render the local camera preview. + * @throws If you have not specified a listener for 'error' events. + */ +MatrixCall.prototype.placeVideoCall = function (remoteVideoElement, localVideoElement) { + debuglog("placeVideoCall"); + checkForErrorListener(this); + this.localVideoElement = localVideoElement; + this.remoteVideoElement = remoteVideoElement; + _placeCallWithConstraints(this, _getUserMediaVideoContraints('video')); + this.type = 'video'; + _tryPlayRemoteStream(this); +}; + +/** + * Place a screen-sharing call to this room. This includes audio. + * This method is EXPERIMENTAL and subject to change without warning. It + * only works in Google Chrome and Firefox >= 44. + * @param {Element} remoteVideoElement a <video> DOM element + * to render video to. + * @param {Element} localVideoElement a <video> DOM element + * to render the local camera preview. + * @throws If you have not specified a listener for 'error' events. + */ +MatrixCall.prototype.placeScreenSharingCall = function (remoteVideoElement, localVideoElement) { + debuglog("placeScreenSharingCall"); + checkForErrorListener(this); + var screenConstraints = _getScreenSharingConstraints(this); + if (!screenConstraints) { + return; + } + this.localVideoElement = localVideoElement; + this.remoteVideoElement = remoteVideoElement; + var self = this; + this.webRtc.getUserMedia(screenConstraints, function (stream) { + self.screenSharingStream = stream; + debuglog("Got screen stream, requesting audio stream..."); + var audioConstraints = _getUserMediaVideoContraints('voice'); + _placeCallWithConstraints(self, audioConstraints); + }, function (err) { + self.emit("error", callError(MatrixCall.ERR_NO_USER_MEDIA, "Failed to get screen-sharing stream: " + err)); + }); + this.type = 'video'; + _tryPlayRemoteStream(this); +}; + +/** + * Play the given HTMLMediaElement, serialising the operation into a chain + * of promises to avoid racing access to the element + * @param {Element} element HTMLMediaElement element to play + * @param {string} queueId Arbitrary ID to track the chain of promises to be used + */ +MatrixCall.prototype.playElement = function (element, queueId) { + console.log("queuing play on " + queueId + " and element " + element); + // XXX: FIXME: Does this leak elements, given the old promises + // may hang around and retain a reference to them? + if (this.mediaPromises[queueId]) { + // XXX: these promises can fail (e.g. by