UNPKG

matrix-js-sdk

Version:

Matrix Client-Server SDK for Javascript

1,509 lines (1,325 loc) 131 kB
/* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd 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. */ "use strict"; var _typeof2 = require("babel-runtime/helpers/typeof"); var _typeof3 = _interopRequireDefault(_typeof2); var _assign = require("babel-runtime/core-js/object/assign"); var _assign2 = _interopRequireDefault(_assign); var _stringify = require("babel-runtime/core-js/json/stringify"); var _stringify2 = _interopRequireDefault(_stringify); var _keys = require("babel-runtime/core-js/object/keys"); var _keys2 = _interopRequireDefault(_keys); var _bluebird = require("bluebird"); var _bluebird2 = _interopRequireDefault(_bluebird); var _regenerator = require("babel-runtime/regenerator"); var _regenerator2 = _interopRequireDefault(_regenerator); var _setDeviceVerification = function () { var _ref4 = (0, _bluebird.coroutine)(_regenerator2.default.mark(function _callee2(client, userId, deviceId, verified, blocked, known) { var dev; return _regenerator2.default.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: if (client._crypto) { _context2.next = 2; break; } throw new Error("End-to-End encryption disabled"); case 2: _context2.next = 4; return (0, _bluebird.resolve)(client._crypto.setDeviceVerification(userId, deviceId, verified, blocked, known)); case 4: dev = _context2.sent; client.emit("deviceVerificationChanged", userId, deviceId, dev); case 6: case "end": return _context2.stop(); } } }, _callee2, this); })); return function _setDeviceVerification(_x4, _x5, _x6, _x7, _x8, _x9) { return _ref4.apply(this, arguments); }; }(); /** * Set the global override for whether the client should ever send encrypted * messages to unverified devices. If false, it can still be overridden * per-room. If true, it overrides the per-room settings. * * @param {boolean} value whether to unilaterally blacklist all * unverified devices */ var _ReEmitter = require("./ReEmitter"); var _ReEmitter2 = _interopRequireDefault(_ReEmitter); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var PushProcessor = require('./pushprocessor'); /** * This is an internal module. See {@link MatrixClient} for the public class. * @module client */ var EventEmitter = require("events").EventEmitter; var url = require('url'); var httpApi = require("./http-api"); var MatrixEvent = require("./models/event").MatrixEvent; var EventStatus = require("./models/event").EventStatus; var EventTimeline = require("./models/event-timeline"); var SearchResult = require("./models/search-result"); var StubStore = require("./store/stub"); var webRtcCall = require("./webrtc/call"); var utils = require("./utils"); var contentRepo = require("./content-repo"); var Filter = require("./filter"); var SyncApi = require("./sync"); var MatrixBaseApis = require("./base-apis"); var MatrixError = httpApi.MatrixError; var SCROLLBACK_DELAY_MS = 3000; var CRYPTO_ENABLED = false; try { var Crypto = require("./crypto"); CRYPTO_ENABLED = true; } catch (e) { console.warn("Unable to load crypto module: crypto will be disabled: " + e); } /** * Construct a Matrix Client. Only directly construct this if you want to use * custom modules. Normally, {@link createClient} should be used * as it specifies 'sensible' defaults for these modules. * @constructor * @extends {external:EventEmitter} * @extends {module:base-apis~MatrixBaseApis} * * @param {Object} opts The configuration options for this client. * @param {string} opts.baseUrl Required. The base URL to the client-server * HTTP API. * @param {string} opts.idBaseUrl Optional. The base identity server URL for * identity server requests. * @param {Function} opts.request Required. The function to invoke for HTTP * requests. The value of this property is typically <code>require("request") * </code> as it returns a function which meets the required interface. See * {@link requestFunction} for more information. * * @param {string} opts.accessToken The access_token for this user. * * @param {string} opts.userId The user ID for this user. * * @param {Object=} opts.store The data store to use. If not specified, * this client will not store any HTTP responses. * * @param {string=} opts.deviceId A unique identifier for this device; used for * tracking things like crypto keys and access tokens. If not specified, * end-to-end crypto will be disabled. * * @param {Object=} opts.sessionStore A store to be used for end-to-end crypto * session data. This should be a {@link * module:store/session/webstorage~WebStorageSessionStore|WebStorageSessionStore}, * or an object implementing the same interface. If not specified, * end-to-end crypto will be disabled. * * @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}. * * @param {Object} opts.queryParams Optional. Extra query parameters to append * to all requests with this client. Useful for application services which require * <code>?user_id=</code>. * * @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of * time to wait before timing out HTTP requests. If not specified, there is no timeout. * * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use * Authorization header instead of query param to send the access token to the server. * * @param {boolean} [opts.timelineSupport = false] Set to true to enable * improved timeline support ({@link * module:client~MatrixClient#getEventTimeline getEventTimeline}). It is * disabled by default for compatibility with older clients - in particular to * maintain support for back-paginating the live timeline after a '/sync' * result with a gap. * * @param {module:crypto.store.base~CryptoStore} opts.cryptoStore * crypto store implementation. */ function MatrixClient(opts) { var _this = this; // Allow trailing slash in HS url if (opts.baseUrl && opts.baseUrl.endsWith("/")) { opts.baseUrl = opts.baseUrl.substr(0, opts.baseUrl.length - 1); } // Allow trailing slash in IS url if (opts.idBaseUrl && opts.idBaseUrl.endsWith("/")) { opts.idBaseUrl = opts.idBaseUrl.substr(0, opts.idBaseUrl.length - 1); } MatrixBaseApis.call(this, opts); this.reEmitter = new _ReEmitter2.default(this); this.store = opts.store || new StubStore(); this.deviceId = opts.deviceId || null; var userId = opts.userId || null; this.credentials = { userId: userId }; this.scheduler = opts.scheduler; if (this.scheduler) { (function () { var self = _this; _this.scheduler.setProcessFunction(function (eventToSend) { var room = self.getRoom(eventToSend.getRoomId()); if (eventToSend.status !== EventStatus.SENDING) { _updatePendingEventStatus(room, eventToSend, EventStatus.SENDING); } return _sendEventHttpRequest(self, eventToSend); }); })(); } this.clientRunning = false; this.callList = { // callId: MatrixCall }; // try constructing a MatrixCall to see if we are running in an environment // which has WebRTC. If we are, listen for and handle m.call.* events. var call = webRtcCall.createNewMatrixCall(this); this._supportsVoip = false; if (call) { setupCallEventHandler(this); this._supportsVoip = true; } this._syncingRetry = null; this._syncApi = null; this._peekSync = null; this._isGuest = false; this._ongoingScrollbacks = {}; this.timelineSupport = Boolean(opts.timelineSupport); this.urlPreviewCache = {}; this._notifTimelineSet = null; this._crypto = null; this._cryptoStore = opts.cryptoStore; this._sessionStore = opts.sessionStore; this._forceTURN = opts.forceTURN || false; if (CRYPTO_ENABLED) { this.olmVersion = Crypto.getOlmVersion(); } } utils.inherits(MatrixClient, EventEmitter); utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype); /** * Clear any data out of the persistent stores used by the client. * * @returns {Promise} Promise which resolves when the stores have been cleared. */ MatrixClient.prototype.clearStores = function () { if (this._clientRunning) { throw new Error("Cannot clear stores while client is running"); } var promises = []; promises.push(this.store.deleteAllData()); if (this._cryptoStore) { promises.push(this._cryptoStore.deleteAllData()); } return _bluebird2.default.all(promises); }; /** * Get the user-id of the logged-in user * * @return {?string} MXID for the logged-in user, or null if not logged in */ MatrixClient.prototype.getUserId = function () { if (this.credentials && this.credentials.userId) { return this.credentials.userId; } return null; }; /** * Get the domain for this client's MXID * @return {?string} Domain of this MXID */ MatrixClient.prototype.getDomain = function () { if (this.credentials && this.credentials.userId) { return this.credentials.userId.replace(/^.*?:/, ''); } return null; }; /** * Get the local part of the current user ID e.g. "foo" in "@foo:bar". * @return {?string} The user ID localpart or null. */ MatrixClient.prototype.getUserIdLocalpart = function () { if (this.credentials && this.credentials.userId) { return this.credentials.userId.split(":")[0].substring(1); } return null; }; /** * Get the device ID of this client * @return {?string} device ID */ MatrixClient.prototype.getDeviceId = function () { return this.deviceId; }; /** * Check if the runtime environment supports VoIP calling. * @return {boolean} True if VoIP is supported. */ MatrixClient.prototype.supportsVoip = function () { return this._supportsVoip; }; /** * Set whether VoIP calls are forced to use only TURN * candidates. This is the same as the forceTURN option * when creating the client. * @param {bool} forceTURN True to force use of TURN servers */ MatrixClient.prototype.setForceTURN = function (forceTURN) { this._forceTURN = forceTURN; }; /** * Get the current sync state. * @return {?string} the sync state, which may be null. * @see module:client~MatrixClient#event:"sync" */ MatrixClient.prototype.getSyncState = function () { if (!this._syncApi) { return null; } return this._syncApi.getSyncState(); }; /** * Return whether the client is configured for a guest account. * @return {boolean} True if this is a guest access_token (or no token is supplied). */ MatrixClient.prototype.isGuest = function () { return this._isGuest; }; /** * Return the provided scheduler, if any. * @return {?module:scheduler~MatrixScheduler} The scheduler or null */ MatrixClient.prototype.getScheduler = function () { return this.scheduler; }; /** * Set whether this client is a guest account. <b>This method is experimental * and may change without warning.</b> * @param {boolean} isGuest True if this is a guest account. */ MatrixClient.prototype.setGuest = function (isGuest) { // EXPERIMENTAL: // If the token is a macaroon, it should be encoded in it that it is a 'guest' // access token, which means that the SDK can determine this entirely without // the dev manually flipping this flag. this._isGuest = isGuest; }; /** * Retry a backed off syncing request immediately. This should only be used when * the user <b>explicitly</b> attempts to retry their lost connection. * @return {boolean} True if this resulted in a request being retried. */ MatrixClient.prototype.retryImmediately = function () { return this._syncApi.retryImmediately(); }; /** * Return the global notification EventTimelineSet, if any * * @return {EventTimelineSet} the globl notification EventTimelineSet */ MatrixClient.prototype.getNotifTimelineSet = function () { return this._notifTimelineSet; }; /** * Set the global notification EventTimelineSet * * @param {EventTimelineSet} notifTimelineSet */ MatrixClient.prototype.setNotifTimelineSet = function (notifTimelineSet) { this._notifTimelineSet = notifTimelineSet; }; // Crypto bits // =========== /** * Initialise support for end-to-end encryption in this client * * You should call this method after creating the matrixclient, but *before* * calling `startClient`, if you want to support end-to-end encryption. * * It will return a Promise which will resolve when the crypto layer has been * successfully initialised. */ MatrixClient.prototype.initCrypto = (0, _bluebird.coroutine)(_regenerator2.default.mark(function _callee() { var userId, crypto; return _regenerator2.default.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: if (!this._crypto) { _context.next = 3; break; } console.warn("Attempt to re-initialise e2e encryption on MatrixClient"); return _context.abrupt("return"); case 3: if (CRYPTO_ENABLED) { _context.next = 5; break; } throw new Error("End-to-end encryption not supported in this js-sdk build: did " + "you remember to load the olm library?"); case 5: if (this._sessionStore) { _context.next = 7; break; } throw new Error("Cannot enable encryption: no sessionStore provided"); case 7: if (this._cryptoStore) { _context.next = 9; break; } throw new Error("Cannot enable encryption: no cryptoStore provided"); case 9: userId = this.getUserId(); if (!(userId === null)) { _context.next = 12; break; } throw new Error("Cannot enable encryption on MatrixClient with unknown userId: " + "ensure userId is passed in createClient()."); case 12: if (!(this.deviceId === null)) { _context.next = 14; break; } throw new Error("Cannot enable encryption on MatrixClient with unknown deviceId: " + "ensure deviceId is passed in createClient()."); case 14: crypto = new Crypto(this, this._sessionStore, userId, this.deviceId, this.store, this._cryptoStore); this.reEmitter.reEmit(crypto, ["crypto.roomKeyRequest", "crypto.roomKeyRequestCancellation"]); _context.next = 18; return (0, _bluebird.resolve)(crypto.init()); case 18: // if crypto initialisation was successful, tell it to attach its event // handlers. crypto.registerEventHandlers(this); this._crypto = crypto; case 20: case "end": return _context.stop(); } } }, _callee, this); })); /** * Is end-to-end crypto enabled for this client. * @return {boolean} True if end-to-end is enabled. */ MatrixClient.prototype.isCryptoEnabled = function () { return this._crypto !== null; }; /** * Get the Ed25519 key for this device * * @return {?string} base64-encoded ed25519 key. Null if crypto is * disabled. */ MatrixClient.prototype.getDeviceEd25519Key = function () { if (!this._crypto) { return null; } return this._crypto.getDeviceEd25519Key(); }; /** * Upload the device keys to the homeserver. * @return {object} A promise that will resolve when the keys are uploaded. */ MatrixClient.prototype.uploadKeys = function () { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } return this._crypto.uploadDeviceKeys(); }; /** * Download the keys for a list of users and stores the keys in the session * store. * @param {Array} userIds The users to fetch. * @param {bool} forceDownload Always download the keys even if cached. * * @return {Promise} A promise which resolves to a map userId->deviceId->{@link * module:crypto~DeviceInfo|DeviceInfo}. */ MatrixClient.prototype.downloadKeys = function (userIds, forceDownload) { if (this._crypto === null) { return _bluebird2.default.reject(new Error("End-to-end encryption disabled")); } return this._crypto.downloadKeys(userIds, forceDownload); }; /** * Get the stored device keys for a user id * * @param {string} userId the user to list keys for. * * @return {Promise<module:crypto-deviceinfo[]>} list of devices */ MatrixClient.prototype.getStoredDevicesForUser = function () { var _ref2 = (0, _bluebird.method)(function (userId) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } return this._crypto.getStoredDevicesForUser(userId) || []; }); return function (_x) { return _ref2.apply(this, arguments); }; }(); /** * Get the stored device key for a user id and device id * * @param {string} userId the user to list keys for. * @param {string} deviceId unique identifier for the device * * @return {Promise<?module:crypto-deviceinfo>} device or null */ MatrixClient.prototype.getStoredDevice = function () { var _ref3 = (0, _bluebird.method)(function (userId, deviceId) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } return this._crypto.getStoredDevice(userId, deviceId) || null; }); return function (_x2, _x3) { return _ref3.apply(this, arguments); }; }(); /** * Mark the given device as verified * * @param {string} userId owner of the device * @param {string} deviceId unique identifier for the device * * @param {boolean=} verified whether to mark the device as verified. defaults * to 'true'. * * @returns {Promise} * * @fires module:client~event:MatrixClient"deviceVerificationChanged" */ MatrixClient.prototype.setDeviceVerified = function (userId, deviceId, verified) { if (verified === undefined) { verified = true; } return _setDeviceVerification(this, userId, deviceId, verified, null); }; /** * Mark the given device as blocked/unblocked * * @param {string} userId owner of the device * @param {string} deviceId unique identifier for the device * * @param {boolean=} blocked whether to mark the device as blocked. defaults * to 'true'. * * @returns {Promise} * * @fires module:client~event:MatrixClient"deviceVerificationChanged" */ MatrixClient.prototype.setDeviceBlocked = function (userId, deviceId, blocked) { if (blocked === undefined) { blocked = true; } return _setDeviceVerification(this, userId, deviceId, null, blocked); }; /** * Mark the given device as known/unknown * * @param {string} userId owner of the device * @param {string} deviceId unique identifier for the device * * @param {boolean=} known whether to mark the device as known. defaults * to 'true'. * * @returns {Promise} * * @fires module:client~event:MatrixClient"deviceVerificationChanged" */ MatrixClient.prototype.setDeviceKnown = function (userId, deviceId, known) { if (known === undefined) { known = true; } return _setDeviceVerification(this, userId, deviceId, null, null, known); }; MatrixClient.prototype.setGlobalBlacklistUnverifiedDevices = function (value) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } this._crypto.setGlobalBlacklistUnverifiedDevices(value); }; /** * @return {boolean} whether to unilaterally blacklist all * unverified devices */ MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function () { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } return this._crypto.getGlobalBlacklistUnverifiedDevices(); }; /** * Get e2e information on the device that sent an event * * @param {MatrixEvent} event event to be checked * * @return {Promise<module:crypto/deviceinfo?>} */ MatrixClient.prototype.getEventSenderDeviceInfo = function () { var _ref5 = (0, _bluebird.method)(function (event) { if (!this._crypto) { return null; } return this._crypto.getEventSenderDeviceInfo(event); }); return function (_x10) { return _ref5.apply(this, arguments); }; }(); /** * Check if the sender of an event is verified * * @param {MatrixEvent} event event to be checked * * @return {boolean} true if the sender of this event has been verified using * {@link module:client~MatrixClient#setDeviceVerified|setDeviceVerified}. */ MatrixClient.prototype.isEventSenderVerified = function () { var _ref6 = (0, _bluebird.coroutine)(_regenerator2.default.mark(function _callee3(event) { var device; return _regenerator2.default.wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: _context3.next = 2; return (0, _bluebird.resolve)(this.getEventSenderDeviceInfo(event)); case 2: device = _context3.sent; if (device) { _context3.next = 5; break; } return _context3.abrupt("return", false); case 5: return _context3.abrupt("return", device.isVerified()); case 6: case "end": return _context3.stop(); } } }, _callee3, this); })); return function (_x11) { return _ref6.apply(this, arguments); }; }(); /** * Enable end-to-end encryption for a room. * @param {string} roomId The room ID to enable encryption in. * @param {object} config The encryption config for the room. * @return {Promise} A promise that will resolve when encryption is set up. */ MatrixClient.prototype.setRoomEncryption = function (roomId, config) { if (!this._crypto) { throw new Error("End-to-End encryption disabled"); } return this._crypto.setRoomEncryption(roomId, config); }; /** * Whether encryption is enabled for a room. * @param {string} roomId the room id to query. * @return {bool} whether encryption is enabled. */ MatrixClient.prototype.isRoomEncrypted = function (roomId) { var room = this.getRoom(roomId); if (!room) { // we don't know about this room, so can't determine if it should be // encrypted. Let's assume not. return false; } // if there is an 'm.room.encryption' event in this room, it should be // encrypted (independently of whether we actually support encryption) var ev = room.currentState.getStateEvents("m.room.encryption", ""); if (ev) { return true; } // we don't have an m.room.encrypted event, but that might be because // the server is hiding it from us. Check the store to see if it was // previously encrypted. if (!this._sessionStore) { return false; } return Boolean(this._sessionStore.getEndToEndRoom(roomId)); }; /** * Get a list containing all of the room keys * * This should be encrypted before returning it to the user. * * @return {module:client.Promise} a promise which resolves to a list of * session export objects */ MatrixClient.prototype.exportRoomKeys = function () { if (!this._crypto) { return _bluebird2.default.reject(new Error("End-to-end encryption disabled")); } return this._crypto.exportRoomKeys(); }; /** * Import a list of room keys previously exported by exportRoomKeys * * @param {Object[]} keys a list of session export objects * * @return {module:client.Promise} a promise which resolves when the keys * have been imported */ MatrixClient.prototype.importRoomKeys = function (keys) { if (!this._crypto) { throw new Error("End-to-end encryption disabled"); } return this._crypto.importRoomKeys(keys); }; // Group ops // ========= // Operations on groups that come down the sync stream (ie. ones the // user is a member of or invited to) /** * Get the group for the given group ID. * This function will return a valid group for any group for which a Group event * has been emitted. * @param {string} groupId The group ID * @return {Group} The Group or null if the group is not known or there is no data store. */ MatrixClient.prototype.getGroup = function (groupId) { return this.store.getGroup(groupId); }; /** * Retrieve all known groups. * @return {Groups[]} A list of groups, or an empty list if there is no data store. */ MatrixClient.prototype.getGroups = function () { return this.store.getGroups(); }; // Room ops // ======== /** * Get the room for the given room ID. * This function will return a valid room for any room for which a Room event * has been emitted. Note in particular that other events, eg. RoomState.members * will be emitted for a room before this function will return the given room. * @param {string} roomId The room ID * @return {Room} The Room or null if it doesn't exist or there is no data store. */ MatrixClient.prototype.getRoom = function (roomId) { return this.store.getRoom(roomId); }; /** * Retrieve all known rooms. * @return {Room[]} A list of rooms, or an empty list if there is no data store. */ MatrixClient.prototype.getRooms = function () { return this.store.getRooms(); }; /** * Retrieve a user. * @param {string} userId The user ID to retrieve. * @return {?User} A user or null if there is no data store or the user does * not exist. */ MatrixClient.prototype.getUser = function (userId) { return this.store.getUser(userId); }; /** * Retrieve all known users. * @return {User[]} A list of users, or an empty list if there is no data store. */ MatrixClient.prototype.getUsers = function () { return this.store.getUsers(); }; // User Account Data operations // ============================ /** * Set account data event for the current user. * @param {string} eventType The event type * @param {Object} contents the contents object for the 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.setAccountData = function (eventType, contents, callback) { var path = utils.encodeUri("/user/$userId/account_data/$type", { $userId: this.credentials.userId, $type: eventType }); return this._http.authedRequest(callback, "PUT", path, undefined, contents); }; /** * Get account data event of given type for the current user. * @param {string} eventType The event type * @param {module:client.callback} callback Optional. * @return {?object} The contents of the given account data event */ MatrixClient.prototype.getAccountData = function (eventType) { return this.store.getAccountData(eventType); }; /** * Gets the users that are ignored by this client * @returns {string[]} The array of users that are ignored (empty if none) */ MatrixClient.prototype.getIgnoredUsers = function () { var event = this.getAccountData("m.ignored_user_list"); if (!event || !event.getContent() || !event.getContent()["ignored_users"]) return []; return (0, _keys2.default)(event.getContent()["ignored_users"]); }; /** * Sets the users that the current user should ignore. * @param {string[]} userIds the user IDs to ignore * @param {module:client.callback} [callback] Optional. * @return {module:client.Promise} Resolves: Account data event * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.setIgnoredUsers = function (userIds, callback) { var content = { ignored_users: {} }; userIds.map(function (u) { return content.ignored_users[u] = {}; }); return this.setAccountData("m.ignored_user_list", content, callback); }; /** * Gets whether or not a specific user is being ignored by this client. * @param {string} userId the user ID to check * @returns {boolean} true if the user is ignored, false otherwise */ MatrixClient.prototype.isUserIgnored = function (userId) { return this.getIgnoredUsers().indexOf(userId) !== -1; }; // Room operations // =============== /** * Join a room. If you have already joined the room, this will no-op. * @param {string} roomIdOrAlias The room ID or room alias to join. * @param {Object} opts Options when joining the room. * @param {boolean} opts.syncRoom True to do a room initial sync on the resulting * room. If false, the <strong>returned Room object will have no current state. * </strong> Default: true. * @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite, * the signing URL is passed in this parameter. * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: Room object. * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.joinRoom = function (roomIdOrAlias, opts, callback) { // to help people when upgrading.. if (utils.isFunction(opts)) { throw new Error("Expected 'opts' object, got function."); } opts = opts || {}; if (opts.syncRoom === undefined) { opts.syncRoom = true; } var room = this.getRoom(roomIdOrAlias); if (room && room.hasMembershipState(this.credentials.userId, "join")) { return _bluebird2.default.resolve(room); } var sign_promise = _bluebird2.default.resolve(); if (opts.inviteSignUrl) { sign_promise = this._http.requestOtherUrl(undefined, 'POST', opts.inviteSignUrl, { mxid: this.credentials.userId }); } var defer = _bluebird2.default.defer(); var self = this; sign_promise.then(function (signed_invite_object) { var data = {}; if (signed_invite_object) { data.third_party_signed = signed_invite_object; } var path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias }); return self._http.authedRequest(undefined, "POST", path, undefined, data); }).then(function (res) { var roomId = res.room_id; var syncApi = new SyncApi(self, self._clientOpts); var room = syncApi.createRoom(roomId); if (opts.syncRoom) { // v2 will do this for us // return syncApi.syncRoom(room); } return _bluebird2.default.resolve(room); }).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) { _updatePendingEventStatus(room, event, EventStatus.SENDING); return _sendEvent(this, room, event); }; /** * Cancel a queued or unsent event. * * @param {MatrixEvent} event Event to cancel * @throws Error if the event is not in QUEUED or NOT_SENT state */ MatrixClient.prototype.cancelPendingEvent = function (event) { if ([EventStatus.QUEUED, EventStatus.NOT_SENT].indexOf(event.status) < 0) { throw new Error("cannot cancel an event with status " + event.status); } // first tell the scheduler to forget about it, if it's queued if (this.scheduler) { this.scheduler.removeEventFromQueue(event); } // then tell the room about the change of state, which will remove it // from the room's list of pending events. var room = this.getRoom(event.getRoomId()); _updatePendingEventStatus(room, event, EventStatus.CANCELLED); }; /** * @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); }; /** * @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.getRoomTags = function (roomId, callback) { var path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/", { $userId: this.credentials.userId, $roomId: roomId }); return this._http.authedRequest(callback, "GET", path, undefined); }; /** * @param {string} roomId * @param {string} tagName name of room tag to be set * @param {object} metadata associated with that tag to be stored * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.setRoomTag = function (roomId, tagName, metadata, callback) { var path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { $userId: this.credentials.userId, $roomId: roomId, $tag: tagName }); return this._http.authedRequest(callback, "PUT", path, undefined, metadata); }; /** * @param {string} roomId * @param {string} tagName name of room tag to be removed * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.deleteRoomTag = function (roomId, tagName, callback) { var path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { $userId: this.credentials.userId, $roomId: roomId, $tag: tagName }); return this._http.authedRequest(callback, "DELETE", path, undefined, undefined); }; /** * @param {string} roomId * @param {string} eventType event type to be set * @param {object} content event content * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.setRoomAccountData = function (roomId, eventType, content, callback) { var path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", { $userId: this.credentials.userId, $roomId: roomId, $type: eventType }); return this._http.authedRequest(callback, "PUT", path, undefined, content); }; /** * Set a user's power level. * @param {string} roomId * @param {string} userId * @param {Number} powerLevel * @param {MatrixEvent} event * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.setPowerLevel = function (roomId, userId, powerLevel, event, callback) { var content = { users: {} }; if (event && event.getType() === "m.room.power_levels") { // take a copy of the content to ensure we don't corrupt // existing client state with a failed power level change content = utils.deepCopy(event.getContent()); } content.users[userId] = powerLevel; var path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", { $roomId: roomId }); return this._http.authedRequest(callback, "PUT", path, undefined, content); }; /** * @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 = this.makeTxnId(); } console.log("sendEvent of type " + eventType + " in " + roomId + " with txnId " + txnId); // 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; localEvent.status = EventStatus.SENDING; // add this event immediately to the local store as 'sending'. if (room) { room.addPendingEvent(localEvent, txnId); } return _sendEvent(this, room, localEvent, callback); }; // encrypts the event if necessary // adds the event to the queue, or sends it // marks the event as sent/unsent // returns a promise which resolves with the result of the send request function _sendEvent(client, room, event, callback) { // Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections, // so that we can handle synchronous and asynchronous exceptions with the // same code path. return _bluebird2.default.resolve().then(function () { var encryptionPromise = _encryptEventIfNeeded(client, event, room); if (!encryptionPromise) { return null; } _updatePendingEventStatus(room, event, EventStatus.ENCRYPTING); return encryptionPromise.then(function () { _updatePendingEventStatus(room, event, EventStatus.SENDING); }); }).then(function () { var promise = void 0; // this event may be queued if (client.scheduler) { // if this returns a promsie then the scheduler has control now and will // resolve/reject when it is done. Internally, the scheduler will invoke // processFn which is set to this._sendEventHttpRequest so the same code // path is executed regardless. promise = client.scheduler.queueEvent(event); if (promise && client.scheduler.getQueueForEvent(event).length > 1) { // event is processed FIFO so if the length is 2 or more we know // this event is stuck behind an earlier event. _updatePendingEventStatus(room, event, EventStatus.QUEUED); } } if (!promise) { promise = _sendEventHttpRequest(client, event); } return promise; }).then(function (res) { // the request was sent OK if (room) { room.updatePendingEvent(event, EventStatus.SENT, res.event_id); } if (callback) { callback(null, res); } return res; }, function (err) { // the request failed to send. console.error("Error sending event", err.stack || err); try { _updatePendingEventStatus(room, event, EventStatus.NOT_SENT); event.error = err; if (callback) { callback(err); } } catch (err2) { console.error("Exception in error handler!", err2.stack || err); } throw err; }); } /** * Encrypt an event according to the configuration of the room, if necessary. * * @param {MatrixClient} client * * @param {module:models/event.MatrixEvent} event event to be sent * * @param {module:models/room?} room destination room. Null if the destination * is not a room we have seen over the sync pipe. * * @return {module:client.Promise?} Promise which resolves when the event has been * encrypted, or null if nothing was needed */ function _encryptEventIfNeeded(client, event, room) { if (event.isEncrypted()) { // this event has already been encrypted; this happens if the // encryption step succeeded, but the send step failed on the first // attempt. return null; } if (!client.isRoomEncrypted(event.getRoomId())) { // looks like this room isn't encrypted. return null; } if (!client._crypto) { throw new Error("This room is configured to use encryption, but your client does " + "not support encryption."); } return client._crypto.encryptEvent(event, room); } function _updatePendingEventStatus(room, event, newStatus) { if (room) { room.updatePendingEvent(event, newStatus); } else { event.status = newStatus; } } function _sendEventHttpRequest(client, event) { var txnId = event._txnId ? event._txnId : client.makeTxnId(); var pathParams = { $roomId: event.getRoomId(), $eventType: event.getWireType(), $stateKey: event.getStateKey(), $txnId: txnId }; var path = void 0; if (event.isState()) { var pathTemplate = "/rooms/$roomId/state/$eventType"; if (event.getStateKey() && event.getStateKey().length > 0) { pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey"; } path = utils.encodeUri(pathTemplate, pathParams); } else { path = utils.encodeUri("/rooms/$roomId/send/$eventType/$txnId", pathParams); } return client._http.authedRequest(undefined, "PUT", path, undefined, event.getWireContent()).then(function (res) { console.log("Event sent to " + event.getRoomId() + " with event id " + res.event_id); return res; }); } /** * @param {string} roomId * @param {Object} content * @param {string} txnId Optional. * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.sendMessage = function (roomId, content, txnId, callback) { if (utils.isFunction(txnId)) { callback = txnId;txnId = undefined; } return this.sendEvent(roomId, "m.room.message", content, txnId, callback); }; /** * @param {string} roomId * @param {string} body * @param {string} txnId Optional. * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.sendTextMessage = function (roomId, body, txnId, callback) { var content = { msgtype: "m.text", body: body }; return this.sendMessage(roomId, content, txnId, callback); }; /** * @param {string} roomId * @param {string} body * @param {string} txnId Optional. * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.sendNotice = function (roomId, body, txnId, callback) { var content = { msgtype: "m.notice", body: body }; return this.sendMessage(roomId, content, txnId, callback); }; /** * @param {string} roomId * @param {string} body * @param {string} txnId Optional. * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.sendEmoteMessage = function (roomId, body, txnId, callback) { var content = { msgtype: "m.emote", body: body }; return this.sendMessage(roomId, content, txnId, callback); }; /** * @param {string} roomId * @param {string} url * @param {Object} info * @param {string} text * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.sendImageMessage = function (roomId, url, info, text, callback) { if (utils.isFunction(text)) { callback = text;text = undefined; } if (!text) { text = "Image"; } var content = { msgtype: "m.image", url: url, info: info, body: text }; return this.sendMessage(roomId, content, callback); }; /** * @param {string} roomId * @param {string} body * @param {string} htmlBody * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.sendHtmlMessage = function (roomId, body, htmlBody, callback) { var content = { msgtype: "m.text", format: "org.matrix.custom.html", body: body, formatted_body: htmlBody }; return this.sendMessage(roomId, content, callback); }; /** * @param {string} roomId * @param {string} body * @param {string} htmlBody * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.sendHtmlNotice = function (roomId, body, htmlBody, callback) { var content = { msgtype: "m.notice", format: "org.matrix.custom.html", body: body, formatted_body: htmlBody }; return this.sendMessage(roomId, content, callback); }; /** * @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.sendHtmlEmote = function (roomId, body, htmlBody, callback) { var content = { msgtype: "m.emote", format: "org.matrix.custom.html", body: body, formatted_body: htmlBody }; return this.sendMessage(roomId, content, callback); }; /** * Send a receipt. * @param {Event} event The event being acknowledged * @param {string} receiptType The kind of receipt e.g. "m.read" * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.sendReceipt = function (event, receiptType, callback) { if (this.isGuest()) { return _bluebird2.default.resolve({}); // guests cannot send receipts so don't bother. } var path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", { $roomId: event.getRoomId(), $receiptType: receiptType, $eventId: event.getId() }); var promise = this._http.authedRequest(callback, "POST", path, undefined, {}); var room = this.getRoom(event.getRoomId()); if (room) { room._addLocalEchoReceipt(this.credentials.userId, event, receiptType); } return promise; }; /** * Send a read receipt. * @param {Event} event The event that has been read. * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.sendReadReceipt = function (event, callback) { return this.sendReceipt(event, "m.read", callback); }; /** * Set a marker to indicate the point in a room before whic