40449 lines
1.4 MiB
40449 lines
1.4 MiB
(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<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
|
(function (global){
|
|
var matrixcs = require("./lib/matrix");
|
|
matrixcs.request(require("browser-request"));
|
|
|
|
// just *accessing* indexedDB throws an exception in firefox with
|
|
// indexeddb disabled.
|
|
var indexedDB;
|
|
try {
|
|
indexedDB = global.indexedDB;
|
|
} catch(e) {}
|
|
|
|
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
|
|
if (indexedDB) {
|
|
matrixcs.setCryptoStoreFactory(
|
|
function() {
|
|
return new matrixcs.IndexedDBCryptoStore(
|
|
indexedDB, "matrix-js-sdk:crypto"
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
module.exports = matrixcs; // keep export for browserify package deps
|
|
global.matrixcs = matrixcs;
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
|
|
},{"./lib/matrix":26,"browser-request":75}],2:[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 _classCallCheck2 = require("babel-runtime/helpers/classCallCheck");
|
|
|
|
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
|
|
|
|
var _createClass2 = require("babel-runtime/helpers/createClass");
|
|
|
|
var _createClass3 = _interopRequireDefault(_createClass2);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
/*
|
|
Copyright 2015, 2016 OpenMarket Ltd
|
|
Copyright 2017 Vector Creations 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
|
|
*/
|
|
|
|
var Reemitter = function () {
|
|
function Reemitter(target) {
|
|
(0, _classCallCheck3.default)(this, Reemitter);
|
|
|
|
this.target = target;
|
|
|
|
// We keep one bound event handler for each event name so we know
|
|
// what event is arriving
|
|
this.boundHandlers = {};
|
|
}
|
|
|
|
(0, _createClass3.default)(Reemitter, [{
|
|
key: "_handleEvent",
|
|
value: function _handleEvent(eventName) {
|
|
var _target;
|
|
|
|
for (var _len = arguments.length, args = Array(_len > 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 <code>require("request")
|
|
* </code> 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
|
|
* <code>?user_id=</code>.
|
|
*
|
|
* @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: <code>{room_id: {string},
|
|
* room_alias: {string(opt)}}</code>
|
|
* @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 <tt>file.name</tt>.
|
|
*
|
|
* @param {string=} opts.type Content-type for the upload. Defaults to
|
|
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
|
|
*
|
|
* @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.<string, Object<string, 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 <code>require("request")
|
|
* </code> 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
|
|
* <code>?user_id=</code>.
|
|
*
|
|
* @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. <b>This method is experimental
|
|
* and may change without warning.</b>
|
|
* @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 <b>explicitly</b> 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<module:crypto-deviceinfo[]>} 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<?module:crypto-deviceinfo>} 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<module:crypto/deviceinfo?>}
|
|
*/
|
|
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 <strong>returned Room object will have no current state.
|
|
* </strong> 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. <strong>This method is experimental and
|
|
* may change.</strong>
|
|
* @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 <i>same</i>
|
|
* 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, <code>Room.oldState.paginationToken</code> will be
|
|
* <code>null</code>.
|
|
* @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
|
|
*
|
|
* <p>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<String>} 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<Object>} 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 <code>limit=</code> to apply
|
|
* to initial sync. Default: 8.
|
|
* @param {Boolean=} opts.includeArchivedRooms True to put <code>archived=true</code>
|
|
* on the <code>/initialSync</code> 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 "<b>chronological</b>", messages will appear
|
|
* in the timeline when the call to <code>sendEvent</code> was made. If
|
|
* "<b>detached</b>", 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<String>
|
|
// 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. <strong>This
|
|
* method is experimental and may change.</strong>
|
|
* @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.
|
|
* <p>
|
|
* 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:
|
|
* <ul>
|
|
*
|
|
* <li>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. <i>This is the equivalent of "syncComplete" in the
|
|
* previous API.</i></li>
|
|
*
|
|
* <li>SYNCING : The client is currently polling for new events from the server.
|
|
* This will be called <i>after</i> processing latest events from a sync.</li>
|
|
*
|
|
* <li>ERROR : The client has had a problem syncing with the server. If this is
|
|
* called <i>before</i> PREPARED then there was a problem performing the initial
|
|
* sync. If this is called <i>after</i> PREPARED then there was a problem polling
|
|
* the server for updates. This may be called multiple times even if the state is
|
|
* already ERROR. <i>This is the equivalent of "syncError" in the previous
|
|
* API.</i></li>
|
|
*
|
|
* <li>RECONNECTING: The sync connection has dropped, but not (yet) in a way that
|
|
* should be considered erroneous.
|
|
* </li>
|
|
*
|
|
* <li>STOPPED: The client has stopped syncing with server due to stopClient
|
|
* being called.
|
|
* </li>
|
|
* </ul>
|
|
* State transition diagram:
|
|
* <pre>
|
|
* +---->STOPPED
|
|
* |
|
|
* +----->PREPARED -------> SYNCING <--+
|
|
* | ^ | ^ |
|
|
* | | | | |
|
|
* | | V | |
|
|
* null ------+ | +--------RECONNECTING |
|
|
* | | V |
|
|
* +------->ERROR ---------------------+
|
|
*
|
|
* NB: 'null' will never be emitted by this event.
|
|
*
|
|
* </pre>
|
|
* Transitions:
|
|
* <ul>
|
|
*
|
|
* <li><code>null -> PREPARED</code> : Occurs when the initial sync is completed
|
|
* first time. This involves setting up filters and obtaining push rules.
|
|
*
|
|
* <li><code>null -> ERROR</code> : Occurs when the initial sync failed first time.
|
|
*
|
|
* <li><code>ERROR -> PREPARED</code> : Occurs when the initial sync succeeds
|
|
* after previously failing.
|
|
*
|
|
* <li><code>PREPARED -> SYNCING</code> : Occurs immediately after transitioning
|
|
* to PREPARED. Starts listening for live updates rather than catching up.
|
|
*
|
|
* <li><code>SYNCING -> RECONNECTING</code> : Occurs when the live update fails.
|
|
*
|
|
* <li><code>RECONNECTING -> RECONNECTING</code> : Can occur if the update calls
|
|
* continue to fail, but the keepalive calls (to /versions) succeed.
|
|
*
|
|
* <li><code>RECONNECTING -> ERROR</code> : Occurs when the keepalive call also fails
|
|
*
|
|
* <li><code>ERROR -> SYNCING</code> : Occurs when the client has performed a
|
|
* live update after having previously failed.
|
|
*
|
|
* <li><code>ERROR -> ERROR</code> : Occurs when the client has failed to keepalive
|
|
* for a second time or more.</li>
|
|
*
|
|
* <li><code>SYNCING -> SYNCING</code> : Occurs when the client has performed a live
|
|
* update. This is called <i>after</i> processing.</li>
|
|
*
|
|
* <li><code>* -> STOPPED</code> : Occurs once the client has stopped syncing or
|
|
* trying to sync after stopClient has been called.</li>
|
|
* </ul>
|
|
*
|
|
* @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" <b>or null</b>.
|
|
*
|
|
* @param {?Object} data Data about this transition.
|
|
*
|
|
* @param {MatrixError} data.err The matrix error if <code>state=ERROR</code>.
|
|
*
|
|
* @param {String} data.oldSyncToken The 'since' token passed to /sync.
|
|
* <code>null</code> for the first successful sync since this client was
|
|
* started. Only present if <code>state=PREPARED</code> or
|
|
* <code>state=SYNCING</code>.
|
|
*
|
|
* @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
|
|
* <code>state=PREPARED</code> or <code>state=SYNCING</code>.
|
|
*
|
|
* @param {boolean} data.catchingUp True if we are working our way through a
|
|
* backlog of events after connecting. Only present if <code>state=SYNCING</code>.
|
|
*
|
|
* @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. <strong>This event
|
|
* is experimental and may change.</strong>
|
|
* @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. <strong>This event is experimental and
|
|
* may change.</strong>
|
|
* @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.
|
|
* <strong>This event is experimental and may change.</strong>
|
|
* @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 <b>one time</b> 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.
|
|
* <b>Caution:</b> 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<bool>} 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<string, string>} 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 "<senderKey>|<session_id>|<message_index>"
|
|
// Values are objects of the form "{id: <event id>, timestamp: <ts>}"
|
|
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<string>} 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
|
|
* <tt>curve25519</tt>, 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<string[]>} 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<?string>} 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.
|
|
* <p>
|
|
* 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<string>} 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<string>} 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<boolean>} 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<string, string>} keysClaimed
|
|
* @property {Array<string>} 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<string>} forwardingCurve25519KeyChain Devices involved in forwarding
|
|
* this session to us.
|
|
* @param {string} sessionId session identifier
|
|
* @param {string} sessionKey base64-encoded secret key
|
|
* @param {Object<string, string>} 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<string>,
|
|
* keysClaimed: Object<string, string>}>}
|
|
*/
|
|
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<boolean>} 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<string>,
|
|
* 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.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
|
|
*/
|
|
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.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
|
|
*/
|
|
/*
|
|
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<module:crypto~EventDecryptionResult>} 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<boolean>} 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<userId, deviceId>} devicemap
|
|
* mapping from userId to deviceId to {@link module:crypto~OlmSessionResult}
|
|
*
|
|
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
|
* map from userid to list of devices
|
|
*
|
|
* @return {array<object<userid, deviceInfo>>}
|
|
*/
|
|
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<userId, deviceInfo>} 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<string, module:crypto/deviceinfo[]>} 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.<string,string>} 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<bool>} 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<module:crypto/deviceinfo>} 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
|
|
* <p>
|
|
* 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}).
|
|
* <p>
|
|
* This method is provided for debugging purposes.
|
|
*
|
|
* @param {string} userId id of user to inspect
|
|
*
|
|
* @return {Promise<Object.<string, {deviceIdKey: string, sessions: object[]}>>}
|
|
*/
|
|
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<module:crypto~EventDecryptionResult>} 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 <tt>type</tt> and <tt>content</tt> 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<string>} 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<string, string>} 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<string, module:crypto/deviceinfo[]>} 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<Number>} 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<Number>} 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<Number>} 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 <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
|
|
*
|
|
* @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 <tt>file.name</tt>.
|
|
*
|
|
* @param {string=} opts.type Content-type for the upload. Defaults to
|
|
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
|
|
*
|
|
* @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 <b>after</b> 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 <code>{data: {Object},
|
|
* headers: {Object}, code: {Number}}</code>.
|
|
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
|
* 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 <b>after</b> 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 <code>{data: {Object},
|
|
* headers: {Object}, code: {Number}}</code>.
|
|
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
|
* 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 <b>after</b> 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 <code>{data: {Object},
|
|
* headers: {Object}, code: {Number}}</code>.
|
|
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
|
* 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 <b>after</b> 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 <code>{data: {Object},
|
|
* headers: {Object}, code: {Number}}</code>.
|
|
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
|
* 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 <code>{data: {Object},
|
|
* headers: {Object}, code: {Number}}</code>.
|
|
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
|
* 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 <b>after</b> 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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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. <strong>Work in progress; unstable.</strong> */
|
|
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 <code>$http</code>. 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
|
|
* <code>opts</code>.
|
|
*/
|
|
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
|
|
* <code>{statusCode: {Number}, headers: {Object}}</code>
|
|
* @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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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 <code>opts.pendingEventOrdering</code> 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.
|
|
*
|
|
* <p>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
|
|
*
|
|
* <p>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 <b>last</b> 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
|
|
*
|
|
* <p>An EventTimeline represents a contiguous sequence of events in a room.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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. <b>Do not access
|
|
* this property</b> 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. <strong>This property is experimental and may change.</strong>
|
|
*/
|
|
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. <code>$143350589368169JsLZx:localhost
|
|
* </code>
|
|
*/
|
|
getId: function getId() {
|
|
return this.event.event_id;
|
|
},
|
|
|
|
/**
|
|
* Get the user_id for this event.
|
|
* @return {string} The user ID, e.g. <code>@alice:matrix.org</code>
|
|
*/
|
|
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. <code>m.room.message</code>
|
|
*/
|
|
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 <code>undefined</code>
|
|
* for <code>m.presence</code> events.
|
|
* @return {string} The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
|
|
* </code>
|
|
*/
|
|
getRoomId: function getRoomId() {
|
|
return this.event.room_id;
|
|
},
|
|
|
|
/**
|
|
* Get the timestamp of this event.
|
|
* @return {Number} The event timestamp, e.g. <code>1433502692297</code>
|
|
*/
|
|
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. <code>new Date(1433502692297)</code>
|
|
*/
|
|
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)
|
|
* <strong>This method is experimental and may change.</strong>
|
|
* @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 <code>undefined
|
|
* </code> for message events.
|
|
* @return {string} The event's <code>state_key</code>.
|
|
*/
|
|
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
|
|
* <tt>"m.room.encrypted"</tt>
|
|
*
|
|
* @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<string, string>}
|
|
*/
|
|
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 <code>m.room.member</code> 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 <code>m.room.power_levels</code>
|
|
* 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 <i>before</i> 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.<string, RoomMember>} members The room member dictionary, keyed
|
|
* on the user's ID.
|
|
* @prop {Object.<string, Object.<string, MatrixEvent>>} 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<RoomMember>} 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 <code>undefined</code> then all matching state events will be
|
|
* returned.
|
|
* @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was
|
|
* <code>undefined</code>, 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 <code>m.room.member</code> 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. <code>m.room.name</code>)
|
|
* @param {string} info.desc The description of the room (e.g.
|
|
* <code>m.room.topic</code>)
|
|
* @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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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 "<b>chronological</b>", messages will appear
|
|
* in the timeline when the call to <code>sendEvent</code> was made. If
|
|
* "<b>detached</b>", 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<MatrixEvent>} 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: <event_id>,
|
|
// data: <receipt_data>
|
|
// }
|
|
// }
|
|
};
|
|
this._receiptCacheByEventId = {
|
|
// $event_id: [{
|
|
// type: $type,
|
|
// userId: $user_id,
|
|
// data: <receipt 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 <code>opts.pendingEventOrdering</code> 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.
|
|
*
|
|
* <p>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
|
|
*
|
|
* <p>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 <b>last</b> 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 <code>null</code>.
|
|
*/
|
|
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. <code>'join'</code>
|
|
* @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.
|
|
*
|
|
* <p>The event is added to either the pendingEventList, or the live timeline,
|
|
* depending on the setting of opts.pendingEventOrdering.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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 <b>only</b>.
|
|
*
|
|
* @throws If <code>duplicateStrategy</code> 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 <b>read up to</b> 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<MatrixEvent>} 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.
|
|
*
|
|
* <p>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'.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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'.
|
|
*
|
|
* <p>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 <code>m.presence</code> 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 <i>before</i> 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<MatrixEvent>} A shallow copy of events in the queue or null.
|
|
* Modifying this array will not modify the list itself. Modifying events in
|
|
* this array <i>will</i> 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 <code>m.room.message</code> 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
|
|
* <code>-1</code>. 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
|
|
* <code>-1</code>, 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 <code>null</code>
|
|
* 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 <code>null</code>,
|
|
* 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<T[]>} 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
|
|
* <code>connect()</code> before this store can be used.
|
|
* @constructor
|
|
* @param {Object} indexedDBInterface The Indexed DB interface e.g
|
|
* <code>window.indexedDB</code>
|
|
* @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<Object[]>} 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<Object[]>} 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<Object>} 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
|
|
* <code>connect()</code> 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<Object[]>} 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
|
|
* <code>startup()</code>. 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 <code>startup()</code> is called.
|
|
* <pre>
|
|
* 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!");
|
|
* }
|
|
* });
|
|
* </pre>
|
|
*
|
|
* @constructor
|
|
* @extends MatrixInMemoryStore
|
|
* @param {Object} opts Options object.
|
|
* @param {Object} opts.indexedDB The Indexed DB interface e.g.
|
|
* <code>window.indexedDB</code>
|
|
* @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<Object>} 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<MatrixEvent>} 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<MatrixEvent>} 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<MatrixEvent>} 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<MatrixEvent>} 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);
|
|
}
|
|
|
|
/**
|
|
* <b>Internal class - unstable.</b>
|
|
* 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 <b>explicitly</b> 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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>Before the window is useful, it must be initialised by calling {@link
|
|
* module:timeline-window~TimelineWindow#load|load}.
|
|
*
|
|
* <p>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
|
|
*
|
|
* <p>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 <code>fn(element){...}</code>
|
|
* @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 <code>fn(element, index, array){...}</code>.
|
|
* @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 <code>Object.keys()</code>.
|
|
* @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 <code>fn(element, index)</code>.
|
|
*/
|
|
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 <code>fn(element, index, array)</code>
|
|
* @param {boolean} reverse True to search in reverse order.
|
|
* @return {*} The first value in the array which returns <code>true</code> 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 <code>fn(element, index, array)</code>. 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.
|
|
* <p>
|
|
* 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<Object>} 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 <code><video></code> DOM element
|
|
* to render video to.
|
|
* @param {Element} localVideoElement a <code><video></code> 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.
|
|
* <b>This method is EXPERIMENTAL and subject to change without warning. It
|
|
* only works in Google Chrome and Firefox >= 44.</b>
|
|
* @param {Element} remoteVideoElement a <code><video></code> DOM element
|
|
* to render video to.
|
|
* @param {Element} localVideoElement a <code><video></code> 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 <video/> being unmounted whilst
|
|
// pending receiving media to play - e.g. whilst switching between
|
|
// rooms before answering an inbound call), and throw unhandled exceptions.
|
|
// However, we should soldier on as best we can even if they fail, given
|
|
// these failures may be non-fatal (as in the case of unmounts)
|
|
this.mediaPromises[queueId] = this.mediaPromises[queueId].then(function () {
|
|
console.log("previous promise completed for " + queueId);
|
|
return element.play();
|
|
}, function () {
|
|
console.log("previous promise failed for " + queueId);
|
|
return element.play();
|
|
});
|
|
} else {
|
|
this.mediaPromises[queueId] = element.play();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Pause the given HTMLMediaElement, serialising the operation into a chain
|
|
* of promises to avoid racing access to the element
|
|
* @param {Element} element HTMLMediaElement element to pause
|
|
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
|
|
*/
|
|
MatrixCall.prototype.pauseElement = function (element, queueId) {
|
|
console.log("queuing pause on " + queueId + " and element " + element);
|
|
if (this.mediaPromises[queueId]) {
|
|
this.mediaPromises[queueId] = this.mediaPromises[queueId].then(function () {
|
|
console.log("previous promise completed for " + queueId);
|
|
return element.pause();
|
|
}, function () {
|
|
console.log("previous promise failed for " + queueId);
|
|
return element.pause();
|
|
});
|
|
} else {
|
|
// pause doesn't actually return a promise, but do this for symmetry
|
|
// and just in case it does in future.
|
|
this.mediaPromises[queueId] = element.pause();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Assign the given HTMLMediaElement by setting the .src attribute on it,
|
|
* serialising the operation into a chain of promises to avoid racing access
|
|
* to the element
|
|
* @param {Element} element HTMLMediaElement element to pause
|
|
* @param {MediaStream} srcObject the srcObject attribute value to assign to the element
|
|
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
|
|
*/
|
|
MatrixCall.prototype.assignElement = function (element, srcObject, queueId) {
|
|
console.log("queuing assign on " + queueId + " element " + element + " for " + srcObject);
|
|
if (this.mediaPromises[queueId]) {
|
|
this.mediaPromises[queueId] = this.mediaPromises[queueId].then(function () {
|
|
console.log("previous promise completed for " + queueId);
|
|
element.srcObject = srcObject;
|
|
}, function () {
|
|
console.log("previous promise failed for " + queueId);
|
|
element.srcObject = srcObject;
|
|
});
|
|
} else {
|
|
element.srcObject = srcObject;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Retrieve the local <code><video></code> DOM element.
|
|
* @return {Element} The dom element
|
|
*/
|
|
MatrixCall.prototype.getLocalVideoElement = function () {
|
|
return this.localVideoElement;
|
|
};
|
|
|
|
/**
|
|
* Retrieve the remote <code><video></code> DOM element
|
|
* used for playing back video capable streams.
|
|
* @return {Element} The dom element
|
|
*/
|
|
MatrixCall.prototype.getRemoteVideoElement = function () {
|
|
return this.remoteVideoElement;
|
|
};
|
|
|
|
/**
|
|
* Retrieve the remote <code><audio></code> DOM element
|
|
* used for playing back audio only streams.
|
|
* @return {Element} The dom element
|
|
*/
|
|
MatrixCall.prototype.getRemoteAudioElement = function () {
|
|
return this.remoteAudioElement;
|
|
};
|
|
|
|
/**
|
|
* Set the local <code><video></code> DOM element. If this call is active,
|
|
* video will be rendered to it immediately.
|
|
* @param {Element} element The <code><video></code> DOM element.
|
|
*/
|
|
MatrixCall.prototype.setLocalVideoElement = function (element) {
|
|
this.localVideoElement = element;
|
|
|
|
if (element && this.localAVStream && this.type === 'video') {
|
|
element.autoplay = true;
|
|
this.assignElement(element, this.localAVStream, "localVideo");
|
|
element.muted = true;
|
|
var self = this;
|
|
setTimeout(function () {
|
|
var vel = self.getLocalVideoElement();
|
|
if (vel.play) {
|
|
self.playElement(vel, "localVideo");
|
|
}
|
|
}, 0);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set the remote <code><video></code> DOM element. If this call is active,
|
|
* the first received video-capable stream will be rendered to it immediately.
|
|
* @param {Element} element The <code><video></code> DOM element.
|
|
*/
|
|
MatrixCall.prototype.setRemoteVideoElement = function (element) {
|
|
this.remoteVideoElement = element;
|
|
_tryPlayRemoteStream(this);
|
|
};
|
|
|
|
/**
|
|
* Set the remote <code><audio></code> DOM element. If this call is active,
|
|
* the first received audio-only stream will be rendered to it immediately.
|
|
* The audio will *not* be rendered from the remoteVideoElement.
|
|
* @param {Element} element The <code><video></code> DOM element.
|
|
*/
|
|
MatrixCall.prototype.setRemoteAudioElement = function (element) {
|
|
this.remoteVideoElement.muted = true;
|
|
this.remoteAudioElement = element;
|
|
this.remoteAudioElement.muted = false;
|
|
_tryPlayRemoteAudioStream(this);
|
|
};
|
|
|
|
/**
|
|
* Configure this call from an invite event. Used by MatrixClient.
|
|
* @protected
|
|
* @param {MatrixEvent} event The m.call.invite event
|
|
*/
|
|
MatrixCall.prototype._initWithInvite = function (event) {
|
|
this.msg = event.getContent();
|
|
this.peerConn = _createPeerConnection(this);
|
|
var self = this;
|
|
if (this.peerConn) {
|
|
this.peerConn.setRemoteDescription(new this.webRtc.RtcSessionDescription(this.msg.offer), hookCallback(self, self._onSetRemoteDescriptionSuccess), hookCallback(self, self._onSetRemoteDescriptionError));
|
|
}
|
|
setState(this, 'ringing');
|
|
this.direction = 'inbound';
|
|
|
|
// firefox and OpenWebRTC's RTCPeerConnection doesn't add streams until it
|
|
// starts getting media on them so we need to figure out whether a video
|
|
// channel has been offered by ourselves.
|
|
if (this.msg.offer && this.msg.offer.sdp && this.msg.offer.sdp.indexOf('m=video') > -1) {
|
|
this.type = 'video';
|
|
} else {
|
|
this.type = 'voice';
|
|
}
|
|
|
|
if (event.getAge()) {
|
|
setTimeout(function () {
|
|
if (self.state == 'ringing') {
|
|
debuglog("Call invite has expired. Hanging up.");
|
|
self.hangupParty = 'remote'; // effectively
|
|
setState(self, 'ended');
|
|
stopAllMedia(self);
|
|
if (self.peerConn.signalingState != 'closed') {
|
|
self.peerConn.close();
|
|
}
|
|
self.emit("hangup", self);
|
|
}
|
|
}, this.msg.lifetime - event.getAge());
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Configure this call from a hangup event. Used by MatrixClient.
|
|
* @protected
|
|
* @param {MatrixEvent} event The m.call.hangup event
|
|
*/
|
|
MatrixCall.prototype._initWithHangup = function (event) {
|
|
// perverse as it may seem, sometimes we want to instantiate a call with a
|
|
// hangup message (because when getting the state of the room on load, events
|
|
// come in reverse order and we want to remember that a call has been hung up)
|
|
this.msg = event.getContent();
|
|
setState(this, 'ended');
|
|
};
|
|
|
|
/**
|
|
* Answer a call.
|
|
*/
|
|
MatrixCall.prototype.answer = function () {
|
|
debuglog("Answering call %s of type %s", this.callId, this.type);
|
|
var self = this;
|
|
|
|
if (self._answerContent) {
|
|
self._sendAnswer();
|
|
return;
|
|
}
|
|
|
|
if (!this.localAVStream && !this.waitForLocalAVStream) {
|
|
this.webRtc.getUserMedia(_getUserMediaVideoContraints(this.type), hookCallback(self, self._maybeGotUserMediaForAnswer), hookCallback(self, self._maybeGotUserMediaForAnswer));
|
|
setState(this, 'wait_local_media');
|
|
} else if (this.localAVStream) {
|
|
this._maybeGotUserMediaForAnswer(this.localAVStream);
|
|
} else if (this.waitForLocalAVStream) {
|
|
setState(this, 'wait_local_media');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Replace this call with a new call, e.g. for glare resolution. Used by
|
|
* MatrixClient.
|
|
* @protected
|
|
* @param {MatrixCall} newCall The new call.
|
|
*/
|
|
MatrixCall.prototype._replacedBy = function (newCall) {
|
|
debuglog(this.callId + " being replaced by " + newCall.callId);
|
|
if (this.state == 'wait_local_media') {
|
|
debuglog("Telling new call to wait for local media");
|
|
newCall.waitForLocalAVStream = true;
|
|
} else if (this.state == 'create_offer') {
|
|
debuglog("Handing local stream to new call");
|
|
newCall._maybeGotUserMediaForAnswer(this.localAVStream);
|
|
delete this.localAVStream;
|
|
} else if (this.state == 'invite_sent') {
|
|
debuglog("Handing local stream to new call");
|
|
newCall._maybeGotUserMediaForAnswer(this.localAVStream);
|
|
delete this.localAVStream;
|
|
}
|
|
newCall.localVideoElement = this.localVideoElement;
|
|
newCall.remoteVideoElement = this.remoteVideoElement;
|
|
newCall.remoteAudioElement = this.remoteAudioElement;
|
|
this.successor = newCall;
|
|
this.emit("replaced", newCall);
|
|
this.hangup(true);
|
|
};
|
|
|
|
/**
|
|
* Hangup a call.
|
|
* @param {string} reason The reason why the call is being hung up.
|
|
* @param {boolean} suppressEvent True to suppress emitting an event.
|
|
*/
|
|
MatrixCall.prototype.hangup = function (reason, suppressEvent) {
|
|
if (this.state == 'ended') return;
|
|
|
|
debuglog("Ending call " + this.callId);
|
|
terminate(this, "local", reason, !suppressEvent);
|
|
var content = {
|
|
version: 0,
|
|
call_id: this.callId,
|
|
reason: reason
|
|
};
|
|
sendEvent(this, 'm.call.hangup', content);
|
|
};
|
|
|
|
/**
|
|
* Set whether the local video preview should be muted or not.
|
|
* @param {boolean} muted True to mute the local video.
|
|
*/
|
|
MatrixCall.prototype.setLocalVideoMuted = function (muted) {
|
|
if (!this.localAVStream) {
|
|
return;
|
|
}
|
|
setTracksEnabled(this.localAVStream.getVideoTracks(), !muted);
|
|
};
|
|
|
|
/**
|
|
* Check if local video is muted.
|
|
*
|
|
* If there are multiple video tracks, <i>all</i> of the tracks need to be muted
|
|
* for this to return true. This means if there are no video tracks, this will
|
|
* return true.
|
|
* @return {Boolean} True if the local preview video is muted, else false
|
|
* (including if the call is not set up yet).
|
|
*/
|
|
MatrixCall.prototype.isLocalVideoMuted = function () {
|
|
if (!this.localAVStream) {
|
|
return false;
|
|
}
|
|
return !isTracksEnabled(this.localAVStream.getVideoTracks());
|
|
};
|
|
|
|
/**
|
|
* Set whether the microphone should be muted or not.
|
|
* @param {boolean} muted True to mute the mic.
|
|
*/
|
|
MatrixCall.prototype.setMicrophoneMuted = function (muted) {
|
|
if (!this.localAVStream) {
|
|
return;
|
|
}
|
|
setTracksEnabled(this.localAVStream.getAudioTracks(), !muted);
|
|
};
|
|
|
|
/**
|
|
* Check if the microphone is muted.
|
|
*
|
|
* If there are multiple audio tracks, <i>all</i> of the tracks need to be muted
|
|
* for this to return true. This means if there are no audio tracks, this will
|
|
* return true.
|
|
* @return {Boolean} True if the mic is muted, else false (including if the call
|
|
* is not set up yet).
|
|
*/
|
|
MatrixCall.prototype.isMicrophoneMuted = function () {
|
|
if (!this.localAVStream) {
|
|
return false;
|
|
}
|
|
return !isTracksEnabled(this.localAVStream.getAudioTracks());
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @private
|
|
* @param {Object} stream
|
|
*/
|
|
MatrixCall.prototype._maybeGotUserMediaForInvite = function (stream) {
|
|
if (this.successor) {
|
|
this.successor._maybeGotUserMediaForAnswer(stream);
|
|
return;
|
|
}
|
|
if (this.state == 'ended') {
|
|
return;
|
|
}
|
|
debuglog("_maybeGotUserMediaForInvite -> " + this.type);
|
|
var self = this;
|
|
|
|
var error = stream;
|
|
var constraints = {
|
|
'mandatory': {
|
|
'OfferToReceiveAudio': true,
|
|
'OfferToReceiveVideo': self.type === 'video'
|
|
}
|
|
};
|
|
if (stream instanceof MediaStream) {
|
|
var videoEl = this.getLocalVideoElement();
|
|
|
|
if (videoEl && this.type == 'video') {
|
|
videoEl.autoplay = true;
|
|
if (this.screenSharingStream) {
|
|
debuglog("Setting screen sharing stream to the local video" + " element");
|
|
this.assignElement(videoEl, this.screenSharingStream, "localVideo");
|
|
} else {
|
|
this.assignElement(videoEl, stream, "localVideo");
|
|
}
|
|
videoEl.muted = true;
|
|
setTimeout(function () {
|
|
var vel = self.getLocalVideoElement();
|
|
if (vel.play) {
|
|
self.playElement(vel, "localVideo");
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
if (this.screenSharingStream) {
|
|
this.screenSharingStream.addTrack(stream.getAudioTracks()[0]);
|
|
stream = this.screenSharingStream;
|
|
}
|
|
|
|
this.localAVStream = stream;
|
|
// why do we enable audio (and only audio) tracks here? -- matthew
|
|
setTracksEnabled(stream.getAudioTracks(), true);
|
|
this.peerConn = _createPeerConnection(this);
|
|
this.peerConn.addStream(stream);
|
|
} else if (error.name === 'PermissionDeniedError') {
|
|
debuglog('User denied access to camera/microphone.' + ' Or possibly you are using an insecure domain. Receiving only.');
|
|
this.peerConn = _createPeerConnection(this);
|
|
} else {
|
|
debuglog('Failed to getUserMedia.');
|
|
this._getUserMediaFailed(error);
|
|
return;
|
|
}
|
|
|
|
this.peerConn.createOffer(hookCallback(self, self._gotLocalOffer), hookCallback(self, self._getLocalOfferFailed), constraints);
|
|
setState(self, 'create_offer');
|
|
};
|
|
|
|
MatrixCall.prototype._sendAnswer = function (stream) {
|
|
var _this = this;
|
|
|
|
sendEvent(this, 'm.call.answer', this._answerContent).then(function () {
|
|
setState(_this, 'connecting');
|
|
// If this isn't the first time we've tried to send the answer,
|
|
// we may have candidates queued up, so send them now.
|
|
_sendCandidateQueue(_this);
|
|
}).catch(function (error) {
|
|
// We've failed to answer: back to the ringing state
|
|
setState(_this, 'ringing');
|
|
_this.client.cancelPendingEvent(error.event);
|
|
|
|
var code = MatrixCall.ERR_SEND_ANSWER;
|
|
var message = "Failed to send answer";
|
|
if (error.name == 'UnknownDeviceError') {
|
|
code = MatrixCall.ERR_UNKNOWN_DEVICES;
|
|
message = "Unknown devices present in the room";
|
|
}
|
|
_this.emit("error", callError(code, message));
|
|
throw error;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @private
|
|
* @param {Object} stream
|
|
*/
|
|
MatrixCall.prototype._maybeGotUserMediaForAnswer = function (stream) {
|
|
var self = this;
|
|
if (self.state == 'ended') {
|
|
return;
|
|
}
|
|
|
|
var error = stream;
|
|
if (stream instanceof MediaStream) {
|
|
var localVidEl = self.getLocalVideoElement();
|
|
|
|
if (localVidEl && self.type == 'video') {
|
|
localVidEl.autoplay = true;
|
|
this.assignElement(localVidEl, stream, "localVideo");
|
|
localVidEl.muted = true;
|
|
setTimeout(function () {
|
|
var vel = self.getLocalVideoElement();
|
|
if (vel.play) {
|
|
self.playElement(vel, "localVideo");
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
self.localAVStream = stream;
|
|
setTracksEnabled(stream.getAudioTracks(), true);
|
|
self.peerConn.addStream(stream);
|
|
} else if (error.name === 'PermissionDeniedError') {
|
|
debuglog('User denied access to camera/microphone.' + ' Or possibly you are using an insecure domain. Receiving only.');
|
|
} else {
|
|
debuglog('Failed to getUserMedia.');
|
|
this._getUserMediaFailed(error);
|
|
return;
|
|
}
|
|
|
|
var constraints = {
|
|
'mandatory': {
|
|
'OfferToReceiveAudio': true,
|
|
'OfferToReceiveVideo': self.type === 'video'
|
|
}
|
|
};
|
|
self.peerConn.createAnswer(function (description) {
|
|
debuglog("Created answer: " + description);
|
|
self.peerConn.setLocalDescription(description, function () {
|
|
self._answerContent = {
|
|
version: 0,
|
|
call_id: self.callId,
|
|
answer: {
|
|
sdp: self.peerConn.localDescription.sdp,
|
|
type: self.peerConn.localDescription.type
|
|
}
|
|
};
|
|
self._sendAnswer();
|
|
}, function () {
|
|
debuglog("Error setting local description!");
|
|
}, constraints);
|
|
}, function (err) {
|
|
debuglog("Failed to create answer: " + err);
|
|
});
|
|
setState(self, 'create_answer');
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @private
|
|
* @param {Object} event
|
|
*/
|
|
MatrixCall.prototype._gotLocalIceCandidate = function (event) {
|
|
if (event.candidate) {
|
|
debuglog("Got local ICE " + event.candidate.sdpMid + " candidate: " + event.candidate.candidate);
|
|
|
|
if (this.state == 'ended') return;
|
|
|
|
// As with the offer, note we need to make a copy of this object, not
|
|
// pass the original: that broke in Chrome ~m43.
|
|
var c = {
|
|
candidate: event.candidate.candidate,
|
|
sdpMid: event.candidate.sdpMid,
|
|
sdpMLineIndex: event.candidate.sdpMLineIndex
|
|
};
|
|
sendCandidate(this, c);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Used by MatrixClient.
|
|
* @protected
|
|
* @param {Object} cand
|
|
*/
|
|
MatrixCall.prototype._gotRemoteIceCandidate = function (cand) {
|
|
if (this.state == 'ended') {
|
|
//debuglog("Ignoring remote ICE candidate because call has ended");
|
|
return;
|
|
}
|
|
debuglog("Got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
|
|
this.peerConn.addIceCandidate(new this.webRtc.RtcIceCandidate(cand), function () {}, function (e) {});
|
|
};
|
|
|
|
/**
|
|
* Used by MatrixClient.
|
|
* @protected
|
|
* @param {Object} msg
|
|
*/
|
|
MatrixCall.prototype._receivedAnswer = function (msg) {
|
|
if (this.state == 'ended') {
|
|
return;
|
|
}
|
|
|
|
var self = this;
|
|
this.peerConn.setRemoteDescription(new this.webRtc.RtcSessionDescription(msg.answer), hookCallback(self, self._onSetRemoteDescriptionSuccess), hookCallback(self, self._onSetRemoteDescriptionError));
|
|
setState(self, 'connecting');
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @private
|
|
* @param {Object} description
|
|
*/
|
|
MatrixCall.prototype._gotLocalOffer = function (description) {
|
|
var self = this;
|
|
debuglog("Created offer: " + description);
|
|
|
|
if (self.state == 'ended') {
|
|
debuglog("Ignoring newly created offer on call ID " + self.callId + " because the call has ended");
|
|
return;
|
|
}
|
|
|
|
self.peerConn.setLocalDescription(description, function () {
|
|
var content = {
|
|
version: 0,
|
|
call_id: self.callId,
|
|
// OpenWebRTC appears to add extra stuff (like the DTLS fingerprint)
|
|
// to the description when setting it on the peerconnection.
|
|
// According to the spec it should only add ICE
|
|
// candidates. Any ICE candidates that have already been generated
|
|
// at this point will probably be sent both in the offer and separately.
|
|
// Also, note that we have to make a new object here, copying the
|
|
// type and sdp properties.
|
|
// Passing the RTCSessionDescription object as-is doesn't work in
|
|
// Chrome (as of about m43).
|
|
offer: {
|
|
sdp: self.peerConn.localDescription.sdp,
|
|
type: self.peerConn.localDescription.type
|
|
},
|
|
lifetime: MatrixCall.CALL_TIMEOUT_MS
|
|
};
|
|
sendEvent(self, 'm.call.invite', content).then(function () {
|
|
setState(self, 'invite_sent');
|
|
setTimeout(function () {
|
|
if (self.state == 'invite_sent') {
|
|
self.hangup('invite_timeout');
|
|
}
|
|
}, MatrixCall.CALL_TIMEOUT_MS);
|
|
}).catch(function (error) {
|
|
var code = MatrixCall.ERR_SEND_INVITE;
|
|
var message = "Failed to send invite";
|
|
if (error.name == 'UnknownDeviceError') {
|
|
code = MatrixCall.ERR_UNKNOWN_DEVICES;
|
|
message = "Unknown devices present in the room";
|
|
}
|
|
|
|
self.client.cancelPendingEvent(error.event);
|
|
terminate(self, "local", code, false);
|
|
self.emit("error", callError(code, message));
|
|
throw error;
|
|
});
|
|
}, function () {
|
|
debuglog("Error setting local description!");
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @private
|
|
* @param {Object} error
|
|
*/
|
|
MatrixCall.prototype._getLocalOfferFailed = function (error) {
|
|
this.emit("error", callError(MatrixCall.ERR_LOCAL_OFFER_FAILED, "Failed to start audio for call!"));
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @private
|
|
* @param {Object} error
|
|
*/
|
|
MatrixCall.prototype._getUserMediaFailed = function (error) {
|
|
terminate(this, "local", 'user_media_failed', false);
|
|
this.emit("error", callError(MatrixCall.ERR_NO_USER_MEDIA, "Couldn't start capturing media! Is your microphone set up and " + "does this app have permission?"));
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @private
|
|
*/
|
|
MatrixCall.prototype._onIceConnectionStateChanged = function () {
|
|
if (this.state == 'ended') {
|
|
return; // because ICE can still complete as we're ending the call
|
|
}
|
|
debuglog("Ice connection state changed to: " + this.peerConn.iceConnectionState);
|
|
// ideally we'd consider the call to be connected when we get media but
|
|
// chrome doesn't implement any of the 'onstarted' events yet
|
|
if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') {
|
|
setState(this, 'connected');
|
|
this.didConnect = true;
|
|
} else if (this.peerConn.iceConnectionState == 'failed') {
|
|
this.hangup('ice_failed');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @private
|
|
*/
|
|
MatrixCall.prototype._onSignallingStateChanged = function () {
|
|
debuglog("call " + this.callId + ": Signalling state changed to: " + this.peerConn.signalingState);
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @private
|
|
*/
|
|
MatrixCall.prototype._onSetRemoteDescriptionSuccess = function () {
|
|
debuglog("Set remote description");
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @private
|
|
* @param {Object} e
|
|
*/
|
|
MatrixCall.prototype._onSetRemoteDescriptionError = function (e) {
|
|
debuglog("Failed to set remote description" + e);
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @private
|
|
* @param {Object} event
|
|
*/
|
|
MatrixCall.prototype._onAddStream = function (event) {
|
|
debuglog("Stream id " + event.stream.id + " added");
|
|
|
|
var s = event.stream;
|
|
|
|
if (s.getVideoTracks().length > 0) {
|
|
this.type = 'video';
|
|
this.remoteAVStream = s;
|
|
this.remoteAStream = s;
|
|
} else {
|
|
this.type = 'voice';
|
|
this.remoteAStream = s;
|
|
}
|
|
|
|
var self = this;
|
|
forAllTracksOnStream(s, function (t) {
|
|
debuglog("Track id " + t.id + " added");
|
|
// not currently implemented in chrome
|
|
t.onstarted = hookCallback(self, self._onRemoteStreamTrackStarted);
|
|
});
|
|
|
|
if (event.stream.oninactive !== undefined) {
|
|
event.stream.oninactive = hookCallback(self, self._onRemoteStreamEnded);
|
|
} else {
|
|
// onended is deprecated from Chrome 54
|
|
event.stream.onended = hookCallback(self, self._onRemoteStreamEnded);
|
|
}
|
|
|
|
// not currently implemented in chrome
|
|
event.stream.onstarted = hookCallback(self, self._onRemoteStreamStarted);
|
|
|
|
if (this.type === 'video') {
|
|
_tryPlayRemoteStream(this);
|
|
_tryPlayRemoteAudioStream(this);
|
|
} else {
|
|
_tryPlayRemoteAudioStream(this);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @private
|
|
* @param {Object} event
|
|
*/
|
|
MatrixCall.prototype._onRemoteStreamStarted = function (event) {
|
|
setState(this, 'connected');
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @private
|
|
* @param {Object} event
|
|
*/
|
|
MatrixCall.prototype._onRemoteStreamEnded = function (event) {
|
|
debuglog("Remote stream ended");
|
|
this.hangupParty = 'remote';
|
|
setState(this, 'ended');
|
|
stopAllMedia(this);
|
|
if (this.peerConn.signalingState != 'closed') {
|
|
this.peerConn.close();
|
|
}
|
|
this.emit("hangup", this);
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @private
|
|
* @param {Object} event
|
|
*/
|
|
MatrixCall.prototype._onRemoteStreamTrackStarted = function (event) {
|
|
setState(this, 'connected');
|
|
};
|
|
|
|
/**
|
|
* Used by MatrixClient.
|
|
* @protected
|
|
* @param {Object} msg
|
|
*/
|
|
MatrixCall.prototype._onHangupReceived = function (msg) {
|
|
debuglog("Hangup received");
|
|
terminate(this, "remote", msg.reason, true);
|
|
};
|
|
|
|
/**
|
|
* Used by MatrixClient.
|
|
* @protected
|
|
* @param {Object} msg
|
|
*/
|
|
MatrixCall.prototype._onAnsweredElsewhere = function (msg) {
|
|
debuglog("Answered elsewhere");
|
|
terminate(this, "remote", "answered_elsewhere", true);
|
|
};
|
|
|
|
var setTracksEnabled = function setTracksEnabled(tracks, enabled) {
|
|
for (var i = 0; i < tracks.length; i++) {
|
|
tracks[i].enabled = enabled;
|
|
}
|
|
};
|
|
|
|
var isTracksEnabled = function isTracksEnabled(tracks) {
|
|
for (var i = 0; i < tracks.length; i++) {
|
|
if (tracks[i].enabled) {
|
|
return true; // at least one track is enabled
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
var setState = function setState(self, state) {
|
|
var oldState = self.state;
|
|
self.state = state;
|
|
self.emit("state", state, oldState);
|
|
};
|
|
|
|
/**
|
|
* Internal
|
|
* @param {MatrixCall} self
|
|
* @param {string} eventType
|
|
* @param {Object} content
|
|
* @return {Promise}
|
|
*/
|
|
var sendEvent = function sendEvent(self, eventType, content) {
|
|
return self.client.sendEvent(self.roomId, eventType, content);
|
|
};
|
|
|
|
var sendCandidate = function sendCandidate(self, content) {
|
|
// Sends candidates with are sent in a special way because we try to amalgamate
|
|
// them into one message
|
|
self.candidateSendQueue.push(content);
|
|
|
|
// Don't send the ICE candidates yet if the call is in the ringing state: this
|
|
// means we tried to pick (ie. started generating candidates) and then failed to
|
|
// send the answer and went back to the ringing state. Queue up the candidates
|
|
// to send if we sucessfully send the answer.
|
|
if (self.state == 'ringing') return;
|
|
|
|
if (self.candidateSendTries === 0) {
|
|
setTimeout(function () {
|
|
_sendCandidateQueue(self);
|
|
}, 100);
|
|
}
|
|
};
|
|
|
|
var terminate = function terminate(self, hangupParty, hangupReason, shouldEmit) {
|
|
if (self.getRemoteVideoElement()) {
|
|
if (self.getRemoteVideoElement().pause) {
|
|
self.pauseElement(self.getRemoteVideoElement(), "remoteVideo");
|
|
}
|
|
self.assignElement(self.getRemoteVideoElement(), null, "remoteVideo");
|
|
}
|
|
if (self.getRemoteAudioElement()) {
|
|
if (self.getRemoteAudioElement().pause) {
|
|
self.pauseElement(self.getRemoteAudioElement(), "remoteAudio");
|
|
}
|
|
self.assignElement(self.getRemoteAudioElement(), null, "remoteAudio");
|
|
}
|
|
if (self.getLocalVideoElement()) {
|
|
if (self.getLocalVideoElement().pause) {
|
|
self.pauseElement(self.getLocalVideoElement(), "localVideo");
|
|
}
|
|
self.assignElement(self.getLocalVideoElement(), null, "localVideo");
|
|
}
|
|
self.hangupParty = hangupParty;
|
|
self.hangupReason = hangupReason;
|
|
setState(self, 'ended');
|
|
stopAllMedia(self);
|
|
if (self.peerConn && self.peerConn.signalingState !== 'closed') {
|
|
self.peerConn.close();
|
|
}
|
|
if (shouldEmit) {
|
|
self.emit("hangup", self);
|
|
}
|
|
};
|
|
|
|
var stopAllMedia = function stopAllMedia(self) {
|
|
debuglog("stopAllMedia (stream=%s)", self.localAVStream);
|
|
if (self.localAVStream) {
|
|
forAllTracksOnStream(self.localAVStream, function (t) {
|
|
if (t.stop) {
|
|
t.stop();
|
|
}
|
|
});
|
|
// also call stop on the main stream so firefox will stop sharing
|
|
// the mic
|
|
if (self.localAVStream.stop) {
|
|
self.localAVStream.stop();
|
|
}
|
|
}
|
|
if (self.screenSharingStream) {
|
|
forAllTracksOnStream(self.screenSharingStream, function (t) {
|
|
if (t.stop) {
|
|
t.stop();
|
|
}
|
|
});
|
|
if (self.screenSharingStream.stop) {
|
|
self.screenSharingStream.stop();
|
|
}
|
|
}
|
|
if (self.remoteAVStream) {
|
|
forAllTracksOnStream(self.remoteAVStream, function (t) {
|
|
if (t.stop) {
|
|
t.stop();
|
|
}
|
|
});
|
|
}
|
|
if (self.remoteAStream) {
|
|
forAllTracksOnStream(self.remoteAStream, function (t) {
|
|
if (t.stop) {
|
|
t.stop();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
var _tryPlayRemoteStream = function _tryPlayRemoteStream(self) {
|
|
if (self.getRemoteVideoElement() && self.remoteAVStream) {
|
|
var player = self.getRemoteVideoElement();
|
|
player.autoplay = true;
|
|
self.assignElement(player, self.remoteAVStream, "remoteVideo");
|
|
setTimeout(function () {
|
|
var vel = self.getRemoteVideoElement();
|
|
if (vel.play) {
|
|
self.playElement(vel, "remoteVideo");
|
|
}
|
|
// OpenWebRTC does not support oniceconnectionstatechange yet
|
|
if (self.webRtc.isOpenWebRTC()) {
|
|
setState(self, 'connected');
|
|
}
|
|
}, 0);
|
|
}
|
|
};
|
|
|
|
var _tryPlayRemoteAudioStream = function _tryPlayRemoteAudioStream(self) {
|
|
if (self.getRemoteAudioElement() && self.remoteAStream) {
|
|
var player = self.getRemoteAudioElement();
|
|
player.autoplay = true;
|
|
self.assignElement(player, self.remoteAStream, "remoteAudio");
|
|
setTimeout(function () {
|
|
var ael = self.getRemoteAudioElement();
|
|
if (ael.play) {
|
|
self.playElement(ael, "remoteAudio");
|
|
}
|
|
// OpenWebRTC does not support oniceconnectionstatechange yet
|
|
if (self.webRtc.isOpenWebRTC()) {
|
|
setState(self, 'connected');
|
|
}
|
|
}, 0);
|
|
}
|
|
};
|
|
|
|
var checkForErrorListener = function checkForErrorListener(self) {
|
|
if (self.listeners("error").length === 0) {
|
|
throw new Error("You MUST attach an error listener using call.on('error', function() {})");
|
|
}
|
|
};
|
|
|
|
var callError = function callError(code, msg) {
|
|
var e = new Error(msg);
|
|
e.code = code;
|
|
return e;
|
|
};
|
|
|
|
var debuglog = function debuglog() {
|
|
if (DEBUG) {
|
|
var _console;
|
|
|
|
(_console = console).log.apply(_console, arguments);
|
|
}
|
|
};
|
|
|
|
var _sendCandidateQueue = function _sendCandidateQueue(self) {
|
|
if (self.candidateSendQueue.length === 0) {
|
|
return;
|
|
}
|
|
|
|
var cands = self.candidateSendQueue;
|
|
self.candidateSendQueue = [];
|
|
++self.candidateSendTries;
|
|
var content = {
|
|
version: 0,
|
|
call_id: self.callId,
|
|
candidates: cands
|
|
};
|
|
debuglog("Attempting to send " + cands.length + " candidates");
|
|
sendEvent(self, 'm.call.candidates', content).then(function () {
|
|
self.candidateSendTries = 0;
|
|
_sendCandidateQueue(self);
|
|
}, function (error) {
|
|
for (var i = 0; i < cands.length; i++) {
|
|
self.candidateSendQueue.push(cands[i]);
|
|
}
|
|
|
|
if (self.candidateSendTries > 5) {
|
|
debuglog("Failed to send candidates on attempt %s. Giving up for now.", self.candidateSendTries);
|
|
self.candidateSendTries = 0;
|
|
return;
|
|
}
|
|
|
|
var delayMs = 500 * Math.pow(2, self.candidateSendTries);
|
|
++self.candidateSendTries;
|
|
debuglog("Failed to send candidates. Retrying in " + delayMs + "ms");
|
|
setTimeout(function () {
|
|
_sendCandidateQueue(self);
|
|
}, delayMs);
|
|
});
|
|
};
|
|
|
|
var _placeCallWithConstraints = function _placeCallWithConstraints(self, constraints) {
|
|
self.client.callList[self.callId] = self;
|
|
self.webRtc.getUserMedia(constraints, hookCallback(self, self._maybeGotUserMediaForInvite), hookCallback(self, self._maybeGotUserMediaForInvite));
|
|
setState(self, 'wait_local_media');
|
|
self.direction = 'outbound';
|
|
self.config = constraints;
|
|
};
|
|
|
|
var _createPeerConnection = function _createPeerConnection(self) {
|
|
var servers = self.turnServers;
|
|
if (self.webRtc.vendor === "mozilla") {
|
|
// modify turnServers struct to match what mozilla expects.
|
|
servers = [];
|
|
for (var i = 0; i < self.turnServers.length; i++) {
|
|
for (var j = 0; j < self.turnServers[i].urls.length; j++) {
|
|
servers.push({
|
|
url: self.turnServers[i].urls[j],
|
|
username: self.turnServers[i].username,
|
|
credential: self.turnServers[i].credential
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
var pc = new self.webRtc.RtcPeerConnection({
|
|
iceTransportPolicy: self.forceTURN ? 'relay' : undefined,
|
|
iceServers: servers
|
|
});
|
|
pc.oniceconnectionstatechange = hookCallback(self, self._onIceConnectionStateChanged);
|
|
pc.onsignalingstatechange = hookCallback(self, self._onSignallingStateChanged);
|
|
pc.onicecandidate = hookCallback(self, self._gotLocalIceCandidate);
|
|
pc.onaddstream = hookCallback(self, self._onAddStream);
|
|
return pc;
|
|
};
|
|
|
|
var _getScreenSharingConstraints = function _getScreenSharingConstraints(call) {
|
|
var screen = global.screen;
|
|
if (!screen) {
|
|
call.emit("error", callError(MatrixCall.ERR_NO_USER_MEDIA, "Couldn't determine screen sharing constaints."));
|
|
return;
|
|
}
|
|
|
|
return {
|
|
video: {
|
|
mediaSource: 'screen',
|
|
mandatory: {
|
|
chromeMediaSource: "screen",
|
|
chromeMediaSourceId: "" + Date.now(),
|
|
maxWidth: screen.width,
|
|
maxHeight: screen.height,
|
|
minFrameRate: 1,
|
|
maxFrameRate: 10
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
var _getUserMediaVideoContraints = function _getUserMediaVideoContraints(callType) {
|
|
var isWebkit = !!global.window.navigator.webkitGetUserMedia;
|
|
|
|
switch (callType) {
|
|
case 'voice':
|
|
return {
|
|
audio: {
|
|
deviceId: audioInput ? { exact: audioInput } : undefined
|
|
}, video: false
|
|
};
|
|
case 'video':
|
|
return {
|
|
audio: {
|
|
deviceId: audioInput ? { exact: audioInput } : undefined
|
|
}, video: {
|
|
deviceId: videoInput ? { exact: videoInput } : undefined,
|
|
/* We want 640x360. Chrome will give it only if we ask exactly,
|
|
FF refuses entirely if we ask exactly, so have to ask for ideal
|
|
instead */
|
|
width: isWebkit ? { exact: 640 } : { ideal: 640 },
|
|
height: isWebkit ? { exact: 360 } : { ideal: 360 }
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
var hookCallback = function hookCallback(call, fn) {
|
|
return function () {
|
|
return fn.apply(call, arguments);
|
|
};
|
|
};
|
|
|
|
var forAllVideoTracksOnStream = function forAllVideoTracksOnStream(s, f) {
|
|
var tracks = s.getVideoTracks();
|
|
for (var i = 0; i < tracks.length; i++) {
|
|
f(tracks[i]);
|
|
}
|
|
};
|
|
|
|
var forAllAudioTracksOnStream = function forAllAudioTracksOnStream(s, f) {
|
|
var tracks = s.getAudioTracks();
|
|
for (var i = 0; i < tracks.length; i++) {
|
|
f(tracks[i]);
|
|
}
|
|
};
|
|
|
|
var forAllTracksOnStream = function forAllTracksOnStream(s, f) {
|
|
forAllVideoTracksOnStream(s, f);
|
|
forAllAudioTracksOnStream(s, f);
|
|
};
|
|
|
|
/** The MatrixCall class. */
|
|
module.exports.MatrixCall = MatrixCall;
|
|
|
|
var audioInput = void 0;
|
|
var videoInput = void 0;
|
|
/**
|
|
* Set an audio input device to use for MatrixCalls
|
|
* @function
|
|
* @param {string=} deviceId the identifier for the device
|
|
* undefined treated as unset
|
|
*/
|
|
module.exports.setAudioInput = function (deviceId) {
|
|
audioInput = deviceId;
|
|
};
|
|
/**
|
|
* Set a video input device to use for MatrixCalls
|
|
* @function
|
|
* @param {string=} deviceId the identifier for the device
|
|
* undefined treated as unset
|
|
*/
|
|
module.exports.setVideoInput = function (deviceId) {
|
|
videoInput = deviceId;
|
|
};
|
|
|
|
/**
|
|
* Create a new Matrix call for the browser.
|
|
* @param {MatrixClient} client The client instance to use.
|
|
* @param {string} roomId The room the call is in.
|
|
* @param {Object?} options DEPRECATED optional options map.
|
|
* @param {boolean} options.forceTURN DEPRECATED whether relay through TURN should be forced. This option is deprecated - use opts.forceTURN when creating the matrix client since it's only possible to set this option on outbound calls.
|
|
* @return {MatrixCall} the call or null if the browser doesn't support calling.
|
|
*/
|
|
module.exports.createNewMatrixCall = function (client, roomId, options) {
|
|
var w = global.window;
|
|
var doc = global.document;
|
|
if (!w || !doc) {
|
|
return null;
|
|
}
|
|
var webRtc = {};
|
|
webRtc.isOpenWebRTC = function () {
|
|
var scripts = doc.getElementById("script");
|
|
if (!scripts || !scripts.length) {
|
|
return false;
|
|
}
|
|
for (var i = 0; i < scripts.length; i++) {
|
|
if (scripts[i].src.indexOf("owr.js") > -1) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
var getUserMedia = w.navigator.getUserMedia || w.navigator.webkitGetUserMedia || w.navigator.mozGetUserMedia;
|
|
if (getUserMedia) {
|
|
webRtc.getUserMedia = function () {
|
|
return getUserMedia.apply(w.navigator, arguments);
|
|
};
|
|
}
|
|
webRtc.RtcPeerConnection = w.RTCPeerConnection || w.webkitRTCPeerConnection || w.mozRTCPeerConnection;
|
|
webRtc.RtcSessionDescription = w.RTCSessionDescription || w.webkitRTCSessionDescription || w.mozRTCSessionDescription;
|
|
webRtc.RtcIceCandidate = w.RTCIceCandidate || w.webkitRTCIceCandidate || w.mozRTCIceCandidate;
|
|
webRtc.vendor = null;
|
|
if (w.mozRTCPeerConnection) {
|
|
webRtc.vendor = "mozilla";
|
|
} else if (w.webkitRTCPeerConnection) {
|
|
webRtc.vendor = "webkit";
|
|
} else if (w.RTCPeerConnection) {
|
|
webRtc.vendor = "generic";
|
|
}
|
|
if (!webRtc.RtcIceCandidate || !webRtc.RtcSessionDescription || !webRtc.RtcPeerConnection || !webRtc.getUserMedia) {
|
|
return null; // WebRTC is not supported.
|
|
}
|
|
|
|
var optionsForceTURN = options ? options.forceTURN : false;
|
|
|
|
var opts = {
|
|
webRtc: webRtc,
|
|
client: client,
|
|
URL: w.URL,
|
|
roomId: roomId,
|
|
turnServers: client.getTurnServers(),
|
|
// call level options
|
|
forceTURN: client._forceTURN || optionsForceTURN
|
|
};
|
|
return new MatrixCall(opts);
|
|
};
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
|
|
},{"../utils":50,"babel-runtime/core-js/object/create":57,"events":186}],52:[function(require,module,exports){
|
|
/* Copyright 2015 Mark Haines
|
|
*
|
|
* 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 escaped = /[\\\"\x00-\x1F]/g;
|
|
var escapes = {};
|
|
for (var i = 0; i < 0x20; ++i) {
|
|
escapes[String.fromCharCode(i)] = (
|
|
'\\U' + ('0000' + i.toString(16)).slice(-4).toUpperCase()
|
|
);
|
|
}
|
|
escapes['\b'] = '\\b';
|
|
escapes['\t'] = '\\t';
|
|
escapes['\n'] = '\\n';
|
|
escapes['\f'] = '\\f';
|
|
escapes['\r'] = '\\r';
|
|
escapes['\"'] = '\\\"';
|
|
escapes['\\'] = '\\\\';
|
|
|
|
function escapeString(value) {
|
|
escaped.lastIndex = 0;
|
|
return value.replace(escaped, function(c) { return escapes[c]; });
|
|
}
|
|
|
|
function stringify(value) {
|
|
switch (typeof value) {
|
|
case 'string':
|
|
return '"' + escapeString(value) + '"';
|
|
case 'number':
|
|
return isFinite(value) ? value : 'null';
|
|
case 'boolean':
|
|
return value;
|
|
case 'object':
|
|
if (value === null) {
|
|
return 'null';
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return stringifyArray(value);
|
|
}
|
|
return stringifyObject(value);
|
|
default:
|
|
throw new Error('Cannot stringify: ' + typeof value);
|
|
}
|
|
}
|
|
|
|
function stringifyArray(array) {
|
|
var sep = '[';
|
|
var result = '';
|
|
for (var i = 0; i < array.length; ++i) {
|
|
result += sep;
|
|
sep = ',';
|
|
result += stringify(array[i]);
|
|
}
|
|
if (sep != ',') {
|
|
return '[]';
|
|
} else {
|
|
return result + ']';
|
|
}
|
|
}
|
|
|
|
function stringifyObject(object) {
|
|
var sep = '{';
|
|
var result = '';
|
|
var keys = Object.keys(object);
|
|
keys.sort();
|
|
for (var i = 0; i < keys.length; ++i) {
|
|
var key = keys[i];
|
|
result += sep + '"' + escapeString(key) + '":';
|
|
sep = ',';
|
|
result += stringify(object[key]);
|
|
}
|
|
if (sep != ',') {
|
|
return '{}';
|
|
} else {
|
|
return result + '}';
|
|
}
|
|
}
|
|
|
|
/** */
|
|
module.exports = {stringify: stringify};
|
|
|
|
},{}],53:[function(require,module,exports){
|
|
module.exports = { "default": require("core-js/library/fn/get-iterator"), __esModule: true };
|
|
},{"core-js/library/fn/get-iterator":77}],54:[function(require,module,exports){
|
|
module.exports = { "default": require("core-js/library/fn/is-iterable"), __esModule: true };
|
|
},{"core-js/library/fn/is-iterable":78}],55:[function(require,module,exports){
|
|
module.exports = { "default": require("core-js/library/fn/json/stringify"), __esModule: true };
|
|
},{"core-js/library/fn/json/stringify":79}],56:[function(require,module,exports){
|
|
module.exports = { "default": require("core-js/library/fn/object/assign"), __esModule: true };
|
|
},{"core-js/library/fn/object/assign":80}],57:[function(require,module,exports){
|
|
module.exports = { "default": require("core-js/library/fn/object/create"), __esModule: true };
|
|
},{"core-js/library/fn/object/create":81}],58:[function(require,module,exports){
|
|
module.exports = { "default": require("core-js/library/fn/object/define-property"), __esModule: true };
|
|
},{"core-js/library/fn/object/define-property":82}],59:[function(require,module,exports){
|
|
module.exports = { "default": require("core-js/library/fn/object/freeze"), __esModule: true };
|
|
},{"core-js/library/fn/object/freeze":83}],60:[function(require,module,exports){
|
|
module.exports = { "default": require("core-js/library/fn/object/get-prototype-of"), __esModule: true };
|
|
},{"core-js/library/fn/object/get-prototype-of":84}],61:[function(require,module,exports){
|
|
module.exports = { "default": require("core-js/library/fn/object/keys"), __esModule: true };
|
|
},{"core-js/library/fn/object/keys":85}],62:[function(require,module,exports){
|
|
module.exports = { "default": require("core-js/library/fn/object/set-prototype-of"), __esModule: true };
|
|
},{"core-js/library/fn/object/set-prototype-of":86}],63:[function(require,module,exports){
|
|
module.exports = { "default": require("core-js/library/fn/set"), __esModule: true };
|
|
},{"core-js/library/fn/set":87}],64:[function(require,module,exports){
|
|
module.exports = { "default": require("core-js/library/fn/symbol"), __esModule: true };
|
|
},{"core-js/library/fn/symbol":88}],65:[function(require,module,exports){
|
|
module.exports = { "default": require("core-js/library/fn/symbol/iterator"), __esModule: true };
|
|
},{"core-js/library/fn/symbol/iterator":89}],66:[function(require,module,exports){
|
|
"use strict";
|
|
|
|
exports.__esModule = true;
|
|
|
|
exports.default = function (instance, Constructor) {
|
|
if (!(instance instanceof Constructor)) {
|
|
throw new TypeError("Cannot call a class as a function");
|
|
}
|
|
};
|
|
},{}],67:[function(require,module,exports){
|
|
"use strict";
|
|
|
|
exports.__esModule = true;
|
|
|
|
var _defineProperty = require("../core-js/object/define-property");
|
|
|
|
var _defineProperty2 = _interopRequireDefault(_defineProperty);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
exports.default = function () {
|
|
function defineProperties(target, props) {
|
|
for (var i = 0; i < props.length; i++) {
|
|
var descriptor = props[i];
|
|
descriptor.enumerable = descriptor.enumerable || false;
|
|
descriptor.configurable = true;
|
|
if ("value" in descriptor) descriptor.writable = true;
|
|
(0, _defineProperty2.default)(target, descriptor.key, descriptor);
|
|
}
|
|
}
|
|
|
|
return function (Constructor, protoProps, staticProps) {
|
|
if (protoProps) defineProperties(Constructor.prototype, protoProps);
|
|
if (staticProps) defineProperties(Constructor, staticProps);
|
|
return Constructor;
|
|
};
|
|
}();
|
|
},{"../core-js/object/define-property":58}],68:[function(require,module,exports){
|
|
"use strict";
|
|
|
|
exports.__esModule = true;
|
|
|
|
var _defineProperty = require("../core-js/object/define-property");
|
|
|
|
var _defineProperty2 = _interopRequireDefault(_defineProperty);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
exports.default = function (obj, key, value) {
|
|
if (key in obj) {
|
|
(0, _defineProperty2.default)(obj, key, {
|
|
value: value,
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true
|
|
});
|
|
} else {
|
|
obj[key] = value;
|
|
}
|
|
|
|
return obj;
|
|
};
|
|
},{"../core-js/object/define-property":58}],69:[function(require,module,exports){
|
|
"use strict";
|
|
|
|
exports.__esModule = true;
|
|
|
|
var _setPrototypeOf = require("../core-js/object/set-prototype-of");
|
|
|
|
var _setPrototypeOf2 = _interopRequireDefault(_setPrototypeOf);
|
|
|
|
var _create = require("../core-js/object/create");
|
|
|
|
var _create2 = _interopRequireDefault(_create);
|
|
|
|
var _typeof2 = require("../helpers/typeof");
|
|
|
|
var _typeof3 = _interopRequireDefault(_typeof2);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
exports.default = function (subClass, superClass) {
|
|
if (typeof superClass !== "function" && superClass !== null) {
|
|
throw new TypeError("Super expression must either be null or a function, not " + (typeof superClass === "undefined" ? "undefined" : (0, _typeof3.default)(superClass)));
|
|
}
|
|
|
|
subClass.prototype = (0, _create2.default)(superClass && superClass.prototype, {
|
|
constructor: {
|
|
value: subClass,
|
|
enumerable: false,
|
|
writable: true,
|
|
configurable: true
|
|
}
|
|
});
|
|
if (superClass) _setPrototypeOf2.default ? (0, _setPrototypeOf2.default)(subClass, superClass) : subClass.__proto__ = superClass;
|
|
};
|
|
},{"../core-js/object/create":57,"../core-js/object/set-prototype-of":62,"../helpers/typeof":72}],70:[function(require,module,exports){
|
|
"use strict";
|
|
|
|
exports.__esModule = true;
|
|
|
|
var _typeof2 = require("../helpers/typeof");
|
|
|
|
var _typeof3 = _interopRequireDefault(_typeof2);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
exports.default = function (self, call) {
|
|
if (!self) {
|
|
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
|
|
}
|
|
|
|
return call && ((typeof call === "undefined" ? "undefined" : (0, _typeof3.default)(call)) === "object" || typeof call === "function") ? call : self;
|
|
};
|
|
},{"../helpers/typeof":72}],71:[function(require,module,exports){
|
|
"use strict";
|
|
|
|
exports.__esModule = true;
|
|
|
|
var _isIterable2 = require("../core-js/is-iterable");
|
|
|
|
var _isIterable3 = _interopRequireDefault(_isIterable2);
|
|
|
|
var _getIterator2 = require("../core-js/get-iterator");
|
|
|
|
var _getIterator3 = _interopRequireDefault(_getIterator2);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
exports.default = function () {
|
|
function sliceIterator(arr, i) {
|
|
var _arr = [];
|
|
var _n = true;
|
|
var _d = false;
|
|
var _e = undefined;
|
|
|
|
try {
|
|
for (var _i = (0, _getIterator3.default)(arr), _s; !(_n = (_s = _i.next()).done); _n = true) {
|
|
_arr.push(_s.value);
|
|
|
|
if (i && _arr.length === i) break;
|
|
}
|
|
} catch (err) {
|
|
_d = true;
|
|
_e = err;
|
|
} finally {
|
|
try {
|
|
if (!_n && _i["return"]) _i["return"]();
|
|
} finally {
|
|
if (_d) throw _e;
|
|
}
|
|
}
|
|
|
|
return _arr;
|
|
}
|
|
|
|
return function (arr, i) {
|
|
if (Array.isArray(arr)) {
|
|
return arr;
|
|
} else if ((0, _isIterable3.default)(Object(arr))) {
|
|
return sliceIterator(arr, i);
|
|
} else {
|
|
throw new TypeError("Invalid attempt to destructure non-iterable instance");
|
|
}
|
|
};
|
|
}();
|
|
},{"../core-js/get-iterator":53,"../core-js/is-iterable":54}],72:[function(require,module,exports){
|
|
"use strict";
|
|
|
|
exports.__esModule = true;
|
|
|
|
var _iterator = require("../core-js/symbol/iterator");
|
|
|
|
var _iterator2 = _interopRequireDefault(_iterator);
|
|
|
|
var _symbol = require("../core-js/symbol");
|
|
|
|
var _symbol2 = _interopRequireDefault(_symbol);
|
|
|
|
var _typeof = typeof _symbol2.default === "function" && typeof _iterator2.default === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof _symbol2.default === "function" && obj.constructor === _symbol2.default && obj !== _symbol2.default.prototype ? "symbol" : typeof obj; };
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
exports.default = typeof _symbol2.default === "function" && _typeof(_iterator2.default) === "symbol" ? function (obj) {
|
|
return typeof obj === "undefined" ? "undefined" : _typeof(obj);
|
|
} : function (obj) {
|
|
return obj && typeof _symbol2.default === "function" && obj.constructor === _symbol2.default && obj !== _symbol2.default.prototype ? "symbol" : typeof obj === "undefined" ? "undefined" : _typeof(obj);
|
|
};
|
|
},{"../core-js/symbol":64,"../core-js/symbol/iterator":65}],73:[function(require,module,exports){
|
|
module.exports = require("regenerator-runtime");
|
|
|
|
},{"regenerator-runtime":192}],74:[function(require,module,exports){
|
|
(function (process,global,setImmediate){
|
|
/* @preserve
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2013-2017 Petka Antonov
|
|
*
|
|
* 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.
|
|
*
|
|
*/
|
|
/**
|
|
* bluebird build version 3.5.1
|
|
* Features enabled: core, race, call_get, generators, map, nodeify, promisify, props, reduce, settle, some, using, timers, filter, any, each
|
|
*/
|
|
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.Promise=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof _dereq_=="function"&&_dereq_;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof _dereq_=="function"&&_dereq_;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise) {
|
|
var SomePromiseArray = Promise._SomePromiseArray;
|
|
function any(promises) {
|
|
var ret = new SomePromiseArray(promises);
|
|
var promise = ret.promise();
|
|
ret.setHowMany(1);
|
|
ret.setUnwrap();
|
|
ret.init();
|
|
return promise;
|
|
}
|
|
|
|
Promise.any = function (promises) {
|
|
return any(promises);
|
|
};
|
|
|
|
Promise.prototype.any = function () {
|
|
return any(this);
|
|
};
|
|
|
|
};
|
|
|
|
},{}],2:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
var firstLineError;
|
|
try {throw new Error(); } catch (e) {firstLineError = e;}
|
|
var schedule = _dereq_("./schedule");
|
|
var Queue = _dereq_("./queue");
|
|
var util = _dereq_("./util");
|
|
|
|
function Async() {
|
|
this._customScheduler = false;
|
|
this._isTickUsed = false;
|
|
this._lateQueue = new Queue(16);
|
|
this._normalQueue = new Queue(16);
|
|
this._haveDrainedQueues = false;
|
|
this._trampolineEnabled = true;
|
|
var self = this;
|
|
this.drainQueues = function () {
|
|
self._drainQueues();
|
|
};
|
|
this._schedule = schedule;
|
|
}
|
|
|
|
Async.prototype.setScheduler = function(fn) {
|
|
var prev = this._schedule;
|
|
this._schedule = fn;
|
|
this._customScheduler = true;
|
|
return prev;
|
|
};
|
|
|
|
Async.prototype.hasCustomScheduler = function() {
|
|
return this._customScheduler;
|
|
};
|
|
|
|
Async.prototype.enableTrampoline = function() {
|
|
this._trampolineEnabled = true;
|
|
};
|
|
|
|
Async.prototype.disableTrampolineIfNecessary = function() {
|
|
if (util.hasDevTools) {
|
|
this._trampolineEnabled = false;
|
|
}
|
|
};
|
|
|
|
Async.prototype.haveItemsQueued = function () {
|
|
return this._isTickUsed || this._haveDrainedQueues;
|
|
};
|
|
|
|
|
|
Async.prototype.fatalError = function(e, isNode) {
|
|
if (isNode) {
|
|
process.stderr.write("Fatal " + (e instanceof Error ? e.stack : e) +
|
|
"\n");
|
|
process.exit(2);
|
|
} else {
|
|
this.throwLater(e);
|
|
}
|
|
};
|
|
|
|
Async.prototype.throwLater = function(fn, arg) {
|
|
if (arguments.length === 1) {
|
|
arg = fn;
|
|
fn = function () { throw arg; };
|
|
}
|
|
if (typeof setTimeout !== "undefined") {
|
|
setTimeout(function() {
|
|
fn(arg);
|
|
}, 0);
|
|
} else try {
|
|
this._schedule(function() {
|
|
fn(arg);
|
|
});
|
|
} catch (e) {
|
|
throw new Error("No async scheduler available\u000a\u000a See http://goo.gl/MqrFmX\u000a");
|
|
}
|
|
};
|
|
|
|
function AsyncInvokeLater(fn, receiver, arg) {
|
|
this._lateQueue.push(fn, receiver, arg);
|
|
this._queueTick();
|
|
}
|
|
|
|
function AsyncInvoke(fn, receiver, arg) {
|
|
this._normalQueue.push(fn, receiver, arg);
|
|
this._queueTick();
|
|
}
|
|
|
|
function AsyncSettlePromises(promise) {
|
|
this._normalQueue._pushOne(promise);
|
|
this._queueTick();
|
|
}
|
|
|
|
if (!util.hasDevTools) {
|
|
Async.prototype.invokeLater = AsyncInvokeLater;
|
|
Async.prototype.invoke = AsyncInvoke;
|
|
Async.prototype.settlePromises = AsyncSettlePromises;
|
|
} else {
|
|
Async.prototype.invokeLater = function (fn, receiver, arg) {
|
|
if (this._trampolineEnabled) {
|
|
AsyncInvokeLater.call(this, fn, receiver, arg);
|
|
} else {
|
|
this._schedule(function() {
|
|
setTimeout(function() {
|
|
fn.call(receiver, arg);
|
|
}, 100);
|
|
});
|
|
}
|
|
};
|
|
|
|
Async.prototype.invoke = function (fn, receiver, arg) {
|
|
if (this._trampolineEnabled) {
|
|
AsyncInvoke.call(this, fn, receiver, arg);
|
|
} else {
|
|
this._schedule(function() {
|
|
fn.call(receiver, arg);
|
|
});
|
|
}
|
|
};
|
|
|
|
Async.prototype.settlePromises = function(promise) {
|
|
if (this._trampolineEnabled) {
|
|
AsyncSettlePromises.call(this, promise);
|
|
} else {
|
|
this._schedule(function() {
|
|
promise._settlePromises();
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
Async.prototype._drainQueue = function(queue) {
|
|
while (queue.length() > 0) {
|
|
var fn = queue.shift();
|
|
if (typeof fn !== "function") {
|
|
fn._settlePromises();
|
|
continue;
|
|
}
|
|
var receiver = queue.shift();
|
|
var arg = queue.shift();
|
|
fn.call(receiver, arg);
|
|
}
|
|
};
|
|
|
|
Async.prototype._drainQueues = function () {
|
|
this._drainQueue(this._normalQueue);
|
|
this._reset();
|
|
this._haveDrainedQueues = true;
|
|
this._drainQueue(this._lateQueue);
|
|
};
|
|
|
|
Async.prototype._queueTick = function () {
|
|
if (!this._isTickUsed) {
|
|
this._isTickUsed = true;
|
|
this._schedule(this.drainQueues);
|
|
}
|
|
};
|
|
|
|
Async.prototype._reset = function () {
|
|
this._isTickUsed = false;
|
|
};
|
|
|
|
module.exports = Async;
|
|
module.exports.firstLineError = firstLineError;
|
|
|
|
},{"./queue":26,"./schedule":29,"./util":36}],3:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise, INTERNAL, tryConvertToPromise, debug) {
|
|
var calledBind = false;
|
|
var rejectThis = function(_, e) {
|
|
this._reject(e);
|
|
};
|
|
|
|
var targetRejected = function(e, context) {
|
|
context.promiseRejectionQueued = true;
|
|
context.bindingPromise._then(rejectThis, rejectThis, null, this, e);
|
|
};
|
|
|
|
var bindingResolved = function(thisArg, context) {
|
|
if (((this._bitField & 50397184) === 0)) {
|
|
this._resolveCallback(context.target);
|
|
}
|
|
};
|
|
|
|
var bindingRejected = function(e, context) {
|
|
if (!context.promiseRejectionQueued) this._reject(e);
|
|
};
|
|
|
|
Promise.prototype.bind = function (thisArg) {
|
|
if (!calledBind) {
|
|
calledBind = true;
|
|
Promise.prototype._propagateFrom = debug.propagateFromFunction();
|
|
Promise.prototype._boundValue = debug.boundValueFunction();
|
|
}
|
|
var maybePromise = tryConvertToPromise(thisArg);
|
|
var ret = new Promise(INTERNAL);
|
|
ret._propagateFrom(this, 1);
|
|
var target = this._target();
|
|
ret._setBoundTo(maybePromise);
|
|
if (maybePromise instanceof Promise) {
|
|
var context = {
|
|
promiseRejectionQueued: false,
|
|
promise: ret,
|
|
target: target,
|
|
bindingPromise: maybePromise
|
|
};
|
|
target._then(INTERNAL, targetRejected, undefined, ret, context);
|
|
maybePromise._then(
|
|
bindingResolved, bindingRejected, undefined, ret, context);
|
|
ret._setOnCancel(maybePromise);
|
|
} else {
|
|
ret._resolveCallback(target);
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
Promise.prototype._setBoundTo = function (obj) {
|
|
if (obj !== undefined) {
|
|
this._bitField = this._bitField | 2097152;
|
|
this._boundTo = obj;
|
|
} else {
|
|
this._bitField = this._bitField & (~2097152);
|
|
}
|
|
};
|
|
|
|
Promise.prototype._isBound = function () {
|
|
return (this._bitField & 2097152) === 2097152;
|
|
};
|
|
|
|
Promise.bind = function (thisArg, value) {
|
|
return Promise.resolve(value).bind(thisArg);
|
|
};
|
|
};
|
|
|
|
},{}],4:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
var old;
|
|
if (typeof Promise !== "undefined") old = Promise;
|
|
function noConflict() {
|
|
try { if (Promise === bluebird) Promise = old; }
|
|
catch (e) {}
|
|
return bluebird;
|
|
}
|
|
var bluebird = _dereq_("./promise")();
|
|
bluebird.noConflict = noConflict;
|
|
module.exports = bluebird;
|
|
|
|
},{"./promise":22}],5:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
var cr = Object.create;
|
|
if (cr) {
|
|
var callerCache = cr(null);
|
|
var getterCache = cr(null);
|
|
callerCache[" size"] = getterCache[" size"] = 0;
|
|
}
|
|
|
|
module.exports = function(Promise) {
|
|
var util = _dereq_("./util");
|
|
var canEvaluate = util.canEvaluate;
|
|
var isIdentifier = util.isIdentifier;
|
|
|
|
var getMethodCaller;
|
|
var getGetter;
|
|
if (!true) {
|
|
var makeMethodCaller = function (methodName) {
|
|
return new Function("ensureMethod", " \n\
|
|
return function(obj) { \n\
|
|
'use strict' \n\
|
|
var len = this.length; \n\
|
|
ensureMethod(obj, 'methodName'); \n\
|
|
switch(len) { \n\
|
|
case 1: return obj.methodName(this[0]); \n\
|
|
case 2: return obj.methodName(this[0], this[1]); \n\
|
|
case 3: return obj.methodName(this[0], this[1], this[2]); \n\
|
|
case 0: return obj.methodName(); \n\
|
|
default: \n\
|
|
return obj.methodName.apply(obj, this); \n\
|
|
} \n\
|
|
}; \n\
|
|
".replace(/methodName/g, methodName))(ensureMethod);
|
|
};
|
|
|
|
var makeGetter = function (propertyName) {
|
|
return new Function("obj", " \n\
|
|
'use strict'; \n\
|
|
return obj.propertyName; \n\
|
|
".replace("propertyName", propertyName));
|
|
};
|
|
|
|
var getCompiled = function(name, compiler, cache) {
|
|
var ret = cache[name];
|
|
if (typeof ret !== "function") {
|
|
if (!isIdentifier(name)) {
|
|
return null;
|
|
}
|
|
ret = compiler(name);
|
|
cache[name] = ret;
|
|
cache[" size"]++;
|
|
if (cache[" size"] > 512) {
|
|
var keys = Object.keys(cache);
|
|
for (var i = 0; i < 256; ++i) delete cache[keys[i]];
|
|
cache[" size"] = keys.length - 256;
|
|
}
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
getMethodCaller = function(name) {
|
|
return getCompiled(name, makeMethodCaller, callerCache);
|
|
};
|
|
|
|
getGetter = function(name) {
|
|
return getCompiled(name, makeGetter, getterCache);
|
|
};
|
|
}
|
|
|
|
function ensureMethod(obj, methodName) {
|
|
var fn;
|
|
if (obj != null) fn = obj[methodName];
|
|
if (typeof fn !== "function") {
|
|
var message = "Object " + util.classString(obj) + " has no method '" +
|
|
util.toString(methodName) + "'";
|
|
throw new Promise.TypeError(message);
|
|
}
|
|
return fn;
|
|
}
|
|
|
|
function caller(obj) {
|
|
var methodName = this.pop();
|
|
var fn = ensureMethod(obj, methodName);
|
|
return fn.apply(obj, this);
|
|
}
|
|
Promise.prototype.call = function (methodName) {
|
|
var args = [].slice.call(arguments, 1);;
|
|
if (!true) {
|
|
if (canEvaluate) {
|
|
var maybeCaller = getMethodCaller(methodName);
|
|
if (maybeCaller !== null) {
|
|
return this._then(
|
|
maybeCaller, undefined, undefined, args, undefined);
|
|
}
|
|
}
|
|
}
|
|
args.push(methodName);
|
|
return this._then(caller, undefined, undefined, args, undefined);
|
|
};
|
|
|
|
function namedGetter(obj) {
|
|
return obj[this];
|
|
}
|
|
function indexedGetter(obj) {
|
|
var index = +this;
|
|
if (index < 0) index = Math.max(0, index + obj.length);
|
|
return obj[index];
|
|
}
|
|
Promise.prototype.get = function (propertyName) {
|
|
var isIndex = (typeof propertyName === "number");
|
|
var getter;
|
|
if (!isIndex) {
|
|
if (canEvaluate) {
|
|
var maybeGetter = getGetter(propertyName);
|
|
getter = maybeGetter !== null ? maybeGetter : namedGetter;
|
|
} else {
|
|
getter = namedGetter;
|
|
}
|
|
} else {
|
|
getter = indexedGetter;
|
|
}
|
|
return this._then(getter, undefined, undefined, propertyName, undefined);
|
|
};
|
|
};
|
|
|
|
},{"./util":36}],6:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise, PromiseArray, apiRejection, debug) {
|
|
var util = _dereq_("./util");
|
|
var tryCatch = util.tryCatch;
|
|
var errorObj = util.errorObj;
|
|
var async = Promise._async;
|
|
|
|
Promise.prototype["break"] = Promise.prototype.cancel = function() {
|
|
if (!debug.cancellation()) return this._warn("cancellation is disabled");
|
|
|
|
var promise = this;
|
|
var child = promise;
|
|
while (promise._isCancellable()) {
|
|
if (!promise._cancelBy(child)) {
|
|
if (child._isFollowing()) {
|
|
child._followee().cancel();
|
|
} else {
|
|
child._cancelBranched();
|
|
}
|
|
break;
|
|
}
|
|
|
|
var parent = promise._cancellationParent;
|
|
if (parent == null || !parent._isCancellable()) {
|
|
if (promise._isFollowing()) {
|
|
promise._followee().cancel();
|
|
} else {
|
|
promise._cancelBranched();
|
|
}
|
|
break;
|
|
} else {
|
|
if (promise._isFollowing()) promise._followee().cancel();
|
|
promise._setWillBeCancelled();
|
|
child = promise;
|
|
promise = parent;
|
|
}
|
|
}
|
|
};
|
|
|
|
Promise.prototype._branchHasCancelled = function() {
|
|
this._branchesRemainingToCancel--;
|
|
};
|
|
|
|
Promise.prototype._enoughBranchesHaveCancelled = function() {
|
|
return this._branchesRemainingToCancel === undefined ||
|
|
this._branchesRemainingToCancel <= 0;
|
|
};
|
|
|
|
Promise.prototype._cancelBy = function(canceller) {
|
|
if (canceller === this) {
|
|
this._branchesRemainingToCancel = 0;
|
|
this._invokeOnCancel();
|
|
return true;
|
|
} else {
|
|
this._branchHasCancelled();
|
|
if (this._enoughBranchesHaveCancelled()) {
|
|
this._invokeOnCancel();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
Promise.prototype._cancelBranched = function() {
|
|
if (this._enoughBranchesHaveCancelled()) {
|
|
this._cancel();
|
|
}
|
|
};
|
|
|
|
Promise.prototype._cancel = function() {
|
|
if (!this._isCancellable()) return;
|
|
this._setCancelled();
|
|
async.invoke(this._cancelPromises, this, undefined);
|
|
};
|
|
|
|
Promise.prototype._cancelPromises = function() {
|
|
if (this._length() > 0) this._settlePromises();
|
|
};
|
|
|
|
Promise.prototype._unsetOnCancel = function() {
|
|
this._onCancelField = undefined;
|
|
};
|
|
|
|
Promise.prototype._isCancellable = function() {
|
|
return this.isPending() && !this._isCancelled();
|
|
};
|
|
|
|
Promise.prototype.isCancellable = function() {
|
|
return this.isPending() && !this.isCancelled();
|
|
};
|
|
|
|
Promise.prototype._doInvokeOnCancel = function(onCancelCallback, internalOnly) {
|
|
if (util.isArray(onCancelCallback)) {
|
|
for (var i = 0; i < onCancelCallback.length; ++i) {
|
|
this._doInvokeOnCancel(onCancelCallback[i], internalOnly);
|
|
}
|
|
} else if (onCancelCallback !== undefined) {
|
|
if (typeof onCancelCallback === "function") {
|
|
if (!internalOnly) {
|
|
var e = tryCatch(onCancelCallback).call(this._boundValue());
|
|
if (e === errorObj) {
|
|
this._attachExtraTrace(e.e);
|
|
async.throwLater(e.e);
|
|
}
|
|
}
|
|
} else {
|
|
onCancelCallback._resultCancelled(this);
|
|
}
|
|
}
|
|
};
|
|
|
|
Promise.prototype._invokeOnCancel = function() {
|
|
var onCancelCallback = this._onCancel();
|
|
this._unsetOnCancel();
|
|
async.invoke(this._doInvokeOnCancel, this, onCancelCallback);
|
|
};
|
|
|
|
Promise.prototype._invokeInternalOnCancel = function() {
|
|
if (this._isCancellable()) {
|
|
this._doInvokeOnCancel(this._onCancel(), true);
|
|
this._unsetOnCancel();
|
|
}
|
|
};
|
|
|
|
Promise.prototype._resultCancelled = function() {
|
|
this.cancel();
|
|
};
|
|
|
|
};
|
|
|
|
},{"./util":36}],7:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(NEXT_FILTER) {
|
|
var util = _dereq_("./util");
|
|
var getKeys = _dereq_("./es5").keys;
|
|
var tryCatch = util.tryCatch;
|
|
var errorObj = util.errorObj;
|
|
|
|
function catchFilter(instances, cb, promise) {
|
|
return function(e) {
|
|
var boundTo = promise._boundValue();
|
|
predicateLoop: for (var i = 0; i < instances.length; ++i) {
|
|
var item = instances[i];
|
|
|
|
if (item === Error ||
|
|
(item != null && item.prototype instanceof Error)) {
|
|
if (e instanceof item) {
|
|
return tryCatch(cb).call(boundTo, e);
|
|
}
|
|
} else if (typeof item === "function") {
|
|
var matchesPredicate = tryCatch(item).call(boundTo, e);
|
|
if (matchesPredicate === errorObj) {
|
|
return matchesPredicate;
|
|
} else if (matchesPredicate) {
|
|
return tryCatch(cb).call(boundTo, e);
|
|
}
|
|
} else if (util.isObject(e)) {
|
|
var keys = getKeys(item);
|
|
for (var j = 0; j < keys.length; ++j) {
|
|
var key = keys[j];
|
|
if (item[key] != e[key]) {
|
|
continue predicateLoop;
|
|
}
|
|
}
|
|
return tryCatch(cb).call(boundTo, e);
|
|
}
|
|
}
|
|
return NEXT_FILTER;
|
|
};
|
|
}
|
|
|
|
return catchFilter;
|
|
};
|
|
|
|
},{"./es5":13,"./util":36}],8:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise) {
|
|
var longStackTraces = false;
|
|
var contextStack = [];
|
|
|
|
Promise.prototype._promiseCreated = function() {};
|
|
Promise.prototype._pushContext = function() {};
|
|
Promise.prototype._popContext = function() {return null;};
|
|
Promise._peekContext = Promise.prototype._peekContext = function() {};
|
|
|
|
function Context() {
|
|
this._trace = new Context.CapturedTrace(peekContext());
|
|
}
|
|
Context.prototype._pushContext = function () {
|
|
if (this._trace !== undefined) {
|
|
this._trace._promiseCreated = null;
|
|
contextStack.push(this._trace);
|
|
}
|
|
};
|
|
|
|
Context.prototype._popContext = function () {
|
|
if (this._trace !== undefined) {
|
|
var trace = contextStack.pop();
|
|
var ret = trace._promiseCreated;
|
|
trace._promiseCreated = null;
|
|
return ret;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
function createContext() {
|
|
if (longStackTraces) return new Context();
|
|
}
|
|
|
|
function peekContext() {
|
|
var lastIndex = contextStack.length - 1;
|
|
if (lastIndex >= 0) {
|
|
return contextStack[lastIndex];
|
|
}
|
|
return undefined;
|
|
}
|
|
Context.CapturedTrace = null;
|
|
Context.create = createContext;
|
|
Context.deactivateLongStackTraces = function() {};
|
|
Context.activateLongStackTraces = function() {
|
|
var Promise_pushContext = Promise.prototype._pushContext;
|
|
var Promise_popContext = Promise.prototype._popContext;
|
|
var Promise_PeekContext = Promise._peekContext;
|
|
var Promise_peekContext = Promise.prototype._peekContext;
|
|
var Promise_promiseCreated = Promise.prototype._promiseCreated;
|
|
Context.deactivateLongStackTraces = function() {
|
|
Promise.prototype._pushContext = Promise_pushContext;
|
|
Promise.prototype._popContext = Promise_popContext;
|
|
Promise._peekContext = Promise_PeekContext;
|
|
Promise.prototype._peekContext = Promise_peekContext;
|
|
Promise.prototype._promiseCreated = Promise_promiseCreated;
|
|
longStackTraces = false;
|
|
};
|
|
longStackTraces = true;
|
|
Promise.prototype._pushContext = Context.prototype._pushContext;
|
|
Promise.prototype._popContext = Context.prototype._popContext;
|
|
Promise._peekContext = Promise.prototype._peekContext = peekContext;
|
|
Promise.prototype._promiseCreated = function() {
|
|
var ctx = this._peekContext();
|
|
if (ctx && ctx._promiseCreated == null) ctx._promiseCreated = this;
|
|
};
|
|
};
|
|
return Context;
|
|
};
|
|
|
|
},{}],9:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise, Context) {
|
|
var getDomain = Promise._getDomain;
|
|
var async = Promise._async;
|
|
var Warning = _dereq_("./errors").Warning;
|
|
var util = _dereq_("./util");
|
|
var canAttachTrace = util.canAttachTrace;
|
|
var unhandledRejectionHandled;
|
|
var possiblyUnhandledRejection;
|
|
var bluebirdFramePattern =
|
|
/[\\\/]bluebird[\\\/]js[\\\/](release|debug|instrumented)/;
|
|
var nodeFramePattern = /\((?:timers\.js):\d+:\d+\)/;
|
|
var parseLinePattern = /[\/<\(](.+?):(\d+):(\d+)\)?\s*$/;
|
|
var stackFramePattern = null;
|
|
var formatStack = null;
|
|
var indentStackFrames = false;
|
|
var printWarning;
|
|
var debugging = !!(util.env("BLUEBIRD_DEBUG") != 0 &&
|
|
(true ||
|
|
util.env("BLUEBIRD_DEBUG") ||
|
|
util.env("NODE_ENV") === "development"));
|
|
|
|
var warnings = !!(util.env("BLUEBIRD_WARNINGS") != 0 &&
|
|
(debugging || util.env("BLUEBIRD_WARNINGS")));
|
|
|
|
var longStackTraces = !!(util.env("BLUEBIRD_LONG_STACK_TRACES") != 0 &&
|
|
(debugging || util.env("BLUEBIRD_LONG_STACK_TRACES")));
|
|
|
|
var wForgottenReturn = util.env("BLUEBIRD_W_FORGOTTEN_RETURN") != 0 &&
|
|
(warnings || !!util.env("BLUEBIRD_W_FORGOTTEN_RETURN"));
|
|
|
|
Promise.prototype.suppressUnhandledRejections = function() {
|
|
var target = this._target();
|
|
target._bitField = ((target._bitField & (~1048576)) |
|
|
524288);
|
|
};
|
|
|
|
Promise.prototype._ensurePossibleRejectionHandled = function () {
|
|
if ((this._bitField & 524288) !== 0) return;
|
|
this._setRejectionIsUnhandled();
|
|
var self = this;
|
|
setTimeout(function() {
|
|
self._notifyUnhandledRejection();
|
|
}, 1);
|
|
};
|
|
|
|
Promise.prototype._notifyUnhandledRejectionIsHandled = function () {
|
|
fireRejectionEvent("rejectionHandled",
|
|
unhandledRejectionHandled, undefined, this);
|
|
};
|
|
|
|
Promise.prototype._setReturnedNonUndefined = function() {
|
|
this._bitField = this._bitField | 268435456;
|
|
};
|
|
|
|
Promise.prototype._returnedNonUndefined = function() {
|
|
return (this._bitField & 268435456) !== 0;
|
|
};
|
|
|
|
Promise.prototype._notifyUnhandledRejection = function () {
|
|
if (this._isRejectionUnhandled()) {
|
|
var reason = this._settledValue();
|
|
this._setUnhandledRejectionIsNotified();
|
|
fireRejectionEvent("unhandledRejection",
|
|
possiblyUnhandledRejection, reason, this);
|
|
}
|
|
};
|
|
|
|
Promise.prototype._setUnhandledRejectionIsNotified = function () {
|
|
this._bitField = this._bitField | 262144;
|
|
};
|
|
|
|
Promise.prototype._unsetUnhandledRejectionIsNotified = function () {
|
|
this._bitField = this._bitField & (~262144);
|
|
};
|
|
|
|
Promise.prototype._isUnhandledRejectionNotified = function () {
|
|
return (this._bitField & 262144) > 0;
|
|
};
|
|
|
|
Promise.prototype._setRejectionIsUnhandled = function () {
|
|
this._bitField = this._bitField | 1048576;
|
|
};
|
|
|
|
Promise.prototype._unsetRejectionIsUnhandled = function () {
|
|
this._bitField = this._bitField & (~1048576);
|
|
if (this._isUnhandledRejectionNotified()) {
|
|
this._unsetUnhandledRejectionIsNotified();
|
|
this._notifyUnhandledRejectionIsHandled();
|
|
}
|
|
};
|
|
|
|
Promise.prototype._isRejectionUnhandled = function () {
|
|
return (this._bitField & 1048576) > 0;
|
|
};
|
|
|
|
Promise.prototype._warn = function(message, shouldUseOwnTrace, promise) {
|
|
return warn(message, shouldUseOwnTrace, promise || this);
|
|
};
|
|
|
|
Promise.onPossiblyUnhandledRejection = function (fn) {
|
|
var domain = getDomain();
|
|
possiblyUnhandledRejection =
|
|
typeof fn === "function" ? (domain === null ?
|
|
fn : util.domainBind(domain, fn))
|
|
: undefined;
|
|
};
|
|
|
|
Promise.onUnhandledRejectionHandled = function (fn) {
|
|
var domain = getDomain();
|
|
unhandledRejectionHandled =
|
|
typeof fn === "function" ? (domain === null ?
|
|
fn : util.domainBind(domain, fn))
|
|
: undefined;
|
|
};
|
|
|
|
var disableLongStackTraces = function() {};
|
|
Promise.longStackTraces = function () {
|
|
if (async.haveItemsQueued() && !config.longStackTraces) {
|
|
throw new Error("cannot enable long stack traces after promises have been created\u000a\u000a See http://goo.gl/MqrFmX\u000a");
|
|
}
|
|
if (!config.longStackTraces && longStackTracesIsSupported()) {
|
|
var Promise_captureStackTrace = Promise.prototype._captureStackTrace;
|
|
var Promise_attachExtraTrace = Promise.prototype._attachExtraTrace;
|
|
config.longStackTraces = true;
|
|
disableLongStackTraces = function() {
|
|
if (async.haveItemsQueued() && !config.longStackTraces) {
|
|
throw new Error("cannot enable long stack traces after promises have been created\u000a\u000a See http://goo.gl/MqrFmX\u000a");
|
|
}
|
|
Promise.prototype._captureStackTrace = Promise_captureStackTrace;
|
|
Promise.prototype._attachExtraTrace = Promise_attachExtraTrace;
|
|
Context.deactivateLongStackTraces();
|
|
async.enableTrampoline();
|
|
config.longStackTraces = false;
|
|
};
|
|
Promise.prototype._captureStackTrace = longStackTracesCaptureStackTrace;
|
|
Promise.prototype._attachExtraTrace = longStackTracesAttachExtraTrace;
|
|
Context.activateLongStackTraces();
|
|
async.disableTrampolineIfNecessary();
|
|
}
|
|
};
|
|
|
|
Promise.hasLongStackTraces = function () {
|
|
return config.longStackTraces && longStackTracesIsSupported();
|
|
};
|
|
|
|
var fireDomEvent = (function() {
|
|
try {
|
|
if (typeof CustomEvent === "function") {
|
|
var event = new CustomEvent("CustomEvent");
|
|
util.global.dispatchEvent(event);
|
|
return function(name, event) {
|
|
var domEvent = new CustomEvent(name.toLowerCase(), {
|
|
detail: event,
|
|
cancelable: true
|
|
});
|
|
return !util.global.dispatchEvent(domEvent);
|
|
};
|
|
} else if (typeof Event === "function") {
|
|
var event = new Event("CustomEvent");
|
|
util.global.dispatchEvent(event);
|
|
return function(name, event) {
|
|
var domEvent = new Event(name.toLowerCase(), {
|
|
cancelable: true
|
|
});
|
|
domEvent.detail = event;
|
|
return !util.global.dispatchEvent(domEvent);
|
|
};
|
|
} else {
|
|
var event = document.createEvent("CustomEvent");
|
|
event.initCustomEvent("testingtheevent", false, true, {});
|
|
util.global.dispatchEvent(event);
|
|
return function(name, event) {
|
|
var domEvent = document.createEvent("CustomEvent");
|
|
domEvent.initCustomEvent(name.toLowerCase(), false, true,
|
|
event);
|
|
return !util.global.dispatchEvent(domEvent);
|
|
};
|
|
}
|
|
} catch (e) {}
|
|
return function() {
|
|
return false;
|
|
};
|
|
})();
|
|
|
|
var fireGlobalEvent = (function() {
|
|
if (util.isNode) {
|
|
return function() {
|
|
return process.emit.apply(process, arguments);
|
|
};
|
|
} else {
|
|
if (!util.global) {
|
|
return function() {
|
|
return false;
|
|
};
|
|
}
|
|
return function(name) {
|
|
var methodName = "on" + name.toLowerCase();
|
|
var method = util.global[methodName];
|
|
if (!method) return false;
|
|
method.apply(util.global, [].slice.call(arguments, 1));
|
|
return true;
|
|
};
|
|
}
|
|
})();
|
|
|
|
function generatePromiseLifecycleEventObject(name, promise) {
|
|
return {promise: promise};
|
|
}
|
|
|
|
var eventToObjectGenerator = {
|
|
promiseCreated: generatePromiseLifecycleEventObject,
|
|
promiseFulfilled: generatePromiseLifecycleEventObject,
|
|
promiseRejected: generatePromiseLifecycleEventObject,
|
|
promiseResolved: generatePromiseLifecycleEventObject,
|
|
promiseCancelled: generatePromiseLifecycleEventObject,
|
|
promiseChained: function(name, promise, child) {
|
|
return {promise: promise, child: child};
|
|
},
|
|
warning: function(name, warning) {
|
|
return {warning: warning};
|
|
},
|
|
unhandledRejection: function (name, reason, promise) {
|
|
return {reason: reason, promise: promise};
|
|
},
|
|
rejectionHandled: generatePromiseLifecycleEventObject
|
|
};
|
|
|
|
var activeFireEvent = function (name) {
|
|
var globalEventFired = false;
|
|
try {
|
|
globalEventFired = fireGlobalEvent.apply(null, arguments);
|
|
} catch (e) {
|
|
async.throwLater(e);
|
|
globalEventFired = true;
|
|
}
|
|
|
|
var domEventFired = false;
|
|
try {
|
|
domEventFired = fireDomEvent(name,
|
|
eventToObjectGenerator[name].apply(null, arguments));
|
|
} catch (e) {
|
|
async.throwLater(e);
|
|
domEventFired = true;
|
|
}
|
|
|
|
return domEventFired || globalEventFired;
|
|
};
|
|
|
|
Promise.config = function(opts) {
|
|
opts = Object(opts);
|
|
if ("longStackTraces" in opts) {
|
|
if (opts.longStackTraces) {
|
|
Promise.longStackTraces();
|
|
} else if (!opts.longStackTraces && Promise.hasLongStackTraces()) {
|
|
disableLongStackTraces();
|
|
}
|
|
}
|
|
if ("warnings" in opts) {
|
|
var warningsOption = opts.warnings;
|
|
config.warnings = !!warningsOption;
|
|
wForgottenReturn = config.warnings;
|
|
|
|
if (util.isObject(warningsOption)) {
|
|
if ("wForgottenReturn" in warningsOption) {
|
|
wForgottenReturn = !!warningsOption.wForgottenReturn;
|
|
}
|
|
}
|
|
}
|
|
if ("cancellation" in opts && opts.cancellation && !config.cancellation) {
|
|
if (async.haveItemsQueued()) {
|
|
throw new Error(
|
|
"cannot enable cancellation after promises are in use");
|
|
}
|
|
Promise.prototype._clearCancellationData =
|
|
cancellationClearCancellationData;
|
|
Promise.prototype._propagateFrom = cancellationPropagateFrom;
|
|
Promise.prototype._onCancel = cancellationOnCancel;
|
|
Promise.prototype._setOnCancel = cancellationSetOnCancel;
|
|
Promise.prototype._attachCancellationCallback =
|
|
cancellationAttachCancellationCallback;
|
|
Promise.prototype._execute = cancellationExecute;
|
|
propagateFromFunction = cancellationPropagateFrom;
|
|
config.cancellation = true;
|
|
}
|
|
if ("monitoring" in opts) {
|
|
if (opts.monitoring && !config.monitoring) {
|
|
config.monitoring = true;
|
|
Promise.prototype._fireEvent = activeFireEvent;
|
|
} else if (!opts.monitoring && config.monitoring) {
|
|
config.monitoring = false;
|
|
Promise.prototype._fireEvent = defaultFireEvent;
|
|
}
|
|
}
|
|
return Promise;
|
|
};
|
|
|
|
function defaultFireEvent() { return false; }
|
|
|
|
Promise.prototype._fireEvent = defaultFireEvent;
|
|
Promise.prototype._execute = function(executor, resolve, reject) {
|
|
try {
|
|
executor(resolve, reject);
|
|
} catch (e) {
|
|
return e;
|
|
}
|
|
};
|
|
Promise.prototype._onCancel = function () {};
|
|
Promise.prototype._setOnCancel = function (handler) { ; };
|
|
Promise.prototype._attachCancellationCallback = function(onCancel) {
|
|
;
|
|
};
|
|
Promise.prototype._captureStackTrace = function () {};
|
|
Promise.prototype._attachExtraTrace = function () {};
|
|
Promise.prototype._clearCancellationData = function() {};
|
|
Promise.prototype._propagateFrom = function (parent, flags) {
|
|
;
|
|
;
|
|
};
|
|
|
|
function cancellationExecute(executor, resolve, reject) {
|
|
var promise = this;
|
|
try {
|
|
executor(resolve, reject, function(onCancel) {
|
|
if (typeof onCancel !== "function") {
|
|
throw new TypeError("onCancel must be a function, got: " +
|
|
util.toString(onCancel));
|
|
}
|
|
promise._attachCancellationCallback(onCancel);
|
|
});
|
|
} catch (e) {
|
|
return e;
|
|
}
|
|
}
|
|
|
|
function cancellationAttachCancellationCallback(onCancel) {
|
|
if (!this._isCancellable()) return this;
|
|
|
|
var previousOnCancel = this._onCancel();
|
|
if (previousOnCancel !== undefined) {
|
|
if (util.isArray(previousOnCancel)) {
|
|
previousOnCancel.push(onCancel);
|
|
} else {
|
|
this._setOnCancel([previousOnCancel, onCancel]);
|
|
}
|
|
} else {
|
|
this._setOnCancel(onCancel);
|
|
}
|
|
}
|
|
|
|
function cancellationOnCancel() {
|
|
return this._onCancelField;
|
|
}
|
|
|
|
function cancellationSetOnCancel(onCancel) {
|
|
this._onCancelField = onCancel;
|
|
}
|
|
|
|
function cancellationClearCancellationData() {
|
|
this._cancellationParent = undefined;
|
|
this._onCancelField = undefined;
|
|
}
|
|
|
|
function cancellationPropagateFrom(parent, flags) {
|
|
if ((flags & 1) !== 0) {
|
|
this._cancellationParent = parent;
|
|
var branchesRemainingToCancel = parent._branchesRemainingToCancel;
|
|
if (branchesRemainingToCancel === undefined) {
|
|
branchesRemainingToCancel = 0;
|
|
}
|
|
parent._branchesRemainingToCancel = branchesRemainingToCancel + 1;
|
|
}
|
|
if ((flags & 2) !== 0 && parent._isBound()) {
|
|
this._setBoundTo(parent._boundTo);
|
|
}
|
|
}
|
|
|
|
function bindingPropagateFrom(parent, flags) {
|
|
if ((flags & 2) !== 0 && parent._isBound()) {
|
|
this._setBoundTo(parent._boundTo);
|
|
}
|
|
}
|
|
var propagateFromFunction = bindingPropagateFrom;
|
|
|
|
function boundValueFunction() {
|
|
var ret = this._boundTo;
|
|
if (ret !== undefined) {
|
|
if (ret instanceof Promise) {
|
|
if (ret.isFulfilled()) {
|
|
return ret.value();
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
function longStackTracesCaptureStackTrace() {
|
|
this._trace = new CapturedTrace(this._peekContext());
|
|
}
|
|
|
|
function longStackTracesAttachExtraTrace(error, ignoreSelf) {
|
|
if (canAttachTrace(error)) {
|
|
var trace = this._trace;
|
|
if (trace !== undefined) {
|
|
if (ignoreSelf) trace = trace._parent;
|
|
}
|
|
if (trace !== undefined) {
|
|
trace.attachExtraTrace(error);
|
|
} else if (!error.__stackCleaned__) {
|
|
var parsed = parseStackAndMessage(error);
|
|
util.notEnumerableProp(error, "stack",
|
|
parsed.message + "\n" + parsed.stack.join("\n"));
|
|
util.notEnumerableProp(error, "__stackCleaned__", true);
|
|
}
|
|
}
|
|
}
|
|
|
|
function checkForgottenReturns(returnValue, promiseCreated, name, promise,
|
|
parent) {
|
|
if (returnValue === undefined && promiseCreated !== null &&
|
|
wForgottenReturn) {
|
|
if (parent !== undefined && parent._returnedNonUndefined()) return;
|
|
if ((promise._bitField & 65535) === 0) return;
|
|
|
|
if (name) name = name + " ";
|
|
var handlerLine = "";
|
|
var creatorLine = "";
|
|
if (promiseCreated._trace) {
|
|
var traceLines = promiseCreated._trace.stack.split("\n");
|
|
var stack = cleanStack(traceLines);
|
|
for (var i = stack.length - 1; i >= 0; --i) {
|
|
var line = stack[i];
|
|
if (!nodeFramePattern.test(line)) {
|
|
var lineMatches = line.match(parseLinePattern);
|
|
if (lineMatches) {
|
|
handlerLine = "at " + lineMatches[1] +
|
|
":" + lineMatches[2] + ":" + lineMatches[3] + " ";
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (stack.length > 0) {
|
|
var firstUserLine = stack[0];
|
|
for (var i = 0; i < traceLines.length; ++i) {
|
|
|
|
if (traceLines[i] === firstUserLine) {
|
|
if (i > 0) {
|
|
creatorLine = "\n" + traceLines[i - 1];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
var msg = "a promise was created in a " + name +
|
|
"handler " + handlerLine + "but was not returned from it, " +
|
|
"see http://goo.gl/rRqMUw" +
|
|
creatorLine;
|
|
promise._warn(msg, true, promiseCreated);
|
|
}
|
|
}
|
|
|
|
function deprecated(name, replacement) {
|
|
var message = name +
|
|
" is deprecated and will be removed in a future version.";
|
|
if (replacement) message += " Use " + replacement + " instead.";
|
|
return warn(message);
|
|
}
|
|
|
|
function warn(message, shouldUseOwnTrace, promise) {
|
|
if (!config.warnings) return;
|
|
var warning = new Warning(message);
|
|
var ctx;
|
|
if (shouldUseOwnTrace) {
|
|
promise._attachExtraTrace(warning);
|
|
} else if (config.longStackTraces && (ctx = Promise._peekContext())) {
|
|
ctx.attachExtraTrace(warning);
|
|
} else {
|
|
var parsed = parseStackAndMessage(warning);
|
|
warning.stack = parsed.message + "\n" + parsed.stack.join("\n");
|
|
}
|
|
|
|
if (!activeFireEvent("warning", warning)) {
|
|
formatAndLogError(warning, "", true);
|
|
}
|
|
}
|
|
|
|
function reconstructStack(message, stacks) {
|
|
for (var i = 0; i < stacks.length - 1; ++i) {
|
|
stacks[i].push("From previous event:");
|
|
stacks[i] = stacks[i].join("\n");
|
|
}
|
|
if (i < stacks.length) {
|
|
stacks[i] = stacks[i].join("\n");
|
|
}
|
|
return message + "\n" + stacks.join("\n");
|
|
}
|
|
|
|
function removeDuplicateOrEmptyJumps(stacks) {
|
|
for (var i = 0; i < stacks.length; ++i) {
|
|
if (stacks[i].length === 0 ||
|
|
((i + 1 < stacks.length) && stacks[i][0] === stacks[i+1][0])) {
|
|
stacks.splice(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
|
|
function removeCommonRoots(stacks) {
|
|
var current = stacks[0];
|
|
for (var i = 1; i < stacks.length; ++i) {
|
|
var prev = stacks[i];
|
|
var currentLastIndex = current.length - 1;
|
|
var currentLastLine = current[currentLastIndex];
|
|
var commonRootMeetPoint = -1;
|
|
|
|
for (var j = prev.length - 1; j >= 0; --j) {
|
|
if (prev[j] === currentLastLine) {
|
|
commonRootMeetPoint = j;
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (var j = commonRootMeetPoint; j >= 0; --j) {
|
|
var line = prev[j];
|
|
if (current[currentLastIndex] === line) {
|
|
current.pop();
|
|
currentLastIndex--;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
current = prev;
|
|
}
|
|
}
|
|
|
|
function cleanStack(stack) {
|
|
var ret = [];
|
|
for (var i = 0; i < stack.length; ++i) {
|
|
var line = stack[i];
|
|
var isTraceLine = " (No stack trace)" === line ||
|
|
stackFramePattern.test(line);
|
|
var isInternalFrame = isTraceLine && shouldIgnore(line);
|
|
if (isTraceLine && !isInternalFrame) {
|
|
if (indentStackFrames && line.charAt(0) !== " ") {
|
|
line = " " + line;
|
|
}
|
|
ret.push(line);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
function stackFramesAsArray(error) {
|
|
var stack = error.stack.replace(/\s+$/g, "").split("\n");
|
|
for (var i = 0; i < stack.length; ++i) {
|
|
var line = stack[i];
|
|
if (" (No stack trace)" === line || stackFramePattern.test(line)) {
|
|
break;
|
|
}
|
|
}
|
|
if (i > 0 && error.name != "SyntaxError") {
|
|
stack = stack.slice(i);
|
|
}
|
|
return stack;
|
|
}
|
|
|
|
function parseStackAndMessage(error) {
|
|
var stack = error.stack;
|
|
var message = error.toString();
|
|
stack = typeof stack === "string" && stack.length > 0
|
|
? stackFramesAsArray(error) : [" (No stack trace)"];
|
|
return {
|
|
message: message,
|
|
stack: error.name == "SyntaxError" ? stack : cleanStack(stack)
|
|
};
|
|
}
|
|
|
|
function formatAndLogError(error, title, isSoft) {
|
|
if (typeof console !== "undefined") {
|
|
var message;
|
|
if (util.isObject(error)) {
|
|
var stack = error.stack;
|
|
message = title + formatStack(stack, error);
|
|
} else {
|
|
message = title + String(error);
|
|
}
|
|
if (typeof printWarning === "function") {
|
|
printWarning(message, isSoft);
|
|
} else if (typeof console.log === "function" ||
|
|
typeof console.log === "object") {
|
|
console.log(message);
|
|
}
|
|
}
|
|
}
|
|
|
|
function fireRejectionEvent(name, localHandler, reason, promise) {
|
|
var localEventFired = false;
|
|
try {
|
|
if (typeof localHandler === "function") {
|
|
localEventFired = true;
|
|
if (name === "rejectionHandled") {
|
|
localHandler(promise);
|
|
} else {
|
|
localHandler(reason, promise);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
async.throwLater(e);
|
|
}
|
|
|
|
if (name === "unhandledRejection") {
|
|
if (!activeFireEvent(name, reason, promise) && !localEventFired) {
|
|
formatAndLogError(reason, "Unhandled rejection ");
|
|
}
|
|
} else {
|
|
activeFireEvent(name, promise);
|
|
}
|
|
}
|
|
|
|
function formatNonError(obj) {
|
|
var str;
|
|
if (typeof obj === "function") {
|
|
str = "[function " +
|
|
(obj.name || "anonymous") +
|
|
"]";
|
|
} else {
|
|
str = obj && typeof obj.toString === "function"
|
|
? obj.toString() : util.toString(obj);
|
|
var ruselessToString = /\[object [a-zA-Z0-9$_]+\]/;
|
|
if (ruselessToString.test(str)) {
|
|
try {
|
|
var newStr = JSON.stringify(obj);
|
|
str = newStr;
|
|
}
|
|
catch(e) {
|
|
|
|
}
|
|
}
|
|
if (str.length === 0) {
|
|
str = "(empty array)";
|
|
}
|
|
}
|
|
return ("(<" + snip(str) + ">, no stack trace)");
|
|
}
|
|
|
|
function snip(str) {
|
|
var maxChars = 41;
|
|
if (str.length < maxChars) {
|
|
return str;
|
|
}
|
|
return str.substr(0, maxChars - 3) + "...";
|
|
}
|
|
|
|
function longStackTracesIsSupported() {
|
|
return typeof captureStackTrace === "function";
|
|
}
|
|
|
|
var shouldIgnore = function() { return false; };
|
|
var parseLineInfoRegex = /[\/<\(]([^:\/]+):(\d+):(?:\d+)\)?\s*$/;
|
|
function parseLineInfo(line) {
|
|
var matches = line.match(parseLineInfoRegex);
|
|
if (matches) {
|
|
return {
|
|
fileName: matches[1],
|
|
line: parseInt(matches[2], 10)
|
|
};
|
|
}
|
|
}
|
|
|
|
function setBounds(firstLineError, lastLineError) {
|
|
if (!longStackTracesIsSupported()) return;
|
|
var firstStackLines = firstLineError.stack.split("\n");
|
|
var lastStackLines = lastLineError.stack.split("\n");
|
|
var firstIndex = -1;
|
|
var lastIndex = -1;
|
|
var firstFileName;
|
|
var lastFileName;
|
|
for (var i = 0; i < firstStackLines.length; ++i) {
|
|
var result = parseLineInfo(firstStackLines[i]);
|
|
if (result) {
|
|
firstFileName = result.fileName;
|
|
firstIndex = result.line;
|
|
break;
|
|
}
|
|
}
|
|
for (var i = 0; i < lastStackLines.length; ++i) {
|
|
var result = parseLineInfo(lastStackLines[i]);
|
|
if (result) {
|
|
lastFileName = result.fileName;
|
|
lastIndex = result.line;
|
|
break;
|
|
}
|
|
}
|
|
if (firstIndex < 0 || lastIndex < 0 || !firstFileName || !lastFileName ||
|
|
firstFileName !== lastFileName || firstIndex >= lastIndex) {
|
|
return;
|
|
}
|
|
|
|
shouldIgnore = function(line) {
|
|
if (bluebirdFramePattern.test(line)) return true;
|
|
var info = parseLineInfo(line);
|
|
if (info) {
|
|
if (info.fileName === firstFileName &&
|
|
(firstIndex <= info.line && info.line <= lastIndex)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
}
|
|
|
|
function CapturedTrace(parent) {
|
|
this._parent = parent;
|
|
this._promisesCreated = 0;
|
|
var length = this._length = 1 + (parent === undefined ? 0 : parent._length);
|
|
captureStackTrace(this, CapturedTrace);
|
|
if (length > 32) this.uncycle();
|
|
}
|
|
util.inherits(CapturedTrace, Error);
|
|
Context.CapturedTrace = CapturedTrace;
|
|
|
|
CapturedTrace.prototype.uncycle = function() {
|
|
var length = this._length;
|
|
if (length < 2) return;
|
|
var nodes = [];
|
|
var stackToIndex = {};
|
|
|
|
for (var i = 0, node = this; node !== undefined; ++i) {
|
|
nodes.push(node);
|
|
node = node._parent;
|
|
}
|
|
length = this._length = i;
|
|
for (var i = length - 1; i >= 0; --i) {
|
|
var stack = nodes[i].stack;
|
|
if (stackToIndex[stack] === undefined) {
|
|
stackToIndex[stack] = i;
|
|
}
|
|
}
|
|
for (var i = 0; i < length; ++i) {
|
|
var currentStack = nodes[i].stack;
|
|
var index = stackToIndex[currentStack];
|
|
if (index !== undefined && index !== i) {
|
|
if (index > 0) {
|
|
nodes[index - 1]._parent = undefined;
|
|
nodes[index - 1]._length = 1;
|
|
}
|
|
nodes[i]._parent = undefined;
|
|
nodes[i]._length = 1;
|
|
var cycleEdgeNode = i > 0 ? nodes[i - 1] : this;
|
|
|
|
if (index < length - 1) {
|
|
cycleEdgeNode._parent = nodes[index + 1];
|
|
cycleEdgeNode._parent.uncycle();
|
|
cycleEdgeNode._length =
|
|
cycleEdgeNode._parent._length + 1;
|
|
} else {
|
|
cycleEdgeNode._parent = undefined;
|
|
cycleEdgeNode._length = 1;
|
|
}
|
|
var currentChildLength = cycleEdgeNode._length + 1;
|
|
for (var j = i - 2; j >= 0; --j) {
|
|
nodes[j]._length = currentChildLength;
|
|
currentChildLength++;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
CapturedTrace.prototype.attachExtraTrace = function(error) {
|
|
if (error.__stackCleaned__) return;
|
|
this.uncycle();
|
|
var parsed = parseStackAndMessage(error);
|
|
var message = parsed.message;
|
|
var stacks = [parsed.stack];
|
|
|
|
var trace = this;
|
|
while (trace !== undefined) {
|
|
stacks.push(cleanStack(trace.stack.split("\n")));
|
|
trace = trace._parent;
|
|
}
|
|
removeCommonRoots(stacks);
|
|
removeDuplicateOrEmptyJumps(stacks);
|
|
util.notEnumerableProp(error, "stack", reconstructStack(message, stacks));
|
|
util.notEnumerableProp(error, "__stackCleaned__", true);
|
|
};
|
|
|
|
var captureStackTrace = (function stackDetection() {
|
|
var v8stackFramePattern = /^\s*at\s*/;
|
|
var v8stackFormatter = function(stack, error) {
|
|
if (typeof stack === "string") return stack;
|
|
|
|
if (error.name !== undefined &&
|
|
error.message !== undefined) {
|
|
return error.toString();
|
|
}
|
|
return formatNonError(error);
|
|
};
|
|
|
|
if (typeof Error.stackTraceLimit === "number" &&
|
|
typeof Error.captureStackTrace === "function") {
|
|
Error.stackTraceLimit += 6;
|
|
stackFramePattern = v8stackFramePattern;
|
|
formatStack = v8stackFormatter;
|
|
var captureStackTrace = Error.captureStackTrace;
|
|
|
|
shouldIgnore = function(line) {
|
|
return bluebirdFramePattern.test(line);
|
|
};
|
|
return function(receiver, ignoreUntil) {
|
|
Error.stackTraceLimit += 6;
|
|
captureStackTrace(receiver, ignoreUntil);
|
|
Error.stackTraceLimit -= 6;
|
|
};
|
|
}
|
|
var err = new Error();
|
|
|
|
if (typeof err.stack === "string" &&
|
|
err.stack.split("\n")[0].indexOf("stackDetection@") >= 0) {
|
|
stackFramePattern = /@/;
|
|
formatStack = v8stackFormatter;
|
|
indentStackFrames = true;
|
|
return function captureStackTrace(o) {
|
|
o.stack = new Error().stack;
|
|
};
|
|
}
|
|
|
|
var hasStackAfterThrow;
|
|
try { throw new Error(); }
|
|
catch(e) {
|
|
hasStackAfterThrow = ("stack" in e);
|
|
}
|
|
if (!("stack" in err) && hasStackAfterThrow &&
|
|
typeof Error.stackTraceLimit === "number") {
|
|
stackFramePattern = v8stackFramePattern;
|
|
formatStack = v8stackFormatter;
|
|
return function captureStackTrace(o) {
|
|
Error.stackTraceLimit += 6;
|
|
try { throw new Error(); }
|
|
catch(e) { o.stack = e.stack; }
|
|
Error.stackTraceLimit -= 6;
|
|
};
|
|
}
|
|
|
|
formatStack = function(stack, error) {
|
|
if (typeof stack === "string") return stack;
|
|
|
|
if ((typeof error === "object" ||
|
|
typeof error === "function") &&
|
|
error.name !== undefined &&
|
|
error.message !== undefined) {
|
|
return error.toString();
|
|
}
|
|
return formatNonError(error);
|
|
};
|
|
|
|
return null;
|
|
|
|
})([]);
|
|
|
|
if (typeof console !== "undefined" && typeof console.warn !== "undefined") {
|
|
printWarning = function (message) {
|
|
console.warn(message);
|
|
};
|
|
if (util.isNode && process.stderr.isTTY) {
|
|
printWarning = function(message, isSoft) {
|
|
var color = isSoft ? "\u001b[33m" : "\u001b[31m";
|
|
console.warn(color + message + "\u001b[0m\n");
|
|
};
|
|
} else if (!util.isNode && typeof (new Error().stack) === "string") {
|
|
printWarning = function(message, isSoft) {
|
|
console.warn("%c" + message,
|
|
isSoft ? "color: darkorange" : "color: red");
|
|
};
|
|
}
|
|
}
|
|
|
|
var config = {
|
|
warnings: warnings,
|
|
longStackTraces: false,
|
|
cancellation: false,
|
|
monitoring: false
|
|
};
|
|
|
|
if (longStackTraces) Promise.longStackTraces();
|
|
|
|
return {
|
|
longStackTraces: function() {
|
|
return config.longStackTraces;
|
|
},
|
|
warnings: function() {
|
|
return config.warnings;
|
|
},
|
|
cancellation: function() {
|
|
return config.cancellation;
|
|
},
|
|
monitoring: function() {
|
|
return config.monitoring;
|
|
},
|
|
propagateFromFunction: function() {
|
|
return propagateFromFunction;
|
|
},
|
|
boundValueFunction: function() {
|
|
return boundValueFunction;
|
|
},
|
|
checkForgottenReturns: checkForgottenReturns,
|
|
setBounds: setBounds,
|
|
warn: warn,
|
|
deprecated: deprecated,
|
|
CapturedTrace: CapturedTrace,
|
|
fireDomEvent: fireDomEvent,
|
|
fireGlobalEvent: fireGlobalEvent
|
|
};
|
|
};
|
|
|
|
},{"./errors":12,"./util":36}],10:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise) {
|
|
function returner() {
|
|
return this.value;
|
|
}
|
|
function thrower() {
|
|
throw this.reason;
|
|
}
|
|
|
|
Promise.prototype["return"] =
|
|
Promise.prototype.thenReturn = function (value) {
|
|
if (value instanceof Promise) value.suppressUnhandledRejections();
|
|
return this._then(
|
|
returner, undefined, undefined, {value: value}, undefined);
|
|
};
|
|
|
|
Promise.prototype["throw"] =
|
|
Promise.prototype.thenThrow = function (reason) {
|
|
return this._then(
|
|
thrower, undefined, undefined, {reason: reason}, undefined);
|
|
};
|
|
|
|
Promise.prototype.catchThrow = function (reason) {
|
|
if (arguments.length <= 1) {
|
|
return this._then(
|
|
undefined, thrower, undefined, {reason: reason}, undefined);
|
|
} else {
|
|
var _reason = arguments[1];
|
|
var handler = function() {throw _reason;};
|
|
return this.caught(reason, handler);
|
|
}
|
|
};
|
|
|
|
Promise.prototype.catchReturn = function (value) {
|
|
if (arguments.length <= 1) {
|
|
if (value instanceof Promise) value.suppressUnhandledRejections();
|
|
return this._then(
|
|
undefined, returner, undefined, {value: value}, undefined);
|
|
} else {
|
|
var _value = arguments[1];
|
|
if (_value instanceof Promise) _value.suppressUnhandledRejections();
|
|
var handler = function() {return _value;};
|
|
return this.caught(value, handler);
|
|
}
|
|
};
|
|
};
|
|
|
|
},{}],11:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise, INTERNAL) {
|
|
var PromiseReduce = Promise.reduce;
|
|
var PromiseAll = Promise.all;
|
|
|
|
function promiseAllThis() {
|
|
return PromiseAll(this);
|
|
}
|
|
|
|
function PromiseMapSeries(promises, fn) {
|
|
return PromiseReduce(promises, fn, INTERNAL, INTERNAL);
|
|
}
|
|
|
|
Promise.prototype.each = function (fn) {
|
|
return PromiseReduce(this, fn, INTERNAL, 0)
|
|
._then(promiseAllThis, undefined, undefined, this, undefined);
|
|
};
|
|
|
|
Promise.prototype.mapSeries = function (fn) {
|
|
return PromiseReduce(this, fn, INTERNAL, INTERNAL);
|
|
};
|
|
|
|
Promise.each = function (promises, fn) {
|
|
return PromiseReduce(promises, fn, INTERNAL, 0)
|
|
._then(promiseAllThis, undefined, undefined, promises, undefined);
|
|
};
|
|
|
|
Promise.mapSeries = PromiseMapSeries;
|
|
};
|
|
|
|
|
|
},{}],12:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
var es5 = _dereq_("./es5");
|
|
var Objectfreeze = es5.freeze;
|
|
var util = _dereq_("./util");
|
|
var inherits = util.inherits;
|
|
var notEnumerableProp = util.notEnumerableProp;
|
|
|
|
function subError(nameProperty, defaultMessage) {
|
|
function SubError(message) {
|
|
if (!(this instanceof SubError)) return new SubError(message);
|
|
notEnumerableProp(this, "message",
|
|
typeof message === "string" ? message : defaultMessage);
|
|
notEnumerableProp(this, "name", nameProperty);
|
|
if (Error.captureStackTrace) {
|
|
Error.captureStackTrace(this, this.constructor);
|
|
} else {
|
|
Error.call(this);
|
|
}
|
|
}
|
|
inherits(SubError, Error);
|
|
return SubError;
|
|
}
|
|
|
|
var _TypeError, _RangeError;
|
|
var Warning = subError("Warning", "warning");
|
|
var CancellationError = subError("CancellationError", "cancellation error");
|
|
var TimeoutError = subError("TimeoutError", "timeout error");
|
|
var AggregateError = subError("AggregateError", "aggregate error");
|
|
try {
|
|
_TypeError = TypeError;
|
|
_RangeError = RangeError;
|
|
} catch(e) {
|
|
_TypeError = subError("TypeError", "type error");
|
|
_RangeError = subError("RangeError", "range error");
|
|
}
|
|
|
|
var methods = ("join pop push shift unshift slice filter forEach some " +
|
|
"every map indexOf lastIndexOf reduce reduceRight sort reverse").split(" ");
|
|
|
|
for (var i = 0; i < methods.length; ++i) {
|
|
if (typeof Array.prototype[methods[i]] === "function") {
|
|
AggregateError.prototype[methods[i]] = Array.prototype[methods[i]];
|
|
}
|
|
}
|
|
|
|
es5.defineProperty(AggregateError.prototype, "length", {
|
|
value: 0,
|
|
configurable: false,
|
|
writable: true,
|
|
enumerable: true
|
|
});
|
|
AggregateError.prototype["isOperational"] = true;
|
|
var level = 0;
|
|
AggregateError.prototype.toString = function() {
|
|
var indent = Array(level * 4 + 1).join(" ");
|
|
var ret = "\n" + indent + "AggregateError of:" + "\n";
|
|
level++;
|
|
indent = Array(level * 4 + 1).join(" ");
|
|
for (var i = 0; i < this.length; ++i) {
|
|
var str = this[i] === this ? "[Circular AggregateError]" : this[i] + "";
|
|
var lines = str.split("\n");
|
|
for (var j = 0; j < lines.length; ++j) {
|
|
lines[j] = indent + lines[j];
|
|
}
|
|
str = lines.join("\n");
|
|
ret += str + "\n";
|
|
}
|
|
level--;
|
|
return ret;
|
|
};
|
|
|
|
function OperationalError(message) {
|
|
if (!(this instanceof OperationalError))
|
|
return new OperationalError(message);
|
|
notEnumerableProp(this, "name", "OperationalError");
|
|
notEnumerableProp(this, "message", message);
|
|
this.cause = message;
|
|
this["isOperational"] = true;
|
|
|
|
if (message instanceof Error) {
|
|
notEnumerableProp(this, "message", message.message);
|
|
notEnumerableProp(this, "stack", message.stack);
|
|
} else if (Error.captureStackTrace) {
|
|
Error.captureStackTrace(this, this.constructor);
|
|
}
|
|
|
|
}
|
|
inherits(OperationalError, Error);
|
|
|
|
var errorTypes = Error["__BluebirdErrorTypes__"];
|
|
if (!errorTypes) {
|
|
errorTypes = Objectfreeze({
|
|
CancellationError: CancellationError,
|
|
TimeoutError: TimeoutError,
|
|
OperationalError: OperationalError,
|
|
RejectionError: OperationalError,
|
|
AggregateError: AggregateError
|
|
});
|
|
es5.defineProperty(Error, "__BluebirdErrorTypes__", {
|
|
value: errorTypes,
|
|
writable: false,
|
|
enumerable: false,
|
|
configurable: false
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
Error: Error,
|
|
TypeError: _TypeError,
|
|
RangeError: _RangeError,
|
|
CancellationError: errorTypes.CancellationError,
|
|
OperationalError: errorTypes.OperationalError,
|
|
TimeoutError: errorTypes.TimeoutError,
|
|
AggregateError: errorTypes.AggregateError,
|
|
Warning: Warning
|
|
};
|
|
|
|
},{"./es5":13,"./util":36}],13:[function(_dereq_,module,exports){
|
|
var isES5 = (function(){
|
|
"use strict";
|
|
return this === undefined;
|
|
})();
|
|
|
|
if (isES5) {
|
|
module.exports = {
|
|
freeze: Object.freeze,
|
|
defineProperty: Object.defineProperty,
|
|
getDescriptor: Object.getOwnPropertyDescriptor,
|
|
keys: Object.keys,
|
|
names: Object.getOwnPropertyNames,
|
|
getPrototypeOf: Object.getPrototypeOf,
|
|
isArray: Array.isArray,
|
|
isES5: isES5,
|
|
propertyIsWritable: function(obj, prop) {
|
|
var descriptor = Object.getOwnPropertyDescriptor(obj, prop);
|
|
return !!(!descriptor || descriptor.writable || descriptor.set);
|
|
}
|
|
};
|
|
} else {
|
|
var has = {}.hasOwnProperty;
|
|
var str = {}.toString;
|
|
var proto = {}.constructor.prototype;
|
|
|
|
var ObjectKeys = function (o) {
|
|
var ret = [];
|
|
for (var key in o) {
|
|
if (has.call(o, key)) {
|
|
ret.push(key);
|
|
}
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
var ObjectGetDescriptor = function(o, key) {
|
|
return {value: o[key]};
|
|
};
|
|
|
|
var ObjectDefineProperty = function (o, key, desc) {
|
|
o[key] = desc.value;
|
|
return o;
|
|
};
|
|
|
|
var ObjectFreeze = function (obj) {
|
|
return obj;
|
|
};
|
|
|
|
var ObjectGetPrototypeOf = function (obj) {
|
|
try {
|
|
return Object(obj).constructor.prototype;
|
|
}
|
|
catch (e) {
|
|
return proto;
|
|
}
|
|
};
|
|
|
|
var ArrayIsArray = function (obj) {
|
|
try {
|
|
return str.call(obj) === "[object Array]";
|
|
}
|
|
catch(e) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
isArray: ArrayIsArray,
|
|
keys: ObjectKeys,
|
|
names: ObjectKeys,
|
|
defineProperty: ObjectDefineProperty,
|
|
getDescriptor: ObjectGetDescriptor,
|
|
freeze: ObjectFreeze,
|
|
getPrototypeOf: ObjectGetPrototypeOf,
|
|
isES5: isES5,
|
|
propertyIsWritable: function() {
|
|
return true;
|
|
}
|
|
};
|
|
}
|
|
|
|
},{}],14:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise, INTERNAL) {
|
|
var PromiseMap = Promise.map;
|
|
|
|
Promise.prototype.filter = function (fn, options) {
|
|
return PromiseMap(this, fn, options, INTERNAL);
|
|
};
|
|
|
|
Promise.filter = function (promises, fn, options) {
|
|
return PromiseMap(promises, fn, options, INTERNAL);
|
|
};
|
|
};
|
|
|
|
},{}],15:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise, tryConvertToPromise, NEXT_FILTER) {
|
|
var util = _dereq_("./util");
|
|
var CancellationError = Promise.CancellationError;
|
|
var errorObj = util.errorObj;
|
|
var catchFilter = _dereq_("./catch_filter")(NEXT_FILTER);
|
|
|
|
function PassThroughHandlerContext(promise, type, handler) {
|
|
this.promise = promise;
|
|
this.type = type;
|
|
this.handler = handler;
|
|
this.called = false;
|
|
this.cancelPromise = null;
|
|
}
|
|
|
|
PassThroughHandlerContext.prototype.isFinallyHandler = function() {
|
|
return this.type === 0;
|
|
};
|
|
|
|
function FinallyHandlerCancelReaction(finallyHandler) {
|
|
this.finallyHandler = finallyHandler;
|
|
}
|
|
|
|
FinallyHandlerCancelReaction.prototype._resultCancelled = function() {
|
|
checkCancel(this.finallyHandler);
|
|
};
|
|
|
|
function checkCancel(ctx, reason) {
|
|
if (ctx.cancelPromise != null) {
|
|
if (arguments.length > 1) {
|
|
ctx.cancelPromise._reject(reason);
|
|
} else {
|
|
ctx.cancelPromise._cancel();
|
|
}
|
|
ctx.cancelPromise = null;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function succeed() {
|
|
return finallyHandler.call(this, this.promise._target()._settledValue());
|
|
}
|
|
function fail(reason) {
|
|
if (checkCancel(this, reason)) return;
|
|
errorObj.e = reason;
|
|
return errorObj;
|
|
}
|
|
function finallyHandler(reasonOrValue) {
|
|
var promise = this.promise;
|
|
var handler = this.handler;
|
|
|
|
if (!this.called) {
|
|
this.called = true;
|
|
var ret = this.isFinallyHandler()
|
|
? handler.call(promise._boundValue())
|
|
: handler.call(promise._boundValue(), reasonOrValue);
|
|
if (ret === NEXT_FILTER) {
|
|
return ret;
|
|
} else if (ret !== undefined) {
|
|
promise._setReturnedNonUndefined();
|
|
var maybePromise = tryConvertToPromise(ret, promise);
|
|
if (maybePromise instanceof Promise) {
|
|
if (this.cancelPromise != null) {
|
|
if (maybePromise._isCancelled()) {
|
|
var reason =
|
|
new CancellationError("late cancellation observer");
|
|
promise._attachExtraTrace(reason);
|
|
errorObj.e = reason;
|
|
return errorObj;
|
|
} else if (maybePromise.isPending()) {
|
|
maybePromise._attachCancellationCallback(
|
|
new FinallyHandlerCancelReaction(this));
|
|
}
|
|
}
|
|
return maybePromise._then(
|
|
succeed, fail, undefined, this, undefined);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (promise.isRejected()) {
|
|
checkCancel(this);
|
|
errorObj.e = reasonOrValue;
|
|
return errorObj;
|
|
} else {
|
|
checkCancel(this);
|
|
return reasonOrValue;
|
|
}
|
|
}
|
|
|
|
Promise.prototype._passThrough = function(handler, type, success, fail) {
|
|
if (typeof handler !== "function") return this.then();
|
|
return this._then(success,
|
|
fail,
|
|
undefined,
|
|
new PassThroughHandlerContext(this, type, handler),
|
|
undefined);
|
|
};
|
|
|
|
Promise.prototype.lastly =
|
|
Promise.prototype["finally"] = function (handler) {
|
|
return this._passThrough(handler,
|
|
0,
|
|
finallyHandler,
|
|
finallyHandler);
|
|
};
|
|
|
|
|
|
Promise.prototype.tap = function (handler) {
|
|
return this._passThrough(handler, 1, finallyHandler);
|
|
};
|
|
|
|
Promise.prototype.tapCatch = function (handlerOrPredicate) {
|
|
var len = arguments.length;
|
|
if(len === 1) {
|
|
return this._passThrough(handlerOrPredicate,
|
|
1,
|
|
undefined,
|
|
finallyHandler);
|
|
} else {
|
|
var catchInstances = new Array(len - 1),
|
|
j = 0, i;
|
|
for (i = 0; i < len - 1; ++i) {
|
|
var item = arguments[i];
|
|
if (util.isObject(item)) {
|
|
catchInstances[j++] = item;
|
|
} else {
|
|
return Promise.reject(new TypeError(
|
|
"tapCatch statement predicate: "
|
|
+ "expecting an object but got " + util.classString(item)
|
|
));
|
|
}
|
|
}
|
|
catchInstances.length = j;
|
|
var handler = arguments[i];
|
|
return this._passThrough(catchFilter(catchInstances, handler, this),
|
|
1,
|
|
undefined,
|
|
finallyHandler);
|
|
}
|
|
|
|
};
|
|
|
|
return PassThroughHandlerContext;
|
|
};
|
|
|
|
},{"./catch_filter":7,"./util":36}],16:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise,
|
|
apiRejection,
|
|
INTERNAL,
|
|
tryConvertToPromise,
|
|
Proxyable,
|
|
debug) {
|
|
var errors = _dereq_("./errors");
|
|
var TypeError = errors.TypeError;
|
|
var util = _dereq_("./util");
|
|
var errorObj = util.errorObj;
|
|
var tryCatch = util.tryCatch;
|
|
var yieldHandlers = [];
|
|
|
|
function promiseFromYieldHandler(value, yieldHandlers, traceParent) {
|
|
for (var i = 0; i < yieldHandlers.length; ++i) {
|
|
traceParent._pushContext();
|
|
var result = tryCatch(yieldHandlers[i])(value);
|
|
traceParent._popContext();
|
|
if (result === errorObj) {
|
|
traceParent._pushContext();
|
|
var ret = Promise.reject(errorObj.e);
|
|
traceParent._popContext();
|
|
return ret;
|
|
}
|
|
var maybePromise = tryConvertToPromise(result, traceParent);
|
|
if (maybePromise instanceof Promise) return maybePromise;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function PromiseSpawn(generatorFunction, receiver, yieldHandler, stack) {
|
|
if (debug.cancellation()) {
|
|
var internal = new Promise(INTERNAL);
|
|
var _finallyPromise = this._finallyPromise = new Promise(INTERNAL);
|
|
this._promise = internal.lastly(function() {
|
|
return _finallyPromise;
|
|
});
|
|
internal._captureStackTrace();
|
|
internal._setOnCancel(this);
|
|
} else {
|
|
var promise = this._promise = new Promise(INTERNAL);
|
|
promise._captureStackTrace();
|
|
}
|
|
this._stack = stack;
|
|
this._generatorFunction = generatorFunction;
|
|
this._receiver = receiver;
|
|
this._generator = undefined;
|
|
this._yieldHandlers = typeof yieldHandler === "function"
|
|
? [yieldHandler].concat(yieldHandlers)
|
|
: yieldHandlers;
|
|
this._yieldedPromise = null;
|
|
this._cancellationPhase = false;
|
|
}
|
|
util.inherits(PromiseSpawn, Proxyable);
|
|
|
|
PromiseSpawn.prototype._isResolved = function() {
|
|
return this._promise === null;
|
|
};
|
|
|
|
PromiseSpawn.prototype._cleanup = function() {
|
|
this._promise = this._generator = null;
|
|
if (debug.cancellation() && this._finallyPromise !== null) {
|
|
this._finallyPromise._fulfill();
|
|
this._finallyPromise = null;
|
|
}
|
|
};
|
|
|
|
PromiseSpawn.prototype._promiseCancelled = function() {
|
|
if (this._isResolved()) return;
|
|
var implementsReturn = typeof this._generator["return"] !== "undefined";
|
|
|
|
var result;
|
|
if (!implementsReturn) {
|
|
var reason = new Promise.CancellationError(
|
|
"generator .return() sentinel");
|
|
Promise.coroutine.returnSentinel = reason;
|
|
this._promise._attachExtraTrace(reason);
|
|
this._promise._pushContext();
|
|
result = tryCatch(this._generator["throw"]).call(this._generator,
|
|
reason);
|
|
this._promise._popContext();
|
|
} else {
|
|
this._promise._pushContext();
|
|
result = tryCatch(this._generator["return"]).call(this._generator,
|
|
undefined);
|
|
this._promise._popContext();
|
|
}
|
|
this._cancellationPhase = true;
|
|
this._yieldedPromise = null;
|
|
this._continue(result);
|
|
};
|
|
|
|
PromiseSpawn.prototype._promiseFulfilled = function(value) {
|
|
this._yieldedPromise = null;
|
|
this._promise._pushContext();
|
|
var result = tryCatch(this._generator.next).call(this._generator, value);
|
|
this._promise._popContext();
|
|
this._continue(result);
|
|
};
|
|
|
|
PromiseSpawn.prototype._promiseRejected = function(reason) {
|
|
this._yieldedPromise = null;
|
|
this._promise._attachExtraTrace(reason);
|
|
this._promise._pushContext();
|
|
var result = tryCatch(this._generator["throw"])
|
|
.call(this._generator, reason);
|
|
this._promise._popContext();
|
|
this._continue(result);
|
|
};
|
|
|
|
PromiseSpawn.prototype._resultCancelled = function() {
|
|
if (this._yieldedPromise instanceof Promise) {
|
|
var promise = this._yieldedPromise;
|
|
this._yieldedPromise = null;
|
|
promise.cancel();
|
|
}
|
|
};
|
|
|
|
PromiseSpawn.prototype.promise = function () {
|
|
return this._promise;
|
|
};
|
|
|
|
PromiseSpawn.prototype._run = function () {
|
|
this._generator = this._generatorFunction.call(this._receiver);
|
|
this._receiver =
|
|
this._generatorFunction = undefined;
|
|
this._promiseFulfilled(undefined);
|
|
};
|
|
|
|
PromiseSpawn.prototype._continue = function (result) {
|
|
var promise = this._promise;
|
|
if (result === errorObj) {
|
|
this._cleanup();
|
|
if (this._cancellationPhase) {
|
|
return promise.cancel();
|
|
} else {
|
|
return promise._rejectCallback(result.e, false);
|
|
}
|
|
}
|
|
|
|
var value = result.value;
|
|
if (result.done === true) {
|
|
this._cleanup();
|
|
if (this._cancellationPhase) {
|
|
return promise.cancel();
|
|
} else {
|
|
return promise._resolveCallback(value);
|
|
}
|
|
} else {
|
|
var maybePromise = tryConvertToPromise(value, this._promise);
|
|
if (!(maybePromise instanceof Promise)) {
|
|
maybePromise =
|
|
promiseFromYieldHandler(maybePromise,
|
|
this._yieldHandlers,
|
|
this._promise);
|
|
if (maybePromise === null) {
|
|
this._promiseRejected(
|
|
new TypeError(
|
|
"A value %s was yielded that could not be treated as a promise\u000a\u000a See http://goo.gl/MqrFmX\u000a\u000a".replace("%s", String(value)) +
|
|
"From coroutine:\u000a" +
|
|
this._stack.split("\n").slice(1, -7).join("\n")
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
maybePromise = maybePromise._target();
|
|
var bitField = maybePromise._bitField;
|
|
;
|
|
if (((bitField & 50397184) === 0)) {
|
|
this._yieldedPromise = maybePromise;
|
|
maybePromise._proxy(this, null);
|
|
} else if (((bitField & 33554432) !== 0)) {
|
|
Promise._async.invoke(
|
|
this._promiseFulfilled, this, maybePromise._value()
|
|
);
|
|
} else if (((bitField & 16777216) !== 0)) {
|
|
Promise._async.invoke(
|
|
this._promiseRejected, this, maybePromise._reason()
|
|
);
|
|
} else {
|
|
this._promiseCancelled();
|
|
}
|
|
}
|
|
};
|
|
|
|
Promise.coroutine = function (generatorFunction, options) {
|
|
if (typeof generatorFunction !== "function") {
|
|
throw new TypeError("generatorFunction must be a function\u000a\u000a See http://goo.gl/MqrFmX\u000a");
|
|
}
|
|
var yieldHandler = Object(options).yieldHandler;
|
|
var PromiseSpawn$ = PromiseSpawn;
|
|
var stack = new Error().stack;
|
|
return function () {
|
|
var generator = generatorFunction.apply(this, arguments);
|
|
var spawn = new PromiseSpawn$(undefined, undefined, yieldHandler,
|
|
stack);
|
|
var ret = spawn.promise();
|
|
spawn._generator = generator;
|
|
spawn._promiseFulfilled(undefined);
|
|
return ret;
|
|
};
|
|
};
|
|
|
|
Promise.coroutine.addYieldHandler = function(fn) {
|
|
if (typeof fn !== "function") {
|
|
throw new TypeError("expecting a function but got " + util.classString(fn));
|
|
}
|
|
yieldHandlers.push(fn);
|
|
};
|
|
|
|
Promise.spawn = function (generatorFunction) {
|
|
debug.deprecated("Promise.spawn()", "Promise.coroutine()");
|
|
if (typeof generatorFunction !== "function") {
|
|
return apiRejection("generatorFunction must be a function\u000a\u000a See http://goo.gl/MqrFmX\u000a");
|
|
}
|
|
var spawn = new PromiseSpawn(generatorFunction, this);
|
|
var ret = spawn.promise();
|
|
spawn._run(Promise.spawn);
|
|
return ret;
|
|
};
|
|
};
|
|
|
|
},{"./errors":12,"./util":36}],17:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports =
|
|
function(Promise, PromiseArray, tryConvertToPromise, INTERNAL, async,
|
|
getDomain) {
|
|
var util = _dereq_("./util");
|
|
var canEvaluate = util.canEvaluate;
|
|
var tryCatch = util.tryCatch;
|
|
var errorObj = util.errorObj;
|
|
var reject;
|
|
|
|
if (!true) {
|
|
if (canEvaluate) {
|
|
var thenCallback = function(i) {
|
|
return new Function("value", "holder", " \n\
|
|
'use strict'; \n\
|
|
holder.pIndex = value; \n\
|
|
holder.checkFulfillment(this); \n\
|
|
".replace(/Index/g, i));
|
|
};
|
|
|
|
var promiseSetter = function(i) {
|
|
return new Function("promise", "holder", " \n\
|
|
'use strict'; \n\
|
|
holder.pIndex = promise; \n\
|
|
".replace(/Index/g, i));
|
|
};
|
|
|
|
var generateHolderClass = function(total) {
|
|
var props = new Array(total);
|
|
for (var i = 0; i < props.length; ++i) {
|
|
props[i] = "this.p" + (i+1);
|
|
}
|
|
var assignment = props.join(" = ") + " = null;";
|
|
var cancellationCode= "var promise;\n" + props.map(function(prop) {
|
|
return " \n\
|
|
promise = " + prop + "; \n\
|
|
if (promise instanceof Promise) { \n\
|
|
promise.cancel(); \n\
|
|
} \n\
|
|
";
|
|
}).join("\n");
|
|
var passedArguments = props.join(", ");
|
|
var name = "Holder$" + total;
|
|
|
|
|
|
var code = "return function(tryCatch, errorObj, Promise, async) { \n\
|
|
'use strict'; \n\
|
|
function [TheName](fn) { \n\
|
|
[TheProperties] \n\
|
|
this.fn = fn; \n\
|
|
this.asyncNeeded = true; \n\
|
|
this.now = 0; \n\
|
|
} \n\
|
|
\n\
|
|
[TheName].prototype._callFunction = function(promise) { \n\
|
|
promise._pushContext(); \n\
|
|
var ret = tryCatch(this.fn)([ThePassedArguments]); \n\
|
|
promise._popContext(); \n\
|
|
if (ret === errorObj) { \n\
|
|
promise._rejectCallback(ret.e, false); \n\
|
|
} else { \n\
|
|
promise._resolveCallback(ret); \n\
|
|
} \n\
|
|
}; \n\
|
|
\n\
|
|
[TheName].prototype.checkFulfillment = function(promise) { \n\
|
|
var now = ++this.now; \n\
|
|
if (now === [TheTotal]) { \n\
|
|
if (this.asyncNeeded) { \n\
|
|
async.invoke(this._callFunction, this, promise); \n\
|
|
} else { \n\
|
|
this._callFunction(promise); \n\
|
|
} \n\
|
|
\n\
|
|
} \n\
|
|
}; \n\
|
|
\n\
|
|
[TheName].prototype._resultCancelled = function() { \n\
|
|
[CancellationCode] \n\
|
|
}; \n\
|
|
\n\
|
|
return [TheName]; \n\
|
|
}(tryCatch, errorObj, Promise, async); \n\
|
|
";
|
|
|
|
code = code.replace(/\[TheName\]/g, name)
|
|
.replace(/\[TheTotal\]/g, total)
|
|
.replace(/\[ThePassedArguments\]/g, passedArguments)
|
|
.replace(/\[TheProperties\]/g, assignment)
|
|
.replace(/\[CancellationCode\]/g, cancellationCode);
|
|
|
|
return new Function("tryCatch", "errorObj", "Promise", "async", code)
|
|
(tryCatch, errorObj, Promise, async);
|
|
};
|
|
|
|
var holderClasses = [];
|
|
var thenCallbacks = [];
|
|
var promiseSetters = [];
|
|
|
|
for (var i = 0; i < 8; ++i) {
|
|
holderClasses.push(generateHolderClass(i + 1));
|
|
thenCallbacks.push(thenCallback(i + 1));
|
|
promiseSetters.push(promiseSetter(i + 1));
|
|
}
|
|
|
|
reject = function (reason) {
|
|
this._reject(reason);
|
|
};
|
|
}}
|
|
|
|
Promise.join = function () {
|
|
var last = arguments.length - 1;
|
|
var fn;
|
|
if (last > 0 && typeof arguments[last] === "function") {
|
|
fn = arguments[last];
|
|
if (!true) {
|
|
if (last <= 8 && canEvaluate) {
|
|
var ret = new Promise(INTERNAL);
|
|
ret._captureStackTrace();
|
|
var HolderClass = holderClasses[last - 1];
|
|
var holder = new HolderClass(fn);
|
|
var callbacks = thenCallbacks;
|
|
|
|
for (var i = 0; i < last; ++i) {
|
|
var maybePromise = tryConvertToPromise(arguments[i], ret);
|
|
if (maybePromise instanceof Promise) {
|
|
maybePromise = maybePromise._target();
|
|
var bitField = maybePromise._bitField;
|
|
;
|
|
if (((bitField & 50397184) === 0)) {
|
|
maybePromise._then(callbacks[i], reject,
|
|
undefined, ret, holder);
|
|
promiseSetters[i](maybePromise, holder);
|
|
holder.asyncNeeded = false;
|
|
} else if (((bitField & 33554432) !== 0)) {
|
|
callbacks[i].call(ret,
|
|
maybePromise._value(), holder);
|
|
} else if (((bitField & 16777216) !== 0)) {
|
|
ret._reject(maybePromise._reason());
|
|
} else {
|
|
ret._cancel();
|
|
}
|
|
} else {
|
|
callbacks[i].call(ret, maybePromise, holder);
|
|
}
|
|
}
|
|
|
|
if (!ret._isFateSealed()) {
|
|
if (holder.asyncNeeded) {
|
|
var domain = getDomain();
|
|
if (domain !== null) {
|
|
holder.fn = util.domainBind(domain, holder.fn);
|
|
}
|
|
}
|
|
ret._setAsyncGuaranteed();
|
|
ret._setOnCancel(holder);
|
|
}
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
var args = [].slice.call(arguments);;
|
|
if (fn) args.pop();
|
|
var ret = new PromiseArray(args).promise();
|
|
return fn !== undefined ? ret.spread(fn) : ret;
|
|
};
|
|
|
|
};
|
|
|
|
},{"./util":36}],18:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise,
|
|
PromiseArray,
|
|
apiRejection,
|
|
tryConvertToPromise,
|
|
INTERNAL,
|
|
debug) {
|
|
var getDomain = Promise._getDomain;
|
|
var util = _dereq_("./util");
|
|
var tryCatch = util.tryCatch;
|
|
var errorObj = util.errorObj;
|
|
var async = Promise._async;
|
|
|
|
function MappingPromiseArray(promises, fn, limit, _filter) {
|
|
this.constructor$(promises);
|
|
this._promise._captureStackTrace();
|
|
var domain = getDomain();
|
|
this._callback = domain === null ? fn : util.domainBind(domain, fn);
|
|
this._preservedValues = _filter === INTERNAL
|
|
? new Array(this.length())
|
|
: null;
|
|
this._limit = limit;
|
|
this._inFlight = 0;
|
|
this._queue = [];
|
|
async.invoke(this._asyncInit, this, undefined);
|
|
}
|
|
util.inherits(MappingPromiseArray, PromiseArray);
|
|
|
|
MappingPromiseArray.prototype._asyncInit = function() {
|
|
this._init$(undefined, -2);
|
|
};
|
|
|
|
MappingPromiseArray.prototype._init = function () {};
|
|
|
|
MappingPromiseArray.prototype._promiseFulfilled = function (value, index) {
|
|
var values = this._values;
|
|
var length = this.length();
|
|
var preservedValues = this._preservedValues;
|
|
var limit = this._limit;
|
|
|
|
if (index < 0) {
|
|
index = (index * -1) - 1;
|
|
values[index] = value;
|
|
if (limit >= 1) {
|
|
this._inFlight--;
|
|
this._drainQueue();
|
|
if (this._isResolved()) return true;
|
|
}
|
|
} else {
|
|
if (limit >= 1 && this._inFlight >= limit) {
|
|
values[index] = value;
|
|
this._queue.push(index);
|
|
return false;
|
|
}
|
|
if (preservedValues !== null) preservedValues[index] = value;
|
|
|
|
var promise = this._promise;
|
|
var callback = this._callback;
|
|
var receiver = promise._boundValue();
|
|
promise._pushContext();
|
|
var ret = tryCatch(callback).call(receiver, value, index, length);
|
|
var promiseCreated = promise._popContext();
|
|
debug.checkForgottenReturns(
|
|
ret,
|
|
promiseCreated,
|
|
preservedValues !== null ? "Promise.filter" : "Promise.map",
|
|
promise
|
|
);
|
|
if (ret === errorObj) {
|
|
this._reject(ret.e);
|
|
return true;
|
|
}
|
|
|
|
var maybePromise = tryConvertToPromise(ret, this._promise);
|
|
if (maybePromise instanceof Promise) {
|
|
maybePromise = maybePromise._target();
|
|
var bitField = maybePromise._bitField;
|
|
;
|
|
if (((bitField & 50397184) === 0)) {
|
|
if (limit >= 1) this._inFlight++;
|
|
values[index] = maybePromise;
|
|
maybePromise._proxy(this, (index + 1) * -1);
|
|
return false;
|
|
} else if (((bitField & 33554432) !== 0)) {
|
|
ret = maybePromise._value();
|
|
} else if (((bitField & 16777216) !== 0)) {
|
|
this._reject(maybePromise._reason());
|
|
return true;
|
|
} else {
|
|
this._cancel();
|
|
return true;
|
|
}
|
|
}
|
|
values[index] = ret;
|
|
}
|
|
var totalResolved = ++this._totalResolved;
|
|
if (totalResolved >= length) {
|
|
if (preservedValues !== null) {
|
|
this._filter(values, preservedValues);
|
|
} else {
|
|
this._resolve(values);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
MappingPromiseArray.prototype._drainQueue = function () {
|
|
var queue = this._queue;
|
|
var limit = this._limit;
|
|
var values = this._values;
|
|
while (queue.length > 0 && this._inFlight < limit) {
|
|
if (this._isResolved()) return;
|
|
var index = queue.pop();
|
|
this._promiseFulfilled(values[index], index);
|
|
}
|
|
};
|
|
|
|
MappingPromiseArray.prototype._filter = function (booleans, values) {
|
|
var len = values.length;
|
|
var ret = new Array(len);
|
|
var j = 0;
|
|
for (var i = 0; i < len; ++i) {
|
|
if (booleans[i]) ret[j++] = values[i];
|
|
}
|
|
ret.length = j;
|
|
this._resolve(ret);
|
|
};
|
|
|
|
MappingPromiseArray.prototype.preservedValues = function () {
|
|
return this._preservedValues;
|
|
};
|
|
|
|
function map(promises, fn, options, _filter) {
|
|
if (typeof fn !== "function") {
|
|
return apiRejection("expecting a function but got " + util.classString(fn));
|
|
}
|
|
|
|
var limit = 0;
|
|
if (options !== undefined) {
|
|
if (typeof options === "object" && options !== null) {
|
|
if (typeof options.concurrency !== "number") {
|
|
return Promise.reject(
|
|
new TypeError("'concurrency' must be a number but it is " +
|
|
util.classString(options.concurrency)));
|
|
}
|
|
limit = options.concurrency;
|
|
} else {
|
|
return Promise.reject(new TypeError(
|
|
"options argument must be an object but it is " +
|
|
util.classString(options)));
|
|
}
|
|
}
|
|
limit = typeof limit === "number" &&
|
|
isFinite(limit) && limit >= 1 ? limit : 0;
|
|
return new MappingPromiseArray(promises, fn, limit, _filter).promise();
|
|
}
|
|
|
|
Promise.prototype.map = function (fn, options) {
|
|
return map(this, fn, options, null);
|
|
};
|
|
|
|
Promise.map = function (promises, fn, options, _filter) {
|
|
return map(promises, fn, options, _filter);
|
|
};
|
|
|
|
|
|
};
|
|
|
|
},{"./util":36}],19:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports =
|
|
function(Promise, INTERNAL, tryConvertToPromise, apiRejection, debug) {
|
|
var util = _dereq_("./util");
|
|
var tryCatch = util.tryCatch;
|
|
|
|
Promise.method = function (fn) {
|
|
if (typeof fn !== "function") {
|
|
throw new Promise.TypeError("expecting a function but got " + util.classString(fn));
|
|
}
|
|
return function () {
|
|
var ret = new Promise(INTERNAL);
|
|
ret._captureStackTrace();
|
|
ret._pushContext();
|
|
var value = tryCatch(fn).apply(this, arguments);
|
|
var promiseCreated = ret._popContext();
|
|
debug.checkForgottenReturns(
|
|
value, promiseCreated, "Promise.method", ret);
|
|
ret._resolveFromSyncValue(value);
|
|
return ret;
|
|
};
|
|
};
|
|
|
|
Promise.attempt = Promise["try"] = function (fn) {
|
|
if (typeof fn !== "function") {
|
|
return apiRejection("expecting a function but got " + util.classString(fn));
|
|
}
|
|
var ret = new Promise(INTERNAL);
|
|
ret._captureStackTrace();
|
|
ret._pushContext();
|
|
var value;
|
|
if (arguments.length > 1) {
|
|
debug.deprecated("calling Promise.try with more than 1 argument");
|
|
var arg = arguments[1];
|
|
var ctx = arguments[2];
|
|
value = util.isArray(arg) ? tryCatch(fn).apply(ctx, arg)
|
|
: tryCatch(fn).call(ctx, arg);
|
|
} else {
|
|
value = tryCatch(fn)();
|
|
}
|
|
var promiseCreated = ret._popContext();
|
|
debug.checkForgottenReturns(
|
|
value, promiseCreated, "Promise.try", ret);
|
|
ret._resolveFromSyncValue(value);
|
|
return ret;
|
|
};
|
|
|
|
Promise.prototype._resolveFromSyncValue = function (value) {
|
|
if (value === util.errorObj) {
|
|
this._rejectCallback(value.e, false);
|
|
} else {
|
|
this._resolveCallback(value, true);
|
|
}
|
|
};
|
|
};
|
|
|
|
},{"./util":36}],20:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
var util = _dereq_("./util");
|
|
var maybeWrapAsError = util.maybeWrapAsError;
|
|
var errors = _dereq_("./errors");
|
|
var OperationalError = errors.OperationalError;
|
|
var es5 = _dereq_("./es5");
|
|
|
|
function isUntypedError(obj) {
|
|
return obj instanceof Error &&
|
|
es5.getPrototypeOf(obj) === Error.prototype;
|
|
}
|
|
|
|
var rErrorKey = /^(?:name|message|stack|cause)$/;
|
|
function wrapAsOperationalError(obj) {
|
|
var ret;
|
|
if (isUntypedError(obj)) {
|
|
ret = new OperationalError(obj);
|
|
ret.name = obj.name;
|
|
ret.message = obj.message;
|
|
ret.stack = obj.stack;
|
|
var keys = es5.keys(obj);
|
|
for (var i = 0; i < keys.length; ++i) {
|
|
var key = keys[i];
|
|
if (!rErrorKey.test(key)) {
|
|
ret[key] = obj[key];
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
util.markAsOriginatingFromRejection(obj);
|
|
return obj;
|
|
}
|
|
|
|
function nodebackForPromise(promise, multiArgs) {
|
|
return function(err, value) {
|
|
if (promise === null) return;
|
|
if (err) {
|
|
var wrapped = wrapAsOperationalError(maybeWrapAsError(err));
|
|
promise._attachExtraTrace(wrapped);
|
|
promise._reject(wrapped);
|
|
} else if (!multiArgs) {
|
|
promise._fulfill(value);
|
|
} else {
|
|
var args = [].slice.call(arguments, 1);;
|
|
promise._fulfill(args);
|
|
}
|
|
promise = null;
|
|
};
|
|
}
|
|
|
|
module.exports = nodebackForPromise;
|
|
|
|
},{"./errors":12,"./es5":13,"./util":36}],21:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise) {
|
|
var util = _dereq_("./util");
|
|
var async = Promise._async;
|
|
var tryCatch = util.tryCatch;
|
|
var errorObj = util.errorObj;
|
|
|
|
function spreadAdapter(val, nodeback) {
|
|
var promise = this;
|
|
if (!util.isArray(val)) return successAdapter.call(promise, val, nodeback);
|
|
var ret =
|
|
tryCatch(nodeback).apply(promise._boundValue(), [null].concat(val));
|
|
if (ret === errorObj) {
|
|
async.throwLater(ret.e);
|
|
}
|
|
}
|
|
|
|
function successAdapter(val, nodeback) {
|
|
var promise = this;
|
|
var receiver = promise._boundValue();
|
|
var ret = val === undefined
|
|
? tryCatch(nodeback).call(receiver, null)
|
|
: tryCatch(nodeback).call(receiver, null, val);
|
|
if (ret === errorObj) {
|
|
async.throwLater(ret.e);
|
|
}
|
|
}
|
|
function errorAdapter(reason, nodeback) {
|
|
var promise = this;
|
|
if (!reason) {
|
|
var newReason = new Error(reason + "");
|
|
newReason.cause = reason;
|
|
reason = newReason;
|
|
}
|
|
var ret = tryCatch(nodeback).call(promise._boundValue(), reason);
|
|
if (ret === errorObj) {
|
|
async.throwLater(ret.e);
|
|
}
|
|
}
|
|
|
|
Promise.prototype.asCallback = Promise.prototype.nodeify = function (nodeback,
|
|
options) {
|
|
if (typeof nodeback == "function") {
|
|
var adapter = successAdapter;
|
|
if (options !== undefined && Object(options).spread) {
|
|
adapter = spreadAdapter;
|
|
}
|
|
this._then(
|
|
adapter,
|
|
errorAdapter,
|
|
undefined,
|
|
this,
|
|
nodeback
|
|
);
|
|
}
|
|
return this;
|
|
};
|
|
};
|
|
|
|
},{"./util":36}],22:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function() {
|
|
var makeSelfResolutionError = function () {
|
|
return new TypeError("circular promise resolution chain\u000a\u000a See http://goo.gl/MqrFmX\u000a");
|
|
};
|
|
var reflectHandler = function() {
|
|
return new Promise.PromiseInspection(this._target());
|
|
};
|
|
var apiRejection = function(msg) {
|
|
return Promise.reject(new TypeError(msg));
|
|
};
|
|
function Proxyable() {}
|
|
var UNDEFINED_BINDING = {};
|
|
var util = _dereq_("./util");
|
|
|
|
var getDomain;
|
|
if (util.isNode) {
|
|
getDomain = function() {
|
|
var ret = process.domain;
|
|
if (ret === undefined) ret = null;
|
|
return ret;
|
|
};
|
|
} else {
|
|
getDomain = function() {
|
|
return null;
|
|
};
|
|
}
|
|
util.notEnumerableProp(Promise, "_getDomain", getDomain);
|
|
|
|
var es5 = _dereq_("./es5");
|
|
var Async = _dereq_("./async");
|
|
var async = new Async();
|
|
es5.defineProperty(Promise, "_async", {value: async});
|
|
var errors = _dereq_("./errors");
|
|
var TypeError = Promise.TypeError = errors.TypeError;
|
|
Promise.RangeError = errors.RangeError;
|
|
var CancellationError = Promise.CancellationError = errors.CancellationError;
|
|
Promise.TimeoutError = errors.TimeoutError;
|
|
Promise.OperationalError = errors.OperationalError;
|
|
Promise.RejectionError = errors.OperationalError;
|
|
Promise.AggregateError = errors.AggregateError;
|
|
var INTERNAL = function(){};
|
|
var APPLY = {};
|
|
var NEXT_FILTER = {};
|
|
var tryConvertToPromise = _dereq_("./thenables")(Promise, INTERNAL);
|
|
var PromiseArray =
|
|
_dereq_("./promise_array")(Promise, INTERNAL,
|
|
tryConvertToPromise, apiRejection, Proxyable);
|
|
var Context = _dereq_("./context")(Promise);
|
|
/*jshint unused:false*/
|
|
var createContext = Context.create;
|
|
var debug = _dereq_("./debuggability")(Promise, Context);
|
|
var CapturedTrace = debug.CapturedTrace;
|
|
var PassThroughHandlerContext =
|
|
_dereq_("./finally")(Promise, tryConvertToPromise, NEXT_FILTER);
|
|
var catchFilter = _dereq_("./catch_filter")(NEXT_FILTER);
|
|
var nodebackForPromise = _dereq_("./nodeback");
|
|
var errorObj = util.errorObj;
|
|
var tryCatch = util.tryCatch;
|
|
function check(self, executor) {
|
|
if (self == null || self.constructor !== Promise) {
|
|
throw new TypeError("the promise constructor cannot be invoked directly\u000a\u000a See http://goo.gl/MqrFmX\u000a");
|
|
}
|
|
if (typeof executor !== "function") {
|
|
throw new TypeError("expecting a function but got " + util.classString(executor));
|
|
}
|
|
|
|
}
|
|
|
|
function Promise(executor) {
|
|
if (executor !== INTERNAL) {
|
|
check(this, executor);
|
|
}
|
|
this._bitField = 0;
|
|
this._fulfillmentHandler0 = undefined;
|
|
this._rejectionHandler0 = undefined;
|
|
this._promise0 = undefined;
|
|
this._receiver0 = undefined;
|
|
this._resolveFromExecutor(executor);
|
|
this._promiseCreated();
|
|
this._fireEvent("promiseCreated", this);
|
|
}
|
|
|
|
Promise.prototype.toString = function () {
|
|
return "[object Promise]";
|
|
};
|
|
|
|
Promise.prototype.caught = Promise.prototype["catch"] = function (fn) {
|
|
var len = arguments.length;
|
|
if (len > 1) {
|
|
var catchInstances = new Array(len - 1),
|
|
j = 0, i;
|
|
for (i = 0; i < len - 1; ++i) {
|
|
var item = arguments[i];
|
|
if (util.isObject(item)) {
|
|
catchInstances[j++] = item;
|
|
} else {
|
|
return apiRejection("Catch statement predicate: " +
|
|
"expecting an object but got " + util.classString(item));
|
|
}
|
|
}
|
|
catchInstances.length = j;
|
|
fn = arguments[i];
|
|
return this.then(undefined, catchFilter(catchInstances, fn, this));
|
|
}
|
|
return this.then(undefined, fn);
|
|
};
|
|
|
|
Promise.prototype.reflect = function () {
|
|
return this._then(reflectHandler,
|
|
reflectHandler, undefined, this, undefined);
|
|
};
|
|
|
|
Promise.prototype.then = function (didFulfill, didReject) {
|
|
if (debug.warnings() && arguments.length > 0 &&
|
|
typeof didFulfill !== "function" &&
|
|
typeof didReject !== "function") {
|
|
var msg = ".then() only accepts functions but was passed: " +
|
|
util.classString(didFulfill);
|
|
if (arguments.length > 1) {
|
|
msg += ", " + util.classString(didReject);
|
|
}
|
|
this._warn(msg);
|
|
}
|
|
return this._then(didFulfill, didReject, undefined, undefined, undefined);
|
|
};
|
|
|
|
Promise.prototype.done = function (didFulfill, didReject) {
|
|
var promise =
|
|
this._then(didFulfill, didReject, undefined, undefined, undefined);
|
|
promise._setIsFinal();
|
|
};
|
|
|
|
Promise.prototype.spread = function (fn) {
|
|
if (typeof fn !== "function") {
|
|
return apiRejection("expecting a function but got " + util.classString(fn));
|
|
}
|
|
return this.all()._then(fn, undefined, undefined, APPLY, undefined);
|
|
};
|
|
|
|
Promise.prototype.toJSON = function () {
|
|
var ret = {
|
|
isFulfilled: false,
|
|
isRejected: false,
|
|
fulfillmentValue: undefined,
|
|
rejectionReason: undefined
|
|
};
|
|
if (this.isFulfilled()) {
|
|
ret.fulfillmentValue = this.value();
|
|
ret.isFulfilled = true;
|
|
} else if (this.isRejected()) {
|
|
ret.rejectionReason = this.reason();
|
|
ret.isRejected = true;
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
Promise.prototype.all = function () {
|
|
if (arguments.length > 0) {
|
|
this._warn(".all() was passed arguments but it does not take any");
|
|
}
|
|
return new PromiseArray(this).promise();
|
|
};
|
|
|
|
Promise.prototype.error = function (fn) {
|
|
return this.caught(util.originatesFromRejection, fn);
|
|
};
|
|
|
|
Promise.getNewLibraryCopy = module.exports;
|
|
|
|
Promise.is = function (val) {
|
|
return val instanceof Promise;
|
|
};
|
|
|
|
Promise.fromNode = Promise.fromCallback = function(fn) {
|
|
var ret = new Promise(INTERNAL);
|
|
ret._captureStackTrace();
|
|
var multiArgs = arguments.length > 1 ? !!Object(arguments[1]).multiArgs
|
|
: false;
|
|
var result = tryCatch(fn)(nodebackForPromise(ret, multiArgs));
|
|
if (result === errorObj) {
|
|
ret._rejectCallback(result.e, true);
|
|
}
|
|
if (!ret._isFateSealed()) ret._setAsyncGuaranteed();
|
|
return ret;
|
|
};
|
|
|
|
Promise.all = function (promises) {
|
|
return new PromiseArray(promises).promise();
|
|
};
|
|
|
|
Promise.cast = function (obj) {
|
|
var ret = tryConvertToPromise(obj);
|
|
if (!(ret instanceof Promise)) {
|
|
ret = new Promise(INTERNAL);
|
|
ret._captureStackTrace();
|
|
ret._setFulfilled();
|
|
ret._rejectionHandler0 = obj;
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
Promise.resolve = Promise.fulfilled = Promise.cast;
|
|
|
|
Promise.reject = Promise.rejected = function (reason) {
|
|
var ret = new Promise(INTERNAL);
|
|
ret._captureStackTrace();
|
|
ret._rejectCallback(reason, true);
|
|
return ret;
|
|
};
|
|
|
|
Promise.setScheduler = function(fn) {
|
|
if (typeof fn !== "function") {
|
|
throw new TypeError("expecting a function but got " + util.classString(fn));
|
|
}
|
|
return async.setScheduler(fn);
|
|
};
|
|
|
|
Promise.prototype._then = function (
|
|
didFulfill,
|
|
didReject,
|
|
_, receiver,
|
|
internalData
|
|
) {
|
|
var haveInternalData = internalData !== undefined;
|
|
var promise = haveInternalData ? internalData : new Promise(INTERNAL);
|
|
var target = this._target();
|
|
var bitField = target._bitField;
|
|
|
|
if (!haveInternalData) {
|
|
promise._propagateFrom(this, 3);
|
|
promise._captureStackTrace();
|
|
if (receiver === undefined &&
|
|
((this._bitField & 2097152) !== 0)) {
|
|
if (!((bitField & 50397184) === 0)) {
|
|
receiver = this._boundValue();
|
|
} else {
|
|
receiver = target === this ? undefined : this._boundTo;
|
|
}
|
|
}
|
|
this._fireEvent("promiseChained", this, promise);
|
|
}
|
|
|
|
var domain = getDomain();
|
|
if (!((bitField & 50397184) === 0)) {
|
|
var handler, value, settler = target._settlePromiseCtx;
|
|
if (((bitField & 33554432) !== 0)) {
|
|
value = target._rejectionHandler0;
|
|
handler = didFulfill;
|
|
} else if (((bitField & 16777216) !== 0)) {
|
|
value = target._fulfillmentHandler0;
|
|
handler = didReject;
|
|
target._unsetRejectionIsUnhandled();
|
|
} else {
|
|
settler = target._settlePromiseLateCancellationObserver;
|
|
value = new CancellationError("late cancellation observer");
|
|
target._attachExtraTrace(value);
|
|
handler = didReject;
|
|
}
|
|
|
|
async.invoke(settler, target, {
|
|
handler: domain === null ? handler
|
|
: (typeof handler === "function" &&
|
|
util.domainBind(domain, handler)),
|
|
promise: promise,
|
|
receiver: receiver,
|
|
value: value
|
|
});
|
|
} else {
|
|
target._addCallbacks(didFulfill, didReject, promise, receiver, domain);
|
|
}
|
|
|
|
return promise;
|
|
};
|
|
|
|
Promise.prototype._length = function () {
|
|
return this._bitField & 65535;
|
|
};
|
|
|
|
Promise.prototype._isFateSealed = function () {
|
|
return (this._bitField & 117506048) !== 0;
|
|
};
|
|
|
|
Promise.prototype._isFollowing = function () {
|
|
return (this._bitField & 67108864) === 67108864;
|
|
};
|
|
|
|
Promise.prototype._setLength = function (len) {
|
|
this._bitField = (this._bitField & -65536) |
|
|
(len & 65535);
|
|
};
|
|
|
|
Promise.prototype._setFulfilled = function () {
|
|
this._bitField = this._bitField | 33554432;
|
|
this._fireEvent("promiseFulfilled", this);
|
|
};
|
|
|
|
Promise.prototype._setRejected = function () {
|
|
this._bitField = this._bitField | 16777216;
|
|
this._fireEvent("promiseRejected", this);
|
|
};
|
|
|
|
Promise.prototype._setFollowing = function () {
|
|
this._bitField = this._bitField | 67108864;
|
|
this._fireEvent("promiseResolved", this);
|
|
};
|
|
|
|
Promise.prototype._setIsFinal = function () {
|
|
this._bitField = this._bitField | 4194304;
|
|
};
|
|
|
|
Promise.prototype._isFinal = function () {
|
|
return (this._bitField & 4194304) > 0;
|
|
};
|
|
|
|
Promise.prototype._unsetCancelled = function() {
|
|
this._bitField = this._bitField & (~65536);
|
|
};
|
|
|
|
Promise.prototype._setCancelled = function() {
|
|
this._bitField = this._bitField | 65536;
|
|
this._fireEvent("promiseCancelled", this);
|
|
};
|
|
|
|
Promise.prototype._setWillBeCancelled = function() {
|
|
this._bitField = this._bitField | 8388608;
|
|
};
|
|
|
|
Promise.prototype._setAsyncGuaranteed = function() {
|
|
if (async.hasCustomScheduler()) return;
|
|
this._bitField = this._bitField | 134217728;
|
|
};
|
|
|
|
Promise.prototype._receiverAt = function (index) {
|
|
var ret = index === 0 ? this._receiver0 : this[
|
|
index * 4 - 4 + 3];
|
|
if (ret === UNDEFINED_BINDING) {
|
|
return undefined;
|
|
} else if (ret === undefined && this._isBound()) {
|
|
return this._boundValue();
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
Promise.prototype._promiseAt = function (index) {
|
|
return this[
|
|
index * 4 - 4 + 2];
|
|
};
|
|
|
|
Promise.prototype._fulfillmentHandlerAt = function (index) {
|
|
return this[
|
|
index * 4 - 4 + 0];
|
|
};
|
|
|
|
Promise.prototype._rejectionHandlerAt = function (index) {
|
|
return this[
|
|
index * 4 - 4 + 1];
|
|
};
|
|
|
|
Promise.prototype._boundValue = function() {};
|
|
|
|
Promise.prototype._migrateCallback0 = function (follower) {
|
|
var bitField = follower._bitField;
|
|
var fulfill = follower._fulfillmentHandler0;
|
|
var reject = follower._rejectionHandler0;
|
|
var promise = follower._promise0;
|
|
var receiver = follower._receiverAt(0);
|
|
if (receiver === undefined) receiver = UNDEFINED_BINDING;
|
|
this._addCallbacks(fulfill, reject, promise, receiver, null);
|
|
};
|
|
|
|
Promise.prototype._migrateCallbackAt = function (follower, index) {
|
|
var fulfill = follower._fulfillmentHandlerAt(index);
|
|
var reject = follower._rejectionHandlerAt(index);
|
|
var promise = follower._promiseAt(index);
|
|
var receiver = follower._receiverAt(index);
|
|
if (receiver === undefined) receiver = UNDEFINED_BINDING;
|
|
this._addCallbacks(fulfill, reject, promise, receiver, null);
|
|
};
|
|
|
|
Promise.prototype._addCallbacks = function (
|
|
fulfill,
|
|
reject,
|
|
promise,
|
|
receiver,
|
|
domain
|
|
) {
|
|
var index = this._length();
|
|
|
|
if (index >= 65535 - 4) {
|
|
index = 0;
|
|
this._setLength(0);
|
|
}
|
|
|
|
if (index === 0) {
|
|
this._promise0 = promise;
|
|
this._receiver0 = receiver;
|
|
if (typeof fulfill === "function") {
|
|
this._fulfillmentHandler0 =
|
|
domain === null ? fulfill : util.domainBind(domain, fulfill);
|
|
}
|
|
if (typeof reject === "function") {
|
|
this._rejectionHandler0 =
|
|
domain === null ? reject : util.domainBind(domain, reject);
|
|
}
|
|
} else {
|
|
var base = index * 4 - 4;
|
|
this[base + 2] = promise;
|
|
this[base + 3] = receiver;
|
|
if (typeof fulfill === "function") {
|
|
this[base + 0] =
|
|
domain === null ? fulfill : util.domainBind(domain, fulfill);
|
|
}
|
|
if (typeof reject === "function") {
|
|
this[base + 1] =
|
|
domain === null ? reject : util.domainBind(domain, reject);
|
|
}
|
|
}
|
|
this._setLength(index + 1);
|
|
return index;
|
|
};
|
|
|
|
Promise.prototype._proxy = function (proxyable, arg) {
|
|
this._addCallbacks(undefined, undefined, arg, proxyable, null);
|
|
};
|
|
|
|
Promise.prototype._resolveCallback = function(value, shouldBind) {
|
|
if (((this._bitField & 117506048) !== 0)) return;
|
|
if (value === this)
|
|
return this._rejectCallback(makeSelfResolutionError(), false);
|
|
var maybePromise = tryConvertToPromise(value, this);
|
|
if (!(maybePromise instanceof Promise)) return this._fulfill(value);
|
|
|
|
if (shouldBind) this._propagateFrom(maybePromise, 2);
|
|
|
|
var promise = maybePromise._target();
|
|
|
|
if (promise === this) {
|
|
this._reject(makeSelfResolutionError());
|
|
return;
|
|
}
|
|
|
|
var bitField = promise._bitField;
|
|
if (((bitField & 50397184) === 0)) {
|
|
var len = this._length();
|
|
if (len > 0) promise._migrateCallback0(this);
|
|
for (var i = 1; i < len; ++i) {
|
|
promise._migrateCallbackAt(this, i);
|
|
}
|
|
this._setFollowing();
|
|
this._setLength(0);
|
|
this._setFollowee(promise);
|
|
} else if (((bitField & 33554432) !== 0)) {
|
|
this._fulfill(promise._value());
|
|
} else if (((bitField & 16777216) !== 0)) {
|
|
this._reject(promise._reason());
|
|
} else {
|
|
var reason = new CancellationError("late cancellation observer");
|
|
promise._attachExtraTrace(reason);
|
|
this._reject(reason);
|
|
}
|
|
};
|
|
|
|
Promise.prototype._rejectCallback =
|
|
function(reason, synchronous, ignoreNonErrorWarnings) {
|
|
var trace = util.ensureErrorObject(reason);
|
|
var hasStack = trace === reason;
|
|
if (!hasStack && !ignoreNonErrorWarnings && debug.warnings()) {
|
|
var message = "a promise was rejected with a non-error: " +
|
|
util.classString(reason);
|
|
this._warn(message, true);
|
|
}
|
|
this._attachExtraTrace(trace, synchronous ? hasStack : false);
|
|
this._reject(reason);
|
|
};
|
|
|
|
Promise.prototype._resolveFromExecutor = function (executor) {
|
|
if (executor === INTERNAL) return;
|
|
var promise = this;
|
|
this._captureStackTrace();
|
|
this._pushContext();
|
|
var synchronous = true;
|
|
var r = this._execute(executor, function(value) {
|
|
promise._resolveCallback(value);
|
|
}, function (reason) {
|
|
promise._rejectCallback(reason, synchronous);
|
|
});
|
|
synchronous = false;
|
|
this._popContext();
|
|
|
|
if (r !== undefined) {
|
|
promise._rejectCallback(r, true);
|
|
}
|
|
};
|
|
|
|
Promise.prototype._settlePromiseFromHandler = function (
|
|
handler, receiver, value, promise
|
|
) {
|
|
var bitField = promise._bitField;
|
|
if (((bitField & 65536) !== 0)) return;
|
|
promise._pushContext();
|
|
var x;
|
|
if (receiver === APPLY) {
|
|
if (!value || typeof value.length !== "number") {
|
|
x = errorObj;
|
|
x.e = new TypeError("cannot .spread() a non-array: " +
|
|
util.classString(value));
|
|
} else {
|
|
x = tryCatch(handler).apply(this._boundValue(), value);
|
|
}
|
|
} else {
|
|
x = tryCatch(handler).call(receiver, value);
|
|
}
|
|
var promiseCreated = promise._popContext();
|
|
bitField = promise._bitField;
|
|
if (((bitField & 65536) !== 0)) return;
|
|
|
|
if (x === NEXT_FILTER) {
|
|
promise._reject(value);
|
|
} else if (x === errorObj) {
|
|
promise._rejectCallback(x.e, false);
|
|
} else {
|
|
debug.checkForgottenReturns(x, promiseCreated, "", promise, this);
|
|
promise._resolveCallback(x);
|
|
}
|
|
};
|
|
|
|
Promise.prototype._target = function() {
|
|
var ret = this;
|
|
while (ret._isFollowing()) ret = ret._followee();
|
|
return ret;
|
|
};
|
|
|
|
Promise.prototype._followee = function() {
|
|
return this._rejectionHandler0;
|
|
};
|
|
|
|
Promise.prototype._setFollowee = function(promise) {
|
|
this._rejectionHandler0 = promise;
|
|
};
|
|
|
|
Promise.prototype._settlePromise = function(promise, handler, receiver, value) {
|
|
var isPromise = promise instanceof Promise;
|
|
var bitField = this._bitField;
|
|
var asyncGuaranteed = ((bitField & 134217728) !== 0);
|
|
if (((bitField & 65536) !== 0)) {
|
|
if (isPromise) promise._invokeInternalOnCancel();
|
|
|
|
if (receiver instanceof PassThroughHandlerContext &&
|
|
receiver.isFinallyHandler()) {
|
|
receiver.cancelPromise = promise;
|
|
if (tryCatch(handler).call(receiver, value) === errorObj) {
|
|
promise._reject(errorObj.e);
|
|
}
|
|
} else if (handler === reflectHandler) {
|
|
promise._fulfill(reflectHandler.call(receiver));
|
|
} else if (receiver instanceof Proxyable) {
|
|
receiver._promiseCancelled(promise);
|
|
} else if (isPromise || promise instanceof PromiseArray) {
|
|
promise._cancel();
|
|
} else {
|
|
receiver.cancel();
|
|
}
|
|
} else if (typeof handler === "function") {
|
|
if (!isPromise) {
|
|
handler.call(receiver, value, promise);
|
|
} else {
|
|
if (asyncGuaranteed) promise._setAsyncGuaranteed();
|
|
this._settlePromiseFromHandler(handler, receiver, value, promise);
|
|
}
|
|
} else if (receiver instanceof Proxyable) {
|
|
if (!receiver._isResolved()) {
|
|
if (((bitField & 33554432) !== 0)) {
|
|
receiver._promiseFulfilled(value, promise);
|
|
} else {
|
|
receiver._promiseRejected(value, promise);
|
|
}
|
|
}
|
|
} else if (isPromise) {
|
|
if (asyncGuaranteed) promise._setAsyncGuaranteed();
|
|
if (((bitField & 33554432) !== 0)) {
|
|
promise._fulfill(value);
|
|
} else {
|
|
promise._reject(value);
|
|
}
|
|
}
|
|
};
|
|
|
|
Promise.prototype._settlePromiseLateCancellationObserver = function(ctx) {
|
|
var handler = ctx.handler;
|
|
var promise = ctx.promise;
|
|
var receiver = ctx.receiver;
|
|
var value = ctx.value;
|
|
if (typeof handler === "function") {
|
|
if (!(promise instanceof Promise)) {
|
|
handler.call(receiver, value, promise);
|
|
} else {
|
|
this._settlePromiseFromHandler(handler, receiver, value, promise);
|
|
}
|
|
} else if (promise instanceof Promise) {
|
|
promise._reject(value);
|
|
}
|
|
};
|
|
|
|
Promise.prototype._settlePromiseCtx = function(ctx) {
|
|
this._settlePromise(ctx.promise, ctx.handler, ctx.receiver, ctx.value);
|
|
};
|
|
|
|
Promise.prototype._settlePromise0 = function(handler, value, bitField) {
|
|
var promise = this._promise0;
|
|
var receiver = this._receiverAt(0);
|
|
this._promise0 = undefined;
|
|
this._receiver0 = undefined;
|
|
this._settlePromise(promise, handler, receiver, value);
|
|
};
|
|
|
|
Promise.prototype._clearCallbackDataAtIndex = function(index) {
|
|
var base = index * 4 - 4;
|
|
this[base + 2] =
|
|
this[base + 3] =
|
|
this[base + 0] =
|
|
this[base + 1] = undefined;
|
|
};
|
|
|
|
Promise.prototype._fulfill = function (value) {
|
|
var bitField = this._bitField;
|
|
if (((bitField & 117506048) >>> 16)) return;
|
|
if (value === this) {
|
|
var err = makeSelfResolutionError();
|
|
this._attachExtraTrace(err);
|
|
return this._reject(err);
|
|
}
|
|
this._setFulfilled();
|
|
this._rejectionHandler0 = value;
|
|
|
|
if ((bitField & 65535) > 0) {
|
|
if (((bitField & 134217728) !== 0)) {
|
|
this._settlePromises();
|
|
} else {
|
|
async.settlePromises(this);
|
|
}
|
|
}
|
|
};
|
|
|
|
Promise.prototype._reject = function (reason) {
|
|
var bitField = this._bitField;
|
|
if (((bitField & 117506048) >>> 16)) return;
|
|
this._setRejected();
|
|
this._fulfillmentHandler0 = reason;
|
|
|
|
if (this._isFinal()) {
|
|
return async.fatalError(reason, util.isNode);
|
|
}
|
|
|
|
if ((bitField & 65535) > 0) {
|
|
async.settlePromises(this);
|
|
} else {
|
|
this._ensurePossibleRejectionHandled();
|
|
}
|
|
};
|
|
|
|
Promise.prototype._fulfillPromises = function (len, value) {
|
|
for (var i = 1; i < len; i++) {
|
|
var handler = this._fulfillmentHandlerAt(i);
|
|
var promise = this._promiseAt(i);
|
|
var receiver = this._receiverAt(i);
|
|
this._clearCallbackDataAtIndex(i);
|
|
this._settlePromise(promise, handler, receiver, value);
|
|
}
|
|
};
|
|
|
|
Promise.prototype._rejectPromises = function (len, reason) {
|
|
for (var i = 1; i < len; i++) {
|
|
var handler = this._rejectionHandlerAt(i);
|
|
var promise = this._promiseAt(i);
|
|
var receiver = this._receiverAt(i);
|
|
this._clearCallbackDataAtIndex(i);
|
|
this._settlePromise(promise, handler, receiver, reason);
|
|
}
|
|
};
|
|
|
|
Promise.prototype._settlePromises = function () {
|
|
var bitField = this._bitField;
|
|
var len = (bitField & 65535);
|
|
|
|
if (len > 0) {
|
|
if (((bitField & 16842752) !== 0)) {
|
|
var reason = this._fulfillmentHandler0;
|
|
this._settlePromise0(this._rejectionHandler0, reason, bitField);
|
|
this._rejectPromises(len, reason);
|
|
} else {
|
|
var value = this._rejectionHandler0;
|
|
this._settlePromise0(this._fulfillmentHandler0, value, bitField);
|
|
this._fulfillPromises(len, value);
|
|
}
|
|
this._setLength(0);
|
|
}
|
|
this._clearCancellationData();
|
|
};
|
|
|
|
Promise.prototype._settledValue = function() {
|
|
var bitField = this._bitField;
|
|
if (((bitField & 33554432) !== 0)) {
|
|
return this._rejectionHandler0;
|
|
} else if (((bitField & 16777216) !== 0)) {
|
|
return this._fulfillmentHandler0;
|
|
}
|
|
};
|
|
|
|
function deferResolve(v) {this.promise._resolveCallback(v);}
|
|
function deferReject(v) {this.promise._rejectCallback(v, false);}
|
|
|
|
Promise.defer = Promise.pending = function() {
|
|
debug.deprecated("Promise.defer", "new Promise");
|
|
var promise = new Promise(INTERNAL);
|
|
return {
|
|
promise: promise,
|
|
resolve: deferResolve,
|
|
reject: deferReject
|
|
};
|
|
};
|
|
|
|
util.notEnumerableProp(Promise,
|
|
"_makeSelfResolutionError",
|
|
makeSelfResolutionError);
|
|
|
|
_dereq_("./method")(Promise, INTERNAL, tryConvertToPromise, apiRejection,
|
|
debug);
|
|
_dereq_("./bind")(Promise, INTERNAL, tryConvertToPromise, debug);
|
|
_dereq_("./cancel")(Promise, PromiseArray, apiRejection, debug);
|
|
_dereq_("./direct_resolve")(Promise);
|
|
_dereq_("./synchronous_inspection")(Promise);
|
|
_dereq_("./join")(
|
|
Promise, PromiseArray, tryConvertToPromise, INTERNAL, async, getDomain);
|
|
Promise.Promise = Promise;
|
|
Promise.version = "3.5.1";
|
|
_dereq_('./map.js')(Promise, PromiseArray, apiRejection, tryConvertToPromise, INTERNAL, debug);
|
|
_dereq_('./call_get.js')(Promise);
|
|
_dereq_('./using.js')(Promise, apiRejection, tryConvertToPromise, createContext, INTERNAL, debug);
|
|
_dereq_('./timers.js')(Promise, INTERNAL, debug);
|
|
_dereq_('./generators.js')(Promise, apiRejection, INTERNAL, tryConvertToPromise, Proxyable, debug);
|
|
_dereq_('./nodeify.js')(Promise);
|
|
_dereq_('./promisify.js')(Promise, INTERNAL);
|
|
_dereq_('./props.js')(Promise, PromiseArray, tryConvertToPromise, apiRejection);
|
|
_dereq_('./race.js')(Promise, INTERNAL, tryConvertToPromise, apiRejection);
|
|
_dereq_('./reduce.js')(Promise, PromiseArray, apiRejection, tryConvertToPromise, INTERNAL, debug);
|
|
_dereq_('./settle.js')(Promise, PromiseArray, debug);
|
|
_dereq_('./some.js')(Promise, PromiseArray, apiRejection);
|
|
_dereq_('./filter.js')(Promise, INTERNAL);
|
|
_dereq_('./each.js')(Promise, INTERNAL);
|
|
_dereq_('./any.js')(Promise);
|
|
|
|
util.toFastProperties(Promise);
|
|
util.toFastProperties(Promise.prototype);
|
|
function fillTypes(value) {
|
|
var p = new Promise(INTERNAL);
|
|
p._fulfillmentHandler0 = value;
|
|
p._rejectionHandler0 = value;
|
|
p._promise0 = value;
|
|
p._receiver0 = value;
|
|
}
|
|
// Complete slack tracking, opt out of field-type tracking and
|
|
// stabilize map
|
|
fillTypes({a: 1});
|
|
fillTypes({b: 2});
|
|
fillTypes({c: 3});
|
|
fillTypes(1);
|
|
fillTypes(function(){});
|
|
fillTypes(undefined);
|
|
fillTypes(false);
|
|
fillTypes(new Promise(INTERNAL));
|
|
debug.setBounds(Async.firstLineError, util.lastLineError);
|
|
return Promise;
|
|
|
|
};
|
|
|
|
},{"./any.js":1,"./async":2,"./bind":3,"./call_get.js":5,"./cancel":6,"./catch_filter":7,"./context":8,"./debuggability":9,"./direct_resolve":10,"./each.js":11,"./errors":12,"./es5":13,"./filter.js":14,"./finally":15,"./generators.js":16,"./join":17,"./map.js":18,"./method":19,"./nodeback":20,"./nodeify.js":21,"./promise_array":23,"./promisify.js":24,"./props.js":25,"./race.js":27,"./reduce.js":28,"./settle.js":30,"./some.js":31,"./synchronous_inspection":32,"./thenables":33,"./timers.js":34,"./using.js":35,"./util":36}],23:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise, INTERNAL, tryConvertToPromise,
|
|
apiRejection, Proxyable) {
|
|
var util = _dereq_("./util");
|
|
var isArray = util.isArray;
|
|
|
|
function toResolutionValue(val) {
|
|
switch(val) {
|
|
case -2: return [];
|
|
case -3: return {};
|
|
case -6: return new Map();
|
|
}
|
|
}
|
|
|
|
function PromiseArray(values) {
|
|
var promise = this._promise = new Promise(INTERNAL);
|
|
if (values instanceof Promise) {
|
|
promise._propagateFrom(values, 3);
|
|
}
|
|
promise._setOnCancel(this);
|
|
this._values = values;
|
|
this._length = 0;
|
|
this._totalResolved = 0;
|
|
this._init(undefined, -2);
|
|
}
|
|
util.inherits(PromiseArray, Proxyable);
|
|
|
|
PromiseArray.prototype.length = function () {
|
|
return this._length;
|
|
};
|
|
|
|
PromiseArray.prototype.promise = function () {
|
|
return this._promise;
|
|
};
|
|
|
|
PromiseArray.prototype._init = function init(_, resolveValueIfEmpty) {
|
|
var values = tryConvertToPromise(this._values, this._promise);
|
|
if (values instanceof Promise) {
|
|
values = values._target();
|
|
var bitField = values._bitField;
|
|
;
|
|
this._values = values;
|
|
|
|
if (((bitField & 50397184) === 0)) {
|
|
this._promise._setAsyncGuaranteed();
|
|
return values._then(
|
|
init,
|
|
this._reject,
|
|
undefined,
|
|
this,
|
|
resolveValueIfEmpty
|
|
);
|
|
} else if (((bitField & 33554432) !== 0)) {
|
|
values = values._value();
|
|
} else if (((bitField & 16777216) !== 0)) {
|
|
return this._reject(values._reason());
|
|
} else {
|
|
return this._cancel();
|
|
}
|
|
}
|
|
values = util.asArray(values);
|
|
if (values === null) {
|
|
var err = apiRejection(
|
|
"expecting an array or an iterable object but got " + util.classString(values)).reason();
|
|
this._promise._rejectCallback(err, false);
|
|
return;
|
|
}
|
|
|
|
if (values.length === 0) {
|
|
if (resolveValueIfEmpty === -5) {
|
|
this._resolveEmptyArray();
|
|
}
|
|
else {
|
|
this._resolve(toResolutionValue(resolveValueIfEmpty));
|
|
}
|
|
return;
|
|
}
|
|
this._iterate(values);
|
|
};
|
|
|
|
PromiseArray.prototype._iterate = function(values) {
|
|
var len = this.getActualLength(values.length);
|
|
this._length = len;
|
|
this._values = this.shouldCopyValues() ? new Array(len) : this._values;
|
|
var result = this._promise;
|
|
var isResolved = false;
|
|
var bitField = null;
|
|
for (var i = 0; i < len; ++i) {
|
|
var maybePromise = tryConvertToPromise(values[i], result);
|
|
|
|
if (maybePromise instanceof Promise) {
|
|
maybePromise = maybePromise._target();
|
|
bitField = maybePromise._bitField;
|
|
} else {
|
|
bitField = null;
|
|
}
|
|
|
|
if (isResolved) {
|
|
if (bitField !== null) {
|
|
maybePromise.suppressUnhandledRejections();
|
|
}
|
|
} else if (bitField !== null) {
|
|
if (((bitField & 50397184) === 0)) {
|
|
maybePromise._proxy(this, i);
|
|
this._values[i] = maybePromise;
|
|
} else if (((bitField & 33554432) !== 0)) {
|
|
isResolved = this._promiseFulfilled(maybePromise._value(), i);
|
|
} else if (((bitField & 16777216) !== 0)) {
|
|
isResolved = this._promiseRejected(maybePromise._reason(), i);
|
|
} else {
|
|
isResolved = this._promiseCancelled(i);
|
|
}
|
|
} else {
|
|
isResolved = this._promiseFulfilled(maybePromise, i);
|
|
}
|
|
}
|
|
if (!isResolved) result._setAsyncGuaranteed();
|
|
};
|
|
|
|
PromiseArray.prototype._isResolved = function () {
|
|
return this._values === null;
|
|
};
|
|
|
|
PromiseArray.prototype._resolve = function (value) {
|
|
this._values = null;
|
|
this._promise._fulfill(value);
|
|
};
|
|
|
|
PromiseArray.prototype._cancel = function() {
|
|
if (this._isResolved() || !this._promise._isCancellable()) return;
|
|
this._values = null;
|
|
this._promise._cancel();
|
|
};
|
|
|
|
PromiseArray.prototype._reject = function (reason) {
|
|
this._values = null;
|
|
this._promise._rejectCallback(reason, false);
|
|
};
|
|
|
|
PromiseArray.prototype._promiseFulfilled = function (value, index) {
|
|
this._values[index] = value;
|
|
var totalResolved = ++this._totalResolved;
|
|
if (totalResolved >= this._length) {
|
|
this._resolve(this._values);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
PromiseArray.prototype._promiseCancelled = function() {
|
|
this._cancel();
|
|
return true;
|
|
};
|
|
|
|
PromiseArray.prototype._promiseRejected = function (reason) {
|
|
this._totalResolved++;
|
|
this._reject(reason);
|
|
return true;
|
|
};
|
|
|
|
PromiseArray.prototype._resultCancelled = function() {
|
|
if (this._isResolved()) return;
|
|
var values = this._values;
|
|
this._cancel();
|
|
if (values instanceof Promise) {
|
|
values.cancel();
|
|
} else {
|
|
for (var i = 0; i < values.length; ++i) {
|
|
if (values[i] instanceof Promise) {
|
|
values[i].cancel();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
PromiseArray.prototype.shouldCopyValues = function () {
|
|
return true;
|
|
};
|
|
|
|
PromiseArray.prototype.getActualLength = function (len) {
|
|
return len;
|
|
};
|
|
|
|
return PromiseArray;
|
|
};
|
|
|
|
},{"./util":36}],24:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise, INTERNAL) {
|
|
var THIS = {};
|
|
var util = _dereq_("./util");
|
|
var nodebackForPromise = _dereq_("./nodeback");
|
|
var withAppended = util.withAppended;
|
|
var maybeWrapAsError = util.maybeWrapAsError;
|
|
var canEvaluate = util.canEvaluate;
|
|
var TypeError = _dereq_("./errors").TypeError;
|
|
var defaultSuffix = "Async";
|
|
var defaultPromisified = {__isPromisified__: true};
|
|
var noCopyProps = [
|
|
"arity", "length",
|
|
"name",
|
|
"arguments",
|
|
"caller",
|
|
"callee",
|
|
"prototype",
|
|
"__isPromisified__"
|
|
];
|
|
var noCopyPropsPattern = new RegExp("^(?:" + noCopyProps.join("|") + ")$");
|
|
|
|
var defaultFilter = function(name) {
|
|
return util.isIdentifier(name) &&
|
|
name.charAt(0) !== "_" &&
|
|
name !== "constructor";
|
|
};
|
|
|
|
function propsFilter(key) {
|
|
return !noCopyPropsPattern.test(key);
|
|
}
|
|
|
|
function isPromisified(fn) {
|
|
try {
|
|
return fn.__isPromisified__ === true;
|
|
}
|
|
catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function hasPromisified(obj, key, suffix) {
|
|
var val = util.getDataPropertyOrDefault(obj, key + suffix,
|
|
defaultPromisified);
|
|
return val ? isPromisified(val) : false;
|
|
}
|
|
function checkValid(ret, suffix, suffixRegexp) {
|
|
for (var i = 0; i < ret.length; i += 2) {
|
|
var key = ret[i];
|
|
if (suffixRegexp.test(key)) {
|
|
var keyWithoutAsyncSuffix = key.replace(suffixRegexp, "");
|
|
for (var j = 0; j < ret.length; j += 2) {
|
|
if (ret[j] === keyWithoutAsyncSuffix) {
|
|
throw new TypeError("Cannot promisify an API that has normal methods with '%s'-suffix\u000a\u000a See http://goo.gl/MqrFmX\u000a"
|
|
.replace("%s", suffix));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function promisifiableMethods(obj, suffix, suffixRegexp, filter) {
|
|
var keys = util.inheritedDataKeys(obj);
|
|
var ret = [];
|
|
for (var i = 0; i < keys.length; ++i) {
|
|
var key = keys[i];
|
|
var value = obj[key];
|
|
var passesDefaultFilter = filter === defaultFilter
|
|
? true : defaultFilter(key, value, obj);
|
|
if (typeof value === "function" &&
|
|
!isPromisified(value) &&
|
|
!hasPromisified(obj, key, suffix) &&
|
|
filter(key, value, obj, passesDefaultFilter)) {
|
|
ret.push(key, value);
|
|
}
|
|
}
|
|
checkValid(ret, suffix, suffixRegexp);
|
|
return ret;
|
|
}
|
|
|
|
var escapeIdentRegex = function(str) {
|
|
return str.replace(/([$])/, "\\$");
|
|
};
|
|
|
|
var makeNodePromisifiedEval;
|
|
if (!true) {
|
|
var switchCaseArgumentOrder = function(likelyArgumentCount) {
|
|
var ret = [likelyArgumentCount];
|
|
var min = Math.max(0, likelyArgumentCount - 1 - 3);
|
|
for(var i = likelyArgumentCount - 1; i >= min; --i) {
|
|
ret.push(i);
|
|
}
|
|
for(var i = likelyArgumentCount + 1; i <= 3; ++i) {
|
|
ret.push(i);
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
var argumentSequence = function(argumentCount) {
|
|
return util.filledRange(argumentCount, "_arg", "");
|
|
};
|
|
|
|
var parameterDeclaration = function(parameterCount) {
|
|
return util.filledRange(
|
|
Math.max(parameterCount, 3), "_arg", "");
|
|
};
|
|
|
|
var parameterCount = function(fn) {
|
|
if (typeof fn.length === "number") {
|
|
return Math.max(Math.min(fn.length, 1023 + 1), 0);
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
makeNodePromisifiedEval =
|
|
function(callback, receiver, originalName, fn, _, multiArgs) {
|
|
var newParameterCount = Math.max(0, parameterCount(fn) - 1);
|
|
var argumentOrder = switchCaseArgumentOrder(newParameterCount);
|
|
var shouldProxyThis = typeof callback === "string" || receiver === THIS;
|
|
|
|
function generateCallForArgumentCount(count) {
|
|
var args = argumentSequence(count).join(", ");
|
|
var comma = count > 0 ? ", " : "";
|
|
var ret;
|
|
if (shouldProxyThis) {
|
|
ret = "ret = callback.call(this, {{args}}, nodeback); break;\n";
|
|
} else {
|
|
ret = receiver === undefined
|
|
? "ret = callback({{args}}, nodeback); break;\n"
|
|
: "ret = callback.call(receiver, {{args}}, nodeback); break;\n";
|
|
}
|
|
return ret.replace("{{args}}", args).replace(", ", comma);
|
|
}
|
|
|
|
function generateArgumentSwitchCase() {
|
|
var ret = "";
|
|
for (var i = 0; i < argumentOrder.length; ++i) {
|
|
ret += "case " + argumentOrder[i] +":" +
|
|
generateCallForArgumentCount(argumentOrder[i]);
|
|
}
|
|
|
|
ret += " \n\
|
|
default: \n\
|
|
var args = new Array(len + 1); \n\
|
|
var i = 0; \n\
|
|
for (var i = 0; i < len; ++i) { \n\
|
|
args[i] = arguments[i]; \n\
|
|
} \n\
|
|
args[i] = nodeback; \n\
|
|
[CodeForCall] \n\
|
|
break; \n\
|
|
".replace("[CodeForCall]", (shouldProxyThis
|
|
? "ret = callback.apply(this, args);\n"
|
|
: "ret = callback.apply(receiver, args);\n"));
|
|
return ret;
|
|
}
|
|
|
|
var getFunctionCode = typeof callback === "string"
|
|
? ("this != null ? this['"+callback+"'] : fn")
|
|
: "fn";
|
|
var body = "'use strict'; \n\
|
|
var ret = function (Parameters) { \n\
|
|
'use strict'; \n\
|
|
var len = arguments.length; \n\
|
|
var promise = new Promise(INTERNAL); \n\
|
|
promise._captureStackTrace(); \n\
|
|
var nodeback = nodebackForPromise(promise, " + multiArgs + "); \n\
|
|
var ret; \n\
|
|
var callback = tryCatch([GetFunctionCode]); \n\
|
|
switch(len) { \n\
|
|
[CodeForSwitchCase] \n\
|
|
} \n\
|
|
if (ret === errorObj) { \n\
|
|
promise._rejectCallback(maybeWrapAsError(ret.e), true, true);\n\
|
|
} \n\
|
|
if (!promise._isFateSealed()) promise._setAsyncGuaranteed(); \n\
|
|
return promise; \n\
|
|
}; \n\
|
|
notEnumerableProp(ret, '__isPromisified__', true); \n\
|
|
return ret; \n\
|
|
".replace("[CodeForSwitchCase]", generateArgumentSwitchCase())
|
|
.replace("[GetFunctionCode]", getFunctionCode);
|
|
body = body.replace("Parameters", parameterDeclaration(newParameterCount));
|
|
return new Function("Promise",
|
|
"fn",
|
|
"receiver",
|
|
"withAppended",
|
|
"maybeWrapAsError",
|
|
"nodebackForPromise",
|
|
"tryCatch",
|
|
"errorObj",
|
|
"notEnumerableProp",
|
|
"INTERNAL",
|
|
body)(
|
|
Promise,
|
|
fn,
|
|
receiver,
|
|
withAppended,
|
|
maybeWrapAsError,
|
|
nodebackForPromise,
|
|
util.tryCatch,
|
|
util.errorObj,
|
|
util.notEnumerableProp,
|
|
INTERNAL);
|
|
};
|
|
}
|
|
|
|
function makeNodePromisifiedClosure(callback, receiver, _, fn, __, multiArgs) {
|
|
var defaultThis = (function() {return this;})();
|
|
var method = callback;
|
|
if (typeof method === "string") {
|
|
callback = fn;
|
|
}
|
|
function promisified() {
|
|
var _receiver = receiver;
|
|
if (receiver === THIS) _receiver = this;
|
|
var promise = new Promise(INTERNAL);
|
|
promise._captureStackTrace();
|
|
var cb = typeof method === "string" && this !== defaultThis
|
|
? this[method] : callback;
|
|
var fn = nodebackForPromise(promise, multiArgs);
|
|
try {
|
|
cb.apply(_receiver, withAppended(arguments, fn));
|
|
} catch(e) {
|
|
promise._rejectCallback(maybeWrapAsError(e), true, true);
|
|
}
|
|
if (!promise._isFateSealed()) promise._setAsyncGuaranteed();
|
|
return promise;
|
|
}
|
|
util.notEnumerableProp(promisified, "__isPromisified__", true);
|
|
return promisified;
|
|
}
|
|
|
|
var makeNodePromisified = canEvaluate
|
|
? makeNodePromisifiedEval
|
|
: makeNodePromisifiedClosure;
|
|
|
|
function promisifyAll(obj, suffix, filter, promisifier, multiArgs) {
|
|
var suffixRegexp = new RegExp(escapeIdentRegex(suffix) + "$");
|
|
var methods =
|
|
promisifiableMethods(obj, suffix, suffixRegexp, filter);
|
|
|
|
for (var i = 0, len = methods.length; i < len; i+= 2) {
|
|
var key = methods[i];
|
|
var fn = methods[i+1];
|
|
var promisifiedKey = key + suffix;
|
|
if (promisifier === makeNodePromisified) {
|
|
obj[promisifiedKey] =
|
|
makeNodePromisified(key, THIS, key, fn, suffix, multiArgs);
|
|
} else {
|
|
var promisified = promisifier(fn, function() {
|
|
return makeNodePromisified(key, THIS, key,
|
|
fn, suffix, multiArgs);
|
|
});
|
|
util.notEnumerableProp(promisified, "__isPromisified__", true);
|
|
obj[promisifiedKey] = promisified;
|
|
}
|
|
}
|
|
util.toFastProperties(obj);
|
|
return obj;
|
|
}
|
|
|
|
function promisify(callback, receiver, multiArgs) {
|
|
return makeNodePromisified(callback, receiver, undefined,
|
|
callback, null, multiArgs);
|
|
}
|
|
|
|
Promise.promisify = function (fn, options) {
|
|
if (typeof fn !== "function") {
|
|
throw new TypeError("expecting a function but got " + util.classString(fn));
|
|
}
|
|
if (isPromisified(fn)) {
|
|
return fn;
|
|
}
|
|
options = Object(options);
|
|
var receiver = options.context === undefined ? THIS : options.context;
|
|
var multiArgs = !!options.multiArgs;
|
|
var ret = promisify(fn, receiver, multiArgs);
|
|
util.copyDescriptors(fn, ret, propsFilter);
|
|
return ret;
|
|
};
|
|
|
|
Promise.promisifyAll = function (target, options) {
|
|
if (typeof target !== "function" && typeof target !== "object") {
|
|
throw new TypeError("the target of promisifyAll must be an object or a function\u000a\u000a See http://goo.gl/MqrFmX\u000a");
|
|
}
|
|
options = Object(options);
|
|
var multiArgs = !!options.multiArgs;
|
|
var suffix = options.suffix;
|
|
if (typeof suffix !== "string") suffix = defaultSuffix;
|
|
var filter = options.filter;
|
|
if (typeof filter !== "function") filter = defaultFilter;
|
|
var promisifier = options.promisifier;
|
|
if (typeof promisifier !== "function") promisifier = makeNodePromisified;
|
|
|
|
if (!util.isIdentifier(suffix)) {
|
|
throw new RangeError("suffix must be a valid identifier\u000a\u000a See http://goo.gl/MqrFmX\u000a");
|
|
}
|
|
|
|
var keys = util.inheritedDataKeys(target);
|
|
for (var i = 0; i < keys.length; ++i) {
|
|
var value = target[keys[i]];
|
|
if (keys[i] !== "constructor" &&
|
|
util.isClass(value)) {
|
|
promisifyAll(value.prototype, suffix, filter, promisifier,
|
|
multiArgs);
|
|
promisifyAll(value, suffix, filter, promisifier, multiArgs);
|
|
}
|
|
}
|
|
|
|
return promisifyAll(target, suffix, filter, promisifier, multiArgs);
|
|
};
|
|
};
|
|
|
|
|
|
},{"./errors":12,"./nodeback":20,"./util":36}],25:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(
|
|
Promise, PromiseArray, tryConvertToPromise, apiRejection) {
|
|
var util = _dereq_("./util");
|
|
var isObject = util.isObject;
|
|
var es5 = _dereq_("./es5");
|
|
var Es6Map;
|
|
if (typeof Map === "function") Es6Map = Map;
|
|
|
|
var mapToEntries = (function() {
|
|
var index = 0;
|
|
var size = 0;
|
|
|
|
function extractEntry(value, key) {
|
|
this[index] = value;
|
|
this[index + size] = key;
|
|
index++;
|
|
}
|
|
|
|
return function mapToEntries(map) {
|
|
size = map.size;
|
|
index = 0;
|
|
var ret = new Array(map.size * 2);
|
|
map.forEach(extractEntry, ret);
|
|
return ret;
|
|
};
|
|
})();
|
|
|
|
var entriesToMap = function(entries) {
|
|
var ret = new Es6Map();
|
|
var length = entries.length / 2 | 0;
|
|
for (var i = 0; i < length; ++i) {
|
|
var key = entries[length + i];
|
|
var value = entries[i];
|
|
ret.set(key, value);
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
function PropertiesPromiseArray(obj) {
|
|
var isMap = false;
|
|
var entries;
|
|
if (Es6Map !== undefined && obj instanceof Es6Map) {
|
|
entries = mapToEntries(obj);
|
|
isMap = true;
|
|
} else {
|
|
var keys = es5.keys(obj);
|
|
var len = keys.length;
|
|
entries = new Array(len * 2);
|
|
for (var i = 0; i < len; ++i) {
|
|
var key = keys[i];
|
|
entries[i] = obj[key];
|
|
entries[i + len] = key;
|
|
}
|
|
}
|
|
this.constructor$(entries);
|
|
this._isMap = isMap;
|
|
this._init$(undefined, isMap ? -6 : -3);
|
|
}
|
|
util.inherits(PropertiesPromiseArray, PromiseArray);
|
|
|
|
PropertiesPromiseArray.prototype._init = function () {};
|
|
|
|
PropertiesPromiseArray.prototype._promiseFulfilled = function (value, index) {
|
|
this._values[index] = value;
|
|
var totalResolved = ++this._totalResolved;
|
|
if (totalResolved >= this._length) {
|
|
var val;
|
|
if (this._isMap) {
|
|
val = entriesToMap(this._values);
|
|
} else {
|
|
val = {};
|
|
var keyOffset = this.length();
|
|
for (var i = 0, len = this.length(); i < len; ++i) {
|
|
val[this._values[i + keyOffset]] = this._values[i];
|
|
}
|
|
}
|
|
this._resolve(val);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
PropertiesPromiseArray.prototype.shouldCopyValues = function () {
|
|
return false;
|
|
};
|
|
|
|
PropertiesPromiseArray.prototype.getActualLength = function (len) {
|
|
return len >> 1;
|
|
};
|
|
|
|
function props(promises) {
|
|
var ret;
|
|
var castValue = tryConvertToPromise(promises);
|
|
|
|
if (!isObject(castValue)) {
|
|
return apiRejection("cannot await properties of a non-object\u000a\u000a See http://goo.gl/MqrFmX\u000a");
|
|
} else if (castValue instanceof Promise) {
|
|
ret = castValue._then(
|
|
Promise.props, undefined, undefined, undefined, undefined);
|
|
} else {
|
|
ret = new PropertiesPromiseArray(castValue).promise();
|
|
}
|
|
|
|
if (castValue instanceof Promise) {
|
|
ret._propagateFrom(castValue, 2);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
Promise.prototype.props = function () {
|
|
return props(this);
|
|
};
|
|
|
|
Promise.props = function (promises) {
|
|
return props(promises);
|
|
};
|
|
};
|
|
|
|
},{"./es5":13,"./util":36}],26:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
function arrayMove(src, srcIndex, dst, dstIndex, len) {
|
|
for (var j = 0; j < len; ++j) {
|
|
dst[j + dstIndex] = src[j + srcIndex];
|
|
src[j + srcIndex] = void 0;
|
|
}
|
|
}
|
|
|
|
function Queue(capacity) {
|
|
this._capacity = capacity;
|
|
this._length = 0;
|
|
this._front = 0;
|
|
}
|
|
|
|
Queue.prototype._willBeOverCapacity = function (size) {
|
|
return this._capacity < size;
|
|
};
|
|
|
|
Queue.prototype._pushOne = function (arg) {
|
|
var length = this.length();
|
|
this._checkCapacity(length + 1);
|
|
var i = (this._front + length) & (this._capacity - 1);
|
|
this[i] = arg;
|
|
this._length = length + 1;
|
|
};
|
|
|
|
Queue.prototype.push = function (fn, receiver, arg) {
|
|
var length = this.length() + 3;
|
|
if (this._willBeOverCapacity(length)) {
|
|
this._pushOne(fn);
|
|
this._pushOne(receiver);
|
|
this._pushOne(arg);
|
|
return;
|
|
}
|
|
var j = this._front + length - 3;
|
|
this._checkCapacity(length);
|
|
var wrapMask = this._capacity - 1;
|
|
this[(j + 0) & wrapMask] = fn;
|
|
this[(j + 1) & wrapMask] = receiver;
|
|
this[(j + 2) & wrapMask] = arg;
|
|
this._length = length;
|
|
};
|
|
|
|
Queue.prototype.shift = function () {
|
|
var front = this._front,
|
|
ret = this[front];
|
|
|
|
this[front] = undefined;
|
|
this._front = (front + 1) & (this._capacity - 1);
|
|
this._length--;
|
|
return ret;
|
|
};
|
|
|
|
Queue.prototype.length = function () {
|
|
return this._length;
|
|
};
|
|
|
|
Queue.prototype._checkCapacity = function (size) {
|
|
if (this._capacity < size) {
|
|
this._resizeTo(this._capacity << 1);
|
|
}
|
|
};
|
|
|
|
Queue.prototype._resizeTo = function (capacity) {
|
|
var oldCapacity = this._capacity;
|
|
this._capacity = capacity;
|
|
var front = this._front;
|
|
var length = this._length;
|
|
var moveItemsCount = (front + length) & (oldCapacity - 1);
|
|
arrayMove(this, 0, this, oldCapacity, moveItemsCount);
|
|
};
|
|
|
|
module.exports = Queue;
|
|
|
|
},{}],27:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(
|
|
Promise, INTERNAL, tryConvertToPromise, apiRejection) {
|
|
var util = _dereq_("./util");
|
|
|
|
var raceLater = function (promise) {
|
|
return promise.then(function(array) {
|
|
return race(array, promise);
|
|
});
|
|
};
|
|
|
|
function race(promises, parent) {
|
|
var maybePromise = tryConvertToPromise(promises);
|
|
|
|
if (maybePromise instanceof Promise) {
|
|
return raceLater(maybePromise);
|
|
} else {
|
|
promises = util.asArray(promises);
|
|
if (promises === null)
|
|
return apiRejection("expecting an array or an iterable object but got " + util.classString(promises));
|
|
}
|
|
|
|
var ret = new Promise(INTERNAL);
|
|
if (parent !== undefined) {
|
|
ret._propagateFrom(parent, 3);
|
|
}
|
|
var fulfill = ret._fulfill;
|
|
var reject = ret._reject;
|
|
for (var i = 0, len = promises.length; i < len; ++i) {
|
|
var val = promises[i];
|
|
|
|
if (val === undefined && !(i in promises)) {
|
|
continue;
|
|
}
|
|
|
|
Promise.cast(val)._then(fulfill, reject, undefined, ret, null);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
Promise.race = function (promises) {
|
|
return race(promises, undefined);
|
|
};
|
|
|
|
Promise.prototype.race = function () {
|
|
return race(this, undefined);
|
|
};
|
|
|
|
};
|
|
|
|
},{"./util":36}],28:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise,
|
|
PromiseArray,
|
|
apiRejection,
|
|
tryConvertToPromise,
|
|
INTERNAL,
|
|
debug) {
|
|
var getDomain = Promise._getDomain;
|
|
var util = _dereq_("./util");
|
|
var tryCatch = util.tryCatch;
|
|
|
|
function ReductionPromiseArray(promises, fn, initialValue, _each) {
|
|
this.constructor$(promises);
|
|
var domain = getDomain();
|
|
this._fn = domain === null ? fn : util.domainBind(domain, fn);
|
|
if (initialValue !== undefined) {
|
|
initialValue = Promise.resolve(initialValue);
|
|
initialValue._attachCancellationCallback(this);
|
|
}
|
|
this._initialValue = initialValue;
|
|
this._currentCancellable = null;
|
|
if(_each === INTERNAL) {
|
|
this._eachValues = Array(this._length);
|
|
} else if (_each === 0) {
|
|
this._eachValues = null;
|
|
} else {
|
|
this._eachValues = undefined;
|
|
}
|
|
this._promise._captureStackTrace();
|
|
this._init$(undefined, -5);
|
|
}
|
|
util.inherits(ReductionPromiseArray, PromiseArray);
|
|
|
|
ReductionPromiseArray.prototype._gotAccum = function(accum) {
|
|
if (this._eachValues !== undefined &&
|
|
this._eachValues !== null &&
|
|
accum !== INTERNAL) {
|
|
this._eachValues.push(accum);
|
|
}
|
|
};
|
|
|
|
ReductionPromiseArray.prototype._eachComplete = function(value) {
|
|
if (this._eachValues !== null) {
|
|
this._eachValues.push(value);
|
|
}
|
|
return this._eachValues;
|
|
};
|
|
|
|
ReductionPromiseArray.prototype._init = function() {};
|
|
|
|
ReductionPromiseArray.prototype._resolveEmptyArray = function() {
|
|
this._resolve(this._eachValues !== undefined ? this._eachValues
|
|
: this._initialValue);
|
|
};
|
|
|
|
ReductionPromiseArray.prototype.shouldCopyValues = function () {
|
|
return false;
|
|
};
|
|
|
|
ReductionPromiseArray.prototype._resolve = function(value) {
|
|
this._promise._resolveCallback(value);
|
|
this._values = null;
|
|
};
|
|
|
|
ReductionPromiseArray.prototype._resultCancelled = function(sender) {
|
|
if (sender === this._initialValue) return this._cancel();
|
|
if (this._isResolved()) return;
|
|
this._resultCancelled$();
|
|
if (this._currentCancellable instanceof Promise) {
|
|
this._currentCancellable.cancel();
|
|
}
|
|
if (this._initialValue instanceof Promise) {
|
|
this._initialValue.cancel();
|
|
}
|
|
};
|
|
|
|
ReductionPromiseArray.prototype._iterate = function (values) {
|
|
this._values = values;
|
|
var value;
|
|
var i;
|
|
var length = values.length;
|
|
if (this._initialValue !== undefined) {
|
|
value = this._initialValue;
|
|
i = 0;
|
|
} else {
|
|
value = Promise.resolve(values[0]);
|
|
i = 1;
|
|
}
|
|
|
|
this._currentCancellable = value;
|
|
|
|
if (!value.isRejected()) {
|
|
for (; i < length; ++i) {
|
|
var ctx = {
|
|
accum: null,
|
|
value: values[i],
|
|
index: i,
|
|
length: length,
|
|
array: this
|
|
};
|
|
value = value._then(gotAccum, undefined, undefined, ctx, undefined);
|
|
}
|
|
}
|
|
|
|
if (this._eachValues !== undefined) {
|
|
value = value
|
|
._then(this._eachComplete, undefined, undefined, this, undefined);
|
|
}
|
|
value._then(completed, completed, undefined, value, this);
|
|
};
|
|
|
|
Promise.prototype.reduce = function (fn, initialValue) {
|
|
return reduce(this, fn, initialValue, null);
|
|
};
|
|
|
|
Promise.reduce = function (promises, fn, initialValue, _each) {
|
|
return reduce(promises, fn, initialValue, _each);
|
|
};
|
|
|
|
function completed(valueOrReason, array) {
|
|
if (this.isFulfilled()) {
|
|
array._resolve(valueOrReason);
|
|
} else {
|
|
array._reject(valueOrReason);
|
|
}
|
|
}
|
|
|
|
function reduce(promises, fn, initialValue, _each) {
|
|
if (typeof fn !== "function") {
|
|
return apiRejection("expecting a function but got " + util.classString(fn));
|
|
}
|
|
var array = new ReductionPromiseArray(promises, fn, initialValue, _each);
|
|
return array.promise();
|
|
}
|
|
|
|
function gotAccum(accum) {
|
|
this.accum = accum;
|
|
this.array._gotAccum(accum);
|
|
var value = tryConvertToPromise(this.value, this.array._promise);
|
|
if (value instanceof Promise) {
|
|
this.array._currentCancellable = value;
|
|
return value._then(gotValue, undefined, undefined, this, undefined);
|
|
} else {
|
|
return gotValue.call(this, value);
|
|
}
|
|
}
|
|
|
|
function gotValue(value) {
|
|
var array = this.array;
|
|
var promise = array._promise;
|
|
var fn = tryCatch(array._fn);
|
|
promise._pushContext();
|
|
var ret;
|
|
if (array._eachValues !== undefined) {
|
|
ret = fn.call(promise._boundValue(), value, this.index, this.length);
|
|
} else {
|
|
ret = fn.call(promise._boundValue(),
|
|
this.accum, value, this.index, this.length);
|
|
}
|
|
if (ret instanceof Promise) {
|
|
array._currentCancellable = ret;
|
|
}
|
|
var promiseCreated = promise._popContext();
|
|
debug.checkForgottenReturns(
|
|
ret,
|
|
promiseCreated,
|
|
array._eachValues !== undefined ? "Promise.each" : "Promise.reduce",
|
|
promise
|
|
);
|
|
return ret;
|
|
}
|
|
};
|
|
|
|
},{"./util":36}],29:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
var util = _dereq_("./util");
|
|
var schedule;
|
|
var noAsyncScheduler = function() {
|
|
throw new Error("No async scheduler available\u000a\u000a See http://goo.gl/MqrFmX\u000a");
|
|
};
|
|
var NativePromise = util.getNativePromise();
|
|
if (util.isNode && typeof MutationObserver === "undefined") {
|
|
var GlobalSetImmediate = global.setImmediate;
|
|
var ProcessNextTick = process.nextTick;
|
|
schedule = util.isRecentNode
|
|
? function(fn) { GlobalSetImmediate.call(global, fn); }
|
|
: function(fn) { ProcessNextTick.call(process, fn); };
|
|
} else if (typeof NativePromise === "function" &&
|
|
typeof NativePromise.resolve === "function") {
|
|
var nativePromise = NativePromise.resolve();
|
|
schedule = function(fn) {
|
|
nativePromise.then(fn);
|
|
};
|
|
} else if ((typeof MutationObserver !== "undefined") &&
|
|
!(typeof window !== "undefined" &&
|
|
window.navigator &&
|
|
(window.navigator.standalone || window.cordova))) {
|
|
schedule = (function() {
|
|
var div = document.createElement("div");
|
|
var opts = {attributes: true};
|
|
var toggleScheduled = false;
|
|
var div2 = document.createElement("div");
|
|
var o2 = new MutationObserver(function() {
|
|
div.classList.toggle("foo");
|
|
toggleScheduled = false;
|
|
});
|
|
o2.observe(div2, opts);
|
|
|
|
var scheduleToggle = function() {
|
|
if (toggleScheduled) return;
|
|
toggleScheduled = true;
|
|
div2.classList.toggle("foo");
|
|
};
|
|
|
|
return function schedule(fn) {
|
|
var o = new MutationObserver(function() {
|
|
o.disconnect();
|
|
fn();
|
|
});
|
|
o.observe(div, opts);
|
|
scheduleToggle();
|
|
};
|
|
})();
|
|
} else if (typeof setImmediate !== "undefined") {
|
|
schedule = function (fn) {
|
|
setImmediate(fn);
|
|
};
|
|
} else if (typeof setTimeout !== "undefined") {
|
|
schedule = function (fn) {
|
|
setTimeout(fn, 0);
|
|
};
|
|
} else {
|
|
schedule = noAsyncScheduler;
|
|
}
|
|
module.exports = schedule;
|
|
|
|
},{"./util":36}],30:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports =
|
|
function(Promise, PromiseArray, debug) {
|
|
var PromiseInspection = Promise.PromiseInspection;
|
|
var util = _dereq_("./util");
|
|
|
|
function SettledPromiseArray(values) {
|
|
this.constructor$(values);
|
|
}
|
|
util.inherits(SettledPromiseArray, PromiseArray);
|
|
|
|
SettledPromiseArray.prototype._promiseResolved = function (index, inspection) {
|
|
this._values[index] = inspection;
|
|
var totalResolved = ++this._totalResolved;
|
|
if (totalResolved >= this._length) {
|
|
this._resolve(this._values);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
SettledPromiseArray.prototype._promiseFulfilled = function (value, index) {
|
|
var ret = new PromiseInspection();
|
|
ret._bitField = 33554432;
|
|
ret._settledValueField = value;
|
|
return this._promiseResolved(index, ret);
|
|
};
|
|
SettledPromiseArray.prototype._promiseRejected = function (reason, index) {
|
|
var ret = new PromiseInspection();
|
|
ret._bitField = 16777216;
|
|
ret._settledValueField = reason;
|
|
return this._promiseResolved(index, ret);
|
|
};
|
|
|
|
Promise.settle = function (promises) {
|
|
debug.deprecated(".settle()", ".reflect()");
|
|
return new SettledPromiseArray(promises).promise();
|
|
};
|
|
|
|
Promise.prototype.settle = function () {
|
|
return Promise.settle(this);
|
|
};
|
|
};
|
|
|
|
},{"./util":36}],31:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports =
|
|
function(Promise, PromiseArray, apiRejection) {
|
|
var util = _dereq_("./util");
|
|
var RangeError = _dereq_("./errors").RangeError;
|
|
var AggregateError = _dereq_("./errors").AggregateError;
|
|
var isArray = util.isArray;
|
|
var CANCELLATION = {};
|
|
|
|
|
|
function SomePromiseArray(values) {
|
|
this.constructor$(values);
|
|
this._howMany = 0;
|
|
this._unwrap = false;
|
|
this._initialized = false;
|
|
}
|
|
util.inherits(SomePromiseArray, PromiseArray);
|
|
|
|
SomePromiseArray.prototype._init = function () {
|
|
if (!this._initialized) {
|
|
return;
|
|
}
|
|
if (this._howMany === 0) {
|
|
this._resolve([]);
|
|
return;
|
|
}
|
|
this._init$(undefined, -5);
|
|
var isArrayResolved = isArray(this._values);
|
|
if (!this._isResolved() &&
|
|
isArrayResolved &&
|
|
this._howMany > this._canPossiblyFulfill()) {
|
|
this._reject(this._getRangeError(this.length()));
|
|
}
|
|
};
|
|
|
|
SomePromiseArray.prototype.init = function () {
|
|
this._initialized = true;
|
|
this._init();
|
|
};
|
|
|
|
SomePromiseArray.prototype.setUnwrap = function () {
|
|
this._unwrap = true;
|
|
};
|
|
|
|
SomePromiseArray.prototype.howMany = function () {
|
|
return this._howMany;
|
|
};
|
|
|
|
SomePromiseArray.prototype.setHowMany = function (count) {
|
|
this._howMany = count;
|
|
};
|
|
|
|
SomePromiseArray.prototype._promiseFulfilled = function (value) {
|
|
this._addFulfilled(value);
|
|
if (this._fulfilled() === this.howMany()) {
|
|
this._values.length = this.howMany();
|
|
if (this.howMany() === 1 && this._unwrap) {
|
|
this._resolve(this._values[0]);
|
|
} else {
|
|
this._resolve(this._values);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
|
|
};
|
|
SomePromiseArray.prototype._promiseRejected = function (reason) {
|
|
this._addRejected(reason);
|
|
return this._checkOutcome();
|
|
};
|
|
|
|
SomePromiseArray.prototype._promiseCancelled = function () {
|
|
if (this._values instanceof Promise || this._values == null) {
|
|
return this._cancel();
|
|
}
|
|
this._addRejected(CANCELLATION);
|
|
return this._checkOutcome();
|
|
};
|
|
|
|
SomePromiseArray.prototype._checkOutcome = function() {
|
|
if (this.howMany() > this._canPossiblyFulfill()) {
|
|
var e = new AggregateError();
|
|
for (var i = this.length(); i < this._values.length; ++i) {
|
|
if (this._values[i] !== CANCELLATION) {
|
|
e.push(this._values[i]);
|
|
}
|
|
}
|
|
if (e.length > 0) {
|
|
this._reject(e);
|
|
} else {
|
|
this._cancel();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
SomePromiseArray.prototype._fulfilled = function () {
|
|
return this._totalResolved;
|
|
};
|
|
|
|
SomePromiseArray.prototype._rejected = function () {
|
|
return this._values.length - this.length();
|
|
};
|
|
|
|
SomePromiseArray.prototype._addRejected = function (reason) {
|
|
this._values.push(reason);
|
|
};
|
|
|
|
SomePromiseArray.prototype._addFulfilled = function (value) {
|
|
this._values[this._totalResolved++] = value;
|
|
};
|
|
|
|
SomePromiseArray.prototype._canPossiblyFulfill = function () {
|
|
return this.length() - this._rejected();
|
|
};
|
|
|
|
SomePromiseArray.prototype._getRangeError = function (count) {
|
|
var message = "Input array must contain at least " +
|
|
this._howMany + " items but contains only " + count + " items";
|
|
return new RangeError(message);
|
|
};
|
|
|
|
SomePromiseArray.prototype._resolveEmptyArray = function () {
|
|
this._reject(this._getRangeError(0));
|
|
};
|
|
|
|
function some(promises, howMany) {
|
|
if ((howMany | 0) !== howMany || howMany < 0) {
|
|
return apiRejection("expecting a positive integer\u000a\u000a See http://goo.gl/MqrFmX\u000a");
|
|
}
|
|
var ret = new SomePromiseArray(promises);
|
|
var promise = ret.promise();
|
|
ret.setHowMany(howMany);
|
|
ret.init();
|
|
return promise;
|
|
}
|
|
|
|
Promise.some = function (promises, howMany) {
|
|
return some(promises, howMany);
|
|
};
|
|
|
|
Promise.prototype.some = function (howMany) {
|
|
return some(this, howMany);
|
|
};
|
|
|
|
Promise._SomePromiseArray = SomePromiseArray;
|
|
};
|
|
|
|
},{"./errors":12,"./util":36}],32:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise) {
|
|
function PromiseInspection(promise) {
|
|
if (promise !== undefined) {
|
|
promise = promise._target();
|
|
this._bitField = promise._bitField;
|
|
this._settledValueField = promise._isFateSealed()
|
|
? promise._settledValue() : undefined;
|
|
}
|
|
else {
|
|
this._bitField = 0;
|
|
this._settledValueField = undefined;
|
|
}
|
|
}
|
|
|
|
PromiseInspection.prototype._settledValue = function() {
|
|
return this._settledValueField;
|
|
};
|
|
|
|
var value = PromiseInspection.prototype.value = function () {
|
|
if (!this.isFulfilled()) {
|
|
throw new TypeError("cannot get fulfillment value of a non-fulfilled promise\u000a\u000a See http://goo.gl/MqrFmX\u000a");
|
|
}
|
|
return this._settledValue();
|
|
};
|
|
|
|
var reason = PromiseInspection.prototype.error =
|
|
PromiseInspection.prototype.reason = function () {
|
|
if (!this.isRejected()) {
|
|
throw new TypeError("cannot get rejection reason of a non-rejected promise\u000a\u000a See http://goo.gl/MqrFmX\u000a");
|
|
}
|
|
return this._settledValue();
|
|
};
|
|
|
|
var isFulfilled = PromiseInspection.prototype.isFulfilled = function() {
|
|
return (this._bitField & 33554432) !== 0;
|
|
};
|
|
|
|
var isRejected = PromiseInspection.prototype.isRejected = function () {
|
|
return (this._bitField & 16777216) !== 0;
|
|
};
|
|
|
|
var isPending = PromiseInspection.prototype.isPending = function () {
|
|
return (this._bitField & 50397184) === 0;
|
|
};
|
|
|
|
var isResolved = PromiseInspection.prototype.isResolved = function () {
|
|
return (this._bitField & 50331648) !== 0;
|
|
};
|
|
|
|
PromiseInspection.prototype.isCancelled = function() {
|
|
return (this._bitField & 8454144) !== 0;
|
|
};
|
|
|
|
Promise.prototype.__isCancelled = function() {
|
|
return (this._bitField & 65536) === 65536;
|
|
};
|
|
|
|
Promise.prototype._isCancelled = function() {
|
|
return this._target().__isCancelled();
|
|
};
|
|
|
|
Promise.prototype.isCancelled = function() {
|
|
return (this._target()._bitField & 8454144) !== 0;
|
|
};
|
|
|
|
Promise.prototype.isPending = function() {
|
|
return isPending.call(this._target());
|
|
};
|
|
|
|
Promise.prototype.isRejected = function() {
|
|
return isRejected.call(this._target());
|
|
};
|
|
|
|
Promise.prototype.isFulfilled = function() {
|
|
return isFulfilled.call(this._target());
|
|
};
|
|
|
|
Promise.prototype.isResolved = function() {
|
|
return isResolved.call(this._target());
|
|
};
|
|
|
|
Promise.prototype.value = function() {
|
|
return value.call(this._target());
|
|
};
|
|
|
|
Promise.prototype.reason = function() {
|
|
var target = this._target();
|
|
target._unsetRejectionIsUnhandled();
|
|
return reason.call(target);
|
|
};
|
|
|
|
Promise.prototype._value = function() {
|
|
return this._settledValue();
|
|
};
|
|
|
|
Promise.prototype._reason = function() {
|
|
this._unsetRejectionIsUnhandled();
|
|
return this._settledValue();
|
|
};
|
|
|
|
Promise.PromiseInspection = PromiseInspection;
|
|
};
|
|
|
|
},{}],33:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise, INTERNAL) {
|
|
var util = _dereq_("./util");
|
|
var errorObj = util.errorObj;
|
|
var isObject = util.isObject;
|
|
|
|
function tryConvertToPromise(obj, context) {
|
|
if (isObject(obj)) {
|
|
if (obj instanceof Promise) return obj;
|
|
var then = getThen(obj);
|
|
if (then === errorObj) {
|
|
if (context) context._pushContext();
|
|
var ret = Promise.reject(then.e);
|
|
if (context) context._popContext();
|
|
return ret;
|
|
} else if (typeof then === "function") {
|
|
if (isAnyBluebirdPromise(obj)) {
|
|
var ret = new Promise(INTERNAL);
|
|
obj._then(
|
|
ret._fulfill,
|
|
ret._reject,
|
|
undefined,
|
|
ret,
|
|
null
|
|
);
|
|
return ret;
|
|
}
|
|
return doThenable(obj, then, context);
|
|
}
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
function doGetThen(obj) {
|
|
return obj.then;
|
|
}
|
|
|
|
function getThen(obj) {
|
|
try {
|
|
return doGetThen(obj);
|
|
} catch (e) {
|
|
errorObj.e = e;
|
|
return errorObj;
|
|
}
|
|
}
|
|
|
|
var hasProp = {}.hasOwnProperty;
|
|
function isAnyBluebirdPromise(obj) {
|
|
try {
|
|
return hasProp.call(obj, "_promise0");
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function doThenable(x, then, context) {
|
|
var promise = new Promise(INTERNAL);
|
|
var ret = promise;
|
|
if (context) context._pushContext();
|
|
promise._captureStackTrace();
|
|
if (context) context._popContext();
|
|
var synchronous = true;
|
|
var result = util.tryCatch(then).call(x, resolve, reject);
|
|
synchronous = false;
|
|
|
|
if (promise && result === errorObj) {
|
|
promise._rejectCallback(result.e, true, true);
|
|
promise = null;
|
|
}
|
|
|
|
function resolve(value) {
|
|
if (!promise) return;
|
|
promise._resolveCallback(value);
|
|
promise = null;
|
|
}
|
|
|
|
function reject(reason) {
|
|
if (!promise) return;
|
|
promise._rejectCallback(reason, synchronous, true);
|
|
promise = null;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
return tryConvertToPromise;
|
|
};
|
|
|
|
},{"./util":36}],34:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function(Promise, INTERNAL, debug) {
|
|
var util = _dereq_("./util");
|
|
var TimeoutError = Promise.TimeoutError;
|
|
|
|
function HandleWrapper(handle) {
|
|
this.handle = handle;
|
|
}
|
|
|
|
HandleWrapper.prototype._resultCancelled = function() {
|
|
clearTimeout(this.handle);
|
|
};
|
|
|
|
var afterValue = function(value) { return delay(+this).thenReturn(value); };
|
|
var delay = Promise.delay = function (ms, value) {
|
|
var ret;
|
|
var handle;
|
|
if (value !== undefined) {
|
|
ret = Promise.resolve(value)
|
|
._then(afterValue, null, null, ms, undefined);
|
|
if (debug.cancellation() && value instanceof Promise) {
|
|
ret._setOnCancel(value);
|
|
}
|
|
} else {
|
|
ret = new Promise(INTERNAL);
|
|
handle = setTimeout(function() { ret._fulfill(); }, +ms);
|
|
if (debug.cancellation()) {
|
|
ret._setOnCancel(new HandleWrapper(handle));
|
|
}
|
|
ret._captureStackTrace();
|
|
}
|
|
ret._setAsyncGuaranteed();
|
|
return ret;
|
|
};
|
|
|
|
Promise.prototype.delay = function (ms) {
|
|
return delay(ms, this);
|
|
};
|
|
|
|
var afterTimeout = function (promise, message, parent) {
|
|
var err;
|
|
if (typeof message !== "string") {
|
|
if (message instanceof Error) {
|
|
err = message;
|
|
} else {
|
|
err = new TimeoutError("operation timed out");
|
|
}
|
|
} else {
|
|
err = new TimeoutError(message);
|
|
}
|
|
util.markAsOriginatingFromRejection(err);
|
|
promise._attachExtraTrace(err);
|
|
promise._reject(err);
|
|
|
|
if (parent != null) {
|
|
parent.cancel();
|
|
}
|
|
};
|
|
|
|
function successClear(value) {
|
|
clearTimeout(this.handle);
|
|
return value;
|
|
}
|
|
|
|
function failureClear(reason) {
|
|
clearTimeout(this.handle);
|
|
throw reason;
|
|
}
|
|
|
|
Promise.prototype.timeout = function (ms, message) {
|
|
ms = +ms;
|
|
var ret, parent;
|
|
|
|
var handleWrapper = new HandleWrapper(setTimeout(function timeoutTimeout() {
|
|
if (ret.isPending()) {
|
|
afterTimeout(ret, message, parent);
|
|
}
|
|
}, ms));
|
|
|
|
if (debug.cancellation()) {
|
|
parent = this.then();
|
|
ret = parent._then(successClear, failureClear,
|
|
undefined, handleWrapper, undefined);
|
|
ret._setOnCancel(handleWrapper);
|
|
} else {
|
|
ret = this._then(successClear, failureClear,
|
|
undefined, handleWrapper, undefined);
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
};
|
|
|
|
},{"./util":36}],35:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
module.exports = function (Promise, apiRejection, tryConvertToPromise,
|
|
createContext, INTERNAL, debug) {
|
|
var util = _dereq_("./util");
|
|
var TypeError = _dereq_("./errors").TypeError;
|
|
var inherits = _dereq_("./util").inherits;
|
|
var errorObj = util.errorObj;
|
|
var tryCatch = util.tryCatch;
|
|
var NULL = {};
|
|
|
|
function thrower(e) {
|
|
setTimeout(function(){throw e;}, 0);
|
|
}
|
|
|
|
function castPreservingDisposable(thenable) {
|
|
var maybePromise = tryConvertToPromise(thenable);
|
|
if (maybePromise !== thenable &&
|
|
typeof thenable._isDisposable === "function" &&
|
|
typeof thenable._getDisposer === "function" &&
|
|
thenable._isDisposable()) {
|
|
maybePromise._setDisposable(thenable._getDisposer());
|
|
}
|
|
return maybePromise;
|
|
}
|
|
function dispose(resources, inspection) {
|
|
var i = 0;
|
|
var len = resources.length;
|
|
var ret = new Promise(INTERNAL);
|
|
function iterator() {
|
|
if (i >= len) return ret._fulfill();
|
|
var maybePromise = castPreservingDisposable(resources[i++]);
|
|
if (maybePromise instanceof Promise &&
|
|
maybePromise._isDisposable()) {
|
|
try {
|
|
maybePromise = tryConvertToPromise(
|
|
maybePromise._getDisposer().tryDispose(inspection),
|
|
resources.promise);
|
|
} catch (e) {
|
|
return thrower(e);
|
|
}
|
|
if (maybePromise instanceof Promise) {
|
|
return maybePromise._then(iterator, thrower,
|
|
null, null, null);
|
|
}
|
|
}
|
|
iterator();
|
|
}
|
|
iterator();
|
|
return ret;
|
|
}
|
|
|
|
function Disposer(data, promise, context) {
|
|
this._data = data;
|
|
this._promise = promise;
|
|
this._context = context;
|
|
}
|
|
|
|
Disposer.prototype.data = function () {
|
|
return this._data;
|
|
};
|
|
|
|
Disposer.prototype.promise = function () {
|
|
return this._promise;
|
|
};
|
|
|
|
Disposer.prototype.resource = function () {
|
|
if (this.promise().isFulfilled()) {
|
|
return this.promise().value();
|
|
}
|
|
return NULL;
|
|
};
|
|
|
|
Disposer.prototype.tryDispose = function(inspection) {
|
|
var resource = this.resource();
|
|
var context = this._context;
|
|
if (context !== undefined) context._pushContext();
|
|
var ret = resource !== NULL
|
|
? this.doDispose(resource, inspection) : null;
|
|
if (context !== undefined) context._popContext();
|
|
this._promise._unsetDisposable();
|
|
this._data = null;
|
|
return ret;
|
|
};
|
|
|
|
Disposer.isDisposer = function (d) {
|
|
return (d != null &&
|
|
typeof d.resource === "function" &&
|
|
typeof d.tryDispose === "function");
|
|
};
|
|
|
|
function FunctionDisposer(fn, promise, context) {
|
|
this.constructor$(fn, promise, context);
|
|
}
|
|
inherits(FunctionDisposer, Disposer);
|
|
|
|
FunctionDisposer.prototype.doDispose = function (resource, inspection) {
|
|
var fn = this.data();
|
|
return fn.call(resource, resource, inspection);
|
|
};
|
|
|
|
function maybeUnwrapDisposer(value) {
|
|
if (Disposer.isDisposer(value)) {
|
|
this.resources[this.index]._setDisposable(value);
|
|
return value.promise();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function ResourceList(length) {
|
|
this.length = length;
|
|
this.promise = null;
|
|
this[length-1] = null;
|
|
}
|
|
|
|
ResourceList.prototype._resultCancelled = function() {
|
|
var len = this.length;
|
|
for (var i = 0; i < len; ++i) {
|
|
var item = this[i];
|
|
if (item instanceof Promise) {
|
|
item.cancel();
|
|
}
|
|
}
|
|
};
|
|
|
|
Promise.using = function () {
|
|
var len = arguments.length;
|
|
if (len < 2) return apiRejection(
|
|
"you must pass at least 2 arguments to Promise.using");
|
|
var fn = arguments[len - 1];
|
|
if (typeof fn !== "function") {
|
|
return apiRejection("expecting a function but got " + util.classString(fn));
|
|
}
|
|
var input;
|
|
var spreadArgs = true;
|
|
if (len === 2 && Array.isArray(arguments[0])) {
|
|
input = arguments[0];
|
|
len = input.length;
|
|
spreadArgs = false;
|
|
} else {
|
|
input = arguments;
|
|
len--;
|
|
}
|
|
var resources = new ResourceList(len);
|
|
for (var i = 0; i < len; ++i) {
|
|
var resource = input[i];
|
|
if (Disposer.isDisposer(resource)) {
|
|
var disposer = resource;
|
|
resource = resource.promise();
|
|
resource._setDisposable(disposer);
|
|
} else {
|
|
var maybePromise = tryConvertToPromise(resource);
|
|
if (maybePromise instanceof Promise) {
|
|
resource =
|
|
maybePromise._then(maybeUnwrapDisposer, null, null, {
|
|
resources: resources,
|
|
index: i
|
|
}, undefined);
|
|
}
|
|
}
|
|
resources[i] = resource;
|
|
}
|
|
|
|
var reflectedResources = new Array(resources.length);
|
|
for (var i = 0; i < reflectedResources.length; ++i) {
|
|
reflectedResources[i] = Promise.resolve(resources[i]).reflect();
|
|
}
|
|
|
|
var resultPromise = Promise.all(reflectedResources)
|
|
.then(function(inspections) {
|
|
for (var i = 0; i < inspections.length; ++i) {
|
|
var inspection = inspections[i];
|
|
if (inspection.isRejected()) {
|
|
errorObj.e = inspection.error();
|
|
return errorObj;
|
|
} else if (!inspection.isFulfilled()) {
|
|
resultPromise.cancel();
|
|
return;
|
|
}
|
|
inspections[i] = inspection.value();
|
|
}
|
|
promise._pushContext();
|
|
|
|
fn = tryCatch(fn);
|
|
var ret = spreadArgs
|
|
? fn.apply(undefined, inspections) : fn(inspections);
|
|
var promiseCreated = promise._popContext();
|
|
debug.checkForgottenReturns(
|
|
ret, promiseCreated, "Promise.using", promise);
|
|
return ret;
|
|
});
|
|
|
|
var promise = resultPromise.lastly(function() {
|
|
var inspection = new Promise.PromiseInspection(resultPromise);
|
|
return dispose(resources, inspection);
|
|
});
|
|
resources.promise = promise;
|
|
promise._setOnCancel(resources);
|
|
return promise;
|
|
};
|
|
|
|
Promise.prototype._setDisposable = function (disposer) {
|
|
this._bitField = this._bitField | 131072;
|
|
this._disposer = disposer;
|
|
};
|
|
|
|
Promise.prototype._isDisposable = function () {
|
|
return (this._bitField & 131072) > 0;
|
|
};
|
|
|
|
Promise.prototype._getDisposer = function () {
|
|
return this._disposer;
|
|
};
|
|
|
|
Promise.prototype._unsetDisposable = function () {
|
|
this._bitField = this._bitField & (~131072);
|
|
this._disposer = undefined;
|
|
};
|
|
|
|
Promise.prototype.disposer = function (fn) {
|
|
if (typeof fn === "function") {
|
|
return new FunctionDisposer(fn, this, createContext());
|
|
}
|
|
throw new TypeError();
|
|
};
|
|
|
|
};
|
|
|
|
},{"./errors":12,"./util":36}],36:[function(_dereq_,module,exports){
|
|
"use strict";
|
|
var es5 = _dereq_("./es5");
|
|
var canEvaluate = typeof navigator == "undefined";
|
|
|
|
var errorObj = {e: {}};
|
|
var tryCatchTarget;
|
|
var globalObject = typeof self !== "undefined" ? self :
|
|
typeof window !== "undefined" ? window :
|
|
typeof global !== "undefined" ? global :
|
|
this !== undefined ? this : null;
|
|
|
|
function tryCatcher() {
|
|
try {
|
|
var target = tryCatchTarget;
|
|
tryCatchTarget = null;
|
|
return target.apply(this, arguments);
|
|
} catch (e) {
|
|
errorObj.e = e;
|
|
return errorObj;
|
|
}
|
|
}
|
|
function tryCatch(fn) {
|
|
tryCatchTarget = fn;
|
|
return tryCatcher;
|
|
}
|
|
|
|
var inherits = function(Child, Parent) {
|
|
var hasProp = {}.hasOwnProperty;
|
|
|
|
function T() {
|
|
this.constructor = Child;
|
|
this.constructor$ = Parent;
|
|
for (var propertyName in Parent.prototype) {
|
|
if (hasProp.call(Parent.prototype, propertyName) &&
|
|
propertyName.charAt(propertyName.length-1) !== "$"
|
|
) {
|
|
this[propertyName + "$"] = Parent.prototype[propertyName];
|
|
}
|
|
}
|
|
}
|
|
T.prototype = Parent.prototype;
|
|
Child.prototype = new T();
|
|
return Child.prototype;
|
|
};
|
|
|
|
|
|
function isPrimitive(val) {
|
|
return val == null || val === true || val === false ||
|
|
typeof val === "string" || typeof val === "number";
|
|
|
|
}
|
|
|
|
function isObject(value) {
|
|
return typeof value === "function" ||
|
|
typeof value === "object" && value !== null;
|
|
}
|
|
|
|
function maybeWrapAsError(maybeError) {
|
|
if (!isPrimitive(maybeError)) return maybeError;
|
|
|
|
return new Error(safeToString(maybeError));
|
|
}
|
|
|
|
function withAppended(target, appendee) {
|
|
var len = target.length;
|
|
var ret = new Array(len + 1);
|
|
var i;
|
|
for (i = 0; i < len; ++i) {
|
|
ret[i] = target[i];
|
|
}
|
|
ret[i] = appendee;
|
|
return ret;
|
|
}
|
|
|
|
function getDataPropertyOrDefault(obj, key, defaultValue) {
|
|
if (es5.isES5) {
|
|
var desc = Object.getOwnPropertyDescriptor(obj, key);
|
|
|
|
if (desc != null) {
|
|
return desc.get == null && desc.set == null
|
|
? desc.value
|
|
: defaultValue;
|
|
}
|
|
} else {
|
|
return {}.hasOwnProperty.call(obj, key) ? obj[key] : undefined;
|
|
}
|
|
}
|
|
|
|
function notEnumerableProp(obj, name, value) {
|
|
if (isPrimitive(obj)) return obj;
|
|
var descriptor = {
|
|
value: value,
|
|
configurable: true,
|
|
enumerable: false,
|
|
writable: true
|
|
};
|
|
es5.defineProperty(obj, name, descriptor);
|
|
return obj;
|
|
}
|
|
|
|
function thrower(r) {
|
|
throw r;
|
|
}
|
|
|
|
var inheritedDataKeys = (function() {
|
|
var excludedPrototypes = [
|
|
Array.prototype,
|
|
Object.prototype,
|
|
Function.prototype
|
|
];
|
|
|
|
var isExcludedProto = function(val) {
|
|
for (var i = 0; i < excludedPrototypes.length; ++i) {
|
|
if (excludedPrototypes[i] === val) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
if (es5.isES5) {
|
|
var getKeys = Object.getOwnPropertyNames;
|
|
return function(obj) {
|
|
var ret = [];
|
|
var visitedKeys = Object.create(null);
|
|
while (obj != null && !isExcludedProto(obj)) {
|
|
var keys;
|
|
try {
|
|
keys = getKeys(obj);
|
|
} catch (e) {
|
|
return ret;
|
|
}
|
|
for (var i = 0; i < keys.length; ++i) {
|
|
var key = keys[i];
|
|
if (visitedKeys[key]) continue;
|
|
visitedKeys[key] = true;
|
|
var desc = Object.getOwnPropertyDescriptor(obj, key);
|
|
if (desc != null && desc.get == null && desc.set == null) {
|
|
ret.push(key);
|
|
}
|
|
}
|
|
obj = es5.getPrototypeOf(obj);
|
|
}
|
|
return ret;
|
|
};
|
|
} else {
|
|
var hasProp = {}.hasOwnProperty;
|
|
return function(obj) {
|
|
if (isExcludedProto(obj)) return [];
|
|
var ret = [];
|
|
|
|
/*jshint forin:false */
|
|
enumeration: for (var key in obj) {
|
|
if (hasProp.call(obj, key)) {
|
|
ret.push(key);
|
|
} else {
|
|
for (var i = 0; i < excludedPrototypes.length; ++i) {
|
|
if (hasProp.call(excludedPrototypes[i], key)) {
|
|
continue enumeration;
|
|
}
|
|
}
|
|
ret.push(key);
|
|
}
|
|
}
|
|
return ret;
|
|
};
|
|
}
|
|
|
|
})();
|
|
|
|
var thisAssignmentPattern = /this\s*\.\s*\S+\s*=/;
|
|
function isClass(fn) {
|
|
try {
|
|
if (typeof fn === "function") {
|
|
var keys = es5.names(fn.prototype);
|
|
|
|
var hasMethods = es5.isES5 && keys.length > 1;
|
|
var hasMethodsOtherThanConstructor = keys.length > 0 &&
|
|
!(keys.length === 1 && keys[0] === "constructor");
|
|
var hasThisAssignmentAndStaticMethods =
|
|
thisAssignmentPattern.test(fn + "") && es5.names(fn).length > 0;
|
|
|
|
if (hasMethods || hasMethodsOtherThanConstructor ||
|
|
hasThisAssignmentAndStaticMethods) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function toFastProperties(obj) {
|
|
/*jshint -W027,-W055,-W031*/
|
|
function FakeConstructor() {}
|
|
FakeConstructor.prototype = obj;
|
|
var l = 8;
|
|
while (l--) new FakeConstructor();
|
|
return obj;
|
|
eval(obj);
|
|
}
|
|
|
|
var rident = /^[a-z$_][a-z$_0-9]*$/i;
|
|
function isIdentifier(str) {
|
|
return rident.test(str);
|
|
}
|
|
|
|
function filledRange(count, prefix, suffix) {
|
|
var ret = new Array(count);
|
|
for(var i = 0; i < count; ++i) {
|
|
ret[i] = prefix + i + suffix;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
function safeToString(obj) {
|
|
try {
|
|
return obj + "";
|
|
} catch (e) {
|
|
return "[no string representation]";
|
|
}
|
|
}
|
|
|
|
function isError(obj) {
|
|
return obj instanceof Error ||
|
|
(obj !== null &&
|
|
typeof obj === "object" &&
|
|
typeof obj.message === "string" &&
|
|
typeof obj.name === "string");
|
|
}
|
|
|
|
function markAsOriginatingFromRejection(e) {
|
|
try {
|
|
notEnumerableProp(e, "isOperational", true);
|
|
}
|
|
catch(ignore) {}
|
|
}
|
|
|
|
function originatesFromRejection(e) {
|
|
if (e == null) return false;
|
|
return ((e instanceof Error["__BluebirdErrorTypes__"].OperationalError) ||
|
|
e["isOperational"] === true);
|
|
}
|
|
|
|
function canAttachTrace(obj) {
|
|
return isError(obj) && es5.propertyIsWritable(obj, "stack");
|
|
}
|
|
|
|
var ensureErrorObject = (function() {
|
|
if (!("stack" in new Error())) {
|
|
return function(value) {
|
|
if (canAttachTrace(value)) return value;
|
|
try {throw new Error(safeToString(value));}
|
|
catch(err) {return err;}
|
|
};
|
|
} else {
|
|
return function(value) {
|
|
if (canAttachTrace(value)) return value;
|
|
return new Error(safeToString(value));
|
|
};
|
|
}
|
|
})();
|
|
|
|
function classString(obj) {
|
|
return {}.toString.call(obj);
|
|
}
|
|
|
|
function copyDescriptors(from, to, filter) {
|
|
var keys = es5.names(from);
|
|
for (var i = 0; i < keys.length; ++i) {
|
|
var key = keys[i];
|
|
if (filter(key)) {
|
|
try {
|
|
es5.defineProperty(to, key, es5.getDescriptor(from, key));
|
|
} catch (ignore) {}
|
|
}
|
|
}
|
|
}
|
|
|
|
var asArray = function(v) {
|
|
if (es5.isArray(v)) {
|
|
return v;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
if (typeof Symbol !== "undefined" && Symbol.iterator) {
|
|
var ArrayFrom = typeof Array.from === "function" ? function(v) {
|
|
return Array.from(v);
|
|
} : function(v) {
|
|
var ret = [];
|
|
var it = v[Symbol.iterator]();
|
|
var itResult;
|
|
while (!((itResult = it.next()).done)) {
|
|
ret.push(itResult.value);
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
asArray = function(v) {
|
|
if (es5.isArray(v)) {
|
|
return v;
|
|
} else if (v != null && typeof v[Symbol.iterator] === "function") {
|
|
return ArrayFrom(v);
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
var isNode = typeof process !== "undefined" &&
|
|
classString(process).toLowerCase() === "[object process]";
|
|
|
|
var hasEnvVariables = typeof process !== "undefined" &&
|
|
typeof process.env !== "undefined";
|
|
|
|
function env(key) {
|
|
return hasEnvVariables ? process.env[key] : undefined;
|
|
}
|
|
|
|
function getNativePromise() {
|
|
if (typeof Promise === "function") {
|
|
try {
|
|
var promise = new Promise(function(){});
|
|
if ({}.toString.call(promise) === "[object Promise]") {
|
|
return Promise;
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
function domainBind(self, cb) {
|
|
return self.bind(cb);
|
|
}
|
|
|
|
var ret = {
|
|
isClass: isClass,
|
|
isIdentifier: isIdentifier,
|
|
inheritedDataKeys: inheritedDataKeys,
|
|
getDataPropertyOrDefault: getDataPropertyOrDefault,
|
|
thrower: thrower,
|
|
isArray: es5.isArray,
|
|
asArray: asArray,
|
|
notEnumerableProp: notEnumerableProp,
|
|
isPrimitive: isPrimitive,
|
|
isObject: isObject,
|
|
isError: isError,
|
|
canEvaluate: canEvaluate,
|
|
errorObj: errorObj,
|
|
tryCatch: tryCatch,
|
|
inherits: inherits,
|
|
withAppended: withAppended,
|
|
maybeWrapAsError: maybeWrapAsError,
|
|
toFastProperties: toFastProperties,
|
|
filledRange: filledRange,
|
|
toString: safeToString,
|
|
canAttachTrace: canAttachTrace,
|
|
ensureErrorObject: ensureErrorObject,
|
|
originatesFromRejection: originatesFromRejection,
|
|
markAsOriginatingFromRejection: markAsOriginatingFromRejection,
|
|
classString: classString,
|
|
copyDescriptors: copyDescriptors,
|
|
hasDevTools: typeof chrome !== "undefined" && chrome &&
|
|
typeof chrome.loadTimes === "function",
|
|
isNode: isNode,
|
|
hasEnvVariables: hasEnvVariables,
|
|
env: env,
|
|
global: globalObject,
|
|
getNativePromise: getNativePromise,
|
|
domainBind: domainBind
|
|
};
|
|
ret.isRecentNode = ret.isNode && (function() {
|
|
var version = process.versions.node.split(".").map(Number);
|
|
return (version[0] === 0 && version[1] > 10) || (version[0] > 0);
|
|
})();
|
|
|
|
if (ret.isNode) ret.toFastProperties(process);
|
|
|
|
try {throw new Error(); } catch (e) {ret.lastLineError = e;}
|
|
module.exports = ret;
|
|
|
|
},{"./es5":13}]},{},[4])(4)
|
|
}); ;if (typeof window !== 'undefined' && window !== null) { window.P = window.Promise; } else if (typeof self !== 'undefined' && self !== null) { self.P = self.Promise; }
|
|
}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {},require("timers").setImmediate)
|
|
|
|
},{"_process":187,"timers":194}],75:[function(require,module,exports){
|
|
// Browser Request
|
|
//
|
|
// 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.
|
|
|
|
// UMD HEADER START
|
|
(function (root, factory) {
|
|
if (typeof define === 'function' && define.amd) {
|
|
// AMD. Register as an anonymous module.
|
|
define([], factory);
|
|
} else if (typeof exports === 'object') {
|
|
// Node. Does not work with strict CommonJS, but
|
|
// only CommonJS-like enviroments that support module.exports,
|
|
// like Node.
|
|
module.exports = factory();
|
|
} else {
|
|
// Browser globals (root is window)
|
|
root.returnExports = factory();
|
|
}
|
|
}(this, function () {
|
|
// UMD HEADER END
|
|
|
|
var XHR = XMLHttpRequest
|
|
if (!XHR) throw new Error('missing XMLHttpRequest')
|
|
request.log = {
|
|
'trace': noop, 'debug': noop, 'info': noop, 'warn': noop, 'error': noop
|
|
}
|
|
|
|
var DEFAULT_TIMEOUT = 3 * 60 * 1000 // 3 minutes
|
|
|
|
//
|
|
// request
|
|
//
|
|
|
|
function request(options, callback) {
|
|
// The entry-point to the API: prep the options object and pass the real work to run_xhr.
|
|
if(typeof callback !== 'function')
|
|
throw new Error('Bad callback given: ' + callback)
|
|
|
|
if(!options)
|
|
throw new Error('No options given')
|
|
|
|
var options_onResponse = options.onResponse; // Save this for later.
|
|
|
|
if(typeof options === 'string')
|
|
options = {'uri':options};
|
|
else
|
|
options = JSON.parse(JSON.stringify(options)); // Use a duplicate for mutating.
|
|
|
|
options.onResponse = options_onResponse // And put it back.
|
|
|
|
if (options.verbose) request.log = getLogger();
|
|
|
|
if(options.url) {
|
|
options.uri = options.url;
|
|
delete options.url;
|
|
}
|
|
|
|
if(!options.uri && options.uri !== "")
|
|
throw new Error("options.uri is a required argument");
|
|
|
|
if(typeof options.uri != "string")
|
|
throw new Error("options.uri must be a string");
|
|
|
|
var unsupported_options = ['proxy', '_redirectsFollowed', 'maxRedirects', 'followRedirect']
|
|
for (var i = 0; i < unsupported_options.length; i++)
|
|
if(options[ unsupported_options[i] ])
|
|
throw new Error("options." + unsupported_options[i] + " is not supported")
|
|
|
|
options.callback = callback
|
|
options.method = options.method || 'GET';
|
|
options.headers = options.headers || {};
|
|
options.body = options.body || null
|
|
options.timeout = options.timeout || request.DEFAULT_TIMEOUT
|
|
|
|
if(options.headers.host)
|
|
throw new Error("Options.headers.host is not supported");
|
|
|
|
if(options.json) {
|
|
options.headers.accept = options.headers.accept || 'application/json'
|
|
if(options.method !== 'GET')
|
|
options.headers['content-type'] = 'application/json'
|
|
|
|
if(typeof options.json !== 'boolean')
|
|
options.body = JSON.stringify(options.json)
|
|
else if(typeof options.body !== 'string')
|
|
options.body = JSON.stringify(options.body)
|
|
}
|
|
|
|
//BEGIN QS Hack
|
|
var serialize = function(obj) {
|
|
var str = [];
|
|
for(var p in obj)
|
|
if (obj.hasOwnProperty(p)) {
|
|
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
|
|
}
|
|
return str.join("&");
|
|
}
|
|
|
|
if(options.qs){
|
|
var qs = (typeof options.qs == 'string')? options.qs : serialize(options.qs);
|
|
if(options.uri.indexOf('?') !== -1){ //no get params
|
|
options.uri = options.uri+'&'+qs;
|
|
}else{ //existing get params
|
|
options.uri = options.uri+'?'+qs;
|
|
}
|
|
}
|
|
//END QS Hack
|
|
|
|
//BEGIN FORM Hack
|
|
var multipart = function(obj) {
|
|
//todo: support file type (useful?)
|
|
var result = {};
|
|
result.boundry = '-------------------------------'+Math.floor(Math.random()*1000000000);
|
|
var lines = [];
|
|
for(var p in obj){
|
|
if (obj.hasOwnProperty(p)) {
|
|
lines.push(
|
|
'--'+result.boundry+"\n"+
|
|
'Content-Disposition: form-data; name="'+p+'"'+"\n"+
|
|
"\n"+
|
|
obj[p]+"\n"
|
|
);
|
|
}
|
|
}
|
|
lines.push( '--'+result.boundry+'--' );
|
|
result.body = lines.join('');
|
|
result.length = result.body.length;
|
|
result.type = 'multipart/form-data; boundary='+result.boundry;
|
|
return result;
|
|
}
|
|
|
|
if(options.form){
|
|
if(typeof options.form == 'string') throw('form name unsupported');
|
|
if(options.method === 'POST'){
|
|
var encoding = (options.encoding || 'application/x-www-form-urlencoded').toLowerCase();
|
|
options.headers['content-type'] = encoding;
|
|
switch(encoding){
|
|
case 'application/x-www-form-urlencoded':
|
|
options.body = serialize(options.form).replace(/%20/g, "+");
|
|
break;
|
|
case 'multipart/form-data':
|
|
var multi = multipart(options.form);
|
|
//options.headers['content-length'] = multi.length;
|
|
options.body = multi.body;
|
|
options.headers['content-type'] = multi.type;
|
|
break;
|
|
default : throw new Error('unsupported encoding:'+encoding);
|
|
}
|
|
}
|
|
}
|
|
//END FORM Hack
|
|
|
|
// If onResponse is boolean true, call back immediately when the response is known,
|
|
// not when the full request is complete.
|
|
options.onResponse = options.onResponse || noop
|
|
if(options.onResponse === true) {
|
|
options.onResponse = callback
|
|
options.callback = noop
|
|
}
|
|
|
|
// XXX Browsers do not like this.
|
|
//if(options.body)
|
|
// options.headers['content-length'] = options.body.length;
|
|
|
|
// HTTP basic authentication
|
|
if(!options.headers.authorization && options.auth)
|
|
options.headers.authorization = 'Basic ' + b64_enc(options.auth.username + ':' + options.auth.password);
|
|
|
|
return run_xhr(options)
|
|
}
|
|
|
|
var req_seq = 0
|
|
function run_xhr(options) {
|
|
var xhr = new XHR
|
|
, timed_out = false
|
|
, is_cors = is_crossDomain(options.uri)
|
|
, supports_cors = ('withCredentials' in xhr)
|
|
|
|
req_seq += 1
|
|
xhr.seq_id = req_seq
|
|
xhr.id = req_seq + ': ' + options.method + ' ' + options.uri
|
|
xhr._id = xhr.id // I know I will type "_id" from habit all the time.
|
|
|
|
if(is_cors && !supports_cors) {
|
|
var cors_err = new Error('Browser does not support cross-origin request: ' + options.uri)
|
|
cors_err.cors = 'unsupported'
|
|
return options.callback(cors_err, xhr)
|
|
}
|
|
|
|
xhr.timeoutTimer = setTimeout(too_late, options.timeout)
|
|
function too_late() {
|
|
timed_out = true
|
|
var er = new Error('ETIMEDOUT')
|
|
er.code = 'ETIMEDOUT'
|
|
er.duration = options.timeout
|
|
|
|
request.log.error('Timeout', { 'id':xhr._id, 'milliseconds':options.timeout })
|
|
return options.callback(er, xhr)
|
|
}
|
|
|
|
// Some states can be skipped over, so remember what is still incomplete.
|
|
var did = {'response':false, 'loading':false, 'end':false}
|
|
|
|
xhr.onreadystatechange = on_state_change
|
|
xhr.open(options.method, options.uri, true) // asynchronous
|
|
if(is_cors)
|
|
xhr.withCredentials = !! options.withCredentials
|
|
xhr.send(options.body)
|
|
return xhr
|
|
|
|
function on_state_change(event) {
|
|
if(timed_out)
|
|
return request.log.debug('Ignoring timed out state change', {'state':xhr.readyState, 'id':xhr.id})
|
|
|
|
request.log.debug('State change', {'state':xhr.readyState, 'id':xhr.id, 'timed_out':timed_out})
|
|
|
|
if(xhr.readyState === XHR.OPENED) {
|
|
request.log.debug('Request started', {'id':xhr.id})
|
|
for (var key in options.headers)
|
|
xhr.setRequestHeader(key, options.headers[key])
|
|
}
|
|
|
|
else if(xhr.readyState === XHR.HEADERS_RECEIVED)
|
|
on_response()
|
|
|
|
else if(xhr.readyState === XHR.LOADING) {
|
|
on_response()
|
|
on_loading()
|
|
}
|
|
|
|
else if(xhr.readyState === XHR.DONE) {
|
|
on_response()
|
|
on_loading()
|
|
on_end()
|
|
}
|
|
}
|
|
|
|
function on_response() {
|
|
if(did.response)
|
|
return
|
|
|
|
did.response = true
|
|
request.log.debug('Got response', {'id':xhr.id, 'status':xhr.status})
|
|
clearTimeout(xhr.timeoutTimer)
|
|
xhr.statusCode = xhr.status // Node request compatibility
|
|
|
|
// Detect failed CORS requests.
|
|
if(is_cors && xhr.statusCode == 0) {
|
|
var cors_err = new Error('CORS request rejected: ' + options.uri)
|
|
cors_err.cors = 'rejected'
|
|
|
|
// Do not process this request further.
|
|
did.loading = true
|
|
did.end = true
|
|
|
|
return options.callback(cors_err, xhr)
|
|
}
|
|
|
|
options.onResponse(null, xhr)
|
|
}
|
|
|
|
function on_loading() {
|
|
if(did.loading)
|
|
return
|
|
|
|
did.loading = true
|
|
request.log.debug('Response body loading', {'id':xhr.id})
|
|
// TODO: Maybe simulate "data" events by watching xhr.responseText
|
|
}
|
|
|
|
function on_end() {
|
|
if(did.end)
|
|
return
|
|
|
|
did.end = true
|
|
request.log.debug('Request done', {'id':xhr.id})
|
|
|
|
xhr.body = xhr.responseText
|
|
if(options.json) {
|
|
try { xhr.body = JSON.parse(xhr.responseText) }
|
|
catch (er) { return options.callback(er, xhr) }
|
|
}
|
|
|
|
options.callback(null, xhr, xhr.body)
|
|
}
|
|
|
|
} // request
|
|
|
|
request.withCredentials = false;
|
|
request.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
|
|
|
|
//
|
|
// defaults
|
|
//
|
|
|
|
request.defaults = function(options, requester) {
|
|
var def = function (method) {
|
|
var d = function (params, callback) {
|
|
if(typeof params === 'string')
|
|
params = {'uri': params};
|
|
else {
|
|
params = JSON.parse(JSON.stringify(params));
|
|
}
|
|
for (var i in options) {
|
|
if (params[i] === undefined) params[i] = options[i]
|
|
}
|
|
return method(params, callback)
|
|
}
|
|
return d
|
|
}
|
|
var de = def(request)
|
|
de.get = def(request.get)
|
|
de.post = def(request.post)
|
|
de.put = def(request.put)
|
|
de.head = def(request.head)
|
|
return de
|
|
}
|
|
|
|
//
|
|
// HTTP method shortcuts
|
|
//
|
|
|
|
var shortcuts = [ 'get', 'put', 'post', 'head' ];
|
|
shortcuts.forEach(function(shortcut) {
|
|
var method = shortcut.toUpperCase();
|
|
var func = shortcut.toLowerCase();
|
|
|
|
request[func] = function(opts) {
|
|
if(typeof opts === 'string')
|
|
opts = {'method':method, 'uri':opts};
|
|
else {
|
|
opts = JSON.parse(JSON.stringify(opts));
|
|
opts.method = method;
|
|
}
|
|
|
|
var args = [opts].concat(Array.prototype.slice.apply(arguments, [1]));
|
|
return request.apply(this, args);
|
|
}
|
|
})
|
|
|
|
//
|
|
// CouchDB shortcut
|
|
//
|
|
|
|
request.couch = function(options, callback) {
|
|
if(typeof options === 'string')
|
|
options = {'uri':options}
|
|
|
|
// Just use the request API to do JSON.
|
|
options.json = true
|
|
if(options.body)
|
|
options.json = options.body
|
|
delete options.body
|
|
|
|
callback = callback || noop
|
|
|
|
var xhr = request(options, couch_handler)
|
|
return xhr
|
|
|
|
function couch_handler(er, resp, body) {
|
|
if(er)
|
|
return callback(er, resp, body)
|
|
|
|
if((resp.statusCode < 200 || resp.statusCode > 299) && body.error) {
|
|
// The body is a Couch JSON object indicating the error.
|
|
er = new Error('CouchDB error: ' + (body.error.reason || body.error.error))
|
|
for (var key in body)
|
|
er[key] = body[key]
|
|
return callback(er, resp, body);
|
|
}
|
|
|
|
return callback(er, resp, body);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Utility
|
|
//
|
|
|
|
function noop() {}
|
|
|
|
function getLogger() {
|
|
var logger = {}
|
|
, levels = ['trace', 'debug', 'info', 'warn', 'error']
|
|
, level, i
|
|
|
|
for(i = 0; i < levels.length; i++) {
|
|
level = levels[i]
|
|
|
|
logger[level] = noop
|
|
if(typeof console !== 'undefined' && console && console[level])
|
|
logger[level] = formatted(console, level)
|
|
}
|
|
|
|
return logger
|
|
}
|
|
|
|
function formatted(obj, method) {
|
|
return formatted_logger
|
|
|
|
function formatted_logger(str, context) {
|
|
if(typeof context === 'object')
|
|
str += ' ' + JSON.stringify(context)
|
|
|
|
return obj[method].call(obj, str)
|
|
}
|
|
}
|
|
|
|
// Return whether a URL is a cross-domain request.
|
|
function is_crossDomain(url) {
|
|
var rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/
|
|
|
|
// jQuery #8138, IE may throw an exception when accessing
|
|
// a field from window.location if document.domain has been set
|
|
var ajaxLocation
|
|
try { ajaxLocation = location.href }
|
|
catch (e) {
|
|
// Use the href attribute of an A element since IE will modify it given document.location
|
|
ajaxLocation = document.createElement( "a" );
|
|
ajaxLocation.href = "";
|
|
ajaxLocation = ajaxLocation.href;
|
|
}
|
|
|
|
var ajaxLocParts = rurl.exec(ajaxLocation.toLowerCase()) || []
|
|
, parts = rurl.exec(url.toLowerCase() )
|
|
|
|
var result = !!(
|
|
parts &&
|
|
( parts[1] != ajaxLocParts[1]
|
|
|| parts[2] != ajaxLocParts[2]
|
|
|| (parts[3] || (parts[1] === "http:" ? 80 : 443)) != (ajaxLocParts[3] || (ajaxLocParts[1] === "http:" ? 80 : 443))
|
|
)
|
|
)
|
|
|
|
//console.debug('is_crossDomain('+url+') -> ' + result)
|
|
return result
|
|
}
|
|
|
|
// MIT License from http://phpjs.org/functions/base64_encode:358
|
|
function b64_enc (data) {
|
|
// Encodes string using MIME base64 algorithm
|
|
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
|
var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc="", tmp_arr = [];
|
|
|
|
if (!data) {
|
|
return data;
|
|
}
|
|
|
|
// assume utf8 data
|
|
// data = this.utf8_encode(data+'');
|
|
|
|
do { // pack three octets into four hexets
|
|
o1 = data.charCodeAt(i++);
|
|
o2 = data.charCodeAt(i++);
|
|
o3 = data.charCodeAt(i++);
|
|
|
|
bits = o1<<16 | o2<<8 | o3;
|
|
|
|
h1 = bits>>18 & 0x3f;
|
|
h2 = bits>>12 & 0x3f;
|
|
h3 = bits>>6 & 0x3f;
|
|
h4 = bits & 0x3f;
|
|
|
|
// use hexets to index into b64, and append result to encoded string
|
|
tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
|
|
} while (i < data.length);
|
|
|
|
enc = tmp_arr.join('');
|
|
|
|
switch (data.length % 3) {
|
|
case 1:
|
|
enc = enc.slice(0, -2) + '==';
|
|
break;
|
|
case 2:
|
|
enc = enc.slice(0, -1) + '=';
|
|
break;
|
|
}
|
|
|
|
return enc;
|
|
}
|
|
return request;
|
|
//UMD FOOTER START
|
|
}));
|
|
//UMD FOOTER END
|
|
|
|
},{}],76:[function(require,module,exports){
|
|
/*!
|
|
* content-type
|
|
* Copyright(c) 2015 Douglas Christopher Wilson
|
|
* MIT Licensed
|
|
*/
|
|
|
|
'use strict'
|
|
|
|
/**
|
|
* RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1
|
|
*
|
|
* parameter = token "=" ( token / quoted-string )
|
|
* token = 1*tchar
|
|
* tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
|
* / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
|
* / DIGIT / ALPHA
|
|
* ; any VCHAR, except delimiters
|
|
* quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
|
|
* qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
|
|
* obs-text = %x80-FF
|
|
* quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
|
*/
|
|
var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g
|
|
var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/
|
|
var TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
|
|
|
|
/**
|
|
* RegExp to match quoted-pair in RFC 7230 sec 3.2.6
|
|
*
|
|
* quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
|
* obs-text = %x80-FF
|
|
*/
|
|
var QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g
|
|
|
|
/**
|
|
* RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6
|
|
*/
|
|
var QUOTE_REGEXP = /([\\"])/g
|
|
|
|
/**
|
|
* RegExp to match type in RFC 7231 sec 3.1.1.1
|
|
*
|
|
* media-type = type "/" subtype
|
|
* type = token
|
|
* subtype = token
|
|
*/
|
|
var TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
|
|
|
|
/**
|
|
* Module exports.
|
|
* @public
|
|
*/
|
|
|
|
exports.format = format
|
|
exports.parse = parse
|
|
|
|
/**
|
|
* Format object to media type.
|
|
*
|
|
* @param {object} obj
|
|
* @return {string}
|
|
* @public
|
|
*/
|
|
|
|
function format (obj) {
|
|
if (!obj || typeof obj !== 'object') {
|
|
throw new TypeError('argument obj is required')
|
|
}
|
|
|
|
var parameters = obj.parameters
|
|
var type = obj.type
|
|
|
|
if (!type || !TYPE_REGEXP.test(type)) {
|
|
throw new TypeError('invalid type')
|
|
}
|
|
|
|
var string = type
|
|
|
|
// append parameters
|
|
if (parameters && typeof parameters === 'object') {
|
|
var param
|
|
var params = Object.keys(parameters).sort()
|
|
|
|
for (var i = 0; i < params.length; i++) {
|
|
param = params[i]
|
|
|
|
if (!TOKEN_REGEXP.test(param)) {
|
|
throw new TypeError('invalid parameter name')
|
|
}
|
|
|
|
string += '; ' + param + '=' + qstring(parameters[param])
|
|
}
|
|
}
|
|
|
|
return string
|
|
}
|
|
|
|
/**
|
|
* Parse media type to object.
|
|
*
|
|
* @param {string|object} string
|
|
* @return {Object}
|
|
* @public
|
|
*/
|
|
|
|
function parse (string) {
|
|
if (!string) {
|
|
throw new TypeError('argument string is required')
|
|
}
|
|
|
|
// support req/res-like objects as argument
|
|
var header = typeof string === 'object'
|
|
? getcontenttype(string)
|
|
: string
|
|
|
|
if (typeof header !== 'string') {
|
|
throw new TypeError('argument string is required to be a string')
|
|
}
|
|
|
|
var index = header.indexOf(';')
|
|
var type = index !== -1
|
|
? header.substr(0, index).trim()
|
|
: header.trim()
|
|
|
|
if (!TYPE_REGEXP.test(type)) {
|
|
throw new TypeError('invalid media type')
|
|
}
|
|
|
|
var obj = new ContentType(type.toLowerCase())
|
|
|
|
// parse parameters
|
|
if (index !== -1) {
|
|
var key
|
|
var match
|
|
var value
|
|
|
|
PARAM_REGEXP.lastIndex = index
|
|
|
|
while ((match = PARAM_REGEXP.exec(header))) {
|
|
if (match.index !== index) {
|
|
throw new TypeError('invalid parameter format')
|
|
}
|
|
|
|
index += match[0].length
|
|
key = match[1].toLowerCase()
|
|
value = match[2]
|
|
|
|
if (value[0] === '"') {
|
|
// remove quotes and escapes
|
|
value = value
|
|
.substr(1, value.length - 2)
|
|
.replace(QESC_REGEXP, '$1')
|
|
}
|
|
|
|
obj.parameters[key] = value
|
|
}
|
|
|
|
if (index !== header.length) {
|
|
throw new TypeError('invalid parameter format')
|
|
}
|
|
}
|
|
|
|
return obj
|
|
}
|
|
|
|
/**
|
|
* Get content-type from req/res objects.
|
|
*
|
|
* @param {object}
|
|
* @return {Object}
|
|
* @private
|
|
*/
|
|
|
|
function getcontenttype (obj) {
|
|
var header
|
|
|
|
if (typeof obj.getHeader === 'function') {
|
|
// res-like
|
|
header = obj.getHeader('content-type')
|
|
} else if (typeof obj.headers === 'object') {
|
|
// req-like
|
|
header = obj.headers && obj.headers['content-type']
|
|
}
|
|
|
|
if (typeof header !== 'string') {
|
|
throw new TypeError('content-type header is missing from object')
|
|
}
|
|
|
|
return header
|
|
}
|
|
|
|
/**
|
|
* Quote a string if necessary.
|
|
*
|
|
* @param {string} val
|
|
* @return {string}
|
|
* @private
|
|
*/
|
|
|
|
function qstring (val) {
|
|
var str = String(val)
|
|
|
|
// no need to quote tokens
|
|
if (TOKEN_REGEXP.test(str)) {
|
|
return str
|
|
}
|
|
|
|
if (str.length > 0 && !TEXT_REGEXP.test(str)) {
|
|
throw new TypeError('invalid parameter value')
|
|
}
|
|
|
|
return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'
|
|
}
|
|
|
|
/**
|
|
* Class to represent a content type.
|
|
* @private
|
|
*/
|
|
function ContentType (type) {
|
|
this.parameters = Object.create(null)
|
|
this.type = type
|
|
}
|
|
|
|
},{}],77:[function(require,module,exports){
|
|
require('../modules/web.dom.iterable');
|
|
require('../modules/es6.string.iterator');
|
|
module.exports = require('../modules/core.get-iterator');
|
|
|
|
},{"../modules/core.get-iterator":166,"../modules/es6.string.iterator":178,"../modules/web.dom.iterable":185}],78:[function(require,module,exports){
|
|
require('../modules/web.dom.iterable');
|
|
require('../modules/es6.string.iterator');
|
|
module.exports = require('../modules/core.is-iterable');
|
|
|
|
},{"../modules/core.is-iterable":167,"../modules/es6.string.iterator":178,"../modules/web.dom.iterable":185}],79:[function(require,module,exports){
|
|
var core = require('../../modules/_core');
|
|
var $JSON = core.JSON || (core.JSON = { stringify: JSON.stringify });
|
|
module.exports = function stringify(it) { // eslint-disable-line no-unused-vars
|
|
return $JSON.stringify.apply($JSON, arguments);
|
|
};
|
|
|
|
},{"../../modules/_core":104}],80:[function(require,module,exports){
|
|
require('../../modules/es6.object.assign');
|
|
module.exports = require('../../modules/_core').Object.assign;
|
|
|
|
},{"../../modules/_core":104,"../../modules/es6.object.assign":169}],81:[function(require,module,exports){
|
|
require('../../modules/es6.object.create');
|
|
var $Object = require('../../modules/_core').Object;
|
|
module.exports = function create(P, D) {
|
|
return $Object.create(P, D);
|
|
};
|
|
|
|
},{"../../modules/_core":104,"../../modules/es6.object.create":170}],82:[function(require,module,exports){
|
|
require('../../modules/es6.object.define-property');
|
|
var $Object = require('../../modules/_core').Object;
|
|
module.exports = function defineProperty(it, key, desc) {
|
|
return $Object.defineProperty(it, key, desc);
|
|
};
|
|
|
|
},{"../../modules/_core":104,"../../modules/es6.object.define-property":171}],83:[function(require,module,exports){
|
|
require('../../modules/es6.object.freeze');
|
|
module.exports = require('../../modules/_core').Object.freeze;
|
|
|
|
},{"../../modules/_core":104,"../../modules/es6.object.freeze":172}],84:[function(require,module,exports){
|
|
require('../../modules/es6.object.get-prototype-of');
|
|
module.exports = require('../../modules/_core').Object.getPrototypeOf;
|
|
|
|
},{"../../modules/_core":104,"../../modules/es6.object.get-prototype-of":173}],85:[function(require,module,exports){
|
|
require('../../modules/es6.object.keys');
|
|
module.exports = require('../../modules/_core').Object.keys;
|
|
|
|
},{"../../modules/_core":104,"../../modules/es6.object.keys":174}],86:[function(require,module,exports){
|
|
require('../../modules/es6.object.set-prototype-of');
|
|
module.exports = require('../../modules/_core').Object.setPrototypeOf;
|
|
|
|
},{"../../modules/_core":104,"../../modules/es6.object.set-prototype-of":175}],87:[function(require,module,exports){
|
|
require('../modules/es6.object.to-string');
|
|
require('../modules/es6.string.iterator');
|
|
require('../modules/web.dom.iterable');
|
|
require('../modules/es6.set');
|
|
require('../modules/es7.set.to-json');
|
|
require('../modules/es7.set.of');
|
|
require('../modules/es7.set.from');
|
|
module.exports = require('../modules/_core').Set;
|
|
|
|
},{"../modules/_core":104,"../modules/es6.object.to-string":176,"../modules/es6.set":177,"../modules/es6.string.iterator":178,"../modules/es7.set.from":180,"../modules/es7.set.of":181,"../modules/es7.set.to-json":182,"../modules/web.dom.iterable":185}],88:[function(require,module,exports){
|
|
require('../../modules/es6.symbol');
|
|
require('../../modules/es6.object.to-string');
|
|
require('../../modules/es7.symbol.async-iterator');
|
|
require('../../modules/es7.symbol.observable');
|
|
module.exports = require('../../modules/_core').Symbol;
|
|
|
|
},{"../../modules/_core":104,"../../modules/es6.object.to-string":176,"../../modules/es6.symbol":179,"../../modules/es7.symbol.async-iterator":183,"../../modules/es7.symbol.observable":184}],89:[function(require,module,exports){
|
|
require('../../modules/es6.string.iterator');
|
|
require('../../modules/web.dom.iterable');
|
|
module.exports = require('../../modules/_wks-ext').f('iterator');
|
|
|
|
},{"../../modules/_wks-ext":163,"../../modules/es6.string.iterator":178,"../../modules/web.dom.iterable":185}],90:[function(require,module,exports){
|
|
module.exports = function (it) {
|
|
if (typeof it != 'function') throw TypeError(it + ' is not a function!');
|
|
return it;
|
|
};
|
|
|
|
},{}],91:[function(require,module,exports){
|
|
module.exports = function () { /* empty */ };
|
|
|
|
},{}],92:[function(require,module,exports){
|
|
module.exports = function (it, Constructor, name, forbiddenField) {
|
|
if (!(it instanceof Constructor) || (forbiddenField !== undefined && forbiddenField in it)) {
|
|
throw TypeError(name + ': incorrect invocation!');
|
|
} return it;
|
|
};
|
|
|
|
},{}],93:[function(require,module,exports){
|
|
var isObject = require('./_is-object');
|
|
module.exports = function (it) {
|
|
if (!isObject(it)) throw TypeError(it + ' is not an object!');
|
|
return it;
|
|
};
|
|
|
|
},{"./_is-object":122}],94:[function(require,module,exports){
|
|
var forOf = require('./_for-of');
|
|
|
|
module.exports = function (iter, ITERATOR) {
|
|
var result = [];
|
|
forOf(iter, false, result.push, result, ITERATOR);
|
|
return result;
|
|
};
|
|
|
|
},{"./_for-of":113}],95:[function(require,module,exports){
|
|
// false -> Array#indexOf
|
|
// true -> Array#includes
|
|
var toIObject = require('./_to-iobject');
|
|
var toLength = require('./_to-length');
|
|
var toAbsoluteIndex = require('./_to-absolute-index');
|
|
module.exports = function (IS_INCLUDES) {
|
|
return function ($this, el, fromIndex) {
|
|
var O = toIObject($this);
|
|
var length = toLength(O.length);
|
|
var index = toAbsoluteIndex(fromIndex, length);
|
|
var value;
|
|
// Array#includes uses SameValueZero equality algorithm
|
|
// eslint-disable-next-line no-self-compare
|
|
if (IS_INCLUDES && el != el) while (length > index) {
|
|
value = O[index++];
|
|
// eslint-disable-next-line no-self-compare
|
|
if (value != value) return true;
|
|
// Array#indexOf ignores holes, Array#includes - not
|
|
} else for (;length > index; index++) if (IS_INCLUDES || index in O) {
|
|
if (O[index] === el) return IS_INCLUDES || index || 0;
|
|
} return !IS_INCLUDES && -1;
|
|
};
|
|
};
|
|
|
|
},{"./_to-absolute-index":154,"./_to-iobject":156,"./_to-length":157}],96:[function(require,module,exports){
|
|
// 0 -> Array#forEach
|
|
// 1 -> Array#map
|
|
// 2 -> Array#filter
|
|
// 3 -> Array#some
|
|
// 4 -> Array#every
|
|
// 5 -> Array#find
|
|
// 6 -> Array#findIndex
|
|
var ctx = require('./_ctx');
|
|
var IObject = require('./_iobject');
|
|
var toObject = require('./_to-object');
|
|
var toLength = require('./_to-length');
|
|
var asc = require('./_array-species-create');
|
|
module.exports = function (TYPE, $create) {
|
|
var IS_MAP = TYPE == 1;
|
|
var IS_FILTER = TYPE == 2;
|
|
var IS_SOME = TYPE == 3;
|
|
var IS_EVERY = TYPE == 4;
|
|
var IS_FIND_INDEX = TYPE == 6;
|
|
var NO_HOLES = TYPE == 5 || IS_FIND_INDEX;
|
|
var create = $create || asc;
|
|
return function ($this, callbackfn, that) {
|
|
var O = toObject($this);
|
|
var self = IObject(O);
|
|
var f = ctx(callbackfn, that, 3);
|
|
var length = toLength(self.length);
|
|
var index = 0;
|
|
var result = IS_MAP ? create($this, length) : IS_FILTER ? create($this, 0) : undefined;
|
|
var val, res;
|
|
for (;length > index; index++) if (NO_HOLES || index in self) {
|
|
val = self[index];
|
|
res = f(val, index, O);
|
|
if (TYPE) {
|
|
if (IS_MAP) result[index] = res; // map
|
|
else if (res) switch (TYPE) {
|
|
case 3: return true; // some
|
|
case 5: return val; // find
|
|
case 6: return index; // findIndex
|
|
case 2: result.push(val); // filter
|
|
} else if (IS_EVERY) return false; // every
|
|
}
|
|
}
|
|
return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : result;
|
|
};
|
|
};
|
|
|
|
},{"./_array-species-create":98,"./_ctx":105,"./_iobject":119,"./_to-length":157,"./_to-object":158}],97:[function(require,module,exports){
|
|
var isObject = require('./_is-object');
|
|
var isArray = require('./_is-array');
|
|
var SPECIES = require('./_wks')('species');
|
|
|
|
module.exports = function (original) {
|
|
var C;
|
|
if (isArray(original)) {
|
|
C = original.constructor;
|
|
// cross-realm fallback
|
|
if (typeof C == 'function' && (C === Array || isArray(C.prototype))) C = undefined;
|
|
if (isObject(C)) {
|
|
C = C[SPECIES];
|
|
if (C === null) C = undefined;
|
|
}
|
|
} return C === undefined ? Array : C;
|
|
};
|
|
|
|
},{"./_is-array":121,"./_is-object":122,"./_wks":164}],98:[function(require,module,exports){
|
|
// 9.4.2.3 ArraySpeciesCreate(originalArray, length)
|
|
var speciesConstructor = require('./_array-species-constructor');
|
|
|
|
module.exports = function (original, length) {
|
|
return new (speciesConstructor(original))(length);
|
|
};
|
|
|
|
},{"./_array-species-constructor":97}],99:[function(require,module,exports){
|
|
// getting tag from 19.1.3.6 Object.prototype.toString()
|
|
var cof = require('./_cof');
|
|
var TAG = require('./_wks')('toStringTag');
|
|
// ES3 wrong here
|
|
var ARG = cof(function () { return arguments; }()) == 'Arguments';
|
|
|
|
// fallback for IE11 Script Access Denied error
|
|
var tryGet = function (it, key) {
|
|
try {
|
|
return it[key];
|
|
} catch (e) { /* empty */ }
|
|
};
|
|
|
|
module.exports = function (it) {
|
|
var O, T, B;
|
|
return it === undefined ? 'Undefined' : it === null ? 'Null'
|
|
// @@toStringTag case
|
|
: typeof (T = tryGet(O = Object(it), TAG)) == 'string' ? T
|
|
// builtinTag case
|
|
: ARG ? cof(O)
|
|
// ES3 arguments fallback
|
|
: (B = cof(O)) == 'Object' && typeof O.callee == 'function' ? 'Arguments' : B;
|
|
};
|
|
|
|
},{"./_cof":100,"./_wks":164}],100:[function(require,module,exports){
|
|
var toString = {}.toString;
|
|
|
|
module.exports = function (it) {
|
|
return toString.call(it).slice(8, -1);
|
|
};
|
|
|
|
},{}],101:[function(require,module,exports){
|
|
'use strict';
|
|
var dP = require('./_object-dp').f;
|
|
var create = require('./_object-create');
|
|
var redefineAll = require('./_redefine-all');
|
|
var ctx = require('./_ctx');
|
|
var anInstance = require('./_an-instance');
|
|
var forOf = require('./_for-of');
|
|
var $iterDefine = require('./_iter-define');
|
|
var step = require('./_iter-step');
|
|
var setSpecies = require('./_set-species');
|
|
var DESCRIPTORS = require('./_descriptors');
|
|
var fastKey = require('./_meta').fastKey;
|
|
var validate = require('./_validate-collection');
|
|
var SIZE = DESCRIPTORS ? '_s' : 'size';
|
|
|
|
var getEntry = function (that, key) {
|
|
// fast case
|
|
var index = fastKey(key);
|
|
var entry;
|
|
if (index !== 'F') return that._i[index];
|
|
// frozen object case
|
|
for (entry = that._f; entry; entry = entry.n) {
|
|
if (entry.k == key) return entry;
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
getConstructor: function (wrapper, NAME, IS_MAP, ADDER) {
|
|
var C = wrapper(function (that, iterable) {
|
|
anInstance(that, C, NAME, '_i');
|
|
that._t = NAME; // collection type
|
|
that._i = create(null); // index
|
|
that._f = undefined; // first entry
|
|
that._l = undefined; // last entry
|
|
that[SIZE] = 0; // size
|
|
if (iterable != undefined) forOf(iterable, IS_MAP, that[ADDER], that);
|
|
});
|
|
redefineAll(C.prototype, {
|
|
// 23.1.3.1 Map.prototype.clear()
|
|
// 23.2.3.2 Set.prototype.clear()
|
|
clear: function clear() {
|
|
for (var that = validate(this, NAME), data = that._i, entry = that._f; entry; entry = entry.n) {
|
|
entry.r = true;
|
|
if (entry.p) entry.p = entry.p.n = undefined;
|
|
delete data[entry.i];
|
|
}
|
|
that._f = that._l = undefined;
|
|
that[SIZE] = 0;
|
|
},
|
|
// 23.1.3.3 Map.prototype.delete(key)
|
|
// 23.2.3.4 Set.prototype.delete(value)
|
|
'delete': function (key) {
|
|
var that = validate(this, NAME);
|
|
var entry = getEntry(that, key);
|
|
if (entry) {
|
|
var next = entry.n;
|
|
var prev = entry.p;
|
|
delete that._i[entry.i];
|
|
entry.r = true;
|
|
if (prev) prev.n = next;
|
|
if (next) next.p = prev;
|
|
if (that._f == entry) that._f = next;
|
|
if (that._l == entry) that._l = prev;
|
|
that[SIZE]--;
|
|
} return !!entry;
|
|
},
|
|
// 23.2.3.6 Set.prototype.forEach(callbackfn, thisArg = undefined)
|
|
// 23.1.3.5 Map.prototype.forEach(callbackfn, thisArg = undefined)
|
|
forEach: function forEach(callbackfn /* , that = undefined */) {
|
|
validate(this, NAME);
|
|
var f = ctx(callbackfn, arguments.length > 1 ? arguments[1] : undefined, 3);
|
|
var entry;
|
|
while (entry = entry ? entry.n : this._f) {
|
|
f(entry.v, entry.k, this);
|
|
// revert to the last existing entry
|
|
while (entry && entry.r) entry = entry.p;
|
|
}
|
|
},
|
|
// 23.1.3.7 Map.prototype.has(key)
|
|
// 23.2.3.7 Set.prototype.has(value)
|
|
has: function has(key) {
|
|
return !!getEntry(validate(this, NAME), key);
|
|
}
|
|
});
|
|
if (DESCRIPTORS) dP(C.prototype, 'size', {
|
|
get: function () {
|
|
return validate(this, NAME)[SIZE];
|
|
}
|
|
});
|
|
return C;
|
|
},
|
|
def: function (that, key, value) {
|
|
var entry = getEntry(that, key);
|
|
var prev, index;
|
|
// change existing entry
|
|
if (entry) {
|
|
entry.v = value;
|
|
// create new entry
|
|
} else {
|
|
that._l = entry = {
|
|
i: index = fastKey(key, true), // <- index
|
|
k: key, // <- key
|
|
v: value, // <- value
|
|
p: prev = that._l, // <- previous entry
|
|
n: undefined, // <- next entry
|
|
r: false // <- removed
|
|
};
|
|
if (!that._f) that._f = entry;
|
|
if (prev) prev.n = entry;
|
|
that[SIZE]++;
|
|
// add to index
|
|
if (index !== 'F') that._i[index] = entry;
|
|
} return that;
|
|
},
|
|
getEntry: getEntry,
|
|
setStrong: function (C, NAME, IS_MAP) {
|
|
// add .keys, .values, .entries, [@@iterator]
|
|
// 23.1.3.4, 23.1.3.8, 23.1.3.11, 23.1.3.12, 23.2.3.5, 23.2.3.8, 23.2.3.10, 23.2.3.11
|
|
$iterDefine(C, NAME, function (iterated, kind) {
|
|
this._t = validate(iterated, NAME); // target
|
|
this._k = kind; // kind
|
|
this._l = undefined; // previous
|
|
}, function () {
|
|
var that = this;
|
|
var kind = that._k;
|
|
var entry = that._l;
|
|
// revert to the last existing entry
|
|
while (entry && entry.r) entry = entry.p;
|
|
// get next entry
|
|
if (!that._t || !(that._l = entry = entry ? entry.n : that._t._f)) {
|
|
// or finish the iteration
|
|
that._t = undefined;
|
|
return step(1);
|
|
}
|
|
// return step by kind
|
|
if (kind == 'keys') return step(0, entry.k);
|
|
if (kind == 'values') return step(0, entry.v);
|
|
return step(0, [entry.k, entry.v]);
|
|
}, IS_MAP ? 'entries' : 'values', !IS_MAP, true);
|
|
|
|
// add [@@species], 23.1.2.2, 23.2.2.2
|
|
setSpecies(NAME);
|
|
}
|
|
};
|
|
|
|
},{"./_an-instance":92,"./_ctx":105,"./_descriptors":107,"./_for-of":113,"./_iter-define":125,"./_iter-step":126,"./_meta":129,"./_object-create":131,"./_object-dp":132,"./_redefine-all":144,"./_set-species":149,"./_validate-collection":161}],102:[function(require,module,exports){
|
|
// https://github.com/DavidBruant/Map-Set.prototype.toJSON
|
|
var classof = require('./_classof');
|
|
var from = require('./_array-from-iterable');
|
|
module.exports = function (NAME) {
|
|
return function toJSON() {
|
|
if (classof(this) != NAME) throw TypeError(NAME + "#toJSON isn't generic");
|
|
return from(this);
|
|
};
|
|
};
|
|
|
|
},{"./_array-from-iterable":94,"./_classof":99}],103:[function(require,module,exports){
|
|
'use strict';
|
|
var global = require('./_global');
|
|
var $export = require('./_export');
|
|
var meta = require('./_meta');
|
|
var fails = require('./_fails');
|
|
var hide = require('./_hide');
|
|
var redefineAll = require('./_redefine-all');
|
|
var forOf = require('./_for-of');
|
|
var anInstance = require('./_an-instance');
|
|
var isObject = require('./_is-object');
|
|
var setToStringTag = require('./_set-to-string-tag');
|
|
var dP = require('./_object-dp').f;
|
|
var each = require('./_array-methods')(0);
|
|
var DESCRIPTORS = require('./_descriptors');
|
|
|
|
module.exports = function (NAME, wrapper, methods, common, IS_MAP, IS_WEAK) {
|
|
var Base = global[NAME];
|
|
var C = Base;
|
|
var ADDER = IS_MAP ? 'set' : 'add';
|
|
var proto = C && C.prototype;
|
|
var O = {};
|
|
if (!DESCRIPTORS || typeof C != 'function' || !(IS_WEAK || proto.forEach && !fails(function () {
|
|
new C().entries().next();
|
|
}))) {
|
|
// create collection constructor
|
|
C = common.getConstructor(wrapper, NAME, IS_MAP, ADDER);
|
|
redefineAll(C.prototype, methods);
|
|
meta.NEED = true;
|
|
} else {
|
|
C = wrapper(function (target, iterable) {
|
|
anInstance(target, C, NAME, '_c');
|
|
target._c = new Base();
|
|
if (iterable != undefined) forOf(iterable, IS_MAP, target[ADDER], target);
|
|
});
|
|
each('add,clear,delete,forEach,get,has,set,keys,values,entries,toJSON'.split(','), function (KEY) {
|
|
var IS_ADDER = KEY == 'add' || KEY == 'set';
|
|
if (KEY in proto && !(IS_WEAK && KEY == 'clear')) hide(C.prototype, KEY, function (a, b) {
|
|
anInstance(this, C, KEY);
|
|
if (!IS_ADDER && IS_WEAK && !isObject(a)) return KEY == 'get' ? undefined : false;
|
|
var result = this._c[KEY](a === 0 ? 0 : a, b);
|
|
return IS_ADDER ? this : result;
|
|
});
|
|
});
|
|
IS_WEAK || dP(C.prototype, 'size', {
|
|
get: function () {
|
|
return this._c.size;
|
|
}
|
|
});
|
|
}
|
|
|
|
setToStringTag(C, NAME);
|
|
|
|
O[NAME] = C;
|
|
$export($export.G + $export.W + $export.F, O);
|
|
|
|
if (!IS_WEAK) common.setStrong(C, NAME, IS_MAP);
|
|
|
|
return C;
|
|
};
|
|
|
|
},{"./_an-instance":92,"./_array-methods":96,"./_descriptors":107,"./_export":111,"./_fails":112,"./_for-of":113,"./_global":114,"./_hide":116,"./_is-object":122,"./_meta":129,"./_object-dp":132,"./_redefine-all":144,"./_set-to-string-tag":150}],104:[function(require,module,exports){
|
|
var core = module.exports = { version: '2.5.6' };
|
|
if (typeof __e == 'number') __e = core; // eslint-disable-line no-undef
|
|
|
|
},{}],105:[function(require,module,exports){
|
|
// optional / simple context binding
|
|
var aFunction = require('./_a-function');
|
|
module.exports = function (fn, that, length) {
|
|
aFunction(fn);
|
|
if (that === undefined) return fn;
|
|
switch (length) {
|
|
case 1: return function (a) {
|
|
return fn.call(that, a);
|
|
};
|
|
case 2: return function (a, b) {
|
|
return fn.call(that, a, b);
|
|
};
|
|
case 3: return function (a, b, c) {
|
|
return fn.call(that, a, b, c);
|
|
};
|
|
}
|
|
return function (/* ...args */) {
|
|
return fn.apply(that, arguments);
|
|
};
|
|
};
|
|
|
|
},{"./_a-function":90}],106:[function(require,module,exports){
|
|
// 7.2.1 RequireObjectCoercible(argument)
|
|
module.exports = function (it) {
|
|
if (it == undefined) throw TypeError("Can't call method on " + it);
|
|
return it;
|
|
};
|
|
|
|
},{}],107:[function(require,module,exports){
|
|
// Thank's IE8 for his funny defineProperty
|
|
module.exports = !require('./_fails')(function () {
|
|
return Object.defineProperty({}, 'a', { get: function () { return 7; } }).a != 7;
|
|
});
|
|
|
|
},{"./_fails":112}],108:[function(require,module,exports){
|
|
var isObject = require('./_is-object');
|
|
var document = require('./_global').document;
|
|
// typeof document.createElement is 'object' in old IE
|
|
var is = isObject(document) && isObject(document.createElement);
|
|
module.exports = function (it) {
|
|
return is ? document.createElement(it) : {};
|
|
};
|
|
|
|
},{"./_global":114,"./_is-object":122}],109:[function(require,module,exports){
|
|
// IE 8- don't enum bug keys
|
|
module.exports = (
|
|
'constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf'
|
|
).split(',');
|
|
|
|
},{}],110:[function(require,module,exports){
|
|
// all enumerable object keys, includes symbols
|
|
var getKeys = require('./_object-keys');
|
|
var gOPS = require('./_object-gops');
|
|
var pIE = require('./_object-pie');
|
|
module.exports = function (it) {
|
|
var result = getKeys(it);
|
|
var getSymbols = gOPS.f;
|
|
if (getSymbols) {
|
|
var symbols = getSymbols(it);
|
|
var isEnum = pIE.f;
|
|
var i = 0;
|
|
var key;
|
|
while (symbols.length > i) if (isEnum.call(it, key = symbols[i++])) result.push(key);
|
|
} return result;
|
|
};
|
|
|
|
},{"./_object-gops":137,"./_object-keys":140,"./_object-pie":141}],111:[function(require,module,exports){
|
|
var global = require('./_global');
|
|
var core = require('./_core');
|
|
var ctx = require('./_ctx');
|
|
var hide = require('./_hide');
|
|
var has = require('./_has');
|
|
var PROTOTYPE = 'prototype';
|
|
|
|
var $export = function (type, name, source) {
|
|
var IS_FORCED = type & $export.F;
|
|
var IS_GLOBAL = type & $export.G;
|
|
var IS_STATIC = type & $export.S;
|
|
var IS_PROTO = type & $export.P;
|
|
var IS_BIND = type & $export.B;
|
|
var IS_WRAP = type & $export.W;
|
|
var exports = IS_GLOBAL ? core : core[name] || (core[name] = {});
|
|
var expProto = exports[PROTOTYPE];
|
|
var target = IS_GLOBAL ? global : IS_STATIC ? global[name] : (global[name] || {})[PROTOTYPE];
|
|
var key, own, out;
|
|
if (IS_GLOBAL) source = name;
|
|
for (key in source) {
|
|
// contains in native
|
|
own = !IS_FORCED && target && target[key] !== undefined;
|
|
if (own && has(exports, key)) continue;
|
|
// export native or passed
|
|
out = own ? target[key] : source[key];
|
|
// prevent global pollution for namespaces
|
|
exports[key] = IS_GLOBAL && typeof target[key] != 'function' ? source[key]
|
|
// bind timers to global for call from export context
|
|
: IS_BIND && own ? ctx(out, global)
|
|
// wrap global constructors for prevent change them in library
|
|
: IS_WRAP && target[key] == out ? (function (C) {
|
|
var F = function (a, b, c) {
|
|
if (this instanceof C) {
|
|
switch (arguments.length) {
|
|
case 0: return new C();
|
|
case 1: return new C(a);
|
|
case 2: return new C(a, b);
|
|
} return new C(a, b, c);
|
|
} return C.apply(this, arguments);
|
|
};
|
|
F[PROTOTYPE] = C[PROTOTYPE];
|
|
return F;
|
|
// make static versions for prototype methods
|
|
})(out) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out;
|
|
// export proto methods to core.%CONSTRUCTOR%.methods.%NAME%
|
|
if (IS_PROTO) {
|
|
(exports.virtual || (exports.virtual = {}))[key] = out;
|
|
// export proto methods to core.%CONSTRUCTOR%.prototype.%NAME%
|
|
if (type & $export.R && expProto && !expProto[key]) hide(expProto, key, out);
|
|
}
|
|
}
|
|
};
|
|
// type bitmap
|
|
$export.F = 1; // forced
|
|
$export.G = 2; // global
|
|
$export.S = 4; // static
|
|
$export.P = 8; // proto
|
|
$export.B = 16; // bind
|
|
$export.W = 32; // wrap
|
|
$export.U = 64; // safe
|
|
$export.R = 128; // real proto method for `library`
|
|
module.exports = $export;
|
|
|
|
},{"./_core":104,"./_ctx":105,"./_global":114,"./_has":115,"./_hide":116}],112:[function(require,module,exports){
|
|
module.exports = function (exec) {
|
|
try {
|
|
return !!exec();
|
|
} catch (e) {
|
|
return true;
|
|
}
|
|
};
|
|
|
|
},{}],113:[function(require,module,exports){
|
|
var ctx = require('./_ctx');
|
|
var call = require('./_iter-call');
|
|
var isArrayIter = require('./_is-array-iter');
|
|
var anObject = require('./_an-object');
|
|
var toLength = require('./_to-length');
|
|
var getIterFn = require('./core.get-iterator-method');
|
|
var BREAK = {};
|
|
var RETURN = {};
|
|
var exports = module.exports = function (iterable, entries, fn, that, ITERATOR) {
|
|
var iterFn = ITERATOR ? function () { return iterable; } : getIterFn(iterable);
|
|
var f = ctx(fn, that, entries ? 2 : 1);
|
|
var index = 0;
|
|
var length, step, iterator, result;
|
|
if (typeof iterFn != 'function') throw TypeError(iterable + ' is not iterable!');
|
|
// fast case for arrays with default iterator
|
|
if (isArrayIter(iterFn)) for (length = toLength(iterable.length); length > index; index++) {
|
|
result = entries ? f(anObject(step = iterable[index])[0], step[1]) : f(iterable[index]);
|
|
if (result === BREAK || result === RETURN) return result;
|
|
} else for (iterator = iterFn.call(iterable); !(step = iterator.next()).done;) {
|
|
result = call(iterator, f, step.value, entries);
|
|
if (result === BREAK || result === RETURN) return result;
|
|
}
|
|
};
|
|
exports.BREAK = BREAK;
|
|
exports.RETURN = RETURN;
|
|
|
|
},{"./_an-object":93,"./_ctx":105,"./_is-array-iter":120,"./_iter-call":123,"./_to-length":157,"./core.get-iterator-method":165}],114:[function(require,module,exports){
|
|
// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028
|
|
var global = module.exports = typeof window != 'undefined' && window.Math == Math
|
|
? window : typeof self != 'undefined' && self.Math == Math ? self
|
|
// eslint-disable-next-line no-new-func
|
|
: Function('return this')();
|
|
if (typeof __g == 'number') __g = global; // eslint-disable-line no-undef
|
|
|
|
},{}],115:[function(require,module,exports){
|
|
var hasOwnProperty = {}.hasOwnProperty;
|
|
module.exports = function (it, key) {
|
|
return hasOwnProperty.call(it, key);
|
|
};
|
|
|
|
},{}],116:[function(require,module,exports){
|
|
var dP = require('./_object-dp');
|
|
var createDesc = require('./_property-desc');
|
|
module.exports = require('./_descriptors') ? function (object, key, value) {
|
|
return dP.f(object, key, createDesc(1, value));
|
|
} : function (object, key, value) {
|
|
object[key] = value;
|
|
return object;
|
|
};
|
|
|
|
},{"./_descriptors":107,"./_object-dp":132,"./_property-desc":143}],117:[function(require,module,exports){
|
|
var document = require('./_global').document;
|
|
module.exports = document && document.documentElement;
|
|
|
|
},{"./_global":114}],118:[function(require,module,exports){
|
|
module.exports = !require('./_descriptors') && !require('./_fails')(function () {
|
|
return Object.defineProperty(require('./_dom-create')('div'), 'a', { get: function () { return 7; } }).a != 7;
|
|
});
|
|
|
|
},{"./_descriptors":107,"./_dom-create":108,"./_fails":112}],119:[function(require,module,exports){
|
|
// fallback for non-array-like ES3 and non-enumerable old V8 strings
|
|
var cof = require('./_cof');
|
|
// eslint-disable-next-line no-prototype-builtins
|
|
module.exports = Object('z').propertyIsEnumerable(0) ? Object : function (it) {
|
|
return cof(it) == 'String' ? it.split('') : Object(it);
|
|
};
|
|
|
|
},{"./_cof":100}],120:[function(require,module,exports){
|
|
// check on default Array iterator
|
|
var Iterators = require('./_iterators');
|
|
var ITERATOR = require('./_wks')('iterator');
|
|
var ArrayProto = Array.prototype;
|
|
|
|
module.exports = function (it) {
|
|
return it !== undefined && (Iterators.Array === it || ArrayProto[ITERATOR] === it);
|
|
};
|
|
|
|
},{"./_iterators":127,"./_wks":164}],121:[function(require,module,exports){
|
|
// 7.2.2 IsArray(argument)
|
|
var cof = require('./_cof');
|
|
module.exports = Array.isArray || function isArray(arg) {
|
|
return cof(arg) == 'Array';
|
|
};
|
|
|
|
},{"./_cof":100}],122:[function(require,module,exports){
|
|
module.exports = function (it) {
|
|
return typeof it === 'object' ? it !== null : typeof it === 'function';
|
|
};
|
|
|
|
},{}],123:[function(require,module,exports){
|
|
// call something on iterator step with safe closing on error
|
|
var anObject = require('./_an-object');
|
|
module.exports = function (iterator, fn, value, entries) {
|
|
try {
|
|
return entries ? fn(anObject(value)[0], value[1]) : fn(value);
|
|
// 7.4.6 IteratorClose(iterator, completion)
|
|
} catch (e) {
|
|
var ret = iterator['return'];
|
|
if (ret !== undefined) anObject(ret.call(iterator));
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
},{"./_an-object":93}],124:[function(require,module,exports){
|
|
'use strict';
|
|
var create = require('./_object-create');
|
|
var descriptor = require('./_property-desc');
|
|
var setToStringTag = require('./_set-to-string-tag');
|
|
var IteratorPrototype = {};
|
|
|
|
// 25.1.2.1.1 %IteratorPrototype%[@@iterator]()
|
|
require('./_hide')(IteratorPrototype, require('./_wks')('iterator'), function () { return this; });
|
|
|
|
module.exports = function (Constructor, NAME, next) {
|
|
Constructor.prototype = create(IteratorPrototype, { next: descriptor(1, next) });
|
|
setToStringTag(Constructor, NAME + ' Iterator');
|
|
};
|
|
|
|
},{"./_hide":116,"./_object-create":131,"./_property-desc":143,"./_set-to-string-tag":150,"./_wks":164}],125:[function(require,module,exports){
|
|
'use strict';
|
|
var LIBRARY = require('./_library');
|
|
var $export = require('./_export');
|
|
var redefine = require('./_redefine');
|
|
var hide = require('./_hide');
|
|
var Iterators = require('./_iterators');
|
|
var $iterCreate = require('./_iter-create');
|
|
var setToStringTag = require('./_set-to-string-tag');
|
|
var getPrototypeOf = require('./_object-gpo');
|
|
var ITERATOR = require('./_wks')('iterator');
|
|
var BUGGY = !([].keys && 'next' in [].keys()); // Safari has buggy iterators w/o `next`
|
|
var FF_ITERATOR = '@@iterator';
|
|
var KEYS = 'keys';
|
|
var VALUES = 'values';
|
|
|
|
var returnThis = function () { return this; };
|
|
|
|
module.exports = function (Base, NAME, Constructor, next, DEFAULT, IS_SET, FORCED) {
|
|
$iterCreate(Constructor, NAME, next);
|
|
var getMethod = function (kind) {
|
|
if (!BUGGY && kind in proto) return proto[kind];
|
|
switch (kind) {
|
|
case KEYS: return function keys() { return new Constructor(this, kind); };
|
|
case VALUES: return function values() { return new Constructor(this, kind); };
|
|
} return function entries() { return new Constructor(this, kind); };
|
|
};
|
|
var TAG = NAME + ' Iterator';
|
|
var DEF_VALUES = DEFAULT == VALUES;
|
|
var VALUES_BUG = false;
|
|
var proto = Base.prototype;
|
|
var $native = proto[ITERATOR] || proto[FF_ITERATOR] || DEFAULT && proto[DEFAULT];
|
|
var $default = $native || getMethod(DEFAULT);
|
|
var $entries = DEFAULT ? !DEF_VALUES ? $default : getMethod('entries') : undefined;
|
|
var $anyNative = NAME == 'Array' ? proto.entries || $native : $native;
|
|
var methods, key, IteratorPrototype;
|
|
// Fix native
|
|
if ($anyNative) {
|
|
IteratorPrototype = getPrototypeOf($anyNative.call(new Base()));
|
|
if (IteratorPrototype !== Object.prototype && IteratorPrototype.next) {
|
|
// Set @@toStringTag to native iterators
|
|
setToStringTag(IteratorPrototype, TAG, true);
|
|
// fix for some old engines
|
|
if (!LIBRARY && typeof IteratorPrototype[ITERATOR] != 'function') hide(IteratorPrototype, ITERATOR, returnThis);
|
|
}
|
|
}
|
|
// fix Array#{values, @@iterator}.name in V8 / FF
|
|
if (DEF_VALUES && $native && $native.name !== VALUES) {
|
|
VALUES_BUG = true;
|
|
$default = function values() { return $native.call(this); };
|
|
}
|
|
// Define iterator
|
|
if ((!LIBRARY || FORCED) && (BUGGY || VALUES_BUG || !proto[ITERATOR])) {
|
|
hide(proto, ITERATOR, $default);
|
|
}
|
|
// Plug for library
|
|
Iterators[NAME] = $default;
|
|
Iterators[TAG] = returnThis;
|
|
if (DEFAULT) {
|
|
methods = {
|
|
values: DEF_VALUES ? $default : getMethod(VALUES),
|
|
keys: IS_SET ? $default : getMethod(KEYS),
|
|
entries: $entries
|
|
};
|
|
if (FORCED) for (key in methods) {
|
|
if (!(key in proto)) redefine(proto, key, methods[key]);
|
|
} else $export($export.P + $export.F * (BUGGY || VALUES_BUG), NAME, methods);
|
|
}
|
|
return methods;
|
|
};
|
|
|
|
},{"./_export":111,"./_hide":116,"./_iter-create":124,"./_iterators":127,"./_library":128,"./_object-gpo":138,"./_redefine":145,"./_set-to-string-tag":150,"./_wks":164}],126:[function(require,module,exports){
|
|
module.exports = function (done, value) {
|
|
return { value: value, done: !!done };
|
|
};
|
|
|
|
},{}],127:[function(require,module,exports){
|
|
module.exports = {};
|
|
|
|
},{}],128:[function(require,module,exports){
|
|
module.exports = true;
|
|
|
|
},{}],129:[function(require,module,exports){
|
|
var META = require('./_uid')('meta');
|
|
var isObject = require('./_is-object');
|
|
var has = require('./_has');
|
|
var setDesc = require('./_object-dp').f;
|
|
var id = 0;
|
|
var isExtensible = Object.isExtensible || function () {
|
|
return true;
|
|
};
|
|
var FREEZE = !require('./_fails')(function () {
|
|
return isExtensible(Object.preventExtensions({}));
|
|
});
|
|
var setMeta = function (it) {
|
|
setDesc(it, META, { value: {
|
|
i: 'O' + ++id, // object ID
|
|
w: {} // weak collections IDs
|
|
} });
|
|
};
|
|
var fastKey = function (it, create) {
|
|
// return primitive with prefix
|
|
if (!isObject(it)) return typeof it == 'symbol' ? it : (typeof it == 'string' ? 'S' : 'P') + it;
|
|
if (!has(it, META)) {
|
|
// can't set metadata to uncaught frozen object
|
|
if (!isExtensible(it)) return 'F';
|
|
// not necessary to add metadata
|
|
if (!create) return 'E';
|
|
// add missing metadata
|
|
setMeta(it);
|
|
// return object ID
|
|
} return it[META].i;
|
|
};
|
|
var getWeak = function (it, create) {
|
|
if (!has(it, META)) {
|
|
// can't set metadata to uncaught frozen object
|
|
if (!isExtensible(it)) return true;
|
|
// not necessary to add metadata
|
|
if (!create) return false;
|
|
// add missing metadata
|
|
setMeta(it);
|
|
// return hash weak collections IDs
|
|
} return it[META].w;
|
|
};
|
|
// add metadata on freeze-family methods calling
|
|
var onFreeze = function (it) {
|
|
if (FREEZE && meta.NEED && isExtensible(it) && !has(it, META)) setMeta(it);
|
|
return it;
|
|
};
|
|
var meta = module.exports = {
|
|
KEY: META,
|
|
NEED: false,
|
|
fastKey: fastKey,
|
|
getWeak: getWeak,
|
|
onFreeze: onFreeze
|
|
};
|
|
|
|
},{"./_fails":112,"./_has":115,"./_is-object":122,"./_object-dp":132,"./_uid":160}],130:[function(require,module,exports){
|
|
'use strict';
|
|
// 19.1.2.1 Object.assign(target, source, ...)
|
|
var getKeys = require('./_object-keys');
|
|
var gOPS = require('./_object-gops');
|
|
var pIE = require('./_object-pie');
|
|
var toObject = require('./_to-object');
|
|
var IObject = require('./_iobject');
|
|
var $assign = Object.assign;
|
|
|
|
// should work with symbols and should have deterministic property order (V8 bug)
|
|
module.exports = !$assign || require('./_fails')(function () {
|
|
var A = {};
|
|
var B = {};
|
|
// eslint-disable-next-line no-undef
|
|
var S = Symbol();
|
|
var K = 'abcdefghijklmnopqrst';
|
|
A[S] = 7;
|
|
K.split('').forEach(function (k) { B[k] = k; });
|
|
return $assign({}, A)[S] != 7 || Object.keys($assign({}, B)).join('') != K;
|
|
}) ? function assign(target, source) { // eslint-disable-line no-unused-vars
|
|
var T = toObject(target);
|
|
var aLen = arguments.length;
|
|
var index = 1;
|
|
var getSymbols = gOPS.f;
|
|
var isEnum = pIE.f;
|
|
while (aLen > index) {
|
|
var S = IObject(arguments[index++]);
|
|
var keys = getSymbols ? getKeys(S).concat(getSymbols(S)) : getKeys(S);
|
|
var length = keys.length;
|
|
var j = 0;
|
|
var key;
|
|
while (length > j) if (isEnum.call(S, key = keys[j++])) T[key] = S[key];
|
|
} return T;
|
|
} : $assign;
|
|
|
|
},{"./_fails":112,"./_iobject":119,"./_object-gops":137,"./_object-keys":140,"./_object-pie":141,"./_to-object":158}],131:[function(require,module,exports){
|
|
// 19.1.2.2 / 15.2.3.5 Object.create(O [, Properties])
|
|
var anObject = require('./_an-object');
|
|
var dPs = require('./_object-dps');
|
|
var enumBugKeys = require('./_enum-bug-keys');
|
|
var IE_PROTO = require('./_shared-key')('IE_PROTO');
|
|
var Empty = function () { /* empty */ };
|
|
var PROTOTYPE = 'prototype';
|
|
|
|
// Create object with fake `null` prototype: use iframe Object with cleared prototype
|
|
var createDict = function () {
|
|
// Thrash, waste and sodomy: IE GC bug
|
|
var iframe = require('./_dom-create')('iframe');
|
|
var i = enumBugKeys.length;
|
|
var lt = '<';
|
|
var gt = '>';
|
|
var iframeDocument;
|
|
iframe.style.display = 'none';
|
|
require('./_html').appendChild(iframe);
|
|
iframe.src = 'javascript:'; // eslint-disable-line no-script-url
|
|
// createDict = iframe.contentWindow.Object;
|
|
// html.removeChild(iframe);
|
|
iframeDocument = iframe.contentWindow.document;
|
|
iframeDocument.open();
|
|
iframeDocument.write(lt + 'script' + gt + 'document.F=Object' + lt + '/script' + gt);
|
|
iframeDocument.close();
|
|
createDict = iframeDocument.F;
|
|
while (i--) delete createDict[PROTOTYPE][enumBugKeys[i]];
|
|
return createDict();
|
|
};
|
|
|
|
module.exports = Object.create || function create(O, Properties) {
|
|
var result;
|
|
if (O !== null) {
|
|
Empty[PROTOTYPE] = anObject(O);
|
|
result = new Empty();
|
|
Empty[PROTOTYPE] = null;
|
|
// add "__proto__" for Object.getPrototypeOf polyfill
|
|
result[IE_PROTO] = O;
|
|
} else result = createDict();
|
|
return Properties === undefined ? result : dPs(result, Properties);
|
|
};
|
|
|
|
},{"./_an-object":93,"./_dom-create":108,"./_enum-bug-keys":109,"./_html":117,"./_object-dps":133,"./_shared-key":151}],132:[function(require,module,exports){
|
|
var anObject = require('./_an-object');
|
|
var IE8_DOM_DEFINE = require('./_ie8-dom-define');
|
|
var toPrimitive = require('./_to-primitive');
|
|
var dP = Object.defineProperty;
|
|
|
|
exports.f = require('./_descriptors') ? Object.defineProperty : function defineProperty(O, P, Attributes) {
|
|
anObject(O);
|
|
P = toPrimitive(P, true);
|
|
anObject(Attributes);
|
|
if (IE8_DOM_DEFINE) try {
|
|
return dP(O, P, Attributes);
|
|
} catch (e) { /* empty */ }
|
|
if ('get' in Attributes || 'set' in Attributes) throw TypeError('Accessors not supported!');
|
|
if ('value' in Attributes) O[P] = Attributes.value;
|
|
return O;
|
|
};
|
|
|
|
},{"./_an-object":93,"./_descriptors":107,"./_ie8-dom-define":118,"./_to-primitive":159}],133:[function(require,module,exports){
|
|
var dP = require('./_object-dp');
|
|
var anObject = require('./_an-object');
|
|
var getKeys = require('./_object-keys');
|
|
|
|
module.exports = require('./_descriptors') ? Object.defineProperties : function defineProperties(O, Properties) {
|
|
anObject(O);
|
|
var keys = getKeys(Properties);
|
|
var length = keys.length;
|
|
var i = 0;
|
|
var P;
|
|
while (length > i) dP.f(O, P = keys[i++], Properties[P]);
|
|
return O;
|
|
};
|
|
|
|
},{"./_an-object":93,"./_descriptors":107,"./_object-dp":132,"./_object-keys":140}],134:[function(require,module,exports){
|
|
var pIE = require('./_object-pie');
|
|
var createDesc = require('./_property-desc');
|
|
var toIObject = require('./_to-iobject');
|
|
var toPrimitive = require('./_to-primitive');
|
|
var has = require('./_has');
|
|
var IE8_DOM_DEFINE = require('./_ie8-dom-define');
|
|
var gOPD = Object.getOwnPropertyDescriptor;
|
|
|
|
exports.f = require('./_descriptors') ? gOPD : function getOwnPropertyDescriptor(O, P) {
|
|
O = toIObject(O);
|
|
P = toPrimitive(P, true);
|
|
if (IE8_DOM_DEFINE) try {
|
|
return gOPD(O, P);
|
|
} catch (e) { /* empty */ }
|
|
if (has(O, P)) return createDesc(!pIE.f.call(O, P), O[P]);
|
|
};
|
|
|
|
},{"./_descriptors":107,"./_has":115,"./_ie8-dom-define":118,"./_object-pie":141,"./_property-desc":143,"./_to-iobject":156,"./_to-primitive":159}],135:[function(require,module,exports){
|
|
// fallback for IE11 buggy Object.getOwnPropertyNames with iframe and window
|
|
var toIObject = require('./_to-iobject');
|
|
var gOPN = require('./_object-gopn').f;
|
|
var toString = {}.toString;
|
|
|
|
var windowNames = typeof window == 'object' && window && Object.getOwnPropertyNames
|
|
? Object.getOwnPropertyNames(window) : [];
|
|
|
|
var getWindowNames = function (it) {
|
|
try {
|
|
return gOPN(it);
|
|
} catch (e) {
|
|
return windowNames.slice();
|
|
}
|
|
};
|
|
|
|
module.exports.f = function getOwnPropertyNames(it) {
|
|
return windowNames && toString.call(it) == '[object Window]' ? getWindowNames(it) : gOPN(toIObject(it));
|
|
};
|
|
|
|
},{"./_object-gopn":136,"./_to-iobject":156}],136:[function(require,module,exports){
|
|
// 19.1.2.7 / 15.2.3.4 Object.getOwnPropertyNames(O)
|
|
var $keys = require('./_object-keys-internal');
|
|
var hiddenKeys = require('./_enum-bug-keys').concat('length', 'prototype');
|
|
|
|
exports.f = Object.getOwnPropertyNames || function getOwnPropertyNames(O) {
|
|
return $keys(O, hiddenKeys);
|
|
};
|
|
|
|
},{"./_enum-bug-keys":109,"./_object-keys-internal":139}],137:[function(require,module,exports){
|
|
exports.f = Object.getOwnPropertySymbols;
|
|
|
|
},{}],138:[function(require,module,exports){
|
|
// 19.1.2.9 / 15.2.3.2 Object.getPrototypeOf(O)
|
|
var has = require('./_has');
|
|
var toObject = require('./_to-object');
|
|
var IE_PROTO = require('./_shared-key')('IE_PROTO');
|
|
var ObjectProto = Object.prototype;
|
|
|
|
module.exports = Object.getPrototypeOf || function (O) {
|
|
O = toObject(O);
|
|
if (has(O, IE_PROTO)) return O[IE_PROTO];
|
|
if (typeof O.constructor == 'function' && O instanceof O.constructor) {
|
|
return O.constructor.prototype;
|
|
} return O instanceof Object ? ObjectProto : null;
|
|
};
|
|
|
|
},{"./_has":115,"./_shared-key":151,"./_to-object":158}],139:[function(require,module,exports){
|
|
var has = require('./_has');
|
|
var toIObject = require('./_to-iobject');
|
|
var arrayIndexOf = require('./_array-includes')(false);
|
|
var IE_PROTO = require('./_shared-key')('IE_PROTO');
|
|
|
|
module.exports = function (object, names) {
|
|
var O = toIObject(object);
|
|
var i = 0;
|
|
var result = [];
|
|
var key;
|
|
for (key in O) if (key != IE_PROTO) has(O, key) && result.push(key);
|
|
// Don't enum bug & hidden keys
|
|
while (names.length > i) if (has(O, key = names[i++])) {
|
|
~arrayIndexOf(result, key) || result.push(key);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
},{"./_array-includes":95,"./_has":115,"./_shared-key":151,"./_to-iobject":156}],140:[function(require,module,exports){
|
|
// 19.1.2.14 / 15.2.3.14 Object.keys(O)
|
|
var $keys = require('./_object-keys-internal');
|
|
var enumBugKeys = require('./_enum-bug-keys');
|
|
|
|
module.exports = Object.keys || function keys(O) {
|
|
return $keys(O, enumBugKeys);
|
|
};
|
|
|
|
},{"./_enum-bug-keys":109,"./_object-keys-internal":139}],141:[function(require,module,exports){
|
|
exports.f = {}.propertyIsEnumerable;
|
|
|
|
},{}],142:[function(require,module,exports){
|
|
// most Object methods by ES6 should accept primitives
|
|
var $export = require('./_export');
|
|
var core = require('./_core');
|
|
var fails = require('./_fails');
|
|
module.exports = function (KEY, exec) {
|
|
var fn = (core.Object || {})[KEY] || Object[KEY];
|
|
var exp = {};
|
|
exp[KEY] = exec(fn);
|
|
$export($export.S + $export.F * fails(function () { fn(1); }), 'Object', exp);
|
|
};
|
|
|
|
},{"./_core":104,"./_export":111,"./_fails":112}],143:[function(require,module,exports){
|
|
module.exports = function (bitmap, value) {
|
|
return {
|
|
enumerable: !(bitmap & 1),
|
|
configurable: !(bitmap & 2),
|
|
writable: !(bitmap & 4),
|
|
value: value
|
|
};
|
|
};
|
|
|
|
},{}],144:[function(require,module,exports){
|
|
var hide = require('./_hide');
|
|
module.exports = function (target, src, safe) {
|
|
for (var key in src) {
|
|
if (safe && target[key]) target[key] = src[key];
|
|
else hide(target, key, src[key]);
|
|
} return target;
|
|
};
|
|
|
|
},{"./_hide":116}],145:[function(require,module,exports){
|
|
module.exports = require('./_hide');
|
|
|
|
},{"./_hide":116}],146:[function(require,module,exports){
|
|
'use strict';
|
|
// https://tc39.github.io/proposal-setmap-offrom/
|
|
var $export = require('./_export');
|
|
var aFunction = require('./_a-function');
|
|
var ctx = require('./_ctx');
|
|
var forOf = require('./_for-of');
|
|
|
|
module.exports = function (COLLECTION) {
|
|
$export($export.S, COLLECTION, { from: function from(source /* , mapFn, thisArg */) {
|
|
var mapFn = arguments[1];
|
|
var mapping, A, n, cb;
|
|
aFunction(this);
|
|
mapping = mapFn !== undefined;
|
|
if (mapping) aFunction(mapFn);
|
|
if (source == undefined) return new this();
|
|
A = [];
|
|
if (mapping) {
|
|
n = 0;
|
|
cb = ctx(mapFn, arguments[2], 2);
|
|
forOf(source, false, function (nextItem) {
|
|
A.push(cb(nextItem, n++));
|
|
});
|
|
} else {
|
|
forOf(source, false, A.push, A);
|
|
}
|
|
return new this(A);
|
|
} });
|
|
};
|
|
|
|
},{"./_a-function":90,"./_ctx":105,"./_export":111,"./_for-of":113}],147:[function(require,module,exports){
|
|
'use strict';
|
|
// https://tc39.github.io/proposal-setmap-offrom/
|
|
var $export = require('./_export');
|
|
|
|
module.exports = function (COLLECTION) {
|
|
$export($export.S, COLLECTION, { of: function of() {
|
|
var length = arguments.length;
|
|
var A = new Array(length);
|
|
while (length--) A[length] = arguments[length];
|
|
return new this(A);
|
|
} });
|
|
};
|
|
|
|
},{"./_export":111}],148:[function(require,module,exports){
|
|
// Works with __proto__ only. Old v8 can't work with null proto objects.
|
|
/* eslint-disable no-proto */
|
|
var isObject = require('./_is-object');
|
|
var anObject = require('./_an-object');
|
|
var check = function (O, proto) {
|
|
anObject(O);
|
|
if (!isObject(proto) && proto !== null) throw TypeError(proto + ": can't set as prototype!");
|
|
};
|
|
module.exports = {
|
|
set: Object.setPrototypeOf || ('__proto__' in {} ? // eslint-disable-line
|
|
function (test, buggy, set) {
|
|
try {
|
|
set = require('./_ctx')(Function.call, require('./_object-gopd').f(Object.prototype, '__proto__').set, 2);
|
|
set(test, []);
|
|
buggy = !(test instanceof Array);
|
|
} catch (e) { buggy = true; }
|
|
return function setPrototypeOf(O, proto) {
|
|
check(O, proto);
|
|
if (buggy) O.__proto__ = proto;
|
|
else set(O, proto);
|
|
return O;
|
|
};
|
|
}({}, false) : undefined),
|
|
check: check
|
|
};
|
|
|
|
},{"./_an-object":93,"./_ctx":105,"./_is-object":122,"./_object-gopd":134}],149:[function(require,module,exports){
|
|
'use strict';
|
|
var global = require('./_global');
|
|
var core = require('./_core');
|
|
var dP = require('./_object-dp');
|
|
var DESCRIPTORS = require('./_descriptors');
|
|
var SPECIES = require('./_wks')('species');
|
|
|
|
module.exports = function (KEY) {
|
|
var C = typeof core[KEY] == 'function' ? core[KEY] : global[KEY];
|
|
if (DESCRIPTORS && C && !C[SPECIES]) dP.f(C, SPECIES, {
|
|
configurable: true,
|
|
get: function () { return this; }
|
|
});
|
|
};
|
|
|
|
},{"./_core":104,"./_descriptors":107,"./_global":114,"./_object-dp":132,"./_wks":164}],150:[function(require,module,exports){
|
|
var def = require('./_object-dp').f;
|
|
var has = require('./_has');
|
|
var TAG = require('./_wks')('toStringTag');
|
|
|
|
module.exports = function (it, tag, stat) {
|
|
if (it && !has(it = stat ? it : it.prototype, TAG)) def(it, TAG, { configurable: true, value: tag });
|
|
};
|
|
|
|
},{"./_has":115,"./_object-dp":132,"./_wks":164}],151:[function(require,module,exports){
|
|
var shared = require('./_shared')('keys');
|
|
var uid = require('./_uid');
|
|
module.exports = function (key) {
|
|
return shared[key] || (shared[key] = uid(key));
|
|
};
|
|
|
|
},{"./_shared":152,"./_uid":160}],152:[function(require,module,exports){
|
|
var core = require('./_core');
|
|
var global = require('./_global');
|
|
var SHARED = '__core-js_shared__';
|
|
var store = global[SHARED] || (global[SHARED] = {});
|
|
|
|
(module.exports = function (key, value) {
|
|
return store[key] || (store[key] = value !== undefined ? value : {});
|
|
})('versions', []).push({
|
|
version: core.version,
|
|
mode: require('./_library') ? 'pure' : 'global',
|
|
copyright: '© 2018 Denis Pushkarev (zloirock.ru)'
|
|
});
|
|
|
|
},{"./_core":104,"./_global":114,"./_library":128}],153:[function(require,module,exports){
|
|
var toInteger = require('./_to-integer');
|
|
var defined = require('./_defined');
|
|
// true -> String#at
|
|
// false -> String#codePointAt
|
|
module.exports = function (TO_STRING) {
|
|
return function (that, pos) {
|
|
var s = String(defined(that));
|
|
var i = toInteger(pos);
|
|
var l = s.length;
|
|
var a, b;
|
|
if (i < 0 || i >= l) return TO_STRING ? '' : undefined;
|
|
a = s.charCodeAt(i);
|
|
return a < 0xd800 || a > 0xdbff || i + 1 === l || (b = s.charCodeAt(i + 1)) < 0xdc00 || b > 0xdfff
|
|
? TO_STRING ? s.charAt(i) : a
|
|
: TO_STRING ? s.slice(i, i + 2) : (a - 0xd800 << 10) + (b - 0xdc00) + 0x10000;
|
|
};
|
|
};
|
|
|
|
},{"./_defined":106,"./_to-integer":155}],154:[function(require,module,exports){
|
|
var toInteger = require('./_to-integer');
|
|
var max = Math.max;
|
|
var min = Math.min;
|
|
module.exports = function (index, length) {
|
|
index = toInteger(index);
|
|
return index < 0 ? max(index + length, 0) : min(index, length);
|
|
};
|
|
|
|
},{"./_to-integer":155}],155:[function(require,module,exports){
|
|
// 7.1.4 ToInteger
|
|
var ceil = Math.ceil;
|
|
var floor = Math.floor;
|
|
module.exports = function (it) {
|
|
return isNaN(it = +it) ? 0 : (it > 0 ? floor : ceil)(it);
|
|
};
|
|
|
|
},{}],156:[function(require,module,exports){
|
|
// to indexed object, toObject with fallback for non-array-like ES3 strings
|
|
var IObject = require('./_iobject');
|
|
var defined = require('./_defined');
|
|
module.exports = function (it) {
|
|
return IObject(defined(it));
|
|
};
|
|
|
|
},{"./_defined":106,"./_iobject":119}],157:[function(require,module,exports){
|
|
// 7.1.15 ToLength
|
|
var toInteger = require('./_to-integer');
|
|
var min = Math.min;
|
|
module.exports = function (it) {
|
|
return it > 0 ? min(toInteger(it), 0x1fffffffffffff) : 0; // pow(2, 53) - 1 == 9007199254740991
|
|
};
|
|
|
|
},{"./_to-integer":155}],158:[function(require,module,exports){
|
|
// 7.1.13 ToObject(argument)
|
|
var defined = require('./_defined');
|
|
module.exports = function (it) {
|
|
return Object(defined(it));
|
|
};
|
|
|
|
},{"./_defined":106}],159:[function(require,module,exports){
|
|
// 7.1.1 ToPrimitive(input [, PreferredType])
|
|
var isObject = require('./_is-object');
|
|
// instead of the ES6 spec version, we didn't implement @@toPrimitive case
|
|
// and the second argument - flag - preferred type is a string
|
|
module.exports = function (it, S) {
|
|
if (!isObject(it)) return it;
|
|
var fn, val;
|
|
if (S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it))) return val;
|
|
if (typeof (fn = it.valueOf) == 'function' && !isObject(val = fn.call(it))) return val;
|
|
if (!S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it))) return val;
|
|
throw TypeError("Can't convert object to primitive value");
|
|
};
|
|
|
|
},{"./_is-object":122}],160:[function(require,module,exports){
|
|
var id = 0;
|
|
var px = Math.random();
|
|
module.exports = function (key) {
|
|
return 'Symbol('.concat(key === undefined ? '' : key, ')_', (++id + px).toString(36));
|
|
};
|
|
|
|
},{}],161:[function(require,module,exports){
|
|
var isObject = require('./_is-object');
|
|
module.exports = function (it, TYPE) {
|
|
if (!isObject(it) || it._t !== TYPE) throw TypeError('Incompatible receiver, ' + TYPE + ' required!');
|
|
return it;
|
|
};
|
|
|
|
},{"./_is-object":122}],162:[function(require,module,exports){
|
|
var global = require('./_global');
|
|
var core = require('./_core');
|
|
var LIBRARY = require('./_library');
|
|
var wksExt = require('./_wks-ext');
|
|
var defineProperty = require('./_object-dp').f;
|
|
module.exports = function (name) {
|
|
var $Symbol = core.Symbol || (core.Symbol = LIBRARY ? {} : global.Symbol || {});
|
|
if (name.charAt(0) != '_' && !(name in $Symbol)) defineProperty($Symbol, name, { value: wksExt.f(name) });
|
|
};
|
|
|
|
},{"./_core":104,"./_global":114,"./_library":128,"./_object-dp":132,"./_wks-ext":163}],163:[function(require,module,exports){
|
|
exports.f = require('./_wks');
|
|
|
|
},{"./_wks":164}],164:[function(require,module,exports){
|
|
var store = require('./_shared')('wks');
|
|
var uid = require('./_uid');
|
|
var Symbol = require('./_global').Symbol;
|
|
var USE_SYMBOL = typeof Symbol == 'function';
|
|
|
|
var $exports = module.exports = function (name) {
|
|
return store[name] || (store[name] =
|
|
USE_SYMBOL && Symbol[name] || (USE_SYMBOL ? Symbol : uid)('Symbol.' + name));
|
|
};
|
|
|
|
$exports.store = store;
|
|
|
|
},{"./_global":114,"./_shared":152,"./_uid":160}],165:[function(require,module,exports){
|
|
var classof = require('./_classof');
|
|
var ITERATOR = require('./_wks')('iterator');
|
|
var Iterators = require('./_iterators');
|
|
module.exports = require('./_core').getIteratorMethod = function (it) {
|
|
if (it != undefined) return it[ITERATOR]
|
|
|| it['@@iterator']
|
|
|| Iterators[classof(it)];
|
|
};
|
|
|
|
},{"./_classof":99,"./_core":104,"./_iterators":127,"./_wks":164}],166:[function(require,module,exports){
|
|
var anObject = require('./_an-object');
|
|
var get = require('./core.get-iterator-method');
|
|
module.exports = require('./_core').getIterator = function (it) {
|
|
var iterFn = get(it);
|
|
if (typeof iterFn != 'function') throw TypeError(it + ' is not iterable!');
|
|
return anObject(iterFn.call(it));
|
|
};
|
|
|
|
},{"./_an-object":93,"./_core":104,"./core.get-iterator-method":165}],167:[function(require,module,exports){
|
|
var classof = require('./_classof');
|
|
var ITERATOR = require('./_wks')('iterator');
|
|
var Iterators = require('./_iterators');
|
|
module.exports = require('./_core').isIterable = function (it) {
|
|
var O = Object(it);
|
|
return O[ITERATOR] !== undefined
|
|
|| '@@iterator' in O
|
|
// eslint-disable-next-line no-prototype-builtins
|
|
|| Iterators.hasOwnProperty(classof(O));
|
|
};
|
|
|
|
},{"./_classof":99,"./_core":104,"./_iterators":127,"./_wks":164}],168:[function(require,module,exports){
|
|
'use strict';
|
|
var addToUnscopables = require('./_add-to-unscopables');
|
|
var step = require('./_iter-step');
|
|
var Iterators = require('./_iterators');
|
|
var toIObject = require('./_to-iobject');
|
|
|
|
// 22.1.3.4 Array.prototype.entries()
|
|
// 22.1.3.13 Array.prototype.keys()
|
|
// 22.1.3.29 Array.prototype.values()
|
|
// 22.1.3.30 Array.prototype[@@iterator]()
|
|
module.exports = require('./_iter-define')(Array, 'Array', function (iterated, kind) {
|
|
this._t = toIObject(iterated); // target
|
|
this._i = 0; // next index
|
|
this._k = kind; // kind
|
|
// 22.1.5.2.1 %ArrayIteratorPrototype%.next()
|
|
}, function () {
|
|
var O = this._t;
|
|
var kind = this._k;
|
|
var index = this._i++;
|
|
if (!O || index >= O.length) {
|
|
this._t = undefined;
|
|
return step(1);
|
|
}
|
|
if (kind == 'keys') return step(0, index);
|
|
if (kind == 'values') return step(0, O[index]);
|
|
return step(0, [index, O[index]]);
|
|
}, 'values');
|
|
|
|
// argumentsList[@@iterator] is %ArrayProto_values% (9.4.4.6, 9.4.4.7)
|
|
Iterators.Arguments = Iterators.Array;
|
|
|
|
addToUnscopables('keys');
|
|
addToUnscopables('values');
|
|
addToUnscopables('entries');
|
|
|
|
},{"./_add-to-unscopables":91,"./_iter-define":125,"./_iter-step":126,"./_iterators":127,"./_to-iobject":156}],169:[function(require,module,exports){
|
|
// 19.1.3.1 Object.assign(target, source)
|
|
var $export = require('./_export');
|
|
|
|
$export($export.S + $export.F, 'Object', { assign: require('./_object-assign') });
|
|
|
|
},{"./_export":111,"./_object-assign":130}],170:[function(require,module,exports){
|
|
var $export = require('./_export');
|
|
// 19.1.2.2 / 15.2.3.5 Object.create(O [, Properties])
|
|
$export($export.S, 'Object', { create: require('./_object-create') });
|
|
|
|
},{"./_export":111,"./_object-create":131}],171:[function(require,module,exports){
|
|
var $export = require('./_export');
|
|
// 19.1.2.4 / 15.2.3.6 Object.defineProperty(O, P, Attributes)
|
|
$export($export.S + $export.F * !require('./_descriptors'), 'Object', { defineProperty: require('./_object-dp').f });
|
|
|
|
},{"./_descriptors":107,"./_export":111,"./_object-dp":132}],172:[function(require,module,exports){
|
|
// 19.1.2.5 Object.freeze(O)
|
|
var isObject = require('./_is-object');
|
|
var meta = require('./_meta').onFreeze;
|
|
|
|
require('./_object-sap')('freeze', function ($freeze) {
|
|
return function freeze(it) {
|
|
return $freeze && isObject(it) ? $freeze(meta(it)) : it;
|
|
};
|
|
});
|
|
|
|
},{"./_is-object":122,"./_meta":129,"./_object-sap":142}],173:[function(require,module,exports){
|
|
// 19.1.2.9 Object.getPrototypeOf(O)
|
|
var toObject = require('./_to-object');
|
|
var $getPrototypeOf = require('./_object-gpo');
|
|
|
|
require('./_object-sap')('getPrototypeOf', function () {
|
|
return function getPrototypeOf(it) {
|
|
return $getPrototypeOf(toObject(it));
|
|
};
|
|
});
|
|
|
|
},{"./_object-gpo":138,"./_object-sap":142,"./_to-object":158}],174:[function(require,module,exports){
|
|
// 19.1.2.14 Object.keys(O)
|
|
var toObject = require('./_to-object');
|
|
var $keys = require('./_object-keys');
|
|
|
|
require('./_object-sap')('keys', function () {
|
|
return function keys(it) {
|
|
return $keys(toObject(it));
|
|
};
|
|
});
|
|
|
|
},{"./_object-keys":140,"./_object-sap":142,"./_to-object":158}],175:[function(require,module,exports){
|
|
// 19.1.3.19 Object.setPrototypeOf(O, proto)
|
|
var $export = require('./_export');
|
|
$export($export.S, 'Object', { setPrototypeOf: require('./_set-proto').set });
|
|
|
|
},{"./_export":111,"./_set-proto":148}],176:[function(require,module,exports){
|
|
|
|
},{}],177:[function(require,module,exports){
|
|
'use strict';
|
|
var strong = require('./_collection-strong');
|
|
var validate = require('./_validate-collection');
|
|
var SET = 'Set';
|
|
|
|
// 23.2 Set Objects
|
|
module.exports = require('./_collection')(SET, function (get) {
|
|
return function Set() { return get(this, arguments.length > 0 ? arguments[0] : undefined); };
|
|
}, {
|
|
// 23.2.3.1 Set.prototype.add(value)
|
|
add: function add(value) {
|
|
return strong.def(validate(this, SET), value = value === 0 ? 0 : value, value);
|
|
}
|
|
}, strong);
|
|
|
|
},{"./_collection":103,"./_collection-strong":101,"./_validate-collection":161}],178:[function(require,module,exports){
|
|
'use strict';
|
|
var $at = require('./_string-at')(true);
|
|
|
|
// 21.1.3.27 String.prototype[@@iterator]()
|
|
require('./_iter-define')(String, 'String', function (iterated) {
|
|
this._t = String(iterated); // target
|
|
this._i = 0; // next index
|
|
// 21.1.5.2.1 %StringIteratorPrototype%.next()
|
|
}, function () {
|
|
var O = this._t;
|
|
var index = this._i;
|
|
var point;
|
|
if (index >= O.length) return { value: undefined, done: true };
|
|
point = $at(O, index);
|
|
this._i += point.length;
|
|
return { value: point, done: false };
|
|
});
|
|
|
|
},{"./_iter-define":125,"./_string-at":153}],179:[function(require,module,exports){
|
|
'use strict';
|
|
// ECMAScript 6 symbols shim
|
|
var global = require('./_global');
|
|
var has = require('./_has');
|
|
var DESCRIPTORS = require('./_descriptors');
|
|
var $export = require('./_export');
|
|
var redefine = require('./_redefine');
|
|
var META = require('./_meta').KEY;
|
|
var $fails = require('./_fails');
|
|
var shared = require('./_shared');
|
|
var setToStringTag = require('./_set-to-string-tag');
|
|
var uid = require('./_uid');
|
|
var wks = require('./_wks');
|
|
var wksExt = require('./_wks-ext');
|
|
var wksDefine = require('./_wks-define');
|
|
var enumKeys = require('./_enum-keys');
|
|
var isArray = require('./_is-array');
|
|
var anObject = require('./_an-object');
|
|
var isObject = require('./_is-object');
|
|
var toIObject = require('./_to-iobject');
|
|
var toPrimitive = require('./_to-primitive');
|
|
var createDesc = require('./_property-desc');
|
|
var _create = require('./_object-create');
|
|
var gOPNExt = require('./_object-gopn-ext');
|
|
var $GOPD = require('./_object-gopd');
|
|
var $DP = require('./_object-dp');
|
|
var $keys = require('./_object-keys');
|
|
var gOPD = $GOPD.f;
|
|
var dP = $DP.f;
|
|
var gOPN = gOPNExt.f;
|
|
var $Symbol = global.Symbol;
|
|
var $JSON = global.JSON;
|
|
var _stringify = $JSON && $JSON.stringify;
|
|
var PROTOTYPE = 'prototype';
|
|
var HIDDEN = wks('_hidden');
|
|
var TO_PRIMITIVE = wks('toPrimitive');
|
|
var isEnum = {}.propertyIsEnumerable;
|
|
var SymbolRegistry = shared('symbol-registry');
|
|
var AllSymbols = shared('symbols');
|
|
var OPSymbols = shared('op-symbols');
|
|
var ObjectProto = Object[PROTOTYPE];
|
|
var USE_NATIVE = typeof $Symbol == 'function';
|
|
var QObject = global.QObject;
|
|
// Don't use setters in Qt Script, https://github.com/zloirock/core-js/issues/173
|
|
var setter = !QObject || !QObject[PROTOTYPE] || !QObject[PROTOTYPE].findChild;
|
|
|
|
// fallback for old Android, https://code.google.com/p/v8/issues/detail?id=687
|
|
var setSymbolDesc = DESCRIPTORS && $fails(function () {
|
|
return _create(dP({}, 'a', {
|
|
get: function () { return dP(this, 'a', { value: 7 }).a; }
|
|
})).a != 7;
|
|
}) ? function (it, key, D) {
|
|
var protoDesc = gOPD(ObjectProto, key);
|
|
if (protoDesc) delete ObjectProto[key];
|
|
dP(it, key, D);
|
|
if (protoDesc && it !== ObjectProto) dP(ObjectProto, key, protoDesc);
|
|
} : dP;
|
|
|
|
var wrap = function (tag) {
|
|
var sym = AllSymbols[tag] = _create($Symbol[PROTOTYPE]);
|
|
sym._k = tag;
|
|
return sym;
|
|
};
|
|
|
|
var isSymbol = USE_NATIVE && typeof $Symbol.iterator == 'symbol' ? function (it) {
|
|
return typeof it == 'symbol';
|
|
} : function (it) {
|
|
return it instanceof $Symbol;
|
|
};
|
|
|
|
var $defineProperty = function defineProperty(it, key, D) {
|
|
if (it === ObjectProto) $defineProperty(OPSymbols, key, D);
|
|
anObject(it);
|
|
key = toPrimitive(key, true);
|
|
anObject(D);
|
|
if (has(AllSymbols, key)) {
|
|
if (!D.enumerable) {
|
|
if (!has(it, HIDDEN)) dP(it, HIDDEN, createDesc(1, {}));
|
|
it[HIDDEN][key] = true;
|
|
} else {
|
|
if (has(it, HIDDEN) && it[HIDDEN][key]) it[HIDDEN][key] = false;
|
|
D = _create(D, { enumerable: createDesc(0, false) });
|
|
} return setSymbolDesc(it, key, D);
|
|
} return dP(it, key, D);
|
|
};
|
|
var $defineProperties = function defineProperties(it, P) {
|
|
anObject(it);
|
|
var keys = enumKeys(P = toIObject(P));
|
|
var i = 0;
|
|
var l = keys.length;
|
|
var key;
|
|
while (l > i) $defineProperty(it, key = keys[i++], P[key]);
|
|
return it;
|
|
};
|
|
var $create = function create(it, P) {
|
|
return P === undefined ? _create(it) : $defineProperties(_create(it), P);
|
|
};
|
|
var $propertyIsEnumerable = function propertyIsEnumerable(key) {
|
|
var E = isEnum.call(this, key = toPrimitive(key, true));
|
|
if (this === ObjectProto && has(AllSymbols, key) && !has(OPSymbols, key)) return false;
|
|
return E || !has(this, key) || !has(AllSymbols, key) || has(this, HIDDEN) && this[HIDDEN][key] ? E : true;
|
|
};
|
|
var $getOwnPropertyDescriptor = function getOwnPropertyDescriptor(it, key) {
|
|
it = toIObject(it);
|
|
key = toPrimitive(key, true);
|
|
if (it === ObjectProto && has(AllSymbols, key) && !has(OPSymbols, key)) return;
|
|
var D = gOPD(it, key);
|
|
if (D && has(AllSymbols, key) && !(has(it, HIDDEN) && it[HIDDEN][key])) D.enumerable = true;
|
|
return D;
|
|
};
|
|
var $getOwnPropertyNames = function getOwnPropertyNames(it) {
|
|
var names = gOPN(toIObject(it));
|
|
var result = [];
|
|
var i = 0;
|
|
var key;
|
|
while (names.length > i) {
|
|
if (!has(AllSymbols, key = names[i++]) && key != HIDDEN && key != META) result.push(key);
|
|
} return result;
|
|
};
|
|
var $getOwnPropertySymbols = function getOwnPropertySymbols(it) {
|
|
var IS_OP = it === ObjectProto;
|
|
var names = gOPN(IS_OP ? OPSymbols : toIObject(it));
|
|
var result = [];
|
|
var i = 0;
|
|
var key;
|
|
while (names.length > i) {
|
|
if (has(AllSymbols, key = names[i++]) && (IS_OP ? has(ObjectProto, key) : true)) result.push(AllSymbols[key]);
|
|
} return result;
|
|
};
|
|
|
|
// 19.4.1.1 Symbol([description])
|
|
if (!USE_NATIVE) {
|
|
$Symbol = function Symbol() {
|
|
if (this instanceof $Symbol) throw TypeError('Symbol is not a constructor!');
|
|
var tag = uid(arguments.length > 0 ? arguments[0] : undefined);
|
|
var $set = function (value) {
|
|
if (this === ObjectProto) $set.call(OPSymbols, value);
|
|
if (has(this, HIDDEN) && has(this[HIDDEN], tag)) this[HIDDEN][tag] = false;
|
|
setSymbolDesc(this, tag, createDesc(1, value));
|
|
};
|
|
if (DESCRIPTORS && setter) setSymbolDesc(ObjectProto, tag, { configurable: true, set: $set });
|
|
return wrap(tag);
|
|
};
|
|
redefine($Symbol[PROTOTYPE], 'toString', function toString() {
|
|
return this._k;
|
|
});
|
|
|
|
$GOPD.f = $getOwnPropertyDescriptor;
|
|
$DP.f = $defineProperty;
|
|
require('./_object-gopn').f = gOPNExt.f = $getOwnPropertyNames;
|
|
require('./_object-pie').f = $propertyIsEnumerable;
|
|
require('./_object-gops').f = $getOwnPropertySymbols;
|
|
|
|
if (DESCRIPTORS && !require('./_library')) {
|
|
redefine(ObjectProto, 'propertyIsEnumerable', $propertyIsEnumerable, true);
|
|
}
|
|
|
|
wksExt.f = function (name) {
|
|
return wrap(wks(name));
|
|
};
|
|
}
|
|
|
|
$export($export.G + $export.W + $export.F * !USE_NATIVE, { Symbol: $Symbol });
|
|
|
|
for (var es6Symbols = (
|
|
// 19.4.2.2, 19.4.2.3, 19.4.2.4, 19.4.2.6, 19.4.2.8, 19.4.2.9, 19.4.2.10, 19.4.2.11, 19.4.2.12, 19.4.2.13, 19.4.2.14
|
|
'hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables'
|
|
).split(','), j = 0; es6Symbols.length > j;)wks(es6Symbols[j++]);
|
|
|
|
for (var wellKnownSymbols = $keys(wks.store), k = 0; wellKnownSymbols.length > k;) wksDefine(wellKnownSymbols[k++]);
|
|
|
|
$export($export.S + $export.F * !USE_NATIVE, 'Symbol', {
|
|
// 19.4.2.1 Symbol.for(key)
|
|
'for': function (key) {
|
|
return has(SymbolRegistry, key += '')
|
|
? SymbolRegistry[key]
|
|
: SymbolRegistry[key] = $Symbol(key);
|
|
},
|
|
// 19.4.2.5 Symbol.keyFor(sym)
|
|
keyFor: function keyFor(sym) {
|
|
if (!isSymbol(sym)) throw TypeError(sym + ' is not a symbol!');
|
|
for (var key in SymbolRegistry) if (SymbolRegistry[key] === sym) return key;
|
|
},
|
|
useSetter: function () { setter = true; },
|
|
useSimple: function () { setter = false; }
|
|
});
|
|
|
|
$export($export.S + $export.F * !USE_NATIVE, 'Object', {
|
|
// 19.1.2.2 Object.create(O [, Properties])
|
|
create: $create,
|
|
// 19.1.2.4 Object.defineProperty(O, P, Attributes)
|
|
defineProperty: $defineProperty,
|
|
// 19.1.2.3 Object.defineProperties(O, Properties)
|
|
defineProperties: $defineProperties,
|
|
// 19.1.2.6 Object.getOwnPropertyDescriptor(O, P)
|
|
getOwnPropertyDescriptor: $getOwnPropertyDescriptor,
|
|
// 19.1.2.7 Object.getOwnPropertyNames(O)
|
|
getOwnPropertyNames: $getOwnPropertyNames,
|
|
// 19.1.2.8 Object.getOwnPropertySymbols(O)
|
|
getOwnPropertySymbols: $getOwnPropertySymbols
|
|
});
|
|
|
|
// 24.3.2 JSON.stringify(value [, replacer [, space]])
|
|
$JSON && $export($export.S + $export.F * (!USE_NATIVE || $fails(function () {
|
|
var S = $Symbol();
|
|
// MS Edge converts symbol values to JSON as {}
|
|
// WebKit converts symbol values to JSON as null
|
|
// V8 throws on boxed symbols
|
|
return _stringify([S]) != '[null]' || _stringify({ a: S }) != '{}' || _stringify(Object(S)) != '{}';
|
|
})), 'JSON', {
|
|
stringify: function stringify(it) {
|
|
var args = [it];
|
|
var i = 1;
|
|
var replacer, $replacer;
|
|
while (arguments.length > i) args.push(arguments[i++]);
|
|
$replacer = replacer = args[1];
|
|
if (!isObject(replacer) && it === undefined || isSymbol(it)) return; // IE8 returns string on undefined
|
|
if (!isArray(replacer)) replacer = function (key, value) {
|
|
if (typeof $replacer == 'function') value = $replacer.call(this, key, value);
|
|
if (!isSymbol(value)) return value;
|
|
};
|
|
args[1] = replacer;
|
|
return _stringify.apply($JSON, args);
|
|
}
|
|
});
|
|
|
|
// 19.4.3.4 Symbol.prototype[@@toPrimitive](hint)
|
|
$Symbol[PROTOTYPE][TO_PRIMITIVE] || require('./_hide')($Symbol[PROTOTYPE], TO_PRIMITIVE, $Symbol[PROTOTYPE].valueOf);
|
|
// 19.4.3.5 Symbol.prototype[@@toStringTag]
|
|
setToStringTag($Symbol, 'Symbol');
|
|
// 20.2.1.9 Math[@@toStringTag]
|
|
setToStringTag(Math, 'Math', true);
|
|
// 24.3.3 JSON[@@toStringTag]
|
|
setToStringTag(global.JSON, 'JSON', true);
|
|
|
|
},{"./_an-object":93,"./_descriptors":107,"./_enum-keys":110,"./_export":111,"./_fails":112,"./_global":114,"./_has":115,"./_hide":116,"./_is-array":121,"./_is-object":122,"./_library":128,"./_meta":129,"./_object-create":131,"./_object-dp":132,"./_object-gopd":134,"./_object-gopn":136,"./_object-gopn-ext":135,"./_object-gops":137,"./_object-keys":140,"./_object-pie":141,"./_property-desc":143,"./_redefine":145,"./_set-to-string-tag":150,"./_shared":152,"./_to-iobject":156,"./_to-primitive":159,"./_uid":160,"./_wks":164,"./_wks-define":162,"./_wks-ext":163}],180:[function(require,module,exports){
|
|
// https://tc39.github.io/proposal-setmap-offrom/#sec-set.from
|
|
require('./_set-collection-from')('Set');
|
|
|
|
},{"./_set-collection-from":146}],181:[function(require,module,exports){
|
|
// https://tc39.github.io/proposal-setmap-offrom/#sec-set.of
|
|
require('./_set-collection-of')('Set');
|
|
|
|
},{"./_set-collection-of":147}],182:[function(require,module,exports){
|
|
// https://github.com/DavidBruant/Map-Set.prototype.toJSON
|
|
var $export = require('./_export');
|
|
|
|
$export($export.P + $export.R, 'Set', { toJSON: require('./_collection-to-json')('Set') });
|
|
|
|
},{"./_collection-to-json":102,"./_export":111}],183:[function(require,module,exports){
|
|
require('./_wks-define')('asyncIterator');
|
|
|
|
},{"./_wks-define":162}],184:[function(require,module,exports){
|
|
require('./_wks-define')('observable');
|
|
|
|
},{"./_wks-define":162}],185:[function(require,module,exports){
|
|
require('./es6.array.iterator');
|
|
var global = require('./_global');
|
|
var hide = require('./_hide');
|
|
var Iterators = require('./_iterators');
|
|
var TO_STRING_TAG = require('./_wks')('toStringTag');
|
|
|
|
var DOMIterables = ('CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,' +
|
|
'DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,' +
|
|
'MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,' +
|
|
'SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,' +
|
|
'TextTrackList,TouchList').split(',');
|
|
|
|
for (var i = 0; i < DOMIterables.length; i++) {
|
|
var NAME = DOMIterables[i];
|
|
var Collection = global[NAME];
|
|
var proto = Collection && Collection.prototype;
|
|
if (proto && !proto[TO_STRING_TAG]) hide(proto, TO_STRING_TAG, NAME);
|
|
Iterators[NAME] = Iterators.Array;
|
|
}
|
|
|
|
},{"./_global":114,"./_hide":116,"./_iterators":127,"./_wks":164,"./es6.array.iterator":168}],186:[function(require,module,exports){
|
|
// 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.
|
|
|
|
function EventEmitter() {
|
|
this._events = this._events || {};
|
|
this._maxListeners = this._maxListeners || undefined;
|
|
}
|
|
module.exports = EventEmitter;
|
|
|
|
// Backwards-compat with node 0.10.x
|
|
EventEmitter.EventEmitter = EventEmitter;
|
|
|
|
EventEmitter.prototype._events = undefined;
|
|
EventEmitter.prototype._maxListeners = undefined;
|
|
|
|
// By default EventEmitters will print a warning if more than 10 listeners are
|
|
// added to it. This is a useful default which helps finding memory leaks.
|
|
EventEmitter.defaultMaxListeners = 10;
|
|
|
|
// Obviously not all Emitters should be limited to 10. This function allows
|
|
// that to be increased. Set to zero for unlimited.
|
|
EventEmitter.prototype.setMaxListeners = function(n) {
|
|
if (!isNumber(n) || n < 0 || isNaN(n))
|
|
throw TypeError('n must be a positive number');
|
|
this._maxListeners = n;
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.emit = function(type) {
|
|
var er, handler, len, args, i, listeners;
|
|
|
|
if (!this._events)
|
|
this._events = {};
|
|
|
|
// If there is no 'error' event listener then throw.
|
|
if (type === 'error') {
|
|
if (!this._events.error ||
|
|
(isObject(this._events.error) && !this._events.error.length)) {
|
|
er = arguments[1];
|
|
if (er instanceof Error) {
|
|
throw er; // Unhandled 'error' event
|
|
} else {
|
|
// At least give some kind of context to the user
|
|
var err = new Error('Uncaught, unspecified "error" event. (' + er + ')');
|
|
err.context = er;
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
handler = this._events[type];
|
|
|
|
if (isUndefined(handler))
|
|
return false;
|
|
|
|
if (isFunction(handler)) {
|
|
switch (arguments.length) {
|
|
// fast cases
|
|
case 1:
|
|
handler.call(this);
|
|
break;
|
|
case 2:
|
|
handler.call(this, arguments[1]);
|
|
break;
|
|
case 3:
|
|
handler.call(this, arguments[1], arguments[2]);
|
|
break;
|
|
// slower
|
|
default:
|
|
args = Array.prototype.slice.call(arguments, 1);
|
|
handler.apply(this, args);
|
|
}
|
|
} else if (isObject(handler)) {
|
|
args = Array.prototype.slice.call(arguments, 1);
|
|
listeners = handler.slice();
|
|
len = listeners.length;
|
|
for (i = 0; i < len; i++)
|
|
listeners[i].apply(this, args);
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
EventEmitter.prototype.addListener = function(type, listener) {
|
|
var m;
|
|
|
|
if (!isFunction(listener))
|
|
throw TypeError('listener must be a function');
|
|
|
|
if (!this._events)
|
|
this._events = {};
|
|
|
|
// To avoid recursion in the case that type === "newListener"! Before
|
|
// adding it to the listeners, first emit "newListener".
|
|
if (this._events.newListener)
|
|
this.emit('newListener', type,
|
|
isFunction(listener.listener) ?
|
|
listener.listener : listener);
|
|
|
|
if (!this._events[type])
|
|
// Optimize the case of one listener. Don't need the extra array object.
|
|
this._events[type] = listener;
|
|
else if (isObject(this._events[type]))
|
|
// If we've already got an array, just append.
|
|
this._events[type].push(listener);
|
|
else
|
|
// Adding the second element, need to change to array.
|
|
this._events[type] = [this._events[type], listener];
|
|
|
|
// Check for listener leak
|
|
if (isObject(this._events[type]) && !this._events[type].warned) {
|
|
if (!isUndefined(this._maxListeners)) {
|
|
m = this._maxListeners;
|
|
} else {
|
|
m = EventEmitter.defaultMaxListeners;
|
|
}
|
|
|
|
if (m && m > 0 && this._events[type].length > m) {
|
|
this._events[type].warned = true;
|
|
console.error('(node) warning: possible EventEmitter memory ' +
|
|
'leak detected. %d listeners added. ' +
|
|
'Use emitter.setMaxListeners() to increase limit.',
|
|
this._events[type].length);
|
|
if (typeof console.trace === 'function') {
|
|
// not supported in IE 10
|
|
console.trace();
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
|
|
|
|
EventEmitter.prototype.once = function(type, listener) {
|
|
if (!isFunction(listener))
|
|
throw TypeError('listener must be a function');
|
|
|
|
var fired = false;
|
|
|
|
function g() {
|
|
this.removeListener(type, g);
|
|
|
|
if (!fired) {
|
|
fired = true;
|
|
listener.apply(this, arguments);
|
|
}
|
|
}
|
|
|
|
g.listener = listener;
|
|
this.on(type, g);
|
|
|
|
return this;
|
|
};
|
|
|
|
// emits a 'removeListener' event iff the listener was removed
|
|
EventEmitter.prototype.removeListener = function(type, listener) {
|
|
var list, position, length, i;
|
|
|
|
if (!isFunction(listener))
|
|
throw TypeError('listener must be a function');
|
|
|
|
if (!this._events || !this._events[type])
|
|
return this;
|
|
|
|
list = this._events[type];
|
|
length = list.length;
|
|
position = -1;
|
|
|
|
if (list === listener ||
|
|
(isFunction(list.listener) && list.listener === listener)) {
|
|
delete this._events[type];
|
|
if (this._events.removeListener)
|
|
this.emit('removeListener', type, listener);
|
|
|
|
} else if (isObject(list)) {
|
|
for (i = length; i-- > 0;) {
|
|
if (list[i] === listener ||
|
|
(list[i].listener && list[i].listener === listener)) {
|
|
position = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (position < 0)
|
|
return this;
|
|
|
|
if (list.length === 1) {
|
|
list.length = 0;
|
|
delete this._events[type];
|
|
} else {
|
|
list.splice(position, 1);
|
|
}
|
|
|
|
if (this._events.removeListener)
|
|
this.emit('removeListener', type, listener);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.removeAllListeners = function(type) {
|
|
var key, listeners;
|
|
|
|
if (!this._events)
|
|
return this;
|
|
|
|
// not listening for removeListener, no need to emit
|
|
if (!this._events.removeListener) {
|
|
if (arguments.length === 0)
|
|
this._events = {};
|
|
else if (this._events[type])
|
|
delete this._events[type];
|
|
return this;
|
|
}
|
|
|
|
// emit removeListener for all listeners on all events
|
|
if (arguments.length === 0) {
|
|
for (key in this._events) {
|
|
if (key === 'removeListener') continue;
|
|
this.removeAllListeners(key);
|
|
}
|
|
this.removeAllListeners('removeListener');
|
|
this._events = {};
|
|
return this;
|
|
}
|
|
|
|
listeners = this._events[type];
|
|
|
|
if (isFunction(listeners)) {
|
|
this.removeListener(type, listeners);
|
|
} else if (listeners) {
|
|
// LIFO order
|
|
while (listeners.length)
|
|
this.removeListener(type, listeners[listeners.length - 1]);
|
|
}
|
|
delete this._events[type];
|
|
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.listeners = function(type) {
|
|
var ret;
|
|
if (!this._events || !this._events[type])
|
|
ret = [];
|
|
else if (isFunction(this._events[type]))
|
|
ret = [this._events[type]];
|
|
else
|
|
ret = this._events[type].slice();
|
|
return ret;
|
|
};
|
|
|
|
EventEmitter.prototype.listenerCount = function(type) {
|
|
if (this._events) {
|
|
var evlistener = this._events[type];
|
|
|
|
if (isFunction(evlistener))
|
|
return 1;
|
|
else if (evlistener)
|
|
return evlistener.length;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
EventEmitter.listenerCount = function(emitter, type) {
|
|
return emitter.listenerCount(type);
|
|
};
|
|
|
|
function isFunction(arg) {
|
|
return typeof arg === 'function';
|
|
}
|
|
|
|
function isNumber(arg) {
|
|
return typeof arg === 'number';
|
|
}
|
|
|
|
function isObject(arg) {
|
|
return typeof arg === 'object' && arg !== null;
|
|
}
|
|
|
|
function isUndefined(arg) {
|
|
return arg === void 0;
|
|
}
|
|
|
|
},{}],187:[function(require,module,exports){
|
|
// shim for using process in browser
|
|
var process = module.exports = {};
|
|
|
|
// cached from whatever global is present so that test runners that stub it
|
|
// don't break things. But we need to wrap it in a try catch in case it is
|
|
// wrapped in strict mode code which doesn't define any globals. It's inside a
|
|
// function because try/catches deoptimize in certain engines.
|
|
|
|
var cachedSetTimeout;
|
|
var cachedClearTimeout;
|
|
|
|
function defaultSetTimout() {
|
|
throw new Error('setTimeout has not been defined');
|
|
}
|
|
function defaultClearTimeout () {
|
|
throw new Error('clearTimeout has not been defined');
|
|
}
|
|
(function () {
|
|
try {
|
|
if (typeof setTimeout === 'function') {
|
|
cachedSetTimeout = setTimeout;
|
|
} else {
|
|
cachedSetTimeout = defaultSetTimout;
|
|
}
|
|
} catch (e) {
|
|
cachedSetTimeout = defaultSetTimout;
|
|
}
|
|
try {
|
|
if (typeof clearTimeout === 'function') {
|
|
cachedClearTimeout = clearTimeout;
|
|
} else {
|
|
cachedClearTimeout = defaultClearTimeout;
|
|
}
|
|
} catch (e) {
|
|
cachedClearTimeout = defaultClearTimeout;
|
|
}
|
|
} ())
|
|
function runTimeout(fun) {
|
|
if (cachedSetTimeout === setTimeout) {
|
|
//normal enviroments in sane situations
|
|
return setTimeout(fun, 0);
|
|
}
|
|
// if setTimeout wasn't available but was latter defined
|
|
if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) {
|
|
cachedSetTimeout = setTimeout;
|
|
return setTimeout(fun, 0);
|
|
}
|
|
try {
|
|
// when when somebody has screwed with setTimeout but no I.E. maddness
|
|
return cachedSetTimeout(fun, 0);
|
|
} catch(e){
|
|
try {
|
|
// When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
|
|
return cachedSetTimeout.call(null, fun, 0);
|
|
} catch(e){
|
|
// same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error
|
|
return cachedSetTimeout.call(this, fun, 0);
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
function runClearTimeout(marker) {
|
|
if (cachedClearTimeout === clearTimeout) {
|
|
//normal enviroments in sane situations
|
|
return clearTimeout(marker);
|
|
}
|
|
// if clearTimeout wasn't available but was latter defined
|
|
if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) {
|
|
cachedClearTimeout = clearTimeout;
|
|
return clearTimeout(marker);
|
|
}
|
|
try {
|
|
// when when somebody has screwed with setTimeout but no I.E. maddness
|
|
return cachedClearTimeout(marker);
|
|
} catch (e){
|
|
try {
|
|
// When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
|
|
return cachedClearTimeout.call(null, marker);
|
|
} catch (e){
|
|
// same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error.
|
|
// Some versions of I.E. have different rules for clearTimeout vs setTimeout
|
|
return cachedClearTimeout.call(this, marker);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
}
|
|
var queue = [];
|
|
var draining = false;
|
|
var currentQueue;
|
|
var queueIndex = -1;
|
|
|
|
function cleanUpNextTick() {
|
|
if (!draining || !currentQueue) {
|
|
return;
|
|
}
|
|
draining = false;
|
|
if (currentQueue.length) {
|
|
queue = currentQueue.concat(queue);
|
|
} else {
|
|
queueIndex = -1;
|
|
}
|
|
if (queue.length) {
|
|
drainQueue();
|
|
}
|
|
}
|
|
|
|
function drainQueue() {
|
|
if (draining) {
|
|
return;
|
|
}
|
|
var timeout = runTimeout(cleanUpNextTick);
|
|
draining = true;
|
|
|
|
var len = queue.length;
|
|
while(len) {
|
|
currentQueue = queue;
|
|
queue = [];
|
|
while (++queueIndex < len) {
|
|
if (currentQueue) {
|
|
currentQueue[queueIndex].run();
|
|
}
|
|
}
|
|
queueIndex = -1;
|
|
len = queue.length;
|
|
}
|
|
currentQueue = null;
|
|
draining = false;
|
|
runClearTimeout(timeout);
|
|
}
|
|
|
|
process.nextTick = function (fun) {
|
|
var args = new Array(arguments.length - 1);
|
|
if (arguments.length > 1) {
|
|
for (var i = 1; i < arguments.length; i++) {
|
|
args[i - 1] = arguments[i];
|
|
}
|
|
}
|
|
queue.push(new Item(fun, args));
|
|
if (queue.length === 1 && !draining) {
|
|
runTimeout(drainQueue);
|
|
}
|
|
};
|
|
|
|
// v8 likes predictible objects
|
|
function Item(fun, array) {
|
|
this.fun = fun;
|
|
this.array = array;
|
|
}
|
|
Item.prototype.run = function () {
|
|
this.fun.apply(null, this.array);
|
|
};
|
|
process.title = 'browser';
|
|
process.browser = true;
|
|
process.env = {};
|
|
process.argv = [];
|
|
process.version = ''; // empty string to avoid regexp issues
|
|
process.versions = {};
|
|
|
|
function noop() {}
|
|
|
|
process.on = noop;
|
|
process.addListener = noop;
|
|
process.once = noop;
|
|
process.off = noop;
|
|
process.removeListener = noop;
|
|
process.removeAllListeners = noop;
|
|
process.emit = noop;
|
|
process.prependListener = noop;
|
|
process.prependOnceListener = noop;
|
|
|
|
process.listeners = function (name) { return [] }
|
|
|
|
process.binding = function (name) {
|
|
throw new Error('process.binding is not supported');
|
|
};
|
|
|
|
process.cwd = function () { return '/' };
|
|
process.chdir = function (dir) {
|
|
throw new Error('process.chdir is not supported');
|
|
};
|
|
process.umask = function() { return 0; };
|
|
|
|
},{}],188:[function(require,module,exports){
|
|
(function (global){
|
|
/*! https://mths.be/punycode v1.4.1 by @mathias */
|
|
;(function(root) {
|
|
|
|
/** Detect free variables */
|
|
var freeExports = typeof exports == 'object' && exports &&
|
|
!exports.nodeType && exports;
|
|
var freeModule = typeof module == 'object' && module &&
|
|
!module.nodeType && module;
|
|
var freeGlobal = typeof global == 'object' && global;
|
|
if (
|
|
freeGlobal.global === freeGlobal ||
|
|
freeGlobal.window === freeGlobal ||
|
|
freeGlobal.self === freeGlobal
|
|
) {
|
|
root = freeGlobal;
|
|
}
|
|
|
|
/**
|
|
* The `punycode` object.
|
|
* @name punycode
|
|
* @type Object
|
|
*/
|
|
var punycode,
|
|
|
|
/** Highest positive signed 32-bit float value */
|
|
maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1
|
|
|
|
/** Bootstring parameters */
|
|
base = 36,
|
|
tMin = 1,
|
|
tMax = 26,
|
|
skew = 38,
|
|
damp = 700,
|
|
initialBias = 72,
|
|
initialN = 128, // 0x80
|
|
delimiter = '-', // '\x2D'
|
|
|
|
/** Regular expressions */
|
|
regexPunycode = /^xn--/,
|
|
regexNonASCII = /[^\x20-\x7E]/, // unprintable ASCII chars + non-ASCII chars
|
|
regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g, // RFC 3490 separators
|
|
|
|
/** Error messages */
|
|
errors = {
|
|
'overflow': 'Overflow: input needs wider integers to process',
|
|
'not-basic': 'Illegal input >= 0x80 (not a basic code point)',
|
|
'invalid-input': 'Invalid input'
|
|
},
|
|
|
|
/** Convenience shortcuts */
|
|
baseMinusTMin = base - tMin,
|
|
floor = Math.floor,
|
|
stringFromCharCode = String.fromCharCode,
|
|
|
|
/** Temporary variable */
|
|
key;
|
|
|
|
/*--------------------------------------------------------------------------*/
|
|
|
|
/**
|
|
* A generic error utility function.
|
|
* @private
|
|
* @param {String} type The error type.
|
|
* @returns {Error} Throws a `RangeError` with the applicable error message.
|
|
*/
|
|
function error(type) {
|
|
throw new RangeError(errors[type]);
|
|
}
|
|
|
|
/**
|
|
* A generic `Array#map` utility function.
|
|
* @private
|
|
* @param {Array} array The array to iterate over.
|
|
* @param {Function} callback The function that gets called for every array
|
|
* item.
|
|
* @returns {Array} A new array of values returned by the callback function.
|
|
*/
|
|
function map(array, fn) {
|
|
var length = array.length;
|
|
var result = [];
|
|
while (length--) {
|
|
result[length] = fn(array[length]);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* A simple `Array#map`-like wrapper to work with domain name strings or email
|
|
* addresses.
|
|
* @private
|
|
* @param {String} domain The domain name or email address.
|
|
* @param {Function} callback The function that gets called for every
|
|
* character.
|
|
* @returns {Array} A new string of characters returned by the callback
|
|
* function.
|
|
*/
|
|
function mapDomain(string, fn) {
|
|
var parts = string.split('@');
|
|
var result = '';
|
|
if (parts.length > 1) {
|
|
// In email addresses, only the domain name should be punycoded. Leave
|
|
// the local part (i.e. everything up to `@`) intact.
|
|
result = parts[0] + '@';
|
|
string = parts[1];
|
|
}
|
|
// Avoid `split(regex)` for IE8 compatibility. See #17.
|
|
string = string.replace(regexSeparators, '\x2E');
|
|
var labels = string.split('.');
|
|
var encoded = map(labels, fn).join('.');
|
|
return result + encoded;
|
|
}
|
|
|
|
/**
|
|
* Creates an array containing the numeric code points of each Unicode
|
|
* character in the string. While JavaScript uses UCS-2 internally,
|
|
* this function will convert a pair of surrogate halves (each of which
|
|
* UCS-2 exposes as separate characters) into a single code point,
|
|
* matching UTF-16.
|
|
* @see `punycode.ucs2.encode`
|
|
* @see <https://mathiasbynens.be/notes/javascript-encoding>
|
|
* @memberOf punycode.ucs2
|
|
* @name decode
|
|
* @param {String} string The Unicode input string (UCS-2).
|
|
* @returns {Array} The new array of code points.
|
|
*/
|
|
function ucs2decode(string) {
|
|
var output = [],
|
|
counter = 0,
|
|
length = string.length,
|
|
value,
|
|
extra;
|
|
while (counter < length) {
|
|
value = string.charCodeAt(counter++);
|
|
if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
|
|
// high surrogate, and there is a next character
|
|
extra = string.charCodeAt(counter++);
|
|
if ((extra & 0xFC00) == 0xDC00) { // low surrogate
|
|
output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
|
|
} else {
|
|
// unmatched surrogate; only append this code unit, in case the next
|
|
// code unit is the high surrogate of a surrogate pair
|
|
output.push(value);
|
|
counter--;
|
|
}
|
|
} else {
|
|
output.push(value);
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Creates a string based on an array of numeric code points.
|
|
* @see `punycode.ucs2.decode`
|
|
* @memberOf punycode.ucs2
|
|
* @name encode
|
|
* @param {Array} codePoints The array of numeric code points.
|
|
* @returns {String} The new Unicode string (UCS-2).
|
|
*/
|
|
function ucs2encode(array) {
|
|
return map(array, function(value) {
|
|
var output = '';
|
|
if (value > 0xFFFF) {
|
|
value -= 0x10000;
|
|
output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800);
|
|
value = 0xDC00 | value & 0x3FF;
|
|
}
|
|
output += stringFromCharCode(value);
|
|
return output;
|
|
}).join('');
|
|
}
|
|
|
|
/**
|
|
* Converts a basic code point into a digit/integer.
|
|
* @see `digitToBasic()`
|
|
* @private
|
|
* @param {Number} codePoint The basic numeric code point value.
|
|
* @returns {Number} The numeric value of a basic code point (for use in
|
|
* representing integers) in the range `0` to `base - 1`, or `base` if
|
|
* the code point does not represent a value.
|
|
*/
|
|
function basicToDigit(codePoint) {
|
|
if (codePoint - 48 < 10) {
|
|
return codePoint - 22;
|
|
}
|
|
if (codePoint - 65 < 26) {
|
|
return codePoint - 65;
|
|
}
|
|
if (codePoint - 97 < 26) {
|
|
return codePoint - 97;
|
|
}
|
|
return base;
|
|
}
|
|
|
|
/**
|
|
* Converts a digit/integer into a basic code point.
|
|
* @see `basicToDigit()`
|
|
* @private
|
|
* @param {Number} digit The numeric value of a basic code point.
|
|
* @returns {Number} The basic code point whose value (when used for
|
|
* representing integers) is `digit`, which needs to be in the range
|
|
* `0` to `base - 1`. If `flag` is non-zero, the uppercase form is
|
|
* used; else, the lowercase form is used. The behavior is undefined
|
|
* if `flag` is non-zero and `digit` has no uppercase form.
|
|
*/
|
|
function digitToBasic(digit, flag) {
|
|
// 0..25 map to ASCII a..z or A..Z
|
|
// 26..35 map to ASCII 0..9
|
|
return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5);
|
|
}
|
|
|
|
/**
|
|
* Bias adaptation function as per section 3.4 of RFC 3492.
|
|
* https://tools.ietf.org/html/rfc3492#section-3.4
|
|
* @private
|
|
*/
|
|
function adapt(delta, numPoints, firstTime) {
|
|
var k = 0;
|
|
delta = firstTime ? floor(delta / damp) : delta >> 1;
|
|
delta += floor(delta / numPoints);
|
|
for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) {
|
|
delta = floor(delta / baseMinusTMin);
|
|
}
|
|
return floor(k + (baseMinusTMin + 1) * delta / (delta + skew));
|
|
}
|
|
|
|
/**
|
|
* Converts a Punycode string of ASCII-only symbols to a string of Unicode
|
|
* symbols.
|
|
* @memberOf punycode
|
|
* @param {String} input The Punycode string of ASCII-only symbols.
|
|
* @returns {String} The resulting string of Unicode symbols.
|
|
*/
|
|
function decode(input) {
|
|
// Don't use UCS-2
|
|
var output = [],
|
|
inputLength = input.length,
|
|
out,
|
|
i = 0,
|
|
n = initialN,
|
|
bias = initialBias,
|
|
basic,
|
|
j,
|
|
index,
|
|
oldi,
|
|
w,
|
|
k,
|
|
digit,
|
|
t,
|
|
/** Cached calculation results */
|
|
baseMinusT;
|
|
|
|
// Handle the basic code points: let `basic` be the number of input code
|
|
// points before the last delimiter, or `0` if there is none, then copy
|
|
// the first basic code points to the output.
|
|
|
|
basic = input.lastIndexOf(delimiter);
|
|
if (basic < 0) {
|
|
basic = 0;
|
|
}
|
|
|
|
for (j = 0; j < basic; ++j) {
|
|
// if it's not a basic code point
|
|
if (input.charCodeAt(j) >= 0x80) {
|
|
error('not-basic');
|
|
}
|
|
output.push(input.charCodeAt(j));
|
|
}
|
|
|
|
// Main decoding loop: start just after the last delimiter if any basic code
|
|
// points were copied; start at the beginning otherwise.
|
|
|
|
for (index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) {
|
|
|
|
// `index` is the index of the next character to be consumed.
|
|
// Decode a generalized variable-length integer into `delta`,
|
|
// which gets added to `i`. The overflow checking is easier
|
|
// if we increase `i` as we go, then subtract off its starting
|
|
// value at the end to obtain `delta`.
|
|
for (oldi = i, w = 1, k = base; /* no condition */; k += base) {
|
|
|
|
if (index >= inputLength) {
|
|
error('invalid-input');
|
|
}
|
|
|
|
digit = basicToDigit(input.charCodeAt(index++));
|
|
|
|
if (digit >= base || digit > floor((maxInt - i) / w)) {
|
|
error('overflow');
|
|
}
|
|
|
|
i += digit * w;
|
|
t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
|
|
|
|
if (digit < t) {
|
|
break;
|
|
}
|
|
|
|
baseMinusT = base - t;
|
|
if (w > floor(maxInt / baseMinusT)) {
|
|
error('overflow');
|
|
}
|
|
|
|
w *= baseMinusT;
|
|
|
|
}
|
|
|
|
out = output.length + 1;
|
|
bias = adapt(i - oldi, out, oldi == 0);
|
|
|
|
// `i` was supposed to wrap around from `out` to `0`,
|
|
// incrementing `n` each time, so we'll fix that now:
|
|
if (floor(i / out) > maxInt - n) {
|
|
error('overflow');
|
|
}
|
|
|
|
n += floor(i / out);
|
|
i %= out;
|
|
|
|
// Insert `n` at position `i` of the output
|
|
output.splice(i++, 0, n);
|
|
|
|
}
|
|
|
|
return ucs2encode(output);
|
|
}
|
|
|
|
/**
|
|
* Converts a string of Unicode symbols (e.g. a domain name label) to a
|
|
* Punycode string of ASCII-only symbols.
|
|
* @memberOf punycode
|
|
* @param {String} input The string of Unicode symbols.
|
|
* @returns {String} The resulting Punycode string of ASCII-only symbols.
|
|
*/
|
|
function encode(input) {
|
|
var n,
|
|
delta,
|
|
handledCPCount,
|
|
basicLength,
|
|
bias,
|
|
j,
|
|
m,
|
|
q,
|
|
k,
|
|
t,
|
|
currentValue,
|
|
output = [],
|
|
/** `inputLength` will hold the number of code points in `input`. */
|
|
inputLength,
|
|
/** Cached calculation results */
|
|
handledCPCountPlusOne,
|
|
baseMinusT,
|
|
qMinusT;
|
|
|
|
// Convert the input in UCS-2 to Unicode
|
|
input = ucs2decode(input);
|
|
|
|
// Cache the length
|
|
inputLength = input.length;
|
|
|
|
// Initialize the state
|
|
n = initialN;
|
|
delta = 0;
|
|
bias = initialBias;
|
|
|
|
// Handle the basic code points
|
|
for (j = 0; j < inputLength; ++j) {
|
|
currentValue = input[j];
|
|
if (currentValue < 0x80) {
|
|
output.push(stringFromCharCode(currentValue));
|
|
}
|
|
}
|
|
|
|
handledCPCount = basicLength = output.length;
|
|
|
|
// `handledCPCount` is the number of code points that have been handled;
|
|
// `basicLength` is the number of basic code points.
|
|
|
|
// Finish the basic string - if it is not empty - with a delimiter
|
|
if (basicLength) {
|
|
output.push(delimiter);
|
|
}
|
|
|
|
// Main encoding loop:
|
|
while (handledCPCount < inputLength) {
|
|
|
|
// All non-basic code points < n have been handled already. Find the next
|
|
// larger one:
|
|
for (m = maxInt, j = 0; j < inputLength; ++j) {
|
|
currentValue = input[j];
|
|
if (currentValue >= n && currentValue < m) {
|
|
m = currentValue;
|
|
}
|
|
}
|
|
|
|
// Increase `delta` enough to advance the decoder's <n,i> state to <m,0>,
|
|
// but guard against overflow
|
|
handledCPCountPlusOne = handledCPCount + 1;
|
|
if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) {
|
|
error('overflow');
|
|
}
|
|
|
|
delta += (m - n) * handledCPCountPlusOne;
|
|
n = m;
|
|
|
|
for (j = 0; j < inputLength; ++j) {
|
|
currentValue = input[j];
|
|
|
|
if (currentValue < n && ++delta > maxInt) {
|
|
error('overflow');
|
|
}
|
|
|
|
if (currentValue == n) {
|
|
// Represent delta as a generalized variable-length integer
|
|
for (q = delta, k = base; /* no condition */; k += base) {
|
|
t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
|
|
if (q < t) {
|
|
break;
|
|
}
|
|
qMinusT = q - t;
|
|
baseMinusT = base - t;
|
|
output.push(
|
|
stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0))
|
|
);
|
|
q = floor(qMinusT / baseMinusT);
|
|
}
|
|
|
|
output.push(stringFromCharCode(digitToBasic(q, 0)));
|
|
bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength);
|
|
delta = 0;
|
|
++handledCPCount;
|
|
}
|
|
}
|
|
|
|
++delta;
|
|
++n;
|
|
|
|
}
|
|
return output.join('');
|
|
}
|
|
|
|
/**
|
|
* Converts a Punycode string representing a domain name or an email address
|
|
* to Unicode. Only the Punycoded parts of the input will be converted, i.e.
|
|
* it doesn't matter if you call it on a string that has already been
|
|
* converted to Unicode.
|
|
* @memberOf punycode
|
|
* @param {String} input The Punycoded domain name or email address to
|
|
* convert to Unicode.
|
|
* @returns {String} The Unicode representation of the given Punycode
|
|
* string.
|
|
*/
|
|
function toUnicode(input) {
|
|
return mapDomain(input, function(string) {
|
|
return regexPunycode.test(string)
|
|
? decode(string.slice(4).toLowerCase())
|
|
: string;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Converts a Unicode string representing a domain name or an email address to
|
|
* Punycode. Only the non-ASCII parts of the domain name will be converted,
|
|
* i.e. it doesn't matter if you call it with a domain that's already in
|
|
* ASCII.
|
|
* @memberOf punycode
|
|
* @param {String} input The domain name or email address to convert, as a
|
|
* Unicode string.
|
|
* @returns {String} The Punycode representation of the given domain name or
|
|
* email address.
|
|
*/
|
|
function toASCII(input) {
|
|
return mapDomain(input, function(string) {
|
|
return regexNonASCII.test(string)
|
|
? 'xn--' + encode(string)
|
|
: string;
|
|
});
|
|
}
|
|
|
|
/*--------------------------------------------------------------------------*/
|
|
|
|
/** Define the public API */
|
|
punycode = {
|
|
/**
|
|
* A string representing the current Punycode.js version number.
|
|
* @memberOf punycode
|
|
* @type String
|
|
*/
|
|
'version': '1.4.1',
|
|
/**
|
|
* An object of methods to convert from JavaScript's internal character
|
|
* representation (UCS-2) to Unicode code points, and back.
|
|
* @see <https://mathiasbynens.be/notes/javascript-encoding>
|
|
* @memberOf punycode
|
|
* @type Object
|
|
*/
|
|
'ucs2': {
|
|
'decode': ucs2decode,
|
|
'encode': ucs2encode
|
|
},
|
|
'decode': decode,
|
|
'encode': encode,
|
|
'toASCII': toASCII,
|
|
'toUnicode': toUnicode
|
|
};
|
|
|
|
/** Expose `punycode` */
|
|
// Some AMD build optimizers, like r.js, check for specific condition patterns
|
|
// like the following:
|
|
if (
|
|
typeof define == 'function' &&
|
|
typeof define.amd == 'object' &&
|
|
define.amd
|
|
) {
|
|
define('punycode', function() {
|
|
return punycode;
|
|
});
|
|
} else if (freeExports && freeModule) {
|
|
if (module.exports == freeExports) {
|
|
// in Node.js, io.js, or RingoJS v0.8.0+
|
|
freeModule.exports = punycode;
|
|
} else {
|
|
// in Narwhal or RingoJS v0.7.0-
|
|
for (key in punycode) {
|
|
punycode.hasOwnProperty(key) && (freeExports[key] = punycode[key]);
|
|
}
|
|
}
|
|
} else {
|
|
// in Rhino or a web browser
|
|
root.punycode = punycode;
|
|
}
|
|
|
|
}(this));
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
|
|
},{}],189:[function(require,module,exports){
|
|
// 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.
|
|
|
|
'use strict';
|
|
|
|
// If obj.hasOwnProperty has been overridden, then calling
|
|
// obj.hasOwnProperty(prop) will break.
|
|
// See: https://github.com/joyent/node/issues/1707
|
|
function hasOwnProperty(obj, prop) {
|
|
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
}
|
|
|
|
module.exports = function(qs, sep, eq, options) {
|
|
sep = sep || '&';
|
|
eq = eq || '=';
|
|
var obj = {};
|
|
|
|
if (typeof qs !== 'string' || qs.length === 0) {
|
|
return obj;
|
|
}
|
|
|
|
var regexp = /\+/g;
|
|
qs = qs.split(sep);
|
|
|
|
var maxKeys = 1000;
|
|
if (options && typeof options.maxKeys === 'number') {
|
|
maxKeys = options.maxKeys;
|
|
}
|
|
|
|
var len = qs.length;
|
|
// maxKeys <= 0 means that we should not limit keys count
|
|
if (maxKeys > 0 && len > maxKeys) {
|
|
len = maxKeys;
|
|
}
|
|
|
|
for (var i = 0; i < len; ++i) {
|
|
var x = qs[i].replace(regexp, '%20'),
|
|
idx = x.indexOf(eq),
|
|
kstr, vstr, k, v;
|
|
|
|
if (idx >= 0) {
|
|
kstr = x.substr(0, idx);
|
|
vstr = x.substr(idx + 1);
|
|
} else {
|
|
kstr = x;
|
|
vstr = '';
|
|
}
|
|
|
|
k = decodeURIComponent(kstr);
|
|
v = decodeURIComponent(vstr);
|
|
|
|
if (!hasOwnProperty(obj, k)) {
|
|
obj[k] = v;
|
|
} else if (isArray(obj[k])) {
|
|
obj[k].push(v);
|
|
} else {
|
|
obj[k] = [obj[k], v];
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
};
|
|
|
|
var isArray = Array.isArray || function (xs) {
|
|
return Object.prototype.toString.call(xs) === '[object Array]';
|
|
};
|
|
|
|
},{}],190:[function(require,module,exports){
|
|
// 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.
|
|
|
|
'use strict';
|
|
|
|
var stringifyPrimitive = function(v) {
|
|
switch (typeof v) {
|
|
case 'string':
|
|
return v;
|
|
|
|
case 'boolean':
|
|
return v ? 'true' : 'false';
|
|
|
|
case 'number':
|
|
return isFinite(v) ? v : '';
|
|
|
|
default:
|
|
return '';
|
|
}
|
|
};
|
|
|
|
module.exports = function(obj, sep, eq, name) {
|
|
sep = sep || '&';
|
|
eq = eq || '=';
|
|
if (obj === null) {
|
|
obj = undefined;
|
|
}
|
|
|
|
if (typeof obj === 'object') {
|
|
return map(objectKeys(obj), function(k) {
|
|
var ks = encodeURIComponent(stringifyPrimitive(k)) + eq;
|
|
if (isArray(obj[k])) {
|
|
return map(obj[k], function(v) {
|
|
return ks + encodeURIComponent(stringifyPrimitive(v));
|
|
}).join(sep);
|
|
} else {
|
|
return ks + encodeURIComponent(stringifyPrimitive(obj[k]));
|
|
}
|
|
}).join(sep);
|
|
|
|
}
|
|
|
|
if (!name) return '';
|
|
return encodeURIComponent(stringifyPrimitive(name)) + eq +
|
|
encodeURIComponent(stringifyPrimitive(obj));
|
|
};
|
|
|
|
var isArray = Array.isArray || function (xs) {
|
|
return Object.prototype.toString.call(xs) === '[object Array]';
|
|
};
|
|
|
|
function map (xs, f) {
|
|
if (xs.map) return xs.map(f);
|
|
var res = [];
|
|
for (var i = 0; i < xs.length; i++) {
|
|
res.push(f(xs[i], i));
|
|
}
|
|
return res;
|
|
}
|
|
|
|
var objectKeys = Object.keys || function (obj) {
|
|
var res = [];
|
|
for (var key in obj) {
|
|
if (Object.prototype.hasOwnProperty.call(obj, key)) res.push(key);
|
|
}
|
|
return res;
|
|
};
|
|
|
|
},{}],191:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
exports.decode = exports.parse = require('./decode');
|
|
exports.encode = exports.stringify = require('./encode');
|
|
|
|
},{"./decode":189,"./encode":190}],192:[function(require,module,exports){
|
|
/**
|
|
* Copyright (c) 2014-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
// This method of obtaining a reference to the global object needs to be
|
|
// kept identical to the way it is obtained in runtime.js
|
|
var g = (function() { return this })() || Function("return this")();
|
|
|
|
// Use `getOwnPropertyNames` because not all browsers support calling
|
|
// `hasOwnProperty` on the global `self` object in a worker. See #183.
|
|
var hadRuntime = g.regeneratorRuntime &&
|
|
Object.getOwnPropertyNames(g).indexOf("regeneratorRuntime") >= 0;
|
|
|
|
// Save the old regeneratorRuntime in case it needs to be restored later.
|
|
var oldRuntime = hadRuntime && g.regeneratorRuntime;
|
|
|
|
// Force reevalutation of runtime.js.
|
|
g.regeneratorRuntime = undefined;
|
|
|
|
module.exports = require("./runtime");
|
|
|
|
if (hadRuntime) {
|
|
// Restore the original runtime.
|
|
g.regeneratorRuntime = oldRuntime;
|
|
} else {
|
|
// Remove the global property added by runtime.js.
|
|
try {
|
|
delete g.regeneratorRuntime;
|
|
} catch(e) {
|
|
g.regeneratorRuntime = undefined;
|
|
}
|
|
}
|
|
|
|
},{"./runtime":193}],193:[function(require,module,exports){
|
|
/**
|
|
* Copyright (c) 2014-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
!(function(global) {
|
|
"use strict";
|
|
|
|
var Op = Object.prototype;
|
|
var hasOwn = Op.hasOwnProperty;
|
|
var undefined; // More compressible than void 0.
|
|
var $Symbol = typeof Symbol === "function" ? Symbol : {};
|
|
var iteratorSymbol = $Symbol.iterator || "@@iterator";
|
|
var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator";
|
|
var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag";
|
|
|
|
var inModule = typeof module === "object";
|
|
var runtime = global.regeneratorRuntime;
|
|
if (runtime) {
|
|
if (inModule) {
|
|
// If regeneratorRuntime is defined globally and we're in a module,
|
|
// make the exports object identical to regeneratorRuntime.
|
|
module.exports = runtime;
|
|
}
|
|
// Don't bother evaluating the rest of this file if the runtime was
|
|
// already defined globally.
|
|
return;
|
|
}
|
|
|
|
// Define the runtime globally (as expected by generated code) as either
|
|
// module.exports (if we're in a module) or a new, empty object.
|
|
runtime = global.regeneratorRuntime = inModule ? module.exports : {};
|
|
|
|
function wrap(innerFn, outerFn, self, tryLocsList) {
|
|
// If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator.
|
|
var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator;
|
|
var generator = Object.create(protoGenerator.prototype);
|
|
var context = new Context(tryLocsList || []);
|
|
|
|
// The ._invoke method unifies the implementations of the .next,
|
|
// .throw, and .return methods.
|
|
generator._invoke = makeInvokeMethod(innerFn, self, context);
|
|
|
|
return generator;
|
|
}
|
|
runtime.wrap = wrap;
|
|
|
|
// Try/catch helper to minimize deoptimizations. Returns a completion
|
|
// record like context.tryEntries[i].completion. This interface could
|
|
// have been (and was previously) designed to take a closure to be
|
|
// invoked without arguments, but in all the cases we care about we
|
|
// already have an existing method we want to call, so there's no need
|
|
// to create a new function object. We can even get away with assuming
|
|
// the method takes exactly one argument, since that happens to be true
|
|
// in every case, so we don't have to touch the arguments object. The
|
|
// only additional allocation required is the completion record, which
|
|
// has a stable shape and so hopefully should be cheap to allocate.
|
|
function tryCatch(fn, obj, arg) {
|
|
try {
|
|
return { type: "normal", arg: fn.call(obj, arg) };
|
|
} catch (err) {
|
|
return { type: "throw", arg: err };
|
|
}
|
|
}
|
|
|
|
var GenStateSuspendedStart = "suspendedStart";
|
|
var GenStateSuspendedYield = "suspendedYield";
|
|
var GenStateExecuting = "executing";
|
|
var GenStateCompleted = "completed";
|
|
|
|
// Returning this object from the innerFn has the same effect as
|
|
// breaking out of the dispatch switch statement.
|
|
var ContinueSentinel = {};
|
|
|
|
// Dummy constructor functions that we use as the .constructor and
|
|
// .constructor.prototype properties for functions that return Generator
|
|
// objects. For full spec compliance, you may wish to configure your
|
|
// minifier not to mangle the names of these two functions.
|
|
function Generator() {}
|
|
function GeneratorFunction() {}
|
|
function GeneratorFunctionPrototype() {}
|
|
|
|
// This is a polyfill for %IteratorPrototype% for environments that
|
|
// don't natively support it.
|
|
var IteratorPrototype = {};
|
|
IteratorPrototype[iteratorSymbol] = function () {
|
|
return this;
|
|
};
|
|
|
|
var getProto = Object.getPrototypeOf;
|
|
var NativeIteratorPrototype = getProto && getProto(getProto(values([])));
|
|
if (NativeIteratorPrototype &&
|
|
NativeIteratorPrototype !== Op &&
|
|
hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) {
|
|
// This environment has a native %IteratorPrototype%; use it instead
|
|
// of the polyfill.
|
|
IteratorPrototype = NativeIteratorPrototype;
|
|
}
|
|
|
|
var Gp = GeneratorFunctionPrototype.prototype =
|
|
Generator.prototype = Object.create(IteratorPrototype);
|
|
GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;
|
|
GeneratorFunctionPrototype.constructor = GeneratorFunction;
|
|
GeneratorFunctionPrototype[toStringTagSymbol] =
|
|
GeneratorFunction.displayName = "GeneratorFunction";
|
|
|
|
// Helper for defining the .next, .throw, and .return methods of the
|
|
// Iterator interface in terms of a single ._invoke method.
|
|
function defineIteratorMethods(prototype) {
|
|
["next", "throw", "return"].forEach(function(method) {
|
|
prototype[method] = function(arg) {
|
|
return this._invoke(method, arg);
|
|
};
|
|
});
|
|
}
|
|
|
|
runtime.isGeneratorFunction = function(genFun) {
|
|
var ctor = typeof genFun === "function" && genFun.constructor;
|
|
return ctor
|
|
? ctor === GeneratorFunction ||
|
|
// For the native GeneratorFunction constructor, the best we can
|
|
// do is to check its .name property.
|
|
(ctor.displayName || ctor.name) === "GeneratorFunction"
|
|
: false;
|
|
};
|
|
|
|
runtime.mark = function(genFun) {
|
|
if (Object.setPrototypeOf) {
|
|
Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);
|
|
} else {
|
|
genFun.__proto__ = GeneratorFunctionPrototype;
|
|
if (!(toStringTagSymbol in genFun)) {
|
|
genFun[toStringTagSymbol] = "GeneratorFunction";
|
|
}
|
|
}
|
|
genFun.prototype = Object.create(Gp);
|
|
return genFun;
|
|
};
|
|
|
|
// Within the body of any async function, `await x` is transformed to
|
|
// `yield regeneratorRuntime.awrap(x)`, so that the runtime can test
|
|
// `hasOwn.call(value, "__await")` to determine if the yielded value is
|
|
// meant to be awaited.
|
|
runtime.awrap = function(arg) {
|
|
return { __await: arg };
|
|
};
|
|
|
|
function AsyncIterator(generator) {
|
|
function invoke(method, arg, resolve, reject) {
|
|
var record = tryCatch(generator[method], generator, arg);
|
|
if (record.type === "throw") {
|
|
reject(record.arg);
|
|
} else {
|
|
var result = record.arg;
|
|
var value = result.value;
|
|
if (value &&
|
|
typeof value === "object" &&
|
|
hasOwn.call(value, "__await")) {
|
|
return Promise.resolve(value.__await).then(function(value) {
|
|
invoke("next", value, resolve, reject);
|
|
}, function(err) {
|
|
invoke("throw", err, resolve, reject);
|
|
});
|
|
}
|
|
|
|
return Promise.resolve(value).then(function(unwrapped) {
|
|
// When a yielded Promise is resolved, its final value becomes
|
|
// the .value of the Promise<{value,done}> result for the
|
|
// current iteration. If the Promise is rejected, however, the
|
|
// result for this iteration will be rejected with the same
|
|
// reason. Note that rejections of yielded Promises are not
|
|
// thrown back into the generator function, as is the case
|
|
// when an awaited Promise is rejected. This difference in
|
|
// behavior between yield and await is important, because it
|
|
// allows the consumer to decide what to do with the yielded
|
|
// rejection (swallow it and continue, manually .throw it back
|
|
// into the generator, abandon iteration, whatever). With
|
|
// await, by contrast, there is no opportunity to examine the
|
|
// rejection reason outside the generator function, so the
|
|
// only option is to throw it from the await expression, and
|
|
// let the generator function handle the exception.
|
|
result.value = unwrapped;
|
|
resolve(result);
|
|
}, reject);
|
|
}
|
|
}
|
|
|
|
var previousPromise;
|
|
|
|
function enqueue(method, arg) {
|
|
function callInvokeWithMethodAndArg() {
|
|
return new Promise(function(resolve, reject) {
|
|
invoke(method, arg, resolve, reject);
|
|
});
|
|
}
|
|
|
|
return previousPromise =
|
|
// If enqueue has been called before, then we want to wait until
|
|
// all previous Promises have been resolved before calling invoke,
|
|
// so that results are always delivered in the correct order. If
|
|
// enqueue has not been called before, then it is important to
|
|
// call invoke immediately, without waiting on a callback to fire,
|
|
// so that the async generator function has the opportunity to do
|
|
// any necessary setup in a predictable way. This predictability
|
|
// is why the Promise constructor synchronously invokes its
|
|
// executor callback, and why async functions synchronously
|
|
// execute code before the first await. Since we implement simple
|
|
// async functions in terms of async generators, it is especially
|
|
// important to get this right, even though it requires care.
|
|
previousPromise ? previousPromise.then(
|
|
callInvokeWithMethodAndArg,
|
|
// Avoid propagating failures to Promises returned by later
|
|
// invocations of the iterator.
|
|
callInvokeWithMethodAndArg
|
|
) : callInvokeWithMethodAndArg();
|
|
}
|
|
|
|
// Define the unified helper method that is used to implement .next,
|
|
// .throw, and .return (see defineIteratorMethods).
|
|
this._invoke = enqueue;
|
|
}
|
|
|
|
defineIteratorMethods(AsyncIterator.prototype);
|
|
AsyncIterator.prototype[asyncIteratorSymbol] = function () {
|
|
return this;
|
|
};
|
|
runtime.AsyncIterator = AsyncIterator;
|
|
|
|
// Note that simple async functions are implemented on top of
|
|
// AsyncIterator objects; they just return a Promise for the value of
|
|
// the final result produced by the iterator.
|
|
runtime.async = function(innerFn, outerFn, self, tryLocsList) {
|
|
var iter = new AsyncIterator(
|
|
wrap(innerFn, outerFn, self, tryLocsList)
|
|
);
|
|
|
|
return runtime.isGeneratorFunction(outerFn)
|
|
? iter // If outerFn is a generator, return the full iterator.
|
|
: iter.next().then(function(result) {
|
|
return result.done ? result.value : iter.next();
|
|
});
|
|
};
|
|
|
|
function makeInvokeMethod(innerFn, self, context) {
|
|
var state = GenStateSuspendedStart;
|
|
|
|
return function invoke(method, arg) {
|
|
if (state === GenStateExecuting) {
|
|
throw new Error("Generator is already running");
|
|
}
|
|
|
|
if (state === GenStateCompleted) {
|
|
if (method === "throw") {
|
|
throw arg;
|
|
}
|
|
|
|
// Be forgiving, per 25.3.3.3.3 of the spec:
|
|
// https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume
|
|
return doneResult();
|
|
}
|
|
|
|
context.method = method;
|
|
context.arg = arg;
|
|
|
|
while (true) {
|
|
var delegate = context.delegate;
|
|
if (delegate) {
|
|
var delegateResult = maybeInvokeDelegate(delegate, context);
|
|
if (delegateResult) {
|
|
if (delegateResult === ContinueSentinel) continue;
|
|
return delegateResult;
|
|
}
|
|
}
|
|
|
|
if (context.method === "next") {
|
|
// Setting context._sent for legacy support of Babel's
|
|
// function.sent implementation.
|
|
context.sent = context._sent = context.arg;
|
|
|
|
} else if (context.method === "throw") {
|
|
if (state === GenStateSuspendedStart) {
|
|
state = GenStateCompleted;
|
|
throw context.arg;
|
|
}
|
|
|
|
context.dispatchException(context.arg);
|
|
|
|
} else if (context.method === "return") {
|
|
context.abrupt("return", context.arg);
|
|
}
|
|
|
|
state = GenStateExecuting;
|
|
|
|
var record = tryCatch(innerFn, self, context);
|
|
if (record.type === "normal") {
|
|
// If an exception is thrown from innerFn, we leave state ===
|
|
// GenStateExecuting and loop back for another invocation.
|
|
state = context.done
|
|
? GenStateCompleted
|
|
: GenStateSuspendedYield;
|
|
|
|
if (record.arg === ContinueSentinel) {
|
|
continue;
|
|
}
|
|
|
|
return {
|
|
value: record.arg,
|
|
done: context.done
|
|
};
|
|
|
|
} else if (record.type === "throw") {
|
|
state = GenStateCompleted;
|
|
// Dispatch the exception by looping back around to the
|
|
// context.dispatchException(context.arg) call above.
|
|
context.method = "throw";
|
|
context.arg = record.arg;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Call delegate.iterator[context.method](context.arg) and handle the
|
|
// result, either by returning a { value, done } result from the
|
|
// delegate iterator, or by modifying context.method and context.arg,
|
|
// setting context.delegate to null, and returning the ContinueSentinel.
|
|
function maybeInvokeDelegate(delegate, context) {
|
|
var method = delegate.iterator[context.method];
|
|
if (method === undefined) {
|
|
// A .throw or .return when the delegate iterator has no .throw
|
|
// method always terminates the yield* loop.
|
|
context.delegate = null;
|
|
|
|
if (context.method === "throw") {
|
|
if (delegate.iterator.return) {
|
|
// If the delegate iterator has a return method, give it a
|
|
// chance to clean up.
|
|
context.method = "return";
|
|
context.arg = undefined;
|
|
maybeInvokeDelegate(delegate, context);
|
|
|
|
if (context.method === "throw") {
|
|
// If maybeInvokeDelegate(context) changed context.method from
|
|
// "return" to "throw", let that override the TypeError below.
|
|
return ContinueSentinel;
|
|
}
|
|
}
|
|
|
|
context.method = "throw";
|
|
context.arg = new TypeError(
|
|
"The iterator does not provide a 'throw' method");
|
|
}
|
|
|
|
return ContinueSentinel;
|
|
}
|
|
|
|
var record = tryCatch(method, delegate.iterator, context.arg);
|
|
|
|
if (record.type === "throw") {
|
|
context.method = "throw";
|
|
context.arg = record.arg;
|
|
context.delegate = null;
|
|
return ContinueSentinel;
|
|
}
|
|
|
|
var info = record.arg;
|
|
|
|
if (! info) {
|
|
context.method = "throw";
|
|
context.arg = new TypeError("iterator result is not an object");
|
|
context.delegate = null;
|
|
return ContinueSentinel;
|
|
}
|
|
|
|
if (info.done) {
|
|
// Assign the result of the finished delegate to the temporary
|
|
// variable specified by delegate.resultName (see delegateYield).
|
|
context[delegate.resultName] = info.value;
|
|
|
|
// Resume execution at the desired location (see delegateYield).
|
|
context.next = delegate.nextLoc;
|
|
|
|
// If context.method was "throw" but the delegate handled the
|
|
// exception, let the outer generator proceed normally. If
|
|
// context.method was "next", forget context.arg since it has been
|
|
// "consumed" by the delegate iterator. If context.method was
|
|
// "return", allow the original .return call to continue in the
|
|
// outer generator.
|
|
if (context.method !== "return") {
|
|
context.method = "next";
|
|
context.arg = undefined;
|
|
}
|
|
|
|
} else {
|
|
// Re-yield the result returned by the delegate method.
|
|
return info;
|
|
}
|
|
|
|
// The delegate iterator is finished, so forget it and continue with
|
|
// the outer generator.
|
|
context.delegate = null;
|
|
return ContinueSentinel;
|
|
}
|
|
|
|
// Define Generator.prototype.{next,throw,return} in terms of the
|
|
// unified ._invoke helper method.
|
|
defineIteratorMethods(Gp);
|
|
|
|
Gp[toStringTagSymbol] = "Generator";
|
|
|
|
// A Generator should always return itself as the iterator object when the
|
|
// @@iterator function is called on it. Some browsers' implementations of the
|
|
// iterator prototype chain incorrectly implement this, causing the Generator
|
|
// object to not be returned from this call. This ensures that doesn't happen.
|
|
// See https://github.com/facebook/regenerator/issues/274 for more details.
|
|
Gp[iteratorSymbol] = function() {
|
|
return this;
|
|
};
|
|
|
|
Gp.toString = function() {
|
|
return "[object Generator]";
|
|
};
|
|
|
|
function pushTryEntry(locs) {
|
|
var entry = { tryLoc: locs[0] };
|
|
|
|
if (1 in locs) {
|
|
entry.catchLoc = locs[1];
|
|
}
|
|
|
|
if (2 in locs) {
|
|
entry.finallyLoc = locs[2];
|
|
entry.afterLoc = locs[3];
|
|
}
|
|
|
|
this.tryEntries.push(entry);
|
|
}
|
|
|
|
function resetTryEntry(entry) {
|
|
var record = entry.completion || {};
|
|
record.type = "normal";
|
|
delete record.arg;
|
|
entry.completion = record;
|
|
}
|
|
|
|
function Context(tryLocsList) {
|
|
// The root entry object (effectively a try statement without a catch
|
|
// or a finally block) gives us a place to store values thrown from
|
|
// locations where there is no enclosing try statement.
|
|
this.tryEntries = [{ tryLoc: "root" }];
|
|
tryLocsList.forEach(pushTryEntry, this);
|
|
this.reset(true);
|
|
}
|
|
|
|
runtime.keys = function(object) {
|
|
var keys = [];
|
|
for (var key in object) {
|
|
keys.push(key);
|
|
}
|
|
keys.reverse();
|
|
|
|
// Rather than returning an object with a next method, we keep
|
|
// things simple and return the next function itself.
|
|
return function next() {
|
|
while (keys.length) {
|
|
var key = keys.pop();
|
|
if (key in object) {
|
|
next.value = key;
|
|
next.done = false;
|
|
return next;
|
|
}
|
|
}
|
|
|
|
// To avoid creating an additional object, we just hang the .value
|
|
// and .done properties off the next function object itself. This
|
|
// also ensures that the minifier will not anonymize the function.
|
|
next.done = true;
|
|
return next;
|
|
};
|
|
};
|
|
|
|
function values(iterable) {
|
|
if (iterable) {
|
|
var iteratorMethod = iterable[iteratorSymbol];
|
|
if (iteratorMethod) {
|
|
return iteratorMethod.call(iterable);
|
|
}
|
|
|
|
if (typeof iterable.next === "function") {
|
|
return iterable;
|
|
}
|
|
|
|
if (!isNaN(iterable.length)) {
|
|
var i = -1, next = function next() {
|
|
while (++i < iterable.length) {
|
|
if (hasOwn.call(iterable, i)) {
|
|
next.value = iterable[i];
|
|
next.done = false;
|
|
return next;
|
|
}
|
|
}
|
|
|
|
next.value = undefined;
|
|
next.done = true;
|
|
|
|
return next;
|
|
};
|
|
|
|
return next.next = next;
|
|
}
|
|
}
|
|
|
|
// Return an iterator with no values.
|
|
return { next: doneResult };
|
|
}
|
|
runtime.values = values;
|
|
|
|
function doneResult() {
|
|
return { value: undefined, done: true };
|
|
}
|
|
|
|
Context.prototype = {
|
|
constructor: Context,
|
|
|
|
reset: function(skipTempReset) {
|
|
this.prev = 0;
|
|
this.next = 0;
|
|
// Resetting context._sent for legacy support of Babel's
|
|
// function.sent implementation.
|
|
this.sent = this._sent = undefined;
|
|
this.done = false;
|
|
this.delegate = null;
|
|
|
|
this.method = "next";
|
|
this.arg = undefined;
|
|
|
|
this.tryEntries.forEach(resetTryEntry);
|
|
|
|
if (!skipTempReset) {
|
|
for (var name in this) {
|
|
// Not sure about the optimal order of these conditions:
|
|
if (name.charAt(0) === "t" &&
|
|
hasOwn.call(this, name) &&
|
|
!isNaN(+name.slice(1))) {
|
|
this[name] = undefined;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
stop: function() {
|
|
this.done = true;
|
|
|
|
var rootEntry = this.tryEntries[0];
|
|
var rootRecord = rootEntry.completion;
|
|
if (rootRecord.type === "throw") {
|
|
throw rootRecord.arg;
|
|
}
|
|
|
|
return this.rval;
|
|
},
|
|
|
|
dispatchException: function(exception) {
|
|
if (this.done) {
|
|
throw exception;
|
|
}
|
|
|
|
var context = this;
|
|
function handle(loc, caught) {
|
|
record.type = "throw";
|
|
record.arg = exception;
|
|
context.next = loc;
|
|
|
|
if (caught) {
|
|
// If the dispatched exception was caught by a catch block,
|
|
// then let that catch block handle the exception normally.
|
|
context.method = "next";
|
|
context.arg = undefined;
|
|
}
|
|
|
|
return !! caught;
|
|
}
|
|
|
|
for (var i = this.tryEntries.length - 1; i >= 0; --i) {
|
|
var entry = this.tryEntries[i];
|
|
var record = entry.completion;
|
|
|
|
if (entry.tryLoc === "root") {
|
|
// Exception thrown outside of any try block that could handle
|
|
// it, so set the completion value of the entire function to
|
|
// throw the exception.
|
|
return handle("end");
|
|
}
|
|
|
|
if (entry.tryLoc <= this.prev) {
|
|
var hasCatch = hasOwn.call(entry, "catchLoc");
|
|
var hasFinally = hasOwn.call(entry, "finallyLoc");
|
|
|
|
if (hasCatch && hasFinally) {
|
|
if (this.prev < entry.catchLoc) {
|
|
return handle(entry.catchLoc, true);
|
|
} else if (this.prev < entry.finallyLoc) {
|
|
return handle(entry.finallyLoc);
|
|
}
|
|
|
|
} else if (hasCatch) {
|
|
if (this.prev < entry.catchLoc) {
|
|
return handle(entry.catchLoc, true);
|
|
}
|
|
|
|
} else if (hasFinally) {
|
|
if (this.prev < entry.finallyLoc) {
|
|
return handle(entry.finallyLoc);
|
|
}
|
|
|
|
} else {
|
|
throw new Error("try statement without catch or finally");
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
abrupt: function(type, arg) {
|
|
for (var i = this.tryEntries.length - 1; i >= 0; --i) {
|
|
var entry = this.tryEntries[i];
|
|
if (entry.tryLoc <= this.prev &&
|
|
hasOwn.call(entry, "finallyLoc") &&
|
|
this.prev < entry.finallyLoc) {
|
|
var finallyEntry = entry;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (finallyEntry &&
|
|
(type === "break" ||
|
|
type === "continue") &&
|
|
finallyEntry.tryLoc <= arg &&
|
|
arg <= finallyEntry.finallyLoc) {
|
|
// Ignore the finally entry if control is not jumping to a
|
|
// location outside the try/catch block.
|
|
finallyEntry = null;
|
|
}
|
|
|
|
var record = finallyEntry ? finallyEntry.completion : {};
|
|
record.type = type;
|
|
record.arg = arg;
|
|
|
|
if (finallyEntry) {
|
|
this.method = "next";
|
|
this.next = finallyEntry.finallyLoc;
|
|
return ContinueSentinel;
|
|
}
|
|
|
|
return this.complete(record);
|
|
},
|
|
|
|
complete: function(record, afterLoc) {
|
|
if (record.type === "throw") {
|
|
throw record.arg;
|
|
}
|
|
|
|
if (record.type === "break" ||
|
|
record.type === "continue") {
|
|
this.next = record.arg;
|
|
} else if (record.type === "return") {
|
|
this.rval = this.arg = record.arg;
|
|
this.method = "return";
|
|
this.next = "end";
|
|
} else if (record.type === "normal" && afterLoc) {
|
|
this.next = afterLoc;
|
|
}
|
|
|
|
return ContinueSentinel;
|
|
},
|
|
|
|
finish: function(finallyLoc) {
|
|
for (var i = this.tryEntries.length - 1; i >= 0; --i) {
|
|
var entry = this.tryEntries[i];
|
|
if (entry.finallyLoc === finallyLoc) {
|
|
this.complete(entry.completion, entry.afterLoc);
|
|
resetTryEntry(entry);
|
|
return ContinueSentinel;
|
|
}
|
|
}
|
|
},
|
|
|
|
"catch": function(tryLoc) {
|
|
for (var i = this.tryEntries.length - 1; i >= 0; --i) {
|
|
var entry = this.tryEntries[i];
|
|
if (entry.tryLoc === tryLoc) {
|
|
var record = entry.completion;
|
|
if (record.type === "throw") {
|
|
var thrown = record.arg;
|
|
resetTryEntry(entry);
|
|
}
|
|
return thrown;
|
|
}
|
|
}
|
|
|
|
// The context.catch method must only be called with a location
|
|
// argument that corresponds to a known catch block.
|
|
throw new Error("illegal catch attempt");
|
|
},
|
|
|
|
delegateYield: function(iterable, resultName, nextLoc) {
|
|
this.delegate = {
|
|
iterator: values(iterable),
|
|
resultName: resultName,
|
|
nextLoc: nextLoc
|
|
};
|
|
|
|
if (this.method === "next") {
|
|
// Deliberately forget the last sent value so that we don't
|
|
// accidentally pass it on to the delegate.
|
|
this.arg = undefined;
|
|
}
|
|
|
|
return ContinueSentinel;
|
|
}
|
|
};
|
|
})(
|
|
// In sloppy mode, unbound `this` refers to the global object, fallback to
|
|
// Function constructor if we're in global strict mode. That is sadly a form
|
|
// of indirect eval which violates Content Security Policy.
|
|
(function() { return this })() || Function("return this")()
|
|
);
|
|
|
|
},{}],194:[function(require,module,exports){
|
|
(function (setImmediate,clearImmediate){
|
|
var nextTick = require('process/browser.js').nextTick;
|
|
var apply = Function.prototype.apply;
|
|
var slice = Array.prototype.slice;
|
|
var immediateIds = {};
|
|
var nextImmediateId = 0;
|
|
|
|
// DOM APIs, for completeness
|
|
|
|
exports.setTimeout = function() {
|
|
return new Timeout(apply.call(setTimeout, window, arguments), clearTimeout);
|
|
};
|
|
exports.setInterval = function() {
|
|
return new Timeout(apply.call(setInterval, window, arguments), clearInterval);
|
|
};
|
|
exports.clearTimeout =
|
|
exports.clearInterval = function(timeout) { timeout.close(); };
|
|
|
|
function Timeout(id, clearFn) {
|
|
this._id = id;
|
|
this._clearFn = clearFn;
|
|
}
|
|
Timeout.prototype.unref = Timeout.prototype.ref = function() {};
|
|
Timeout.prototype.close = function() {
|
|
this._clearFn.call(window, this._id);
|
|
};
|
|
|
|
// Does not start the time, just sets up the members needed.
|
|
exports.enroll = function(item, msecs) {
|
|
clearTimeout(item._idleTimeoutId);
|
|
item._idleTimeout = msecs;
|
|
};
|
|
|
|
exports.unenroll = function(item) {
|
|
clearTimeout(item._idleTimeoutId);
|
|
item._idleTimeout = -1;
|
|
};
|
|
|
|
exports._unrefActive = exports.active = function(item) {
|
|
clearTimeout(item._idleTimeoutId);
|
|
|
|
var msecs = item._idleTimeout;
|
|
if (msecs >= 0) {
|
|
item._idleTimeoutId = setTimeout(function onTimeout() {
|
|
if (item._onTimeout)
|
|
item._onTimeout();
|
|
}, msecs);
|
|
}
|
|
};
|
|
|
|
// That's not how node.js implements it but the exposed api is the same.
|
|
exports.setImmediate = typeof setImmediate === "function" ? setImmediate : function(fn) {
|
|
var id = nextImmediateId++;
|
|
var args = arguments.length < 2 ? false : slice.call(arguments, 1);
|
|
|
|
immediateIds[id] = true;
|
|
|
|
nextTick(function onNextTick() {
|
|
if (immediateIds[id]) {
|
|
// fn.call() is faster so we optimize for the common use-case
|
|
// @see http://jsperf.com/call-apply-segu
|
|
if (args) {
|
|
fn.apply(null, args);
|
|
} else {
|
|
fn.call(null);
|
|
}
|
|
// Prevent ids from leaking
|
|
exports.clearImmediate(id);
|
|
}
|
|
});
|
|
|
|
return id;
|
|
};
|
|
|
|
exports.clearImmediate = typeof clearImmediate === "function" ? clearImmediate : function(id) {
|
|
delete immediateIds[id];
|
|
};
|
|
}).call(this,require("timers").setImmediate,require("timers").clearImmediate)
|
|
|
|
},{"process/browser.js":187,"timers":194}],195:[function(require,module,exports){
|
|
// 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.
|
|
|
|
'use strict';
|
|
|
|
var punycode = require('punycode');
|
|
var util = require('./util');
|
|
|
|
exports.parse = urlParse;
|
|
exports.resolve = urlResolve;
|
|
exports.resolveObject = urlResolveObject;
|
|
exports.format = urlFormat;
|
|
|
|
exports.Url = Url;
|
|
|
|
function Url() {
|
|
this.protocol = null;
|
|
this.slashes = null;
|
|
this.auth = null;
|
|
this.host = null;
|
|
this.port = null;
|
|
this.hostname = null;
|
|
this.hash = null;
|
|
this.search = null;
|
|
this.query = null;
|
|
this.pathname = null;
|
|
this.path = null;
|
|
this.href = null;
|
|
}
|
|
|
|
// Reference: RFC 3986, RFC 1808, RFC 2396
|
|
|
|
// define these here so at least they only have to be
|
|
// compiled once on the first module load.
|
|
var protocolPattern = /^([a-z0-9.+-]+:)/i,
|
|
portPattern = /:[0-9]*$/,
|
|
|
|
// Special case for a simple path URL
|
|
simplePathPattern = /^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,
|
|
|
|
// RFC 2396: characters reserved for delimiting URLs.
|
|
// We actually just auto-escape these.
|
|
delims = ['<', '>', '"', '`', ' ', '\r', '\n', '\t'],
|
|
|
|
// RFC 2396: characters not allowed for various reasons.
|
|
unwise = ['{', '}', '|', '\\', '^', '`'].concat(delims),
|
|
|
|
// Allowed by RFCs, but cause of XSS attacks. Always escape these.
|
|
autoEscape = ['\''].concat(unwise),
|
|
// Characters that are never ever allowed in a hostname.
|
|
// Note that any invalid chars are also handled, but these
|
|
// are the ones that are *expected* to be seen, so we fast-path
|
|
// them.
|
|
nonHostChars = ['%', '/', '?', ';', '#'].concat(autoEscape),
|
|
hostEndingChars = ['/', '?', '#'],
|
|
hostnameMaxLen = 255,
|
|
hostnamePartPattern = /^[+a-z0-9A-Z_-]{0,63}$/,
|
|
hostnamePartStart = /^([+a-z0-9A-Z_-]{0,63})(.*)$/,
|
|
// protocols that can allow "unsafe" and "unwise" chars.
|
|
unsafeProtocol = {
|
|
'javascript': true,
|
|
'javascript:': true
|
|
},
|
|
// protocols that never have a hostname.
|
|
hostlessProtocol = {
|
|
'javascript': true,
|
|
'javascript:': true
|
|
},
|
|
// protocols that always contain a // bit.
|
|
slashedProtocol = {
|
|
'http': true,
|
|
'https': true,
|
|
'ftp': true,
|
|
'gopher': true,
|
|
'file': true,
|
|
'http:': true,
|
|
'https:': true,
|
|
'ftp:': true,
|
|
'gopher:': true,
|
|
'file:': true
|
|
},
|
|
querystring = require('querystring');
|
|
|
|
function urlParse(url, parseQueryString, slashesDenoteHost) {
|
|
if (url && util.isObject(url) && url instanceof Url) return url;
|
|
|
|
var u = new Url;
|
|
u.parse(url, parseQueryString, slashesDenoteHost);
|
|
return u;
|
|
}
|
|
|
|
Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) {
|
|
if (!util.isString(url)) {
|
|
throw new TypeError("Parameter 'url' must be a string, not " + typeof url);
|
|
}
|
|
|
|
// Copy chrome, IE, opera backslash-handling behavior.
|
|
// Back slashes before the query string get converted to forward slashes
|
|
// See: https://code.google.com/p/chromium/issues/detail?id=25916
|
|
var queryIndex = url.indexOf('?'),
|
|
splitter =
|
|
(queryIndex !== -1 && queryIndex < url.indexOf('#')) ? '?' : '#',
|
|
uSplit = url.split(splitter),
|
|
slashRegex = /\\/g;
|
|
uSplit[0] = uSplit[0].replace(slashRegex, '/');
|
|
url = uSplit.join(splitter);
|
|
|
|
var rest = url;
|
|
|
|
// trim before proceeding.
|
|
// This is to support parse stuff like " http://foo.com \n"
|
|
rest = rest.trim();
|
|
|
|
if (!slashesDenoteHost && url.split('#').length === 1) {
|
|
// Try fast path regexp
|
|
var simplePath = simplePathPattern.exec(rest);
|
|
if (simplePath) {
|
|
this.path = rest;
|
|
this.href = rest;
|
|
this.pathname = simplePath[1];
|
|
if (simplePath[2]) {
|
|
this.search = simplePath[2];
|
|
if (parseQueryString) {
|
|
this.query = querystring.parse(this.search.substr(1));
|
|
} else {
|
|
this.query = this.search.substr(1);
|
|
}
|
|
} else if (parseQueryString) {
|
|
this.search = '';
|
|
this.query = {};
|
|
}
|
|
return this;
|
|
}
|
|
}
|
|
|
|
var proto = protocolPattern.exec(rest);
|
|
if (proto) {
|
|
proto = proto[0];
|
|
var lowerProto = proto.toLowerCase();
|
|
this.protocol = lowerProto;
|
|
rest = rest.substr(proto.length);
|
|
}
|
|
|
|
// figure out if it's got a host
|
|
// user@server is *always* interpreted as a hostname, and url
|
|
// resolution will treat //foo/bar as host=foo,path=bar because that's
|
|
// how the browser resolves relative URLs.
|
|
if (slashesDenoteHost || proto || rest.match(/^\/\/[^@\/]+@[^@\/]+/)) {
|
|
var slashes = rest.substr(0, 2) === '//';
|
|
if (slashes && !(proto && hostlessProtocol[proto])) {
|
|
rest = rest.substr(2);
|
|
this.slashes = true;
|
|
}
|
|
}
|
|
|
|
if (!hostlessProtocol[proto] &&
|
|
(slashes || (proto && !slashedProtocol[proto]))) {
|
|
|
|
// there's a hostname.
|
|
// the first instance of /, ?, ;, or # ends the host.
|
|
//
|
|
// If there is an @ in the hostname, then non-host chars *are* allowed
|
|
// to the left of the last @ sign, unless some host-ending character
|
|
// comes *before* the @-sign.
|
|
// URLs are obnoxious.
|
|
//
|
|
// ex:
|
|
// http://a@b@c/ => user:a@b host:c
|
|
// http://a@b?@c => user:a host:c path:/?@c
|
|
|
|
// v0.12 TODO(isaacs): This is not quite how Chrome does things.
|
|
// Review our test case against browsers more comprehensively.
|
|
|
|
// find the first instance of any hostEndingChars
|
|
var hostEnd = -1;
|
|
for (var i = 0; i < hostEndingChars.length; i++) {
|
|
var hec = rest.indexOf(hostEndingChars[i]);
|
|
if (hec !== -1 && (hostEnd === -1 || hec < hostEnd))
|
|
hostEnd = hec;
|
|
}
|
|
|
|
// at this point, either we have an explicit point where the
|
|
// auth portion cannot go past, or the last @ char is the decider.
|
|
var auth, atSign;
|
|
if (hostEnd === -1) {
|
|
// atSign can be anywhere.
|
|
atSign = rest.lastIndexOf('@');
|
|
} else {
|
|
// atSign must be in auth portion.
|
|
// http://a@b/c@d => host:b auth:a path:/c@d
|
|
atSign = rest.lastIndexOf('@', hostEnd);
|
|
}
|
|
|
|
// Now we have a portion which is definitely the auth.
|
|
// Pull that off.
|
|
if (atSign !== -1) {
|
|
auth = rest.slice(0, atSign);
|
|
rest = rest.slice(atSign + 1);
|
|
this.auth = decodeURIComponent(auth);
|
|
}
|
|
|
|
// the host is the remaining to the left of the first non-host char
|
|
hostEnd = -1;
|
|
for (var i = 0; i < nonHostChars.length; i++) {
|
|
var hec = rest.indexOf(nonHostChars[i]);
|
|
if (hec !== -1 && (hostEnd === -1 || hec < hostEnd))
|
|
hostEnd = hec;
|
|
}
|
|
// if we still have not hit it, then the entire thing is a host.
|
|
if (hostEnd === -1)
|
|
hostEnd = rest.length;
|
|
|
|
this.host = rest.slice(0, hostEnd);
|
|
rest = rest.slice(hostEnd);
|
|
|
|
// pull out port.
|
|
this.parseHost();
|
|
|
|
// we've indicated that there is a hostname,
|
|
// so even if it's empty, it has to be present.
|
|
this.hostname = this.hostname || '';
|
|
|
|
// if hostname begins with [ and ends with ]
|
|
// assume that it's an IPv6 address.
|
|
var ipv6Hostname = this.hostname[0] === '[' &&
|
|
this.hostname[this.hostname.length - 1] === ']';
|
|
|
|
// validate a little.
|
|
if (!ipv6Hostname) {
|
|
var hostparts = this.hostname.split(/\./);
|
|
for (var i = 0, l = hostparts.length; i < l; i++) {
|
|
var part = hostparts[i];
|
|
if (!part) continue;
|
|
if (!part.match(hostnamePartPattern)) {
|
|
var newpart = '';
|
|
for (var j = 0, k = part.length; j < k; j++) {
|
|
if (part.charCodeAt(j) > 127) {
|
|
// we replace non-ASCII char with a temporary placeholder
|
|
// we need this to make sure size of hostname is not
|
|
// broken by replacing non-ASCII by nothing
|
|
newpart += 'x';
|
|
} else {
|
|
newpart += part[j];
|
|
}
|
|
}
|
|
// we test again with ASCII char only
|
|
if (!newpart.match(hostnamePartPattern)) {
|
|
var validParts = hostparts.slice(0, i);
|
|
var notHost = hostparts.slice(i + 1);
|
|
var bit = part.match(hostnamePartStart);
|
|
if (bit) {
|
|
validParts.push(bit[1]);
|
|
notHost.unshift(bit[2]);
|
|
}
|
|
if (notHost.length) {
|
|
rest = '/' + notHost.join('.') + rest;
|
|
}
|
|
this.hostname = validParts.join('.');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.hostname.length > hostnameMaxLen) {
|
|
this.hostname = '';
|
|
} else {
|
|
// hostnames are always lower case.
|
|
this.hostname = this.hostname.toLowerCase();
|
|
}
|
|
|
|
if (!ipv6Hostname) {
|
|
// IDNA Support: Returns a punycoded representation of "domain".
|
|
// It only converts parts of the domain name that
|
|
// have non-ASCII characters, i.e. it doesn't matter if
|
|
// you call it with a domain that already is ASCII-only.
|
|
this.hostname = punycode.toASCII(this.hostname);
|
|
}
|
|
|
|
var p = this.port ? ':' + this.port : '';
|
|
var h = this.hostname || '';
|
|
this.host = h + p;
|
|
this.href += this.host;
|
|
|
|
// strip [ and ] from the hostname
|
|
// the host field still retains them, though
|
|
if (ipv6Hostname) {
|
|
this.hostname = this.hostname.substr(1, this.hostname.length - 2);
|
|
if (rest[0] !== '/') {
|
|
rest = '/' + rest;
|
|
}
|
|
}
|
|
}
|
|
|
|
// now rest is set to the post-host stuff.
|
|
// chop off any delim chars.
|
|
if (!unsafeProtocol[lowerProto]) {
|
|
|
|
// First, make 100% sure that any "autoEscape" chars get
|
|
// escaped, even if encodeURIComponent doesn't think they
|
|
// need to be.
|
|
for (var i = 0, l = autoEscape.length; i < l; i++) {
|
|
var ae = autoEscape[i];
|
|
if (rest.indexOf(ae) === -1)
|
|
continue;
|
|
var esc = encodeURIComponent(ae);
|
|
if (esc === ae) {
|
|
esc = escape(ae);
|
|
}
|
|
rest = rest.split(ae).join(esc);
|
|
}
|
|
}
|
|
|
|
|
|
// chop off from the tail first.
|
|
var hash = rest.indexOf('#');
|
|
if (hash !== -1) {
|
|
// got a fragment string.
|
|
this.hash = rest.substr(hash);
|
|
rest = rest.slice(0, hash);
|
|
}
|
|
var qm = rest.indexOf('?');
|
|
if (qm !== -1) {
|
|
this.search = rest.substr(qm);
|
|
this.query = rest.substr(qm + 1);
|
|
if (parseQueryString) {
|
|
this.query = querystring.parse(this.query);
|
|
}
|
|
rest = rest.slice(0, qm);
|
|
} else if (parseQueryString) {
|
|
// no query string, but parseQueryString still requested
|
|
this.search = '';
|
|
this.query = {};
|
|
}
|
|
if (rest) this.pathname = rest;
|
|
if (slashedProtocol[lowerProto] &&
|
|
this.hostname && !this.pathname) {
|
|
this.pathname = '/';
|
|
}
|
|
|
|
//to support http.request
|
|
if (this.pathname || this.search) {
|
|
var p = this.pathname || '';
|
|
var s = this.search || '';
|
|
this.path = p + s;
|
|
}
|
|
|
|
// finally, reconstruct the href based on what has been validated.
|
|
this.href = this.format();
|
|
return this;
|
|
};
|
|
|
|
// format a parsed object into a url string
|
|
function urlFormat(obj) {
|
|
// ensure it's an object, and not a string url.
|
|
// If it's an obj, this is a no-op.
|
|
// this way, you can call url_format() on strings
|
|
// to clean up potentially wonky urls.
|
|
if (util.isString(obj)) obj = urlParse(obj);
|
|
if (!(obj instanceof Url)) return Url.prototype.format.call(obj);
|
|
return obj.format();
|
|
}
|
|
|
|
Url.prototype.format = function() {
|
|
var auth = this.auth || '';
|
|
if (auth) {
|
|
auth = encodeURIComponent(auth);
|
|
auth = auth.replace(/%3A/i, ':');
|
|
auth += '@';
|
|
}
|
|
|
|
var protocol = this.protocol || '',
|
|
pathname = this.pathname || '',
|
|
hash = this.hash || '',
|
|
host = false,
|
|
query = '';
|
|
|
|
if (this.host) {
|
|
host = auth + this.host;
|
|
} else if (this.hostname) {
|
|
host = auth + (this.hostname.indexOf(':') === -1 ?
|
|
this.hostname :
|
|
'[' + this.hostname + ']');
|
|
if (this.port) {
|
|
host += ':' + this.port;
|
|
}
|
|
}
|
|
|
|
if (this.query &&
|
|
util.isObject(this.query) &&
|
|
Object.keys(this.query).length) {
|
|
query = querystring.stringify(this.query);
|
|
}
|
|
|
|
var search = this.search || (query && ('?' + query)) || '';
|
|
|
|
if (protocol && protocol.substr(-1) !== ':') protocol += ':';
|
|
|
|
// only the slashedProtocols get the //. Not mailto:, xmpp:, etc.
|
|
// unless they had them to begin with.
|
|
if (this.slashes ||
|
|
(!protocol || slashedProtocol[protocol]) && host !== false) {
|
|
host = '//' + (host || '');
|
|
if (pathname && pathname.charAt(0) !== '/') pathname = '/' + pathname;
|
|
} else if (!host) {
|
|
host = '';
|
|
}
|
|
|
|
if (hash && hash.charAt(0) !== '#') hash = '#' + hash;
|
|
if (search && search.charAt(0) !== '?') search = '?' + search;
|
|
|
|
pathname = pathname.replace(/[?#]/g, function(match) {
|
|
return encodeURIComponent(match);
|
|
});
|
|
search = search.replace('#', '%23');
|
|
|
|
return protocol + host + pathname + search + hash;
|
|
};
|
|
|
|
function urlResolve(source, relative) {
|
|
return urlParse(source, false, true).resolve(relative);
|
|
}
|
|
|
|
Url.prototype.resolve = function(relative) {
|
|
return this.resolveObject(urlParse(relative, false, true)).format();
|
|
};
|
|
|
|
function urlResolveObject(source, relative) {
|
|
if (!source) return relative;
|
|
return urlParse(source, false, true).resolveObject(relative);
|
|
}
|
|
|
|
Url.prototype.resolveObject = function(relative) {
|
|
if (util.isString(relative)) {
|
|
var rel = new Url();
|
|
rel.parse(relative, false, true);
|
|
relative = rel;
|
|
}
|
|
|
|
var result = new Url();
|
|
var tkeys = Object.keys(this);
|
|
for (var tk = 0; tk < tkeys.length; tk++) {
|
|
var tkey = tkeys[tk];
|
|
result[tkey] = this[tkey];
|
|
}
|
|
|
|
// hash is always overridden, no matter what.
|
|
// even href="" will remove it.
|
|
result.hash = relative.hash;
|
|
|
|
// if the relative url is empty, then there's nothing left to do here.
|
|
if (relative.href === '') {
|
|
result.href = result.format();
|
|
return result;
|
|
}
|
|
|
|
// hrefs like //foo/bar always cut to the protocol.
|
|
if (relative.slashes && !relative.protocol) {
|
|
// take everything except the protocol from relative
|
|
var rkeys = Object.keys(relative);
|
|
for (var rk = 0; rk < rkeys.length; rk++) {
|
|
var rkey = rkeys[rk];
|
|
if (rkey !== 'protocol')
|
|
result[rkey] = relative[rkey];
|
|
}
|
|
|
|
//urlParse appends trailing / to urls like http://www.example.com
|
|
if (slashedProtocol[result.protocol] &&
|
|
result.hostname && !result.pathname) {
|
|
result.path = result.pathname = '/';
|
|
}
|
|
|
|
result.href = result.format();
|
|
return result;
|
|
}
|
|
|
|
if (relative.protocol && relative.protocol !== result.protocol) {
|
|
// if it's a known url protocol, then changing
|
|
// the protocol does weird things
|
|
// first, if it's not file:, then we MUST have a host,
|
|
// and if there was a path
|
|
// to begin with, then we MUST have a path.
|
|
// if it is file:, then the host is dropped,
|
|
// because that's known to be hostless.
|
|
// anything else is assumed to be absolute.
|
|
if (!slashedProtocol[relative.protocol]) {
|
|
var keys = Object.keys(relative);
|
|
for (var v = 0; v < keys.length; v++) {
|
|
var k = keys[v];
|
|
result[k] = relative[k];
|
|
}
|
|
result.href = result.format();
|
|
return result;
|
|
}
|
|
|
|
result.protocol = relative.protocol;
|
|
if (!relative.host && !hostlessProtocol[relative.protocol]) {
|
|
var relPath = (relative.pathname || '').split('/');
|
|
while (relPath.length && !(relative.host = relPath.shift()));
|
|
if (!relative.host) relative.host = '';
|
|
if (!relative.hostname) relative.hostname = '';
|
|
if (relPath[0] !== '') relPath.unshift('');
|
|
if (relPath.length < 2) relPath.unshift('');
|
|
result.pathname = relPath.join('/');
|
|
} else {
|
|
result.pathname = relative.pathname;
|
|
}
|
|
result.search = relative.search;
|
|
result.query = relative.query;
|
|
result.host = relative.host || '';
|
|
result.auth = relative.auth;
|
|
result.hostname = relative.hostname || relative.host;
|
|
result.port = relative.port;
|
|
// to support http.request
|
|
if (result.pathname || result.search) {
|
|
var p = result.pathname || '';
|
|
var s = result.search || '';
|
|
result.path = p + s;
|
|
}
|
|
result.slashes = result.slashes || relative.slashes;
|
|
result.href = result.format();
|
|
return result;
|
|
}
|
|
|
|
var isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/'),
|
|
isRelAbs = (
|
|
relative.host ||
|
|
relative.pathname && relative.pathname.charAt(0) === '/'
|
|
),
|
|
mustEndAbs = (isRelAbs || isSourceAbs ||
|
|
(result.host && relative.pathname)),
|
|
removeAllDots = mustEndAbs,
|
|
srcPath = result.pathname && result.pathname.split('/') || [],
|
|
relPath = relative.pathname && relative.pathname.split('/') || [],
|
|
psychotic = result.protocol && !slashedProtocol[result.protocol];
|
|
|
|
// if the url is a non-slashed url, then relative
|
|
// links like ../.. should be able
|
|
// to crawl up to the hostname, as well. This is strange.
|
|
// result.protocol has already been set by now.
|
|
// Later on, put the first path part into the host field.
|
|
if (psychotic) {
|
|
result.hostname = '';
|
|
result.port = null;
|
|
if (result.host) {
|
|
if (srcPath[0] === '') srcPath[0] = result.host;
|
|
else srcPath.unshift(result.host);
|
|
}
|
|
result.host = '';
|
|
if (relative.protocol) {
|
|
relative.hostname = null;
|
|
relative.port = null;
|
|
if (relative.host) {
|
|
if (relPath[0] === '') relPath[0] = relative.host;
|
|
else relPath.unshift(relative.host);
|
|
}
|
|
relative.host = null;
|
|
}
|
|
mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === '');
|
|
}
|
|
|
|
if (isRelAbs) {
|
|
// it's absolute.
|
|
result.host = (relative.host || relative.host === '') ?
|
|
relative.host : result.host;
|
|
result.hostname = (relative.hostname || relative.hostname === '') ?
|
|
relative.hostname : result.hostname;
|
|
result.search = relative.search;
|
|
result.query = relative.query;
|
|
srcPath = relPath;
|
|
// fall through to the dot-handling below.
|
|
} else if (relPath.length) {
|
|
// it's relative
|
|
// throw away the existing file, and take the new path instead.
|
|
if (!srcPath) srcPath = [];
|
|
srcPath.pop();
|
|
srcPath = srcPath.concat(relPath);
|
|
result.search = relative.search;
|
|
result.query = relative.query;
|
|
} else if (!util.isNullOrUndefined(relative.search)) {
|
|
// just pull out the search.
|
|
// like href='?foo'.
|
|
// Put this after the other two cases because it simplifies the booleans
|
|
if (psychotic) {
|
|
result.hostname = result.host = srcPath.shift();
|
|
//occationaly the auth can get stuck only in host
|
|
//this especially happens in cases like
|
|
//url.resolveObject('mailto:local1@domain1', 'local2@domain2')
|
|
var authInHost = result.host && result.host.indexOf('@') > 0 ?
|
|
result.host.split('@') : false;
|
|
if (authInHost) {
|
|
result.auth = authInHost.shift();
|
|
result.host = result.hostname = authInHost.shift();
|
|
}
|
|
}
|
|
result.search = relative.search;
|
|
result.query = relative.query;
|
|
//to support http.request
|
|
if (!util.isNull(result.pathname) || !util.isNull(result.search)) {
|
|
result.path = (result.pathname ? result.pathname : '') +
|
|
(result.search ? result.search : '');
|
|
}
|
|
result.href = result.format();
|
|
return result;
|
|
}
|
|
|
|
if (!srcPath.length) {
|
|
// no path at all. easy.
|
|
// we've already handled the other stuff above.
|
|
result.pathname = null;
|
|
//to support http.request
|
|
if (result.search) {
|
|
result.path = '/' + result.search;
|
|
} else {
|
|
result.path = null;
|
|
}
|
|
result.href = result.format();
|
|
return result;
|
|
}
|
|
|
|
// if a url ENDs in . or .., then it must get a trailing slash.
|
|
// however, if it ends in anything else non-slashy,
|
|
// then it must NOT get a trailing slash.
|
|
var last = srcPath.slice(-1)[0];
|
|
var hasTrailingSlash = (
|
|
(result.host || relative.host || srcPath.length > 1) &&
|
|
(last === '.' || last === '..') || last === '');
|
|
|
|
// strip single dots, resolve double dots to parent dir
|
|
// if the path tries to go above the root, `up` ends up > 0
|
|
var up = 0;
|
|
for (var i = srcPath.length; i >= 0; i--) {
|
|
last = srcPath[i];
|
|
if (last === '.') {
|
|
srcPath.splice(i, 1);
|
|
} else if (last === '..') {
|
|
srcPath.splice(i, 1);
|
|
up++;
|
|
} else if (up) {
|
|
srcPath.splice(i, 1);
|
|
up--;
|
|
}
|
|
}
|
|
|
|
// if the path is allowed to go above the root, restore leading ..s
|
|
if (!mustEndAbs && !removeAllDots) {
|
|
for (; up--; up) {
|
|
srcPath.unshift('..');
|
|
}
|
|
}
|
|
|
|
if (mustEndAbs && srcPath[0] !== '' &&
|
|
(!srcPath[0] || srcPath[0].charAt(0) !== '/')) {
|
|
srcPath.unshift('');
|
|
}
|
|
|
|
if (hasTrailingSlash && (srcPath.join('/').substr(-1) !== '/')) {
|
|
srcPath.push('');
|
|
}
|
|
|
|
var isAbsolute = srcPath[0] === '' ||
|
|
(srcPath[0] && srcPath[0].charAt(0) === '/');
|
|
|
|
// put the host back
|
|
if (psychotic) {
|
|
result.hostname = result.host = isAbsolute ? '' :
|
|
srcPath.length ? srcPath.shift() : '';
|
|
//occationaly the auth can get stuck only in host
|
|
//this especially happens in cases like
|
|
//url.resolveObject('mailto:local1@domain1', 'local2@domain2')
|
|
var authInHost = result.host && result.host.indexOf('@') > 0 ?
|
|
result.host.split('@') : false;
|
|
if (authInHost) {
|
|
result.auth = authInHost.shift();
|
|
result.host = result.hostname = authInHost.shift();
|
|
}
|
|
}
|
|
|
|
mustEndAbs = mustEndAbs || (result.host && srcPath.length);
|
|
|
|
if (mustEndAbs && !isAbsolute) {
|
|
srcPath.unshift('');
|
|
}
|
|
|
|
if (!srcPath.length) {
|
|
result.pathname = null;
|
|
result.path = null;
|
|
} else {
|
|
result.pathname = srcPath.join('/');
|
|
}
|
|
|
|
//to support request.http
|
|
if (!util.isNull(result.pathname) || !util.isNull(result.search)) {
|
|
result.path = (result.pathname ? result.pathname : '') +
|
|
(result.search ? result.search : '');
|
|
}
|
|
result.auth = relative.auth || result.auth;
|
|
result.slashes = result.slashes || relative.slashes;
|
|
result.href = result.format();
|
|
return result;
|
|
};
|
|
|
|
Url.prototype.parseHost = function() {
|
|
var host = this.host;
|
|
var port = portPattern.exec(host);
|
|
if (port) {
|
|
port = port[0];
|
|
if (port !== ':') {
|
|
this.port = port.substr(1);
|
|
}
|
|
host = host.substr(0, host.length - port.length);
|
|
}
|
|
if (host) this.hostname = host;
|
|
};
|
|
|
|
},{"./util":196,"punycode":188,"querystring":191}],196:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
module.exports = {
|
|
isString: function(arg) {
|
|
return typeof(arg) === 'string';
|
|
},
|
|
isObject: function(arg) {
|
|
return typeof(arg) === 'object' && arg !== null;
|
|
},
|
|
isNull: function(arg) {
|
|
return arg === null;
|
|
},
|
|
isNullOrUndefined: function(arg) {
|
|
return arg == null;
|
|
}
|
|
};
|
|
|
|
},{}]},{},[1])
|
|
//# sourceMappingURL=browser-matrix.js.map
|