You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
10865 lines
353 KiB
JavaScript
10865 lines
353 KiB
JavaScript
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;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 require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||
(function (global){
|
||
var matrixcs = require("./lib/matrix");
|
||
matrixcs.request(require("browser-request"));
|
||
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":5,"browser-request":20}],2:[function(require,module,exports){
|
||
"use strict";
|
||
|
||
var PushProcessor = require('./pushprocessor');
|
||
|
||
/**
|
||
* This is an internal module. See {@link MatrixClient} for the public class.
|
||
* @module client
|
||
*/
|
||
var EventEmitter = require("events").EventEmitter;
|
||
var q = require("q");
|
||
|
||
var httpApi = require("./http-api");
|
||
var MatrixEvent = require("./models/event").MatrixEvent;
|
||
var EventStatus = require("./models/event").EventStatus;
|
||
var StubStore = require("./store/stub");
|
||
var Room = require("./models/room");
|
||
var User = require("./models/user");
|
||
var webRtcCall = require("./webrtc/call");
|
||
var utils = require("./utils");
|
||
var contentRepo = require("./content-repo");
|
||
|
||
var CRYPTO_ENABLED = false;
|
||
|
||
try {
|
||
var Olm = require("olm");
|
||
if (Olm.Account && Olm.Session) {
|
||
CRYPTO_ENABLED = true;
|
||
}
|
||
} catch (e) {
|
||
// Olm not installed.
|
||
}
|
||
|
||
// TODO:
|
||
// Internal: rate limiting
|
||
|
||
var OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
|
||
|
||
/**
|
||
* 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}
|
||
* @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 Optional. The data store to use. If not specified,
|
||
* this client will not store any HTTP responses.
|
||
* @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
|
||
* @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>.
|
||
* {@link module:scheduler~MatrixScheduler#setProcessFunction}.
|
||
*/
|
||
function MatrixClient(opts) {
|
||
utils.checkObjectHasKeys(opts, ["baseUrl", "request"]);
|
||
|
||
this.baseUrl = opts.baseUrl;
|
||
this.idBaseUrl = opts.idBaseUrl;
|
||
|
||
this.store = opts.store || new StubStore();
|
||
this.sessionStore = opts.sessionStore || null;
|
||
this.accountKey = "DEFAULT_KEY";
|
||
this.deviceId = opts.deviceId;
|
||
if (CRYPTO_ENABLED && this.sessionStore !== null) {
|
||
var e2eAccount = this.sessionStore.getEndToEndAccount();
|
||
var account = new Olm.Account();
|
||
try {
|
||
if (e2eAccount === null) {
|
||
account.create();
|
||
} else {
|
||
account.unpickle(this.accountKey, e2eAccount);
|
||
}
|
||
var e2eKeys = JSON.parse(account.identity_keys());
|
||
var json = '{"algorithms":["' + OLM_ALGORITHM + '"]';
|
||
json += ',"device_id":"' + this.deviceId + '"';
|
||
json += ',"keys":';
|
||
json += '{"ed25519:' + this.deviceId + '":';
|
||
json += JSON.stringify(e2eKeys.ed25519);
|
||
json += ',"curve25519:' + this.deviceId + '":';
|
||
json += JSON.stringify(e2eKeys.curve25519);
|
||
json += '}';
|
||
json += ',"user_id":' + JSON.stringify(opts.userId);
|
||
json += '}';
|
||
var signature = account.sign(json);
|
||
this.deviceKeys = JSON.parse(json);
|
||
var signatures = {};
|
||
signatures[opts.userId] = {};
|
||
signatures[opts.userId]["ed25519:" + this.deviceId] = signature;
|
||
this.deviceKeys.signatures = signatures;
|
||
this.deviceCurve25519Key = e2eKeys.curve25519;
|
||
var pickled = account.pickle(this.accountKey);
|
||
this.sessionStore.storeEndToEndAccount(pickled);
|
||
var myDevices = this.sessionStore.getEndToEndDevicesForUser(
|
||
opts.userId
|
||
) || {};
|
||
myDevices[opts.deviceId] = this.deviceKeys;
|
||
this.sessionStore.storeEndToEndDevicesForUser(
|
||
opts.userId, myDevices
|
||
);
|
||
} finally {
|
||
account.free();
|
||
}
|
||
}
|
||
this.scheduler = opts.scheduler;
|
||
if (this.scheduler) {
|
||
var self = this;
|
||
this.scheduler.setProcessFunction(function(eventToSend) {
|
||
eventToSend.status = EventStatus.SENDING;
|
||
return _sendEventHttpRequest(self, eventToSend);
|
||
});
|
||
}
|
||
this.clientRunning = false;
|
||
|
||
var httpOpts = {
|
||
baseUrl: opts.baseUrl,
|
||
idBaseUrl: opts.idBaseUrl,
|
||
accessToken: opts.accessToken,
|
||
request: opts.request,
|
||
prefix: httpApi.PREFIX_V1,
|
||
onlyData: true,
|
||
extraParams: opts.queryParams
|
||
};
|
||
this.credentials = {
|
||
userId: (opts.userId || null)
|
||
};
|
||
this._http = new httpApi.MatrixHttpApi(httpOpts);
|
||
this._syncingRooms = {
|
||
// room_id: Promise
|
||
};
|
||
this.callList = {
|
||
// callId: MatrixCall
|
||
};
|
||
this._config = {}; // see startClient()
|
||
|
||
// 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;
|
||
}
|
||
|
||
}
|
||
utils.inherits(MatrixClient, EventEmitter);
|
||
|
||
/**
|
||
* Get the Homserver URL of this client
|
||
* @return {string} Homeserver URL of this client
|
||
*/
|
||
MatrixClient.prototype.getHomeserverUrl = function() {
|
||
return this.baseUrl;
|
||
};
|
||
|
||
/**
|
||
* Get the Identity Server URL of this client
|
||
* @return {string} Identity Server URL of this client
|
||
*/
|
||
MatrixClient.prototype.getIdentityServerUrl = function() {
|
||
return this.idBaseUrl;
|
||
};
|
||
|
||
/**
|
||
* Is end-to-end crypto enabled for this client.
|
||
* @return {boolean} True if end-to-end is enabled.
|
||
*/
|
||
MatrixClient.prototype.isCryptoEnabled = function() {
|
||
return CRYPTO_ENABLED && this.sessionStore !== null;
|
||
};
|
||
|
||
|
||
/**
|
||
* Upload the device keys to the homeserver and ensure that the
|
||
* homeserver has enough one-time keys.
|
||
* @param {number} maxKeys The maximum number of keys to generate
|
||
* @param {object} deferred A deferred to resolve when the keys are uploaded.
|
||
* @return {object} A promise that will resolve when the keys are uploaded.
|
||
*/
|
||
MatrixClient.prototype.uploadKeys = function(maxKeys, deferred) {
|
||
if (!CRYPTO_ENABLED || this.sessionStore === null) {
|
||
return q.reject(new Error("End-to-end encryption disabled"));
|
||
}
|
||
var first_time = deferred === undefined;
|
||
deferred = deferred || q.defer();
|
||
var path = "/keys/upload/" + this.deviceId;
|
||
var pickled = this.sessionStore.getEndToEndAccount();
|
||
if (!pickled) {
|
||
return q.reject(new Error("End-to-end account not found"));
|
||
}
|
||
var account = new Olm.Account();
|
||
var oneTimeKeys;
|
||
try {
|
||
account.unpickle(this.accountKey, pickled);
|
||
oneTimeKeys = JSON.parse(account.one_time_keys());
|
||
var maxOneTimeKeys = account.max_number_of_one_time_keys();
|
||
} finally {
|
||
account.free();
|
||
}
|
||
var oneTimeJson = {};
|
||
|
||
for (var keyId in oneTimeKeys.curve25519) {
|
||
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
|
||
oneTimeJson["curve25519:" + keyId] = oneTimeKeys.curve25519[keyId];
|
||
}
|
||
}
|
||
var content = {
|
||
device_keys: this.deviceKeys,
|
||
one_time_keys: oneTimeJson
|
||
};
|
||
var self = this;
|
||
this._http.authedRequestWithPrefix(
|
||
undefined, "POST", path, undefined, content, httpApi.PREFIX_V2_ALPHA
|
||
).then(function(res) {
|
||
var keyLimit = Math.floor(maxOneTimeKeys / 2);
|
||
var keyCount = res.one_time_key_counts.curve25519 || 0;
|
||
var generateKeys = (keyCount < keyLimit);
|
||
var pickled = self.sessionStore.getEndToEndAccount();
|
||
|
||
var account = new Olm.Account();
|
||
try {
|
||
account.unpickle(self.accountKey, pickled);
|
||
account.mark_keys_as_published();
|
||
if (generateKeys) {
|
||
var numberToGenerate = keyLimit - keyCount;
|
||
if (maxKeys) {
|
||
numberToGenerate = Math.min(numberToGenerate, maxKeys);
|
||
}
|
||
account.generate_one_time_keys(numberToGenerate);
|
||
}
|
||
pickled = account.pickle(self.accountKey);
|
||
self.sessionStore.storeEndToEndAccount(pickled);
|
||
} finally {
|
||
account.free();
|
||
}
|
||
if (generateKeys && first_time) {
|
||
self.uploadKeys(maxKeys, deferred);
|
||
} else {
|
||
deferred.resolve();
|
||
}
|
||
});
|
||
return deferred.promise;
|
||
};
|
||
|
||
|
||
/**
|
||
* 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 {object} A promise that will resolve when the keys are downloadded.
|
||
*/
|
||
MatrixClient.prototype.downloadKeys = function(userIds, forceDownload) {
|
||
if (!CRYPTO_ENABLED || this.sessionStore === null) {
|
||
return q.reject(new Error("End-to-end encryption disabled"));
|
||
}
|
||
var stored = {};
|
||
var notStored = {};
|
||
var downloadKeys = false;
|
||
for (var i = 0; i < userIds.length; ++i) {
|
||
var userId = userIds[i];
|
||
if (!forceDownload) {
|
||
var devices = this.sessionStore.getEndToEndDevicesForUser(userId);
|
||
if (devices) {
|
||
stored[userId] = devices;
|
||
continue;
|
||
}
|
||
}
|
||
downloadKeys = true;
|
||
notStored[userId] = {};
|
||
}
|
||
var deferred = q.defer();
|
||
if (downloadKeys) {
|
||
var path = "/keys/query";
|
||
var content = {device_keys: notStored};
|
||
var self = this;
|
||
this._http.authedRequestWithPrefix(
|
||
undefined, "POST", path, undefined, content,
|
||
httpApi.PREFIX_V2_ALPHA
|
||
).then(function(res) {
|
||
for (var userId in res.device_keys) {
|
||
if (userId in notStored) {
|
||
self.sessionStore.storeEndToEndDevicesForUser(
|
||
userId, res.device_keys[userId]
|
||
);
|
||
// TODO: validate the ed25519 signature.
|
||
stored[userId] = res.device_keys[userId];
|
||
}
|
||
}
|
||
deferred.resolve(stored);
|
||
});
|
||
} else {
|
||
deferred.resolve(stored);
|
||
}
|
||
return deferred.promise;
|
||
};
|
||
|
||
/**
|
||
* List the stored device keys for a user id
|
||
* @param {string} userId the user to list keys for.
|
||
* @return {Array} list of devices with "id" and "key" parameters.
|
||
*/
|
||
MatrixClient.prototype.listDeviceKeys = function(userId) {
|
||
if (!CRYPTO_ENABLED) {
|
||
return [];
|
||
}
|
||
var devices = this.sessionStore.getEndToEndDevicesForUser(userId);
|
||
var result = [];
|
||
if (devices) {
|
||
var deviceId;
|
||
var deviceIds = [];
|
||
for (deviceId in devices) {
|
||
if (devices.hasOwnProperty(deviceId)) {
|
||
deviceIds.push(deviceId);
|
||
}
|
||
}
|
||
deviceIds.sort();
|
||
for (var i = 0; i < deviceIds.length; ++i) {
|
||
deviceId = deviceIds[i];
|
||
var device = devices[deviceId];
|
||
var ed25519Key = device.keys["ed25519:" + deviceId];
|
||
if (ed25519Key) {
|
||
result.push({
|
||
id: deviceId,
|
||
key: ed25519Key
|
||
});
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
};
|
||
|
||
/**
|
||
* 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 {Object} A promise that will resolve when encryption is setup.
|
||
*/
|
||
MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
|
||
if (!this.sessionStore || !CRYPTO_ENABLED) {
|
||
return q.reject(new Error("End-to-End encryption disabled"));
|
||
}
|
||
if (config.algorithm === OLM_ALGORITHM) {
|
||
if (!config.members) {
|
||
throw new Error(
|
||
"Config must include a 'members' list with a list of userIds"
|
||
);
|
||
}
|
||
var devicesWithoutSession = [];
|
||
var userWithoutDevices = [];
|
||
for (var i = 0; i < config.members.length; ++i) {
|
||
var userId = config.members[i];
|
||
var devices = this.sessionStore.getEndToEndDevicesForUser(userId);
|
||
if (!devices) {
|
||
userWithoutDevices.push(userId);
|
||
} else {
|
||
for (var deviceId in devices) {
|
||
if (devices.hasOwnProperty(deviceId)) {
|
||
var keys = devices[deviceId];
|
||
var key = keys.keys["curve25519:" + deviceId];
|
||
if (key == this.deviceCurve25519Key) {
|
||
continue;
|
||
}
|
||
if (!this.sessionStore.getEndToEndSessions(key)) {
|
||
devicesWithoutSession.push([userId, deviceId, key]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
var deferred = q.defer();
|
||
if (devicesWithoutSession.length > 0) {
|
||
var queries = {};
|
||
for (i = 0; i < devicesWithoutSession.length; ++i) {
|
||
var device = devicesWithoutSession[i];
|
||
var query = queries[device[0]] || {};
|
||
queries[device[0]] = query;
|
||
query[device[1]] = "curve25519";
|
||
}
|
||
var path = "/keys/claim";
|
||
var content = {one_time_keys: queries};
|
||
var self = this;
|
||
this._http.authedRequestWithPrefix(
|
||
undefined, "POST", path, undefined, content,
|
||
httpApi.PREFIX_V2_ALPHA
|
||
).done(function(res) {
|
||
var missing = {};
|
||
for (i = 0; i < devicesWithoutSession.length; ++i) {
|
||
var device = devicesWithoutSession[i];
|
||
var userRes = res.one_time_keys[device[0]] || {};
|
||
var deviceRes = userRes[device[1]];
|
||
var oneTimeKey;
|
||
for (var keyId in deviceRes) {
|
||
if (keyId.indexOf("curve25519:") === 0) {
|
||
oneTimeKey = deviceRes[keyId];
|
||
}
|
||
}
|
||
if (oneTimeKey) {
|
||
var session = new Olm.Session();
|
||
var account = new Olm.Account();
|
||
try {
|
||
var pickled = self.sessionStore.getEndToEndAccount();
|
||
account.unpickle(self.accountKey, pickled);
|
||
session.create_outbound(account, device[2], oneTimeKey);
|
||
var sessionId = session.session_id();
|
||
pickled = session.pickle(self.accountKey);
|
||
self.sessionStore.storeEndToEndSession(
|
||
device[2], sessionId, pickled
|
||
);
|
||
} finally {
|
||
session.free();
|
||
account.free();
|
||
}
|
||
} else {
|
||
missing[device[0]] = missing[device[0]] || [];
|
||
missing[device[0]].push([device[1]]);
|
||
}
|
||
}
|
||
deferred.resolve({
|
||
missingUsers: userWithoutDevices,
|
||
missingDevices: missing
|
||
});
|
||
});
|
||
} else {
|
||
deferred.resolve({
|
||
missingUsers: userWithoutDevices,
|
||
missingDevices: []
|
||
});
|
||
}
|
||
this.sessionStore.storeEndToEndRoom(roomId, config);
|
||
return deferred.promise;
|
||
} else {
|
||
throw new Error("Unknown algorithm: " + config.algorithm);
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Disable encryption for a room.
|
||
* @param {string} roomId the room to disable encryption for.
|
||
*/
|
||
MatrixClient.prototype.disableRoomEncryption = function(roomId) {
|
||
if (this.sessionStore !== null) {
|
||
this.sessionStore.storeEndToEndRoom(roomId, null);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 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) {
|
||
if (CRYPTO_ENABLED && this.sessionStore !== null) {
|
||
return (this.sessionStore.getEndToEndRoom(roomId) && true) || false;
|
||
} else {
|
||
return false;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Get the room for the given room ID.
|
||
* @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);
|
||
};
|
||
|
||
// 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.
|
||
*/
|
||
MatrixClient.prototype.createRoom = function(options, callback) {
|
||
// valid options include: room_alias_name, visibility, invite
|
||
return this._http.authedRequest(
|
||
callback, "POST", "/createRoom", undefined, options
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 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 {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 || {
|
||
syncRoom: true
|
||
};
|
||
var room = this.getRoom(roomIdOrAlias);
|
||
if (room && room.hasMembershipState(this.credentials.userId, "join")) {
|
||
return q(room);
|
||
}
|
||
var path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias});
|
||
var defer = q.defer();
|
||
var self = this;
|
||
this._http.authedRequest(undefined, "POST", path, undefined, {}).then(
|
||
function(res) {
|
||
var roomId = res.room_id;
|
||
var room = createNewRoom(self, roomId);
|
||
if (opts.syncRoom) {
|
||
return _syncRoom(self, room);
|
||
}
|
||
return q(room);
|
||
}, function(err) {
|
||
_reject(callback, defer, err);
|
||
}).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) {
|
||
event.status = EventStatus.SENDING;
|
||
return _sendEvent(this, room, event);
|
||
};
|
||
|
||
/**
|
||
* @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);
|
||
};
|
||
|
||
/**
|
||
* 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
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
MatrixClient.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.
|
||
*/
|
||
MatrixClient.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} 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 = "m" + new Date().getTime();
|
||
}
|
||
|
||
// 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;
|
||
|
||
// add this event immediately to the local store as 'sending'.
|
||
if (room) {
|
||
localEvent.status = EventStatus.SENDING;
|
||
room.addEventsToTimeline([localEvent]);
|
||
}
|
||
|
||
if (eventType === "m.room.message" && this.sessionStore && CRYPTO_ENABLED) {
|
||
var e2eRoomInfo = this.sessionStore.getEndToEndRoom(roomId);
|
||
if (e2eRoomInfo) {
|
||
var encryptedContent = _encryptMessage(
|
||
this, roomId, e2eRoomInfo, eventType, content, txnId, callback
|
||
);
|
||
localEvent.encryptedType = "m.room.encrypted";
|
||
localEvent.encryptedContent = encryptedContent;
|
||
}
|
||
// TODO: Specify this in the event constructor rather than fiddling
|
||
// with the event object internals.
|
||
localEvent.encrypted = true;
|
||
}
|
||
|
||
return _sendEvent(this, room, localEvent, callback);
|
||
};
|
||
|
||
function _encryptMessage(client, roomId, e2eRoomInfo, eventType, content,
|
||
txnId, callback) {
|
||
if (!client.sessionStore) {
|
||
throw new Error(
|
||
"Client must have an end-to-end session store to encrypt messages"
|
||
);
|
||
}
|
||
|
||
if (e2eRoomInfo.algorithm === OLM_ALGORITHM) {
|
||
var participantKeys = [];
|
||
for (var i = 0; i < e2eRoomInfo.members.length; ++i) {
|
||
var userId = e2eRoomInfo.members[i];
|
||
var devices = client.sessionStore.getEndToEndDevicesForUser(userId);
|
||
for (var deviceId in devices) {
|
||
if (devices.hasOwnProperty(deviceId)) {
|
||
var keys = devices[deviceId];
|
||
for (var keyId in keys.keys) {
|
||
if (keyId.indexOf("curve25519:") === 0) {
|
||
participantKeys.push(keys.keys[keyId]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
participantKeys.sort();
|
||
var participantHash = ""; // Olm.sha256(participantKeys.join());
|
||
var payloadJson = {
|
||
room_id: roomId,
|
||
type: eventType,
|
||
fingerprint: participantHash,
|
||
sender_device: client.deviceId,
|
||
content: content
|
||
};
|
||
var ciphertext = {};
|
||
var payloadString = JSON.stringify(payloadJson);
|
||
for (i = 0; i < participantKeys.length; ++i) {
|
||
var deviceKey = participantKeys[i];
|
||
if (deviceKey == client.deviceCurve25519Key) {
|
||
continue;
|
||
}
|
||
var sessions = client.sessionStore.getEndToEndSessions(
|
||
deviceKey
|
||
);
|
||
var sessionIds = [];
|
||
for (var sessionId in sessions) {
|
||
if (sessions.hasOwnProperty(sessionId)) {
|
||
sessionIds.push(sessionId);
|
||
}
|
||
}
|
||
// Use the session with the lowest ID.
|
||
sessionIds.sort();
|
||
if (sessionIds.length === 0) {
|
||
// If we don't have a session for a device then
|
||
// we can't encrypt a message for it.
|
||
continue;
|
||
}
|
||
sessionId = sessionIds[0];
|
||
var session = new Olm.Session();
|
||
try {
|
||
session.unpickle(client.accountKey, sessions[sessionId]);
|
||
ciphertext[deviceKey] = session.encrypt(payloadString);
|
||
var pickled = session.pickle(client.accountKey);
|
||
client.sessionStore.storeEndToEndSession(
|
||
deviceKey, sessionId, pickled
|
||
);
|
||
} finally {
|
||
session.free();
|
||
}
|
||
}
|
||
var encryptedContent = {
|
||
algorithm: e2eRoomInfo.algorithm,
|
||
sender_key: client.deviceCurve25519Key,
|
||
ciphertext: ciphertext
|
||
};
|
||
return encryptedContent;
|
||
} else {
|
||
throw new Error("Unknown end-to-end algorithm: " + e2eRoomInfo.algorithm);
|
||
}
|
||
}
|
||
|
||
function _decryptMessage(client, event) {
|
||
if (client.sessionStore === null || !CRYPTO_ENABLED) {
|
||
// End to end encryption isn't enabled if we don't have a session
|
||
// store.
|
||
return _badEncryptedMessage(event, "**Encryption not enabled**");
|
||
}
|
||
|
||
var content = event.getContent();
|
||
if (content.algorithm === OLM_ALGORITHM) {
|
||
var deviceKey = content.sender_key;
|
||
var ciphertext = content.ciphertext;
|
||
|
||
if (!ciphertext) {
|
||
return _badEncryptedMessage(event, "**Missing ciphertext**");
|
||
}
|
||
if (!(client.deviceCurve25519Key in content.ciphertext)) {
|
||
return _badEncryptedMessage(event, "**Not included in recipients**");
|
||
}
|
||
var message = content.ciphertext[client.deviceCurve25519Key];
|
||
var sessions = client.sessionStore.getEndToEndSessions(deviceKey);
|
||
var payloadString = null;
|
||
var foundSession = false;
|
||
var session;
|
||
for (var sessionId in sessions) {
|
||
if (sessions.hasOwnProperty(sessionId)) {
|
||
session = new Olm.Session();
|
||
try {
|
||
session.unpickle(client.accountKey, sessions[sessionId]);
|
||
if (message.type === 0 && session.matches_inbound(message.body)) {
|
||
foundSession = true;
|
||
}
|
||
payloadString = session.decrypt(message.type, message.body);
|
||
var pickled = session.pickle(client.accountKey);
|
||
client.sessionStore.storeEndToEndSession(
|
||
deviceKey, sessionId, pickled
|
||
);
|
||
} catch (e) {
|
||
// Failed to decrypt with an existing session.
|
||
console.log(
|
||
"Failed to decrypt with an existing session: " + e.message
|
||
);
|
||
} finally {
|
||
session.free();
|
||
}
|
||
}
|
||
}
|
||
|
||
if (message.type === 0 && !foundSession && payloadString === null) {
|
||
var account = new Olm.Account();
|
||
session = new Olm.Session();
|
||
try {
|
||
var account_data = client.sessionStore.getEndToEndAccount();
|
||
account.unpickle(client.accountKey, account_data);
|
||
session.create_inbound_from(account, deviceKey, message.body);
|
||
payloadString = session.decrypt(message.type, message.body);
|
||
account.remove_one_time_keys(session);
|
||
var pickledSession = session.pickle(client.accountKey);
|
||
var pickledAccount = account.pickle(client.accountKey);
|
||
sessionId = session.session_id();
|
||
client.sessionStore.storeEndToEndSession(
|
||
deviceKey, sessionId, pickledSession
|
||
);
|
||
client.sessionStore.storeEndToEndAccount(pickledAccount);
|
||
} catch (e) {
|
||
// Failed to decrypt with a new session.
|
||
} finally {
|
||
session.free();
|
||
account.free();
|
||
}
|
||
}
|
||
|
||
if (payloadString !== null) {
|
||
var payload = JSON.parse(payloadString);
|
||
return new MatrixEvent({
|
||
// TODO: Add a key to indicate that the event was encrypted.
|
||
// TODO: Check the sender user id matches the sender key.
|
||
origin_server_ts: event.getTs(),
|
||
room_id: payload.room_id,
|
||
user_id: event.getSender(),
|
||
event_id: event.getId(),
|
||
type: payload.type,
|
||
content: payload.content
|
||
}, "encrypted");
|
||
} else {
|
||
return _badEncryptedMessage(event, "**Bad Encrypted Message**");
|
||
}
|
||
}
|
||
}
|
||
|
||
function _badEncryptedMessage(event, reason) {
|
||
return new MatrixEvent({
|
||
type: "m.room.message",
|
||
// TODO: Add rest of the event keys.
|
||
origin_server_ts: event.getTs(),
|
||
room_id: event.getRoomId(),
|
||
user_id: event.getSender(),
|
||
event_id: event.getId(),
|
||
content: {
|
||
msgtype: "m.bad.encrypted",
|
||
body: reason,
|
||
content: event.getContent()
|
||
}
|
||
});
|
||
}
|
||
|
||
function _sendEvent(client, room, event, callback) {
|
||
var defer = q.defer();
|
||
var promise;
|
||
// 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.
|
||
event.status = EventStatus.QUEUED;
|
||
}
|
||
}
|
||
|
||
if (!promise) {
|
||
promise = _sendEventHttpRequest(client, event);
|
||
}
|
||
|
||
promise.done(function(res) { // the request was sent OK
|
||
if (room) {
|
||
var eventId = res.event_id;
|
||
// try to find an event with this event_id. If we find it, this is
|
||
// the echo of this event *from the event stream* so we can remove
|
||
// the fake event we made above. If we don't find it, we're still
|
||
// waiting on the real event and so should assign the fake event
|
||
// with the real event_id for matching later.
|
||
var matchingEvent = utils.findElement(room.timeline, function(ev) {
|
||
return ev.getId() === eventId;
|
||
}, true);
|
||
if (matchingEvent) {
|
||
if (event.encryptedType) {
|
||
// Replace the content and type of the event with the
|
||
// plaintext that we sent to the server.
|
||
// TODO: Persist the changes if we storing events somewhere
|
||
// otherthan in memory.
|
||
matchingEvent.event.content = event.event.content;
|
||
matchingEvent.event.type = event.event.type;
|
||
}
|
||
utils.removeElement(room.timeline, function(ev) {
|
||
return ev.getId() === event.getId();
|
||
}, true);
|
||
}
|
||
else {
|
||
event.event.event_id = res.event_id;
|
||
event.status = null;
|
||
}
|
||
}
|
||
|
||
_resolve(callback, defer, res);
|
||
}, function(err) {
|
||
// the request failed to send.
|
||
event.status = EventStatus.NOT_SENT;
|
||
_reject(callback, defer, err);
|
||
});
|
||
|
||
return defer.promise;
|
||
}
|
||
|
||
function _sendEventHttpRequest(client, event) {
|
||
var pathParams = {
|
||
$roomId: event.getRoomId(),
|
||
$eventType: event.getWireType(),
|
||
$stateKey: event.getStateKey(),
|
||
$txnId: event._txnId ? event._txnId : new Date().getTime()
|
||
};
|
||
|
||
var path;
|
||
|
||
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()
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @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 = {
|
||
msgtype: "m.text",
|
||
body: 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 = {
|
||
msgtype: "m.notice",
|
||
body: 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 = {
|
||
msgtype: "m.emote",
|
||
body: 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} 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 = {
|
||
msgtype: "m.text",
|
||
format: "org.matrix.custom.html",
|
||
body: body,
|
||
formatted_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 = {
|
||
msgtype: "m.notice",
|
||
format: "org.matrix.custom.html",
|
||
body: body,
|
||
formatted_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) {
|
||
var path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||
$roomId: event.getRoomId(),
|
||
$receiptType: receiptType,
|
||
$eventId: event.getId()
|
||
});
|
||
return this._http.authedRequestWithPrefix(
|
||
callback, "POST", path, undefined, {}, httpApi.PREFIX_V2_ALPHA
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 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);
|
||
};
|
||
|
||
|
||
/**
|
||
* Upload a file to the media repository on the home server.
|
||
* @param {File} file object
|
||
* @param {module:client.callback} callback Optional.
|
||
* @return {module:client.Promise} Resolves: TODO
|
||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||
*/
|
||
MatrixClient.prototype.uploadContent = function(file, callback) {
|
||
return this._http.uploadContent(file, callback);
|
||
};
|
||
|
||
/**
|
||
* @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) {
|
||
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
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
MatrixClient.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
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
MatrixClient.prototype.getRoomIdForAlias = function(alias, callback) {
|
||
var path = utils.encodeUri("/directory/room/$alias", {
|
||
$alias: alias
|
||
});
|
||
return this._http.authedRequest(
|
||
callback, "GET", path
|
||
);
|
||
};
|
||
|
||
/**
|
||
* @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.
|
||
*/
|
||
MatrixClient.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 {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);
|
||
};
|
||
|
||
/**
|
||
* @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 {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.unban = function(roomId, userId, callback) {
|
||
// unbanning = set their state to leave
|
||
return _setMembershipState(
|
||
this, roomId, userId, "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.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.
|
||
* @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._pushActions === undefined) {
|
||
var pushProcessor = new PushProcessor(this);
|
||
event._pushActions = pushProcessor.actionsForEvent(event.event);
|
||
}
|
||
return event._pushActions;
|
||
};
|
||
|
||
// 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.
|
||
*/
|
||
MatrixClient.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);
|
||
};
|
||
|
||
/**
|
||
* @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".
|
||
* @return {?string} the avatar URL or null.
|
||
*/
|
||
MatrixClient.prototype.mxcUrlToHttp =
|
||
function(mxcUrl, width, height, resizeMethod) {
|
||
return contentRepo.getHttpUriForMxc(
|
||
this.baseUrl, mxcUrl, width, height, resizeMethod
|
||
);
|
||
};
|
||
|
||
/**
|
||
* @param {module:client.callback} callback Optional.
|
||
* @return {module:client.Promise} Resolves: TODO
|
||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||
*/
|
||
MatrixClient.prototype.getThreePids = function(callback) {
|
||
var path = "/account/3pid";
|
||
return this._http.authedRequestWithPrefix(
|
||
callback, "GET", path, undefined, undefined, httpApi.PREFIX_V2_ALPHA
|
||
);
|
||
};
|
||
|
||
/**
|
||
* @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.
|
||
*/
|
||
MatrixClient.prototype.addThreePid = function(creds, bind, callback) {
|
||
var path = "/account/3pid";
|
||
var data = {
|
||
'threePidCreds': creds,
|
||
'bind': bind
|
||
};
|
||
return this._http.authedRequestWithPrefix(
|
||
callback, "POST", path, null, data, httpApi.PREFIX_V2_ALPHA
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
MatrixClient.prototype.setPassword = function(authDict, newPassword, callback) {
|
||
var path = "/account/password";
|
||
var data = {
|
||
'auth': authDict,
|
||
'new_password': newPassword
|
||
};
|
||
|
||
return this._http.authedRequestWithPrefix(
|
||
callback, "POST", path, null, data, httpApi.PREFIX_V2_ALPHA
|
||
);
|
||
};
|
||
|
||
/**
|
||
* @param {string} presence
|
||
* @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(presence, callback) {
|
||
var path = utils.encodeUri("/presence/$userId/status", {
|
||
$userId: this.credentials.userId
|
||
});
|
||
var validStates = ["offline", "online", "unavailable"];
|
||
if (validStates.indexOf(presence) == -1) {
|
||
throw new Error("Bad presence value: " + presence);
|
||
}
|
||
var content = {
|
||
presence: presence
|
||
};
|
||
return this._http.authedRequest(
|
||
callback, "PUT", path, undefined, content
|
||
);
|
||
};
|
||
|
||
// Public (non-authed) 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.publicRooms = function(callback) {
|
||
return this._http.request(callback, "GET", "/publicRooms");
|
||
};
|
||
|
||
/**
|
||
* @param {module:client.callback} callback Optional.
|
||
* @return {module:client.Promise} Resolves: TODO
|
||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||
*/
|
||
MatrixClient.prototype.loginFlows = function(callback) {
|
||
return this._http.request(callback, "GET", "/login");
|
||
};
|
||
|
||
/**
|
||
* @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.
|
||
*/
|
||
MatrixClient.prototype.resolveRoomAlias = function(roomAlias, callback) {
|
||
var path = utils.encodeUri("/directory/room/$alias", {$alias: roomAlias});
|
||
return this._http.request(callback, "GET", path);
|
||
};
|
||
|
||
/**
|
||
* @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.
|
||
*/
|
||
MatrixClient.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 }
|
||
);
|
||
};
|
||
|
||
/**
|
||
* @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.roomState = function(roomId, callback) {
|
||
var path = utils.encodeUri("/rooms/$roomId/state", {$roomId: roomId});
|
||
return this._http.authedRequest(callback, "GET", path);
|
||
};
|
||
|
||
/**
|
||
* Retrieve older messages from the given room and put them in the timeline.
|
||
* @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;
|
||
|
||
if (room.oldState.paginationToken === null) {
|
||
return q(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 q(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 = q.defer();
|
||
var self = this;
|
||
this._http.authedRequest(callback, "GET", path, params).done(function(res) {
|
||
var matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self));
|
||
room.addEventsToTimeline(matrixEvents, true);
|
||
room.oldState.paginationToken = res.end;
|
||
if (res.chunk.length < limit) {
|
||
room.oldState.paginationToken = null;
|
||
}
|
||
self.store.storeEvents(room, matrixEvents, res.end, true);
|
||
_resolve(callback, defer, room);
|
||
}, function(err) {
|
||
_reject(callback, defer, err);
|
||
});
|
||
return defer.promise;
|
||
};
|
||
|
||
// Registration/Login operations
|
||
// =============================
|
||
|
||
/**
|
||
* @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.
|
||
*/
|
||
MatrixClient.prototype.login = function(loginType, data, callback) {
|
||
data.type = loginType;
|
||
return this._http.authedRequest(
|
||
callback, "POST", "/login", undefined, data
|
||
);
|
||
};
|
||
|
||
/**
|
||
* @param {string} username
|
||
* @param {string} password
|
||
* @param {string} sessionId
|
||
* @param {Object} auth
|
||
* @param {module:client.callback} callback Optional.
|
||
* @return {module:client.Promise} Resolves: TODO
|
||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||
*/
|
||
MatrixClient.prototype.register = function(username, password,
|
||
sessionId, auth, callback) {
|
||
if (auth === undefined) { auth = {}; }
|
||
if (sessionId) { auth.session = sessionId; }
|
||
|
||
var params = {
|
||
auth: auth
|
||
};
|
||
if (username !== undefined) { params.username = username; }
|
||
if (password !== undefined) { params.password = password; }
|
||
|
||
return this._http.requestWithPrefix(
|
||
callback, "POST", "/register", undefined,
|
||
params, httpApi.PREFIX_V2_ALPHA
|
||
);
|
||
};
|
||
|
||
/**
|
||
* @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.
|
||
*/
|
||
MatrixClient.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.
|
||
*/
|
||
MatrixClient.prototype.loginWithSAML2 = function(relayState, callback) {
|
||
return this.login("m.login.saml2", {
|
||
relay_state: relayState
|
||
}, callback);
|
||
};
|
||
|
||
/**
|
||
* @param {module:client.callback} callback Optional.
|
||
* @return {module:client.Promise} Resolves: TODO
|
||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||
*/
|
||
MatrixClient.prototype.getCasServer = function(callback) {
|
||
return this._http.request(
|
||
callback, "GET", "/login/cas", undefined, undefined
|
||
);
|
||
};
|
||
|
||
/**
|
||
* @param {string} ticket (Received from CAS)
|
||
* @param {string} service Service to which the token was granted
|
||
* @param {module:client.callback} callback Optional.
|
||
* @return {module:client.Promise} Resolves: TODO
|
||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||
*/
|
||
MatrixClient.prototype.loginWithCas = function(ticket, service, callback) {
|
||
return this.login("m.login.cas", {
|
||
ticket: ticket,
|
||
service: service
|
||
}, callback);
|
||
};
|
||
|
||
// Push 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.pushRules = 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.
|
||
*/
|
||
MatrixClient.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.
|
||
*/
|
||
MatrixClient.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);
|
||
};
|
||
|
||
/**
|
||
* 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) {
|
||
return this.search({
|
||
body: {
|
||
search_categories: {
|
||
room_events: {
|
||
keys: opts.keys,
|
||
search_term: opts.query
|
||
}
|
||
}
|
||
}
|
||
}, callback);
|
||
};
|
||
|
||
/**
|
||
* Perform a server-side search.
|
||
* @param {Object} opts
|
||
* @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.
|
||
*/
|
||
MatrixClient.prototype.search = function(opts, callback) {
|
||
return this._http.authedRequest(
|
||
callback, "POST", "/search", undefined, opts.body
|
||
);
|
||
};
|
||
|
||
|
||
// 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 || [];
|
||
};
|
||
|
||
/**
|
||
* @return {boolean} true if there is a valid access_token for this client.
|
||
*/
|
||
MatrixClient.prototype.isLoggedIn = function() {
|
||
return this._http.opts.accessToken !== undefined;
|
||
};
|
||
|
||
|
||
// 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
|
||
|
||
|
||
/**
|
||
* This is an internal method.
|
||
* @param {MatrixClient} client
|
||
* @param {integer} historyLen
|
||
* @param {integer} includeArchived
|
||
*/
|
||
function doInitialSync(client, historyLen, includeArchived) {
|
||
var qps = { limit: historyLen };
|
||
if (includeArchived) {
|
||
qps.archived = true;
|
||
}
|
||
client._http.authedRequest(
|
||
undefined, "GET", "/initialSync", qps
|
||
).done(function(data) {
|
||
var i, j;
|
||
// intercept the results and put them into our store
|
||
if (!(client.store instanceof StubStore)) {
|
||
utils.forEach(
|
||
utils.map(data.presence, _PojoToMatrixEventMapper(client)),
|
||
function(e) {
|
||
var user = createNewUser(client, e.getContent().user_id);
|
||
user.setPresenceEvent(e);
|
||
client.store.storeUser(user);
|
||
});
|
||
|
||
// group receipts by room ID.
|
||
var receiptsByRoom = {};
|
||
data.receipts = data.receipts || [];
|
||
utils.forEach(data.receipts.map(_PojoToMatrixEventMapper(client)),
|
||
function(receiptEvent) {
|
||
if (!receiptsByRoom[receiptEvent.getRoomId()]) {
|
||
receiptsByRoom[receiptEvent.getRoomId()] = [];
|
||
}
|
||
receiptsByRoom[receiptEvent.getRoomId()].push(receiptEvent);
|
||
}
|
||
);
|
||
|
||
for (i = 0; i < data.rooms.length; i++) {
|
||
var room = createNewRoom(client, data.rooms[i].room_id);
|
||
if (!data.rooms[i].state) {
|
||
data.rooms[i].state = [];
|
||
}
|
||
if (data.rooms[i].membership === "invite") {
|
||
var inviteEvent = data.rooms[i].invite;
|
||
if (!inviteEvent) {
|
||
// fallback for servers which don't serve the invite key yet
|
||
inviteEvent = {
|
||
event_id: "$fake_" + room.roomId,
|
||
content: {
|
||
membership: "invite"
|
||
},
|
||
state_key: client.credentials.userId,
|
||
user_id: data.rooms[i].inviter,
|
||
room_id: room.roomId,
|
||
type: "m.room.member"
|
||
};
|
||
}
|
||
data.rooms[i].state.push(inviteEvent);
|
||
}
|
||
|
||
_processRoomEvents(
|
||
client, room, data.rooms[i].state, data.rooms[i].messages
|
||
);
|
||
|
||
var receipts = receiptsByRoom[room.roomId] || [];
|
||
for (j = 0; j < receipts.length; j++) {
|
||
room.addReceipt(receipts[j]);
|
||
}
|
||
|
||
// cache the name/summary/etc prior to storage since we don't
|
||
// know how the store will serialise the Room.
|
||
room.recalculate(client.credentials.userId);
|
||
|
||
client.store.storeRoom(room);
|
||
client.emit("Room", room);
|
||
}
|
||
}
|
||
|
||
if (data) {
|
||
client.store.setSyncToken(data.end);
|
||
var events = [];
|
||
for (i = 0; i < data.presence.length; i++) {
|
||
events.push(new MatrixEvent(data.presence[i]));
|
||
}
|
||
for (i = 0; i < data.rooms.length; i++) {
|
||
if (data.rooms[i].state) {
|
||
for (j = 0; j < data.rooms[i].state.length; j++) {
|
||
events.push(new MatrixEvent(data.rooms[i].state[j]));
|
||
}
|
||
}
|
||
if (data.rooms[i].messages) {
|
||
for (j = 0; j < data.rooms[i].messages.chunk.length; j++) {
|
||
events.push(
|
||
new MatrixEvent(data.rooms[i].messages.chunk[j])
|
||
);
|
||
}
|
||
}
|
||
}
|
||
utils.forEach(events, function(e) {
|
||
client.emit("event", e);
|
||
});
|
||
}
|
||
|
||
client.clientRunning = true;
|
||
client.emit("syncComplete");
|
||
_pollForEvents(client);
|
||
}, function(err) {
|
||
console.error("/initialSync error: %s", err);
|
||
client.emit("syncError", err);
|
||
// TODO: Retries.
|
||
});
|
||
}
|
||
|
||
/**
|
||
* High level helper method to call initialSync, emit the resulting events,
|
||
* and then start polling the eventStream for new events. To listen for these
|
||
* events, add a listener for {@link module:client~MatrixClient#event:"event"}
|
||
* via {@link module:client~MatrixClient#on}.
|
||
* @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.
|
||
*/
|
||
MatrixClient.prototype.startClient = function(opts) {
|
||
if (this.clientRunning) {
|
||
// client is already running.
|
||
return;
|
||
}
|
||
// backwards compat for when 'opts' was 'historyLen'.
|
||
if (typeof opts === "number") {
|
||
opts = {
|
||
initialSyncLimit: opts
|
||
};
|
||
}
|
||
|
||
opts = opts || {};
|
||
opts.initialSyncLimit = opts.initialSyncLimit || 8;
|
||
opts.includeArchivedRooms = opts.includeArchivedRooms || false;
|
||
opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false;
|
||
this._config = opts;
|
||
|
||
if (CRYPTO_ENABLED && this.sessionStore !== null) {
|
||
this.uploadKeys(5);
|
||
}
|
||
|
||
if (this.store.getSyncToken()) {
|
||
// resume from where we left off.
|
||
_pollForEvents(this);
|
||
return;
|
||
}
|
||
|
||
// periodically poll for turn servers if we support voip
|
||
checkTurnServers(this);
|
||
|
||
var self = this;
|
||
this.pushRules().done(function(result) {
|
||
self.pushRules = result;
|
||
doInitialSync(self, opts.initialSyncLimit, opts.includeArchivedRooms);
|
||
}, function(err) {
|
||
self.emit("syncError", err);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* This is an internal method.
|
||
* @param {MatrixClient} client
|
||
*/
|
||
function _pollForEvents(client) {
|
||
var self = client;
|
||
if (!client.clientRunning) {
|
||
return;
|
||
}
|
||
var discardResult = false;
|
||
var timeoutObj = setTimeout(function() {
|
||
discardResult = true;
|
||
console.error("/events request timed out.");
|
||
_pollForEvents(client);
|
||
}, 40000);
|
||
|
||
client._http.authedRequest(undefined, "GET", "/events", {
|
||
from: client.store.getSyncToken(),
|
||
timeout: 30000
|
||
}).done(function(data) {
|
||
if (discardResult) {
|
||
return;
|
||
}
|
||
else {
|
||
clearTimeout(timeoutObj);
|
||
}
|
||
var events = [];
|
||
if (data) {
|
||
events = utils.map(data.chunk, _PojoToMatrixEventMapper(self));
|
||
}
|
||
if (!(self.store instanceof StubStore)) {
|
||
var roomIdsWithNewInvites = {};
|
||
// bucket events based on room.
|
||
var i = 0;
|
||
var roomIdToEvents = {};
|
||
for (i = 0; i < events.length; i++) {
|
||
var roomId = events[i].getRoomId();
|
||
// possible to have no room ID e.g. for presence events.
|
||
if (roomId) {
|
||
if (!roomIdToEvents[roomId]) {
|
||
roomIdToEvents[roomId] = [];
|
||
}
|
||
roomIdToEvents[roomId].push(events[i]);
|
||
if (events[i].getType() === "m.room.member" &&
|
||
events[i].getContent().membership === "invite") {
|
||
roomIdsWithNewInvites[roomId] = true;
|
||
}
|
||
}
|
||
else if (events[i].getType() === "m.presence") {
|
||
var usr = self.store.getUser(events[i].getContent().user_id);
|
||
if (usr) {
|
||
usr.setPresenceEvent(events[i]);
|
||
}
|
||
else {
|
||
usr = createNewUser(self, events[i].getContent().user_id);
|
||
usr.setPresenceEvent(events[i]);
|
||
self.store.storeUser(usr);
|
||
}
|
||
}
|
||
}
|
||
|
||
// add events to room
|
||
var roomIds = utils.keys(roomIdToEvents);
|
||
utils.forEach(roomIds, function(roomId) {
|
||
var room = self.store.getRoom(roomId);
|
||
var isBrandNewRoom = false;
|
||
if (!room) {
|
||
room = createNewRoom(self, roomId);
|
||
isBrandNewRoom = true;
|
||
}
|
||
|
||
var wasJoined = room.hasMembershipState(
|
||
self.credentials.userId, "join"
|
||
);
|
||
|
||
room.addEvents(roomIdToEvents[roomId], "replace");
|
||
room.recalculate(self.credentials.userId);
|
||
|
||
// store the Room for things like invite events so developers
|
||
// can update the UI
|
||
if (isBrandNewRoom) {
|
||
self.store.storeRoom(room);
|
||
self.emit("Room", room);
|
||
}
|
||
|
||
var justJoined = room.hasMembershipState(
|
||
self.credentials.userId, "join"
|
||
);
|
||
|
||
if (!wasJoined && justJoined) {
|
||
// we've just transitioned into a join state for this room,
|
||
// so sync state.
|
||
_syncRoom(self, room);
|
||
}
|
||
});
|
||
|
||
Object.keys(roomIdsWithNewInvites).forEach(function(inviteRoomId) {
|
||
_resolveInvites(self, self.store.getRoom(inviteRoomId));
|
||
});
|
||
}
|
||
if (data) {
|
||
self.store.setSyncToken(data.end);
|
||
utils.forEach(events, function(e) {
|
||
self.emit("event", e);
|
||
});
|
||
}
|
||
_pollForEvents(self);
|
||
}, function(err) {
|
||
console.error("/events error: %s", JSON.stringify(err));
|
||
if (discardResult) {
|
||
return;
|
||
}
|
||
else {
|
||
clearTimeout(timeoutObj);
|
||
}
|
||
self.emit("syncError", err);
|
||
// retry every few seconds
|
||
// FIXME: this should be exponential backoff with an option to nudge
|
||
setTimeout(function() {
|
||
_pollForEvents(self);
|
||
}, 2000);
|
||
});
|
||
}
|
||
|
||
function _syncRoom(client, room) {
|
||
if (client._syncingRooms[room.roomId]) {
|
||
return client._syncingRooms[room.roomId];
|
||
}
|
||
var defer = q.defer();
|
||
client._syncingRooms[room.roomId] = defer.promise;
|
||
client.roomInitialSync(room.roomId, 8).done(function(res) {
|
||
room.timeline = []; // blow away any previous messages.
|
||
_processRoomEvents(client, room, res.state, res.messages);
|
||
room.recalculate(client.credentials.userId);
|
||
client.store.storeRoom(room);
|
||
client.emit("Room", room);
|
||
defer.resolve(room);
|
||
client._syncingRooms[room.roomId] = undefined;
|
||
}, function(err) {
|
||
defer.reject(err);
|
||
client._syncingRooms[room.roomId] = undefined;
|
||
});
|
||
return defer.promise;
|
||
}
|
||
|
||
function _processRoomEvents(client, room, stateEventList, messageChunk) {
|
||
// "old" and "current" state are the same initially; they
|
||
// start diverging if the user paginates.
|
||
// We must deep copy otherwise membership changes in old state
|
||
// will leak through to current state!
|
||
var oldStateEvents = utils.map(
|
||
utils.deepCopy(stateEventList), _PojoToMatrixEventMapper(client)
|
||
);
|
||
var stateEvents = utils.map(stateEventList, _PojoToMatrixEventMapper(client));
|
||
room.oldState.setStateEvents(oldStateEvents);
|
||
room.currentState.setStateEvents(stateEvents);
|
||
|
||
_resolveInvites(client, room);
|
||
|
||
// add events to the timeline *after* setting the state
|
||
// events so messages use the right display names. Initial sync
|
||
// returns messages in chronological order, so we need to reverse
|
||
// it to get most recent -> oldest. We need it in that order in
|
||
// order to diverge old/current state correctly.
|
||
room.addEventsToTimeline(
|
||
utils.map(
|
||
messageChunk ? messageChunk.chunk : [],
|
||
_PojoToMatrixEventMapper(client)
|
||
).reverse(), true
|
||
);
|
||
if (messageChunk) {
|
||
room.oldState.paginationToken = messageChunk.start;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* High level helper method to stop the client from polling and allow a
|
||
* clean shutdown.
|
||
*/
|
||
MatrixClient.prototype.stopClient = function() {
|
||
this.clientRunning = false;
|
||
// TODO: f.e. Room => self.store.storeRoom(room) ?
|
||
};
|
||
|
||
|
||
function reEmit(reEmitEntity, emittableEntity, eventNames) {
|
||
utils.forEach(eventNames, function(eventName) {
|
||
// setup a listener on the entity (the Room, User, etc) for this event
|
||
emittableEntity.on(eventName, function() {
|
||
// take the args from the listener and reuse them, adding the
|
||
// event name to the arg list so it works with .emit()
|
||
// Transformation Example:
|
||
// listener on "foo" => function(a,b) { ... }
|
||
// Re-emit on "thing" => thing.emit("foo", a, b)
|
||
var newArgs = [eventName];
|
||
for (var i = 0; i < arguments.length; i++) {
|
||
newArgs.push(arguments[i]);
|
||
}
|
||
reEmitEntity.emit.apply(reEmitEntity, newArgs);
|
||
});
|
||
});
|
||
}
|
||
|
||
function _resolveInvites(client, room) {
|
||
if (!room || !client._config.resolveInvitesToProfiles) {
|
||
return;
|
||
}
|
||
// 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;
|
||
if (user) {
|
||
promise = q({
|
||
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;
|
||
member.setMembershipEvent(inviteEvent, room.currentState); // fire listeners
|
||
}, function(err) {
|
||
// OH WELL.
|
||
});
|
||
});
|
||
}
|
||
|
||
function setupCallEventHandler(client) {
|
||
var candidatesByCall = {
|
||
// callId: [Candidate]
|
||
};
|
||
client.on("event", function(event) {
|
||
if (event.getType().indexOf("m.call.") !== 0) {
|
||
return; // not a call event
|
||
}
|
||
var content = event.getContent();
|
||
var call = content.call_id ? client.callList[content.call_id] : undefined;
|
||
var i;
|
||
//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());
|
||
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;
|
||
var existingCalls = utils.values(client.callList);
|
||
for (i = 0; i < existingCalls.length; ++i) {
|
||
var thisCall = existingCalls[i];
|
||
if (call.room_id === thisCall.room_id &&
|
||
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;
|
||
}
|
||
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
|
||
setTimeout(function() { checkTurnServers(client); },
|
||
(res.ttl || (60 * 60)) * 1000 * 0.9
|
||
);
|
||
}
|
||
}, function(err) {
|
||
console.error("Failed to get TURN URIs");
|
||
setTimeout(function() { checkTurnServers(client); }, 60000);
|
||
});
|
||
}
|
||
|
||
function createNewUser(client, userId) {
|
||
var user = new User(userId);
|
||
reEmit(client, user, ["User.avatarUrl", "User.displayName", "User.presence"]);
|
||
return user;
|
||
}
|
||
|
||
function createNewRoom(client, roomId) {
|
||
var room = new Room(roomId);
|
||
reEmit(client, room, ["Room.name", "Room.timeline"]);
|
||
|
||
// 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?)
|
||
reEmit(client, room.currentState, [
|
||
"RoomState.events", "RoomState.members", "RoomState.newMember"
|
||
]);
|
||
room.currentState.on("RoomState.newMember", function(event, state, member) {
|
||
member.user = client.getUser(member.userId);
|
||
reEmit(
|
||
client, member,
|
||
[
|
||
"RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel",
|
||
"RoomMember.membership"
|
||
]
|
||
);
|
||
});
|
||
return room;
|
||
}
|
||
|
||
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.getType() === "m.room.encrypted") {
|
||
return _decryptMessage(client, event);
|
||
} else {
|
||
return event;
|
||
}
|
||
}
|
||
return mapper;
|
||
}
|
||
|
||
// Identity Server Operations
|
||
// ==========================
|
||
|
||
/**
|
||
* @param {string} email
|
||
* @param {string} clientSecret
|
||
* @param {string} sendAttempt
|
||
* @param {string} nextLink 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.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
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 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.
|
||
* @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 has a problem syncing. <strong>This event is experimental
|
||
* and may change.</strong>
|
||
* @event module:client~MatrixClient#"syncError"
|
||
* @param {MatrixError} err The matrix error which caused this event to fire.
|
||
* @example
|
||
* matrixClient.on("syncError", function(err){
|
||
* // update UI to say "Connection Lost"
|
||
* });
|
||
*/
|
||
|
||
/**
|
||
* Fires when the SDK has finished catching up and is now listening for live
|
||
* events. <strong>This event is experimental and may change.</strong>
|
||
* @event module:client~MatrixClient#"syncComplete"
|
||
* @example
|
||
* matrixClient.on("syncComplete", function(){
|
||
* var rooms = matrixClient.getRooms();
|
||
* });
|
||
*/
|
||
|
||
/**
|
||
* 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 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
|
||
* });
|
||
*/
|
||
|
||
// 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)
|
||
*/
|
||
|
||
},{"./content-repo":3,"./http-api":4,"./models/event":6,"./models/room":10,"./models/user":11,"./pushprocessor":12,"./store/stub":16,"./utils":18,"./webrtc/call":19,"events":21,"olm":undefined,"q":23}],3:[function(require,module,exports){
|
||
/**
|
||
* @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".
|
||
* @return {string} The complete URL to the content.
|
||
*/
|
||
getHttpUriForMxc: function(baseUrl, mxc, width, height, resizeMethod) {
|
||
if (typeof mxc !== "string" || !mxc) {
|
||
return mxc;
|
||
}
|
||
if (mxc.indexOf("mxc://") !== 0) {
|
||
return mxc;
|
||
}
|
||
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("#"),
|
||
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(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":18}],4:[function(require,module,exports){
|
||
(function (global){
|
||
"use strict";
|
||
/**
|
||
* This is an internal module. See {@link MatrixHttpApi} for the public class.
|
||
* @module http-api
|
||
*/
|
||
var q = require("q");
|
||
var utils = require("./utils");
|
||
|
||
/*
|
||
TODO:
|
||
- CS: complete register function (doing stages)
|
||
- Identity server: linkEmail, authEmail, bindEmail, lookup3pid
|
||
*/
|
||
|
||
/**
|
||
* A constant representing the URI path for version 1 of the Client-Server HTTP API.
|
||
*/
|
||
module.exports.PREFIX_V1 = "/_matrix/client/api/v1";
|
||
|
||
/**
|
||
* A constant representing the URI path for version 2 alpha of the Client-Server
|
||
* HTTP API.
|
||
*/
|
||
module.exports.PREFIX_V2_ALPHA = "/_matrix/client/v2_alpha";
|
||
|
||
/**
|
||
* URI path for the identity API
|
||
*/
|
||
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
|
||
|
||
/**
|
||
* Construct a MatrixHttpApi.
|
||
* @constructor
|
||
* @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/api/v1'. See PREFIX_V1 and PREFIX_V2_ALPHA for constants.
|
||
* @param {bool} opts.onlyData True to return only the 'data' component of the
|
||
* response (e.g. the parsed HTTP body). If false, requests will return status
|
||
* codes and headers in addition to data. Default: false.
|
||
* @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.
|
||
*/
|
||
module.exports.MatrixHttpApi = function MatrixHttpApi(opts) {
|
||
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
|
||
opts.onlyData = opts.onlyData || false;
|
||
this.opts = opts;
|
||
};
|
||
|
||
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() {
|
||
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 {File} file A File object (in a browser) or in Node,
|
||
an object with properties:
|
||
name: The file's name
|
||
stream: A read stream
|
||
* @param {Function} callback Optional. The callback to invoke on
|
||
* success/failure. See the promise return values for more information.
|
||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||
*/
|
||
uploadContent: function(file, callback) {
|
||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||
throw Error(
|
||
"Expected callback to be a function but got " + typeof callback
|
||
);
|
||
}
|
||
var defer = q.defer();
|
||
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
|
||
// 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)
|
||
if (global.XMLHttpRequest) {
|
||
var xhr = new global.XMLHttpRequest();
|
||
var cb = requestCallback(defer, callback, this.opts.onlyData);
|
||
|
||
var timeout_fn = function() {
|
||
xhr.abort();
|
||
cb(new Error('Timeout'));
|
||
};
|
||
|
||
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
|
||
|
||
xhr.onreadystatechange = function() {
|
||
switch (xhr.readyState) {
|
||
case global.XMLHttpRequest.DONE:
|
||
clearTimeout(xhr.timeout_timer);
|
||
|
||
if (!xhr.responseText) {
|
||
cb(new Error('No response body.'));
|
||
return;
|
||
}
|
||
|
||
var resp = JSON.parse(xhr.responseText);
|
||
if (resp.content_uri === undefined) {
|
||
cb(new Error('Bad response'));
|
||
return;
|
||
}
|
||
|
||
cb(undefined, xhr, resp.content_uri);
|
||
break;
|
||
}
|
||
};
|
||
xhr.upload.addEventListener("progress", function(ev) {
|
||
clearTimeout(xhr.timeout_timer);
|
||
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
|
||
defer.notify(ev);
|
||
});
|
||
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
|
||
url += "&filename=" + encodeURIComponent(file.name);
|
||
|
||
xhr.open("POST", url);
|
||
if (file.type) {
|
||
xhr.setRequestHeader("Content-Type", file.type);
|
||
} else {
|
||
// if the file doesn't have a mime type, use a default since
|
||
// the HS errors if we don't supply one.
|
||
xhr.setRequestHeader("Content-Type", 'application/octet-stream');
|
||
}
|
||
xhr.send(file);
|
||
} else {
|
||
var queryParams = {
|
||
filename: file.name,
|
||
access_token: this.opts.accessToken
|
||
};
|
||
file.stream.pipe(
|
||
this.opts.request({
|
||
uri: url,
|
||
qs: queryParams,
|
||
method: "POST"
|
||
}, requestCallback(defer, callback, this.opts.onlyData))
|
||
);
|
||
}
|
||
|
||
return defer.promise;
|
||
},
|
||
|
||
idServerRequest: function(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
|
||
);
|
||
}
|
||
|
||
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 = q.defer();
|
||
this.opts.request(
|
||
opts,
|
||
requestCallback(defer, callback, this.opts.onlyData)
|
||
);
|
||
return defer.promise;
|
||
},
|
||
|
||
/**
|
||
* 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).
|
||
* @param {Object} data The HTTP JSON body.
|
||
* @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(callback, method, path, queryParams, data) {
|
||
if (!queryParams) { queryParams = {}; }
|
||
queryParams.access_token = this.opts.accessToken;
|
||
return this.request(callback, method, path, queryParams, data);
|
||
},
|
||
|
||
/**
|
||
* 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).
|
||
* @param {Object} data The HTTP JSON body.
|
||
* @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(callback, method, path, queryParams, data) {
|
||
return this.requestWithPrefix(
|
||
callback, method, path, queryParams, data, this.opts.prefix
|
||
);
|
||
},
|
||
|
||
/**
|
||
* 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".
|
||
* @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.
|
||
*/
|
||
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
|
||
prefix) {
|
||
var fullUri = this.opts.baseUrl + prefix + path;
|
||
if (!queryParams) {
|
||
queryParams = {};
|
||
}
|
||
queryParams.access_token = this.opts.accessToken;
|
||
return this._request(callback, method, fullUri, queryParams, data);
|
||
},
|
||
|
||
/**
|
||
* 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".
|
||
* @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.
|
||
*/
|
||
requestWithPrefix: function(callback, method, path, queryParams, data, prefix) {
|
||
var fullUri = this.opts.baseUrl + prefix + path;
|
||
if (!queryParams) {
|
||
queryParams = {};
|
||
}
|
||
return this._request(callback, method, fullUri, queryParams, data);
|
||
},
|
||
|
||
_request: function(callback, method, uri, queryParams, data) {
|
||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||
throw Error(
|
||
"Expected callback to be a function but got " + typeof callback
|
||
);
|
||
}
|
||
if (!queryParams) {
|
||
queryParams = {};
|
||
}
|
||
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 defer = q.defer();
|
||
try {
|
||
this.opts.request(
|
||
{
|
||
uri: uri,
|
||
method: method,
|
||
withCredentials: false,
|
||
qs: queryParams,
|
||
body: data,
|
||
json: true,
|
||
_matrix_opts: this.opts
|
||
},
|
||
requestCallback(defer, callback, this.opts.onlyData)
|
||
);
|
||
}
|
||
catch (ex) {
|
||
defer.reject(ex);
|
||
if (callback) {
|
||
callback(ex);
|
||
}
|
||
}
|
||
return defer.promise;
|
||
}
|
||
};
|
||
|
||
/*
|
||
* 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).
|
||
*
|
||
* If onlyData is true, the defer/callback is invoked with the body of the
|
||
* response, otherwise the result code.
|
||
*/
|
||
var requestCallback = function(defer, userDefinedCallback, onlyData) {
|
||
userDefinedCallback = userDefinedCallback || function() {};
|
||
|
||
return function(err, response, body) {
|
||
if (!err && response.statusCode >= 400) {
|
||
err = new module.exports.MatrixError(body);
|
||
err.httpStatus = response.statusCode;
|
||
}
|
||
|
||
if (err) {
|
||
defer.reject(err);
|
||
userDefinedCallback(err);
|
||
}
|
||
else {
|
||
var res = {
|
||
code: response.statusCode,
|
||
headers: response.headers,
|
||
data: body
|
||
};
|
||
defer.resolve(onlyData ? body : res);
|
||
userDefinedCallback(null, onlyData ? body : res);
|
||
}
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 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) {
|
||
this.errcode = errorJson.errcode;
|
||
this.name = errorJson.errcode || "Unknown error code";
|
||
this.message = errorJson.error || "Unknown message";
|
||
this.data = errorJson;
|
||
};
|
||
module.exports.MatrixError.prototype = Object.create(Error.prototype);
|
||
/** */
|
||
module.exports.MatrixError.prototype.constructor = module.exports.MatrixError;
|
||
|
||
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
||
},{"./utils":18,"q":23}],5:[function(require,module,exports){
|
||
"use strict";
|
||
|
||
/** 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/webstorage~WebStorageStore|WebStorageStore} class.
|
||
* <strong>Work in progress; unstable.</strong> */
|
||
module.exports.WebStorageStore = require("./store/webstorage");
|
||
/** 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|Room} class. */
|
||
module.exports.Room = require("./models/room");
|
||
/** The {@link module:models/room-member~RoomMember|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");
|
||
|
||
/**
|
||
* 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;
|
||
|
||
// expose the underlying request object so different environments can use
|
||
// different request libs (e.g. request or browser-request)
|
||
var request;
|
||
/**
|
||
* 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;
|
||
};
|
||
|
||
/**
|
||
* 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.
|
||
* @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();
|
||
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
|
||
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.
|
||
*/
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
|
||
},{"./client":2,"./content-repo":3,"./http-api":4,"./models/event":6,"./models/room":10,"./models/room-member":7,"./models/room-state":8,"./models/user":11,"./scheduler":13,"./store/memory":14,"./store/session/webstorage":15,"./store/webstorage":17,"./webrtc/call":19}],6:[function(require,module,exports){
|
||
"use strict";
|
||
|
||
/**
|
||
* This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for
|
||
* the public classes.
|
||
* @module models/event
|
||
*/
|
||
|
||
|
||
/**
|
||
* 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 event is in the process of being sent. */
|
||
SENDING: "sending",
|
||
/** The event is in a queue waiting to be sent. */
|
||
QUEUED: "queued"
|
||
};
|
||
|
||
/**
|
||
* Construct a Matrix Event object
|
||
* @constructor
|
||
* @param {Object} event The raw event to be wrapped in this DAO
|
||
* @param {boolean} encrypted Was the event encrypted
|
||
* @prop {Object} event The raw 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.
|
||
* @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 {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, encrypted) {
|
||
this.event = event || {};
|
||
this.sender = null;
|
||
this.target = null;
|
||
this.status = null;
|
||
this.forwardLooking = true;
|
||
this.encrypted = Boolean(encrypted);
|
||
};
|
||
module.exports.MatrixEvent.prototype = {
|
||
|
||
/**
|
||
* Get the event_id for this event.
|
||
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
|
||
* </code>
|
||
*/
|
||
getId: function() {
|
||
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() {
|
||
return this.event.user_id;
|
||
},
|
||
|
||
/**
|
||
* Get the type of event.
|
||
* @return {string} The event type, e.g. <code>m.room.message</code>
|
||
*/
|
||
getType: function() {
|
||
return this.event.type;
|
||
},
|
||
|
||
/**
|
||
* Get the type of the event that will be sent to the homeserver.
|
||
* @return {string} The event type.
|
||
*/
|
||
getWireType: function() {
|
||
return this.encryptedType || 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() {
|
||
return this.event.room_id;
|
||
},
|
||
|
||
/**
|
||
* Get the timestamp of this event.
|
||
* @return {Number} The event timestamp, e.g. <code>1433502692297</code>
|
||
*/
|
||
getTs: function() {
|
||
return this.event.origin_server_ts;
|
||
},
|
||
|
||
/**
|
||
* Get the event content JSON.
|
||
* @return {Object} The event content JSON, or an empty object.
|
||
*/
|
||
getContent: function() {
|
||
return this.event.content || {};
|
||
},
|
||
|
||
/**
|
||
* Get the event content JSON that will be sent to the homeserver.
|
||
* @return {Object} The event content JSON, or an empty object.
|
||
*/
|
||
getWireContent: function() {
|
||
return this.encryptedContent || 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() {
|
||
return 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.
|
||
* <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() {
|
||
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() {
|
||
return this.event.age;
|
||
},
|
||
|
||
/**
|
||
* 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() {
|
||
return this.event.state_key;
|
||
},
|
||
|
||
/**
|
||
* Check if this event is a state event.
|
||
* @return {boolean} True if this is a state event.
|
||
*/
|
||
isState: function() {
|
||
return this.event.state_key !== undefined;
|
||
},
|
||
|
||
/**
|
||
* Check if the event is encrypted.
|
||
* @return {boolean} True if this event is encrypted.
|
||
*/
|
||
isEncrypted: function() {
|
||
return this.encrypted;
|
||
}
|
||
};
|
||
|
||
},{}],7:[function(require,module,exports){
|
||
"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
|
||
* @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.
|
||
* @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.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);
|
||
if (oldMembership !== this.membership) {
|
||
this._updateModifiedTime();
|
||
this.emit("RoomMember.membership", event, this);
|
||
}
|
||
if (oldName !== this.name) {
|
||
this._updateModifiedTime();
|
||
this.emit("RoomMember.name", event, this);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 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 maxLevel = powerLevelEvent.getContent().users_default || 0;
|
||
utils.forEach(utils.values(powerLevelEvent.getContent().users), function(lvl) {
|
||
maxLevel = Math.max(maxLevel, lvl);
|
||
});
|
||
var oldPowerLevel = this.powerLevel;
|
||
var oldPowerLevelNorm = this.powerLevelNorm;
|
||
this.powerLevel = (
|
||
powerLevelEvent.getContent().users[this.userId] ||
|
||
powerLevelEvent.getContent().users_default ||
|
||
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.
|
||
* @return {?string} the avatar URL or null.
|
||
*/
|
||
RoomMember.prototype.getAvatarUrl =
|
||
function(baseUrl, width, height, resizeMethod, allowDefault) {
|
||
if (allowDefault === undefined) { allowDefault = true; }
|
||
if (!this.events.member && !allowDefault) {
|
||
return null;
|
||
}
|
||
var rawUrl = this.events.member ? this.events.member.getContent().avatar_url : null;
|
||
if (rawUrl) {
|
||
return ContentRepo.getHttpUriForMxc(
|
||
baseUrl, rawUrl, width, height, resizeMethod
|
||
);
|
||
}
|
||
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;
|
||
|
||
/*
|
||
// FIXME: this would be great but still needs to use the
|
||
// full userId to disambiguate if needed...
|
||
|
||
if (!displayName) {
|
||
var matches = selfUserId.match(/^@(.*?):/);
|
||
if (matches) {
|
||
return matches[1];
|
||
}
|
||
else {
|
||
return selfUserId;
|
||
}
|
||
}
|
||
*/
|
||
|
||
if (!displayName) {
|
||
return selfUserId;
|
||
}
|
||
|
||
if (!roomState) {
|
||
return displayName;
|
||
}
|
||
|
||
var userIds = roomState.getUserIdsWithDisplayName(displayName);
|
||
var otherUsers = userIds.filter(function(u) {
|
||
return u !== selfUserId;
|
||
});
|
||
if (otherUsers.length > 0) {
|
||
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.
|
||
* @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.
|
||
* @example
|
||
* matrixClient.on("RoomMember.membership", function(event, member){
|
||
* 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":3,"../utils":18,"events":21}],8:[function(require,module,exports){
|
||
"use strict";
|
||
/**
|
||
* @module models/room-state
|
||
*/
|
||
var EventEmitter = require("events").EventEmitter;
|
||
|
||
var utils = require("../utils");
|
||
var RoomMember = require("./room-member");
|
||
|
||
/**
|
||
* Construct room state.
|
||
* @constructor
|
||
* @param {string} roomId Required. The ID of the room which has this state.
|
||
* @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 = {};
|
||
}
|
||
utils.inherits(RoomState, EventEmitter);
|
||
|
||
/**
|
||
* 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) {
|
||
return this._sentinels[userId] || null;
|
||
};
|
||
|
||
/**
|
||
* 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
|
||
);
|
||
}
|
||
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();
|
||
var member = self.members[userId];
|
||
if (!member) {
|
||
member = new RoomMember(event.getRoomId(), userId);
|
||
self.emit("RoomState.newMember", event, self, member);
|
||
}
|
||
// Add a new sentinel for this change. We apply the same
|
||
// operations to both sentinel and member rather than deep copying
|
||
// so we don't make assumptions about the properties of RoomMember
|
||
// (e.g. and manage to break it because deep copying doesn't do
|
||
// everything).
|
||
var sentinel = new RoomMember(event.getRoomId(), userId);
|
||
utils.forEach([member, sentinel], function(roomMember) {
|
||
roomMember.setMembershipEvent(event, self);
|
||
// this member may have a power level already, so set it.
|
||
var pwrLvlEvent = self.getStateEvents("m.room.power_levels", "");
|
||
if (pwrLvlEvent) {
|
||
roomMember.setPowerLevelEvent(pwrLvlEvent);
|
||
}
|
||
});
|
||
|
||
self._sentinels[userId] = sentinel;
|
||
self.members[userId] = member;
|
||
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);
|
||
});
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 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);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 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] || [];
|
||
};
|
||
|
||
/**
|
||
* The RoomState class.
|
||
*/
|
||
module.exports = RoomState;
|
||
|
||
|
||
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":18,"./room-member":7,"events":21}],9:[function(require,module,exports){
|
||
"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;
|
||
|
||
},{}],10:[function(require,module,exports){
|
||
"use strict";
|
||
/**
|
||
* @module models/room
|
||
*/
|
||
var EventEmitter = require("events").EventEmitter;
|
||
|
||
var RoomState = require("./room-state");
|
||
var RoomSummary = require("./room-summary");
|
||
var MatrixEvent = require("./event").MatrixEvent;
|
||
var utils = require("../utils");
|
||
var ContentRepo = require("../content-repo");
|
||
|
||
/**
|
||
* Construct a new Room.
|
||
* @constructor
|
||
* @param {string} roomId Required. The ID of this room.
|
||
* @param {*} 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.
|
||
* @prop {string} roomId The ID of this room.
|
||
* @prop {string} name The human-readable display name for this room.
|
||
* @prop {Array<MatrixEvent>} timeline The ordered list of message events for
|
||
* this room.
|
||
* @prop {RoomState} oldState The state of the room at the time of the oldest
|
||
* event in the timeline.
|
||
* @prop {RoomState} currentState The state of the room at the time of the
|
||
* newest event in the timeline.
|
||
* @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, storageToken) {
|
||
this.roomId = roomId;
|
||
this.name = roomId;
|
||
this.timeline = [];
|
||
this.oldState = new RoomState(roomId);
|
||
this.currentState = new RoomState(roomId);
|
||
this.summary = null;
|
||
this.storageToken = storageToken;
|
||
this._redactions = [];
|
||
// 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>
|
||
// }]
|
||
};
|
||
}
|
||
utils.inherits(Room, EventEmitter);
|
||
|
||
/**
|
||
* 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 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;
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 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) {
|
||
return utils.filter(this.currentState.getMembers(), function(m) {
|
||
return m.membership === membership && m.userId === userId;
|
||
}).length > 0;
|
||
};
|
||
|
||
/**
|
||
* Add some events to this room's timeline. Will fire "Room.timeline" for
|
||
* each event added.
|
||
* @param {MatrixEvent[]} events A list of events to add.
|
||
* @param {boolean} toStartOfTimeline True to add these events to the start
|
||
* (oldest) instead of the end (newest) of the timeline. If true, the oldest
|
||
* event will be the <b>last</b> element of 'events'.
|
||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||
*/
|
||
Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline) {
|
||
var stateContext = toStartOfTimeline ? this.oldState : this.currentState;
|
||
|
||
function checkForRedaction(redactEvent) {
|
||
return function(e) {
|
||
return e.getId() === redactEvent.event.redacts;
|
||
};
|
||
}
|
||
|
||
for (var i = 0; i < events.length; i++) {
|
||
if (toStartOfTimeline && this._redactions.indexOf(events[i].getId()) >= 0) {
|
||
continue; // do not add the redacted event.
|
||
}
|
||
|
||
setEventMetadata(events[i], stateContext, toStartOfTimeline);
|
||
// modify state
|
||
if (events[i].isState()) {
|
||
stateContext.setStateEvents([events[i]]);
|
||
// 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.
|
||
if (!events[i].sender) {
|
||
setEventMetadata(events[i], stateContext, toStartOfTimeline);
|
||
}
|
||
}
|
||
if (events[i].getType() === "m.room.redaction") {
|
||
// try to remove the element
|
||
var removed = utils.removeElement(
|
||
this.timeline, checkForRedaction(events[i])
|
||
);
|
||
if (!removed && toStartOfTimeline) {
|
||
// redactions will trickle in BEFORE the event redacted so make
|
||
// a note of the redacted event; we'll check it later.
|
||
this._redactions.push(events[i].event.redacts);
|
||
}
|
||
// 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.
|
||
}
|
||
|
||
// TODO: pass through filter to see if this should be added to the timeline.
|
||
if (toStartOfTimeline) {
|
||
this.timeline.unshift(events[i]);
|
||
}
|
||
else {
|
||
this.timeline.push(events[i]);
|
||
}
|
||
this.emit("Room.timeline", events[i], this, Boolean(toStartOfTimeline));
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 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 not specified, no duplicate suppression is
|
||
* performed (this improves performance). 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 '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.addEvents = function(events, duplicateStrategy) {
|
||
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
||
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
||
}
|
||
for (var 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]);
|
||
}
|
||
else {
|
||
if (duplicateStrategy) {
|
||
// is there a duplicate?
|
||
var shouldIgnore = false;
|
||
for (var j = 0; j < this.timeline.length; j++) {
|
||
if (this.timeline[j].getId() === events[i].getId()) {
|
||
if (duplicateStrategy === "replace") {
|
||
// still need to set the right metadata on this event
|
||
setEventMetadata(
|
||
events[i],
|
||
this.currentState,
|
||
false
|
||
);
|
||
if (!this.timeline[j].encryptedType) {
|
||
this.timeline[j] = events[i];
|
||
}
|
||
// skip the insert so we don't add this event twice.
|
||
// Don't break in case we replace multiple events.
|
||
shouldIgnore = true;
|
||
}
|
||
else if (duplicateStrategy === "ignore") {
|
||
shouldIgnore = true;
|
||
break; // stop searching, we're skipping the insert
|
||
}
|
||
}
|
||
}
|
||
if (shouldIgnore) {
|
||
continue; // skip the insertion of this event.
|
||
}
|
||
}
|
||
// TODO: We should have a filter to say "only add state event
|
||
// types X Y Z to the timeline".
|
||
this.addEventsToTimeline([events[i]]);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 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 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.
|
||
*/
|
||
Room.prototype.addReceipt = function(event) {
|
||
// event content looks like:
|
||
// content: {
|
||
// $event_id: {
|
||
// $receipt_type: {
|
||
// $user_id: {
|
||
// ts: $timestamp
|
||
// }
|
||
// }
|
||
// }
|
||
// }
|
||
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 (!self._receipts[receiptType]) {
|
||
self._receipts[receiptType] = {};
|
||
}
|
||
if (!self._receipts[receiptType][userId]) {
|
||
self._receipts[receiptType][userId] = {};
|
||
}
|
||
self._receipts[receiptType][userId] = {
|
||
eventId: eventId,
|
||
data: receipt
|
||
};
|
||
});
|
||
});
|
||
});
|
||
|
||
// pre-cache receipts by event
|
||
self._receiptCacheByEventId = {};
|
||
utils.keys(self._receipts).forEach(function(receiptType) {
|
||
utils.keys(self._receipts[receiptType]).forEach(function(userId) {
|
||
var receipt = self._receipts[receiptType][userId];
|
||
if (!self._receiptCacheByEventId[receipt.eventId]) {
|
||
self._receiptCacheByEventId[receipt.eventId] = [];
|
||
}
|
||
self._receiptCacheByEventId[receipt.eventId].push({
|
||
userId: userId,
|
||
type: receiptType,
|
||
data: receipt.data
|
||
});
|
||
});
|
||
});
|
||
};
|
||
|
||
function setEventMetadata(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;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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.
|
||
* @return {string} The calculated room name.
|
||
*/
|
||
function calculateRoomName(room, userId) {
|
||
// check for an alias, if any. for now, assume first alias is the
|
||
// official one.
|
||
var alias;
|
||
var canonicalAlias = room.currentState.getStateEvents("m.room.canonical_alias", "");
|
||
if (canonicalAlias) {
|
||
alias = canonicalAlias.getContent().alias;
|
||
}
|
||
|
||
if (!alias) {
|
||
var mRoomAliases = room.currentState.getStateEvents("m.room.aliases")[0];
|
||
if (mRoomAliases && utils.isArray(mRoomAliases.getContent().aliases)) {
|
||
alias = mRoomAliases.getContent().aliases[0];
|
||
}
|
||
}
|
||
|
||
var mRoomName = room.currentState.getStateEvents('m.room.name', '');
|
||
if (mRoomName) {
|
||
return mRoomName.getContent().name + (false && alias ? " (" + alias + ")" : "");
|
||
}
|
||
else if (alias) {
|
||
return alias;
|
||
}
|
||
else {
|
||
// get members that are NOT ourselves and are actually in the room.
|
||
var members = utils.filter(room.currentState.getMembers(), function(m) {
|
||
return (m.userId !== userId && m.membership !== "leave");
|
||
});
|
||
// TODO: Localisation
|
||
if (members.length === 0) {
|
||
var memberList = utils.filter(room.currentState.getMembers(), function(m) {
|
||
return (m.membership !== "leave");
|
||
});
|
||
if (memberList.length === 1) {
|
||
// we exist, but no one else... self-chat or invite.
|
||
if (memberList[0].membership === "invite") {
|
||
if (memberList[0].events.member) {
|
||
// extract who invited us to the room
|
||
return "Invite from " + memberList[0].events.member.getSender();
|
||
}
|
||
else {
|
||
return "Room Invite";
|
||
}
|
||
}
|
||
else {
|
||
return userId;
|
||
}
|
||
}
|
||
else {
|
||
// there really isn't anyone in this room...
|
||
return "?";
|
||
}
|
||
}
|
||
else if (members.length === 1) {
|
||
return members[0].name;
|
||
}
|
||
else if (members.length === 2) {
|
||
return (
|
||
members[0].name + " and " + members[1].name
|
||
);
|
||
}
|
||
else {
|
||
return (
|
||
members[0].name + " and " + (members.length - 1) + " others"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* The Room class.
|
||
*/
|
||
module.exports = Room;
|
||
|
||
/**
|
||
* 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 whose Room.timeline was updated.
|
||
* @param {boolean} toStartOfTimeline True if this event was added to the start
|
||
* (beginning; oldest) of the timeline e.g. due to pagination.
|
||
* @example
|
||
* matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline){
|
||
* if (toStartOfTimeline) {
|
||
* var messageToAppend = room.timeline[room.timeline.length - 1];
|
||
* }
|
||
* });
|
||
*/
|
||
|
||
/**
|
||
* 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;
|
||
* });
|
||
*/
|
||
|
||
},{"../content-repo":3,"../utils":18,"./event":6,"./room-state":8,"./room-summary":9,"events":21}],11:[function(require,module,exports){
|
||
"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 {Number} lastActiveAgo The last time the user performed some action in ms.
|
||
* @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.displayName = userId;
|
||
this.avatarUrl = null;
|
||
this.lastActiveAgo = 0;
|
||
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 !== this.avatarUrl) {
|
||
eventsToFire.push("User.avatarUrl");
|
||
}
|
||
if (event.getContent().displayname !== this.displayName) {
|
||
eventsToFire.push("User.displayName");
|
||
}
|
||
|
||
this.presence = event.getContent().presence;
|
||
this.displayName = event.getContent().displayname;
|
||
this.avatarUrl = event.getContent().avatar_url;
|
||
this.lastActiveAgo = event.getContent().last_active_ago;
|
||
|
||
if (eventsToFire.length > 0) {
|
||
this._updateModifiedTime();
|
||
}
|
||
|
||
for (var i = 0; i < eventsToFire.length; i++) {
|
||
this.emit(eventsToFire[i], event, this);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 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;
|
||
};
|
||
|
||
/**
|
||
* The User class.
|
||
*/
|
||
module.exports = User;
|
||
|
||
/**
|
||
* 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 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":18,"events":21}],12:[function(require,module,exports){
|
||
/**
|
||
* @module pushprocessor
|
||
*/
|
||
|
||
/**
|
||
* Construct a Push Processor.
|
||
* @constructor
|
||
* @param {Object} client The Matrix client object to use
|
||
*/
|
||
function PushProcessor(client) {
|
||
var escapeRegExp = function(string) {
|
||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
};
|
||
|
||
var matchingRuleFromKindSet = function(ev, kindset, device) {
|
||
var rulekinds_in_order = ['override', 'content', 'room', 'sender', 'underride'];
|
||
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 (ruleMatchesEvent(rawrule, ev)) {
|
||
rule.kind = kind;
|
||
return rule;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
var templateRuleToRaw = function(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',
|
||
'pattern': tprule.rule_id
|
||
});
|
||
break;
|
||
case 'sender':
|
||
if (!tprule.rule_id) { return null; }
|
||
rawrule.conditions.push({
|
||
'kind': 'event_match',
|
||
'key': 'user_id',
|
||
'pattern': 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 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;
|
||
};
|
||
|
||
var eventFulfillsCondition = function(cond, ev) {
|
||
var condition_functions = {
|
||
"event_match": eventFulfillsEventMatchCondition,
|
||
"device": eventFulfillsDeviceCondition,
|
||
"contains_display_name": eventFulfillsDisplayNameCondition,
|
||
"room_member_count": eventFulfillsRoomMemberCountCondition
|
||
};
|
||
if (condition_functions[cond.kind]) {
|
||
return condition_functions[cond.kind](cond, ev);
|
||
}
|
||
return true;
|
||
};
|
||
|
||
var eventFulfillsRoomMemberCountCondition = function(cond, ev) {
|
||
if (!cond.is) { return false; }
|
||
|
||
var room = client.getRoom(ev.room_id);
|
||
if (!room || !room.currentState || !room.currentState.members) { return false; }
|
||
|
||
var memberCount = Object.keys(room.currentState.members).length;
|
||
|
||
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(cond, ev) {
|
||
if (!ev.content || ! ev.content.body || typeof ev.content.body != 'string') {
|
||
return false;
|
||
}
|
||
|
||
var room = client.getRoom(ev.room_id);
|
||
if (!room || !room.currentState || !room.currentState.members ||
|
||
!room.currentState.getMember(client.credentials.userId)) { return false; }
|
||
|
||
var displayName = room.currentState.getMember(client.credentials.userId).name;
|
||
|
||
var pat = new RegExp("\\b" + escapeRegExp(displayName) + "\\b", 'i');
|
||
return ev.content.body.search(pat) > -1;
|
||
};
|
||
|
||
var eventFulfillsDeviceCondition = function(cond, ev) {
|
||
return false; // XXX: Allow a profile tag to be set for the web client instance
|
||
};
|
||
|
||
var eventFulfillsEventMatchCondition = function(cond, ev) {
|
||
var val = valueForDottedKey(cond.key, ev);
|
||
if (!val || typeof val != 'string') { return false; }
|
||
|
||
var pat;
|
||
if (cond.key == 'content.body') {
|
||
pat = '\\b' + globToRegexp(cond.pattern) + '\\b';
|
||
} else {
|
||
pat = '^' + globToRegexp(cond.pattern) + '$';
|
||
}
|
||
var regex = new RegExp(pat, 'i');
|
||
return !!val.match(regex);
|
||
};
|
||
|
||
var globToRegexp = function(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(/\\\*/, '.*');
|
||
pat = pat.replace(/\?/, '.');
|
||
pat = pat.replace(/\\\[(!|)(.*)\\]/, function(match, p1, p2, offset, string) {
|
||
var first = p1 && '^' || '';
|
||
var second = p2.replace(/\\\-/, '-');
|
||
return '[' + first + second + ']';
|
||
});
|
||
return pat;
|
||
};
|
||
|
||
var valueForDottedKey = function(key, ev) {
|
||
var parts = key.split('.');
|
||
var val = ev;
|
||
while (parts.length > 0) {
|
||
var thispart = parts.shift();
|
||
if (!val[thispart]) { return null; }
|
||
val = val[thispart];
|
||
}
|
||
return val;
|
||
};
|
||
|
||
var matchingRuleForEventWithRulesets = function(ev, rulesets) {
|
||
if (!rulesets) { return null; }
|
||
if (ev.user_id == client.credentials.userId) { return null; }
|
||
|
||
var allDevNames = Object.keys(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 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 === 'object') {
|
||
if (action.value === undefined) { action.value = true; }
|
||
actionobj.tweaks[action.set_tweak] = action.value;
|
||
}
|
||
}
|
||
return actionobj;
|
||
};
|
||
|
||
var pushActionsForEventAndRulesets = function(ev, rulesets) {
|
||
var rule = matchingRuleForEventWithRulesets(ev, rulesets);
|
||
if (!rule) { return {}; }
|
||
|
||
var actionObj = 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.actionsForEvent = function(ev) {
|
||
return pushActionsForEventAndRulesets(ev, client.pushRules);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @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;
|
||
|
||
},{}],13:[function(require,module,exports){
|
||
"use strict";
|
||
/**
|
||
* This is an internal module which manages queuing, scheduling and retrying
|
||
* of requests.
|
||
* @module scheduler
|
||
*/
|
||
var utils = require("./utils");
|
||
var q = require("q");
|
||
|
||
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()) {
|
||
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 = q.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;
|
||
}
|
||
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) {
|
||
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":18,"q":23}],14:[function(require,module,exports){
|
||
"use strict";
|
||
/**
|
||
* This is an internal module. See {@link MatrixInMemoryStore} for the public class.
|
||
* @module store/memory
|
||
*/
|
||
var utils = require("../utils");
|
||
|
||
/**
|
||
* Construct a new in-memory data store for the Matrix Client.
|
||
* @constructor
|
||
*/
|
||
module.exports.MatrixInMemoryStore = function MatrixInMemoryStore() {
|
||
this.rooms = {
|
||
// roomId: Room
|
||
};
|
||
this.users = {
|
||
// userId: User
|
||
};
|
||
this.syncToken = null;
|
||
};
|
||
|
||
module.exports.MatrixInMemoryStore.prototype = {
|
||
|
||
/**
|
||
* Retrieve the token to stream from.
|
||
* @return {string} The token or null.
|
||
*/
|
||
getSyncToken: function() {
|
||
return this.syncToken;
|
||
},
|
||
|
||
/**
|
||
* Set the token to stream from.
|
||
* @param {string} token The token to stream from.
|
||
*/
|
||
setSyncToken: function(token) {
|
||
this.syncToken = token;
|
||
},
|
||
|
||
/**
|
||
* Store the given room.
|
||
* @param {Room} room The room to be stored. All properties must be stored.
|
||
*/
|
||
storeRoom: function(room) {
|
||
this.rooms[room.roomId] = room;
|
||
},
|
||
|
||
/**
|
||
* Retrieve a room by its' room ID.
|
||
* @param {string} roomId The room ID.
|
||
* @return {Room} The room or null.
|
||
*/
|
||
getRoom: function(roomId) {
|
||
return this.rooms[roomId] || null;
|
||
},
|
||
|
||
/**
|
||
* Retrieve all known rooms.
|
||
* @return {Room[]} A list of rooms, which may be empty.
|
||
*/
|
||
getRooms: function() {
|
||
return utils.values(this.rooms);
|
||
},
|
||
|
||
/**
|
||
* Retrieve a summary of all the rooms.
|
||
* @return {RoomSummary[]} A summary of each room.
|
||
*/
|
||
getRoomSummaries: function() {
|
||
return utils.map(utils.values(this.rooms), function(room) {
|
||
return room.summary;
|
||
});
|
||
},
|
||
|
||
/**
|
||
* Store a User.
|
||
* @param {User} user The user to store.
|
||
*/
|
||
storeUser: function(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(userId) {
|
||
return this.users[userId] || null;
|
||
},
|
||
|
||
/**
|
||
* 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(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(room, events, token, toStart) {
|
||
// no-op because they've already been added to the room instance.
|
||
}
|
||
|
||
// TODO
|
||
//setMaxHistoryPerRoom: function(maxHistory) {},
|
||
|
||
// TODO
|
||
//reapOldMessages: function() {},
|
||
};
|
||
|
||
},{"../utils":18}],15:[function(require,module,exports){
|
||
"use strict";
|
||
|
||
/**
|
||
* @module store/session/webstorage
|
||
*/
|
||
|
||
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)) {
|
||
throw new Error(
|
||
"Supplied webStore does not meet the WebStorage API interface"
|
||
);
|
||
}
|
||
}
|
||
|
||
WebStorageSessionStore.prototype = {
|
||
|
||
/**
|
||
* Store the end to end account for the logged-in user.
|
||
* @param {string} account Base64 encoded account.
|
||
*/
|
||
storeEndToEndAccount: function(account) {
|
||
this.store.setItem(KEY_END_TO_END_ACCOUNT, account);
|
||
},
|
||
|
||
/**
|
||
* Load the end to end account for the logged-in user.
|
||
* @return {?string} Base64 encoded account.
|
||
*/
|
||
getEndToEndAccount: function() {
|
||
return this.store.getItem(KEY_END_TO_END_ACCOUNT);
|
||
},
|
||
|
||
/**
|
||
* Stores the known devices for a user.
|
||
* @param {string} userId The user's ID.
|
||
* @param {object} devices A map from device ID to keys for the device.
|
||
*/
|
||
storeEndToEndDevicesForUser: function(userId, devices) {
|
||
setJsonItem(this.store, keyEndToEndDevicesForUser(userId), devices);
|
||
},
|
||
|
||
/**
|
||
* Retrieves the known devices for a user.
|
||
* @param {string} userId The user's ID.
|
||
* @return {object} A map from device ID to keys for the device.
|
||
*/
|
||
getEndToEndDevicesForUser: function(userId) {
|
||
return getJsonItem(this.store, keyEndToEndDevicesForUser(userId));
|
||
},
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
storeEndToEndSession: function(deviceKey, sessionId, session) {
|
||
var sessions = this.getEndToEndSessions(deviceKey) || {};
|
||
sessions[sessionId] = session;
|
||
setJsonItem(
|
||
this.store, keyEndToEndSessions(deviceKey), sessions
|
||
);
|
||
},
|
||
|
||
/**
|
||
* 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(deviceKey) {
|
||
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
|
||
},
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
storeEndToEndRoom: function(roomId, roomInfo) {
|
||
setJsonItem(this.store, keyEndToEndRoom(roomId), roomInfo);
|
||
},
|
||
|
||
/**
|
||
* Get the end-to-end state for a room
|
||
* @param {string} roomId The room's ID.
|
||
* @return {object} The end-to-end info for the room.
|
||
*/
|
||
getEndToEndRoom: function(roomId) {
|
||
return getJsonItem(this.store, keyEndToEndRoom(roomId));
|
||
}
|
||
};
|
||
|
||
var KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||
|
||
function keyEndToEndDevicesForUser(userId) {
|
||
return E2E_PREFIX + "devices/" + userId;
|
||
}
|
||
|
||
function keyEndToEndSessions(deviceKey) {
|
||
return E2E_PREFIX + "sessions/" + deviceKey;
|
||
}
|
||
|
||
function keyEndToEndRoom(roomId) {
|
||
return E2E_PREFIX + "rooms/" + roomId;
|
||
}
|
||
|
||
function getJsonItem(store, key) {
|
||
try {
|
||
return JSON.parse(store.getItem(key));
|
||
}
|
||
catch (e) {
|
||
debuglog("Failed to get key %s: %s", key, e);
|
||
debuglog(e.stack);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function setJsonItem(store, key, val) {
|
||
store.setItem(key, JSON.stringify(val));
|
||
}
|
||
|
||
function debuglog() {
|
||
if (DEBUG) {
|
||
console.log.apply(console, arguments);
|
||
}
|
||
}
|
||
|
||
/** */
|
||
module.exports = WebStorageSessionStore;
|
||
|
||
},{"../../utils":18}],16:[function(require,module,exports){
|
||
"use strict";
|
||
/**
|
||
* 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() {
|
||
return this.fromToken;
|
||
},
|
||
|
||
/**
|
||
* Set the sync token.
|
||
* @param {string} token
|
||
*/
|
||
setSyncToken: function(token) {
|
||
this.fromToken = token;
|
||
},
|
||
|
||
/**
|
||
* No-op.
|
||
* @param {Room} room
|
||
*/
|
||
storeRoom: function(room) {
|
||
},
|
||
|
||
/**
|
||
* No-op.
|
||
* @param {string} roomId
|
||
* @return {null}
|
||
*/
|
||
getRoom: function(roomId) {
|
||
return null;
|
||
},
|
||
|
||
/**
|
||
* No-op.
|
||
* @return {Array} An empty array.
|
||
*/
|
||
getRooms: function() {
|
||
return [];
|
||
},
|
||
|
||
/**
|
||
* No-op.
|
||
* @return {Array} An empty array.
|
||
*/
|
||
getRoomSummaries: function() {
|
||
return [];
|
||
},
|
||
|
||
/**
|
||
* No-op.
|
||
* @param {User} user
|
||
*/
|
||
storeUser: function(user) {
|
||
},
|
||
|
||
/**
|
||
* No-op.
|
||
* @param {string} userId
|
||
* @return {null}
|
||
*/
|
||
getUser: function(userId) {
|
||
return null;
|
||
},
|
||
|
||
/**
|
||
* No-op.
|
||
* @param {Room} room
|
||
* @param {integer} limit
|
||
* @return {Array}
|
||
*/
|
||
scrollback: function(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(room, events, token, toStart) {
|
||
}
|
||
|
||
// TODO
|
||
//setMaxHistoryPerRoom: function(maxHistory) {},
|
||
|
||
// TODO
|
||
//reapOldMessages: function() {},
|
||
};
|
||
|
||
/** Stub Store class. */
|
||
module.exports = StubStore;
|
||
|
||
},{}],17:[function(require,module,exports){
|
||
"use strict";
|
||
/**
|
||
* This is an internal module. Implementation details:
|
||
* <pre>
|
||
* Room data is stored as follows:
|
||
* room_$ROOMID_timeline_$INDEX : [ Event, Event, Event ]
|
||
* room_$ROOMID_state : {
|
||
* pagination_token: <oldState.paginationToken>,
|
||
* events: {
|
||
* <event_type>: { <state_key> : {JSON} }
|
||
* }
|
||
* }
|
||
* User data is stored as follows:
|
||
* user_$USERID : User
|
||
* Sync token:
|
||
* sync_token : $TOKEN
|
||
*
|
||
* Room Retrieval
|
||
* --------------
|
||
* Retrieving a room requires the $ROOMID which then pulls out the current state
|
||
* from room_$ROOMID_state. A defined starting batch of timeline events are then
|
||
* extracted from the highest numbered $INDEX for room_$ROOMID_timeline_$INDEX
|
||
* (more indices as required). The $INDEX may be negative. These are
|
||
* added to the timeline in the same way as /initialSync (old state will diverge).
|
||
* If there exists a room_$ROOMID_timeline_live key, then a timeline sync should
|
||
* be performed before retrieving.
|
||
*
|
||
* Retrieval of earlier messages
|
||
* -----------------------------
|
||
* The earliest event the Room instance knows about is E. Retrieving earlier
|
||
* messages requires a Room which has a storageToken defined.
|
||
* This token maps to the index I where the Room is at. Events are then retrieved from
|
||
* room_$ROOMID_timeline_{I} and elements before E are extracted. If the limit
|
||
* demands more events, I-1 is retrieved, up until I=min $INDEX where it gives
|
||
* less than the limit. Index may go negative if you have paginated in the past.
|
||
*
|
||
* Full Insertion
|
||
* --------------
|
||
* Storing a room requires the timeline and state keys for $ROOMID to
|
||
* be blown away and completely replaced, which is computationally expensive.
|
||
* Room.timeline is batched according to the given batch size B. These batches
|
||
* are then inserted into storage as room_$ROOMID_timeline_$INDEX. Finally,
|
||
* the current room state is persisted to room_$ROOMID_state.
|
||
*
|
||
* Incremental Insertion
|
||
* ---------------------
|
||
* As events arrive, the store can quickly persist these new events. This
|
||
* involves pushing the events to room_$ROOMID_timeline_live. If the
|
||
* current room state has been modified by the new event, then
|
||
* room_$ROOMID_state should be updated in addition to the timeline.
|
||
*
|
||
* Timeline sync
|
||
* -------------
|
||
* Retrieval of events from the timeline depends on the proper batching of
|
||
* events. This is computationally expensive to perform on every new event, so
|
||
* is deferred by inserting live events to room_$ROOMID_timeline_live. A
|
||
* timeline sync reconciles timeline_live and timeline_$INDEX. This involves
|
||
* retrieving _live and the highest numbered $INDEX batch. If the batch is < B,
|
||
* the earliest entries from _live are inserted into the $INDEX until the
|
||
* batch == B. Then, the remaining entries in _live are batched to $INDEX+1,
|
||
* $INDEX+2, and so on. The easiest way to visualise this is that the timeline
|
||
* goes from old to new, left to right:
|
||
* -2 -1 0 1
|
||
* <--OLD---------------------------------------NEW-->
|
||
* [a,b,c] [d,e,f] [g,h,i] [j,k,l]
|
||
*
|
||
* Purging
|
||
* -------
|
||
* Events from the timeline can be purged by removing the lowest
|
||
* timeline_$INDEX in the store.
|
||
*
|
||
* Example
|
||
* -------
|
||
* A room with room_id !foo:bar has 9 messages (M1->9 where 9=newest) with a
|
||
* batch size of 4. The very first time, there is no entry for !foo:bar until
|
||
* storeRoom() is called, which results in the keys: [Full Insert]
|
||
* room_!foo:bar_timeline_0 : [M1, M2, M3, M4]
|
||
* room_!foo:bar_timeline_1 : [M5, M6, M7, M8]
|
||
* room_!foo:bar_timeline_2 : [M9]
|
||
* room_!foo:bar_state: { ... }
|
||
*
|
||
* 5 new messages (N1-5, 5=newest) arrive and are then added: [Incremental Insert]
|
||
* room_!foo:bar_timeline_live: [N1]
|
||
* room_!foo:bar_timeline_live: [N1, N2]
|
||
* room_!foo:bar_timeline_live: [N1, N2, N3]
|
||
* room_!foo:bar_timeline_live: [N1, N2, N3, N4]
|
||
* room_!foo:bar_timeline_live: [N1, N2, N3, N4, N5]
|
||
*
|
||
* App is shutdown. Restarts. The timeline is synced [Timeline Sync]
|
||
* room_!foo:bar_timeline_2 : [M9, N1, N2, N3]
|
||
* room_!foo:bar_timeline_3 : [N4, N5]
|
||
* room_!foo:bar_timeline_live: []
|
||
*
|
||
* And the room is retrieved with 8 messages: [Room Retrieval]
|
||
* Room.timeline: [M7, M8, M9, N1, N2, N3, N4, N5]
|
||
* Room.storageToken: => early_index = 1 because that's where M7 is.
|
||
*
|
||
* 3 earlier messages are requested: [Earlier retrieval]
|
||
* Use storageToken to find batch index 1. Scan batch for earliest event ID.
|
||
* earliest event = M7
|
||
* events = room_!foo:bar_timeline_1 where event < M7 = [M5, M6]
|
||
* Too few events, use next index (0) and get 1 more:
|
||
* events = room_!foo:bar_timeline_0 = [M1, M2, M3, M4] => [M4]
|
||
* Return concatentation:
|
||
* [M4, M5, M6]
|
||
*
|
||
* Purge oldest events: [Purge]
|
||
* del room_!foo:bar_timeline_0
|
||
* </pre>
|
||
* @module store/webstorage
|
||
*/
|
||
var DEBUG = false; // set true to enable console logging.
|
||
var utils = require("../utils");
|
||
var Room = require("../models/room");
|
||
var User = require("../models/user");
|
||
var MatrixEvent = require("../models/event").MatrixEvent;
|
||
|
||
/**
|
||
* Construct a web storage store, capable of storing rooms and users.
|
||
* @constructor
|
||
* @param {WebStorage} webStore A web storage implementation, e.g.
|
||
* 'window.localStorage' or 'window.sessionStorage' or a custom implementation.
|
||
* @param {integer} batchSize The number of events to store per key/value (room
|
||
* scoped). Use -1 to store all events for a room under one key/value.
|
||
* @throws if the supplied 'store' does not meet the Storage interface of the
|
||
* WebStorage API.
|
||
*/
|
||
function WebStorageStore(webStore, batchSize) {
|
||
this.store = webStore;
|
||
this.batchSize = batchSize;
|
||
if (!utils.isFunction(webStore.getItem) || !utils.isFunction(webStore.setItem) ||
|
||
!utils.isFunction(webStore.removeItem) || !utils.isFunction(webStore.key)) {
|
||
throw new Error(
|
||
"Supplied webStore does not meet the WebStorage API interface"
|
||
);
|
||
}
|
||
if (!parseInt(webStore.length) && webStore.length !== 0) {
|
||
throw new Error(
|
||
"Supplied webStore does not meet the WebStorage API interface (length)"
|
||
);
|
||
}
|
||
// cached list of room_ids this is storing.
|
||
this._roomIds = [];
|
||
this._syncedWithStore = false;
|
||
// tokens used to remember which index the room instance is at.
|
||
this._tokens = [
|
||
// { earliestIndex: -4 }
|
||
];
|
||
}
|
||
|
||
|
||
/**
|
||
* Retrieve the token to stream from.
|
||
* @return {string} The token or null.
|
||
*/
|
||
WebStorageStore.prototype.getSyncToken = function() {
|
||
return this.store.getItem("sync_token");
|
||
};
|
||
|
||
/**
|
||
* Set the token to stream from.
|
||
* @param {string} token The token to stream from.
|
||
*/
|
||
WebStorageStore.prototype.setSyncToken = function(token) {
|
||
this.store.setItem("sync_token", token);
|
||
};
|
||
|
||
/**
|
||
* Store a room in web storage.
|
||
* @param {Room} room
|
||
*/
|
||
WebStorageStore.prototype.storeRoom = function(room) {
|
||
var serRoom = SerialisedRoom.fromRoom(room, this.batchSize);
|
||
persist(this.store, serRoom);
|
||
if (this._roomIds.indexOf(room.roomId) === -1) {
|
||
this._roomIds.push(room.roomId);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Retrieve a room from web storage.
|
||
* @param {string} roomId
|
||
* @return {?Room}
|
||
*/
|
||
WebStorageStore.prototype.getRoom = function(roomId) {
|
||
// probe if room exists; break early if not. Every room should have state.
|
||
if (!getItem(this.store, keyName(roomId, "state"))) {
|
||
debuglog("getRoom: No room with id %s found.", roomId);
|
||
return null;
|
||
}
|
||
var timelineKeys = getTimelineIndices(this.store, roomId);
|
||
if (timelineKeys.indexOf("live") !== -1) {
|
||
debuglog("getRoom: Live events found. Syncing timeline for %s", roomId);
|
||
this._syncTimeline(roomId, timelineKeys);
|
||
}
|
||
return loadRoom(this.store, roomId, this.batchSize, this._tokens);
|
||
};
|
||
|
||
/**
|
||
* Get a list of all rooms from web storage.
|
||
* @return {Array} An empty array.
|
||
*/
|
||
WebStorageStore.prototype.getRooms = function() {
|
||
var rooms = [];
|
||
var i;
|
||
if (!this._syncedWithStore) {
|
||
// sync with the store to set this._roomIds correctly. We know there is
|
||
// exactly one 'state' key for each room, so we grab them.
|
||
this._roomIds = [];
|
||
for (i = 0; i < this.store.length; i++) {
|
||
if (this.store.key(i).indexOf("room_") === 0 &&
|
||
this.store.key(i).indexOf("_state") !== -1) {
|
||
// grab the middle bit which is the room ID
|
||
var k = this.store.key(i);
|
||
this._roomIds.push(
|
||
k.substring("room_".length, k.length - "_state".length)
|
||
);
|
||
}
|
||
}
|
||
this._syncedWithStore = true;
|
||
}
|
||
// call getRoom on each room_id
|
||
for (i = 0; i < this._roomIds.length; i++) {
|
||
var rm = this.getRoom(this._roomIds[i]);
|
||
if (rm) {
|
||
rooms.push(rm);
|
||
}
|
||
}
|
||
return rooms;
|
||
};
|
||
|
||
/**
|
||
* Get a list of summaries from web storage.
|
||
* @return {Array} An empty array.
|
||
*/
|
||
WebStorageStore.prototype.getRoomSummaries = function() {
|
||
return [];
|
||
};
|
||
|
||
/**
|
||
* Store a user in web storage.
|
||
* @param {User} user
|
||
*/
|
||
WebStorageStore.prototype.storeUser = function(user) {
|
||
// persist the events used to make the user, we can reconstruct on demand.
|
||
setItem(this.store, "user_" + user.userId, {
|
||
presence: user.events.presence ? user.events.presence.event : null
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Get a user from web storage.
|
||
* @param {string} userId
|
||
* @return {User}
|
||
*/
|
||
WebStorageStore.prototype.getUser = function(userId) {
|
||
var userData = getItem(this.store, "user_" + userId);
|
||
if (!userData) {
|
||
return null;
|
||
}
|
||
var user = new User(userId);
|
||
if (userData.presence) {
|
||
user.setPresenceEvent(new MatrixEvent(userData.presence));
|
||
}
|
||
return user;
|
||
};
|
||
|
||
/**
|
||
* Retrieve scrollback for this room. Automatically adds events to the timeline.
|
||
* @param {Room} room The matrix room to add the events to the start of the timeline.
|
||
* @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. The last element
|
||
* is the 'oldest' (for parity with homeserver scrollback APIs).
|
||
*/
|
||
WebStorageStore.prototype.scrollback = function(room, limit) {
|
||
if (room.storageToken === undefined || room.storageToken >= this._tokens.length) {
|
||
return [];
|
||
}
|
||
// find the index of the earliest event in this room's timeline
|
||
var storeData = this._tokens[room.storageToken] || {};
|
||
var i;
|
||
var earliestIndex = storeData.earliestIndex;
|
||
var earliestEventId = room.timeline[0] ? room.timeline[0].getId() : null;
|
||
debuglog(
|
||
"scrollback in %s (timeline=%s msgs) i=%s, timeline[0].id=%s - req %s events",
|
||
room.roomId, room.timeline.length, earliestIndex, earliestEventId, limit
|
||
);
|
||
var batch = getItem(
|
||
this.store, keyName(room.roomId, "timeline", earliestIndex)
|
||
);
|
||
if (!batch) {
|
||
// bad room or already at start, either way we have nothing to give.
|
||
debuglog("No batch with index %s found.", earliestIndex);
|
||
return [];
|
||
}
|
||
// populate from this batch first
|
||
var scrollback = [];
|
||
var foundEventId = false;
|
||
for (i = batch.length - 1; i >= 0; i--) {
|
||
// go back and find the earliest event ID, THEN start adding entries.
|
||
// Make a MatrixEvent so we don't assume .event_id exists
|
||
// (e.g v2/v3 JSON may be different)
|
||
var matrixEvent = new MatrixEvent(batch[i]);
|
||
if (matrixEvent.getId() === earliestEventId) {
|
||
foundEventId = true;
|
||
debuglog(
|
||
"Found timeline[0] event at position %s in batch %s",
|
||
i, earliestIndex
|
||
);
|
||
continue;
|
||
}
|
||
if (!foundEventId) {
|
||
continue;
|
||
}
|
||
// add entry
|
||
debuglog("Add event at position %s in batch %s", i, earliestIndex);
|
||
scrollback.push(batch[i]);
|
||
if (scrollback.length === limit) {
|
||
break;
|
||
}
|
||
}
|
||
if (scrollback.length === limit) {
|
||
debuglog("Batch has enough events to satisfy request.");
|
||
return scrollback;
|
||
}
|
||
if (!foundEventId) {
|
||
// the earliest index batch didn't contain the event. In other words,
|
||
// this timeline is at a state we don't know, so bail.
|
||
debuglog(
|
||
"Failed to find event ID %s in batch %s", earliestEventId, earliestIndex
|
||
);
|
||
return [];
|
||
}
|
||
|
||
// get the requested earlier events from earlier batches
|
||
while (scrollback.length < limit) {
|
||
earliestIndex--;
|
||
batch = getItem(
|
||
this.store, keyName(room.roomId, "timeline", earliestIndex)
|
||
);
|
||
if (!batch) {
|
||
// no more events
|
||
debuglog("No batch found at index %s", earliestIndex);
|
||
break;
|
||
}
|
||
for (i = batch.length - 1; i >= 0; i--) {
|
||
debuglog("Add event at position %s in batch %s", i, earliestIndex);
|
||
scrollback.push(batch[i]);
|
||
if (scrollback.length === limit) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
debuglog(
|
||
"Out of %s requested events, returning %s. New index=%s",
|
||
limit, scrollback.length, earliestIndex
|
||
);
|
||
room.addEventsToTimeline(utils.map(scrollback, function(e) {
|
||
return new MatrixEvent(e);
|
||
}), true);
|
||
|
||
this._tokens[room.storageToken] = {
|
||
earliestIndex: earliestIndex
|
||
};
|
||
return scrollback;
|
||
};
|
||
|
||
/**
|
||
* 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. The last element
|
||
* is the 'oldest' (for parity with homeserver scrollback APIs).
|
||
*/
|
||
WebStorageStore.prototype.storeEvents = function(room, events, token, toStart) {
|
||
if (toStart) {
|
||
// add paginated events to lowest batch indexes (can go -ve)
|
||
var lowIndex = getIndexExtremity(
|
||
getTimelineIndices(this.store, room.roomId), true
|
||
);
|
||
var i, key, batch;
|
||
for (i = 0; i < events.length; i++) { // loop events to be stored
|
||
key = keyName(room.roomId, "timeline", lowIndex);
|
||
batch = getItem(this.store, key) || [];
|
||
while (batch.length < this.batchSize && i < events.length) {
|
||
batch.unshift(events[i].event);
|
||
i++; // increment to insert next event into this batch
|
||
}
|
||
i--; // decrement to avoid skipping one (for loop ++s)
|
||
setItem(this.store, key, batch);
|
||
lowIndex--; // decrement index to get a new batch.
|
||
}
|
||
}
|
||
else {
|
||
// dump as live events
|
||
var liveEvents = getItem(
|
||
this.store, keyName(room.roomId, "timeline", "live")
|
||
) || [];
|
||
debuglog(
|
||
"Adding %s events to %s live list (which has %s already)",
|
||
events.length, room.roomId, liveEvents.length
|
||
);
|
||
var updateState = false;
|
||
liveEvents = liveEvents.concat(utils.map(events, function(me) {
|
||
// cheeky check to avoid looping twice
|
||
if (me.isState()) {
|
||
updateState = true;
|
||
}
|
||
return me.event;
|
||
}));
|
||
setItem(
|
||
this.store, keyName(room.roomId, "timeline", "live"), liveEvents
|
||
);
|
||
if (updateState) {
|
||
debuglog("Storing state for %s as new events updated state", room.roomId);
|
||
// use 0 batch size; we don't care about batching right now.
|
||
var serRoom = SerialisedRoom.fromRoom(room, 0);
|
||
setItem(this.store, keyName(serRoom.roomId, "state"), serRoom.state);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Sync the 'live' timeline, batching live events according to 'batchSize'.
|
||
* @param {string} roomId The room to sync the timeline.
|
||
* @param {Array<String>} timelineIndices Optional. The indices in the timeline
|
||
* if known already.
|
||
*/
|
||
WebStorageStore.prototype._syncTimeline = function(roomId, timelineIndices) {
|
||
timelineIndices = timelineIndices || getTimelineIndices(this.store, roomId);
|
||
var liveEvents = getItem(this.store, keyName(roomId, "timeline", "live")) || [];
|
||
|
||
// get the highest numbered $INDEX batch
|
||
var highestIndex = getIndexExtremity(timelineIndices);
|
||
var hiKey = keyName(roomId, "timeline", highestIndex);
|
||
var hiBatch = getItem(this.store, hiKey) || [];
|
||
// fill up the existing batch first.
|
||
while (hiBatch.length < this.batchSize && liveEvents.length > 0) {
|
||
hiBatch.push(liveEvents.shift());
|
||
}
|
||
setItem(this.store, hiKey, hiBatch);
|
||
|
||
// start adding new batches as required
|
||
var batch = [];
|
||
while (liveEvents.length > 0) {
|
||
batch.push(liveEvents.shift());
|
||
if (batch.length === this.batchSize || liveEvents.length === 0) {
|
||
// persist the full batch and make another
|
||
highestIndex++;
|
||
hiKey = keyName(roomId, "timeline", highestIndex);
|
||
setItem(this.store, hiKey, batch);
|
||
batch = [];
|
||
}
|
||
}
|
||
// reset live array
|
||
setItem(this.store, keyName(roomId, "timeline", "live"), []);
|
||
};
|
||
|
||
function SerialisedRoom(roomId) {
|
||
this.state = {
|
||
events: {}
|
||
};
|
||
this.timeline = {
|
||
// $INDEX: []
|
||
};
|
||
this.roomId = roomId;
|
||
}
|
||
|
||
/**
|
||
* Convert a Room instance into a SerialisedRoom instance which can be stored
|
||
* in the key value store.
|
||
* @param {Room} room The matrix room to convert
|
||
* @param {integer} batchSize The number of events per timeline batch
|
||
* @return {SerialisedRoom} A serialised room representation of 'room'.
|
||
*/
|
||
SerialisedRoom.fromRoom = function(room, batchSize) {
|
||
var self = new SerialisedRoom(room.roomId);
|
||
var index;
|
||
self.state.pagination_token = room.oldState.paginationToken;
|
||
// [room_$ROOMID_state] downcast to POJO from MatrixEvent
|
||
utils.forEach(utils.keys(room.currentState.events), function(eventType) {
|
||
utils.forEach(utils.keys(room.currentState.events[eventType]), function(skey) {
|
||
if (!self.state.events[eventType]) {
|
||
self.state.events[eventType] = {};
|
||
}
|
||
self.state.events[eventType][skey] = (
|
||
room.currentState.events[eventType][skey].event
|
||
);
|
||
});
|
||
});
|
||
|
||
// [room_$ROOMID_timeline_$INDEX]
|
||
if (batchSize > 0) {
|
||
index = 0;
|
||
while (index * batchSize < room.timeline.length) {
|
||
self.timeline[index] = room.timeline.slice(
|
||
index * batchSize, (index + 1) * batchSize
|
||
);
|
||
self.timeline[index] = utils.map(self.timeline[index], function(me) {
|
||
// use POJO not MatrixEvent
|
||
return me.event;
|
||
});
|
||
index++;
|
||
}
|
||
}
|
||
else { // don't batch
|
||
self.timeline[0] = utils.map(room.timeline, function(matrixEvent) {
|
||
return matrixEvent.event;
|
||
});
|
||
}
|
||
return self;
|
||
};
|
||
|
||
function loadRoom(store, roomId, numEvents, tokenArray) {
|
||
var room = new Room(roomId, tokenArray.length);
|
||
|
||
// populate state (flatten nested struct to event array)
|
||
var currentStateMap = getItem(store, keyName(roomId, "state"));
|
||
var stateEvents = [];
|
||
utils.forEach(utils.keys(currentStateMap.events), function(eventType) {
|
||
utils.forEach(utils.keys(currentStateMap.events[eventType]), function(skey) {
|
||
stateEvents.push(currentStateMap.events[eventType][skey]);
|
||
});
|
||
});
|
||
// TODO: Fix logic dupe with MatrixClient._processRoomEvents
|
||
var oldStateEvents = utils.map(
|
||
utils.deepCopy(stateEvents), function(e) {
|
||
return new MatrixEvent(e);
|
||
}
|
||
);
|
||
var currentStateEvents = utils.map(stateEvents, function(e) {
|
||
return new MatrixEvent(e);
|
||
}
|
||
);
|
||
room.oldState.setStateEvents(oldStateEvents);
|
||
room.currentState.setStateEvents(currentStateEvents);
|
||
|
||
// add most recent numEvents
|
||
var recentEvents = [];
|
||
var index = getIndexExtremity(getTimelineIndices(store, roomId));
|
||
var eventIndex = index;
|
||
var i, key, batch;
|
||
while (recentEvents.length < numEvents) {
|
||
key = keyName(roomId, "timeline", index);
|
||
batch = getItem(store, key) || [];
|
||
if (batch.length === 0) {
|
||
// nothing left in the store.
|
||
break;
|
||
}
|
||
for (i = batch.length - 1; i >= 0; i--) {
|
||
recentEvents.unshift(new MatrixEvent(batch[i]));
|
||
if (recentEvents.length === numEvents) {
|
||
eventIndex = index;
|
||
break;
|
||
}
|
||
}
|
||
index--;
|
||
}
|
||
// add events backwards to diverge old state correctly.
|
||
room.addEventsToTimeline(recentEvents.reverse(), true);
|
||
room.oldState.paginationToken = currentStateMap.pagination_token;
|
||
// set the token data to let us know which index this room instance is at
|
||
// for scrollback.
|
||
tokenArray.push({
|
||
earliestIndex: eventIndex
|
||
});
|
||
return room;
|
||
}
|
||
|
||
function persist(store, serRoom) {
|
||
setItem(store, keyName(serRoom.roomId, "state"), serRoom.state);
|
||
utils.forEach(utils.keys(serRoom.timeline), function(index) {
|
||
setItem(store,
|
||
keyName(serRoom.roomId, "timeline", index),
|
||
serRoom.timeline[index]
|
||
);
|
||
});
|
||
}
|
||
|
||
function getTimelineIndices(store, roomId) {
|
||
var keys = [];
|
||
for (var i = 0; i < store.length; i++) {
|
||
if (store.key(i).indexOf(keyName(roomId, "timeline_")) !== -1) {
|
||
// e.g. room_$ROOMID_timeline_0 => 0
|
||
keys.push(
|
||
store.key(i).replace(keyName(roomId, "timeline_"), "")
|
||
);
|
||
}
|
||
}
|
||
return keys;
|
||
}
|
||
|
||
function getIndexExtremity(timelineIndices, getLowest) {
|
||
var extremity, index;
|
||
for (var i = 0; i < timelineIndices.length; i++) {
|
||
index = parseInt(timelineIndices[i]);
|
||
if (!isNaN(index) && (
|
||
extremity === undefined ||
|
||
!getLowest && index > extremity ||
|
||
getLowest && index < extremity)) {
|
||
extremity = index;
|
||
}
|
||
}
|
||
return extremity;
|
||
}
|
||
|
||
function keyName(roomId, key, index) {
|
||
return "room_" + roomId + "_" + key + (
|
||
index === undefined ? "" : ("_" + index)
|
||
);
|
||
}
|
||
|
||
function getItem(store, key) {
|
||
try {
|
||
return JSON.parse(store.getItem(key));
|
||
}
|
||
catch (e) {
|
||
debuglog("Failed to get key %s: %s", key, e);
|
||
debuglog(e.stack);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function setItem(store, key, val) {
|
||
store.setItem(key, JSON.stringify(val));
|
||
}
|
||
|
||
function debuglog() {
|
||
if (DEBUG) {
|
||
console.log.apply(console, arguments);
|
||
}
|
||
}
|
||
|
||
/*
|
||
function delRoomStruct(store, roomId) {
|
||
var prefix = "room_" + roomId;
|
||
var keysToRemove = [];
|
||
for (var i = 0; i < store.length; i++) {
|
||
if (store.key(i).indexOf(prefix) !== -1) {
|
||
keysToRemove.push(store.key(i));
|
||
}
|
||
}
|
||
utils.forEach(keysToRemove, function(key) {
|
||
store.removeItem(key);
|
||
});
|
||
} */
|
||
|
||
/** Web Storage Store class. */
|
||
module.exports = WebStorageStore;
|
||
|
||
},{"../models/event":6,"../models/room":10,"../models/user":11,"../utils":18}],18:[function(require,module,exports){
|
||
"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
|
||
*/
|
||
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;
|
||
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;
|
||
if (reverse) {
|
||
for (i = array.length - 1; i >= 0; i--) {
|
||
if (fn(array[i], i, array)) {
|
||
array.splice(i, 1);
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
for (i = 0; i < array.length; i++) {
|
||
if (fn(array[i], i, array)) {
|
||
array.splice(i, 1);
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
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 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(JSON.stringify(obj));
|
||
};
|
||
|
||
|
||
/**
|
||
* 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, A, k;
|
||
|
||
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.
|
||
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;
|
||
};
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 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 Object.create != '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 != '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 = Object.create(superCtor.prototype, {
|
||
constructor: {
|
||
value: ctor,
|
||
enumerable: false,
|
||
writable: true,
|
||
configurable: true
|
||
}
|
||
});
|
||
};
|
||
|
||
},{}],19:[function(require,module,exports){
|
||
(function (global){
|
||
"use strict";
|
||
/**
|
||
* This is an internal module. See {@link createNewMatrixCall} for the public API.
|
||
* @module webrtc/call
|
||
*/
|
||
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)
|
||
|
||
/**
|
||
* 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 {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.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();
|
||
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;
|
||
|
||
this.screenSharingStream = 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";
|
||
|
||
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.</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 = _getChromeScreenSharingConstraints(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);
|
||
};
|
||
|
||
/**
|
||
* 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;
|
||
element.src = this.URL.createObjectURL(this.localAVStream);
|
||
element.muted = true;
|
||
var self = this;
|
||
setTimeout(function() {
|
||
var vel = self.getLocalVideoElement();
|
||
if (vel.play) {
|
||
vel.play();
|
||
}
|
||
}, 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.
|
||
* @param {Element} element The <code><video></code> DOM element.
|
||
*/
|
||
MatrixCall.prototype.setRemoteAudioElement = function(element) {
|
||
this.remoteAudioElement = element;
|
||
_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 (!this.localAVStream && !this.waitForLocalAVStream) {
|
||
this.webRtc.getUserMedia(
|
||
_getUserMediaVideoContraints(this.type),
|
||
hookCallback(self, self._gotUserMediaForAnswer),
|
||
hookCallback(self, self._getUserMediaFailed)
|
||
);
|
||
setState(this, 'wait_local_media');
|
||
} else if (this.localAVStream) {
|
||
this._gotUserMediaForAnswer(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._gotUserMediaForAnswer(this.localAVStream);
|
||
delete(this.localAVStream);
|
||
} else if (this.state == 'invite_sent') {
|
||
debuglog("Handing local stream to new call");
|
||
newCall._gotUserMediaForAnswer(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) {
|
||
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._gotUserMediaForInvite = function(stream) {
|
||
if (this.successor) {
|
||
this.successor._gotUserMediaForAnswer(stream);
|
||
return;
|
||
}
|
||
if (this.state == 'ended') {
|
||
return;
|
||
}
|
||
debuglog("_gotUserMediaForInvite -> " + this.type);
|
||
var self = this;
|
||
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");
|
||
videoEl.src = this.URL.createObjectURL(this.screenSharingStream);
|
||
}
|
||
else {
|
||
videoEl.src = this.URL.createObjectURL(stream);
|
||
}
|
||
videoEl.muted = true;
|
||
setTimeout(function() {
|
||
var vel = self.getLocalVideoElement();
|
||
if (vel.play) {
|
||
vel.play();
|
||
}
|
||
}, 0);
|
||
}
|
||
|
||
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);
|
||
if (this.screenSharingStream) {
|
||
console.log("Adding screen-sharing stream to peer connection");
|
||
this.peerConn.addStream(this.screenSharingStream);
|
||
// let's use this for the local preview...
|
||
this.localAVStream = this.screenSharingStream;
|
||
}
|
||
this.peerConn.createOffer(
|
||
hookCallback(self, self._gotLocalOffer),
|
||
hookCallback(self, self._getLocalOfferFailed)
|
||
);
|
||
setState(self, 'create_offer');
|
||
};
|
||
|
||
/**
|
||
* Internal
|
||
* @private
|
||
* @param {Object} stream
|
||
*/
|
||
MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
|
||
var self = this;
|
||
if (self.state == 'ended') {
|
||
return;
|
||
}
|
||
var localVidEl = self.getLocalVideoElement();
|
||
|
||
if (localVidEl && self.type == 'video') {
|
||
localVidEl.autoplay = true;
|
||
localVidEl.src = self.URL.createObjectURL(stream);
|
||
localVidEl.muted = true;
|
||
setTimeout(function() {
|
||
var vel = self.getLocalVideoElement();
|
||
if (vel.play) {
|
||
vel.play();
|
||
}
|
||
}, 0);
|
||
}
|
||
|
||
self.localAVStream = stream;
|
||
setTracksEnabled(stream.getAudioTracks(), true);
|
||
self.peerConn.addStream(stream);
|
||
|
||
var constraints = {
|
||
'mandatory': {
|
||
'OfferToReceiveAudio': true,
|
||
'OfferToReceiveVideo': self.type == 'video'
|
||
}
|
||
};
|
||
self.peerConn.createAnswer(function(description) {
|
||
debuglog("Created answer: " + description);
|
||
self.peerConn.setLocalDescription(description, function() {
|
||
var content = {
|
||
version: 0,
|
||
call_id: self.callId,
|
||
answer: {
|
||
sdp: self.peerConn.localDescription.sdp,
|
||
type: self.peerConn.localDescription.type
|
||
}
|
||
};
|
||
sendEvent(self, 'm.call.answer', content);
|
||
setState(self, 'connecting');
|
||
}, 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
|
||
);
|
||
// 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);
|
||
|
||
setTimeout(function() {
|
||
if (self.state == 'invite_sent') {
|
||
self.hangup('invite_timeout');
|
||
}
|
||
}, MatrixCall.CALL_TIMEOUT_MS);
|
||
setState(self, 'invite_sent');
|
||
}, 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) {
|
||
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?"
|
||
)
|
||
);
|
||
this.hangup("user_media_failed");
|
||
};
|
||
|
||
/**
|
||
* 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;
|
||
} 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);
|
||
});
|
||
|
||
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);
|
||
}
|
||
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(tracks, enabled) {
|
||
for (var i = 0; i < tracks.length; i++) {
|
||
tracks[i].enabled = enabled;
|
||
}
|
||
};
|
||
|
||
var isTracksEnabled = function(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(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(self, eventType, content) {
|
||
return self.client.sendEvent(self.roomId, eventType, content);
|
||
};
|
||
|
||
var sendCandidate = function(self, content) {
|
||
// Sends candidates with are sent in a special way because we try to amalgamate
|
||
// them into one message
|
||
self.candidateSendQueue.push(content);
|
||
if (self.candidateSendTries === 0) {
|
||
setTimeout(function() {
|
||
_sendCandidateQueue(self);
|
||
}, 100);
|
||
}
|
||
};
|
||
|
||
var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
|
||
if (self.getRemoteVideoElement()) {
|
||
if (self.getRemoteVideoElement().pause) {
|
||
self.getRemoteVideoElement().pause();
|
||
}
|
||
self.getRemoteVideoElement().src = "";
|
||
}
|
||
if (self.getRemoteAudioElement()) {
|
||
if (self.getRemoteAudioElement().pause) {
|
||
self.getRemoteAudioElement().pause();
|
||
}
|
||
self.getRemoteAudioElement().src = "";
|
||
}
|
||
if (self.getLocalVideoElement()) {
|
||
if (self.getLocalVideoElement().pause) {
|
||
self.getLocalVideoElement().pause();
|
||
}
|
||
self.getLocalVideoElement().src = "";
|
||
}
|
||
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(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(self) {
|
||
if (self.getRemoteVideoElement() && self.remoteAVStream) {
|
||
var player = self.getRemoteVideoElement();
|
||
player.autoplay = true;
|
||
player.src = self.URL.createObjectURL(self.remoteAVStream);
|
||
setTimeout(function() {
|
||
var vel = self.getRemoteVideoElement();
|
||
if (vel.play) {
|
||
vel.play();
|
||
}
|
||
// OpenWebRTC does not support oniceconnectionstatechange yet
|
||
if (self.webRtc.isOpenWebRTC()) {
|
||
setState(self, 'connected');
|
||
}
|
||
}, 0);
|
||
}
|
||
};
|
||
|
||
var _tryPlayRemoteAudioStream = function(self) {
|
||
if (self.getRemoteAudioElement() && self.remoteAStream) {
|
||
var player = self.getRemoteAudioElement();
|
||
player.autoplay = true;
|
||
player.src = self.URL.createObjectURL(self.remoteAStream);
|
||
setTimeout(function() {
|
||
var ael = self.getRemoteAudioElement();
|
||
if (ael.play) {
|
||
ael.play();
|
||
}
|
||
// OpenWebRTC does not support oniceconnectionstatechange yet
|
||
if (self.webRtc.isOpenWebRTC()) {
|
||
setState(self, 'connected');
|
||
}
|
||
}, 0);
|
||
}
|
||
};
|
||
|
||
var checkForErrorListener = function(self) {
|
||
if (self.listeners("error").length === 0) {
|
||
throw new Error(
|
||
"You MUST attach an error listener using call.on('error', function() {})"
|
||
);
|
||
}
|
||
};
|
||
|
||
var callError = function(code, msg) {
|
||
var e = new Error(msg);
|
||
e.code = code;
|
||
return e;
|
||
};
|
||
|
||
var debuglog = function() {
|
||
if (DEBUG) {
|
||
console.log.apply(console, arguments);
|
||
}
|
||
};
|
||
|
||
var _sendCandidateQueue = function(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(self, constraints) {
|
||
self.client.callList[self.callId] = self;
|
||
self.webRtc.getUserMedia(
|
||
constraints,
|
||
hookCallback(self, self._gotUserMediaForInvite),
|
||
hookCallback(self, self._getUserMediaFailed)
|
||
);
|
||
setState(self, 'wait_local_media');
|
||
self.direction = 'outbound';
|
||
self.config = constraints;
|
||
};
|
||
|
||
var _createPeerConnection = function(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({
|
||
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 _getChromeScreenSharingConstraints = function(call) {
|
||
var screen = global.screen;
|
||
if (!screen) {
|
||
call.emit("error", callError(
|
||
MatrixCall.ERR_NO_USER_MEDIA,
|
||
"Couldn't determine screen sharing constaints."
|
||
));
|
||
return;
|
||
}
|
||
// it won't work at all if you're not on HTTPS so whine whine whine
|
||
if (!global.window || global.window.location.protocol !== "https:") {
|
||
call.emit("error", callError(
|
||
MatrixCall.ERR_NO_USER_MEDIA,
|
||
"You need to be using HTTPS to place a screen-sharing call."
|
||
));
|
||
return;
|
||
}
|
||
|
||
return {
|
||
video: {
|
||
mandatory: {
|
||
chromeMediaSource: "screen",
|
||
chromeMediaSourceId: "" + Date.now(),
|
||
maxWidth: screen.width,
|
||
maxHeight: screen.height,
|
||
minFrameRate: 1,
|
||
maxFrameRate: 10
|
||
}
|
||
}
|
||
};
|
||
};
|
||
|
||
var _getUserMediaVideoContraints = function(callType) {
|
||
switch (callType) {
|
||
case 'voice':
|
||
return ({audio: true, video: false});
|
||
case 'video':
|
||
return ({audio: true, video: {
|
||
mandatory: {
|
||
minWidth: 640,
|
||
maxWidth: 640,
|
||
minHeight: 360,
|
||
maxHeight: 360
|
||
}
|
||
}});
|
||
}
|
||
};
|
||
|
||
var hookCallback = function(call, fn) {
|
||
return function() {
|
||
return fn.apply(call, arguments);
|
||
};
|
||
};
|
||
|
||
var forAllVideoTracksOnStream = function(s, f) {
|
||
var tracks = s.getVideoTracks();
|
||
for (var i = 0; i < tracks.length; i++) {
|
||
f(tracks[i]);
|
||
}
|
||
};
|
||
|
||
var forAllAudioTracksOnStream = function(s, f) {
|
||
var tracks = s.getAudioTracks();
|
||
for (var i = 0; i < tracks.length; i++) {
|
||
f(tracks[i]);
|
||
}
|
||
};
|
||
|
||
var forAllTracksOnStream = function(s, f) {
|
||
forAllVideoTracksOnStream(s, f);
|
||
forAllAudioTracksOnStream(s, f);
|
||
};
|
||
|
||
/** The MatrixCall class. */
|
||
module.exports.MatrixCall = MatrixCall;
|
||
|
||
/**
|
||
* 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.
|
||
* @return {MatrixCall} the call or null if the browser doesn't support calling.
|
||
*/
|
||
module.exports.createNewMatrixCall = function(client, roomId) {
|
||
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 opts = {
|
||
webRtc: webRtc,
|
||
client: client,
|
||
URL: w.URL,
|
||
roomId: roomId,
|
||
turnServers: client.getTurnServers()
|
||
};
|
||
return new MatrixCall(opts);
|
||
};
|
||
|
||
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
||
},{"../utils":18,"events":21}],20:[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
|
||
|
||
},{}],21:[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
|
||
}
|
||
throw TypeError('Uncaught, unspecified "error" event.');
|
||
}
|
||
}
|
||
|
||
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:
|
||
len = arguments.length;
|
||
args = new Array(len - 1);
|
||
for (i = 1; i < len; i++)
|
||
args[i - 1] = arguments[i];
|
||
handler.apply(this, args);
|
||
}
|
||
} else if (isObject(handler)) {
|
||
len = arguments.length;
|
||
args = new Array(len - 1);
|
||
for (i = 1; i < len; i++)
|
||
args[i - 1] = arguments[i];
|
||
|
||
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) {
|
||
var m;
|
||
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 {
|
||
// 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.listenerCount = function(emitter, type) {
|
||
var ret;
|
||
if (!emitter._events || !emitter._events[type])
|
||
ret = 0;
|
||
else if (isFunction(emitter._events[type]))
|
||
ret = 1;
|
||
else
|
||
ret = emitter._events[type].length;
|
||
return ret;
|
||
};
|
||
|
||
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;
|
||
}
|
||
|
||
},{}],22:[function(require,module,exports){
|
||
// shim for using process in browser
|
||
|
||
var process = module.exports = {};
|
||
var queue = [];
|
||
var draining = false;
|
||
var currentQueue;
|
||
var queueIndex = -1;
|
||
|
||
function cleanUpNextTick() {
|
||
draining = false;
|
||
if (currentQueue.length) {
|
||
queue = currentQueue.concat(queue);
|
||
} else {
|
||
queueIndex = -1;
|
||
}
|
||
if (queue.length) {
|
||
drainQueue();
|
||
}
|
||
}
|
||
|
||
function drainQueue() {
|
||
if (draining) {
|
||
return;
|
||
}
|
||
var timeout = setTimeout(cleanUpNextTick);
|
||
draining = true;
|
||
|
||
var len = queue.length;
|
||
while(len) {
|
||
currentQueue = queue;
|
||
queue = [];
|
||
while (++queueIndex < len) {
|
||
currentQueue[queueIndex].run();
|
||
}
|
||
queueIndex = -1;
|
||
len = queue.length;
|
||
}
|
||
currentQueue = null;
|
||
draining = false;
|
||
clearTimeout(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) {
|
||
setTimeout(drainQueue, 0);
|
||
}
|
||
};
|
||
|
||
// 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.binding = function (name) {
|
||
throw new Error('process.binding is not supported');
|
||
};
|
||
|
||
// TODO(shtylman)
|
||
process.cwd = function () { return '/' };
|
||
process.chdir = function (dir) {
|
||
throw new Error('process.chdir is not supported');
|
||
};
|
||
process.umask = function() { return 0; };
|
||
|
||
},{}],23:[function(require,module,exports){
|
||
(function (process){
|
||
// vim:ts=4:sts=4:sw=4:
|
||
/*!
|
||
*
|
||
* Copyright 2009-2012 Kris Kowal under the terms of the MIT
|
||
* license found at http://github.com/kriskowal/q/raw/master/LICENSE
|
||
*
|
||
* With parts by Tyler Close
|
||
* Copyright 2007-2009 Tyler Close under the terms of the MIT X license found
|
||
* at http://www.opensource.org/licenses/mit-license.html
|
||
* Forked at ref_send.js version: 2009-05-11
|
||
*
|
||
* With parts by Mark Miller
|
||
* Copyright (C) 2011 Google Inc.
|
||
*
|
||
* 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 (definition) {
|
||
"use strict";
|
||
|
||
// This file will function properly as a <script> tag, or a module
|
||
// using CommonJS and NodeJS or RequireJS module formats. In
|
||
// Common/Node/RequireJS, the module exports the Q API and when
|
||
// executed as a simple <script>, it creates a Q global instead.
|
||
|
||
// Montage Require
|
||
if (typeof bootstrap === "function") {
|
||
bootstrap("promise", definition);
|
||
|
||
// CommonJS
|
||
} else if (typeof exports === "object" && typeof module === "object") {
|
||
module.exports = definition();
|
||
|
||
// RequireJS
|
||
} else if (typeof define === "function" && define.amd) {
|
||
define(definition);
|
||
|
||
// SES (Secure EcmaScript)
|
||
} else if (typeof ses !== "undefined") {
|
||
if (!ses.ok()) {
|
||
return;
|
||
} else {
|
||
ses.makeQ = definition;
|
||
}
|
||
|
||
// <script>
|
||
} else if (typeof window !== "undefined" || typeof self !== "undefined") {
|
||
// Prefer window over self for add-on scripts. Use self for
|
||
// non-windowed contexts.
|
||
var global = typeof window !== "undefined" ? window : self;
|
||
|
||
// Get the `window` object, save the previous Q global
|
||
// and initialize Q as a global.
|
||
var previousQ = global.Q;
|
||
global.Q = definition();
|
||
|
||
// Add a noConflict function so Q can be removed from the
|
||
// global namespace.
|
||
global.Q.noConflict = function () {
|
||
global.Q = previousQ;
|
||
return this;
|
||
};
|
||
|
||
} else {
|
||
throw new Error("This environment was not anticipated by Q. Please file a bug.");
|
||
}
|
||
|
||
})(function () {
|
||
"use strict";
|
||
|
||
var hasStacks = false;
|
||
try {
|
||
throw new Error();
|
||
} catch (e) {
|
||
hasStacks = !!e.stack;
|
||
}
|
||
|
||
// All code after this point will be filtered from stack traces reported
|
||
// by Q.
|
||
var qStartingLine = captureLine();
|
||
var qFileName;
|
||
|
||
// shims
|
||
|
||
// used for fallback in "allResolved"
|
||
var noop = function () {};
|
||
|
||
// Use the fastest possible means to execute a task in a future turn
|
||
// of the event loop.
|
||
var nextTick =(function () {
|
||
// linked list of tasks (single, with head node)
|
||
var head = {task: void 0, next: null};
|
||
var tail = head;
|
||
var flushing = false;
|
||
var requestTick = void 0;
|
||
var isNodeJS = false;
|
||
// queue for late tasks, used by unhandled rejection tracking
|
||
var laterQueue = [];
|
||
|
||
function flush() {
|
||
/* jshint loopfunc: true */
|
||
var task, domain;
|
||
|
||
while (head.next) {
|
||
head = head.next;
|
||
task = head.task;
|
||
head.task = void 0;
|
||
domain = head.domain;
|
||
|
||
if (domain) {
|
||
head.domain = void 0;
|
||
domain.enter();
|
||
}
|
||
runSingle(task, domain);
|
||
|
||
}
|
||
while (laterQueue.length) {
|
||
task = laterQueue.pop();
|
||
runSingle(task);
|
||
}
|
||
flushing = false;
|
||
}
|
||
// runs a single function in the async queue
|
||
function runSingle(task, domain) {
|
||
try {
|
||
task();
|
||
|
||
} catch (e) {
|
||
if (isNodeJS) {
|
||
// In node, uncaught exceptions are considered fatal errors.
|
||
// Re-throw them synchronously to interrupt flushing!
|
||
|
||
// Ensure continuation if the uncaught exception is suppressed
|
||
// listening "uncaughtException" events (as domains does).
|
||
// Continue in next event to avoid tick recursion.
|
||
if (domain) {
|
||
domain.exit();
|
||
}
|
||
setTimeout(flush, 0);
|
||
if (domain) {
|
||
domain.enter();
|
||
}
|
||
|
||
throw e;
|
||
|
||
} else {
|
||
// In browsers, uncaught exceptions are not fatal.
|
||
// Re-throw them asynchronously to avoid slow-downs.
|
||
setTimeout(function () {
|
||
throw e;
|
||
}, 0);
|
||
}
|
||
}
|
||
|
||
if (domain) {
|
||
domain.exit();
|
||
}
|
||
}
|
||
|
||
nextTick = function (task) {
|
||
tail = tail.next = {
|
||
task: task,
|
||
domain: isNodeJS && process.domain,
|
||
next: null
|
||
};
|
||
|
||
if (!flushing) {
|
||
flushing = true;
|
||
requestTick();
|
||
}
|
||
};
|
||
|
||
if (typeof process === "object" &&
|
||
process.toString() === "[object process]" && process.nextTick) {
|
||
// Ensure Q is in a real Node environment, with a `process.nextTick`.
|
||
// To see through fake Node environments:
|
||
// * Mocha test runner - exposes a `process` global without a `nextTick`
|
||
// * Browserify - exposes a `process.nexTick` function that uses
|
||
// `setTimeout`. In this case `setImmediate` is preferred because
|
||
// it is faster. Browserify's `process.toString()` yields
|
||
// "[object Object]", while in a real Node environment
|
||
// `process.nextTick()` yields "[object process]".
|
||
isNodeJS = true;
|
||
|
||
requestTick = function () {
|
||
process.nextTick(flush);
|
||
};
|
||
|
||
} else if (typeof setImmediate === "function") {
|
||
// In IE10, Node.js 0.9+, or https://github.com/NobleJS/setImmediate
|
||
if (typeof window !== "undefined") {
|
||
requestTick = setImmediate.bind(window, flush);
|
||
} else {
|
||
requestTick = function () {
|
||
setImmediate(flush);
|
||
};
|
||
}
|
||
|
||
} else if (typeof MessageChannel !== "undefined") {
|
||
// modern browsers
|
||
// http://www.nonblocking.io/2011/06/windownexttick.html
|
||
var channel = new MessageChannel();
|
||
// At least Safari Version 6.0.5 (8536.30.1) intermittently cannot create
|
||
// working message ports the first time a page loads.
|
||
channel.port1.onmessage = function () {
|
||
requestTick = requestPortTick;
|
||
channel.port1.onmessage = flush;
|
||
flush();
|
||
};
|
||
var requestPortTick = function () {
|
||
// Opera requires us to provide a message payload, regardless of
|
||
// whether we use it.
|
||
channel.port2.postMessage(0);
|
||
};
|
||
requestTick = function () {
|
||
setTimeout(flush, 0);
|
||
requestPortTick();
|
||
};
|
||
|
||
} else {
|
||
// old browsers
|
||
requestTick = function () {
|
||
setTimeout(flush, 0);
|
||
};
|
||
}
|
||
// runs a task after all other tasks have been run
|
||
// this is useful for unhandled rejection tracking that needs to happen
|
||
// after all `then`d tasks have been run.
|
||
nextTick.runAfter = function (task) {
|
||
laterQueue.push(task);
|
||
if (!flushing) {
|
||
flushing = true;
|
||
requestTick();
|
||
}
|
||
};
|
||
return nextTick;
|
||
})();
|
||
|
||
// Attempt to make generics safe in the face of downstream
|
||
// modifications.
|
||
// There is no situation where this is necessary.
|
||
// If you need a security guarantee, these primordials need to be
|
||
// deeply frozen anyway, and if you don’t need a security guarantee,
|
||
// this is just plain paranoid.
|
||
// However, this **might** have the nice side-effect of reducing the size of
|
||
// the minified code by reducing x.call() to merely x()
|
||
// See Mark Miller’s explanation of what this does.
|
||
// http://wiki.ecmascript.org/doku.php?id=conventions:safe_meta_programming
|
||
var call = Function.call;
|
||
function uncurryThis(f) {
|
||
return function () {
|
||
return call.apply(f, arguments);
|
||
};
|
||
}
|
||
// This is equivalent, but slower:
|
||
// uncurryThis = Function_bind.bind(Function_bind.call);
|
||
// http://jsperf.com/uncurrythis
|
||
|
||
var array_slice = uncurryThis(Array.prototype.slice);
|
||
|
||
var array_reduce = uncurryThis(
|
||
Array.prototype.reduce || function (callback, basis) {
|
||
var index = 0,
|
||
length = this.length;
|
||
// concerning the initial value, if one is not provided
|
||
if (arguments.length === 1) {
|
||
// seek to the first value in the array, accounting
|
||
// for the possibility that is is a sparse array
|
||
do {
|
||
if (index in this) {
|
||
basis = this[index++];
|
||
break;
|
||
}
|
||
if (++index >= length) {
|
||
throw new TypeError();
|
||
}
|
||
} while (1);
|
||
}
|
||
// reduce
|
||
for (; index < length; index++) {
|
||
// account for the possibility that the array is sparse
|
||
if (index in this) {
|
||
basis = callback(basis, this[index], index);
|
||
}
|
||
}
|
||
return basis;
|
||
}
|
||
);
|
||
|
||
var array_indexOf = uncurryThis(
|
||
Array.prototype.indexOf || function (value) {
|
||
// not a very good shim, but good enough for our one use of it
|
||
for (var i = 0; i < this.length; i++) {
|
||
if (this[i] === value) {
|
||
return i;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
);
|
||
|
||
var array_map = uncurryThis(
|
||
Array.prototype.map || function (callback, thisp) {
|
||
var self = this;
|
||
var collect = [];
|
||
array_reduce(self, function (undefined, value, index) {
|
||
collect.push(callback.call(thisp, value, index, self));
|
||
}, void 0);
|
||
return collect;
|
||
}
|
||
);
|
||
|
||
var object_create = Object.create || function (prototype) {
|
||
function Type() { }
|
||
Type.prototype = prototype;
|
||
return new Type();
|
||
};
|
||
|
||
var object_hasOwnProperty = uncurryThis(Object.prototype.hasOwnProperty);
|
||
|
||
var object_keys = Object.keys || function (object) {
|
||
var keys = [];
|
||
for (var key in object) {
|
||
if (object_hasOwnProperty(object, key)) {
|
||
keys.push(key);
|
||
}
|
||
}
|
||
return keys;
|
||
};
|
||
|
||
var object_toString = uncurryThis(Object.prototype.toString);
|
||
|
||
function isObject(value) {
|
||
return value === Object(value);
|
||
}
|
||
|
||
// generator related shims
|
||
|
||
// FIXME: Remove this function once ES6 generators are in SpiderMonkey.
|
||
function isStopIteration(exception) {
|
||
return (
|
||
object_toString(exception) === "[object StopIteration]" ||
|
||
exception instanceof QReturnValue
|
||
);
|
||
}
|
||
|
||
// FIXME: Remove this helper and Q.return once ES6 generators are in
|
||
// SpiderMonkey.
|
||
var QReturnValue;
|
||
if (typeof ReturnValue !== "undefined") {
|
||
QReturnValue = ReturnValue;
|
||
} else {
|
||
QReturnValue = function (value) {
|
||
this.value = value;
|
||
};
|
||
}
|
||
|
||
// long stack traces
|
||
|
||
var STACK_JUMP_SEPARATOR = "From previous event:";
|
||
|
||
function makeStackTraceLong(error, promise) {
|
||
// If possible, transform the error stack trace by removing Node and Q
|
||
// cruft, then concatenating with the stack trace of `promise`. See #57.
|
||
if (hasStacks &&
|
||
promise.stack &&
|
||
typeof error === "object" &&
|
||
error !== null &&
|
||
error.stack &&
|
||
error.stack.indexOf(STACK_JUMP_SEPARATOR) === -1
|
||
) {
|
||
var stacks = [];
|
||
for (var p = promise; !!p; p = p.source) {
|
||
if (p.stack) {
|
||
stacks.unshift(p.stack);
|
||
}
|
||
}
|
||
stacks.unshift(error.stack);
|
||
|
||
var concatedStacks = stacks.join("\n" + STACK_JUMP_SEPARATOR + "\n");
|
||
error.stack = filterStackString(concatedStacks);
|
||
}
|
||
}
|
||
|
||
function filterStackString(stackString) {
|
||
var lines = stackString.split("\n");
|
||
var desiredLines = [];
|
||
for (var i = 0; i < lines.length; ++i) {
|
||
var line = lines[i];
|
||
|
||
if (!isInternalFrame(line) && !isNodeFrame(line) && line) {
|
||
desiredLines.push(line);
|
||
}
|
||
}
|
||
return desiredLines.join("\n");
|
||
}
|
||
|
||
function isNodeFrame(stackLine) {
|
||
return stackLine.indexOf("(module.js:") !== -1 ||
|
||
stackLine.indexOf("(node.js:") !== -1;
|
||
}
|
||
|
||
function getFileNameAndLineNumber(stackLine) {
|
||
// Named functions: "at functionName (filename:lineNumber:columnNumber)"
|
||
// In IE10 function name can have spaces ("Anonymous function") O_o
|
||
var attempt1 = /at .+ \((.+):(\d+):(?:\d+)\)$/.exec(stackLine);
|
||
if (attempt1) {
|
||
return [attempt1[1], Number(attempt1[2])];
|
||
}
|
||
|
||
// Anonymous functions: "at filename:lineNumber:columnNumber"
|
||
var attempt2 = /at ([^ ]+):(\d+):(?:\d+)$/.exec(stackLine);
|
||
if (attempt2) {
|
||
return [attempt2[1], Number(attempt2[2])];
|
||
}
|
||
|
||
// Firefox style: "function@filename:lineNumber or @filename:lineNumber"
|
||
var attempt3 = /.*@(.+):(\d+)$/.exec(stackLine);
|
||
if (attempt3) {
|
||
return [attempt3[1], Number(attempt3[2])];
|
||
}
|
||
}
|
||
|
||
function isInternalFrame(stackLine) {
|
||
var fileNameAndLineNumber = getFileNameAndLineNumber(stackLine);
|
||
|
||
if (!fileNameAndLineNumber) {
|
||
return false;
|
||
}
|
||
|
||
var fileName = fileNameAndLineNumber[0];
|
||
var lineNumber = fileNameAndLineNumber[1];
|
||
|
||
return fileName === qFileName &&
|
||
lineNumber >= qStartingLine &&
|
||
lineNumber <= qEndingLine;
|
||
}
|
||
|
||
// discover own file name and line number range for filtering stack
|
||
// traces
|
||
function captureLine() {
|
||
if (!hasStacks) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
throw new Error();
|
||
} catch (e) {
|
||
var lines = e.stack.split("\n");
|
||
var firstLine = lines[0].indexOf("@") > 0 ? lines[1] : lines[2];
|
||
var fileNameAndLineNumber = getFileNameAndLineNumber(firstLine);
|
||
if (!fileNameAndLineNumber) {
|
||
return;
|
||
}
|
||
|
||
qFileName = fileNameAndLineNumber[0];
|
||
return fileNameAndLineNumber[1];
|
||
}
|
||
}
|
||
|
||
function deprecate(callback, name, alternative) {
|
||
return function () {
|
||
if (typeof console !== "undefined" &&
|
||
typeof console.warn === "function") {
|
||
console.warn(name + " is deprecated, use " + alternative +
|
||
" instead.", new Error("").stack);
|
||
}
|
||
return callback.apply(callback, arguments);
|
||
};
|
||
}
|
||
|
||
// end of shims
|
||
// beginning of real work
|
||
|
||
/**
|
||
* Constructs a promise for an immediate reference, passes promises through, or
|
||
* coerces promises from different systems.
|
||
* @param value immediate reference or promise
|
||
*/
|
||
function Q(value) {
|
||
// If the object is already a Promise, return it directly. This enables
|
||
// the resolve function to both be used to created references from objects,
|
||
// but to tolerably coerce non-promises to promises.
|
||
if (value instanceof Promise) {
|
||
return value;
|
||
}
|
||
|
||
// assimilate thenables
|
||
if (isPromiseAlike(value)) {
|
||
return coerce(value);
|
||
} else {
|
||
return fulfill(value);
|
||
}
|
||
}
|
||
Q.resolve = Q;
|
||
|
||
/**
|
||
* Performs a task in a future turn of the event loop.
|
||
* @param {Function} task
|
||
*/
|
||
Q.nextTick = nextTick;
|
||
|
||
/**
|
||
* Controls whether or not long stack traces will be on
|
||
*/
|
||
Q.longStackSupport = false;
|
||
|
||
// enable long stacks if Q_DEBUG is set
|
||
if (typeof process === "object" && process && process.env && process.env.Q_DEBUG) {
|
||
Q.longStackSupport = true;
|
||
}
|
||
|
||
/**
|
||
* Constructs a {promise, resolve, reject} object.
|
||
*
|
||
* `resolve` is a callback to invoke with a more resolved value for the
|
||
* promise. To fulfill the promise, invoke `resolve` with any value that is
|
||
* not a thenable. To reject the promise, invoke `resolve` with a rejected
|
||
* thenable, or invoke `reject` with the reason directly. To resolve the
|
||
* promise to another thenable, thus putting it in the same state, invoke
|
||
* `resolve` with that other thenable.
|
||
*/
|
||
Q.defer = defer;
|
||
function defer() {
|
||
// if "messages" is an "Array", that indicates that the promise has not yet
|
||
// been resolved. If it is "undefined", it has been resolved. Each
|
||
// element of the messages array is itself an array of complete arguments to
|
||
// forward to the resolved promise. We coerce the resolution value to a
|
||
// promise using the `resolve` function because it handles both fully
|
||
// non-thenable values and other thenables gracefully.
|
||
var messages = [], progressListeners = [], resolvedPromise;
|
||
|
||
var deferred = object_create(defer.prototype);
|
||
var promise = object_create(Promise.prototype);
|
||
|
||
promise.promiseDispatch = function (resolve, op, operands) {
|
||
var args = array_slice(arguments);
|
||
if (messages) {
|
||
messages.push(args);
|
||
if (op === "when" && operands[1]) { // progress operand
|
||
progressListeners.push(operands[1]);
|
||
}
|
||
} else {
|
||
Q.nextTick(function () {
|
||
resolvedPromise.promiseDispatch.apply(resolvedPromise, args);
|
||
});
|
||
}
|
||
};
|
||
|
||
// XXX deprecated
|
||
promise.valueOf = function () {
|
||
if (messages) {
|
||
return promise;
|
||
}
|
||
var nearerValue = nearer(resolvedPromise);
|
||
if (isPromise(nearerValue)) {
|
||
resolvedPromise = nearerValue; // shorten chain
|
||
}
|
||
return nearerValue;
|
||
};
|
||
|
||
promise.inspect = function () {
|
||
if (!resolvedPromise) {
|
||
return { state: "pending" };
|
||
}
|
||
return resolvedPromise.inspect();
|
||
};
|
||
|
||
if (Q.longStackSupport && hasStacks) {
|
||
try {
|
||
throw new Error();
|
||
} catch (e) {
|
||
// NOTE: don't try to use `Error.captureStackTrace` or transfer the
|
||
// accessor around; that causes memory leaks as per GH-111. Just
|
||
// reify the stack trace as a string ASAP.
|
||
//
|
||
// At the same time, cut off the first line; it's always just
|
||
// "[object Promise]\n", as per the `toString`.
|
||
promise.stack = e.stack.substring(e.stack.indexOf("\n") + 1);
|
||
}
|
||
}
|
||
|
||
// NOTE: we do the checks for `resolvedPromise` in each method, instead of
|
||
// consolidating them into `become`, since otherwise we'd create new
|
||
// promises with the lines `become(whatever(value))`. See e.g. GH-252.
|
||
|
||
function become(newPromise) {
|
||
resolvedPromise = newPromise;
|
||
promise.source = newPromise;
|
||
|
||
array_reduce(messages, function (undefined, message) {
|
||
Q.nextTick(function () {
|
||
newPromise.promiseDispatch.apply(newPromise, message);
|
||
});
|
||
}, void 0);
|
||
|
||
messages = void 0;
|
||
progressListeners = void 0;
|
||
}
|
||
|
||
deferred.promise = promise;
|
||
deferred.resolve = function (value) {
|
||
if (resolvedPromise) {
|
||
return;
|
||
}
|
||
|
||
become(Q(value));
|
||
};
|
||
|
||
deferred.fulfill = function (value) {
|
||
if (resolvedPromise) {
|
||
return;
|
||
}
|
||
|
||
become(fulfill(value));
|
||
};
|
||
deferred.reject = function (reason) {
|
||
if (resolvedPromise) {
|
||
return;
|
||
}
|
||
|
||
become(reject(reason));
|
||
};
|
||
deferred.notify = function (progress) {
|
||
if (resolvedPromise) {
|
||
return;
|
||
}
|
||
|
||
array_reduce(progressListeners, function (undefined, progressListener) {
|
||
Q.nextTick(function () {
|
||
progressListener(progress);
|
||
});
|
||
}, void 0);
|
||
};
|
||
|
||
return deferred;
|
||
}
|
||
|
||
/**
|
||
* Creates a Node-style callback that will resolve or reject the deferred
|
||
* promise.
|
||
* @returns a nodeback
|
||
*/
|
||
defer.prototype.makeNodeResolver = function () {
|
||
var self = this;
|
||
return function (error, value) {
|
||
if (error) {
|
||
self.reject(error);
|
||
} else if (arguments.length > 2) {
|
||
self.resolve(array_slice(arguments, 1));
|
||
} else {
|
||
self.resolve(value);
|
||
}
|
||
};
|
||
};
|
||
|
||
/**
|
||
* @param resolver {Function} a function that returns nothing and accepts
|
||
* the resolve, reject, and notify functions for a deferred.
|
||
* @returns a promise that may be resolved with the given resolve and reject
|
||
* functions, or rejected by a thrown exception in resolver
|
||
*/
|
||
Q.Promise = promise; // ES6
|
||
Q.promise = promise;
|
||
function promise(resolver) {
|
||
if (typeof resolver !== "function") {
|
||
throw new TypeError("resolver must be a function.");
|
||
}
|
||
var deferred = defer();
|
||
try {
|
||
resolver(deferred.resolve, deferred.reject, deferred.notify);
|
||
} catch (reason) {
|
||
deferred.reject(reason);
|
||
}
|
||
return deferred.promise;
|
||
}
|
||
|
||
promise.race = race; // ES6
|
||
promise.all = all; // ES6
|
||
promise.reject = reject; // ES6
|
||
promise.resolve = Q; // ES6
|
||
|
||
// XXX experimental. This method is a way to denote that a local value is
|
||
// serializable and should be immediately dispatched to a remote upon request,
|
||
// instead of passing a reference.
|
||
Q.passByCopy = function (object) {
|
||
//freeze(object);
|
||
//passByCopies.set(object, true);
|
||
return object;
|
||
};
|
||
|
||
Promise.prototype.passByCopy = function () {
|
||
//freeze(object);
|
||
//passByCopies.set(object, true);
|
||
return this;
|
||
};
|
||
|
||
/**
|
||
* If two promises eventually fulfill to the same value, promises that value,
|
||
* but otherwise rejects.
|
||
* @param x {Any*}
|
||
* @param y {Any*}
|
||
* @returns {Any*} a promise for x and y if they are the same, but a rejection
|
||
* otherwise.
|
||
*
|
||
*/
|
||
Q.join = function (x, y) {
|
||
return Q(x).join(y);
|
||
};
|
||
|
||
Promise.prototype.join = function (that) {
|
||
return Q([this, that]).spread(function (x, y) {
|
||
if (x === y) {
|
||
// TODO: "===" should be Object.is or equiv
|
||
return x;
|
||
} else {
|
||
throw new Error("Can't join: not the same: " + x + " " + y);
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Returns a promise for the first of an array of promises to become settled.
|
||
* @param answers {Array[Any*]} promises to race
|
||
* @returns {Any*} the first promise to be settled
|
||
*/
|
||
Q.race = race;
|
||
function race(answerPs) {
|
||
return promise(function (resolve, reject) {
|
||
// Switch to this once we can assume at least ES5
|
||
// answerPs.forEach(function (answerP) {
|
||
// Q(answerP).then(resolve, reject);
|
||
// });
|
||
// Use this in the meantime
|
||
for (var i = 0, len = answerPs.length; i < len; i++) {
|
||
Q(answerPs[i]).then(resolve, reject);
|
||
}
|
||
});
|
||
}
|
||
|
||
Promise.prototype.race = function () {
|
||
return this.then(Q.race);
|
||
};
|
||
|
||
/**
|
||
* Constructs a Promise with a promise descriptor object and optional fallback
|
||
* function. The descriptor contains methods like when(rejected), get(name),
|
||
* set(name, value), post(name, args), and delete(name), which all
|
||
* return either a value, a promise for a value, or a rejection. The fallback
|
||
* accepts the operation name, a resolver, and any further arguments that would
|
||
* have been forwarded to the appropriate method above had a method been
|
||
* provided with the proper name. The API makes no guarantees about the nature
|
||
* of the returned object, apart from that it is usable whereever promises are
|
||
* bought and sold.
|
||
*/
|
||
Q.makePromise = Promise;
|
||
function Promise(descriptor, fallback, inspect) {
|
||
if (fallback === void 0) {
|
||
fallback = function (op) {
|
||
return reject(new Error(
|
||
"Promise does not support operation: " + op
|
||
));
|
||
};
|
||
}
|
||
if (inspect === void 0) {
|
||
inspect = function () {
|
||
return {state: "unknown"};
|
||
};
|
||
}
|
||
|
||
var promise = object_create(Promise.prototype);
|
||
|
||
promise.promiseDispatch = function (resolve, op, args) {
|
||
var result;
|
||
try {
|
||
if (descriptor[op]) {
|
||
result = descriptor[op].apply(promise, args);
|
||
} else {
|
||
result = fallback.call(promise, op, args);
|
||
}
|
||
} catch (exception) {
|
||
result = reject(exception);
|
||
}
|
||
if (resolve) {
|
||
resolve(result);
|
||
}
|
||
};
|
||
|
||
promise.inspect = inspect;
|
||
|
||
// XXX deprecated `valueOf` and `exception` support
|
||
if (inspect) {
|
||
var inspected = inspect();
|
||
if (inspected.state === "rejected") {
|
||
promise.exception = inspected.reason;
|
||
}
|
||
|
||
promise.valueOf = function () {
|
||
var inspected = inspect();
|
||
if (inspected.state === "pending" ||
|
||
inspected.state === "rejected") {
|
||
return promise;
|
||
}
|
||
return inspected.value;
|
||
};
|
||
}
|
||
|
||
return promise;
|
||
}
|
||
|
||
Promise.prototype.toString = function () {
|
||
return "[object Promise]";
|
||
};
|
||
|
||
Promise.prototype.then = function (fulfilled, rejected, progressed) {
|
||
var self = this;
|
||
var deferred = defer();
|
||
var done = false; // ensure the untrusted promise makes at most a
|
||
// single call to one of the callbacks
|
||
|
||
function _fulfilled(value) {
|
||
try {
|
||
return typeof fulfilled === "function" ? fulfilled(value) : value;
|
||
} catch (exception) {
|
||
return reject(exception);
|
||
}
|
||
}
|
||
|
||
function _rejected(exception) {
|
||
if (typeof rejected === "function") {
|
||
makeStackTraceLong(exception, self);
|
||
try {
|
||
return rejected(exception);
|
||
} catch (newException) {
|
||
return reject(newException);
|
||
}
|
||
}
|
||
return reject(exception);
|
||
}
|
||
|
||
function _progressed(value) {
|
||
return typeof progressed === "function" ? progressed(value) : value;
|
||
}
|
||
|
||
Q.nextTick(function () {
|
||
self.promiseDispatch(function (value) {
|
||
if (done) {
|
||
return;
|
||
}
|
||
done = true;
|
||
|
||
deferred.resolve(_fulfilled(value));
|
||
}, "when", [function (exception) {
|
||
if (done) {
|
||
return;
|
||
}
|
||
done = true;
|
||
|
||
deferred.resolve(_rejected(exception));
|
||
}]);
|
||
});
|
||
|
||
// Progress propagator need to be attached in the current tick.
|
||
self.promiseDispatch(void 0, "when", [void 0, function (value) {
|
||
var newValue;
|
||
var threw = false;
|
||
try {
|
||
newValue = _progressed(value);
|
||
} catch (e) {
|
||
threw = true;
|
||
if (Q.onerror) {
|
||
Q.onerror(e);
|
||
} else {
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
if (!threw) {
|
||
deferred.notify(newValue);
|
||
}
|
||
}]);
|
||
|
||
return deferred.promise;
|
||
};
|
||
|
||
Q.tap = function (promise, callback) {
|
||
return Q(promise).tap(callback);
|
||
};
|
||
|
||
/**
|
||
* Works almost like "finally", but not called for rejections.
|
||
* Original resolution value is passed through callback unaffected.
|
||
* Callback may return a promise that will be awaited for.
|
||
* @param {Function} callback
|
||
* @returns {Q.Promise}
|
||
* @example
|
||
* doSomething()
|
||
* .then(...)
|
||
* .tap(console.log)
|
||
* .then(...);
|
||
*/
|
||
Promise.prototype.tap = function (callback) {
|
||
callback = Q(callback);
|
||
|
||
return this.then(function (value) {
|
||
return callback.fcall(value).thenResolve(value);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Registers an observer on a promise.
|
||
*
|
||
* Guarantees:
|
||
*
|
||
* 1. that fulfilled and rejected will be called only once.
|
||
* 2. that either the fulfilled callback or the rejected callback will be
|
||
* called, but not both.
|
||
* 3. that fulfilled and rejected will not be called in this turn.
|
||
*
|
||
* @param value promise or immediate reference to observe
|
||
* @param fulfilled function to be called with the fulfilled value
|
||
* @param rejected function to be called with the rejection exception
|
||
* @param progressed function to be called on any progress notifications
|
||
* @return promise for the return value from the invoked callback
|
||
*/
|
||
Q.when = when;
|
||
function when(value, fulfilled, rejected, progressed) {
|
||
return Q(value).then(fulfilled, rejected, progressed);
|
||
}
|
||
|
||
Promise.prototype.thenResolve = function (value) {
|
||
return this.then(function () { return value; });
|
||
};
|
||
|
||
Q.thenResolve = function (promise, value) {
|
||
return Q(promise).thenResolve(value);
|
||
};
|
||
|
||
Promise.prototype.thenReject = function (reason) {
|
||
return this.then(function () { throw reason; });
|
||
};
|
||
|
||
Q.thenReject = function (promise, reason) {
|
||
return Q(promise).thenReject(reason);
|
||
};
|
||
|
||
/**
|
||
* If an object is not a promise, it is as "near" as possible.
|
||
* If a promise is rejected, it is as "near" as possible too.
|
||
* If it’s a fulfilled promise, the fulfillment value is nearer.
|
||
* If it’s a deferred promise and the deferred has been resolved, the
|
||
* resolution is "nearer".
|
||
* @param object
|
||
* @returns most resolved (nearest) form of the object
|
||
*/
|
||
|
||
// XXX should we re-do this?
|
||
Q.nearer = nearer;
|
||
function nearer(value) {
|
||
if (isPromise(value)) {
|
||
var inspected = value.inspect();
|
||
if (inspected.state === "fulfilled") {
|
||
return inspected.value;
|
||
}
|
||
}
|
||
return value;
|
||
}
|
||
|
||
/**
|
||
* @returns whether the given object is a promise.
|
||
* Otherwise it is a fulfilled value.
|
||
*/
|
||
Q.isPromise = isPromise;
|
||
function isPromise(object) {
|
||
return object instanceof Promise;
|
||
}
|
||
|
||
Q.isPromiseAlike = isPromiseAlike;
|
||
function isPromiseAlike(object) {
|
||
return isObject(object) && typeof object.then === "function";
|
||
}
|
||
|
||
/**
|
||
* @returns whether the given object is a pending promise, meaning not
|
||
* fulfilled or rejected.
|
||
*/
|
||
Q.isPending = isPending;
|
||
function isPending(object) {
|
||
return isPromise(object) && object.inspect().state === "pending";
|
||
}
|
||
|
||
Promise.prototype.isPending = function () {
|
||
return this.inspect().state === "pending";
|
||
};
|
||
|
||
/**
|
||
* @returns whether the given object is a value or fulfilled
|
||
* promise.
|
||
*/
|
||
Q.isFulfilled = isFulfilled;
|
||
function isFulfilled(object) {
|
||
return !isPromise(object) || object.inspect().state === "fulfilled";
|
||
}
|
||
|
||
Promise.prototype.isFulfilled = function () {
|
||
return this.inspect().state === "fulfilled";
|
||
};
|
||
|
||
/**
|
||
* @returns whether the given object is a rejected promise.
|
||
*/
|
||
Q.isRejected = isRejected;
|
||
function isRejected(object) {
|
||
return isPromise(object) && object.inspect().state === "rejected";
|
||
}
|
||
|
||
Promise.prototype.isRejected = function () {
|
||
return this.inspect().state === "rejected";
|
||
};
|
||
|
||
//// BEGIN UNHANDLED REJECTION TRACKING
|
||
|
||
// This promise library consumes exceptions thrown in handlers so they can be
|
||
// handled by a subsequent promise. The exceptions get added to this array when
|
||
// they are created, and removed when they are handled. Note that in ES6 or
|
||
// shimmed environments, this would naturally be a `Set`.
|
||
var unhandledReasons = [];
|
||
var unhandledRejections = [];
|
||
var reportedUnhandledRejections = [];
|
||
var trackUnhandledRejections = true;
|
||
|
||
function resetUnhandledRejections() {
|
||
unhandledReasons.length = 0;
|
||
unhandledRejections.length = 0;
|
||
|
||
if (!trackUnhandledRejections) {
|
||
trackUnhandledRejections = true;
|
||
}
|
||
}
|
||
|
||
function trackRejection(promise, reason) {
|
||
if (!trackUnhandledRejections) {
|
||
return;
|
||
}
|
||
if (typeof process === "object" && typeof process.emit === "function") {
|
||
Q.nextTick.runAfter(function () {
|
||
if (array_indexOf(unhandledRejections, promise) !== -1) {
|
||
process.emit("unhandledRejection", reason, promise);
|
||
reportedUnhandledRejections.push(promise);
|
||
}
|
||
});
|
||
}
|
||
|
||
unhandledRejections.push(promise);
|
||
if (reason && typeof reason.stack !== "undefined") {
|
||
unhandledReasons.push(reason.stack);
|
||
} else {
|
||
unhandledReasons.push("(no stack) " + reason);
|
||
}
|
||
}
|
||
|
||
function untrackRejection(promise) {
|
||
if (!trackUnhandledRejections) {
|
||
return;
|
||
}
|
||
|
||
var at = array_indexOf(unhandledRejections, promise);
|
||
if (at !== -1) {
|
||
if (typeof process === "object" && typeof process.emit === "function") {
|
||
Q.nextTick.runAfter(function () {
|
||
var atReport = array_indexOf(reportedUnhandledRejections, promise);
|
||
if (atReport !== -1) {
|
||
process.emit("rejectionHandled", unhandledReasons[at], promise);
|
||
reportedUnhandledRejections.splice(atReport, 1);
|
||
}
|
||
});
|
||
}
|
||
unhandledRejections.splice(at, 1);
|
||
unhandledReasons.splice(at, 1);
|
||
}
|
||
}
|
||
|
||
Q.resetUnhandledRejections = resetUnhandledRejections;
|
||
|
||
Q.getUnhandledReasons = function () {
|
||
// Make a copy so that consumers can't interfere with our internal state.
|
||
return unhandledReasons.slice();
|
||
};
|
||
|
||
Q.stopUnhandledRejectionTracking = function () {
|
||
resetUnhandledRejections();
|
||
trackUnhandledRejections = false;
|
||
};
|
||
|
||
resetUnhandledRejections();
|
||
|
||
//// END UNHANDLED REJECTION TRACKING
|
||
|
||
/**
|
||
* Constructs a rejected promise.
|
||
* @param reason value describing the failure
|
||
*/
|
||
Q.reject = reject;
|
||
function reject(reason) {
|
||
var rejection = Promise({
|
||
"when": function (rejected) {
|
||
// note that the error has been handled
|
||
if (rejected) {
|
||
untrackRejection(this);
|
||
}
|
||
return rejected ? rejected(reason) : this;
|
||
}
|
||
}, function fallback() {
|
||
return this;
|
||
}, function inspect() {
|
||
return { state: "rejected", reason: reason };
|
||
});
|
||
|
||
// Note that the reason has not been handled.
|
||
trackRejection(rejection, reason);
|
||
|
||
return rejection;
|
||
}
|
||
|
||
/**
|
||
* Constructs a fulfilled promise for an immediate reference.
|
||
* @param value immediate reference
|
||
*/
|
||
Q.fulfill = fulfill;
|
||
function fulfill(value) {
|
||
return Promise({
|
||
"when": function () {
|
||
return value;
|
||
},
|
||
"get": function (name) {
|
||
return value[name];
|
||
},
|
||
"set": function (name, rhs) {
|
||
value[name] = rhs;
|
||
},
|
||
"delete": function (name) {
|
||
delete value[name];
|
||
},
|
||
"post": function (name, args) {
|
||
// Mark Miller proposes that post with no name should apply a
|
||
// promised function.
|
||
if (name === null || name === void 0) {
|
||
return value.apply(void 0, args);
|
||
} else {
|
||
return value[name].apply(value, args);
|
||
}
|
||
},
|
||
"apply": function (thisp, args) {
|
||
return value.apply(thisp, args);
|
||
},
|
||
"keys": function () {
|
||
return object_keys(value);
|
||
}
|
||
}, void 0, function inspect() {
|
||
return { state: "fulfilled", value: value };
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Converts thenables to Q promises.
|
||
* @param promise thenable promise
|
||
* @returns a Q promise
|
||
*/
|
||
function coerce(promise) {
|
||
var deferred = defer();
|
||
Q.nextTick(function () {
|
||
try {
|
||
promise.then(deferred.resolve, deferred.reject, deferred.notify);
|
||
} catch (exception) {
|
||
deferred.reject(exception);
|
||
}
|
||
});
|
||
return deferred.promise;
|
||
}
|
||
|
||
/**
|
||
* Annotates an object such that it will never be
|
||
* transferred away from this process over any promise
|
||
* communication channel.
|
||
* @param object
|
||
* @returns promise a wrapping of that object that
|
||
* additionally responds to the "isDef" message
|
||
* without a rejection.
|
||
*/
|
||
Q.master = master;
|
||
function master(object) {
|
||
return Promise({
|
||
"isDef": function () {}
|
||
}, function fallback(op, args) {
|
||
return dispatch(object, op, args);
|
||
}, function () {
|
||
return Q(object).inspect();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Spreads the values of a promised array of arguments into the
|
||
* fulfillment callback.
|
||
* @param fulfilled callback that receives variadic arguments from the
|
||
* promised array
|
||
* @param rejected callback that receives the exception if the promise
|
||
* is rejected.
|
||
* @returns a promise for the return value or thrown exception of
|
||
* either callback.
|
||
*/
|
||
Q.spread = spread;
|
||
function spread(value, fulfilled, rejected) {
|
||
return Q(value).spread(fulfilled, rejected);
|
||
}
|
||
|
||
Promise.prototype.spread = function (fulfilled, rejected) {
|
||
return this.all().then(function (array) {
|
||
return fulfilled.apply(void 0, array);
|
||
}, rejected);
|
||
};
|
||
|
||
/**
|
||
* The async function is a decorator for generator functions, turning
|
||
* them into asynchronous generators. Although generators are only part
|
||
* of the newest ECMAScript 6 drafts, this code does not cause syntax
|
||
* errors in older engines. This code should continue to work and will
|
||
* in fact improve over time as the language improves.
|
||
*
|
||
* ES6 generators are currently part of V8 version 3.19 with the
|
||
* --harmony-generators runtime flag enabled. SpiderMonkey has had them
|
||
* for longer, but under an older Python-inspired form. This function
|
||
* works on both kinds of generators.
|
||
*
|
||
* Decorates a generator function such that:
|
||
* - it may yield promises
|
||
* - execution will continue when that promise is fulfilled
|
||
* - the value of the yield expression will be the fulfilled value
|
||
* - it returns a promise for the return value (when the generator
|
||
* stops iterating)
|
||
* - the decorated function returns a promise for the return value
|
||
* of the generator or the first rejected promise among those
|
||
* yielded.
|
||
* - if an error is thrown in the generator, it propagates through
|
||
* every following yield until it is caught, or until it escapes
|
||
* the generator function altogether, and is translated into a
|
||
* rejection for the promise returned by the decorated generator.
|
||
*/
|
||
Q.async = async;
|
||
function async(makeGenerator) {
|
||
return function () {
|
||
// when verb is "send", arg is a value
|
||
// when verb is "throw", arg is an exception
|
||
function continuer(verb, arg) {
|
||
var result;
|
||
|
||
// Until V8 3.19 / Chromium 29 is released, SpiderMonkey is the only
|
||
// engine that has a deployed base of browsers that support generators.
|
||
// However, SM's generators use the Python-inspired semantics of
|
||
// outdated ES6 drafts. We would like to support ES6, but we'd also
|
||
// like to make it possible to use generators in deployed browsers, so
|
||
// we also support Python-style generators. At some point we can remove
|
||
// this block.
|
||
|
||
if (typeof StopIteration === "undefined") {
|
||
// ES6 Generators
|
||
try {
|
||
result = generator[verb](arg);
|
||
} catch (exception) {
|
||
return reject(exception);
|
||
}
|
||
if (result.done) {
|
||
return Q(result.value);
|
||
} else {
|
||
return when(result.value, callback, errback);
|
||
}
|
||
} else {
|
||
// SpiderMonkey Generators
|
||
// FIXME: Remove this case when SM does ES6 generators.
|
||
try {
|
||
result = generator[verb](arg);
|
||
} catch (exception) {
|
||
if (isStopIteration(exception)) {
|
||
return Q(exception.value);
|
||
} else {
|
||
return reject(exception);
|
||
}
|
||
}
|
||
return when(result, callback, errback);
|
||
}
|
||
}
|
||
var generator = makeGenerator.apply(this, arguments);
|
||
var callback = continuer.bind(continuer, "next");
|
||
var errback = continuer.bind(continuer, "throw");
|
||
return callback();
|
||
};
|
||
}
|
||
|
||
/**
|
||
* The spawn function is a small wrapper around async that immediately
|
||
* calls the generator and also ends the promise chain, so that any
|
||
* unhandled errors are thrown instead of forwarded to the error
|
||
* handler. This is useful because it's extremely common to run
|
||
* generators at the top-level to work with libraries.
|
||
*/
|
||
Q.spawn = spawn;
|
||
function spawn(makeGenerator) {
|
||
Q.done(Q.async(makeGenerator)());
|
||
}
|
||
|
||
// FIXME: Remove this interface once ES6 generators are in SpiderMonkey.
|
||
/**
|
||
* Throws a ReturnValue exception to stop an asynchronous generator.
|
||
*
|
||
* This interface is a stop-gap measure to support generator return
|
||
* values in older Firefox/SpiderMonkey. In browsers that support ES6
|
||
* generators like Chromium 29, just use "return" in your generator
|
||
* functions.
|
||
*
|
||
* @param value the return value for the surrounding generator
|
||
* @throws ReturnValue exception with the value.
|
||
* @example
|
||
* // ES6 style
|
||
* Q.async(function* () {
|
||
* var foo = yield getFooPromise();
|
||
* var bar = yield getBarPromise();
|
||
* return foo + bar;
|
||
* })
|
||
* // Older SpiderMonkey style
|
||
* Q.async(function () {
|
||
* var foo = yield getFooPromise();
|
||
* var bar = yield getBarPromise();
|
||
* Q.return(foo + bar);
|
||
* })
|
||
*/
|
||
Q["return"] = _return;
|
||
function _return(value) {
|
||
throw new QReturnValue(value);
|
||
}
|
||
|
||
/**
|
||
* The promised function decorator ensures that any promise arguments
|
||
* are settled and passed as values (`this` is also settled and passed
|
||
* as a value). It will also ensure that the result of a function is
|
||
* always a promise.
|
||
*
|
||
* @example
|
||
* var add = Q.promised(function (a, b) {
|
||
* return a + b;
|
||
* });
|
||
* add(Q(a), Q(B));
|
||
*
|
||
* @param {function} callback The function to decorate
|
||
* @returns {function} a function that has been decorated.
|
||
*/
|
||
Q.promised = promised;
|
||
function promised(callback) {
|
||
return function () {
|
||
return spread([this, all(arguments)], function (self, args) {
|
||
return callback.apply(self, args);
|
||
});
|
||
};
|
||
}
|
||
|
||
/**
|
||
* sends a message to a value in a future turn
|
||
* @param object* the recipient
|
||
* @param op the name of the message operation, e.g., "when",
|
||
* @param args further arguments to be forwarded to the operation
|
||
* @returns result {Promise} a promise for the result of the operation
|
||
*/
|
||
Q.dispatch = dispatch;
|
||
function dispatch(object, op, args) {
|
||
return Q(object).dispatch(op, args);
|
||
}
|
||
|
||
Promise.prototype.dispatch = function (op, args) {
|
||
var self = this;
|
||
var deferred = defer();
|
||
Q.nextTick(function () {
|
||
self.promiseDispatch(deferred.resolve, op, args);
|
||
});
|
||
return deferred.promise;
|
||
};
|
||
|
||
/**
|
||
* Gets the value of a property in a future turn.
|
||
* @param object promise or immediate reference for target object
|
||
* @param name name of property to get
|
||
* @return promise for the property value
|
||
*/
|
||
Q.get = function (object, key) {
|
||
return Q(object).dispatch("get", [key]);
|
||
};
|
||
|
||
Promise.prototype.get = function (key) {
|
||
return this.dispatch("get", [key]);
|
||
};
|
||
|
||
/**
|
||
* Sets the value of a property in a future turn.
|
||
* @param object promise or immediate reference for object object
|
||
* @param name name of property to set
|
||
* @param value new value of property
|
||
* @return promise for the return value
|
||
*/
|
||
Q.set = function (object, key, value) {
|
||
return Q(object).dispatch("set", [key, value]);
|
||
};
|
||
|
||
Promise.prototype.set = function (key, value) {
|
||
return this.dispatch("set", [key, value]);
|
||
};
|
||
|
||
/**
|
||
* Deletes a property in a future turn.
|
||
* @param object promise or immediate reference for target object
|
||
* @param name name of property to delete
|
||
* @return promise for the return value
|
||
*/
|
||
Q.del = // XXX legacy
|
||
Q["delete"] = function (object, key) {
|
||
return Q(object).dispatch("delete", [key]);
|
||
};
|
||
|
||
Promise.prototype.del = // XXX legacy
|
||
Promise.prototype["delete"] = function (key) {
|
||
return this.dispatch("delete", [key]);
|
||
};
|
||
|
||
/**
|
||
* Invokes a method in a future turn.
|
||
* @param object promise or immediate reference for target object
|
||
* @param name name of method to invoke
|
||
* @param value a value to post, typically an array of
|
||
* invocation arguments for promises that
|
||
* are ultimately backed with `resolve` values,
|
||
* as opposed to those backed with URLs
|
||
* wherein the posted value can be any
|
||
* JSON serializable object.
|
||
* @return promise for the return value
|
||
*/
|
||
// bound locally because it is used by other methods
|
||
Q.mapply = // XXX As proposed by "Redsandro"
|
||
Q.post = function (object, name, args) {
|
||
return Q(object).dispatch("post", [name, args]);
|
||
};
|
||
|
||
Promise.prototype.mapply = // XXX As proposed by "Redsandro"
|
||
Promise.prototype.post = function (name, args) {
|
||
return this.dispatch("post", [name, args]);
|
||
};
|
||
|
||
/**
|
||
* Invokes a method in a future turn.
|
||
* @param object promise or immediate reference for target object
|
||
* @param name name of method to invoke
|
||
* @param ...args array of invocation arguments
|
||
* @return promise for the return value
|
||
*/
|
||
Q.send = // XXX Mark Miller's proposed parlance
|
||
Q.mcall = // XXX As proposed by "Redsandro"
|
||
Q.invoke = function (object, name /*...args*/) {
|
||
return Q(object).dispatch("post", [name, array_slice(arguments, 2)]);
|
||
};
|
||
|
||
Promise.prototype.send = // XXX Mark Miller's proposed parlance
|
||
Promise.prototype.mcall = // XXX As proposed by "Redsandro"
|
||
Promise.prototype.invoke = function (name /*...args*/) {
|
||
return this.dispatch("post", [name, array_slice(arguments, 1)]);
|
||
};
|
||
|
||
/**
|
||
* Applies the promised function in a future turn.
|
||
* @param object promise or immediate reference for target function
|
||
* @param args array of application arguments
|
||
*/
|
||
Q.fapply = function (object, args) {
|
||
return Q(object).dispatch("apply", [void 0, args]);
|
||
};
|
||
|
||
Promise.prototype.fapply = function (args) {
|
||
return this.dispatch("apply", [void 0, args]);
|
||
};
|
||
|
||
/**
|
||
* Calls the promised function in a future turn.
|
||
* @param object promise or immediate reference for target function
|
||
* @param ...args array of application arguments
|
||
*/
|
||
Q["try"] =
|
||
Q.fcall = function (object /* ...args*/) {
|
||
return Q(object).dispatch("apply", [void 0, array_slice(arguments, 1)]);
|
||
};
|
||
|
||
Promise.prototype.fcall = function (/*...args*/) {
|
||
return this.dispatch("apply", [void 0, array_slice(arguments)]);
|
||
};
|
||
|
||
/**
|
||
* Binds the promised function, transforming return values into a fulfilled
|
||
* promise and thrown errors into a rejected one.
|
||
* @param object promise or immediate reference for target function
|
||
* @param ...args array of application arguments
|
||
*/
|
||
Q.fbind = function (object /*...args*/) {
|
||
var promise = Q(object);
|
||
var args = array_slice(arguments, 1);
|
||
return function fbound() {
|
||
return promise.dispatch("apply", [
|
||
this,
|
||
args.concat(array_slice(arguments))
|
||
]);
|
||
};
|
||
};
|
||
Promise.prototype.fbind = function (/*...args*/) {
|
||
var promise = this;
|
||
var args = array_slice(arguments);
|
||
return function fbound() {
|
||
return promise.dispatch("apply", [
|
||
this,
|
||
args.concat(array_slice(arguments))
|
||
]);
|
||
};
|
||
};
|
||
|
||
/**
|
||
* Requests the names of the owned properties of a promised
|
||
* object in a future turn.
|
||
* @param object promise or immediate reference for target object
|
||
* @return promise for the keys of the eventually settled object
|
||
*/
|
||
Q.keys = function (object) {
|
||
return Q(object).dispatch("keys", []);
|
||
};
|
||
|
||
Promise.prototype.keys = function () {
|
||
return this.dispatch("keys", []);
|
||
};
|
||
|
||
/**
|
||
* Turns an array of promises into a promise for an array. If any of
|
||
* the promises gets rejected, the whole array is rejected immediately.
|
||
* @param {Array*} an array (or promise for an array) of values (or
|
||
* promises for values)
|
||
* @returns a promise for an array of the corresponding values
|
||
*/
|
||
// By Mark Miller
|
||
// http://wiki.ecmascript.org/doku.php?id=strawman:concurrency&rev=1308776521#allfulfilled
|
||
Q.all = all;
|
||
function all(promises) {
|
||
return when(promises, function (promises) {
|
||
var pendingCount = 0;
|
||
var deferred = defer();
|
||
array_reduce(promises, function (undefined, promise, index) {
|
||
var snapshot;
|
||
if (
|
||
isPromise(promise) &&
|
||
(snapshot = promise.inspect()).state === "fulfilled"
|
||
) {
|
||
promises[index] = snapshot.value;
|
||
} else {
|
||
++pendingCount;
|
||
when(
|
||
promise,
|
||
function (value) {
|
||
promises[index] = value;
|
||
if (--pendingCount === 0) {
|
||
deferred.resolve(promises);
|
||
}
|
||
},
|
||
deferred.reject,
|
||
function (progress) {
|
||
deferred.notify({ index: index, value: progress });
|
||
}
|
||
);
|
||
}
|
||
}, void 0);
|
||
if (pendingCount === 0) {
|
||
deferred.resolve(promises);
|
||
}
|
||
return deferred.promise;
|
||
});
|
||
}
|
||
|
||
Promise.prototype.all = function () {
|
||
return all(this);
|
||
};
|
||
|
||
/**
|
||
* Returns the first resolved promise of an array. Prior rejected promises are
|
||
* ignored. Rejects only if all promises are rejected.
|
||
* @param {Array*} an array containing values or promises for values
|
||
* @returns a promise fulfilled with the value of the first resolved promise,
|
||
* or a rejected promise if all promises are rejected.
|
||
*/
|
||
Q.any = any;
|
||
|
||
function any(promises) {
|
||
if (promises.length === 0) {
|
||
return Q.resolve();
|
||
}
|
||
|
||
var deferred = Q.defer();
|
||
var pendingCount = 0;
|
||
array_reduce(promises, function (prev, current, index) {
|
||
var promise = promises[index];
|
||
|
||
pendingCount++;
|
||
|
||
when(promise, onFulfilled, onRejected, onProgress);
|
||
function onFulfilled(result) {
|
||
deferred.resolve(result);
|
||
}
|
||
function onRejected() {
|
||
pendingCount--;
|
||
if (pendingCount === 0) {
|
||
deferred.reject(new Error(
|
||
"Can't get fulfillment value from any promise, all " +
|
||
"promises were rejected."
|
||
));
|
||
}
|
||
}
|
||
function onProgress(progress) {
|
||
deferred.notify({
|
||
index: index,
|
||
value: progress
|
||
});
|
||
}
|
||
}, undefined);
|
||
|
||
return deferred.promise;
|
||
}
|
||
|
||
Promise.prototype.any = function () {
|
||
return any(this);
|
||
};
|
||
|
||
/**
|
||
* Waits for all promises to be settled, either fulfilled or
|
||
* rejected. This is distinct from `all` since that would stop
|
||
* waiting at the first rejection. The promise returned by
|
||
* `allResolved` will never be rejected.
|
||
* @param promises a promise for an array (or an array) of promises
|
||
* (or values)
|
||
* @return a promise for an array of promises
|
||
*/
|
||
Q.allResolved = deprecate(allResolved, "allResolved", "allSettled");
|
||
function allResolved(promises) {
|
||
return when(promises, function (promises) {
|
||
promises = array_map(promises, Q);
|
||
return when(all(array_map(promises, function (promise) {
|
||
return when(promise, noop, noop);
|
||
})), function () {
|
||
return promises;
|
||
});
|
||
});
|
||
}
|
||
|
||
Promise.prototype.allResolved = function () {
|
||
return allResolved(this);
|
||
};
|
||
|
||
/**
|
||
* @see Promise#allSettled
|
||
*/
|
||
Q.allSettled = allSettled;
|
||
function allSettled(promises) {
|
||
return Q(promises).allSettled();
|
||
}
|
||
|
||
/**
|
||
* Turns an array of promises into a promise for an array of their states (as
|
||
* returned by `inspect`) when they have all settled.
|
||
* @param {Array[Any*]} values an array (or promise for an array) of values (or
|
||
* promises for values)
|
||
* @returns {Array[State]} an array of states for the respective values.
|
||
*/
|
||
Promise.prototype.allSettled = function () {
|
||
return this.then(function (promises) {
|
||
return all(array_map(promises, function (promise) {
|
||
promise = Q(promise);
|
||
function regardless() {
|
||
return promise.inspect();
|
||
}
|
||
return promise.then(regardless, regardless);
|
||
}));
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Captures the failure of a promise, giving an oportunity to recover
|
||
* with a callback. If the given promise is fulfilled, the returned
|
||
* promise is fulfilled.
|
||
* @param {Any*} promise for something
|
||
* @param {Function} callback to fulfill the returned promise if the
|
||
* given promise is rejected
|
||
* @returns a promise for the return value of the callback
|
||
*/
|
||
Q.fail = // XXX legacy
|
||
Q["catch"] = function (object, rejected) {
|
||
return Q(object).then(void 0, rejected);
|
||
};
|
||
|
||
Promise.prototype.fail = // XXX legacy
|
||
Promise.prototype["catch"] = function (rejected) {
|
||
return this.then(void 0, rejected);
|
||
};
|
||
|
||
/**
|
||
* Attaches a listener that can respond to progress notifications from a
|
||
* promise's originating deferred. This listener receives the exact arguments
|
||
* passed to ``deferred.notify``.
|
||
* @param {Any*} promise for something
|
||
* @param {Function} callback to receive any progress notifications
|
||
* @returns the given promise, unchanged
|
||
*/
|
||
Q.progress = progress;
|
||
function progress(object, progressed) {
|
||
return Q(object).then(void 0, void 0, progressed);
|
||
}
|
||
|
||
Promise.prototype.progress = function (progressed) {
|
||
return this.then(void 0, void 0, progressed);
|
||
};
|
||
|
||
/**
|
||
* Provides an opportunity to observe the settling of a promise,
|
||
* regardless of whether the promise is fulfilled or rejected. Forwards
|
||
* the resolution to the returned promise when the callback is done.
|
||
* The callback can return a promise to defer completion.
|
||
* @param {Any*} promise
|
||
* @param {Function} callback to observe the resolution of the given
|
||
* promise, takes no arguments.
|
||
* @returns a promise for the resolution of the given promise when
|
||
* ``fin`` is done.
|
||
*/
|
||
Q.fin = // XXX legacy
|
||
Q["finally"] = function (object, callback) {
|
||
return Q(object)["finally"](callback);
|
||
};
|
||
|
||
Promise.prototype.fin = // XXX legacy
|
||
Promise.prototype["finally"] = function (callback) {
|
||
callback = Q(callback);
|
||
return this.then(function (value) {
|
||
return callback.fcall().then(function () {
|
||
return value;
|
||
});
|
||
}, function (reason) {
|
||
// TODO attempt to recycle the rejection with "this".
|
||
return callback.fcall().then(function () {
|
||
throw reason;
|
||
});
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Terminates a chain of promises, forcing rejections to be
|
||
* thrown as exceptions.
|
||
* @param {Any*} promise at the end of a chain of promises
|
||
* @returns nothing
|
||
*/
|
||
Q.done = function (object, fulfilled, rejected, progress) {
|
||
return Q(object).done(fulfilled, rejected, progress);
|
||
};
|
||
|
||
Promise.prototype.done = function (fulfilled, rejected, progress) {
|
||
var onUnhandledError = function (error) {
|
||
// forward to a future turn so that ``when``
|
||
// does not catch it and turn it into a rejection.
|
||
Q.nextTick(function () {
|
||
makeStackTraceLong(error, promise);
|
||
if (Q.onerror) {
|
||
Q.onerror(error);
|
||
} else {
|
||
throw error;
|
||
}
|
||
});
|
||
};
|
||
|
||
// Avoid unnecessary `nextTick`ing via an unnecessary `when`.
|
||
var promise = fulfilled || rejected || progress ?
|
||
this.then(fulfilled, rejected, progress) :
|
||
this;
|
||
|
||
if (typeof process === "object" && process && process.domain) {
|
||
onUnhandledError = process.domain.bind(onUnhandledError);
|
||
}
|
||
|
||
promise.then(void 0, onUnhandledError);
|
||
};
|
||
|
||
/**
|
||
* Causes a promise to be rejected if it does not get fulfilled before
|
||
* some milliseconds time out.
|
||
* @param {Any*} promise
|
||
* @param {Number} milliseconds timeout
|
||
* @param {Any*} custom error message or Error object (optional)
|
||
* @returns a promise for the resolution of the given promise if it is
|
||
* fulfilled before the timeout, otherwise rejected.
|
||
*/
|
||
Q.timeout = function (object, ms, error) {
|
||
return Q(object).timeout(ms, error);
|
||
};
|
||
|
||
Promise.prototype.timeout = function (ms, error) {
|
||
var deferred = defer();
|
||
var timeoutId = setTimeout(function () {
|
||
if (!error || "string" === typeof error) {
|
||
error = new Error(error || "Timed out after " + ms + " ms");
|
||
error.code = "ETIMEDOUT";
|
||
}
|
||
deferred.reject(error);
|
||
}, ms);
|
||
|
||
this.then(function (value) {
|
||
clearTimeout(timeoutId);
|
||
deferred.resolve(value);
|
||
}, function (exception) {
|
||
clearTimeout(timeoutId);
|
||
deferred.reject(exception);
|
||
}, deferred.notify);
|
||
|
||
return deferred.promise;
|
||
};
|
||
|
||
/**
|
||
* Returns a promise for the given value (or promised value), some
|
||
* milliseconds after it resolved. Passes rejections immediately.
|
||
* @param {Any*} promise
|
||
* @param {Number} milliseconds
|
||
* @returns a promise for the resolution of the given promise after milliseconds
|
||
* time has elapsed since the resolution of the given promise.
|
||
* If the given promise rejects, that is passed immediately.
|
||
*/
|
||
Q.delay = function (object, timeout) {
|
||
if (timeout === void 0) {
|
||
timeout = object;
|
||
object = void 0;
|
||
}
|
||
return Q(object).delay(timeout);
|
||
};
|
||
|
||
Promise.prototype.delay = function (timeout) {
|
||
return this.then(function (value) {
|
||
var deferred = defer();
|
||
setTimeout(function () {
|
||
deferred.resolve(value);
|
||
}, timeout);
|
||
return deferred.promise;
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Passes a continuation to a Node function, which is called with the given
|
||
* arguments provided as an array, and returns a promise.
|
||
*
|
||
* Q.nfapply(FS.readFile, [__filename])
|
||
* .then(function (content) {
|
||
* })
|
||
*
|
||
*/
|
||
Q.nfapply = function (callback, args) {
|
||
return Q(callback).nfapply(args);
|
||
};
|
||
|
||
Promise.prototype.nfapply = function (args) {
|
||
var deferred = defer();
|
||
var nodeArgs = array_slice(args);
|
||
nodeArgs.push(deferred.makeNodeResolver());
|
||
this.fapply(nodeArgs).fail(deferred.reject);
|
||
return deferred.promise;
|
||
};
|
||
|
||
/**
|
||
* Passes a continuation to a Node function, which is called with the given
|
||
* arguments provided individually, and returns a promise.
|
||
* @example
|
||
* Q.nfcall(FS.readFile, __filename)
|
||
* .then(function (content) {
|
||
* })
|
||
*
|
||
*/
|
||
Q.nfcall = function (callback /*...args*/) {
|
||
var args = array_slice(arguments, 1);
|
||
return Q(callback).nfapply(args);
|
||
};
|
||
|
||
Promise.prototype.nfcall = function (/*...args*/) {
|
||
var nodeArgs = array_slice(arguments);
|
||
var deferred = defer();
|
||
nodeArgs.push(deferred.makeNodeResolver());
|
||
this.fapply(nodeArgs).fail(deferred.reject);
|
||
return deferred.promise;
|
||
};
|
||
|
||
/**
|
||
* Wraps a NodeJS continuation passing function and returns an equivalent
|
||
* version that returns a promise.
|
||
* @example
|
||
* Q.nfbind(FS.readFile, __filename)("utf-8")
|
||
* .then(console.log)
|
||
* .done()
|
||
*/
|
||
Q.nfbind =
|
||
Q.denodeify = function (callback /*...args*/) {
|
||
var baseArgs = array_slice(arguments, 1);
|
||
return function () {
|
||
var nodeArgs = baseArgs.concat(array_slice(arguments));
|
||
var deferred = defer();
|
||
nodeArgs.push(deferred.makeNodeResolver());
|
||
Q(callback).fapply(nodeArgs).fail(deferred.reject);
|
||
return deferred.promise;
|
||
};
|
||
};
|
||
|
||
Promise.prototype.nfbind =
|
||
Promise.prototype.denodeify = function (/*...args*/) {
|
||
var args = array_slice(arguments);
|
||
args.unshift(this);
|
||
return Q.denodeify.apply(void 0, args);
|
||
};
|
||
|
||
Q.nbind = function (callback, thisp /*...args*/) {
|
||
var baseArgs = array_slice(arguments, 2);
|
||
return function () {
|
||
var nodeArgs = baseArgs.concat(array_slice(arguments));
|
||
var deferred = defer();
|
||
nodeArgs.push(deferred.makeNodeResolver());
|
||
function bound() {
|
||
return callback.apply(thisp, arguments);
|
||
}
|
||
Q(bound).fapply(nodeArgs).fail(deferred.reject);
|
||
return deferred.promise;
|
||
};
|
||
};
|
||
|
||
Promise.prototype.nbind = function (/*thisp, ...args*/) {
|
||
var args = array_slice(arguments, 0);
|
||
args.unshift(this);
|
||
return Q.nbind.apply(void 0, args);
|
||
};
|
||
|
||
/**
|
||
* Calls a method of a Node-style object that accepts a Node-style
|
||
* callback with a given array of arguments, plus a provided callback.
|
||
* @param object an object that has the named method
|
||
* @param {String} name name of the method of object
|
||
* @param {Array} args arguments to pass to the method; the callback
|
||
* will be provided by Q and appended to these arguments.
|
||
* @returns a promise for the value or error
|
||
*/
|
||
Q.nmapply = // XXX As proposed by "Redsandro"
|
||
Q.npost = function (object, name, args) {
|
||
return Q(object).npost(name, args);
|
||
};
|
||
|
||
Promise.prototype.nmapply = // XXX As proposed by "Redsandro"
|
||
Promise.prototype.npost = function (name, args) {
|
||
var nodeArgs = array_slice(args || []);
|
||
var deferred = defer();
|
||
nodeArgs.push(deferred.makeNodeResolver());
|
||
this.dispatch("post", [name, nodeArgs]).fail(deferred.reject);
|
||
return deferred.promise;
|
||
};
|
||
|
||
/**
|
||
* Calls a method of a Node-style object that accepts a Node-style
|
||
* callback, forwarding the given variadic arguments, plus a provided
|
||
* callback argument.
|
||
* @param object an object that has the named method
|
||
* @param {String} name name of the method of object
|
||
* @param ...args arguments to pass to the method; the callback will
|
||
* be provided by Q and appended to these arguments.
|
||
* @returns a promise for the value or error
|
||
*/
|
||
Q.nsend = // XXX Based on Mark Miller's proposed "send"
|
||
Q.nmcall = // XXX Based on "Redsandro's" proposal
|
||
Q.ninvoke = function (object, name /*...args*/) {
|
||
var nodeArgs = array_slice(arguments, 2);
|
||
var deferred = defer();
|
||
nodeArgs.push(deferred.makeNodeResolver());
|
||
Q(object).dispatch("post", [name, nodeArgs]).fail(deferred.reject);
|
||
return deferred.promise;
|
||
};
|
||
|
||
Promise.prototype.nsend = // XXX Based on Mark Miller's proposed "send"
|
||
Promise.prototype.nmcall = // XXX Based on "Redsandro's" proposal
|
||
Promise.prototype.ninvoke = function (name /*...args*/) {
|
||
var nodeArgs = array_slice(arguments, 1);
|
||
var deferred = defer();
|
||
nodeArgs.push(deferred.makeNodeResolver());
|
||
this.dispatch("post", [name, nodeArgs]).fail(deferred.reject);
|
||
return deferred.promise;
|
||
};
|
||
|
||
/**
|
||
* If a function would like to support both Node continuation-passing-style and
|
||
* promise-returning-style, it can end its internal promise chain with
|
||
* `nodeify(nodeback)`, forwarding the optional nodeback argument. If the user
|
||
* elects to use a nodeback, the result will be sent there. If they do not
|
||
* pass a nodeback, they will receive the result promise.
|
||
* @param object a result (or a promise for a result)
|
||
* @param {Function} nodeback a Node.js-style callback
|
||
* @returns either the promise or nothing
|
||
*/
|
||
Q.nodeify = nodeify;
|
||
function nodeify(object, nodeback) {
|
||
return Q(object).nodeify(nodeback);
|
||
}
|
||
|
||
Promise.prototype.nodeify = function (nodeback) {
|
||
if (nodeback) {
|
||
this.then(function (value) {
|
||
Q.nextTick(function () {
|
||
nodeback(null, value);
|
||
});
|
||
}, function (error) {
|
||
Q.nextTick(function () {
|
||
nodeback(error);
|
||
});
|
||
});
|
||
} else {
|
||
return this;
|
||
}
|
||
};
|
||
|
||
Q.noConflict = function() {
|
||
throw new Error("Q.noConflict only works when Q is used as a global");
|
||
};
|
||
|
||
// All code before this point will be filtered from stack traces.
|
||
var qEndingLine = captureLine();
|
||
|
||
return Q;
|
||
|
||
});
|
||
|
||
}).call(this,require('_process'))
|
||
},{"_process":22}]},{},[1]);
|