UNPKG

@rocket.chat/forked-matrix-bot-sdk

Version:

TypeScript/JavaScript SDK for Matrix bots and appservices

1,037 lines 107 kB
"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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var _a, _b; Object.defineProperty(exports, "__esModule", { value: true }); exports.MatrixClient = void 0; const events_1 = require("events"); const MemoryStorageProvider_1 = require("./storage/MemoryStorageProvider"); const UnstableApis_1 = require("./UnstableApis"); const request_1 = require("./request"); const LogService_1 = require("./logging/LogService"); const htmlencode_1 = require("htmlencode"); 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 html_to_text_1 = require("html-to-text"); 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 SYNC_BACKOFF_MIN_MS = 5000; const SYNC_BACKOFF_MAX_MS = 15000; /** * A client that is capable of interacting with a matrix homeserver. */ class MatrixClient extends events_1.EventEmitter { /** * 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) { super(); this.homeserverUrl = homeserverUrl; this.accessToken = accessToken; this.storage = storage; this.cryptoStore = 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. */ this.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. */ this.syncingTimeout = 30000; this.requestId = 0; this.lastJoinedRoomIds = []; this.joinStrategy = null; this.eventProcessors = {}; this.filterId = 0; this.stopSyncing = false; this.metricsInstance = new Metrics_1.Metrics(); this.unstableApisInstance = new UnstableApis_1.UnstableApis(this); /** * 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. */ this.persistTokenAfterSync = false; 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); LogService_1.LogService.debug("MatrixClientLite", "End-to-end encryption client created"); } else { // LogService.trace("MatrixClientLite", "Not setting up encryption"); } if (!this.storage) this.storage = new MemoryStorageProvider_1.MemoryStorageProvider(); this.dms = new DMs_1.DMs(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. */ getIdentityServerClient(identityServerName) { return __awaiter(this, void 0, void 0, function* () { const oidcToken = yield 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); } } processEvent(event) { return __awaiter(this, void 0, void 0, function* () { if (!event) return event; if (!this.eventProcessors[event["type"]]) return event; for (const processor of this.eventProcessors[event["type"]]) { yield processor.processEvent(event, this, EventKind_1.EventKind.RoomEvent); } return event; }); } /** * Retrieves an OpenID Connect token from the homeserver for the current user. * @returns {Promise<OpenIDConnectToken>} Resolves to the token. */ getOpenIDConnectToken() { return __awaiter(this, void 0, void 0, function* () { const userId = encodeURIComponent(yield this.getUserId()); return this.doRequest("POST", "/_matrix/client/r0/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. */ getAccountData(eventType) { return __awaiter(this, void 0, void 0, function* () { const userId = encodeURIComponent(yield this.getUserId()); eventType = encodeURIComponent(eventType); return this.doRequest("GET", "/_matrix/client/r0/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. */ getRoomAccountData(eventType, roomId) { return __awaiter(this, void 0, void 0, function* () { const userId = encodeURIComponent(yield this.getUserId()); eventType = encodeURIComponent(eventType); roomId = encodeURIComponent(roomId); return this.doRequest("GET", "/_matrix/client/r0/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. */ getSafeAccountData(eventType, defaultContent = null) { return __awaiter(this, void 0, void 0, function* () { try { return yield 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. */ getSafeRoomAccountData(eventType, roomId, defaultContent = null) { return __awaiter(this, void 0, void 0, function* () { try { return yield 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 */ setAccountData(eventType, content) { return __awaiter(this, void 0, void 0, function* () { const userId = encodeURIComponent(yield this.getUserId()); eventType = encodeURIComponent(eventType); return this.doRequest("PUT", "/_matrix/client/r0/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 */ setRoomAccountData(eventType, roomId, content) { return __awaiter(this, void 0, void 0, function* () { const userId = encodeURIComponent(yield this.getUserId()); eventType = encodeURIComponent(eventType); roomId = encodeURIComponent(roomId); return this.doRequest("PUT", "/_matrix/client/r0/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. */ getPresenceStatus() { return __awaiter(this, void 0, void 0, function* () { return this.getPresenceStatusFor(yield 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. */ getPresenceStatusFor(userId) { return __awaiter(this, void 0, void 0, function* () { return this.doRequest("GET", "/_matrix/client/r0/presence/" + encodeURIComponent(userId) + "/status").then(r => new Presence_1.Presence(r)); }); } /** * Sets the presence status for the current user. * @param {"online"|"offline"|"unavailable"} 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. */ setPresenceStatus(presence, statusMessage = null) { return __awaiter(this, void 0, void 0, function* () { return this.doRequest("PUT", "/_matrix/client/r0/presence/" + encodeURIComponent(yield 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. */ getPublishedAlias(roomIdOrAlias) { return __awaiter(this, void 0, void 0, function* () { try { const roomId = yield this.resolveRoom(roomIdOrAlias); const event = yield 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/r0/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/r0/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/r0/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/r0/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 */ resolveRoom(roomIdOrAlias) { return __awaiter(this, void 0, void 0, function* () { 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/r0/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/r0/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/r0/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/r0/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/r0/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 */ getUserId() { return __awaiter(this, void 0, void 0, function* () { if (this.userId) return this.userId; // getWhoAmI should populate `this.userId` for us yield this.getWhoAmI(); return this.userId; }); } /** * Gets the user's information from the server directly. * @returns {Promise<IWhoAmI>} The "who am I" response. */ getWhoAmI() { return __awaiter(this, void 0, void 0, function* () { const whoami = yield this.doRequest("GET", "/_matrix/client/r0/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 */ start(filter = null) { return __awaiter(this, void 0, void 0, function* () { yield 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 = yield this.getJoinedRooms(); const userId = yield this.getUserId(); if (this.crypto) { LogService_1.LogService.debug("MatrixClientLite", "Preparing end-to-end encryption"); yield this.crypto.prepare(this.lastJoinedRoomIds); LogService_1.LogService.info("MatrixClientLite", "End-to-end encryption enabled"); } let createFilter = false; // noinspection ES6RedundantAwait let existingFilter = yield 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"); return this.doRequest("POST", "/_matrix/client/r0/user/" + encodeURIComponent(userId) + "/filter", null, filter).then((response) => __awaiter(this, void 0, void 0, function* () { this.filterId = response["filter_id"]; // noinspection ES6RedundantAwait yield Promise.resolve(this.storage.setSyncToken(null)); // noinspection ES6RedundantAwait yield 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(); } startSync(emitFn = null) { return __awaiter(this, void 0, void 0, function* () { // noinspection ES6RedundantAwait let token = yield Promise.resolve(this.storage.getSyncToken()); const promiseWhile = () => __awaiter(this, void 0, void 0, function* () { if (this.stopSyncing) { LogService_1.LogService.info("MatrixClientLite", "Client stop requested - stopping sync"); return; } try { const response = yield this.doSync(token); token = response["next_batch"]; if (!this.persistTokenAfterSync) { yield Promise.resolve(this.storage.setSyncToken(token)); } LogService_1.LogService.debug("MatrixClientLite", "Received sync. Next token: " + token); yield this.processSync(response, emitFn); if (this.persistTokenAfterSync) { yield Promise.resolve(this.storage.setSyncToken(token)); } } catch (e) { 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`); yield 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/r0/sync", conf, null, (token ? 40000 : 600000)); } processSync(raw, emitFn = null) { var _a, _b, _c, _d, _e, _f, _g; return __awaiter(this, void 0, void 0, function* () { if (!emitFn) emitFn = (e, ...p) => Promise.resolve(this.emit(e, ...p)); if (!raw) return; // nothing to process if (this.crypto) { const inbox = []; if ((_a = raw['to_device']) === null || _a === void 0 ? void 0 : _a['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 = (_b = raw['device_one_time_keys_count']) !== null && _b !== void 0 ? _b : {}; const changed = (_d = (_c = raw['device_lists']) === null || _c === void 0 ? void 0 : _c['changed']) !== null && _d !== void 0 ? _d : []; const left = (_f = (_e = raw['device_lists']) === null || _e === void 0 ? void 0 : _e['left']) !== null && _f !== void 0 ? _f : []; yield this.crypto.updateSyncData(inbox, counts, unusedFallbacks, changed, left); } // Always process device messages first to ensure there are decryption keys if (raw['groups']) { const leave = raw['groups']['leave'] || {}; for (const groupId of Object.keys(leave)) { yield emitFn("unstable.group.leave", groupId, leave[groupId]); } const join = raw['groups']['join'] || {}; for (const groupId of Object.keys(join)) { yield emitFn("unstable.group.join", groupId, join[groupId]); } const invite = raw['groups']['invite'] || {}; for (const groupId of Object.keys(invite)) { yield emitFn("unstable.group.invite", groupId, invite[groupId]); } } if (raw['account_data'] && raw['account_data']['events']) { for (const event of raw['account_data']['events']) { yield emitFn("account_data", event); } } if (!raw['rooms']) return; // nothing more to process let leftRooms = raw['rooms']['leave'] || {}; let inviteRooms = raw['rooms']['invite'] || {}; let joinedRooms = raw['rooms']['join'] || {}; // Process rooms we've left first for (let roomId in leftRooms) { const room = leftRooms[roomId]; if (room['account_data'] && room['account_data']['events']) { for (const event of room['account_data']['events']) { yield emitFn("room.account_data", roomId, event); } } if (!room['timeline'] || !room['timeline']['events']) continue; let leaveEvent = null; for (let event of room['timeline']['events']) { if (event['type'] !== 'm.room.member') continue; if (event['state_key'] !== (yield this.getUserId())) 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 = yield this.processEvent(leaveEvent); yield emitFn("room.leave", roomId, leaveEvent); this.lastJoinedRoomIds = this.lastJoinedRoomIds.filter(r => r !== roomId); } // Process rooms we've been invited to for (let roomId in inviteRooms) { const room = inviteRooms[roomId]; if (!room['invite_state'] || !room['invite_state']['events']) continue; let inviteEvent = null; for (let event of room['invite_state']['events']) { if (event['type'] !== 'm.room.member') continue; if (event['state_key'] !== (yield 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 = yield this.processEvent(inviteEvent); yield emitFn("room.invite", roomId, inviteEvent); } // Process rooms we've joined and their events for (let roomId in joinedRooms) { if (this.lastJoinedRoomIds.indexOf(roomId) === -1) { yield emitFn("room.join", roomId); this.lastJoinedRoomIds.push(roomId); } const room = joinedRooms[roomId]; if (room['account_data'] && room['account_data']['events']) { for (const event of room['account_data']['events']) { yield emitFn("room.account_data", roomId, event); } } if (!room['timeline'] || !room['timeline']['events']) continue; for (let event of room['timeline']['events']) { event = yield this.processEvent(event); if (event['type'] === 'm.room.encrypted' && (yield ((_g = this.crypto) === null || _g === void 0 ? void 0 : _g.isRoomEncrypted(roomId)))) { yield emitFn("room.encrypted_event", roomId, event); try { event = (yield this.crypto.decryptRoomEvent(new EncryptedRoomEvent_1.EncryptedRoomEvent(event), roomId)).raw; event = yield this.processEvent(event); yield emitFn("room.decrypted_event", roomId, event); } catch (e) { LogService_1.LogService.error("MatrixClientLite", `Decryption error on ${roomId} ${event['event_id']}`, e); yield emitFn("room.failed_decryption", roomId, event, e); } } if (event['type'] === 'm.room.message') { yield emitFn("room.message", roomId, event); } if (event['type'] === 'm.room.tombstone' && event['state_key'] === '') { yield emitFn("room.archived", roomId, event); } if (event['type'] === 'm.room.create' && event['state_key'] === '' && event['content'] && event['content']['predecessor'] && event['content']['predecessor']['room_id']) { yield emitFn("room.upgraded", roomId, event); } yield 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 */ getEvent(roomId, eventId) { var _a; return __awaiter(this, void 0, void 0, function* () { const event = yield this.getRawEvent(roomId, eventId); if (event['type'] === 'm.room.encrypted' && (yield ((_a = this.crypto) === null || _a === void 0 ? void 0 : _a.isRoomEncrypted(roomId)))) { return this.processEvent((yield 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/r0/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/r0/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) { return this.doRequest("GET", "/_matrix/client/r0/rooms/" + encodeURIComponent(roomId) + "/state/" + encodeURIComponent(type) + "/" + encodeURIComponent(stateKey ? stateKey : '')) .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 */ getEventContext(roomId, eventId, limit = 10) { return __awaiter(this, void 0, void 0, function* () { const res = yield this.doRequest("GET", "/_matrix/client/r0/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)), }; }); } /** * 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/r0/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 */ setDisplayName(displayName) { return __awaiter(this, void 0, void 0, function* () { const userId = encodeURIComponent(yield this.getUserId()); return this.doRequest("PUT", "/_matrix/client/r0/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 */ setAvatarUrl(avatarUrl) { return __awaiter(this, void 0, void 0, function* () { const userId = encodeURIComponent(yield this.getUserId()); return this.doRequest("PUT", "/_matrix/client/r0/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 */ joinRoom(roomIdOrAlias, viaServers = []) { return __awaiter(this, void 0, void 0, function* () { const apiCall = (targetIdOrAlias) => { targetIdOrAlias = encodeURIComponent(targetIdOrAlias); const qs = {}; if (viaServers.length > 0) qs['server_name'] = viaServers; return this.doRequest("POST", "/_matrix/client/r0/join/" + targetIdOrAlias, qs).then(response => { return response['room_id']; }); }; const userId = yield 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/r0/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/r0/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. */ getJoinedRoomMembersWithProfiles(roomId) { return __awaiter(this, void 0, void 0, function* () { return (yield this.doRequest("GET", "/_matrix/client/r0/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<any[]>} Resolves to the membership events of the users in the room. */ getRoomMembers(roomId, batchToken = null, membership = null, notMembership = null) { const qs = {}; if (batchToken) qs["at"] = batchToken; if (membership) qs["membership"] = membership; if (notMembership) qs["not_membership"] = notMembership; return this.doRequest("GET", "/_matrix/client/r0/rooms/" + encodeURIComponent(roomId) + "/members", qs).then(r => { return r['chunk'].map(e => new MembershipEvent_1.MembershipEvent(e)); }); } /** * Leaves the given room * @param {string} roomId the room ID to leave * @returns {Promise<any>} resolves when left */ leaveRoom(roomId) { return this.doRequest("POST", "/_matrix/client/r0/rooms/" + encodeURIComponent(roomId) + "/leave"); } /** * Sends a read receipt for an event in a room * @param {string} roomId the room ID to send the receipt to * @param {string} eventId the event ID to set the receipt at * @returns {Promise<any>} resolves when the receipt has been sent */ sendReadReceipt(roomId, eventId) { return this.doRequest("POST", "/_matrix/client/r0/rooms/" + encodeURIComponent(roomId) + "/receipt/m.read/" + encodeURIComponent(eventId), null, {}); } /** * Sets the typing status of the current user in a room * @param {string} roomId the room ID the user is typing in * @param {boolean} typing is the user currently typing * @param {number} timeout how long should the server preserve the typing state, in milliseconds * @returns {Promise<any>} resolves when the typing state has been set */ setTyping(roomId, typing, timeout = 30000) { return __awaiter(this, void 0, void 0, function* () { const userId = yield this.getUserId(); return this.doRequest("PUT", "/_matrix/client/r0/rooms/" + encodeURIComponent(roomId) + "/typing/" + encodeURIComponent(userId), null, { typing, timeout, }); }); } /** * Replies to a given event with the given text. The event is sent with a msgtype of m.text. * The message will be encrypted if the client supports encryption and the room is encrypted. * @param {string} roomId the room ID to reply in * @param {any} event the event to reply to * @param {string} text the text to reply with * @param {string} html the HTML to reply with, or falsey to use the `text` * @returns {Promise<string>} resolves to the event ID which was sent */ replyText(roomId, event, text, html = null) { if (!html) html = (0, htmlencode_1.htmlEncode)(text); const reply = RichReply_1.RichReply.createFor(roomId, event, text, html); return this.sendMessage(roomId, reply); } /** * Replies to a given event with the given HTML. The event is sent with a msgtype of m.text. * The message will be encrypted if the client supports encryption and the room is encrypted. * @param {string} roomId the room ID to reply in * @param {any} event the event to reply to * @param {string} html the HTML to reply with. * @returns {Promise<string>} resolves to the event ID which was sent */ replyHtmlText(roomId, event, html) { const text = (0, html_to_text_1.htmlToText)(html, { wordwrap: false }); const reply = RichReply_1.RichReply.createFor(roomId, event, text, html); return this.sendMessage(roomId, reply); } /** * Replies to a given ev