@vector-im/matrix-bot-sdk
Version:
TypeScript/JavaScript SDK for Matrix bots and appservices
1,070 lines (1,069 loc) • 113 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var _a, _b;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MatrixClient = void 0;
const events_1 = require("events");
const htmlencode_1 = require("htmlencode");
const html_to_text_1 = require("html-to-text");
const MemoryStorageProvider_1 = require("./storage/MemoryStorageProvider");
const UnstableApis_1 = require("./UnstableApis");
const request_1 = require("./request");
const LogService_1 = require("./logging/LogService");
const RichReply_1 = require("./helpers/RichReply");
const Metrics_1 = require("./metrics/Metrics");
const decorators_1 = require("./metrics/decorators");
const AdminApis_1 = require("./AdminApis");
const Presence_1 = require("./models/Presence");
const MembershipEvent_1 = require("./models/events/MembershipEvent");
const RoomEvent_1 = require("./models/events/RoomEvent");
const EventKind_1 = require("./models/events/EventKind");
const IdentityClient_1 = require("./identity/IdentityClient");
const http_1 = require("./http");
const Spaces_1 = require("./models/Spaces");
const PowerLevelAction_1 = require("./models/PowerLevelAction");
const CryptoClient_1 = require("./e2ee/CryptoClient");
const Crypto_1 = require("./models/Crypto");
const decorators_2 = require("./e2ee/decorators");
const EncryptedRoomEvent_1 = require("./models/events/EncryptedRoomEvent");
const RustSdkCryptoStorageProvider_1 = require("./storage/RustSdkCryptoStorageProvider");
const DMs_1 = require("./DMs");
const MatrixError_1 = require("./models/MatrixError");
const MXCUrl_1 = require("./models/MXCUrl");
const MatrixContentScannerClient_1 = require("./MatrixContentScannerClient");
const SYNC_BACKOFF_MIN_MS = 5000;
const SYNC_BACKOFF_MAX_MS = 15000;
const VERSIONS_CACHE_MS = 7200000; // 2 hours
const CAPABILITES_CACHE_MS = 7200000; // 2 hours
/**
* A client that is capable of interacting with a matrix homeserver.
*/
class MatrixClient extends events_1.EventEmitter {
homeserverUrl;
accessToken;
storage;
cryptoStore;
/**
* The presence status to use while syncing. The valid values are "online" to set the account as online,
* "offline" to set the user as offline, "unavailable" for marking the user away, and null for not setting
* an explicit presence (the default).
*
* Has no effect if the client is not syncing. Does not apply until the next sync request.
*/
syncingPresence = null;
/**
* The number of milliseconds to wait for new events for on the next sync.
*
* Has no effect if the client is not syncing. Does not apply until the next sync request.
*/
syncingTimeout = 30000;
/**
* The crypto manager instance for this client. Generally speaking, this shouldn't
* need to be accessed but is made available.
*
* Will be null/undefined if crypto is not possible.
*/
crypto;
/**
* The Content Scanner API instance for this client. This is set if `opts.enableContentScanner`
* is true. The `downloadContent` and `crypto.decryptMedia` methods automatically go via
* the content scanner when this is set.
*/
contentScannerInstance;
/**
* The DM manager instance for this client.
*/
dms;
userId;
requestId = 0;
lastJoinedRoomIds = [];
impersonatedUserId;
impersonatedDeviceId;
joinStrategy = null;
eventProcessors = {};
filterId = 0;
stopSyncing = false;
metricsInstance = new Metrics_1.Metrics();
unstableApisInstance = new UnstableApis_1.UnstableApis(this);
cachedVersions;
versionsLastFetched = 0;
cachedCapabilites;
capabilitesLastFetched = 0;
/**
* Set this to true to have the client only persist the sync token after the sync
* has been processed successfully. Note that if this is true then when the sync
* loop throws an error the client will not persist a token.
*/
persistTokenAfterSync = false;
/**
* Creates a new matrix client
* @param {string} homeserverUrl The homeserver's client-server API URL
* @param {string} accessToken The access token for the homeserver
* @param {IStorageProvider} storage The storage provider to use. Defaults to MemoryStorageProvider.
* @param {ICryptoStorageProvider} cryptoStore Optional crypto storage provider to use. If not supplied,
* end-to-end encryption will not be functional in this client.
*/
constructor(homeserverUrl, accessToken, storage = null, cryptoStore = null, opts = {}) {
super();
this.homeserverUrl = homeserverUrl;
this.accessToken = accessToken;
this.storage = storage;
this.cryptoStore = cryptoStore;
if (this.homeserverUrl.endsWith("/")) {
this.homeserverUrl = this.homeserverUrl.substring(0, this.homeserverUrl.length - 1);
}
if (this.cryptoStore) {
if (!this.storage || this.storage instanceof MemoryStorageProvider_1.MemoryStorageProvider) {
LogService_1.LogService.warn("MatrixClientLite", "Starting an encryption-capable client with a memory store is not considered a good idea.");
}
if (!(this.cryptoStore instanceof RustSdkCryptoStorageProvider_1.RustSdkCryptoStorageProvider)) {
throw new Error("Cannot support custom encryption stores: Use a RustSdkCryptoStorageProvider");
}
this.crypto = new CryptoClient_1.CryptoClient(this);
this.on("room.event", (roomId, event) => {
// noinspection JSIgnoredPromiseFromCall
this.crypto.onRoomEvent(roomId, event);
});
this.on("room.join", (roomId) => {
// noinspection JSIgnoredPromiseFromCall
this.crypto.onRoomJoin(roomId);
});
LogService_1.LogService.debug("MatrixClientLite", "End-to-end encryption client created");
}
if (!this.storage)
this.storage = new MemoryStorageProvider_1.MemoryStorageProvider();
this.dms = new DMs_1.DMs(this);
if (opts.enableContentScanner) {
this.contentScannerInstance = new MatrixContentScannerClient_1.MatrixContentScannerClient(this);
}
}
/**
* The storage provider for this client. Direct access is usually not required.
*/
get storageProvider() {
return this.storage;
}
/**
* The metrics instance for this client
*/
get metrics() {
return this.metricsInstance;
}
/**
* Assigns a new metrics instance, overwriting the old one.
* @param {Metrics} metrics The new metrics instance.
*/
set metrics(metrics) {
if (!metrics)
throw new Error("Metrics cannot be null/undefined");
this.metricsInstance = metrics;
}
/**
* Gets the unstable API access class. This is generally not recommended to be
* used by clients.
* @return {UnstableApis} The unstable API access class.
*/
get unstableApis() {
return this.unstableApisInstance;
}
/**
* Gets the admin API access class.
* @return {AdminApis} The admin API access class.
*/
get adminApis() {
return new AdminApis_1.AdminApis(this);
}
/**
* Sets a user ID to impersonate as. This will assume that the access token for this client
* is for an application service, and that the userId given is within the reach of the
* application service. Setting this to null will stop future impersonation. The user ID is
* assumed to already be valid
* @param {string} userId The user ID to masquerade as, or `null` to clear masquerading.
* @param {string} deviceId Optional device ID to impersonate under the given user, if supported
* by the server. Check the whoami response after setting.
*/
impersonateUserId(userId, deviceId) {
this.impersonatedUserId = userId;
this.userId = userId;
if (userId) {
this.impersonatedDeviceId = deviceId;
}
else if (deviceId) {
throw new Error("Cannot impersonate just a device: need a user ID");
}
else {
this.impersonatedDeviceId = null;
}
}
/**
* Acquires an identity server client for communicating with an identity server. Note that
* this will automatically do the login portion to establish a usable token with the identity
* server provided, but it will not automatically accept any terms of service.
*
* The identity server name provided will in future be resolved to a server address - for now
* that resolution is assumed to be prefixing the name with `https://`.
* @param {string} identityServerName The domain of the identity server to connect to.
* @returns {Promise<IdentityClient>} Resolves to a prepared identity client.
*/
async getIdentityServerClient(identityServerName) {
const oidcToken = await this.getOpenIDConnectToken();
return IdentityClient_1.IdentityClient.acquire(oidcToken, `https://${identityServerName}`, this);
}
/**
* Sets the strategy to use for when joinRoom is called on this client
* @param {IJoinRoomStrategy} strategy The strategy to use, or null to use none
*/
setJoinStrategy(strategy) {
this.joinStrategy = strategy;
}
/**
* Adds a preprocessor to the event pipeline. When this client encounters an event, it
* will try to run it through the preprocessors it can in the order they were added.
* @param {IPreprocessor} preprocessor the preprocessor to add
*/
addPreprocessor(preprocessor) {
if (!preprocessor)
throw new Error("Preprocessor cannot be null");
const eventTypes = preprocessor.getSupportedEventTypes();
if (!eventTypes)
return; // Nothing to do
for (const eventType of eventTypes) {
if (!this.eventProcessors[eventType])
this.eventProcessors[eventType] = [];
this.eventProcessors[eventType].push(preprocessor);
}
}
async processEvent(event) {
if (!event)
return event;
if (!this.eventProcessors[event["type"]])
return event;
for (const processor of this.eventProcessors[event["type"]]) {
await processor.processEvent(event, this, EventKind_1.EventKind.RoomEvent);
}
return event;
}
/**
* Get the set of capabilites for the authenticated client.
* @returns {Promise<MatrixCapabilities>} Resolves to the server's supported versions.
*/
async getCapabilities() {
if (!this.cachedCapabilites || (Date.now() - this.capabilitesLastFetched) >= CAPABILITES_CACHE_MS) {
this.cachedCapabilites = (await this.doRequest("GET", "/_matrix/client/v3/capabilities")).capabilities;
this.capabilitesLastFetched = Date.now();
}
return this.cachedCapabilites;
}
/**
* Retrieves the server's supported specification versions and unstable features.
* @returns {Promise<ServerVersions>} Resolves to the server's supported versions.
*/
async getServerVersions() {
if (!this.cachedVersions || (Date.now() - this.versionsLastFetched) >= VERSIONS_CACHE_MS) {
this.cachedVersions = await this.doRequest("GET", "/_matrix/client/versions");
this.versionsLastFetched = Date.now();
}
return this.cachedVersions;
}
/**
* Determines if the server supports a given unstable feature flag. Useful for determining
* if the server can support an unstable MSC.
* @param {string} feature The feature name to look for.
* @returns {Promise<boolean>} Resolves to true if the server supports the flag, false otherwise.
*/
async doesServerSupportUnstableFeature(feature) {
return !!(await this.getServerVersions()).unstable_features?.[feature];
}
/**
* Determines if the server supports a given version of the specification or not.
* @param {string} version The version to look for. Eg: "v1.1"
* @returns {Promise<boolean>} Resolves to true if the server supports the version, false otherwise.
*/
async doesServerSupportVersion(version) {
return (await this.getServerVersions()).versions.includes(version);
}
/**
* Determines if the server supports at least one of the given specification versions or not.
* @param {string[]} versions The versions to look for. Eg: ["v1.1"]
* @returns {Promise<boolean>} Resolves to true if the server supports any of the versions, false otherwise.
*/
async doesServerSupportAnyOneVersion(versions) {
for (const version of versions) {
if (await this.doesServerSupportVersion(version)) {
return true;
}
}
return false;
}
/**
* Retrieves an OpenID Connect token from the homeserver for the current user.
* @returns {Promise<OpenIDConnectToken>} Resolves to the token.
*/
async getOpenIDConnectToken() {
const userId = encodeURIComponent(await this.getUserId());
return this.doRequest("POST", "/_matrix/client/v3/user/" + userId + "/openid/request_token", null, {});
}
/**
* Retrieves content from account data.
* @param {string} eventType The type of account data to retrieve.
* @returns {Promise<any>} Resolves to the content of that account data.
*/
async getAccountData(eventType) {
const userId = encodeURIComponent(await this.getUserId());
eventType = encodeURIComponent(eventType);
return this.doRequest("GET", "/_matrix/client/v3/user/" + userId + "/account_data/" + eventType);
}
/**
* Retrieves content from room account data.
* @param {string} eventType The type of room account data to retrieve.
* @param {string} roomId The room to read the account data from.
* @returns {Promise<any>} Resolves to the content of that account data.
*/
async getRoomAccountData(eventType, roomId) {
const userId = encodeURIComponent(await this.getUserId());
eventType = encodeURIComponent(eventType);
roomId = encodeURIComponent(roomId);
return this.doRequest("GET", "/_matrix/client/v3/user/" + userId + "/rooms/" + roomId + "/account_data/" + eventType);
}
/**
* Retrieves content from account data. If the account data request throws an error,
* this simply returns the default provided.
* @param {string} eventType The type of account data to retrieve.
* @param {any} defaultContent The default value. Defaults to null.
* @returns {Promise<any>} Resolves to the content of that account data, or the default.
*/
async getSafeAccountData(eventType, defaultContent = null) {
try {
return await this.getAccountData(eventType);
}
catch (e) {
LogService_1.LogService.warn("MatrixClient", `Error getting ${eventType} account data:`, (0, LogService_1.extractRequestError)(e));
return defaultContent;
}
}
/**
* Retrieves content from room account data. If the account data request throws an error,
* this simply returns the default provided.
* @param {string} eventType The type of room account data to retrieve.
* @param {string} roomId The room to read the account data from.
* @param {any} defaultContent The default value. Defaults to null.
* @returns {Promise<any>} Resolves to the content of that room account data, or the default.
*/
async getSafeRoomAccountData(eventType, roomId, defaultContent = null) {
try {
return await this.getRoomAccountData(eventType, roomId);
}
catch (e) {
LogService_1.LogService.warn("MatrixClient", `Error getting ${eventType} room account data in ${roomId}:`, (0, LogService_1.extractRequestError)(e));
return defaultContent;
}
}
/**
* Sets account data.
* @param {string} eventType The type of account data to set
* @param {any} content The content to set
* @returns {Promise<any>} Resolves when updated
*/
async setAccountData(eventType, content) {
const userId = encodeURIComponent(await this.getUserId());
eventType = encodeURIComponent(eventType);
return this.doRequest("PUT", "/_matrix/client/v3/user/" + userId + "/account_data/" + eventType, null, content);
}
/**
* Sets room account data.
* @param {string} eventType The type of room account data to set
* @param {string} roomId The room to set account data in
* @param {any} content The content to set
* @returns {Promise<any>} Resolves when updated
*/
async setRoomAccountData(eventType, roomId, content) {
const userId = encodeURIComponent(await this.getUserId());
eventType = encodeURIComponent(eventType);
roomId = encodeURIComponent(roomId);
return this.doRequest("PUT", "/_matrix/client/v3/user/" + userId + "/rooms/" + roomId + "/account_data/" + eventType, null, content);
}
/**
* Gets the presence information for the current user.
* @returns {Promise<Presence>} Resolves to the presence status of the user.
*/
async getPresenceStatus() {
return this.getPresenceStatusFor(await this.getUserId());
}
/**
* Gets the presence information for a given user.
* @param {string} userId The user ID to look up the presence of.
* @returns {Promise<Presence>} Resolves to the presence status of the user.
*/
async getPresenceStatusFor(userId) {
return this.doRequest("GET", "/_matrix/client/v3/presence/" + encodeURIComponent(userId) + "/status").then(r => new Presence_1.Presence(r));
}
/**
* Sets the presence status for the current user.
* @param {PresenceState} presence The new presence state for the user.
* @param {string?} statusMessage Optional status message to include with the presence.
* @returns {Promise<any>} Resolves when complete.
*/
async setPresenceStatus(presence, statusMessage = undefined) {
return this.doRequest("PUT", "/_matrix/client/v3/presence/" + encodeURIComponent(await this.getUserId()) + "/status", null, {
presence: presence,
status_msg: statusMessage,
});
}
/**
* Gets a published alias for the given room. These are supplied by the room admins
* and should point to the room, but may not. This is primarily intended to be used
* in the context of rendering a mention (pill) for a room.
* @param {string} roomIdOrAlias The room ID or alias to get an alias for.
* @returns {Promise<string>} Resolves to a published room alias, or falsey if none found.
*/
async getPublishedAlias(roomIdOrAlias) {
try {
const roomId = await this.resolveRoom(roomIdOrAlias);
const event = await this.getRoomStateEvent(roomId, "m.room.canonical_alias", "");
if (!event)
return null;
const canonical = event['alias'];
const alt = event['alt_aliases'] || [];
return canonical || alt[0];
}
catch (e) {
// Assume none
return null;
}
}
/**
* Adds a new room alias to the room directory
* @param {string} alias The alias to add (eg: "#my-room:matrix.org")
* @param {string} roomId The room ID to add the alias to
* @returns {Promise} resolves when the alias has been added
*/
createRoomAlias(alias, roomId) {
alias = encodeURIComponent(alias);
return this.doRequest("PUT", "/_matrix/client/v3/directory/room/" + alias, null, {
"room_id": roomId,
});
}
/**
* Removes a room alias from the room directory
* @param {string} alias The alias to remove
* @returns {Promise} resolves when the alias has been deleted
*/
deleteRoomAlias(alias) {
alias = encodeURIComponent(alias);
return this.doRequest("DELETE", "/_matrix/client/v3/directory/room/" + alias);
}
/**
* Sets the visibility of a room in the directory.
* @param {string} roomId The room ID to manipulate the visibility of
* @param {"public" | "private"} visibility The visibility to set for the room
* @return {Promise} resolves when the visibility has been updated
*/
setDirectoryVisibility(roomId, visibility) {
roomId = encodeURIComponent(roomId);
return this.doRequest("PUT", "/_matrix/client/v3/directory/list/room/" + roomId, null, {
"visibility": visibility,
});
}
/**
* Gets the visibility of a room in the directory.
* @param {string} roomId The room ID to query the visibility of
* @return {Promise<"public"|"private">} The visibility of the room
*/
getDirectoryVisibility(roomId) {
roomId = encodeURIComponent(roomId);
return this.doRequest("GET", "/_matrix/client/v3/directory/list/room/" + roomId).then(response => {
return response["visibility"];
});
}
/**
* Resolves a room ID or alias to a room ID. If the given ID or alias looks like a room ID
* already, it will be returned as-is. If the room ID or alias looks like a room alias, it
* will be resolved to a room ID if possible. If the room ID or alias is neither, an error
* will be raised.
* @param {string} roomIdOrAlias the room ID or alias to resolve to a room ID
* @returns {Promise<string>} resolves to the room ID
*/
async resolveRoom(roomIdOrAlias) {
if (roomIdOrAlias.startsWith("!"))
return roomIdOrAlias; // probably
if (roomIdOrAlias.startsWith("#"))
return this.lookupRoomAlias(roomIdOrAlias).then(r => r.roomId);
throw new Error("Invalid room ID or alias");
}
/**
* Does a room directory lookup for a given room alias
* @param {string} roomAlias the room alias to look up in the room directory
* @returns {Promise<RoomDirectoryLookupResponse>} resolves to the room's information
*/
lookupRoomAlias(roomAlias) {
return this.doRequest("GET", "/_matrix/client/v3/directory/room/" + encodeURIComponent(roomAlias)).then(response => {
return {
roomId: response["room_id"],
residentServers: response["servers"],
};
});
}
/**
* Invites a user to a room.
* @param {string} userId the user ID to invite
* @param {string} roomId the room ID to invite the user to
* @returns {Promise<any>} resolves when completed
*/
inviteUser(userId, roomId) {
return this.doRequest("POST", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/invite", null, {
user_id: userId,
});
}
/**
* Kicks a user from a room.
* @param {string} userId the user ID to kick
* @param {string} roomId the room ID to kick the user in
* @param {string?} reason optional reason for the kick
* @returns {Promise<any>} resolves when completed
*/
kickUser(userId, roomId, reason = null) {
return this.doRequest("POST", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/kick", null, {
user_id: userId,
reason: reason,
});
}
/**
* Bans a user from a room.
* @param {string} userId the user ID to ban
* @param {string} roomId the room ID to set the ban in
* @param {string?} reason optional reason for the ban
* @returns {Promise<any>} resolves when completed
*/
banUser(userId, roomId, reason = null) {
return this.doRequest("POST", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/ban", null, {
user_id: userId,
reason: reason,
});
}
/**
* Unbans a user in a room.
* @param {string} userId the user ID to unban
* @param {string} roomId the room ID to lift the ban in
* @returns {Promise<any>} resolves when completed
*/
unbanUser(userId, roomId) {
return this.doRequest("POST", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/unban", null, {
user_id: userId,
});
}
/**
* Gets the current user ID for this client
* @returns {Promise<string>} The user ID of this client
*/
async getUserId() {
if (this.userId)
return this.userId;
// getWhoAmI should populate `this.userId` for us
await this.getWhoAmI();
return this.userId;
}
/**
* Gets the user's information from the server directly.
* @returns {Promise<IWhoAmI>} The "who am I" response.
*/
async getWhoAmI() {
const whoami = await this.doRequest("GET", "/_matrix/client/v3/account/whoami");
this.userId = whoami["user_id"];
return whoami;
}
/**
* Stops the client from syncing.
*/
stop() {
this.stopSyncing = true;
}
/**
* Starts syncing the client with an optional filter
* @param {any} filter The filter to use, or null for none
* @returns {Promise<any>} Resolves when the client has started syncing
*/
async start(filter = null) {
await this.dms.update();
this.stopSyncing = false;
if (!filter || typeof (filter) !== "object") {
LogService_1.LogService.trace("MatrixClientLite", "No filter given or invalid object - using defaults.");
filter = null;
}
LogService_1.LogService.trace("MatrixClientLite", "Populating joined rooms to avoid excessive join emits");
this.lastJoinedRoomIds = await this.getJoinedRooms();
const userId = await this.getUserId();
if (this.crypto) {
LogService_1.LogService.debug("MatrixClientLite", "Preparing end-to-end encryption");
await this.crypto.prepare();
LogService_1.LogService.info("MatrixClientLite", "End-to-end encryption enabled");
}
let createFilter = false;
// noinspection ES6RedundantAwait
const existingFilter = await Promise.resolve(this.storage.getFilter());
if (existingFilter) {
LogService_1.LogService.trace("MatrixClientLite", "Found existing filter. Checking consistency with given filter");
if (JSON.stringify(existingFilter.filter) === JSON.stringify(filter)) {
LogService_1.LogService.trace("MatrixClientLite", "Filters match");
this.filterId = existingFilter.id;
}
else {
createFilter = true;
}
}
else {
createFilter = true;
}
if (createFilter && filter) {
LogService_1.LogService.trace("MatrixClientLite", "Creating new filter");
await this.doRequest("POST", "/_matrix/client/v3/user/" + encodeURIComponent(userId) + "/filter", null, filter).then(async (response) => {
this.filterId = response["filter_id"];
// noinspection ES6RedundantAwait
await Promise.resolve(this.storage.setSyncToken(null));
// noinspection ES6RedundantAwait
await Promise.resolve(this.storage.setFilter({
id: this.filterId,
filter: filter,
}));
});
}
LogService_1.LogService.trace("MatrixClientLite", "Starting sync with filter ID " + this.filterId);
return this.startSyncInternal();
}
startSyncInternal() {
return this.startSync();
}
async startSync(emitFn = null) {
// noinspection ES6RedundantAwait
let token = await Promise.resolve(this.storage.getSyncToken());
const promiseWhile = async () => {
if (this.stopSyncing) {
LogService_1.LogService.info("MatrixClientLite", "Client stop requested - stopping sync");
return;
}
try {
const response = await this.doSync(token);
token = response["next_batch"];
if (!this.persistTokenAfterSync) {
await Promise.resolve(this.storage.setSyncToken(token));
}
LogService_1.LogService.debug("MatrixClientLite", "Received sync. Next token: " + token);
await this.processSync(response, emitFn);
if (this.persistTokenAfterSync) {
await Promise.resolve(this.storage.setSyncToken(token));
}
}
catch (e) {
// If we've requested to stop syncing, don't bother checking the error.
if (this.stopSyncing) {
LogService_1.LogService.info("MatrixClientLite", "Client stop requested - cancelling sync");
return;
}
LogService_1.LogService.error("MatrixClientLite", "Error handling sync " + (0, LogService_1.extractRequestError)(e));
const backoffTime = SYNC_BACKOFF_MIN_MS + Math.random() * (SYNC_BACKOFF_MAX_MS - SYNC_BACKOFF_MIN_MS);
LogService_1.LogService.info("MatrixClientLite", `Backing off for ${backoffTime}ms`);
await new Promise((r) => setTimeout(r, backoffTime));
}
return promiseWhile();
};
promiseWhile(); // start the loop
}
doSync(token) {
LogService_1.LogService.debug("MatrixClientLite", "Performing sync with token " + token);
const conf = {
full_state: false,
timeout: Math.max(0, this.syncingTimeout),
};
// synapse complains if the variables are null, so we have to have it unset instead
if (token)
conf["since"] = token;
if (this.filterId)
conf['filter'] = this.filterId;
if (this.syncingPresence)
conf['presence'] = this.syncingPresence;
// timeout is 40s if we have a token, otherwise 10min
return this.doRequest("GET", "/_matrix/client/v3/sync", conf, null, (token ? 40000 : 600000));
}
async processSync(raw, emitFn = null) {
if (!emitFn)
emitFn = (e, ...p) => Promise.resolve(this.emit(e, ...p));
if (!raw)
return; // nothing to process
if (this.crypto) {
const inbox = [];
if (raw['to_device']?.['events']) {
inbox.push(...raw['to_device']['events']);
// TODO: Emit or do something with unknown messages?
}
let unusedFallbacks = [];
if (raw['org.matrix.msc2732.device_unused_fallback_key_types']) {
unusedFallbacks = raw['org.matrix.msc2732.device_unused_fallback_key_types'];
}
else if (raw['device_unused_fallback_key_types']) {
unusedFallbacks = raw['device_unused_fallback_key_types'];
}
const counts = raw['device_one_time_keys_count'] ?? {};
const changed = raw['device_lists']?.['changed'] ?? [];
const left = raw['device_lists']?.['left'] ?? [];
await this.crypto.updateSyncData(inbox, counts, unusedFallbacks, changed, left);
}
// Always process device messages first to ensure there are decryption keys
if (raw['account_data'] && raw['account_data']['events']) {
for (const event of raw['account_data']['events']) {
await emitFn("account_data", event);
}
}
if (!raw['rooms'])
return; // nothing more to process
const leftRooms = raw['rooms']['leave'] || {};
const inviteRooms = raw['rooms']['invite'] || {};
const joinedRooms = raw['rooms']['join'] || {};
// Process rooms we've left first
for (const roomId in leftRooms) {
const room = leftRooms[roomId];
if (room['account_data'] && room['account_data']['events']) {
for (const event of room['account_data']['events']) {
await emitFn("room.account_data", roomId, event);
}
}
if (!room['timeline'] || !room['timeline']['events'])
continue;
let leaveEvent = null;
for (const event of room['timeline']['events']) {
if (event['type'] !== 'm.room.member')
continue;
if (event['state_key'] !== await this.getUserId())
continue;
const membership = event["content"]?.["membership"];
if (membership !== "leave" && membership !== "ban")
continue;
const oldAge = leaveEvent && leaveEvent['unsigned'] && leaveEvent['unsigned']['age'] ? leaveEvent['unsigned']['age'] : 0;
const newAge = event['unsigned'] && event['unsigned']['age'] ? event['unsigned']['age'] : 0;
if (leaveEvent && oldAge < newAge)
continue;
leaveEvent = event;
}
if (!leaveEvent) {
LogService_1.LogService.warn("MatrixClientLite", "Left room " + roomId + " without receiving an event");
continue;
}
leaveEvent = await this.processEvent(leaveEvent);
await emitFn("room.leave", roomId, leaveEvent);
this.lastJoinedRoomIds = this.lastJoinedRoomIds.filter(r => r !== roomId);
}
// Process rooms we've been invited to
for (const roomId in inviteRooms) {
const room = inviteRooms[roomId];
if (!room['invite_state'] || !room['invite_state']['events'])
continue;
let inviteEvent = null;
for (const event of room['invite_state']['events']) {
if (event['type'] !== 'm.room.member')
continue;
if (event['state_key'] !== await this.getUserId())
continue;
if (!event['content'])
continue;
if (event['content']['membership'] !== "invite")
continue;
const oldAge = inviteEvent && inviteEvent['unsigned'] && inviteEvent['unsigned']['age'] ? inviteEvent['unsigned']['age'] : 0;
const newAge = event['unsigned'] && event['unsigned']['age'] ? event['unsigned']['age'] : 0;
if (inviteEvent && oldAge < newAge)
continue;
inviteEvent = event;
}
if (!inviteEvent) {
LogService_1.LogService.warn("MatrixClientLite", "Invited to room " + roomId + " without receiving an event");
continue;
}
inviteEvent = await this.processEvent(inviteEvent);
await emitFn("room.invite", roomId, inviteEvent);
}
// Process rooms we've joined and their events
for (const roomId in joinedRooms) {
const room = joinedRooms[roomId];
if (room['account_data'] && room['account_data']['events']) {
for (const event of room['account_data']['events']) {
await emitFn("room.account_data", roomId, event);
}
}
if (!room['timeline'] || !room['timeline']['events'])
continue;
for (let event of room['timeline']['events']) {
if (event['type'] === "m.room.member" && event['state_key'] === await this.getUserId()) {
if (event['content']?.['membership'] === "join" && this.lastJoinedRoomIds.indexOf(roomId) === -1) {
await emitFn("room.join", roomId, await this.processEvent(event));
this.lastJoinedRoomIds.push(roomId);
}
}
event = await this.processEvent(event);
if (event['type'] === 'm.room.encrypted' && await this.crypto?.isRoomEncrypted(roomId)) {
await emitFn("room.encrypted_event", roomId, event);
try {
event = (await this.crypto.decryptRoomEvent(new EncryptedRoomEvent_1.EncryptedRoomEvent(event), roomId)).raw;
event = await this.processEvent(event);
await emitFn("room.decrypted_event", roomId, event);
}
catch (e) {
LogService_1.LogService.error("MatrixClientLite", `Decryption error on ${roomId} ${event['event_id']}`, e);
await emitFn("room.failed_decryption", roomId, event, e);
}
}
if (event['type'] === 'm.room.message') {
await emitFn("room.message", roomId, event);
}
if (event['type'] === 'm.room.tombstone' && event['state_key'] === '') {
await emitFn("room.archived", roomId, event);
}
if (event['type'] === 'm.room.create' && event['state_key'] === '' && event['content']
&& event['content']['predecessor'] && event['content']['predecessor']['room_id']) {
await emitFn("room.upgraded", roomId, event);
}
await emitFn("room.event", roomId, event);
}
}
}
/**
* Gets an event for a room. If the event is encrypted, and the client supports encryption,
* and the room is encrypted, then this will return a decrypted event.
* @param {string} roomId the room ID to get the event in
* @param {string} eventId the event ID to look up
* @returns {Promise<any>} resolves to the found event
*/
async getEvent(roomId, eventId) {
const event = await this.getRawEvent(roomId, eventId);
if (event['type'] === 'm.room.encrypted' && await this.crypto?.isRoomEncrypted(roomId)) {
return this.processEvent((await this.crypto.decryptRoomEvent(new EncryptedRoomEvent_1.EncryptedRoomEvent(event), roomId)).raw);
}
return event;
}
/**
* Gets an event for a room. Returned as a raw event.
* @param {string} roomId the room ID to get the event in
* @param {string} eventId the event ID to look up
* @returns {Promise<any>} resolves to the found event
*/
getRawEvent(roomId, eventId) {
return this.doRequest("GET", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/event/" + encodeURIComponent(eventId))
.then(ev => this.processEvent(ev));
}
/**
* Gets the room state for the given room. Returned as raw events.
* @param {string} roomId the room ID to get state for
* @returns {Promise<any[]>} resolves to the room's state
*/
getRoomState(roomId) {
return this.doRequest("GET", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/state")
.then(state => Promise.all(state.map(ev => this.processEvent(ev))));
}
/**
* Gets the state events for a given room of a given type under the given state key.
* @param {string} roomId the room ID
* @param {string} type the event type
* @param {String} stateKey the state key, falsey if not needed
* @returns {Promise<any|any[]>} resolves to the state event(s)
* @deprecated It is not possible to get an array of events - use getRoomStateEvent instead
*/
getRoomStateEvents(roomId, type, stateKey) {
return this.getRoomStateEvent(roomId, type, stateKey);
}
/**
* Gets a state event for a given room of a given type under the given state key.
* @param {string} roomId the room ID
* @param {string} type the event type
* @param {String} stateKey the state key
* @returns {Promise<any>} resolves to the state event
*/
getRoomStateEvent(roomId, type, stateKey) {
const path = "/_matrix/client/v3/rooms/"
+ encodeURIComponent(roomId) + "/state/"
+ encodeURIComponent(type) + "/"
+ encodeURIComponent(stateKey ? stateKey : '');
return this.doRequest("GET", path)
.then(ev => this.processEvent(ev));
}
/**
* Gets the context surrounding an event.
* @param {string} roomId The room ID to get the context in.
* @param {string} eventId The event ID to get the context of.
* @param {number} limit The maximum number of events to return on either side of the event.
* @returns {Promise<EventContext>} The context of the event
*/
async getEventContext(roomId, eventId, limit = 10) {
const res = await this.doRequest("GET", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/context/" + encodeURIComponent(eventId), { limit });
return {
event: new RoomEvent_1.RoomEvent(res['event']),
before: res['events_before'].map(e => new RoomEvent_1.RoomEvent(e)),
after: res['events_after'].map(e => new RoomEvent_1.RoomEvent(e)),
state: res['state'].map(e => new RoomEvent_1.StateEvent(e)),
};
}
/**
* Get the nearest event to a given timestamp, either forwards or backwards.
* @param roomId The room ID to get the context in.
* @param ts The event ID to get the context of.
* @param dir The maximum number of events to return on either side of the event.
* @returns The ID and origin server timestamp of the event.
*/
async getEventNearestToTimestamp(roomId, ts, dir) {
return await this.doRequest("GET", "/_matrix/client/v1/rooms/" + encodeURIComponent(roomId) + "/timestamp_to_event", { ts, dir });
}
/**
* Gets the profile for a given user
* @param {string} userId the user ID to lookup
* @returns {Promise<any>} the profile of the user
*/
getUserProfile(userId) {
return this.doRequest("GET", "/_matrix/client/v3/profile/" + encodeURIComponent(userId));
}
/**
* Sets a new display name for the user.
* @param {string} displayName the new display name for the user, or null to clear
* @returns {Promise<any>} resolves when complete
*/
async setDisplayName(displayName) {
const userId = encodeURIComponent(await this.getUserId());
return this.doRequest("PUT", "/_matrix/client/v3/profile/" + userId + "/displayname", null, {
displayname: displayName,
});
}
/**
* Sets a new avatar url for the user.
* @param {string} avatarUrl the new avatar URL for the user, in the form of a Matrix Content URI
* @returns {Promise<any>} resolves when complete
*/
async setAvatarUrl(avatarUrl) {
const userId = encodeURIComponent(await this.getUserId());
return this.doRequest("PUT", "/_matrix/client/v3/profile/" + userId + "/avatar_url", null, {
avatar_url: avatarUrl,
});
}
/**
* Joins the given room
* @param {string} roomIdOrAlias the room ID or alias to join
* @param {string[]} viaServers the server names to try and join through
* @returns {Promise<string>} resolves to the joined room ID
*/
async joinRoom(roomIdOrAlias, viaServers = []) {
const apiCall = (targetIdOrAlias) => {
targetIdOrAlias = encodeURIComponent(targetIdOrAlias);
const qs = {};
if (viaServers.length > 0)
qs['server_name'] = viaServers;
return this.doRequest("POST", "/_matrix/client/v3/join/" + targetIdOrAlias, qs, {}).then(response => {
return response['room_id'];
});
};
const userId = await this.getUserId();
if (this.joinStrategy)
return this.joinStrategy.joinRoom(roomIdOrAlias, userId, apiCall);
else
return apiCall(roomIdOrAlias);
}
/**
* Gets a list of joined room IDs
* @returns {Promise<string[]>} resolves to a list of room IDs the client participates in
*/
getJoinedRooms() {
return this.doRequest("GET", "/_matrix/client/v3/joined_rooms").then(response => response['joined_rooms']);
}
/**
* Gets the joined members in a room. The client must be in the room to make this request.
* @param {string} roomId The room ID to get the joined members of.
* @returns {Promise<string>} The joined user IDs in the room
*/
getJoinedRoomMembers(roomId) {
return this.doRequest("GET", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/joined_members").then(response => {
return Object.keys(response['joined']);
});
}
/**
* Gets the joined members in a room, as an object mapping userIds to profiles. The client must be in the room to make this request.
* @param {string} roomId The room ID to get the joined members of.
* @returns {Object} The joined user IDs in the room as an object mapped to a set of profiles.
*/
async getJoinedRoomMembersWithProfiles(roomId) {
return (await this.doRequest("GET", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/joined_members")).joined;
}
/**
* Gets the membership events of users in the room. Defaults to all membership
* types, though this can be controlled with the membership and notMembership
* arguments. To change the point in time, use the batchToken.
* @param {string} roomId The room ID to get members in.
* @param {string} batchToken The point in time to get members at (or null for 'now')
* @param {string[]} membership The membership kinds to search for.
* @param {string[]} notMembership The membership kinds to not search for.
* @returns {Promise<MembershipEvent[]>} Resolves to the membership events of the users in the room.
* @see getRoomMembersByMembership
* @see getRoomMembersWithoutMembership
* @see getAllRoomMembers
*/
getRoomMembers(roomId, batchToken = null, membership = null, notMembership = null) {
if (!membership && !notMembership) {
return this.getAllRoomMembers(roomId, batchToken);
}
return Promise.all([
...(membership ?? []).map(m => this.getRoomMembersAt(roomId, m, null, batchToken)),
...(notMembership ?? []).map(m => this.getRoomMembersAt(roomId, null, m, batchToken)),
]).then(r => r.reduce((p, c) => {
p.push(...c);
return p;
}, [])).then(r => {
// Shouldn't ever happen, but dedupe just in case.
const vals = new Map();
for (const ev of r) {
if (!vals.has(ev.membershipFor)) {
vals.set(ev.membershipFor, ev);
}
}
return Array.from(vals.values());
});
}
/**
* Gets all room members in the room, optionally at a given point in time.
* @param {string} roomId The room ID to get members of.
* @param {string} atToken Optional batch token to get members at. Leave falsy for "now".
* @returns {Promise<MembershipEvent[]>} Resolves to the member events in the room.
*/
getAllRoomMembers(roomId, atToken) {
return this.getRoomMembersAt(roomId, null, null, atToken);
}
/**
* Gets the membership events of users in the room which have a particular membership type. To change
* the point in time the server should return membership events at, use `atToken`.
* @param {string} roomId The room ID to get members in.
* @param {Membership} membership The membership to search for.
* @param {string?} atToken Optional batch token to use, or null for "now".
* @returns {Promise<MembershipEvent[]>} Resolves to the membership events of the users in the room.
*/
getRoomMembersByMembership(roomId, membership, atToken) {
return this.getRoomMembersAt(roomId, membership, null, atToken);
}
/**
* Gets the membership events of users in the room which lack a particular membership type. To change
* the point in time the server should return membership events at, use `atToken`.
* @param {string} roomId The room ID to get members in.
* @param {Membership} notMembership The membership to NOT search for.
* @param {string?} atToken Optional batch token to use, or null for "now".
* @returns {Promise<MembershipEvent[]>} Resolves to the membership events of the users in the room.
*/
async getRoomMembersWithoutMembership(roomId, notMembership, atToken) {
return this.getRoomMembersAt(roomId, null, notMembership, atToken);
}
getRoomMembersAt(roomId, membership, notMembership, atToken) {
const qs = {};
if (atToken)
qs["at"] = atToken;
if (membership)
qs["membership"] = membership;
if (notMembership)
qs["not_membership"] = notMembership;
return this.doRequest("GET", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/members", qs).then(r => {
return r['chunk'].map(e => new MembershipEvent_1.MembershipEvent(e));