(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 * {@link module:scheduler~MatrixScheduler#setProcessFunction}. */ function MatrixClient(opts) { utils.checkObjectHasKeys(opts, ["baseUrl", "request"]); utils.checkObjectHasNoAdditionalKeys(opts, ["baseUrl", "request", "accessToken", "userId", "store", "scheduler"] ); this.store = opts.store || new StubStore(); this.scheduler = opts.scheduler; if (this.scheduler) { var self = this; this.scheduler.setProcessFunction(function(eventToSend) { return _sendEventHttpRequest(self, eventToSend); }); } // track our position in the overall eventstream this.fromToken = undefined; this.clientRunning = false; var httpOpts = { baseUrl: opts.baseUrl, accessToken: opts.accessToken, request: opts.request, prefix: httpApi.PREFIX_V1, onlyData: true }; this.credentials = { userId: (opts.userId || null) }; this._http = new httpApi.MatrixHttpApi(httpOpts); this._syncingRooms = { // room_id: Promise }; } utils.inherits(MatrixClient, EventEmitter); /** * 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(); }; // 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. * @param {string} roomIdOrAlias The room ID or room alias to join. * @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, callback) { 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); return _syncRoom(self, 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 {Object} 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.type === "m.room.power_levels") { content = event.content; } 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]); } return _sendEvent(this, room, localEvent, callback); }; 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) { 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 fake 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) { 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.getType(), $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.getContent() ); } /** * @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.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 {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 ); }; /** * @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 } ); } // 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 ); }; /** * @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 ); }; /** * @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.registerFlows = function(callback) { return this._http.request(callback, "GET", "/register"); }; /** * @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. * @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; } var path = utils.encodeUri( "/rooms/$roomId/messages", {$roomId: room.roomId} ); var params = { from: room.oldState.paginationToken, limit: (limit || 30), dir: 'b' }; var defer = q.defer(); this._http.authedRequest(callback, "GET", path, params).done(function(res) { // res.chunk end start room.addEventsToTimeline( utils.map(res.chunk, _PojoToMatrixEventMapper), true ); room.oldState.paginationToken = res.end; _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} 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.register = function(loginType, data, callback) { data.type = loginType; return this._http.authedRequest( callback, "POST", "/register", undefined, data ); }; /** * @param {string} user * @param {string} password * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.loginWithPassword = function(user, password, callback) { return this.login("m.login.password", { user: user, password: password }, 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); }; // 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"); }; /** * @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 /** * 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 {Number} historyLen amount of historical timeline events to * emit during from the initial sync. Default: 12. */ MatrixClient.prototype.startClient = function(historyLen) { if (this.clientRunning) { // client is already running. return; } if (this.fromToken) { // resume from where we left off. _pollForEvents(this); return; } var self = this; this._http.authedRequest( undefined, "GET", "/initialSync", { limit: (historyLen || 12) } ).done(function(data) { var i, j; // intercept the results and put them into our store if (!(self.store instanceof StubStore)) { utils.forEach(utils.map(data.presence, _PojoToMatrixEventMapper), function(e) { var user = createNewUser(self, e.getContent().user_id); user.setPresenceEvent(e); self.store.storeUser(user); }); for (i = 0; i < data.rooms.length; i++) { var room = createNewRoom(self, data.rooms[i].room_id); if (!data.rooms[i].state) { data.rooms[i].state = []; } if (data.rooms[i].membership === "invite") { // create fake invite state event (v1 sucks) data.rooms[i].state.push({ event_id: "$fake_" + room.roomId, content: { membership: "invite" }, state_key: self.credentials.userId, user_id: data.rooms[i].inviter, room_id: room.roomId, type: "m.room.member" }); } _processRoomEvents( room, data.rooms[i].state, data.rooms[i].messages ); // cache the name/summary/etc prior to storage since we don't // know how the store will serialise the Room. room.recalculate(self.credentials.userId); self.store.storeRoom(room); self.emit("Room", room); } } if (data) { self.fromToken = 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) { self.emit("event", e); }); } self.clientRunning = true; self.emit("syncComplete"); _pollForEvents(self); }, function(err) { self.emit("syncError", err); // TODO: Retries. }); }; /** * 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; _pollForEvents(client); }, 40000); client._http.authedRequest(undefined, "GET", "/events", { from: client.fromToken, timeout: 30000 }).done(function(data) { if (discardResult) { return; } else { clearTimeout(timeoutObj); } var events = []; if (data) { events = utils.map(data.chunk, function(event) { return new MatrixEvent(event); }); } if (!(self.store instanceof StubStore)) { // 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]); } } // 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); } }); } if (data) { self.fromToken = data.end; utils.forEach(events, function(e) { self.emit("event", e); }); } _pollForEvents(self); }, function(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(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(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 ); var stateEvents = utils.map(stateEventList, _PojoToMatrixEventMapper); room.oldState.setStateEvents(oldStateEvents); room.currentState.setStateEvents(stateEvents); // 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 ).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 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) { 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(plainOldJsObject) { return new MatrixEvent(plainOldJsObject); } /** */ module.exports.MatrixClient = MatrixClient; // 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; * }); */ // 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) */ },{"./http-api":2,"./models/event":4,"./models/room":8,"./models/user":9,"./store/stub":12,"./utils":13,"events":15,"q":17}],2:[function(require,module,exports){ "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 - uploadContent (?) */ /** * 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_PREFIX = "/_matrix/client/v2_alpha"; /** * 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. */ module.exports.MatrixHttpApi = function MatrixHttpApi(opts) { utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]); opts.onlyData = opts.onlyData || false; this.opts = opts; }; module.exports.MatrixHttpApi.prototype = { // URI functions // ============= /** * Get the HTTP URL for an MXC URI. * @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(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 this.credentials.baseUrl + prefix + serverAndMediaId + (utils.keys(params).length === 0 ? "" : ("?" + utils.encodeParams(params))) + fragment; }, /** * Get an identicon URL from an arbitrary string. * @param {string} identiconString The string to create an identicon for. * @param {Number} width The desired width of the image in pixels. * @param {Number} height The desired height of the image in pixels. * @return {string} The complete URL to the identicon. */ getIdenticonUri: function(identiconString, width, height) { if (!identiconString) { return; } 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 this.credentials.baseUrl + path + (utils.keys(params).length === 0 ? "" : ("?" + utils.encodeParams(params))); }, /** * 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.credentials.accessToken }; return { base: this.credentials.baseUrl, path: "/_matrix/media/v1/upload", params: params }; }, /** * 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 ); } var defer = q.defer(); 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) ); return defer.promise; } }; 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); } 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} name The Matrix 'errcode' value, e.g. "M_FORBIDDEN". * @prop {string} message The Matrix 'error' value, e.g. "Missing token." * @prop {Object} data The raw Matrix error JSON used to construct this object. */ module.exports.MatrixError = function MatrixError(errorJson) { 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; },{"./utils":13,"q":17}],3:[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: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"); // 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 {string} opts.baseUrl The base URL to the client-server HTTP API. * @param {string} opts.accessToken Optional. The access_token for this user. * @param {string} opts.userId Optional. The user ID for this user. * @param {Object} opts.store Set to {@link module:store/memory.MatrixInMemoryStore}. * @param {Object} opts.scheduler Set to {@link module:scheduler~MatrixScheduler}. * @return {MatrixClient} A new matrix client. */ module.exports.createClient = function(opts) { if (typeof opts === "string") { opts = { "baseUrl": opts }; } opts.request = request; opts.store = new module.exports.MatrixInMemoryStore(); 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":1,"./http-api":2,"./models/event":4,"./models/room":8,"./models/room-member":5,"./models/room-state":6,"./models/user":9,"./scheduler":10,"./store/memory":11}],4:[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" }; /** * Construct a Matrix Event object * @constructor * @param {Object} event The raw event to be wrapped in this DAO * @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) { this.event = event || {}; this.sender = null; this.target = null; this.status = null; this.forwardLooking = true; }; 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 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 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; } }; },{}],5:[function(require,module,exports){ "use strict"; /** * @module models/room-member */ var EventEmitter = require("events").EventEmitter; 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 }; } 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.emit("RoomMember.membership", event, this); } if (oldName !== this.name) { 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.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.emit("RoomMember.typing", event, this); } }; function calculateDisplayName(member, event, roomState) { var displayName = event.getDirectionalContent().displayname; var selfUserId = member.userId; if (!displayName) { return selfUserId; } if (!roomState) { return displayName; } var stateEvents = utils.filter( roomState.getStateEvents("m.room.member"), function(e) { return e.getContent().displayname === displayName && e.getSender() !== selfUserId; } ); if (stateEvents.length > 1) { // need to disambiguate return displayName + " (" + selfUserId + ")"; } return displayName; } /** * The RoomMember class. */ module.exports = RoomMember; /** * Fires whenever any room member's name changes. * @event module:client~MatrixClient#"RoomMember.name" * @param {MatrixEvent} event The matrix event which caused this event to fire. * @param {RoomMember} member The member whose RoomMember.name changed. * @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; * }); */ },{"../utils":13,"events":15}],6:[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 }; } 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.events" */ RoomState.prototype.setStateEvents = function(stateEvents) { var self = this; 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; self.emit("RoomState.events", event, self); 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); }); }; /** * The RoomState class. */ module.exports = RoomState; /** * 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":13,"./room-member":5,"events":15}],7:[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; },{}],8:[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 utils = require("../utils"); /** * Construct a new Room. * @constructor * @param {string} roomId Required. The ID of this room. * @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. */ function Room(roomId) { this.roomId = roomId; this.name = roomId; this.timeline = []; this.oldState = new RoomState(roomId); this.currentState = new RoomState(roomId); this.summary = null; } utils.inherits(Room, EventEmitter); /** * 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 utils.filter(this.currentState.getMembers(), function(m) { return m.membership === "join"; }); }; /** * 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; for (var i = 0; i < events.length; i++) { // set sender and target properties events[i].sender = stateContext.getSentinelMember( events[i].getSender() ); if (events[i].getType() === "m.room.member") { events[i].target = stateContext.getSentinelMember( events[i].getStateKey() ); } // modify state if (events[i].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) { events[i].forwardLooking = false; } stateContext.setStateEvents([events[i]], toStartOfTimeline); } // 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, 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 (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") { 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) { 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); } }; /** * 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 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 + (alias ? " (" + alias + ")" : ""); } else if (alias) { return alias; } else { // get members that are NOT ourselves. var members = utils.filter(room.currentState.getMembers(), function(m) { return m.userId !== userId; }); // TODO: Localisation if (members.length === 0) { var memberList = room.currentState.getMembers(); if (memberList.length === 1) { // we exist, but no one else... self-chat or invite. if (memberList[0].membership === "invite") { 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; * }); */ },{"../utils":13,"./room-state":6,"./room-summary":7,"events":15}],9:[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 }; } 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; } this.events.presence = event; var eventsToFire = []; if (event.getContent().presence !== this.presence) { 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; for (var i = 0; i < eventsToFire.length; i++) { this.emit(eventsToFire[i], event, this); } }; /** * 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":13,"events":15}],10:[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; } /** * 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.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. 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":13,"q":17}],11:[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 }; }; module.exports.MatrixInMemoryStore.prototype = { /** * 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; } // TODO //setMaxHistoryPerRoom: function(maxHistory) {}, // TODO //reapOldMessages: function() {}, }; },{"../utils":13}],12:[function(require,module,exports){ "use strict"; /** * This is an internal module. * @module store/stub */ /** * Construct a stub store. This does no-ops on all store methods. * @constructor */ function StubStore() { } StubStore.prototype = { /** * 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; } // TODO //setMaxHistoryPerRoom: function(maxHistory) {}, // TODO //reapOldMessages: function() {}, }; /** Stub Store class. */ module.exports = StubStore; },{}],13:[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. */ 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; } } } else { for (i = 0; i < array.length; i++) { if (fn(array[i], i, array)) { array.splice(i, 1); return; } } } }; /** * 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); } } }; /** * Assigns all the properties in src to dst. If these properties are Objects, * then both src and dst will refer to the same thing. * @param {Object} src The object to copy properties from. * @param {Object} dst The object to write properties to. */ module.exports.shallowCopy = function(src, dst) { for (var i in src) { if (src.hasOwnProperty(i)) { dst[i] = src[i]; } } }; /** * 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)); }; /** * 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 } }); }; },{}],14:[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 },{}],15:[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; } },{}],16:[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; }; },{}],17:[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