1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00
Files
matrix-js-sdk/dist/0.3.0/browser-matrix-0.3.0.js
2015-10-28 16:54:07 +00:00

10865 lines
353 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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>&lt;video&gt;</code> DOM element
* to render video to.
* @param {Element} localVideoElement a <code>&lt;video&gt;</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>&lt;video&gt;</code> DOM element
* to render video to.
* @param {Element} localVideoElement a <code>&lt;video&gt;</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>&lt;video&gt;</code> DOM element.
* @return {Element} The dom element
*/
MatrixCall.prototype.getLocalVideoElement = function() {
return this.localVideoElement;
};
/**
* Retrieve the remote <code>&lt;video&gt;</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>&lt;audio&gt;</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>&lt;video&gt;</code> DOM element. If this call is active,
* video will be rendered to it immediately.
* @param {Element} element The <code>&lt;video&gt;</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>&lt;video&gt;</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>&lt;video&gt;</code> DOM element.
*/
MatrixCall.prototype.setRemoteVideoElement = function(element) {
this.remoteVideoElement = element;
_tryPlayRemoteStream(this);
};
/**
* Set the remote <code>&lt;audio&gt;</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>&lt;video&gt;</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 dont 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 Millers 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 its a fulfilled promise, the fulfillment value is nearer.
* If its 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]);