require("request")
* as it returns a function which meets the required interface. See
* {@link requestFunction} for more information.
*
* @param {string} opts.accessToken The access_token for this user.
*
* @param {string} opts.userId The user ID for this user.
*
* @param {Object=} opts.store 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
* ?user_id=.
*
* @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) {
// 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 ReEmitter(this);
this.store = opts.store || new StubStore();
this.deviceId = opts.deviceId || null;
const userId = (opts.userId || null);
this.credentials = {
userId: userId,
};
this.scheduler = opts.scheduler;
if (this.scheduler) {
const self = this;
this.scheduler.setProcessFunction(function(eventToSend) {
const 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.
const 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();
}
// List of which rooms have encryption enabled: separate from crypto because
// we still want to know which rooms are encrypted even if crypto is disabled:
// we don't want to start sending unencrypted events to them.
this._roomList = new RoomList(this._cryptoStore, this._sessionStore);
// The pushprocessor caches useful things, so keep one and re-use it
this._pushProcessor = new PushProcessor(this);
}
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");
}
const promises = [];
promises.push(this.store.deleteAllData());
if (this._cryptoStore) {
promises.push(this._cryptoStore.deleteAllData());
}
return Promise.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();
};
/**
* Returns the additional data object associated with
* the current sync state, or null if there is no
* such data.
* Sync errors, if available, are put in the 'error' key of
* this object.
* @return {?Object}
*/
MatrixClient.prototype.getSyncStateData = function() {
if (!this._syncApi) {
return null;
}
return this._syncApi.getSyncStateData();
};
/**
* 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. This method is experimental
* and may change without warning.
* @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 explicitly 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 = async function() {
if (this._crypto) {
console.warn("Attempt to re-initialise e2e encryption on MatrixClient");
return;
}
if (!this._sessionStore) {
// this is temporary, the sessionstore is supposed to be going away
throw new Error(`Cannot enable encryption: no sessionStore provided`);
}
if (!this._cryptoStore) {
// the cryptostore is provided by sdk.createClient, so this shouldn't happen
throw new Error(`Cannot enable encryption: no cryptoStore provided`);
}
// initialise the list of encrypted rooms (whether or not crypto is enabled)
await this._roomList.init();
if (!CRYPTO_ENABLED) {
throw new Error(
`End-to-end encryption not supported in this js-sdk build: did ` +
`you remember to load the olm library?`,
);
}
const userId = this.getUserId();
if (userId === null) {
throw new Error(
`Cannot enable encryption on MatrixClient with unknown userId: ` +
`ensure userId is passed in createClient().`,
);
}
if (this.deviceId === null) {
throw new Error(
`Cannot enable encryption on MatrixClient with unknown deviceId: ` +
`ensure deviceId is passed in createClient().`,
);
}
const crypto = new Crypto(
this,
this._sessionStore,
userId, this.deviceId,
this.store,
this._cryptoStore,
this._roomList,
);
this.reEmitter.reEmit(crypto, [
"crypto.roomKeyRequest",
"crypto.roomKeyRequestCancellation",
"crypto.warning",
]);
await crypto.init();
// if crypto initialisation was successful, tell it to attach its event
// handlers.
crypto.registerEventHandlers(this);
this._crypto = crypto;
};
/**
* 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 Promise.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 {PromiseRoom.oldState.paginationToken will be
* null.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.scrollback = function(room, limit, callback) {
if (utils.isFunction(limit)) {
callback = limit; limit = undefined;
}
limit = limit || 30;
let timeToWaitMs = 0;
let info = this._ongoingScrollbacks[room.roomId] || {};
if (info.promise) {
return info.promise;
} else if (info.errorTs) {
const timeWaitedMs = Date.now() - info.errorTs;
timeToWaitMs = Math.max(SCROLLBACK_DELAY_MS - timeWaitedMs, 0);
}
if (room.oldState.paginationToken === null) {
return Promise.resolve(room); // already at the start.
}
// attempt to grab more events from the store first
const numAdded = this.store.scrollback(room, limit).length;
if (numAdded === limit) {
// store contained everything we needed.
return Promise.resolve(room);
}
// reduce the required number of events appropriately
limit = limit - numAdded;
const path = utils.encodeUri(
"/rooms/$roomId/messages", {$roomId: room.roomId},
);
const params = {
from: room.oldState.paginationToken,
limit: limit,
dir: 'b',
};
const defer = Promise.defer();
info = {
promise: defer.promise,
errorTs: null,
};
const self = this;
// wait for a time before doing this request
// (which may be 0 in order not to special case the code paths)
Promise.delay(timeToWaitMs).then(function() {
return self._http.authedRequest(callback, "GET", path, params);
}).done(function(res) {
const matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self));
room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline());
room.oldState.paginationToken = res.end;
if (res.chunk.length === 0) {
room.oldState.paginationToken = null;
}
self.store.storeEvents(room, matrixEvents, res.end, true);
self._ongoingScrollbacks[room.roomId] = null;
_resolve(callback, defer, room);
}, function(err) {
self._ongoingScrollbacks[room.roomId] = {
errorTs: Date.now(),
};
_reject(callback, defer, err);
});
this._ongoingScrollbacks[room.roomId] = info;
return defer.promise;
};
/**
* Take an EventContext, and back/forward-fill results.
*
* @param {module:models/event-context.EventContext} eventContext context
* object to be updated
* @param {Object} opts
* @param {boolean} opts.backwards true to fill backwards, false to go forwards
* @param {boolean} opts.limit number of events to request
*
* @return {module:client.Promise} Resolves: updated EventContext object
* @return {Error} Rejects: with an error response.
*/
MatrixClient.prototype.paginateEventContext = function(eventContext, opts) {
// TODO: we should implement a backoff (as per scrollback()) to deal more
// nicely with HTTP errors.
opts = opts || {};
const backwards = opts.backwards || false;
const token = eventContext.getPaginateToken(backwards);
if (!token) {
// no more results.
return Promise.reject(new Error("No paginate token"));
}
const dir = backwards ? 'b' : 'f';
const pendingRequest = eventContext._paginateRequests[dir];
if (pendingRequest) {
// already a request in progress - return the existing promise
return pendingRequest;
}
const path = utils.encodeUri(
"/rooms/$roomId/messages", {$roomId: eventContext.getEvent().getRoomId()},
);
const params = {
from: token,
limit: ('limit' in opts) ? opts.limit : 30,
dir: dir,
};
const self = this;
const promise =
self._http.authedRequest(undefined, "GET", path, params,
).then(function(res) {
let token = res.end;
if (res.chunk.length === 0) {
token = null;
} else {
const matrixEvents = utils.map(res.chunk, self.getEventMapper());
if (backwards) {
// eventContext expects the events in timeline order, but
// back-pagination returns them in reverse order.
matrixEvents.reverse();
}
eventContext.addEvents(matrixEvents, backwards);
}
eventContext.setPaginateToken(token, backwards);
return eventContext;
}).finally(function() {
eventContext._paginateRequests[dir] = null;
});
eventContext._paginateRequests[dir] = promise;
return promise;
};
/**
* Get an EventTimeline for the given event
*
* If the EventTimelineSet object already has the given event in its store, the
* corresponding timeline will be returned. Otherwise, a /context request is
* made, and used to construct an EventTimeline.
*
* @param {EventTimelineSet} timelineSet The timelineSet to look for the event in
* @param {string} eventId The ID of the event to look for
*
* @return {module:client.Promise} Resolves:
* {@link module:models/event-timeline~EventTimeline} including the given
* event
*/
MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) {
// don't allow any timeline support unless it's been enabled.
if (!this.timelineSupport) {
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
" parameter to true when creating MatrixClient to enable" +
" it.");
}
if (timelineSet.getTimelineForEvent(eventId)) {
return Promise.resolve(timelineSet.getTimelineForEvent(eventId));
}
const path = utils.encodeUri(
"/rooms/$roomId/context/$eventId", {
$roomId: timelineSet.room.roomId,
$eventId: eventId,
},
);
// TODO: we should implement a backoff (as per scrollback()) to deal more
// nicely with HTTP errors.
const self = this;
const promise =
self._http.authedRequest(undefined, "GET", path,
).then(function(res) {
if (!res.event) {
throw new Error("'event' not in '/context' result - homeserver too old?");
}
// by the time the request completes, the event might have ended up in
// the timeline.
if (timelineSet.getTimelineForEvent(eventId)) {
return timelineSet.getTimelineForEvent(eventId);
}
// we start with the last event, since that's the point at which we
// have known state.
// events_after is already backwards; events_before is forwards.
res.events_after.reverse();
const events = res.events_after
.concat([res.event])
.concat(res.events_before);
const matrixEvents = utils.map(events, self.getEventMapper());
let timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId());
if (!timeline) {
timeline = timelineSet.addTimeline();
timeline.initialiseState(utils.map(res.state,
self.getEventMapper()));
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
}
timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start);
// there is no guarantee that the event ended up in "timeline" (we
// might have switched to a neighbouring timeline) - so check the
// room's index again. On the other hand, there's no guarantee the
// event ended up anywhere, if it was later redacted, so we just
// return the timeline we first thought of.
const tl = timelineSet.getTimelineForEvent(eventId) || timeline;
return tl;
});
return promise;
};
/**
* Take an EventTimeline, and back/forward-fill results.
*
* @param {module:models/event-timeline~EventTimeline} eventTimeline timeline
* object to be updated
* @param {Object} [opts]
* @param {bool} [opts.backwards = false] true to fill backwards,
* false to go forwards
* @param {number} [opts.limit = 30] number of events to request
*
* @return {module:client.Promise} Resolves to a boolean: false if there are no
* events and we reached either end of the timeline; else true.
*/
MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
const isNotifTimeline = (eventTimeline.getTimelineSet() === this._notifTimelineSet);
// TODO: we should implement a backoff (as per scrollback()) to deal more
// nicely with HTTP errors.
opts = opts || {};
const backwards = opts.backwards || false;
if (isNotifTimeline) {
if (!backwards) {
throw new Error("paginateNotifTimeline can only paginate backwards");
}
}
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
const token = eventTimeline.getPaginationToken(dir);
if (!token) {
// no token - no results.
return Promise.resolve(false);
}
const pendingRequest = eventTimeline._paginationRequests[dir];
if (pendingRequest) {
// already a request in progress - return the existing promise
return pendingRequest;
}
let path, params, promise;
const self = this;
if (isNotifTimeline) {
path = "/notifications";
params = {
limit: ('limit' in opts) ? opts.limit : 30,
only: 'highlight',
};
if (token && token !== "end") {
params.from = token;
}
promise =
this._http.authedRequestWithPrefix(undefined, "GET", path, params,
undefined, httpApi.PREFIX_UNSTABLE,
).then(function(res) {
const token = res.next_token;
const matrixEvents = [];
for (let i = 0; i < res.notifications.length; i++) {
const notification = res.notifications[i];
const event = self.getEventMapper()(notification.event);
event.setPushActions(
PushProcessor.actionListToActionsObject(notification.actions),
);
event.event.room_id = notification.room_id; // XXX: gutwrenching
matrixEvents[i] = event;
}
eventTimeline.getTimelineSet()
.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
// if we've hit the end of the timeline, we need to stop trying to
// paginate. We need to keep the 'forwards' token though, to make sure
// we can recover from gappy syncs.
if (backwards && !res.next_token) {
eventTimeline.setPaginationToken(null, dir);
}
return res.next_token ? true : false;
}).finally(function() {
eventTimeline._paginationRequests[dir] = null;
});
eventTimeline._paginationRequests[dir] = promise;
} else {
const room = this.getRoom(eventTimeline.getRoomId());
if (!room) {
throw new Error("Unknown room " + eventTimeline.getRoomId());
}
path = utils.encodeUri(
"/rooms/$roomId/messages", {$roomId: eventTimeline.getRoomId()},
);
params = {
from: token,
limit: ('limit' in opts) ? opts.limit : 30,
dir: dir,
};
const filter = eventTimeline.getFilter();
if (filter) {
// XXX: it's horrific that /messages' filter parameter doesn't match
// /sync's one - see https://matrix.org/jira/browse/SPEC-451
params.filter = JSON.stringify(filter.getRoomTimelineFilterComponent());
}
promise =
this._http.authedRequest(undefined, "GET", path, params,
).then(function(res) {
const token = res.end;
const matrixEvents = utils.map(res.chunk, self.getEventMapper());
eventTimeline.getTimelineSet()
.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
// if we've hit the end of the timeline, we need to stop trying to
// paginate. We need to keep the 'forwards' token though, to make sure
// we can recover from gappy syncs.
if (backwards && res.end == res.start) {
eventTimeline.setPaginationToken(null, dir);
}
return res.end != res.start;
}).finally(function() {
eventTimeline._paginationRequests[dir] = null;
});
eventTimeline._paginationRequests[dir] = promise;
}
return promise;
};
/**
* Reset the notifTimelineSet entirely, paginating in some historical notifs as
* a starting point for subsequent pagination.
*/
MatrixClient.prototype.resetNotifTimelineSet = function() {
if (!this._notifTimelineSet) {
return;
}
// FIXME: This thing is a total hack, and results in duplicate events being
// added to the timeline both from /sync and /notifications, and lots of
// slow and wasteful processing and pagination. The correct solution is to
// extend /messages or /search or something to filter on notifications.
// use the fictitious token 'end'. in practice we would ideally give it
// the oldest backwards pagination token from /sync, but /sync doesn't
// know about /notifications, so we have no choice but to start paginating
// from the current point in time. This may well overlap with historical
// notifs which are then inserted into the timeline by /sync responses.
this._notifTimelineSet.resetLiveTimeline('end', null);
// we could try to paginate a single event at this point in order to get
// a more valid pagination token, but it just ends up with an out of order
// timeline. given what a mess this is and given we're going to have duplicate
// events anyway, just leave it with the dummy token for now.
/*
this.paginateNotifTimeline(this._notifTimelineSet.getLiveTimeline(), {
backwards: true,
limit: 1
});
*/
};
/**
* Peek into a room and receive updates about the room. This only works if the
* history visibility for the room is world_readable.
* @param {String} roomId The room to attempt to peek into.
* @return {module:client.Promise} Resolves: Room object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.peekInRoom = function(roomId) {
if (this._peekSync) {
this._peekSync.stopPeeking();
}
this._peekSync = new SyncApi(this, this._clientOpts);
return this._peekSync.peek(roomId);
};
/**
* Stop any ongoing room peeking.
*/
MatrixClient.prototype.stopPeeking = function() {
if (this._peekSync) {
this._peekSync.stopPeeking();
this._peekSync = null;
}
};
/**
* Set r/w flags for guest access in a room.
* @param {string} roomId The room to configure guest access in.
* @param {Object} opts Options
* @param {boolean} opts.allowJoin True to allow guests to join this room. This
* implicitly gives guests write access. If false or not given, guests are
* explicitly forbidden from joining the room.
* @param {boolean} opts.allowRead True to set history visibility to
* be world_readable. This gives guests read access *from this point forward*.
* If false or not given, history visibility is not modified.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setGuestAccess = function(roomId, opts) {
const writePromise = this.sendStateEvent(roomId, "m.room.guest_access", {
guest_access: opts.allowJoin ? "can_join" : "forbidden",
});
let readPromise = Promise.resolve();
if (opts.allowRead) {
readPromise = this.sendStateEvent(roomId, "m.room.history_visibility", {
history_visibility: "world_readable",
});
}
return Promise.all([readPromise, writePromise]);
};
// Registration/Login operations
// =============================
/**
* Requests an email verification token for the purposes of registration.
* This API proxies the Identity Server /validate/email/requestToken API,
* adding registration-specific behaviour. Specifically, if an account with
* the given email address already exists, it will either send an email
* to the address informing them of this or return M_THREEPID_IN_USE
* (which one is up to the Home Server).
*
* requestEmailToken calls the equivalent API directly on the ID server,
* therefore bypassing the registration-specific logic.
*
* Parameters and return value are as for requestEmailToken
* @param {string} email As requestEmailToken
* @param {string} clientSecret As requestEmailToken
* @param {number} sendAttempt As requestEmailToken
* @param {string} nextLink As requestEmailToken
* @return {module:client.Promise} Resolves: As requestEmailToken
*/
MatrixClient.prototype.requestRegisterEmailToken = function(email, clientSecret,
sendAttempt, nextLink) {
return this._requestTokenFromEndpoint(
"/register/email/requestToken",
{
email: email,
client_secret: clientSecret,
send_attempt: sendAttempt,
next_link: nextLink,
},
);
};
/**
* Requests a text message verification token for the purposes of registration.
* This API proxies the Identity Server /validate/msisdn/requestToken API,
* adding registration-specific behaviour, as with requestRegisterEmailToken.
*
* @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in which
* phoneNumber should be parsed relative to.
* @param {string} phoneNumber The phone number, in national or international format
* @param {string} clientSecret As requestEmailToken
* @param {number} sendAttempt As requestEmailToken
* @param {string} nextLink As requestEmailToken
* @return {module:client.Promise} Resolves: As requestEmailToken
*/
MatrixClient.prototype.requestRegisterMsisdnToken = function(phoneCountry, phoneNumber,
clientSecret, sendAttempt, nextLink) {
return this._requestTokenFromEndpoint(
"/register/msisdn/requestToken",
{
country: phoneCountry,
phone_number: phoneNumber,
client_secret: clientSecret,
send_attempt: sendAttempt,
next_link: nextLink,
},
);
};
/**
* Requests an email verification token for the purposes of adding a
* third party identifier to an account.
* This API proxies the Identity Server /validate/email/requestToken API,
* adding specific behaviour for the addition of email addresses to an
* account. Specifically, if an account with
* the given email address already exists, it will either send an email
* to the address informing them of this or return M_THREEPID_IN_USE
* (which one is up to the Home Server).
*
* requestEmailToken calls the equivalent API directly on the ID server,
* therefore bypassing the email addition specific logic.
*
* @param {string} email As requestEmailToken
* @param {string} clientSecret As requestEmailToken
* @param {number} sendAttempt As requestEmailToken
* @param {string} nextLink As requestEmailToken
* @return {module:client.Promise} Resolves: As requestEmailToken
*/
MatrixClient.prototype.requestAdd3pidEmailToken = function(email, clientSecret,
sendAttempt, nextLink) {
return this._requestTokenFromEndpoint(
"/account/3pid/email/requestToken",
{
email: email,
client_secret: clientSecret,
send_attempt: sendAttempt,
next_link: nextLink,
},
);
};
/**
* Requests a text message verification token for the purposes of adding a
* third party identifier to an account.
* This API proxies the Identity Server /validate/email/requestToken API,
* adding specific behaviour for the addition of phone numbers to an
* account, as requestAdd3pidEmailToken.
*
* @param {string} phoneCountry As requestRegisterMsisdnToken
* @param {string} phoneNumber As requestRegisterMsisdnToken
* @param {string} clientSecret As requestEmailToken
* @param {number} sendAttempt As requestEmailToken
* @param {string} nextLink As requestEmailToken
* @return {module:client.Promise} Resolves: As requestEmailToken
*/
MatrixClient.prototype.requestAdd3pidMsisdnToken = function(phoneCountry, phoneNumber,
clientSecret, sendAttempt, nextLink) {
return this._requestTokenFromEndpoint(
"/account/3pid/msisdn/requestToken",
{
country: phoneCountry,
phone_number: phoneNumber,
client_secret: clientSecret,
send_attempt: sendAttempt,
next_link: nextLink,
},
);
};
/**
* Requests an email verification token for the purposes of resetting
* the password on an account.
* This API proxies the Identity Server /validate/email/requestToken API,
* adding specific behaviour for the password resetting. Specifically,
* if no account with the given email address exists, it may either
* return M_THREEPID_NOT_FOUND or send an email
* to the address informing them of this (which one is up to the Home Server).
*
* requestEmailToken calls the equivalent API directly on the ID server,
* therefore bypassing the password reset specific logic.
*
* @param {string} email As requestEmailToken
* @param {string} clientSecret As requestEmailToken
* @param {number} sendAttempt As requestEmailToken
* @param {string} nextLink As requestEmailToken
* @param {module:client.callback} callback Optional. As requestEmailToken
* @return {module:client.Promise} Resolves: As requestEmailToken
*/
MatrixClient.prototype.requestPasswordEmailToken = function(email, clientSecret,
sendAttempt, nextLink) {
return this._requestTokenFromEndpoint(
"/account/password/email/requestToken",
{
email: email,
client_secret: clientSecret,
send_attempt: sendAttempt,
next_link: nextLink,
},
);
};
/**
* Requests a text message verification token for the purposes of resetting
* the password on an account.
* This API proxies the Identity Server /validate/email/requestToken API,
* adding specific behaviour for the password resetting, as requestPasswordEmailToken.
*
* @param {string} phoneCountry As requestRegisterMsisdnToken
* @param {string} phoneNumber As requestRegisterMsisdnToken
* @param {string} clientSecret As requestEmailToken
* @param {number} sendAttempt As requestEmailToken
* @param {string} nextLink As requestEmailToken
* @return {module:client.Promise} Resolves: As requestEmailToken
*/
MatrixClient.prototype.requestPasswordMsisdnToken = function(phoneCountry, phoneNumber,
clientSecret, sendAttempt, nextLink) {
return this._requestTokenFromEndpoint(
"/account/password/msisdn/requestToken",
{
country: phoneCountry,
phone_number: phoneNumber,
client_secret: clientSecret,
send_attempt: sendAttempt,
next_link: nextLink,
},
);
};
/**
* Internal utility function for requesting validation tokens from usage-specific
* requestToken endpoints.
*
* @param {string} endpoint The endpoint to send the request to
* @param {object} params Parameters for the POST request
* @return {module:client.Promise} Resolves: As requestEmailToken
*/
MatrixClient.prototype._requestTokenFromEndpoint = function(endpoint, params) {
const id_server_url = url.parse(this.idBaseUrl);
if (id_server_url.host === null) {
throw new Error("Invalid ID server URL: " + this.idBaseUrl);
}
const postParams = Object.assign({}, params, {
id_server: id_server_url.host,
});
return this._http.request(
undefined, "POST", endpoint, undefined,
postParams,
);
};
// Push operations
// ===============
/**
* Get the room-kind push rule associated with a room.
* @param {string} scope "global" or device-specific.
* @param {string} roomId the id of the room.
* @return {object} the rule or undefined.
*/
MatrixClient.prototype.getRoomPushRule = function(scope, roomId) {
// There can be only room-kind push rule per room
// and its id is the room id.
if (this.pushRules) {
for (let i = 0; i < this.pushRules[scope].room.length; i++) {
const rule = this.pushRules[scope].room[i];
if (rule.rule_id === roomId) {
return rule;
}
}
} else {
throw new Error(
"SyncApi.sync() must be done before accessing to push rules.",
);
}
};
/**
* Set a room-kind muting push rule in a room.
* The operation also updates MatrixClient.pushRules at the end.
* @param {string} scope "global" or device-specific.
* @param {string} roomId the id of the room.
* @param {string} mute the mute state.
* @return {module:client.Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
const self = this;
let deferred, hasDontNotifyRule;
// Get the existing room-kind push rule if any
const roomPushRule = this.getRoomPushRule(scope, roomId);
if (roomPushRule) {
if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
hasDontNotifyRule = true;
}
}
if (!mute) {
// Remove the rule only if it is a muting rule
if (hasDontNotifyRule) {
deferred = this.deletePushRule(scope, "room", roomPushRule.rule_id);
}
} else {
if (!roomPushRule) {
deferred = this.addPushRule(scope, "room", roomId, {
actions: ["dont_notify"],
});
} else if (!hasDontNotifyRule) {
// Remove the existing one before setting the mute push rule
// This is a workaround to SYN-590 (Push rule update fails)
deferred = Promise.defer();
this.deletePushRule(scope, "room", roomPushRule.rule_id)
.done(function() {
self.addPushRule(scope, "room", roomId, {
actions: ["dont_notify"],
}).done(function() {
deferred.resolve();
}, function(err) {
deferred.reject(err);
});
}, function(err) {
deferred.reject(err);
});
deferred = deferred.promise;
}
}
if (deferred) {
// Update this.pushRules when the operation completes
const ruleRefreshDeferred = Promise.defer();
deferred.done(function() {
self.getPushRules().done(function(result) {
self.pushRules = result;
ruleRefreshDeferred.resolve();
}, function(err) {
ruleRefreshDeferred.reject(err);
});
}, function(err) {
// Update it even if the previous operation fails. This can help the
// app to recover when push settings has been modifed from another client
self.getPushRules().done(function(result) {
self.pushRules = result;
ruleRefreshDeferred.reject(err);
}, function(err2) {
ruleRefreshDeferred.reject(err);
});
});
return ruleRefreshDeferred.promise;
}
};
// Search
// ======
/**
* Perform a server-side search for messages containing the given text.
* @param {Object} opts Options for the search.
* @param {string} opts.query The text to query.
* @param {string=} opts.keys The keys to search on. Defaults to all keys. One
* of "content.body", "content.name", "content.topic".
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.searchMessageText = function(opts, callback) {
const roomEvents = {
search_term: opts.query,
};
if ('keys' in opts) {
roomEvents.keys = opts.keys;
}
return this.search({
body: {
search_categories: {
room_events: roomEvents,
},
},
}, callback);
};
/**
* Perform a server-side search for room events.
*
* The returned promise resolves to an object containing the fields:
*
* * {number} count: estimate of the number of results
* * {string} next_batch: token for back-pagination; if undefined, there are
* no more results
* * {Array} highlights: a list of words to highlight from the stemming
* algorithm
* * {Array} results: a list of results
*
* Each entry in the results list is a {module:models/search-result.SearchResult}.
*
* @param {Object} opts
* @param {string} opts.term the term to search for
* @param {Object} opts.filter a JSON filter object to pass in the request
* @return {module:client.Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.searchRoomEvents = function(opts) {
// TODO: support groups
const body = {
search_categories: {
room_events: {
search_term: opts.term,
filter: opts.filter,
order_by: "recent",
event_context: {
before_limit: 1,
after_limit: 1,
include_profile: true,
},
},
},
};
const searchResults = {
_query: body,
results: [],
highlights: [],
};
return this.search({body: body}).then(
this._processRoomEventsSearch.bind(this, searchResults),
);
};
/**
* Take a result from an earlier searchRoomEvents call, and backfill results.
*
* @param {object} searchResults the results object to be updated
* @return {module:client.Promise} Resolves: updated result object
* @return {Error} Rejects: with an error response.
*/
MatrixClient.prototype.backPaginateRoomEventsSearch = function(searchResults) {
// TODO: we should implement a backoff (as per scrollback()) to deal more
// nicely with HTTP errors.
if (!searchResults.next_batch) {
return Promise.reject(new Error("Cannot backpaginate event search any further"));
}
if (searchResults.pendingRequest) {
// already a request in progress - return the existing promise
return searchResults.pendingRequest;
}
const searchOpts = {
body: searchResults._query,
next_batch: searchResults.next_batch,
};
const promise = this.search(searchOpts).then(
this._processRoomEventsSearch.bind(this, searchResults),
).finally(function() {
searchResults.pendingRequest = null;
});
searchResults.pendingRequest = promise;
return promise;
};
/**
* helper for searchRoomEvents and backPaginateRoomEventsSearch. Processes the
* response from the API call and updates the searchResults
*
* @param {Object} searchResults
* @param {Object} response
* @return {Object} searchResults
* @private
*/
MatrixClient.prototype._processRoomEventsSearch = function(searchResults, response) {
const room_events = response.search_categories.room_events;
searchResults.count = room_events.count;
searchResults.next_batch = room_events.next_batch;
// combine the highlight list with our existing list; build an object
// to avoid O(N^2) fail
const highlights = {};
room_events.highlights.forEach(function(hl) {
highlights[hl] = 1;
});
searchResults.highlights.forEach(function(hl) {
highlights[hl] = 1;
});
// turn it back into a list.
searchResults.highlights = Object.keys(highlights);
// append the new results to our existing results
for (let i = 0; i < room_events.results.length; i++) {
const sr = SearchResult.fromJson(room_events.results[i], this.getEventMapper());
searchResults.results.push(sr);
}
return searchResults;
};
/**
* Populate the store with rooms the user has left.
* @return {module:client.Promise} Resolves: TODO - Resolved when the rooms have
* been added to the data store.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.syncLeftRooms = function() {
// Guard against multiple calls whilst ongoing and multiple calls post success
if (this._syncedLeftRooms) {
return Promise.resolve([]); // don't call syncRooms again if it succeeded.
}
if (this._syncLeftRoomsPromise) {
return this._syncLeftRoomsPromise; // return the ongoing request
}
const self = this;
const syncApi = new SyncApi(this, this._clientOpts);
this._syncLeftRoomsPromise = syncApi.syncLeftRooms();
// cleanup locks
this._syncLeftRoomsPromise.then(function(res) {
console.log("Marking success of sync left room request");
self._syncedLeftRooms = true; // flip the bit on success
}).finally(function() {
self._syncLeftRoomsPromise = null; // cleanup ongoing request state
});
return this._syncLeftRoomsPromise;
};
// Filters
// =======
/**
* Create a new filter.
* @param {Object} content The HTTP body for the request
* @return {Filter} Resolves to a Filter object.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.createFilter = function(content) {
const self = this;
const path = utils.encodeUri("/user/$userId/filter", {
$userId: this.credentials.userId,
});
return this._http.authedRequest(
undefined, "POST", path, undefined, content,
).then(function(response) {
// persist the filter
const filter = Filter.fromJson(
self.credentials.userId, response.filter_id, content,
);
self.store.storeFilter(filter);
return filter;
});
};
/**
* Retrieve a filter.
* @param {string} userId The user ID of the filter owner
* @param {string} filterId The filter ID to retrieve
* @param {boolean} allowCached True to allow cached filters to be returned.
* Default: True.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getFilter = function(userId, filterId, allowCached) {
if (allowCached) {
const filter = this.store.getFilter(userId, filterId);
if (filter) {
return Promise.resolve(filter);
}
}
const self = this;
const path = utils.encodeUri("/user/$userId/filter/$filterId", {
$userId: userId,
$filterId: filterId,
});
return this._http.authedRequest(
undefined, "GET", path, undefined, undefined,
).then(function(response) {
// persist the filter
const filter = Filter.fromJson(
userId, filterId, response,
);
self.store.storeFilter(filter);
return filter;
});
};
/**
* @param {string} filterName
* @param {Filter} filter
* @return {Promise