From d1e51de7ec66fe29d93fd2093181b980da91453d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 3 Jun 2015 17:55:12 +0100 Subject: [PATCH] Split out matrix.js into different files. Glue things back. Added a models directory. Added store, http-api and client files. Slowly transitioning to the architecture outlined in SYJS-5. --- dist/browser-matrix-dev.js | 678 ++++++++++++---------- lib/client.js | 293 ++++++++++ lib/http-api.js | 634 +++++++++++++++++++++ lib/matrix.js | 1105 +----------------------------------- lib/models/event.js | 37 ++ lib/models/room.js | 14 + lib/store/memory.js | 163 ++++++ 7 files changed, 1513 insertions(+), 1411 deletions(-) create mode 100644 lib/client.js create mode 100644 lib/http-api.js create mode 100644 lib/models/event.js create mode 100644 lib/models/room.js create mode 100644 lib/store/memory.js diff --git a/dist/browser-matrix-dev.js b/dist/browser-matrix-dev.js index e7c244372..84df2ade7 100644 --- a/dist/browser-matrix-dev.js +++ b/dist/browser-matrix-dev.js @@ -1,24 +1,18 @@ (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 1) { + return displayName + " (" + userId + ")"; + } + else { + return displayName; + } + }, + + /* + * High level helper method to call initialSync, emit the resulting events, + * and then start polling the eventStream for new events. + * @param {function} callback Callback invoked whenever new event are available + * @param {Number} historyLen amount of historical timeline events to + * emit during from the initial sync. + */ + startClient: function(callback, historyLen) { + historyLen = historyLen || 12; + + var self = this; + if (!this.fromToken) { + this._http.initialSync(historyLen, function(err, data) { + var i, j; + if (err) { + if (this.config && this.config.debug) { + console.error( + "startClient error on initialSync: %s", + JSON.stringify(err) + ); + } + callback(err); + return; + } + if (self.store) { + var eventMapper = function(event) { + return new MatrixEvent(event); + }; + // intercept the results and put them into our store + self.store.setPresenceEvents( + map(data.presence, eventMapper) + ); + for (i = 0; i < data.rooms.length; i++) { + self.store.setStateEvents( + map(data.rooms[i].state, eventMapper) + ); + self.store.setEvents( + map(data.rooms[i].messages.chunk, eventMapper) + ); + } + } + 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++) { + for (j = 0; j < data.rooms[i].state.length; j++) { + events.push(new MatrixEvent(data.rooms[i].state[j])); + } + for (j = 0; j < data.rooms[i].messages.chunk.length; j++) { + events.push( + new MatrixEvent(data.rooms[i].messages.chunk[j]) + ); + } + } + callback(undefined, events, false); + } + + self.clientRunning = true; + self._pollForEvents(callback); + }); + } + else { + this._pollForEvents(callback); + } + }, + + _pollForEvents: function(callback) { + var self = this; + if (!this.clientRunning) { + return; + } + this._http.eventStream(this.fromToken, 30000, function(err, data) { + if (err) { + if (this.config && this.config.debug) { + console.error( + "error polling for events via eventStream: %s", + JSON.stringify(err) + ); + } + callback(err); + // retry every few seconds + // FIXME: this should be exponential backoff with an option to nudge + setTimeout(function() { + self._pollForEvents(callback); + }, 2000); + return; + } + + if (self.store) { + self.store.setEvents(map(data.chunk, + function(event) { + return new MatrixEvent(event); + } + )); + } + if (data) { + self.fromToken = data.end; + var events = []; + for (var j = 0; j < data.chunk.length; j++) { + events.push(new MatrixEvent(data.chunk[j])); + } + callback(undefined, events, true); + } + self._pollForEvents(callback); + }); + }, + + /* + * High level helper method to stop the client from polling and allow a + * clean shutdown. + */ + stopClient: function() { + this.clientRunning = false; + }, +}; + +var 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; +}; + +/** + * The high-level Matrix Client class. + */ +module.exports = MatrixClient; // expose the class diff --git a/lib/http-api.js b/lib/http-api.js new file mode 100644 index 000000000..f2612f5f5 --- /dev/null +++ b/lib/http-api.js @@ -0,0 +1,634 @@ +"use strict"; + +/* +TODO: +- CS: complete register function (doing stages) +- Identity server: linkEmail, authEmail, bindEmail, lookup3pid +- uploadContent (?) +*/ +var CLIENT_PREFIX = "/_matrix/client/api/v1"; +var CLIENT_V2_PREFIX = "/_matrix/client/v2_alpha"; +var HEADERS = { + "User-Agent": "matrix-js" +}; + +/* + * Construct a MatrixHttpApi. + * @param {Object} credentials The credentials for this client + * @param {Object} config The config for this client. + * @param {Function} request The request function for doing HTTP requests + */ +function MatrixHttpApi(credentials, config, request) { + if (typeof credentials === "string") { + credentials = { + "baseUrl": credentials + }; + } + var requiredKeys = [ + "baseUrl" + ]; + for (var i = 0; i < requiredKeys.length; i++) { + if (!credentials.hasOwnProperty(requiredKeys[i])) { + throw new Error("Missing required key: " + requiredKeys[i]); + } + } + if (config && config.noUserAgent) { + HEADERS = undefined; + } + this.config = config; + this.credentials = credentials; + this.request = request; +} + +MatrixHttpApi.prototype = { + // Room operations + // =============== + + createRoom: function(options, callback) { + // valid options include: room_alias_name, visibility, invite + return this._doAuthedRequest( + callback, "POST", "/createRoom", undefined, options + ); + }, + + joinRoom: function(roomIdOrAlias, callback) { + var path = encodeUri("/join/$roomid", { $roomid: roomIdOrAlias}); + return this._doAuthedRequest(callback, "POST", path, undefined, {}); + }, + + setRoomName: function(roomId, name, callback) { + return this.sendStateEvent(roomId, "m.room.name", {name: name}, + undefined, callback); + }, + + setRoomTopic: function(roomId, topic, callback) { + return this.sendStateEvent(roomId, "m.room.topic", {topic: topic}, + undefined, callback); + }, + + 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 = encodeUri("/rooms/$roomId/state/m.room.power_levels", { + $roomId: roomId + }); + return this._doAuthedRequest( + callback, "PUT", path, undefined, content + ); + }, + + getStateEvent: function(roomId, eventType, stateKey, callback) { + var pathParams = { + $roomId: roomId, + $eventType: eventType, + $stateKey: stateKey + }; + var path = encodeUri("/rooms/$roomId/state/$eventType", pathParams); + if (stateKey !== undefined) { + path = encodeUri(path + "/$stateKey", pathParams); + } + return this._doAuthedRequest( + callback, "GET", path + ); + }, + + sendStateEvent: function(roomId, eventType, content, stateKey, + callback) { + var pathParams = { + $roomId: roomId, + $eventType: eventType, + $stateKey: stateKey + }; + var path = encodeUri("/rooms/$roomId/state/$eventType", pathParams); + if (stateKey !== undefined) { + path = encodeUri(path + "/$stateKey", pathParams); + } + return this._doAuthedRequest( + callback, "PUT", path, undefined, content + ); + }, + + sendEvent: function(roomId, eventType, content, txnId, callback) { + if (isFunction(txnId)) { callback = txnId; txnId = undefined; } + + if (!txnId) { + txnId = "m" + new Date().getTime(); + } + + var path = encodeUri("/rooms/$roomId/send/$eventType/$txnId", { + $roomId: roomId, + $eventType: eventType, + $txnId: txnId + }); + return this._doAuthedRequest( + callback, "PUT", path, undefined, content + ); + }, + + sendMessage: function(roomId, content, txnId, callback) { + if (isFunction(txnId)) { callback = txnId; txnId = undefined; } + return this.sendEvent( + roomId, "m.room.message", content, txnId, callback + ); + }, + + sendTextMessage: function(roomId, body, txnId, callback) { + var content = { + msgtype: "m.text", + body: body + }; + return this.sendMessage(roomId, content, txnId, callback); + }, + + sendEmoteMessage: function(roomId, body, txnId, callback) { + var content = { + msgtype: "m.emote", + body: body + }; + return this.sendMessage(roomId, content, txnId, callback); + }, + + sendImageMessage: function(roomId, url, info, text, callback) { + if (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); + }, + + 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); + }, + + sendTyping: function(roomId, isTyping, timeoutMs, callback) { + var path = encodeUri("/rooms/$roomId/typing/$userId", { + $roomId: roomId, + $userId: this.credentials.userId + }); + var data = { + typing: isTyping + }; + if (isTyping) { + data.timeout = timeoutMs ? timeoutMs : 20000; + } + return this._doAuthedRequest( + callback, "PUT", path, undefined, data + ); + }, + + redactEvent: function(roomId, eventId, callback) { + var path = encodeUri("/rooms/$roomId/redact/$eventId", { + $roomId: roomId, + $eventId: eventId + }); + return this._doAuthedRequest(callback, "POST", path, undefined, {}); + }, + + invite: function(roomId, userId, callback) { + return this._membershipChange(roomId, userId, "invite", undefined, + callback); + }, + + leave: function(roomId, callback) { + return this._membershipChange(roomId, undefined, "leave", undefined, + callback); + }, + + ban: function(roomId, userId, reason, callback) { + return this._membershipChange(roomId, userId, "ban", reason, + callback); + }, + + unban: function(roomId, userId, callback) { + // unbanning = set their state to leave + return this._setMembershipState( + roomId, userId, "leave", undefined, callback + ); + }, + + kick: function(roomId, userId, reason, callback) { + return this._setMembershipState( + roomId, userId, "leave", reason, callback + ); + }, + + _setMembershipState: function(roomId, userId, membershipValue, reason, + callback) { + if (isFunction(reason)) { callback = reason; reason = undefined; } + + var path = encodeUri( + "/rooms/$roomId/state/m.room.member/$userId", + { $roomId: roomId, $userId: userId} + ); + + return this._doAuthedRequest(callback, "PUT", path, undefined, { + membership: membershipValue, + reason: reason + }); + }, + + _membershipChange: function(roomId, userId, membership, reason, + callback) { + if (isFunction(reason)) { callback = reason; reason = undefined; } + + var path = encodeUri("/rooms/$room_id/$membership", { + $room_id: roomId, + $membership: membership + }); + return this._doAuthedRequest( + callback, "POST", path, undefined, { + user_id: userId, // may be undefined e.g. on leave + reason: reason + } + ); + }, + + // Profile operations + // ================== + + getProfileInfo: function(userId, info, callback) { + if (isFunction(info)) { callback = info; info = undefined; } + + var path = info ? + encodeUri("/profile/$userId/$info", + { $userId: userId, $info: info }) : + encodeUri("/profile/$userId", + { $userId: userId }); + return this._doAuthedRequest(callback, "GET", path); + }, + + setProfileInfo: function(info, data, callback) { + var path = encodeUri("/profile/$userId/$info", { + $userId: this.credentials.userId, + $info: info + }); + return this._doAuthedRequest( + callback, "PUT", path, undefined, data + ); + }, + + setDisplayName: function(name, callback) { + return this.setProfileInfo( + "displayname", { displayname: name }, callback + ); + }, + + setAvatarUrl: function(url, callback) { + return this.setProfileInfo( + "avatar_url", { avatar_url: url }, callback + ); + }, + + getThreePids: function(creds, bind, callback) { + var path = "/account/3pid"; + return this._doAuthedV2Request( + callback, "GET", path, undefined, undefined + ); + }, + + addThreePid: function(creds, bind, callback) { + var path = "/account/3pid"; + var data = { + 'threePidCreds': creds, + 'bind': bind + }; + return this._doAuthedV2Request( + callback, "POST", path, undefined, data + ); + }, + + setPresence: function(presence, callback) { + var path = 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._doAuthedRequest( + callback, "PUT", path, undefined, content + ); + }, + + // Public (non-authed) operations + // ============================== + + publicRooms: function(callback) { + return this._doRequest(callback, "GET", "/publicRooms"); + }, + + registerFlows: function(callback) { + return this._doRequest(callback, "GET", "/register"); + }, + + loginFlows: function(callback) { + return this._doRequest(callback, "GET", "/login"); + }, + + resolveRoomAlias: function(roomAlias, callback) { + var path = encodeUri("/directory/room/$alias", {$alias: roomAlias}); + return this._doRequest(callback, "GET", path); + }, + + // Syncing operations + // ================== + + initialSync: function(limit, callback) { + var params = { + limit: limit + }; + return this._doAuthedRequest( + callback, "GET", "/initialSync", params + ); + }, + + roomInitialSync: function(roomId, limit, callback) { + if (isFunction(limit)) { callback = limit; limit = undefined; } + var path = encodeUri("/rooms/$roomId/initialSync", + {$roomId: roomId} + ); + if (!limit) { + limit = 30; + } + return this._doAuthedRequest( + callback, "GET", path, { limit: limit } + ); + }, + + roomState: function(roomId, callback) { + var path = encodeUri("/rooms/$roomId/state", {$roomId: roomId}); + return this._doAuthedRequest(callback, "GET", path); + }, + + scrollback: function(roomId, from, limit, callback) { + if (isFunction(limit)) { callback = limit; limit = undefined; } + var path = encodeUri("/rooms/$roomId/messages", {$roomId: roomId}); + if (!limit) { + limit = 30; + } + var params = { + from: from, + limit: limit, + dir: 'b' + }; + return this._doAuthedRequest(callback, "GET", path, params); + }, + + eventStream: function(from, timeout, callback) { + if (isFunction(timeout)) { callback = timeout; timeout = undefined;} + if (!timeout) { + timeout = 30000; + } + + var params = { + from: from, + timeout: timeout + }; + return this._doAuthedRequest(callback, "GET", "/events", params); + }, + + // Registration/Login operations + // ============================= + + login: function(loginType, data, callback) { + data.type = loginType; + return this._doAuthedRequest( + callback, "POST", "/login", undefined, data + ); + }, + + register: function(loginType, data, callback) { + data.type = loginType; + return this._doAuthedRequest( + callback, "POST", "/register", undefined, data + ); + }, + + loginWithPassword: function(user, password, callback) { + return this.login("m.login.password", { + user: user, + password: password + }, callback); + }, + + // Push operations + // =============== + + pushRules: function(callback) { + return this._doAuthedRequest(callback, "GET", "/pushrules/"); + }, + + addPushRule: function(scope, kind, ruleId, body, callback) { + // NB. Scope not uri encoded because devices need the '/' + var path = encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { + $kind: kind, + $ruleId: ruleId + }); + return this._doAuthedRequest( + callback, "PUT", path, undefined, body + ); + }, + + deletePushRule: function(scope, kind, ruleId, callback) { + // NB. Scope not uri encoded because devices need the '/' + var path = encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { + $kind: kind, + $ruleId: ruleId + }); + return this._doAuthedRequest(callback, "DELETE", path); + }, + + // VoIP operations + // =============== + + turnServer: function(callback) { + return this._doAuthedRequest(callback, "GET", "/voip/turnServer"); + }, + + // URI functions + // ============= + + 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 (Object.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 + + (Object.keys(params).length === 0 ? "" : + ("?" + encodeParams(params))) + fragment; + }, + + getIdenticonUri: function(identiconString, width, height) { + if (!identiconString) { + return; + } + if (!width) { width = 96; } + if (!height) { height = 96; } + var params = { + width: width, + height: height + }; + + var path = encodeUri("/_matrix/media/v1/identicon/$ident", { + $ident: identiconString + }); + return this.credentials.baseUrl + path + + (Object.keys(params).length === 0 ? "" : + ("?" + 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 + }; + }, + + // Internals + // ========= + + _doAuthedRequest: function(callback, method, path, params, data) { + if (!params) { params = {}; } + params.access_token = this.credentials.accessToken; + return this._doRequest(callback, method, path, params, data); + }, + + _doAuthedV2Request: function(callback, method, path, params, data) { + if (!params) { params = {}; } + params.access_token = this.credentials.accessToken; + return this._doV2Request(callback, method, path, params, data); + }, + + _doRequest: function(callback, method, path, params, data) { + var fullUri = this.credentials.baseUrl + CLIENT_PREFIX + path; + if (!params) { params = {}; } + return this._request(callback, method, fullUri, params, data); + }, + + _doV2Request: function(callback, method, path, params, data) { + var fullUri = this.credentials.baseUrl + CLIENT_V2_PREFIX + path; + if (!params) { params = {}; } + return this._request(callback, method, fullUri, params, data); + }, + + _request: function(callback, method, uri, params, data) { + if (callback !== undefined && !isFunction(callback)) { + throw Error("Expected callback to be a function"); + } + + return this.request( + { + uri: uri, + method: method, + withCredentials: false, + qs: params, + body: data, + json: true, + headers: HEADERS, + _matrix_credentials: this.credentials + }, + requestCallback(callback) + ); + } +}; + +var encodeUri = function(pathTemplate, variables) { + for (var key in variables) { + if (!variables.hasOwnProperty(key)) { continue; } + pathTemplate = pathTemplate.replace( + key, encodeURIComponent(variables[key]) + ); + } + return pathTemplate; +}; + +// avoiding deps on jquery and co +var 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); +}; + +var requestCallback = function(userDefinedCallback) { + if (!userDefinedCallback) { + return undefined; + } + return function(err, response, body) { + if (err) { + return userDefinedCallback(err); + } + if (response.statusCode >= 400) { + return userDefinedCallback(body); + } + else { + userDefinedCallback(null, body); + } + }; +}; + +var isFunction = function(value) { + return Object.prototype.toString.call(value) == "[object Function]"; +}; + +/** + * The Matrix HTTP API class. + */ +module.exports = MatrixHttpApi; diff --git a/lib/matrix.js b/lib/matrix.js index c376119d5..394321c4b 100644 --- a/lib/matrix.js +++ b/lib/matrix.js @@ -1,12 +1,13 @@ "use strict"; -/* -TODO: -- CS: complete register function (doing stages) -- Internal: rate limiting -- Identity server: linkEmail, authEmail, bindEmail, lookup3pid -- uploadContent (?) -*/ +/** The Matrix Event class */ +module.exports.MatrixEvent = require("./models/event").MatrixEvent; +/** An in-memory store for the SDK */ +module.exports.MatrixInMemoryStore = require("./store/memory"); +/** The raw HTTP API */ +module.exports.MatrixHttpApi = require("./http-api"); +/** The managed client class */ +module.exports.MatrixClient = require("./client"); // expose the underlying request object so different environments can use // different request libs (e.g. request or browser-request) @@ -19,45 +20,6 @@ module.exports.request = function(r) { request = r; }; -/* - * Construct a Matrix Client. - * @param {Object} credentials The credentials for this client - * @param {Object} config The config (if any) for this client. - * Valid config params include: - * noUserAgent: true // to avoid warnings whilst setting UA headers - * debug: true // to use console.err() style debugging from the lib - * @param {Object} store The data store (if any) for this client. - */ -function MatrixClient(credentials, config, store) { - if (typeof credentials === "string") { - credentials = { - "baseUrl": credentials - }; - } - var requiredKeys = [ - "baseUrl" - ]; - for (var i = 0; i < requiredKeys.length; i++) { - if (!credentials.hasOwnProperty(requiredKeys[i])) { - throw new Error("Missing required key: " + requiredKeys[i]); - } - } - if (config && config.noUserAgent) { - HEADERS = undefined; - } - this.config = config; - this.credentials = credentials; - this.store = store; - - // track our position in the overall eventstream - this.fromToken = undefined; - this.clientRunning = false; -} -/** - * The high-level Matrix Client class. - */ -module.exports.MatrixClient = MatrixClient; // expose the class - /** * Create a new Matrix Client. * @param {Object} credentials The Matrix credentials to use. @@ -66,1055 +28,6 @@ module.exports.MatrixClient = MatrixClient; // expose the class * @return {MatrixClient} A new Matrix Client */ module.exports.createClient = function(credentials, config, store) { - return new MatrixClient(credentials, config, store); + return new module.exports.MatrixClient(credentials, config, store, request); }; -var CLIENT_PREFIX = "/_matrix/client/api/v1"; -var CLIENT_V2_PREFIX = "/_matrix/client/v2_alpha"; -var HEADERS = { - "User-Agent": "matrix-js" -}; - -// Basic DAOs to abstract slightly from the line protocol and let the -// application customise events with domain-specific info -// (e.g. chat-specific semantics) if it so desires. - -/* - * Construct a Matrix Event object - * @param {Object} event The raw event to be wrapped in this DAO - */ -function MatrixEvent(event) { - this.event = event || {}; -} - -/** - * An event from Matrix. - */ -module.exports.MatrixEvent = MatrixEvent; - -MatrixEvent.prototype = { - getId: function() { - return this.event.event_id; - }, - getSender: function() { - return this.event.user_id; - }, - getType: function() { - return this.event.type; - }, - getRoomId: function() { - return this.event.room_id; - }, - getTs: function() { - return this.event.ts; - }, - getContent: function() { - return this.event.content; - }, - isState: function() { - return this.event.state_key !== undefined; - }, -}; - -function MatrixInMemoryStore() { - this.rooms = { - // state: { }, - // timeline: [ ], - }; - - this.presence = { - // presence objects keyed by userId - }; -} - -/** - * An in-memory store for Matrix. - */ -module.exports.MatrixInMemoryStore = MatrixInMemoryStore; - -// XXX: this is currently quite procedural - we could possibly pass back -// models of Rooms, Users, Events, etc instead. -MatrixInMemoryStore.prototype = { - - /* - * Add an array of one or more state MatrixEvents into the store, overwriting - * any existing state with the same {room, type, stateKey} tuple. - */ - setStateEvents: function(stateEvents) { - // we store stateEvents indexed by room, event type and state key. - for (var i = 0; i < stateEvents.length; i++) { - var event = stateEvents[i].event; - var roomId = event.room_id; - if (this.rooms[roomId] === undefined) { - this.rooms[roomId] = {}; - } - if (this.rooms[roomId].state === undefined) { - this.rooms[roomId].state = {}; - } - if (this.rooms[roomId].state[event.type] === undefined) { - this.rooms[roomId].state[event.type] = {}; - } - this.rooms[roomId].state[event.type][event.state_key] = stateEvents[i]; - } - }, - - /* - * Add a single state MatrixEvents into the store, overwriting - * any existing state with the same {room, type, stateKey} tuple. - */ - setStateEvent: function(stateEvent) { - this.setStateEvents([stateEvent]); - }, - - /* - * Return a list of MatrixEvents from the store - * @param {String} roomId the Room ID whose state is to be returned - * @param {String} type the type of the state events to be returned (optional) - * @param {String} stateKey the stateKey of the state events to be returned - * (optional, requires type to be specified) - * @return {MatrixEvent[]} an array of MatrixEvents from the store, - * filtered by roomid, type and state key. - */ - getStateEvents: function(roomId, type, stateKey) { - var stateEvents = []; - if (stateKey === undefined && type === undefined) { - for (type in this.rooms[roomId].state) { - if (this.rooms[roomId].state.hasOwnProperty(type)) { - for (stateKey in this.rooms[roomId].state[type]) { - if (this.rooms[roomId].state[type].hasOwnProperty(stateKey)) { - stateEvents.push( - this.rooms[roomId].state[type][stateKey] - ); - } - } - } - } - return stateEvents; - } - else if (stateKey === undefined) { - for (stateKey in this.rooms[roomId].state[type]) { - if (this.rooms[roomId].state[type].hasOwnProperty(stateKey)) { - stateEvents.push(this.rooms[roomId].state[type][stateKey]); - } - } - return stateEvents; - } - else { - return [this.rooms[roomId].state[type][stateKey]]; - } - }, - - /* - * Return a single state MatrixEvent from the store for the given roomId - * and type. - * @param {String} roomId the Room ID whose state is to be returned - * @param {String} type the type of the state events to be returned - * @param {String} stateKey the stateKey of the state events to be returned - * @return {MatrixEvent} a single MatrixEvent from the store, filtered - * by roomid, type and state key. - */ - getStateEvent: function(roomId, type, stateKey) { - return this.rooms[roomId].state[type][stateKey]; - }, - - /* - * Adds a list of arbitrary MatrixEvents into the store. - * If the event is a state event, it is also updates state. - */ - setEvents: function(events) { - for (var i = 0; i < events.length; i++) { - var event = events[i].event; - if (event.type === "m.presence") { - this.setPresenceEvents([events[i]]); - continue; - } - var roomId = event.room_id; - if (this.rooms[roomId] === undefined) { - this.rooms[roomId] = {}; - } - if (this.rooms[roomId].timeline === undefined) { - this.rooms[roomId].timeline = []; - } - if (event.state_key !== undefined) { - this.setStateEvents([events[i]]); - } - this.rooms[roomId].timeline.push(events[i]); - } - }, - - /* - * Get the timeline of events for a given room - * TODO: ordering! - */ - getEvents: function(roomId) { - return this.room[roomId].timeline; - }, - - setPresenceEvents: function(presenceEvents) { - for (var i = 0; i < presenceEvents.length; i++) { - var matrixEvent = presenceEvents[i]; - this.presence[matrixEvent.event.user_id] = matrixEvent; - } - }, - - getPresenceEvents: function(userId) { - return this.presence[userId]; - }, - - getRoomList: function() { - var roomIds = []; - for (var roomId in this.rooms) { - if (this.rooms.hasOwnProperty(roomId)) { - roomIds.push(roomId); - } - } - return roomIds; - }, - - // TODO - //setMaxHistoryPerRoom: function(maxHistory) {}, - - // TODO - //reapOldMessages: function() {}, -}; - -MatrixClient.prototype = { - isLoggedIn: function() { - return this.credentials.accessToken !== undefined && - this.credentials.userId !== 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 - - /* - * Helper method for retrieving the name of a room suitable for display - * in the UI - * TODO: in future, this should be being generated serverside. - * @param {String} roomId ID of room whose name is to be resolved - * @return {String} human-readable label for room. - */ - getFriendlyRoomName: function(roomId) { - // we need a store to track the inputs for calculating room names - if (!this.store) { - return roomId; - } - - // check for an alias, if any. for now, assume first alias is the - // official one. - var alias; - var mRoomAliases = this.store.getStateEvents(roomId, 'm.room.aliases')[0]; - if (mRoomAliases) { - alias = mRoomAliases.event.content.aliases[0]; - } - - var mRoomName = this.store.getStateEvent(roomId, 'm.room.name', ''); - if (mRoomName) { - return mRoomName.event.content.name + (alias ? " (" + alias + ")" : ""); - } - else if (alias) { - return alias; - } - else { - var userId = this.credentials.userId; - var members = this.store.getStateEvents(roomId, 'm.room.member') - .filter(function(event) { - return event.event.user_id !== userId; - }); - - if (members.length === 0) { - return "Unknown"; - } - else if (members.length == 1) { - return ( - members[0].event.content.displayname || - members[0].event.user_id - ); - } - else if (members.length == 2) { - return ( - (members[0].event.content.displayname || - members[0].event.user_id) + - " and " + - (members[1].event.content.displayname || - members[1].event.user_id) - ); - } - else { - return ( - (members[0].event.content.displayname || - members[0].event.user_id) + - " and " + - (members.length - 1) + " others" - ); - } - } - }, - - /* - * Helper method for retrieving the name of a user suitable for display - * in the UI in the context of a room - i.e. disambiguating from any - * other users in the room. - * XXX: This could perhaps also be generated serverside, perhaps by just passing - * a 'disambiguate' flag down on membership entries which have ambiguous - * displaynames? - * @param {String} userId ID of the user whose name is to be resolved - * @param {String} roomId ID of room to be used as the context for - * resolving the name. - * @return {String} human-readable name of the user. - */ - getFriendlyDisplayName: function(userId, roomId) { - // we need a store to track the inputs for calculating display names - if (!this.store) { return userId; } - - var displayName; - var memberEvent = this.store.getStateEvent(roomId, 'm.room.member', userId); - if (memberEvent && memberEvent.event.content.displayname) { - displayName = memberEvent.event.content.displayname; - } - else { - return userId; - } - - var members = this.store.getStateEvents(roomId, 'm.room.member') - .filter(function(event) { - return event.event.content.displayname === displayName; - }); - - if (members.length > 1) { - return displayName + " (" + userId + ")"; - } - else { - return displayName; - } - }, - - /* - * High level helper method to call initialSync, emit the resulting events, - * and then start polling the eventStream for new events. - * @param {function} callback Callback invoked whenever new event are available - * @param {Number} historyLen amount of historical timeline events to - * emit during from the initial sync. - */ - startClient: function(callback, historyLen) { - historyLen = historyLen || 12; - - var self = this; - if (!this.fromToken) { - this.initialSync(historyLen, function(err, data) { - if (err) { - if (this.config && this.config.debug) { - console.error( - "startClient error on initialSync: %s", - JSON.stringify(err) - ); - } - callback(err); - } else { - var events = []; - var i, j; - for (i = 0; i < data.presence.length; i++) { - events.push(new MatrixEvent(data.presence[i])); - } - for (i = 0; i < data.rooms.length; i++) { - for (j = 0; j < data.rooms[i].state.length; j++) { - events.push(new MatrixEvent(data.rooms[i].state[j])); - } - for (j = 0; j < data.rooms[i].messages.chunk.length; j++) { - events.push( - new MatrixEvent(data.rooms[i].messages.chunk[j]) - ); - } - } - callback(undefined, events, false); - self.clientRunning = true; - self._pollForEvents(callback); - } - }); - } - else { - this._pollForEvents(callback); - } - }, - - _pollForEvents: function(callback) { - var self = this; - if (!this.clientRunning) { - return; - } - this.eventStream(this.fromToken, 30000, function(err, data) { - if (err) { - if (this.config && this.config.debug) { - console.error( - "error polling for events via eventStream: %s", - JSON.stringify(err) - ); - } - callback(err); - // retry every few seconds - // FIXME: this should be exponential backoff with an option to nudge - setTimeout(function() { - self._pollForEvents(callback); - }, 2000); - } else { - var events = []; - for (var j = 0; j < data.chunk.length; j++) { - events.push(new MatrixEvent(data.chunk[j])); - } - callback(undefined, events, true); - self._pollForEvents(callback); - } - }); - }, - - /* - * High level helper method to stop the client from polling and allow a - * clean shutdown. - */ - stopClient: function() { - this.clientRunning = false; - }, - - // Room operations - // =============== - - createRoom: function(options, callback) { - // valid options include: room_alias_name, visibility, invite - return this._doAuthedRequest( - callback, "POST", "/createRoom", undefined, options - ); - }, - - joinRoom: function(roomIdOrAlias, callback) { - var path = encodeUri("/join/$roomid", { $roomid: roomIdOrAlias}); - return this._doAuthedRequest(callback, "POST", path, undefined, {}); - }, - - setRoomName: function(roomId, name, callback) { - return this.sendStateEvent(roomId, "m.room.name", {name: name}, - undefined, callback); - }, - - setRoomTopic: function(roomId, topic, callback) { - return this.sendStateEvent(roomId, "m.room.topic", {topic: topic}, - undefined, callback); - }, - - 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 = encodeUri("/rooms/$roomId/state/m.room.power_levels", { - $roomId: roomId - }); - return this._doAuthedRequest( - callback, "PUT", path, undefined, content - ); - }, - - getStateEvent: function(roomId, eventType, stateKey, callback) { - var pathParams = { - $roomId: roomId, - $eventType: eventType, - $stateKey: stateKey - }; - var path = encodeUri("/rooms/$roomId/state/$eventType", pathParams); - if (stateKey !== undefined) { - path = encodeUri(path + "/$stateKey", pathParams); - } - return this._doAuthedRequest( - callback, "GET", path - ); - }, - - sendStateEvent: function(roomId, eventType, content, stateKey, - callback) { - var pathParams = { - $roomId: roomId, - $eventType: eventType, - $stateKey: stateKey - }; - var path = encodeUri("/rooms/$roomId/state/$eventType", pathParams); - if (stateKey !== undefined) { - path = encodeUri(path + "/$stateKey", pathParams); - } - return this._doAuthedRequest( - callback, "PUT", path, undefined, content - ); - }, - - sendEvent: function(roomId, eventType, content, txnId, callback) { - if (isFunction(txnId)) { callback = txnId; txnId = undefined; } - - if (!txnId) { - txnId = "m" + new Date().getTime(); - } - - var path = encodeUri("/rooms/$roomId/send/$eventType/$txnId", { - $roomId: roomId, - $eventType: eventType, - $txnId: txnId - }); - return this._doAuthedRequest( - callback, "PUT", path, undefined, content - ); - }, - - sendMessage: function(roomId, content, txnId, callback) { - if (isFunction(txnId)) { callback = txnId; txnId = undefined; } - return this.sendEvent( - roomId, "m.room.message", content, txnId, callback - ); - }, - - sendTextMessage: function(roomId, body, txnId, callback) { - var content = { - msgtype: "m.text", - body: body - }; - return this.sendMessage(roomId, content, txnId, callback); - }, - - sendEmoteMessage: function(roomId, body, txnId, callback) { - var content = { - msgtype: "m.emote", - body: body - }; - return this.sendMessage(roomId, content, txnId, callback); - }, - - sendImageMessage: function(roomId, url, info, text, callback) { - if (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); - }, - - 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); - }, - - sendTyping: function(roomId, isTyping, timeoutMs, callback) { - var path = encodeUri("/rooms/$roomId/typing/$userId", { - $roomId: roomId, - $userId: this.credentials.userId - }); - var data = { - typing: isTyping - }; - if (isTyping) { - data.timeout = timeoutMs ? timeoutMs : 20000; - } - return this._doAuthedRequest( - callback, "PUT", path, undefined, data - ); - }, - - redactEvent: function(roomId, eventId, callback) { - var path = encodeUri("/rooms/$roomId/redact/$eventId", { - $roomId: roomId, - $eventId: eventId - }); - return this._doAuthedRequest(callback, "POST", path, undefined, {}); - }, - - invite: function(roomId, userId, callback) { - return this._membershipChange(roomId, userId, "invite", undefined, - callback); - }, - - leave: function(roomId, callback) { - return this._membershipChange(roomId, undefined, "leave", undefined, - callback); - }, - - ban: function(roomId, userId, reason, callback) { - return this._membershipChange(roomId, userId, "ban", reason, - callback); - }, - - unban: function(roomId, userId, callback) { - // unbanning = set their state to leave - return this._setMembershipState( - roomId, userId, "leave", undefined, callback - ); - }, - - kick: function(roomId, userId, reason, callback) { - return this._setMembershipState( - roomId, userId, "leave", reason, callback - ); - }, - - _setMembershipState: function(roomId, userId, membershipValue, reason, - callback) { - if (isFunction(reason)) { callback = reason; reason = undefined; } - - var path = encodeUri( - "/rooms/$roomId/state/m.room.member/$userId", - { $roomId: roomId, $userId: userId} - ); - - return this._doAuthedRequest(callback, "PUT", path, undefined, { - membership: membershipValue, - reason: reason - }); - }, - - _membershipChange: function(roomId, userId, membership, reason, - callback) { - if (isFunction(reason)) { callback = reason; reason = undefined; } - - var path = encodeUri("/rooms/$room_id/$membership", { - $room_id: roomId, - $membership: membership - }); - return this._doAuthedRequest( - callback, "POST", path, undefined, { - user_id: userId, // may be undefined e.g. on leave - reason: reason - } - ); - }, - - // Profile operations - // ================== - - getProfileInfo: function(userId, info, callback) { - if (isFunction(info)) { callback = info; info = undefined; } - - var path = info ? - encodeUri("/profile/$userId/$info", - { $userId: userId, $info: info }) : - encodeUri("/profile/$userId", - { $userId: userId }); - return this._doAuthedRequest(callback, "GET", path); - }, - - setProfileInfo: function(info, data, callback) { - var path = encodeUri("/profile/$userId/$info", { - $userId: this.credentials.userId, - $info: info - }); - return this._doAuthedRequest( - callback, "PUT", path, undefined, data - ); - }, - - setDisplayName: function(name, callback) { - return this.setProfileInfo( - "displayname", { displayname: name }, callback - ); - }, - - setAvatarUrl: function(url, callback) { - return this.setProfileInfo( - "avatar_url", { avatar_url: url }, callback - ); - }, - - getThreePids: function(creds, bind, callback) { - var path = "/account/3pid"; - return this._doAuthedV2Request( - callback, "GET", path, undefined, undefined - ); - }, - - addThreePid: function(creds, bind, callback) { - var path = "/account/3pid"; - var data = { - 'threePidCreds': creds, - 'bind': bind - }; - return this._doAuthedV2Request( - callback, "POST", path, undefined, data - ); - }, - - setPresence: function(presence, callback) { - var path = 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._doAuthedRequest( - callback, "PUT", path, undefined, content - ); - }, - - // Public (non-authed) operations - // ============================== - - publicRooms: function(callback) { - return this._doRequest(callback, "GET", "/publicRooms"); - }, - - registerFlows: function(callback) { - return this._doRequest(callback, "GET", "/register"); - }, - - loginFlows: function(callback) { - return this._doRequest(callback, "GET", "/login"); - }, - - resolveRoomAlias: function(roomAlias, callback) { - var path = encodeUri("/directory/room/$alias", {$alias: roomAlias}); - return this._doRequest(callback, "GET", path); - }, - - // Syncing operations - // ================== - - initialSync: function(limit, callback) { - var params = { - limit: limit - }; - var self = this; - return this._doAuthedRequest( - function(err, data) { - if (self.store) { - var eventMapper = function(event) { - return new MatrixEvent(event); - }; - // intercept the results and put them into our store - self.store.setPresenceEvents( - map(data.presence, eventMapper) - ); - for (var i = 0; i < data.rooms.length; i++) { - self.store.setStateEvents( - map(data.rooms[i].state, eventMapper) - ); - self.store.setEvents( - map(data.rooms[i].messages.chunk, eventMapper) - ); - } - } - if (data) { - self.fromToken = data.end; - } - callback(err, data); // continue with original callback - }, "GET", "/initialSync", params - ); - }, - - roomInitialSync: function(roomId, limit, callback) { - if (isFunction(limit)) { callback = limit; limit = undefined; } - var path = encodeUri("/rooms/$roomId/initialSync", - {$roomId: roomId} - ); - if (!limit) { - limit = 30; - } - return this._doAuthedRequest( - callback, "GET", path, { limit: limit } - ); - }, - - roomState: function(roomId, callback) { - var path = encodeUri("/rooms/$roomId/state", {$roomId: roomId}); - return this._doAuthedRequest(callback, "GET", path); - }, - - scrollback: function(roomId, from, limit, callback) { - if (isFunction(limit)) { callback = limit; limit = undefined; } - var path = encodeUri("/rooms/$roomId/messages", {$roomId: roomId}); - if (!limit) { - limit = 30; - } - var params = { - from: from, - limit: limit, - dir: 'b' - }; - return this._doAuthedRequest(callback, "GET", path, params); - }, - - eventStream: function(from, timeout, callback) { - if (isFunction(timeout)) { callback = timeout; timeout = undefined;} - if (!timeout) { - timeout = 30000; - } - - var params = { - from: from, - timeout: timeout - }; - var self = this; - return this._doAuthedRequest( - function(err, data) { - if (self.store) { - self.store.setEvents(map(data.chunk, - function(event) { - return new MatrixEvent(event); - } - )); - } - if (data) { - self.fromToken = data.end; - } - callback(err, data); // continue with original callback - }, "GET", "/events", params); - }, - - // Registration/Login operations - // ============================= - - login: function(loginType, data, callback) { - data.type = loginType; - return this._doAuthedRequest( - callback, "POST", "/login", undefined, data - ); - // XXX: surely we should store the results of this into our credentials - }, - - register: function(loginType, data, callback) { - data.type = loginType; - return this._doAuthedRequest( - callback, "POST", "/register", undefined, data - ); - }, - - loginWithPassword: function(user, password, callback) { - return this.login("m.login.password", { - user: user, - password: password - }, callback); - }, - - // Push operations - // =============== - - pushRules: function(callback) { - return this._doAuthedRequest(callback, "GET", "/pushrules/"); - }, - - addPushRule: function(scope, kind, ruleId, body, callback) { - // NB. Scope not uri encoded because devices need the '/' - var path = encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { - $kind: kind, - $ruleId: ruleId - }); - return this._doAuthedRequest( - callback, "PUT", path, undefined, body - ); - }, - - deletePushRule: function(scope, kind, ruleId, callback) { - // NB. Scope not uri encoded because devices need the '/' - var path = encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { - $kind: kind, - $ruleId: ruleId - }); - return this._doAuthedRequest(callback, "DELETE", path); - }, - - // VoIP operations - // =============== - - turnServer: function(callback) { - return this._doAuthedRequest(callback, "GET", "/voip/turnServer"); - }, - - // URI functions - // ============= - - 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 (Object.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 + - (Object.keys(params).length === 0 ? "" : - ("?" + encodeParams(params))) + fragment; - }, - - getIdenticonUri: function(identiconString, width, height) { - if (!identiconString) { - return; - } - if (!width) { width = 96; } - if (!height) { height = 96; } - var params = { - width: width, - height: height - }; - - var path = encodeUri("/_matrix/media/v1/identicon/$ident", { - $ident: identiconString - }); - return this.credentials.baseUrl + path + - (Object.keys(params).length === 0 ? "" : - ("?" + 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 - }; - }, - - // Internals - // ========= - - _doAuthedRequest: function(callback, method, path, params, data) { - if (!params) { params = {}; } - params.access_token = this.credentials.accessToken; - return this._doRequest(callback, method, path, params, data); - }, - - _doAuthedV2Request: function(callback, method, path, params, data) { - if (!params) { params = {}; } - params.access_token = this.credentials.accessToken; - return this._doV2Request(callback, method, path, params, data); - }, - - _doRequest: function(callback, method, path, params, data) { - var fullUri = this.credentials.baseUrl + CLIENT_PREFIX + path; - if (!params) { params = {}; } - return this._request(callback, method, fullUri, params, data); - }, - - _doV2Request: function(callback, method, path, params, data) { - var fullUri = this.credentials.baseUrl + CLIENT_V2_PREFIX + path; - if (!params) { params = {}; } - return this._request(callback, method, fullUri, params, data); - }, - - _request: function(callback, method, uri, params, data) { - if (callback !== undefined && !isFunction(callback)) { - throw Error("Expected callback to be a function"); - } - - return request( - { - uri: uri, - method: method, - withCredentials: false, - qs: params, - body: data, - json: true, - headers: HEADERS, - _matrix_credentials: this.credentials - }, - requestCallback(callback) - ); - } -}; - -var encodeUri = function(pathTemplate, variables) { - for (var key in variables) { - if (!variables.hasOwnProperty(key)) { continue; } - pathTemplate = pathTemplate.replace( - key, encodeURIComponent(variables[key]) - ); - } - return pathTemplate; -}; - -// avoiding deps on jquery and co -var 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); -}; - -var requestCallback = function(userDefinedCallback) { - if (!userDefinedCallback) { - return undefined; - } - return function(err, response, body) { - if (err) { - return userDefinedCallback(err); - } - if (response.statusCode >= 400) { - return userDefinedCallback(body); - } - else { - userDefinedCallback(null, body); - } - }; -}; - -var isFunction = function(value) { - return Object.prototype.toString.call(value) == "[object Function]"; -}; - -var 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; -}; diff --git a/lib/models/event.js b/lib/models/event.js new file mode 100644 index 000000000..431f10394 --- /dev/null +++ b/lib/models/event.js @@ -0,0 +1,37 @@ +"use strict"; + +/* + * Construct a Matrix Event object + * @param {Object} event The raw event to be wrapped in this DAO + */ +function MatrixEvent(event) { + this.event = event || {}; +} +MatrixEvent.prototype = { + getId: function() { + return this.event.event_id; + }, + getSender: function() { + return this.event.user_id; + }, + getType: function() { + return this.event.type; + }, + getRoomId: function() { + return this.event.room_id; + }, + getTs: function() { + return this.event.ts; + }, + getContent: function() { + return this.event.content; + }, + isState: function() { + return this.event.state_key !== undefined; + }, +}; + +/** + * An event from Matrix. + */ +module.exports.MatrixEvent = MatrixEvent; diff --git a/lib/models/room.js b/lib/models/room.js new file mode 100644 index 000000000..a84dfce20 --- /dev/null +++ b/lib/models/room.js @@ -0,0 +1,14 @@ +"use strict"; + +function Room(roomId) { + this.roomId = roomId; + this.name = roomId; + this.timeline = []; + this.oldState = null; + this.currentState = null; +} + +/** + * The Room class. + */ +module.exports = Room; diff --git a/lib/store/memory.js b/lib/store/memory.js new file mode 100644 index 000000000..b62125ec9 --- /dev/null +++ b/lib/store/memory.js @@ -0,0 +1,163 @@ +"use strict"; + +function MatrixInMemoryStore() { + this.rooms = { + // state: { }, + // timeline: [ ], + }; + + this.presence = { + // presence objects keyed by userId + }; +} + +// XXX: this is currently quite procedural - we could possibly pass back +// models of Rooms, Users, Events, etc instead. +MatrixInMemoryStore.prototype = { + + /* + * Add an array of one or more state MatrixEvents into the store, overwriting + * any existing state with the same {room, type, stateKey} tuple. + */ + setStateEvents: function(stateEvents) { + // we store stateEvents indexed by room, event type and state key. + for (var i = 0; i < stateEvents.length; i++) { + var event = stateEvents[i].event; + var roomId = event.room_id; + if (this.rooms[roomId] === undefined) { + this.rooms[roomId] = {}; + } + if (this.rooms[roomId].state === undefined) { + this.rooms[roomId].state = {}; + } + if (this.rooms[roomId].state[event.type] === undefined) { + this.rooms[roomId].state[event.type] = {}; + } + this.rooms[roomId].state[event.type][event.state_key] = stateEvents[i]; + } + }, + + /* + * Add a single state MatrixEvents into the store, overwriting + * any existing state with the same {room, type, stateKey} tuple. + */ + setStateEvent: function(stateEvent) { + this.setStateEvents([stateEvent]); + }, + + /* + * Return a list of MatrixEvents from the store + * @param {String} roomId the Room ID whose state is to be returned + * @param {String} type the type of the state events to be returned (optional) + * @param {String} stateKey the stateKey of the state events to be returned + * (optional, requires type to be specified) + * @return {MatrixEvent[]} an array of MatrixEvents from the store, + * filtered by roomid, type and state key. + */ + getStateEvents: function(roomId, type, stateKey) { + var stateEvents = []; + if (stateKey === undefined && type === undefined) { + for (type in this.rooms[roomId].state) { + if (this.rooms[roomId].state.hasOwnProperty(type)) { + for (stateKey in this.rooms[roomId].state[type]) { + if (this.rooms[roomId].state[type].hasOwnProperty(stateKey)) { + stateEvents.push( + this.rooms[roomId].state[type][stateKey] + ); + } + } + } + } + return stateEvents; + } + else if (stateKey === undefined) { + for (stateKey in this.rooms[roomId].state[type]) { + if (this.rooms[roomId].state[type].hasOwnProperty(stateKey)) { + stateEvents.push(this.rooms[roomId].state[type][stateKey]); + } + } + return stateEvents; + } + else { + return [this.rooms[roomId].state[type][stateKey]]; + } + }, + + /* + * Return a single state MatrixEvent from the store for the given roomId + * and type. + * @param {String} roomId the Room ID whose state is to be returned + * @param {String} type the type of the state events to be returned + * @param {String} stateKey the stateKey of the state events to be returned + * @return {MatrixEvent} a single MatrixEvent from the store, filtered + * by roomid, type and state key. + */ + getStateEvent: function(roomId, type, stateKey) { + return this.rooms[roomId].state[type][stateKey]; + }, + + /* + * Adds a list of arbitrary MatrixEvents into the store. + * If the event is a state event, it is also updates state. + */ + setEvents: function(events) { + for (var i = 0; i < events.length; i++) { + var event = events[i].event; + if (event.type === "m.presence") { + this.setPresenceEvents([events[i]]); + continue; + } + var roomId = event.room_id; + if (this.rooms[roomId] === undefined) { + this.rooms[roomId] = {}; + } + if (this.rooms[roomId].timeline === undefined) { + this.rooms[roomId].timeline = []; + } + if (event.state_key !== undefined) { + this.setStateEvents([events[i]]); + } + this.rooms[roomId].timeline.push(events[i]); + } + }, + + /* + * Get the timeline of events for a given room + * TODO: ordering! + */ + getEvents: function(roomId) { + return this.room[roomId].timeline; + }, + + setPresenceEvents: function(presenceEvents) { + for (var i = 0; i < presenceEvents.length; i++) { + var matrixEvent = presenceEvents[i]; + this.presence[matrixEvent.event.user_id] = matrixEvent; + } + }, + + getPresenceEvents: function(userId) { + return this.presence[userId]; + }, + + getRoomList: function() { + var roomIds = []; + for (var roomId in this.rooms) { + if (this.rooms.hasOwnProperty(roomId)) { + roomIds.push(roomId); + } + } + return roomIds; + }, + + // TODO + //setMaxHistoryPerRoom: function(maxHistory) {}, + + // TODO + //reapOldMessages: function() {}, +}; + +/** + * An in-memory store for Matrix. + */ +module.exports = MatrixInMemoryStore;