(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;orequire("request") * as it returns a function which meets the required interface. See * {@link requestFunction} for more information. * @param {string} opts.accessToken The access_token for this user. * @param {string} opts.userId The user ID for this user. * @param {Object} opts.store 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 * ?user_id=. * {@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: {room_id: {string}, * room_alias: {string(opt)}} * @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 returned Room object will have no current state. * 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. This method is experimental and * may change. * @param {string} mxcUrl The MXC URL * @param {Number} width The desired width of the thumbnail. * @param {Number} height The desired height of the thumbnail. * @param {string} resizeMethod The thumbnail resize method to use, either * "crop" or "scale". * @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, Room.oldState.paginationToken will be * null. * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.scrollback = function(room, limit, callback) { if (utils.isFunction(limit)) { callback = limit; limit = undefined; } limit = limit || 30; 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} 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 limit= to apply * to initial sync. Default: 8. * @param {Boolean} opts.includeArchivedRooms True to put archived=true * on the /initialSync request. Default: false. * @param {Boolean} opts.resolveInvitesToProfiles True to do /profile requests * on every invite event if the displayname/avatar_url is not known for this user ID. * Default: false. */ 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. This * method is experimental and may change. * @return {string} A new client secret */ MatrixClient.prototype.generateClientSecret = function() { var ret = ""; var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for (var i = 0; i < 32; i++) { ret += chars.charAt(Math.floor(Math.random() * chars.length)); } return ret; }; /** */ module.exports.MatrixClient = MatrixClient; /** */ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; // MatrixClient Event JSDocs /** * Fires whenever the SDK receives a new event. * @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. This event is experimental * and may change. * @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. This event is experimental and may change. * @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. This event is experimental and * may change. * @event module:client~MatrixClient#"Room" * @param {Room} room The newly created, fully populated room. * @example * matrixClient.on("Room", function(room){ * var roomId = room.roomId; * }); */ /** * Fires whenever 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 one time listener for the event. This listener is invoked only * the next time the event is fired, after which it is removed. * @function external:EventEmitter#once * @param {string} event The event to listen for. * @param {Function} listener The function to invoke. * @return {EventEmitter} for call chaining. */ /** * Remove a listener from the listener array for the specified event. * Caution: changes array indices in the listener array behind the * listener. * @function external:EventEmitter#removeListener * @param {string} event The event to listen for. * @param {Function} listener The function to invoke. * @return {EventEmitter} for call chaining. */ /** * Removes all listeners, or those of the specified event. It's not a good idea * to remove listeners that were added elsewhere in the code, especially when * it's on an emitter that you didn't create (e.g. sockets or file streams). * @function external:EventEmitter#removeAllListeners * @param {string} event Optional. The event to remove listeners for. * @return {EventEmitter} for call chaining. */ /** * Execute each of the listeners in order with the supplied arguments. * @function external:EventEmitter#emit * @param {string} event The event to emit. * @param {Function} listener The function to invoke. * @return {boolean} true if event had listeners, false otherwise. */ /** * By default EventEmitters will print a warning if more than 10 listeners are * added for a particular event. This is a useful default which helps finding * memory leaks. Obviously not all Emitters should be limited to 10. This * function allows that to be increased. Set to zero for unlimited. * @function external:EventEmitter#setMaxListeners * @param {Number} n The max number of listeners. * @return {EventEmitter} for call chaining. */ // MatrixClient Callback JSDocs /** * The standard MatrixClient callback interface. Functions which accept this * will specify 2 return arguments. These arguments map to the 2 parameters * specified in this callback. * @callback module:client.callback * @param {Object} err The error value, the "rejected" value or null. * @param {Object} data The data returned, the "resolved" value. */ /** * {@link https://github.com/kriskowal/q|A promise implementation (Q)}. Functions * which return this will specify 2 return arguments. These arguments map to the * "onFulfilled" and "onRejected" values of the Promise. * @typedef {Object} Promise * @static * @property {Function} then promise.then(onFulfilled, onRejected, onProgress) * @property {Function} catch promise.catch(onRejected) * @property {Function} finally promise.finally(callback) * @property {Function} done promise.done(onFulfilled, onRejected, onProgress) */ },{"./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 {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 after the supplied prefix e.g. * "/createRoom". * @param {Object} queryParams A dict of query params (these will NOT be * urlencoded). * @param {Object} data The HTTP JSON body. * @return {module:client.Promise} Resolves to {data: {Object}, * headers: {Object}, code: {Number}}. * If onlyData is set, this will resolve to the data * object only. * @return {module:http-api.MatrixError} Rejects with an error if a problem * occurred. This includes network problems and Matrix-specific error JSON. */ authedRequest: function(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 after the supplied prefix e.g. * "/createRoom". * @param {Object} queryParams A dict of query params (these will NOT be * urlencoded). * @param {Object} data The HTTP JSON body. * @return {module:client.Promise} Resolves to {data: {Object}, * headers: {Object}, code: {Number}}. * If onlyData is set, this will resolve to the data * object only. * @return {module:http-api.MatrixError} Rejects with an error if a problem * occurred. This includes network problems and Matrix-specific error JSON. */ request: function(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 after the supplied prefix e.g. * "/createRoom". * @param {Object} queryParams A dict of query params (these will NOT be * urlencoded). * @param {Object} data The HTTP JSON body. * @param {string} prefix The full prefix to use e.g. * "/_matrix/client/v2_alpha". * @return {module:client.Promise} Resolves to {data: {Object}, * headers: {Object}, code: {Number}}. * If onlyData is set, this will resolve to the data * object only. * @return {module:http-api.MatrixError} Rejects with an error if a problem * occurred. This includes network problems and Matrix-specific error JSON. */ 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 after the supplied prefix e.g. * "/createRoom". * @param {Object} queryParams A dict of query params (these will NOT be * urlencoded). * @param {Object} data The HTTP JSON body. * @param {string} prefix The full prefix to use e.g. * "/_matrix/client/v2_alpha". * @return {module:client.Promise} Resolves to {data: {Object}, * headers: {Object}, code: {Number}}. * If onlyData is set, this will resolve to the data * object only. * @return {module:http-api.MatrixError} Rejects with an error if a problem * occurred. This includes network problems and Matrix-specific error JSON. */ 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. * Work in progress; unstable. */ 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. Work in progress; unstable. */ module.exports.WebStorageSessionStore = require("./store/session/webstorage"); /** True if crypto libraries are being used on this client. */ module.exports.CRYPTO_ENABLED = require("./client").CRYPTO_ENABLED; /** {@link module:content-repo|ContentRepo} utility functions. */ module.exports.ContentRepo = require("./content-repo"); /** * 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 $http. This should * be set prior to calling {@link createClient}. * @param {requestFunction} r The request function to use. */ module.exports.request = function(r) { request = r; }; /** * 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 * opts. */ module.exports.createClient = function(opts) { if (typeof opts === "string") { opts = { "baseUrl": opts }; } opts.request = opts.request || request; opts.store = opts.store || new module.exports.MatrixInMemoryStore(); 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 * {statusCode: {Number}, headers: {Object}} * @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. Do not access this property * directly unless you absolutely have to. Prefer the getter methods defined on * this class. Using the getter methods shields your app from * changes to event JSON between Matrix versions. * @prop {RoomMember} sender The room member who sent this event, or null e.g. * this is a presence event. * @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. This property is experimental and may change. */ 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. $143350589368169JsLZx:localhost * */ getId: function() { return this.event.event_id; }, /** * Get the user_id for this event. * @return {string} The user ID, e.g. @alice:matrix.org */ getSender: function() { return this.event.user_id; }, /** * Get the type of event. * @return {string} The event type, e.g. m.room.message */ 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 undefined * for m.presence events. * @return {string} The room ID, e.g. !cURbafjkfsMDVwdRDQ:matrix.org * */ getRoomId: function() { return this.event.room_id; }, /** * Get the timestamp of this event. * @return {Number} The event timestamp, e.g. 1433502692297 */ 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. * This method is experimental and may change. * @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 undefined * for message events. * @return {string} The event's state_key. */ 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 m.room.member event * @param {RoomState} roomState Optional. The room state to take into account * when calculating (e.g. for disambiguating users with the same name). * @fires module:client~MatrixClient#event:"RoomMember.name" * @fires module:client~MatrixClient#event:"RoomMember.membership" */ RoomMember.prototype.setMembershipEvent = function(event, roomState) { if (event.getType() !== "m.room.member") { return; } this.events.member = event; var oldMembership = this.membership; this.membership = event.getDirectionalContent().membership; var oldName = this.name; this.name = calculateDisplayName(this, event, roomState); 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 m.room.power_levels * event * @fires module:client~MatrixClient#event:"RoomMember.powerLevel" */ RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) { if (powerLevelEvent.getType() !== "m.room.power_levels") { return; } var 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 before firing events. * @return {number} The timestamp */ RoomMember.prototype.getLastModifiedTime = function() { return this._modified; }; /** * Get the avatar URL for a room member. * @param {string} baseUrl The base homeserver URL See * {@link module:client~MatrixClient#getHomeserverUrl}. * @param {Number} width The desired width of the thumbnail. * @param {Number} height The desired height of the thumbnail. * @param {string} resizeMethod The thumbnail resize method to use, either * "crop" or "scale". * @param {Boolean} allowDefault (optional) Passing false causes this method to * return null if the user has no avatar image. Otherwise, a default image URL * will be returned. Default: true. * @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.} members The room member dictionary, keyed * on the user's ID. * @prop {Object.>} events The state * events dictionary, keyed on the event type and then the state_key value. * @prop {string} paginationToken The pagination token for this state. */ function RoomState(roomId) { this.roomId = roomId; this.members = { // userId: RoomMember }; this.events = { // eventType: { stateKey: MatrixEvent } }; this.paginationToken = null; this._sentinels = { // userId: RoomMember }; this._updateModifiedTime(); this._displayNameToUserIds = {}; this._userIdsToDisplayNames = {}; } utils.inherits(RoomState, EventEmitter); /** * Get all RoomMembers in this room. * @return {Array} A list of RoomMembers. */ RoomState.prototype.getMembers = function() { return utils.values(this.members); }; /** * Get a room member by their user ID. * @param {string} userId The room member's user ID. * @return {RoomMember} The member or null if they do not exist. */ RoomState.prototype.getMember = function(userId) { return this.members[userId] || null; }; /** * Get a room member whose properties will not change with this room state. You * typically want this if you want to attach a RoomMember to a MatrixEvent which * may no longer be represented correctly by Room.currentState or Room.oldState. * The term 'sentinel' refers to the fact that this RoomMember is an unchanging * guardian for state at this particular point in time. * @param {string} userId The room member's user ID. * @return {RoomMember} The member or null if they do not exist. */ RoomState.prototype.getSentinelMember = function(userId) { 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 undefined then all matching state events will be * returned. * @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was * undefined, else a single event (or null if no match found). */ RoomState.prototype.getStateEvents = function(eventType, stateKey) { if (!this.events[eventType]) { // no match return stateKey === undefined ? [] : null; } if (stateKey === undefined) { // return all values return utils.values(this.events[eventType]); } var event = this.events[eventType][stateKey]; return event ? event : null; }; /** * Add an array of one or more state MatrixEvents, overwriting * any existing state with the same {type, stateKey} tuple. Will fire * "RoomState.events" for every event added. May fire "RoomState.members" * if there are m.room.member events. * @param {MatrixEvent[]} stateEvents a list of state events for this room. * @fires module:client~MatrixClient#event:"RoomState.members" * @fires module:client~MatrixClient#event:"RoomState.newMember" * @fires module:client~MatrixClient#event:"RoomState.events" */ RoomState.prototype.setStateEvents = function(stateEvents) { var self = this; this._updateModifiedTime(); // update the core event dict utils.forEach(stateEvents, function(event) { if (event.getRoomId() !== self.roomId) { return; } if (!event.isState()) { return; } if (self.events[event.getType()] === undefined) { self.events[event.getType()] = {}; } self.events[event.getType()][event.getStateKey()] = event; if (event.getType() === "m.room.member") { _updateDisplayNameCache( self, event.getStateKey(), event.getContent().displayname ); } 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. m.room.name) * @param {string} info.desc The description of the room (e.g. * m.room.topic) * @param {Number} info.numMembers The number of joined users. * @param {string[]} info.aliases The list of aliases for this room. * @param {Number} info.timestamp The timestamp for this room. */ function RoomSummary(roomId, info) { this.roomId = roomId; this.info = info; } /** * The RoomSummary class. */ module.exports = RoomSummary; },{}],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} 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: , // data: // } // } }; this._receiptCacheByEventId = { // $event_id: [{ // type: $type, // userId: $user_id, // 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 null. */ Room.prototype.getMember = function(userId) { var member = this.currentState.members[userId]; if (!member) { return null; } return member; }; /** * Get a list of members whose membership state is "join". * @return {RoomMember[]} A list of currently joined members. */ Room.prototype.getJoinedMembers = function() { return this.getMembersWithMembership("join"); }; /** * Get a list of members with given membership state. * @param {string} membership The membership state. * @return {RoomMember[]} A list of members with the given membership state. */ Room.prototype.getMembersWithMembership = function(membership) { return utils.filter(this.currentState.getMembers(), function(m) { return m.membership === membership; }); }; /** * Check if the given user_id has the given membership state. * @param {string} userId The user ID to check. * @param {string} membership The membership e.g. 'join' * @return {boolean} True if this user_id has the given membership state. */ Room.prototype.hasMembershipState = function(userId, membership) { 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 last 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 only. * @throws If duplicateStrategy 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 read up to the given event. * @param {MatrixEvent} event the event to get read receipts for. * @return {String[]} A list of user IDs. */ Room.prototype.getUsersReadUpTo = function(event) { return this.getReceiptsForEvent(event).filter(function(receipt) { return receipt.type === "m.read"; }).map(function(receipt) { return receipt.userId; }); }; /** * Get 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 m.presence event. * @fires module:client~MatrixClient#event:"User.presence" * @fires module:client~MatrixClient#event:"User.displayName" * @fires module:client~MatrixClient#event:"User.avatarUrl" */ User.prototype.setPresenceEvent = function(event) { if (event.getType() !== "m.presence") { return; } var firstFire = this.events.presence === null; this.events.presence = event; var eventsToFire = []; if (event.getContent().presence !== this.presence || firstFire) { eventsToFire.push("User.presence"); } if (event.getContent().avatar_url !== 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 before 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} A shallow copy of events in the queue or null. * Modifying this array will not modify the list itself. Modifying events in * this array will modify the underlying event in the queue. * @see MatrixScheduler.removeEventFromQueue To remove an event from the queue. */ MatrixScheduler.prototype.getQueueForEvent = function(event) { var name = this.queueAlgorithm(event); if (!name || !this._queues[name]) { return null; } return utils.map(this._queues[name], function(obj) { return obj.event; }); }; /** * Remove this event from the queue. The event is equal to another event if they * have the same ID returned from event.getId(). * @param {MatrixEvent} event The event to remove. * @return {boolean} True if this event was removed. */ MatrixScheduler.prototype.removeEventFromQueue = function(event) { var name = this.queueAlgorithm(event); if (!name || !this._queues[name]) { return false; } var removed = false; utils.removeElement(this._queues[name], function(element) { if (element.event.getId() === event.getId()) { 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 m.room.message events and lets other events continue * concurrently. * @param {MatrixEvent} event * @return {string} * @see module:scheduler~queueAlgorithm */ MatrixScheduler.QUEUE_MESSAGES = function(event) { if (event.getType() === "m.room.message") { // put these events in the 'message' queue. return "message"; } // allow all other events continue concurrently. return null; }; function _startProcessingQueues(scheduler) { if (!scheduler._procFn) { return; } // for each inactive queue with events in them utils.forEach(utils.filter(utils.keys(scheduler._queues), function(queueName) { return scheduler._activeQueues.indexOf(queueName) === -1 && scheduler._queues[queueName].length > 0; }), function(queueName) { // mark the queue as active scheduler._activeQueues.push(queueName); // begin processing the head of the queue debuglog("Spinning up queue: '%s'", queueName); _processQueue(scheduler, queueName); }); } function _processQueue(scheduler, queueName) { // get head of queue var obj = _peekNextEvent(scheduler, queueName); if (!obj) { // queue is empty. Mark as inactive and stop recursing. var index = scheduler._activeQueues.indexOf(queueName); if (index >= 0) { scheduler._activeQueues.splice(index, 1); } debuglog("Stopping queue '%s' as it is now empty", queueName); return; } debuglog( "Queue '%s' has %s pending events", queueName, scheduler._queues[queueName].length ); // fire the process function and if it resolves, resolve the deferred. Else // invoke the retry algorithm. scheduler._procFn(obj.event).done(function(res) { // remove this from the queue _removeNextEvent(scheduler, queueName); debuglog("Queue '%s' sent event %s", queueName, obj.event.getId()); obj.defer.resolve(res); // keep processing _processQueue(scheduler, queueName); }, function(err) { obj.attempts += 1; // ask the retry algorithm when/if we should try again var waitTimeMs = scheduler.retryAlgorithm(obj.event, obj.attempts, err); debuglog( "retry(%s) err=%s event_id=%s waitTime=%s", obj.attempts, err, obj.event.getId(), waitTimeMs ); if (waitTimeMs === -1) { // give up (you quitter!) debuglog( "Queue '%s' giving up on event %s", queueName, obj.event.getId() ); // remove this from the queue _removeNextEvent(scheduler, queueName); obj.defer.reject(err); // process next event _processQueue(scheduler, queueName); } else { setTimeout(function() { _processQueue(scheduler, queueName); }, waitTimeMs); } }); } function _peekNextEvent(scheduler, queueName) { var queue = scheduler._queues[queueName]; if (!utils.isArray(queue)) { return null; } return queue[0]; } function _removeNextEvent(scheduler, queueName) { var queue = scheduler._queues[queueName]; if (!utils.isArray(queue)) { return null; } return queue.shift(); } function debuglog() { if (DEBUG) { console.log.apply(console, arguments); } } /** * The retry algorithm to apply when retrying events. To stop retrying, return * -1. If this event was part of a queue, it will be removed from * the queue. * @callback retryAlgorithm * @param {MatrixEvent} event The event being retried. * @param {Number} attempts The number of failed attempts. This will always be * >= 1. * @param {MatrixError} err The most recent error message received when trying * to send this event. * @return {Number} The number of milliseconds to wait before trying again. If * this is 0, the request will be immediately retried. If this is * -1, the event will be marked as * {@link module:models/event.EventStatus.NOT_SENT} and will not be retried. */ /** * The queuing algorithm to apply to events. This function must be idempotent as * it may be called multiple times with the same event. All queues created are * serviced in a FIFO manner. To send the event ASAP, return null * which will not put this event in a queue. Events that fail to send that form * part of a queue will be removed from the queue and the next event in the * queue will be sent. * @callback queueAlgorithm * @param {MatrixEvent} event The event to be sent. * @return {string} The name of the queue to put the event into. If a queue with * this name does not exist, it will be created. If this is null, * the event is not put into a queue and will be sent concurrently. */ /** * The function to invoke to process (send) events in the queue. * @callback processFn * @param {MatrixEvent} event The event to send. * @return {Promise} Resolved/rejected depending on the outcome of the request. */ /** * The MatrixScheduler class. */ module.exports = MatrixScheduler; },{"./utils":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} 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} 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} 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: *
 * Room data is stored as follows:
 *   room_$ROOMID_timeline_$INDEX : [ Event, Event, Event ]
 *   room_$ROOMID_state : {
 *                          pagination_token: ,
 *                          events: {
 *                            : {  : {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
 * 
* @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} 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} 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} 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 fn(element){...} * @return {Array} A new array with the results of the function. */ module.exports.map = function(array, fn) { var results = new Array(array.length); for (var i = 0; i < array.length; i++) { results[i] = fn(array[i]); } return results; }; /** * Applies a filter function to the given array. * @param {Array} array The array to apply the function to. * @param {Function} fn The function that will be invoked for each element in * the array. It should return true to keep the element. The function signature * looks like fn(element, index, array){...}. * @return {Array} A new array with the results of the function. */ module.exports.filter = function(array, fn) { var results = []; for (var i = 0; i < array.length; i++) { if (fn(array[i], i, array)) { results.push(array[i]); } } return results; }; /** * Get the keys for an object. Same as Object.keys(). * @param {Object} obj The object to get the keys for. * @return {string[]} The keys of the object. */ module.exports.keys = function(obj) { var keys = []; for (var key in obj) { if (!obj.hasOwnProperty(key)) { continue; } keys.push(key); } return keys; }; /** * Get the values for an object. * @param {Object} obj The object to get the values for. * @return {Array<*>} The values of the object. */ module.exports.values = function(obj) { var values = []; for (var key in obj) { if (!obj.hasOwnProperty(key)) { continue; } values.push(obj[key]); } return values; }; /** * Invoke a function for each item in the array. * @param {Array} array The array. * @param {Function} fn The function to invoke for each element. Has the * function signature fn(element, index). */ module.exports.forEach = function(array, fn) { for (var i = 0; i < array.length; i++) { fn(array[i], i); } }; /** * The findElement() method returns a value in the array, if an element in the array * satisfies (returns true) the provided testing function. Otherwise undefined * is returned. * @param {Array} array The array. * @param {Function} fn Function to execute on each value in the array, with the * function signature fn(element, index, array) * @param {boolean} reverse True to search in reverse order. * @return {*} The first value in the array which returns true for * the given function. */ module.exports.findElement = function(array, fn, reverse) { var i; if (reverse) { for (i = array.length - 1; i >= 0; i--) { if (fn(array[i], i, array)) { return array[i]; } } } else { for (i = 0; i < array.length; i++) { if (fn(array[i], i, array)) { return array[i]; } } } }; /** * The removeElement() method removes the first element in the array that * satisfies (returns true) the provided testing function. * @param {Array} array The array. * @param {Function} fn Function to execute on each value in the array, with the * function signature fn(element, index, array). Return true to * remove this element and break. * @param {boolean} reverse True to search in reverse order. * @return {boolean} True if an element was removed. */ module.exports.removeElement = function(array, fn, reverse) { var i; 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} 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 <video> DOM element * to render video to. * @param {Element} localVideoElement a <video> DOM element * to render the local camera preview. * @throws If you have not specified a listener for 'error' events. */ MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoElement) { debuglog("placeVideoCall"); checkForErrorListener(this); this.localVideoElement = localVideoElement; this.remoteVideoElement = remoteVideoElement; _placeCallWithConstraints(this, _getUserMediaVideoContraints('video')); this.type = 'video'; _tryPlayRemoteStream(this); }; /** * Place a screen-sharing call to this room. This includes audio. * This method is EXPERIMENTAL and subject to change without warning. It * only works in Google Chrome. * @param {Element} remoteVideoElement a <video> DOM element * to render video to. * @param {Element} localVideoElement a <video> DOM element * to render the local camera preview. * @throws If you have not specified a listener for 'error' events. */ MatrixCall.prototype.placeScreenSharingCall = function(remoteVideoElement, localVideoElement) { debuglog("placeScreenSharingCall"); checkForErrorListener(this); var screenConstraints = _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 <video> DOM element. * @return {Element} The dom element */ MatrixCall.prototype.getLocalVideoElement = function() { return this.localVideoElement; }; /** * Retrieve the remote <video> DOM element * used for playing back video capable streams. * @return {Element} The dom element */ MatrixCall.prototype.getRemoteVideoElement = function() { return this.remoteVideoElement; }; /** * Retrieve the remote <audio> DOM element * used for playing back audio only streams. * @return {Element} The dom element */ MatrixCall.prototype.getRemoteAudioElement = function() { return this.remoteAudioElement; }; /** * Set the local <video> DOM element. If this call is active, * video will be rendered to it immediately. * @param {Element} element The <video> 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 <video> DOM element. If this call is active, * the first received video-capable stream will be rendered to it immediately. * @param {Element} element The <video> DOM element. */ MatrixCall.prototype.setRemoteVideoElement = function(element) { this.remoteVideoElement = element; _tryPlayRemoteStream(this); }; /** * Set the remote <audio> DOM element. If this call is active, * the first received audio-only stream will be rendered to it immediately. * @param {Element} element The <video> 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, all 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, all 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