Source

MatrixClient.ts

  1. import { EventEmitter } from "events";
  2. import { htmlEncode } from "htmlencode";
  3. import { htmlToText } from "html-to-text";
  4. import { IStorageProvider } from "./storage/IStorageProvider";
  5. import { MemoryStorageProvider } from "./storage/MemoryStorageProvider";
  6. import { IJoinRoomStrategy } from "./strategies/JoinRoomStrategy";
  7. import { UnstableApis } from "./UnstableApis";
  8. import { IPreprocessor } from "./preprocessors/IPreprocessor";
  9. import { getRequestFn } from "./request";
  10. import { extractRequestError, LogService } from "./logging/LogService";
  11. import { RichReply } from "./helpers/RichReply";
  12. import { Metrics } from "./metrics/Metrics";
  13. import { timedMatrixClientFunctionCall } from "./metrics/decorators";
  14. import { AdminApis } from "./AdminApis";
  15. import { Presence } from "./models/Presence";
  16. import { Membership, MembershipEvent } from "./models/events/MembershipEvent";
  17. import { RoomEvent, RoomEventContent, StateEvent } from "./models/events/RoomEvent";
  18. import { EventContext } from "./models/EventContext";
  19. import { PowerLevelBounds } from "./models/PowerLevelBounds";
  20. import { EventKind } from "./models/events/EventKind";
  21. import { IdentityClient } from "./identity/IdentityClient";
  22. import { OpenIDConnectToken } from "./models/OpenIDConnect";
  23. import { doHttpRequest } from "./http";
  24. import { Space, SpaceCreateOptions } from "./models/Spaces";
  25. import { PowerLevelAction } from "./models/PowerLevelAction";
  26. import { CryptoClient } from "./e2ee/CryptoClient";
  27. import {
  28. FallbackKey,
  29. IToDeviceMessage,
  30. MultiUserDeviceListResponse,
  31. OTKAlgorithm,
  32. OTKClaimResponse,
  33. OTKCounts,
  34. OTKs,
  35. OwnUserDevice,
  36. } from "./models/Crypto";
  37. import { requiresCrypto } from "./e2ee/decorators";
  38. import { ICryptoStorageProvider } from "./storage/ICryptoStorageProvider";
  39. import { EncryptedRoomEvent } from "./models/events/EncryptedRoomEvent";
  40. import { IWhoAmI } from "./models/Account";
  41. import { RustSdkCryptoStorageProvider } from "./storage/RustSdkCryptoStorageProvider";
  42. import { DMs } from "./DMs";
  43. import { ServerVersions } from "./models/ServerVersions";
  44. import { RoomCreateOptions } from "./models/CreateRoom";
  45. import { PresenceState } from './models/events/PresenceEvent';
  46. const SYNC_BACKOFF_MIN_MS = 5000;
  47. const SYNC_BACKOFF_MAX_MS = 15000;
  48. const VERSIONS_CACHE_MS = 7200000; // 2 hours
  49. /**
  50. * A client that is capable of interacting with a matrix homeserver.
  51. */
  52. export class MatrixClient extends EventEmitter {
  53. /**
  54. * The presence status to use while syncing. The valid values are "online" to set the account as online,
  55. * "offline" to set the user as offline, "unavailable" for marking the user away, and null for not setting
  56. * an explicit presence (the default).
  57. *
  58. * Has no effect if the client is not syncing. Does not apply until the next sync request.
  59. */
  60. public syncingPresence: PresenceState | null = null;
  61. /**
  62. * The number of milliseconds to wait for new events for on the next sync.
  63. *
  64. * Has no effect if the client is not syncing. Does not apply until the next sync request.
  65. */
  66. public syncingTimeout = 30000;
  67. /**
  68. * The crypto manager instance for this client. Generally speaking, this shouldn't
  69. * need to be accessed but is made available.
  70. *
  71. * Will be null/undefined if crypto is not possible.
  72. */
  73. public readonly crypto: CryptoClient;
  74. /**
  75. * The DM manager instance for this client.
  76. */
  77. public readonly dms: DMs;
  78. private userId: string;
  79. private requestId = 0;
  80. private lastJoinedRoomIds: string[] = [];
  81. private impersonatedUserId: string;
  82. private impersonatedDeviceId: string;
  83. private joinStrategy: IJoinRoomStrategy = null;
  84. private eventProcessors: { [eventType: string]: IPreprocessor[] } = {};
  85. private filterId = 0;
  86. private stopSyncing = false;
  87. private metricsInstance: Metrics = new Metrics();
  88. private unstableApisInstance = new UnstableApis(this);
  89. private cachedVersions: ServerVersions;
  90. private versionsLastFetched = 0;
  91. /**
  92. * Set this to true to have the client only persist the sync token after the sync
  93. * has been processed successfully. Note that if this is true then when the sync
  94. * loop throws an error the client will not persist a token.
  95. */
  96. protected persistTokenAfterSync = false;
  97. /**
  98. * Creates a new matrix client
  99. * @param {string} homeserverUrl The homeserver's client-server API URL
  100. * @param {string} accessToken The access token for the homeserver
  101. * @param {IStorageProvider} storage The storage provider to use. Defaults to MemoryStorageProvider.
  102. * @param {ICryptoStorageProvider} cryptoStore Optional crypto storage provider to use. If not supplied,
  103. * end-to-end encryption will not be functional in this client.
  104. */
  105. constructor(
  106. public readonly homeserverUrl: string,
  107. public readonly accessToken: string,
  108. private storage: IStorageProvider = null,
  109. public readonly cryptoStore: ICryptoStorageProvider = null,
  110. ) {
  111. super();
  112. if (this.homeserverUrl.endsWith("/")) {
  113. this.homeserverUrl = this.homeserverUrl.substring(0, this.homeserverUrl.length - 1);
  114. }
  115. if (this.cryptoStore) {
  116. if (!this.storage || this.storage instanceof MemoryStorageProvider) {
  117. LogService.warn("MatrixClientLite", "Starting an encryption-capable client with a memory store is not considered a good idea.");
  118. }
  119. if (!(this.cryptoStore instanceof RustSdkCryptoStorageProvider)) {
  120. throw new Error("Cannot support custom encryption stores: Use a RustSdkCryptoStorageProvider");
  121. }
  122. this.crypto = new CryptoClient(this);
  123. this.on("room.event", (roomId, event) => {
  124. // noinspection JSIgnoredPromiseFromCall
  125. this.crypto.onRoomEvent(roomId, event);
  126. });
  127. this.on("room.join", (roomId) => {
  128. // noinspection JSIgnoredPromiseFromCall
  129. this.crypto.onRoomJoin(roomId);
  130. });
  131. LogService.debug("MatrixClientLite", "End-to-end encryption client created");
  132. } else {
  133. // LogService.trace("MatrixClientLite", "Not setting up encryption");
  134. }
  135. if (!this.storage) this.storage = new MemoryStorageProvider();
  136. this.dms = new DMs(this);
  137. }
  138. /**
  139. * The storage provider for this client. Direct access is usually not required.
  140. */
  141. public get storageProvider(): IStorageProvider {
  142. return this.storage;
  143. }
  144. /**
  145. * The metrics instance for this client
  146. */
  147. public get metrics(): Metrics {
  148. return this.metricsInstance;
  149. }
  150. /**
  151. * Assigns a new metrics instance, overwriting the old one.
  152. * @param {Metrics} metrics The new metrics instance.
  153. */
  154. public set metrics(metrics: Metrics) {
  155. if (!metrics) throw new Error("Metrics cannot be null/undefined");
  156. this.metricsInstance = metrics;
  157. }
  158. /**
  159. * Gets the unstable API access class. This is generally not recommended to be
  160. * used by clients.
  161. * @return {UnstableApis} The unstable API access class.
  162. */
  163. public get unstableApis(): UnstableApis {
  164. return this.unstableApisInstance;
  165. }
  166. /**
  167. * Gets the admin API access class.
  168. * @return {AdminApis} The admin API access class.
  169. */
  170. public get adminApis(): AdminApis {
  171. return new AdminApis(this);
  172. }
  173. /**
  174. * Sets a user ID to impersonate as. This will assume that the access token for this client
  175. * is for an application service, and that the userId given is within the reach of the
  176. * application service. Setting this to null will stop future impersonation. The user ID is
  177. * assumed to already be valid
  178. * @param {string} userId The user ID to masquerade as, or `null` to clear masquerading.
  179. * @param {string} deviceId Optional device ID to impersonate under the given user, if supported
  180. * by the server. Check the whoami response after setting.
  181. */
  182. public impersonateUserId(userId: string | null, deviceId?: string): void {
  183. this.impersonatedUserId = userId;
  184. this.userId = userId;
  185. if (userId) {
  186. this.impersonatedDeviceId = deviceId;
  187. } else if (deviceId) {
  188. throw new Error("Cannot impersonate just a device: need a user ID");
  189. } else {
  190. this.impersonatedDeviceId = null;
  191. }
  192. }
  193. /**
  194. * Acquires an identity server client for communicating with an identity server. Note that
  195. * this will automatically do the login portion to establish a usable token with the identity
  196. * server provided, but it will not automatically accept any terms of service.
  197. *
  198. * The identity server name provided will in future be resolved to a server address - for now
  199. * that resolution is assumed to be prefixing the name with `https://`.
  200. * @param {string} identityServerName The domain of the identity server to connect to.
  201. * @returns {Promise<IdentityClient>} Resolves to a prepared identity client.
  202. */
  203. public async getIdentityServerClient(identityServerName: string): Promise<IdentityClient> {
  204. const oidcToken = await this.getOpenIDConnectToken();
  205. return IdentityClient.acquire(oidcToken, `https://${identityServerName}`, this);
  206. }
  207. /**
  208. * Sets the strategy to use for when joinRoom is called on this client
  209. * @param {IJoinRoomStrategy} strategy The strategy to use, or null to use none
  210. */
  211. public setJoinStrategy(strategy: IJoinRoomStrategy): void {
  212. this.joinStrategy = strategy;
  213. }
  214. /**
  215. * Adds a preprocessor to the event pipeline. When this client encounters an event, it
  216. * will try to run it through the preprocessors it can in the order they were added.
  217. * @param {IPreprocessor} preprocessor the preprocessor to add
  218. */
  219. public addPreprocessor(preprocessor: IPreprocessor): void {
  220. if (!preprocessor) throw new Error("Preprocessor cannot be null");
  221. const eventTypes = preprocessor.getSupportedEventTypes();
  222. if (!eventTypes) return; // Nothing to do
  223. for (const eventType of eventTypes) {
  224. if (!this.eventProcessors[eventType]) this.eventProcessors[eventType] = [];
  225. this.eventProcessors[eventType].push(preprocessor);
  226. }
  227. }
  228. private async processEvent(event: any): Promise<any> {
  229. if (!event) return event;
  230. if (!this.eventProcessors[event["type"]]) return event;
  231. for (const processor of this.eventProcessors[event["type"]]) {
  232. await processor.processEvent(event, this, EventKind.RoomEvent);
  233. }
  234. return event;
  235. }
  236. /**
  237. * Retrieves the server's supported specification versions and unstable features.
  238. * @returns {Promise<ServerVersions>} Resolves to the server's supported versions.
  239. */
  240. @timedMatrixClientFunctionCall()
  241. public async getServerVersions(): Promise<ServerVersions> {
  242. if (!this.cachedVersions || (Date.now() - this.versionsLastFetched) >= VERSIONS_CACHE_MS) {
  243. this.cachedVersions = await this.doRequest("GET", "/_matrix/client/versions");
  244. this.versionsLastFetched = Date.now();
  245. }
  246. return this.cachedVersions;
  247. }
  248. /**
  249. * Determines if the server supports a given unstable feature flag. Useful for determining
  250. * if the server can support an unstable MSC.
  251. * @param {string} feature The feature name to look for.
  252. * @returns {Promise<boolean>} Resolves to true if the server supports the flag, false otherwise.
  253. */
  254. public async doesServerSupportUnstableFeature(feature: string): Promise<boolean> {
  255. return !!(await this.getServerVersions()).unstable_features?.[feature];
  256. }
  257. /**
  258. * Determines if the server supports a given version of the specification or not.
  259. * @param {string} version The version to look for. Eg: "v1.1"
  260. * @returns {Promise<boolean>} Resolves to true if the server supports the version, false otherwise.
  261. */
  262. public async doesServerSupportVersion(version: string): Promise<boolean> {
  263. return (await this.getServerVersions()).versions.includes(version);
  264. }
  265. /**
  266. * Determines if the server supports at least one of the given specification versions or not.
  267. * @param {string[]} versions The versions to look for. Eg: ["v1.1"]
  268. * @returns {Promise<boolean>} Resolves to true if the server supports any of the versions, false otherwise.
  269. */
  270. public async doesServerSupportAnyOneVersion(versions: string[]): Promise<boolean> {
  271. for (const version of versions) {
  272. if (await this.doesServerSupportVersion(version)) {
  273. return true;
  274. }
  275. }
  276. return false;
  277. }
  278. /**
  279. * Retrieves an OpenID Connect token from the homeserver for the current user.
  280. * @returns {Promise<OpenIDConnectToken>} Resolves to the token.
  281. */
  282. @timedMatrixClientFunctionCall()
  283. public async getOpenIDConnectToken(): Promise<OpenIDConnectToken> {
  284. const userId = encodeURIComponent(await this.getUserId());
  285. return this.doRequest("POST", "/_matrix/client/v3/user/" + userId + "/openid/request_token", null, {});
  286. }
  287. /**
  288. * Retrieves content from account data.
  289. * @param {string} eventType The type of account data to retrieve.
  290. * @returns {Promise<any>} Resolves to the content of that account data.
  291. */
  292. @timedMatrixClientFunctionCall()
  293. public async getAccountData<T>(eventType: string): Promise<T> {
  294. const userId = encodeURIComponent(await this.getUserId());
  295. eventType = encodeURIComponent(eventType);
  296. return this.doRequest("GET", "/_matrix/client/v3/user/" + userId + "/account_data/" + eventType);
  297. }
  298. /**
  299. * Retrieves content from room account data.
  300. * @param {string} eventType The type of room account data to retrieve.
  301. * @param {string} roomId The room to read the account data from.
  302. * @returns {Promise<any>} Resolves to the content of that account data.
  303. */
  304. @timedMatrixClientFunctionCall()
  305. public async getRoomAccountData<T>(eventType: string, roomId: string): Promise<T> {
  306. const userId = encodeURIComponent(await this.getUserId());
  307. eventType = encodeURIComponent(eventType);
  308. roomId = encodeURIComponent(roomId);
  309. return this.doRequest("GET", "/_matrix/client/v3/user/" + userId + "/rooms/" + roomId + "/account_data/" + eventType);
  310. }
  311. /**
  312. * Retrieves content from account data. If the account data request throws an error,
  313. * this simply returns the default provided.
  314. * @param {string} eventType The type of account data to retrieve.
  315. * @param {any} defaultContent The default value. Defaults to null.
  316. * @returns {Promise<any>} Resolves to the content of that account data, or the default.
  317. */
  318. @timedMatrixClientFunctionCall()
  319. public async getSafeAccountData<T>(eventType: string, defaultContent: T = null): Promise<T> {
  320. try {
  321. return await this.getAccountData(eventType);
  322. } catch (e) {
  323. LogService.warn("MatrixClient", `Error getting ${eventType} account data:`, extractRequestError(e));
  324. return defaultContent;
  325. }
  326. }
  327. /**
  328. * Retrieves content from room account data. If the account data request throws an error,
  329. * this simply returns the default provided.
  330. * @param {string} eventType The type of room account data to retrieve.
  331. * @param {string} roomId The room to read the account data from.
  332. * @param {any} defaultContent The default value. Defaults to null.
  333. * @returns {Promise<any>} Resolves to the content of that room account data, or the default.
  334. */
  335. @timedMatrixClientFunctionCall()
  336. public async getSafeRoomAccountData<T>(eventType: string, roomId: string, defaultContent: T = null): Promise<T> {
  337. try {
  338. return await this.getRoomAccountData(eventType, roomId);
  339. } catch (e) {
  340. LogService.warn("MatrixClient", `Error getting ${eventType} room account data in ${roomId}:`, extractRequestError(e));
  341. return defaultContent;
  342. }
  343. }
  344. /**
  345. * Sets account data.
  346. * @param {string} eventType The type of account data to set
  347. * @param {any} content The content to set
  348. * @returns {Promise<any>} Resolves when updated
  349. */
  350. @timedMatrixClientFunctionCall()
  351. public async setAccountData(eventType: string, content: any): Promise<any> {
  352. const userId = encodeURIComponent(await this.getUserId());
  353. eventType = encodeURIComponent(eventType);
  354. return this.doRequest("PUT", "/_matrix/client/v3/user/" + userId + "/account_data/" + eventType, null, content);
  355. }
  356. /**
  357. * Sets room account data.
  358. * @param {string} eventType The type of room account data to set
  359. * @param {string} roomId The room to set account data in
  360. * @param {any} content The content to set
  361. * @returns {Promise<any>} Resolves when updated
  362. */
  363. @timedMatrixClientFunctionCall()
  364. public async setRoomAccountData(eventType: string, roomId: string, content: any): Promise<any> {
  365. const userId = encodeURIComponent(await this.getUserId());
  366. eventType = encodeURIComponent(eventType);
  367. roomId = encodeURIComponent(roomId);
  368. return this.doRequest("PUT", "/_matrix/client/v3/user/" + userId + "/rooms/" + roomId + "/account_data/" + eventType, null, content);
  369. }
  370. /**
  371. * Gets the presence information for the current user.
  372. * @returns {Promise<Presence>} Resolves to the presence status of the user.
  373. */
  374. @timedMatrixClientFunctionCall()
  375. public async getPresenceStatus(): Promise<Presence> {
  376. return this.getPresenceStatusFor(await this.getUserId());
  377. }
  378. /**
  379. * Gets the presence information for a given user.
  380. * @param {string} userId The user ID to look up the presence of.
  381. * @returns {Promise<Presence>} Resolves to the presence status of the user.
  382. */
  383. @timedMatrixClientFunctionCall()
  384. public async getPresenceStatusFor(userId: string): Promise<Presence> {
  385. return this.doRequest("GET", "/_matrix/client/v3/presence/" + encodeURIComponent(userId) + "/status").then(r => new Presence(r));
  386. }
  387. /**
  388. * Sets the presence status for the current user.
  389. * @param {PresenceState} presence The new presence state for the user.
  390. * @param {string?} statusMessage Optional status message to include with the presence.
  391. * @returns {Promise<any>} Resolves when complete.
  392. */
  393. @timedMatrixClientFunctionCall()
  394. public async setPresenceStatus(presence: PresenceState, statusMessage: string | undefined = undefined): Promise<any> {
  395. return this.doRequest("PUT", "/_matrix/client/v3/presence/" + encodeURIComponent(await this.getUserId()) + "/status", null, {
  396. presence: presence,
  397. status_msg: statusMessage,
  398. });
  399. }
  400. /**
  401. * Gets a published alias for the given room. These are supplied by the room admins
  402. * and should point to the room, but may not. This is primarily intended to be used
  403. * in the context of rendering a mention (pill) for a room.
  404. * @param {string} roomIdOrAlias The room ID or alias to get an alias for.
  405. * @returns {Promise<string>} Resolves to a published room alias, or falsey if none found.
  406. */
  407. @timedMatrixClientFunctionCall()
  408. public async getPublishedAlias(roomIdOrAlias: string): Promise<string> {
  409. try {
  410. const roomId = await this.resolveRoom(roomIdOrAlias);
  411. const event = await this.getRoomStateEvent(roomId, "m.room.canonical_alias", "");
  412. if (!event) return null;
  413. const canonical = event['alias'];
  414. const alt = event['alt_aliases'] || [];
  415. return canonical || alt[0];
  416. } catch (e) {
  417. // Assume none
  418. return null;
  419. }
  420. }
  421. /**
  422. * Adds a new room alias to the room directory
  423. * @param {string} alias The alias to add (eg: "#my-room:matrix.org")
  424. * @param {string} roomId The room ID to add the alias to
  425. * @returns {Promise} resolves when the alias has been added
  426. */
  427. @timedMatrixClientFunctionCall()
  428. public createRoomAlias(alias: string, roomId: string): Promise<any> {
  429. alias = encodeURIComponent(alias);
  430. return this.doRequest("PUT", "/_matrix/client/v3/directory/room/" + alias, null, {
  431. "room_id": roomId,
  432. });
  433. }
  434. /**
  435. * Removes a room alias from the room directory
  436. * @param {string} alias The alias to remove
  437. * @returns {Promise} resolves when the alias has been deleted
  438. */
  439. @timedMatrixClientFunctionCall()
  440. public deleteRoomAlias(alias: string): Promise<any> {
  441. alias = encodeURIComponent(alias);
  442. return this.doRequest("DELETE", "/_matrix/client/v3/directory/room/" + alias);
  443. }
  444. /**
  445. * Sets the visibility of a room in the directory.
  446. * @param {string} roomId The room ID to manipulate the visibility of
  447. * @param {"public" | "private"} visibility The visibility to set for the room
  448. * @return {Promise} resolves when the visibility has been updated
  449. */
  450. @timedMatrixClientFunctionCall()
  451. public setDirectoryVisibility(roomId: string, visibility: "public" | "private"): Promise<any> {
  452. roomId = encodeURIComponent(roomId);
  453. return this.doRequest("PUT", "/_matrix/client/v3/directory/list/room/" + roomId, null, {
  454. "visibility": visibility,
  455. });
  456. }
  457. /**
  458. * Gets the visibility of a room in the directory.
  459. * @param {string} roomId The room ID to query the visibility of
  460. * @return {Promise<"public"|"private">} The visibility of the room
  461. */
  462. @timedMatrixClientFunctionCall()
  463. public getDirectoryVisibility(roomId: string): Promise<"public" | "private"> {
  464. roomId = encodeURIComponent(roomId);
  465. return this.doRequest("GET", "/_matrix/client/v3/directory/list/room/" + roomId).then(response => {
  466. return response["visibility"];
  467. });
  468. }
  469. /**
  470. * Resolves a room ID or alias to a room ID. If the given ID or alias looks like a room ID
  471. * already, it will be returned as-is. If the room ID or alias looks like a room alias, it
  472. * will be resolved to a room ID if possible. If the room ID or alias is neither, an error
  473. * will be raised.
  474. * @param {string} roomIdOrAlias the room ID or alias to resolve to a room ID
  475. * @returns {Promise<string>} resolves to the room ID
  476. */
  477. @timedMatrixClientFunctionCall()
  478. public async resolveRoom(roomIdOrAlias: string): Promise<string> {
  479. if (roomIdOrAlias.startsWith("!")) return roomIdOrAlias; // probably
  480. if (roomIdOrAlias.startsWith("#")) return this.lookupRoomAlias(roomIdOrAlias).then(r => r.roomId);
  481. throw new Error("Invalid room ID or alias");
  482. }
  483. /**
  484. * Does a room directory lookup for a given room alias
  485. * @param {string} roomAlias the room alias to look up in the room directory
  486. * @returns {Promise<RoomDirectoryLookupResponse>} resolves to the room's information
  487. */
  488. @timedMatrixClientFunctionCall()
  489. public lookupRoomAlias(roomAlias: string): Promise<RoomDirectoryLookupResponse> {
  490. return this.doRequest("GET", "/_matrix/client/v3/directory/room/" + encodeURIComponent(roomAlias)).then(response => {
  491. return {
  492. roomId: response["room_id"],
  493. residentServers: response["servers"],
  494. };
  495. });
  496. }
  497. /**
  498. * Invites a user to a room.
  499. * @param {string} userId the user ID to invite
  500. * @param {string} roomId the room ID to invite the user to
  501. * @returns {Promise<any>} resolves when completed
  502. */
  503. @timedMatrixClientFunctionCall()
  504. public inviteUser(userId, roomId) {
  505. return this.doRequest("POST", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/invite", null, {
  506. user_id: userId,
  507. });
  508. }
  509. /**
  510. * Kicks a user from a room.
  511. * @param {string} userId the user ID to kick
  512. * @param {string} roomId the room ID to kick the user in
  513. * @param {string?} reason optional reason for the kick
  514. * @returns {Promise<any>} resolves when completed
  515. */
  516. @timedMatrixClientFunctionCall()
  517. public kickUser(userId, roomId, reason = null) {
  518. return this.doRequest("POST", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/kick", null, {
  519. user_id: userId,
  520. reason: reason,
  521. });
  522. }
  523. /**
  524. * Bans a user from a room.
  525. * @param {string} userId the user ID to ban
  526. * @param {string} roomId the room ID to set the ban in
  527. * @param {string?} reason optional reason for the ban
  528. * @returns {Promise<any>} resolves when completed
  529. */
  530. @timedMatrixClientFunctionCall()
  531. public banUser(userId, roomId, reason = null) {
  532. return this.doRequest("POST", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/ban", null, {
  533. user_id: userId,
  534. reason: reason,
  535. });
  536. }
  537. /**
  538. * Unbans a user in a room.
  539. * @param {string} userId the user ID to unban
  540. * @param {string} roomId the room ID to lift the ban in
  541. * @returns {Promise<any>} resolves when completed
  542. */
  543. @timedMatrixClientFunctionCall()
  544. public unbanUser(userId, roomId) {
  545. return this.doRequest("POST", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/unban", null, {
  546. user_id: userId,
  547. });
  548. }
  549. /**
  550. * Gets the current user ID for this client
  551. * @returns {Promise<string>} The user ID of this client
  552. */
  553. @timedMatrixClientFunctionCall()
  554. public async getUserId(): Promise<string> {
  555. if (this.userId) return this.userId;
  556. // getWhoAmI should populate `this.userId` for us
  557. await this.getWhoAmI();
  558. return this.userId;
  559. }
  560. /**
  561. * Gets the user's information from the server directly.
  562. * @returns {Promise<IWhoAmI>} The "who am I" response.
  563. */
  564. public async getWhoAmI(): Promise<IWhoAmI> {
  565. const whoami = await this.doRequest("GET", "/_matrix/client/v3/account/whoami");
  566. this.userId = whoami["user_id"];
  567. return whoami;
  568. }
  569. /**
  570. * Stops the client from syncing.
  571. */
  572. public stop() {
  573. this.stopSyncing = true;
  574. }
  575. /**
  576. * Starts syncing the client with an optional filter
  577. * @param {any} filter The filter to use, or null for none
  578. * @returns {Promise<any>} Resolves when the client has started syncing
  579. */
  580. public async start(filter: any = null): Promise<any> {
  581. await this.dms.update();
  582. this.stopSyncing = false;
  583. if (!filter || typeof (filter) !== "object") {
  584. LogService.trace("MatrixClientLite", "No filter given or invalid object - using defaults.");
  585. filter = null;
  586. }
  587. LogService.trace("MatrixClientLite", "Populating joined rooms to avoid excessive join emits");
  588. this.lastJoinedRoomIds = await this.getJoinedRooms();
  589. const userId = await this.getUserId();
  590. if (this.crypto) {
  591. LogService.debug("MatrixClientLite", "Preparing end-to-end encryption");
  592. await this.crypto.prepare(this.lastJoinedRoomIds);
  593. LogService.info("MatrixClientLite", "End-to-end encryption enabled");
  594. }
  595. let createFilter = false;
  596. // noinspection ES6RedundantAwait
  597. const existingFilter = await Promise.resolve(this.storage.getFilter());
  598. if (existingFilter) {
  599. LogService.trace("MatrixClientLite", "Found existing filter. Checking consistency with given filter");
  600. if (JSON.stringify(existingFilter.filter) === JSON.stringify(filter)) {
  601. LogService.trace("MatrixClientLite", "Filters match");
  602. this.filterId = existingFilter.id;
  603. } else {
  604. createFilter = true;
  605. }
  606. } else {
  607. createFilter = true;
  608. }
  609. if (createFilter && filter) {
  610. LogService.trace("MatrixClientLite", "Creating new filter");
  611. await this.doRequest("POST", "/_matrix/client/v3/user/" + encodeURIComponent(userId) + "/filter", null, filter).then(async response => {
  612. this.filterId = response["filter_id"];
  613. // noinspection ES6RedundantAwait
  614. await Promise.resolve(this.storage.setSyncToken(null));
  615. // noinspection ES6RedundantAwait
  616. await Promise.resolve(this.storage.setFilter({
  617. id: this.filterId,
  618. filter: filter,
  619. }));
  620. });
  621. }
  622. LogService.trace("MatrixClientLite", "Starting sync with filter ID " + this.filterId);
  623. return this.startSyncInternal();
  624. }
  625. protected startSyncInternal(): Promise<any> {
  626. return this.startSync();
  627. }
  628. protected async startSync(emitFn: (emitEventType: string, ...payload: any[]) => Promise<any> = null) {
  629. // noinspection ES6RedundantAwait
  630. let token = await Promise.resolve(this.storage.getSyncToken());
  631. const promiseWhile = async () => {
  632. if (this.stopSyncing) {
  633. LogService.info("MatrixClientLite", "Client stop requested - stopping sync");
  634. return;
  635. }
  636. try {
  637. const response = await this.doSync(token);
  638. token = response["next_batch"];
  639. if (!this.persistTokenAfterSync) {
  640. await Promise.resolve(this.storage.setSyncToken(token));
  641. }
  642. LogService.debug("MatrixClientLite", "Received sync. Next token: " + token);
  643. await this.processSync(response, emitFn);
  644. if (this.persistTokenAfterSync) {
  645. await Promise.resolve(this.storage.setSyncToken(token));
  646. }
  647. } catch (e) {
  648. // If we've requested to stop syncing, don't bother checking the error.
  649. if (this.stopSyncing) {
  650. LogService.info("MatrixClientLite", "Client stop requested - cancelling sync");
  651. return;
  652. }
  653. LogService.error("MatrixClientLite", "Error handling sync " + extractRequestError(e));
  654. const backoffTime = SYNC_BACKOFF_MIN_MS + Math.random() * (SYNC_BACKOFF_MAX_MS - SYNC_BACKOFF_MIN_MS);
  655. LogService.info("MatrixClientLite", `Backing off for ${backoffTime}ms`);
  656. await new Promise((r) => setTimeout(r, backoffTime));
  657. }
  658. return promiseWhile();
  659. };
  660. promiseWhile(); // start the loop
  661. }
  662. @timedMatrixClientFunctionCall()
  663. protected doSync(token: string): Promise<any> {
  664. LogService.debug("MatrixClientLite", "Performing sync with token " + token);
  665. const conf = {
  666. full_state: false,
  667. timeout: Math.max(0, this.syncingTimeout),
  668. };
  669. // synapse complains if the variables are null, so we have to have it unset instead
  670. if (token) conf["since"] = token;
  671. if (this.filterId) conf['filter'] = this.filterId;
  672. if (this.syncingPresence) conf['presence'] = this.syncingPresence;
  673. // timeout is 40s if we have a token, otherwise 10min
  674. return this.doRequest("GET", "/_matrix/client/v3/sync", conf, null, (token ? 40000 : 600000));
  675. }
  676. @timedMatrixClientFunctionCall()
  677. protected async processSync(raw: any, emitFn: (emitEventType: string, ...payload: any[]) => Promise<any> = null): Promise<any> {
  678. if (!emitFn) emitFn = (e, ...p) => Promise.resolve<any>(this.emit(e, ...p));
  679. if (!raw) return; // nothing to process
  680. if (this.crypto) {
  681. const inbox: IToDeviceMessage[] = [];
  682. if (raw['to_device']?.['events']) {
  683. inbox.push(...raw['to_device']['events']);
  684. // TODO: Emit or do something with unknown messages?
  685. }
  686. let unusedFallbacks: OTKAlgorithm[] = [];
  687. if (raw['org.matrix.msc2732.device_unused_fallback_key_types']) {
  688. unusedFallbacks = raw['org.matrix.msc2732.device_unused_fallback_key_types'];
  689. } else if (raw['device_unused_fallback_key_types']) {
  690. unusedFallbacks = raw['device_unused_fallback_key_types'];
  691. }
  692. const counts = raw['device_one_time_keys_count'] ?? {};
  693. const changed = raw['device_lists']?.['changed'] ?? [];
  694. const left = raw['device_lists']?.['left'] ?? [];
  695. await this.crypto.updateSyncData(inbox, counts, unusedFallbacks, changed, left);
  696. }
  697. // Always process device messages first to ensure there are decryption keys
  698. if (raw['account_data'] && raw['account_data']['events']) {
  699. for (const event of raw['account_data']['events']) {
  700. await emitFn("account_data", event);
  701. }
  702. }
  703. if (!raw['rooms']) return; // nothing more to process
  704. const leftRooms = raw['rooms']['leave'] || {};
  705. const inviteRooms = raw['rooms']['invite'] || {};
  706. const joinedRooms = raw['rooms']['join'] || {};
  707. // Process rooms we've left first
  708. for (const roomId in leftRooms) {
  709. const room = leftRooms[roomId];
  710. if (room['account_data'] && room['account_data']['events']) {
  711. for (const event of room['account_data']['events']) {
  712. await emitFn("room.account_data", roomId, event);
  713. }
  714. }
  715. if (!room['timeline'] || !room['timeline']['events']) continue;
  716. let leaveEvent = null;
  717. for (const event of room['timeline']['events']) {
  718. if (event['type'] !== 'm.room.member') continue;
  719. if (event['state_key'] !== await this.getUserId()) continue;
  720. const membership = event["content"]?.["membership"];
  721. if (membership !== "leave" && membership !== "ban") continue;
  722. const oldAge = leaveEvent && leaveEvent['unsigned'] && leaveEvent['unsigned']['age'] ? leaveEvent['unsigned']['age'] : 0;
  723. const newAge = event['unsigned'] && event['unsigned']['age'] ? event['unsigned']['age'] : 0;
  724. if (leaveEvent && oldAge < newAge) continue;
  725. leaveEvent = event;
  726. }
  727. if (!leaveEvent) {
  728. LogService.warn("MatrixClientLite", "Left room " + roomId + " without receiving an event");
  729. continue;
  730. }
  731. leaveEvent = await this.processEvent(leaveEvent);
  732. await emitFn("room.leave", roomId, leaveEvent);
  733. this.lastJoinedRoomIds = this.lastJoinedRoomIds.filter(r => r !== roomId);
  734. }
  735. // Process rooms we've been invited to
  736. for (const roomId in inviteRooms) {
  737. const room = inviteRooms[roomId];
  738. if (!room['invite_state'] || !room['invite_state']['events']) continue;
  739. let inviteEvent = null;
  740. for (const event of room['invite_state']['events']) {
  741. if (event['type'] !== 'm.room.member') continue;
  742. if (event['state_key'] !== await this.getUserId()) continue;
  743. if (!event['content']) continue;
  744. if (event['content']['membership'] !== "invite") continue;
  745. const oldAge = inviteEvent && inviteEvent['unsigned'] && inviteEvent['unsigned']['age'] ? inviteEvent['unsigned']['age'] : 0;
  746. const newAge = event['unsigned'] && event['unsigned']['age'] ? event['unsigned']['age'] : 0;
  747. if (inviteEvent && oldAge < newAge) continue;
  748. inviteEvent = event;
  749. }
  750. if (!inviteEvent) {
  751. LogService.warn("MatrixClientLite", "Invited to room " + roomId + " without receiving an event");
  752. continue;
  753. }
  754. inviteEvent = await this.processEvent(inviteEvent);
  755. await emitFn("room.invite", roomId, inviteEvent);
  756. }
  757. // Process rooms we've joined and their events
  758. for (const roomId in joinedRooms) {
  759. const room = joinedRooms[roomId];
  760. if (room['account_data'] && room['account_data']['events']) {
  761. for (const event of room['account_data']['events']) {
  762. await emitFn("room.account_data", roomId, event);
  763. }
  764. }
  765. if (!room['timeline'] || !room['timeline']['events']) continue;
  766. for (let event of room['timeline']['events']) {
  767. if (event['type'] === "m.room.member" && event['state_key'] === await this.getUserId()) {
  768. if (event['content']?.['membership'] === "join" && this.lastJoinedRoomIds.indexOf(roomId) === -1) {
  769. await emitFn("room.join", roomId, await this.processEvent(event));
  770. this.lastJoinedRoomIds.push(roomId);
  771. }
  772. }
  773. event = await this.processEvent(event);
  774. if (event['type'] === 'm.room.encrypted' && await this.crypto?.isRoomEncrypted(roomId)) {
  775. await emitFn("room.encrypted_event", roomId, event);
  776. try {
  777. event = (await this.crypto.decryptRoomEvent(new EncryptedRoomEvent(event), roomId)).raw;
  778. event = await this.processEvent(event);
  779. await emitFn("room.decrypted_event", roomId, event);
  780. } catch (e) {
  781. LogService.error("MatrixClientLite", `Decryption error on ${roomId} ${event['event_id']}`, e);
  782. await emitFn("room.failed_decryption", roomId, event, e);
  783. }
  784. }
  785. if (event['type'] === 'm.room.message') {
  786. await emitFn("room.message", roomId, event);
  787. }
  788. if (event['type'] === 'm.room.tombstone' && event['state_key'] === '') {
  789. await emitFn("room.archived", roomId, event);
  790. }
  791. if (event['type'] === 'm.room.create' && event['state_key'] === '' && event['content']
  792. && event['content']['predecessor'] && event['content']['predecessor']['room_id']) {
  793. await emitFn("room.upgraded", roomId, event);
  794. }
  795. await emitFn("room.event", roomId, event);
  796. }
  797. }
  798. }
  799. /**
  800. * Gets an event for a room. If the event is encrypted, and the client supports encryption,
  801. * and the room is encrypted, then this will return a decrypted event.
  802. * @param {string} roomId the room ID to get the event in
  803. * @param {string} eventId the event ID to look up
  804. * @returns {Promise<any>} resolves to the found event
  805. */
  806. @timedMatrixClientFunctionCall()
  807. public async getEvent(roomId: string, eventId: string): Promise<any> {
  808. const event = await this.getRawEvent(roomId, eventId);
  809. if (event['type'] === 'm.room.encrypted' && await this.crypto?.isRoomEncrypted(roomId)) {
  810. return this.processEvent((await this.crypto.decryptRoomEvent(new EncryptedRoomEvent(event), roomId)).raw);
  811. }
  812. return event;
  813. }
  814. /**
  815. * Gets an event for a room. Returned as a raw event.
  816. * @param {string} roomId the room ID to get the event in
  817. * @param {string} eventId the event ID to look up
  818. * @returns {Promise<any>} resolves to the found event
  819. */
  820. @timedMatrixClientFunctionCall()
  821. public getRawEvent(roomId: string, eventId: string): Promise<any> {
  822. return this.doRequest("GET", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/event/" + encodeURIComponent(eventId))
  823. .then(ev => this.processEvent(ev));
  824. }
  825. /**
  826. * Gets the room state for the given room. Returned as raw events.
  827. * @param {string} roomId the room ID to get state for
  828. * @returns {Promise<any[]>} resolves to the room's state
  829. */
  830. @timedMatrixClientFunctionCall()
  831. public getRoomState(roomId: string): Promise<any[]> {
  832. return this.doRequest("GET", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/state")
  833. .then(state => Promise.all(state.map(ev => this.processEvent(ev))));
  834. }
  835. /**
  836. * Gets the state events for a given room of a given type under the given state key.
  837. * @param {string} roomId the room ID
  838. * @param {string} type the event type
  839. * @param {String} stateKey the state key, falsey if not needed
  840. * @returns {Promise<any|any[]>} resolves to the state event(s)
  841. * @deprecated It is not possible to get an array of events - use getRoomStateEvent instead
  842. */
  843. @timedMatrixClientFunctionCall()
  844. public getRoomStateEvents(roomId, type, stateKey): Promise<any | any[]> {
  845. return this.getRoomStateEvent(roomId, type, stateKey);
  846. }
  847. /**
  848. * Gets a state event for a given room of a given type under the given state key.
  849. * @param {string} roomId the room ID
  850. * @param {string} type the event type
  851. * @param {String} stateKey the state key
  852. * @returns {Promise<any>} resolves to the state event
  853. */
  854. @timedMatrixClientFunctionCall()
  855. public getRoomStateEvent(roomId, type, stateKey): Promise<any> {
  856. const path = "/_matrix/client/v3/rooms/"
  857. + encodeURIComponent(roomId) + "/state/"
  858. + encodeURIComponent(type) + "/"
  859. + encodeURIComponent(stateKey ? stateKey : '');
  860. return this.doRequest("GET", path)
  861. .then(ev => this.processEvent(ev));
  862. }
  863. /**
  864. * Gets the context surrounding an event.
  865. * @param {string} roomId The room ID to get the context in.
  866. * @param {string} eventId The event ID to get the context of.
  867. * @param {number} limit The maximum number of events to return on either side of the event.
  868. * @returns {Promise<EventContext>} The context of the event
  869. */
  870. @timedMatrixClientFunctionCall()
  871. public async getEventContext(roomId: string, eventId: string, limit = 10): Promise<EventContext> {
  872. const res = await this.doRequest("GET", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/context/" + encodeURIComponent(eventId), { limit });
  873. return {
  874. event: new RoomEvent<RoomEventContent>(res['event']),
  875. before: res['events_before'].map(e => new RoomEvent<RoomEventContent>(e)),
  876. after: res['events_after'].map(e => new RoomEvent<RoomEventContent>(e)),
  877. state: res['state'].map(e => new StateEvent<RoomEventContent>(e)),
  878. };
  879. }
  880. /**
  881. * Gets the profile for a given user
  882. * @param {string} userId the user ID to lookup
  883. * @returns {Promise<any>} the profile of the user
  884. */
  885. @timedMatrixClientFunctionCall()
  886. public getUserProfile(userId: string): Promise<any> {
  887. return this.doRequest("GET", "/_matrix/client/v3/profile/" + encodeURIComponent(userId));
  888. }
  889. /**
  890. * Sets a new display name for the user.
  891. * @param {string} displayName the new display name for the user, or null to clear
  892. * @returns {Promise<any>} resolves when complete
  893. */
  894. @timedMatrixClientFunctionCall()
  895. public async setDisplayName(displayName: string): Promise<any> {
  896. const userId = encodeURIComponent(await this.getUserId());
  897. return this.doRequest("PUT", "/_matrix/client/v3/profile/" + userId + "/displayname", null, {
  898. displayname: displayName,
  899. });
  900. }
  901. /**
  902. * Sets a new avatar url for the user.
  903. * @param {string} avatarUrl the new avatar URL for the user, in the form of a Matrix Content URI
  904. * @returns {Promise<any>} resolves when complete
  905. */
  906. @timedMatrixClientFunctionCall()
  907. public async setAvatarUrl(avatarUrl: string): Promise<any> {
  908. const userId = encodeURIComponent(await this.getUserId());
  909. return this.doRequest("PUT", "/_matrix/client/v3/profile/" + userId + "/avatar_url", null, {
  910. avatar_url: avatarUrl,
  911. });
  912. }
  913. /**
  914. * Joins the given room
  915. * @param {string} roomIdOrAlias the room ID or alias to join
  916. * @param {string[]} viaServers the server names to try and join through
  917. * @returns {Promise<string>} resolves to the joined room ID
  918. */
  919. @timedMatrixClientFunctionCall()
  920. public async joinRoom(roomIdOrAlias: string, viaServers: string[] = []): Promise<string> {
  921. const apiCall = (targetIdOrAlias: string) => {
  922. targetIdOrAlias = encodeURIComponent(targetIdOrAlias);
  923. const qs = {};
  924. if (viaServers.length > 0) qs['server_name'] = viaServers;
  925. return this.doRequest("POST", "/_matrix/client/v3/join/" + targetIdOrAlias, qs, {}).then(response => {
  926. return response['room_id'];
  927. });
  928. };
  929. const userId = await this.getUserId();
  930. if (this.joinStrategy) return this.joinStrategy.joinRoom(roomIdOrAlias, userId, apiCall);
  931. else return apiCall(roomIdOrAlias);
  932. }
  933. /**
  934. * Gets a list of joined room IDs
  935. * @returns {Promise<string[]>} resolves to a list of room IDs the client participates in
  936. */
  937. @timedMatrixClientFunctionCall()
  938. public getJoinedRooms(): Promise<string[]> {
  939. return this.doRequest("GET", "/_matrix/client/v3/joined_rooms").then(response => response['joined_rooms']);
  940. }
  941. /**
  942. * Gets the joined members in a room. The client must be in the room to make this request.
  943. * @param {string} roomId The room ID to get the joined members of.
  944. * @returns {Promise<string>} The joined user IDs in the room
  945. */
  946. @timedMatrixClientFunctionCall()
  947. public getJoinedRoomMembers(roomId: string): Promise<string[]> {
  948. return this.doRequest("GET", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/joined_members").then(response => {
  949. return Object.keys(response['joined']);
  950. });
  951. }
  952. /**
  953. * 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.
  954. * @param {string} roomId The room ID to get the joined members of.
  955. * @returns {Object} The joined user IDs in the room as an object mapped to a set of profiles.
  956. */
  957. @timedMatrixClientFunctionCall()
  958. public async getJoinedRoomMembersWithProfiles(roomId: string): Promise<{ [userId: string]: { display_name?: string, avatar_url?: string } }> {
  959. return (await this.doRequest("GET", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/joined_members")).joined;
  960. }
  961. /**
  962. * Gets the membership events of users in the room. Defaults to all membership
  963. * types, though this can be controlled with the membership and notMembership
  964. * arguments. To change the point in time, use the batchToken.
  965. * @param {string} roomId The room ID to get members in.
  966. * @param {string} batchToken The point in time to get members at (or null for 'now')
  967. * @param {string[]} membership The membership kinds to search for.
  968. * @param {string[]} notMembership The membership kinds to not search for.
  969. * @returns {Promise<MembershipEvent[]>} Resolves to the membership events of the users in the room.
  970. * @see getRoomMembersByMembership
  971. * @see getRoomMembersWithoutMembership
  972. * @see getAllRoomMembers
  973. */
  974. @timedMatrixClientFunctionCall()
  975. public getRoomMembers(roomId: string, batchToken: string = null, membership: Membership[] = null, notMembership: Membership[] = null): Promise<MembershipEvent[]> {
  976. if (!membership && !notMembership) {
  977. return this.getAllRoomMembers(roomId, batchToken);
  978. }
  979. return Promise.all([
  980. ...(membership ?? []).map(m => this.getRoomMembersAt(roomId, m, null, batchToken)),
  981. ...(notMembership ?? []).map(m => this.getRoomMembersAt(roomId, null, m, batchToken)),
  982. ]).then(r => r.reduce((p, c) => {
  983. p.push(...c);
  984. return p;
  985. }, [])).then(r => {
  986. // Shouldn't ever happen, but dedupe just in case.
  987. const vals = new Map<string, MembershipEvent>();
  988. for (const ev of r) {
  989. if (!vals.has(ev.membershipFor)) {
  990. vals.set(ev.membershipFor, ev);
  991. }
  992. }
  993. return Array.from(vals.values());
  994. });
  995. }
  996. /**
  997. * Gets all room members in the room, optionally at a given point in time.
  998. * @param {string} roomId The room ID to get members of.
  999. * @param {string} atToken Optional batch token to get members at. Leave falsy for "now".
  1000. * @returns {Promise<MembershipEvent[]>} Resolves to the member events in the room.
  1001. */
  1002. @timedMatrixClientFunctionCall()
  1003. public getAllRoomMembers(roomId: string, atToken?: string): Promise<MembershipEvent[]> {
  1004. return this.getRoomMembersAt(roomId, null, null, atToken);
  1005. }
  1006. /**
  1007. * Gets the membership events of users in the room which have a particular membership type. To change
  1008. * the point in time the server should return membership events at, use `atToken`.
  1009. * @param {string} roomId The room ID to get members in.
  1010. * @param {Membership} membership The membership to search for.
  1011. * @param {string?} atToken Optional batch token to use, or null for "now".
  1012. * @returns {Promise<MembershipEvent[]>} Resolves to the membership events of the users in the room.
  1013. */
  1014. @timedMatrixClientFunctionCall()
  1015. public getRoomMembersByMembership(roomId: string, membership: Membership, atToken?: string): Promise<MembershipEvent[]> {
  1016. return this.getRoomMembersAt(roomId, membership, null, atToken);
  1017. }
  1018. /**
  1019. * Gets the membership events of users in the room which lack a particular membership type. To change
  1020. * the point in time the server should return membership events at, use `atToken`.
  1021. * @param {string} roomId The room ID to get members in.
  1022. * @param {Membership} notMembership The membership to NOT search for.
  1023. * @param {string?} atToken Optional batch token to use, or null for "now".
  1024. * @returns {Promise<MembershipEvent[]>} Resolves to the membership events of the users in the room.
  1025. */
  1026. @timedMatrixClientFunctionCall()
  1027. public async getRoomMembersWithoutMembership(roomId: string, notMembership: Membership, atToken?: string): Promise<MembershipEvent[]> {
  1028. return this.getRoomMembersAt(roomId, null, notMembership, atToken);
  1029. }
  1030. private getRoomMembersAt(roomId: string, membership: Membership | null, notMembership: Membership | null, atToken: string | null): Promise<MembershipEvent[]> {
  1031. const qs = {};
  1032. if (atToken) qs["at"] = atToken;
  1033. if (membership) qs["membership"] = membership;
  1034. if (notMembership) qs["not_membership"] = notMembership;
  1035. return this.doRequest("GET", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/members", qs).then(r => {
  1036. return r['chunk'].map(e => new MembershipEvent(e));
  1037. });
  1038. }
  1039. /**
  1040. * Leaves the given room
  1041. * @param {string} roomId the room ID to leave
  1042. * @param {string=} reason Optional reason to be included as the reason for leaving the room.
  1043. * @returns {Promise<any>} resolves when left
  1044. */
  1045. @timedMatrixClientFunctionCall()
  1046. public leaveRoom(roomId: string, reason?: string): Promise<any> {
  1047. return this.doRequest("POST", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/leave", null, { reason });
  1048. }
  1049. /**
  1050. * Forgets the given room
  1051. * @param {string} roomId the room ID to forget
  1052. * @returns {Promise<{}>} Resolves when forgotten
  1053. */
  1054. @timedMatrixClientFunctionCall()
  1055. public forgetRoom(roomId: string): Promise<{}> {
  1056. return this.doRequest("POST", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/forget");
  1057. }
  1058. /**
  1059. * Sends a read receipt for an event in a room
  1060. * @param {string} roomId the room ID to send the receipt to
  1061. * @param {string} eventId the event ID to set the receipt at
  1062. * @returns {Promise<any>} resolves when the receipt has been sent
  1063. */
  1064. @timedMatrixClientFunctionCall()
  1065. public sendReadReceipt(roomId: string, eventId: string): Promise<any> {
  1066. return this.doRequest("POST", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/receipt/m.read/" + encodeURIComponent(eventId), null, {});
  1067. }
  1068. /**
  1069. * Sets the typing status of the current user in a room
  1070. * @param {string} roomId the room ID the user is typing in
  1071. * @param {boolean} typing is the user currently typing
  1072. * @param {number} timeout how long should the server preserve the typing state, in milliseconds
  1073. * @returns {Promise<any>} resolves when the typing state has been set
  1074. */
  1075. @timedMatrixClientFunctionCall()
  1076. public async setTyping(roomId: string, typing: boolean, timeout = 30000): Promise<any> {
  1077. const userId = await this.getUserId();
  1078. return this.doRequest("PUT", "/_matrix/client/v3/rooms/" + encodeURIComponent(roomId) + "/typing/" + encodeURIComponent(userId), null, {
  1079. typing,
  1080. timeout,
  1081. });
  1082. }
  1083. /**
  1084. * Replies to a given event with the given text. The event is sent with a msgtype of m.text.
  1085. * The message will be encrypted if the client supports encryption and the room is encrypted.
  1086. * @param {string} roomId the room ID to reply in
  1087. * @param {any} event the event to reply to
  1088. * @param {string} text the text to reply with
  1089. * @param {string} html the HTML to reply with, or falsey to use the `text`
  1090. * @returns {Promise<string>} resolves to the event ID which was sent
  1091. */
  1092. @timedMatrixClientFunctionCall()
  1093. public replyText(roomId: string, event: any, text: string, html: string = null): Promise<string> {
  1094. if (!html) html = htmlEncode(text);
  1095. const reply = RichReply.createFor(roomId, event, text, html);
  1096. return this.sendMessage(roomId, reply);
  1097. }
  1098. /**
  1099. * Replies to a given event with the given HTML. The event is sent with a msgtype of m.text.
  1100. * The message will be encrypted if the client supports encryption and the room is encrypted.
  1101. * @param {string} roomId the room ID to reply in
  1102. * @param {any} event the event to reply to
  1103. * @param {string} html the HTML to reply with.
  1104. * @returns {Promise<string>} resolves to the event ID which was sent
  1105. */
  1106. @timedMatrixClientFunctionCall()
  1107. public replyHtmlText(roomId: string, event: any, html: string): Promise<string> {
  1108. const text = htmlToText(html, { wordwrap: false });
  1109. const reply = RichReply.createFor(roomId, event, text, html);
  1110. return this.sendMessage(roomId, reply);
  1111. }
  1112. /**
  1113. * Replies to a given event with the given text. The event is sent with a msgtype of m.notice.
  1114. * The message will be encrypted if the client supports encryption and the room is encrypted.
  1115. * @param {string} roomId the room ID to reply in
  1116. * @param {any} event the event to reply to
  1117. * @param {string} text the text to reply with
  1118. * @param {string} html the HTML to reply with, or falsey to use the `text`
  1119. * @returns {Promise<string>} resolves to the event ID which was sent
  1120. */
  1121. @timedMatrixClientFunctionCall()
  1122. public replyNotice(roomId: string, event: any, text: string, html: string = null): Promise<string> {
  1123. if (!html) html = htmlEncode(text);
  1124. const reply = RichReply.createFor(roomId, event, text, html);
  1125. reply['msgtype'] = 'm.notice';
  1126. return this.sendMessage(roomId, reply);
  1127. }
  1128. /**
  1129. * Replies to a given event with the given HTML. The event is sent with a msgtype of m.notice.
  1130. * The message will be encrypted if the client supports encryption and the room is encrypted.
  1131. * @param {string} roomId the room ID to reply in
  1132. * @param {any} event the event to reply to
  1133. * @param {string} html the HTML to reply with.
  1134. * @returns {Promise<string>} resolves to the event ID which was sent
  1135. */
  1136. @timedMatrixClientFunctionCall()
  1137. public replyHtmlNotice(roomId: string, event: any, html: string): Promise<string> {
  1138. const text = htmlToText(html, { wordwrap: false });
  1139. const reply = RichReply.createFor(roomId, event, text, html);
  1140. reply['msgtype'] = 'm.notice';
  1141. return this.sendMessage(roomId, reply);
  1142. }
  1143. /**
  1144. * Sends a notice to the given room. The message will be encrypted if the client supports
  1145. * encryption and the room is encrypted.
  1146. * @param {string} roomId the room ID to send the notice to
  1147. * @param {string} text the text to send
  1148. * @returns {Promise<string>} resolves to the event ID that represents the message
  1149. */
  1150. @timedMatrixClientFunctionCall()
  1151. public sendNotice(roomId: string, text: string): Promise<string> {
  1152. return this.sendMessage(roomId, {
  1153. body: text,
  1154. msgtype: "m.notice",
  1155. });
  1156. }
  1157. /**
  1158. * Sends a notice to the given room with HTML content. The message will be encrypted if the client supports
  1159. * encryption and the room is encrypted.
  1160. * @param {string} roomId the room ID to send the notice to
  1161. * @param {string} html the HTML to send
  1162. * @returns {Promise<string>} resolves to the event ID that represents the message
  1163. */
  1164. @timedMatrixClientFunctionCall()
  1165. public sendHtmlNotice(roomId: string, html: string): Promise<string> {
  1166. return this.sendMessage(roomId, {
  1167. body: htmlToText(html, { wordwrap: false }),
  1168. msgtype: "m.notice",
  1169. format: "org.matrix.custom.html",
  1170. formatted_body: html,
  1171. });
  1172. }
  1173. /**
  1174. * Sends a text message to the given room. The message will be encrypted if the client supports
  1175. * encryption and the room is encrypted.
  1176. * @param {string} roomId the room ID to send the text to
  1177. * @param {string} text the text to send
  1178. * @returns {Promise<string>} resolves to the event ID that represents the message
  1179. */
  1180. @timedMatrixClientFunctionCall()
  1181. public sendText(roomId: string, text: string): Promise<string> {
  1182. return this.sendMessage(roomId, {
  1183. body: text,
  1184. msgtype: "m.text",
  1185. });
  1186. }
  1187. /**
  1188. * Sends a text message to the given room with HTML content. The message will be encrypted if the client supports
  1189. * encryption and the room is encrypted.
  1190. * @param {string} roomId the room ID to send the text to
  1191. * @param {string} html the HTML to send
  1192. * @returns {Promise<string>} resolves to the event ID that represents the message
  1193. */
  1194. @timedMatrixClientFunctionCall()
  1195. public sendHtmlText(roomId: string, html: string): Promise<string> {
  1196. return this.sendMessage(roomId, {
  1197. body: htmlToText(html, { wordwrap: false }),
  1198. msgtype: "m.text",
  1199. format: "org.matrix.custom.html",
  1200. formatted_body: html,
  1201. });
  1202. }
  1203. /**
  1204. * Sends a message to the given room. The message will be encrypted if the client supports
  1205. * encryption and the room is encrypted.
  1206. * @param {string} roomId the room ID to send the message to
  1207. * @param {object} content the event content to send
  1208. * @returns {Promise<string>} resolves to the event ID that represents the message
  1209. */
  1210. @timedMatrixClientFunctionCall()
  1211. public sendMessage(roomId: string, content: any): Promise<string> {
  1212. return this.sendEvent(roomId, "m.room.message", content);
  1213. }
  1214. /**
  1215. * Sends an event to the given room. This will encrypt the event before sending if the room is
  1216. * encrypted and the client supports encryption. Use sendRawEvent() to avoid this behaviour.
  1217. * @param {string} roomId the room ID to send the event to
  1218. * @param {string} eventType the type of event to send
  1219. * @param {string} content the event body to send
  1220. * @returns {Promise<string>} resolves to the event ID that represents the event
  1221. */
  1222. @timedMatrixClientFunctionCall()
  1223. public async sendEvent(roomId: string, eventType: string, content: any): Promise<string> {
  1224. if (await this.crypto?.isRoomEncrypted(roomId)) {
  1225. content = await this.crypto.encryptRoomEvent(roomId, eventType, content);
  1226. eventType = "m.room.encrypted";
  1227. }
  1228. return this.sendRawEvent(roomId, eventType, content);
  1229. }
  1230. /**
  1231. * Sends an event to the given room.
  1232. * @param {string} roomId the room ID to send the event to
  1233. * @param {string} eventType the type of event to send
  1234. * @param {string} content the event body to send
  1235. * @returns {Promise<string>} resolves to the event ID that represents the event
  1236. */
  1237. @timedMatrixClientFunctionCall()
  1238. public async sendRawEvent(roomId: string, eventType: string, content: any): Promise<string> {
  1239. const txnId = (new Date().getTime()) + "__inc" + (++this.requestId);
  1240. const path = "/_matrix/client/v3/rooms/"
  1241. + encodeURIComponent(roomId) + "/send/"
  1242. + encodeURIComponent(eventType) + "/"
  1243. + encodeURIComponent(txnId);
  1244. return this.doRequest("PUT", path, null, content).then(response => {
  1245. return response['event_id'];
  1246. });
  1247. }
  1248. /**
  1249. * Sends a state event to the given room
  1250. * @param {string} roomId the room ID to send the event to
  1251. * @param {string} type the event type to send
  1252. * @param {string} stateKey the state key to send, should not be null
  1253. * @param {string} content the event body to send
  1254. * @returns {Promise<string>} resolves to the event ID that represents the message
  1255. */
  1256. @timedMatrixClientFunctionCall()
  1257. public sendStateEvent(roomId: string, type: string, stateKey: string, content: any): Promise<string> {
  1258. const path = "/_matrix/client/v3/rooms/"
  1259. + encodeURIComponent(roomId) + "/state/"
  1260. + encodeURIComponent(type) + "/"
  1261. + encodeURIComponent(stateKey);
  1262. return this.doRequest("PUT", path, null, content).then(response => {
  1263. return response['event_id'];
  1264. });
  1265. }
  1266. /**
  1267. * Redact an event in a given room
  1268. * @param {string} roomId the room ID to send the redaction to
  1269. * @param {string} eventId the event ID to redact
  1270. * @param {String} reason an optional reason for redacting the event
  1271. * @returns {Promise<string>} resolves to the event ID that represents the redaction
  1272. */
  1273. @timedMatrixClientFunctionCall()
  1274. public redactEvent(roomId: string, eventId: string, reason: string | null = null): Promise<string> {
  1275. const txnId = (new Date().getTime()) + "__inc" + (++this.requestId);
  1276. const content = reason !== null ? { reason } : {};
  1277. return this.doRequest("PUT", `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`, null, content).then(response => {
  1278. return response['event_id'];
  1279. });
  1280. }
  1281. /**
  1282. * Creates a room. See the RoomCreateOptions interface
  1283. * for more information on what to provide for `properties`. Note that creating
  1284. * a room may cause the bot/appservice to raise a join event.
  1285. * @param {RoomCreateOptions} properties the properties of the room.
  1286. * @returns {Promise<string>} resolves to the room ID that represents the room
  1287. */
  1288. @timedMatrixClientFunctionCall()
  1289. public createRoom(properties: RoomCreateOptions = {}): Promise<string> {
  1290. return this.doRequest("POST", "/_matrix/client/v3/createRoom", null, properties).then(response => {
  1291. return response['room_id'];
  1292. });
  1293. }
  1294. /**
  1295. * Checks if a given user has a required power level required to send the given event.
  1296. * @param {string} userId the user ID to check the power level of
  1297. * @param {string} roomId the room ID to check the power level in
  1298. * @param {string} eventType the event type to look for in the `events` property of the power levels
  1299. * @param {boolean} isState true to indicate the event is intended to be a state event
  1300. * @returns {Promise<boolean>} resolves to true if the user has the required power level, resolves to false otherwise
  1301. */
  1302. @timedMatrixClientFunctionCall()
  1303. public async userHasPowerLevelFor(userId: string, roomId: string, eventType: string, isState: boolean): Promise<boolean> {
  1304. const powerLevelsEvent = await this.getRoomStateEvent(roomId, "m.room.power_levels", "");
  1305. if (!powerLevelsEvent) {
  1306. // This is technically supposed to be non-fatal, but it's pretty unreasonable for a room to be missing
  1307. // power levels.
  1308. throw new Error("No power level event found");
  1309. }
  1310. let requiredPower = isState ? 50 : 0;
  1311. if (isState && Number.isFinite(powerLevelsEvent["state_default"])) requiredPower = powerLevelsEvent["state_default"];
  1312. if (!isState && Number.isFinite(powerLevelsEvent["events_default"])) requiredPower = powerLevelsEvent["events_default"];
  1313. if (Number.isFinite(powerLevelsEvent["events"]?.[eventType])) requiredPower = powerLevelsEvent["events"][eventType];
  1314. let userPower = 0;
  1315. if (Number.isFinite(powerLevelsEvent["users_default"])) userPower = powerLevelsEvent["users_default"];
  1316. if (Number.isFinite(powerLevelsEvent["users"]?.[userId])) userPower = powerLevelsEvent["users"][userId];
  1317. return userPower >= requiredPower;
  1318. }
  1319. /**
  1320. * Checks if a given user has a required power level to perform the given action
  1321. * @param {string} userId the user ID to check the power level of
  1322. * @param {string} roomId the room ID to check the power level in
  1323. * @param {PowerLevelAction} action the action to check power level for
  1324. * @returns {Promise<boolean>} resolves to true if the user has the required power level, resolves to false otherwise
  1325. */
  1326. @timedMatrixClientFunctionCall()
  1327. public async userHasPowerLevelForAction(userId: string, roomId: string, action: PowerLevelAction): Promise<boolean> {
  1328. const powerLevelsEvent = await this.getRoomStateEvent(roomId, "m.room.power_levels", "");
  1329. if (!powerLevelsEvent) {
  1330. // This is technically supposed to be non-fatal, but it's pretty unreasonable for a room to be missing
  1331. // power levels.
  1332. throw new Error("No power level event found");
  1333. }
  1334. const defaultForActions: { [A in PowerLevelAction]: number } = {
  1335. [PowerLevelAction.Ban]: 50,
  1336. [PowerLevelAction.Invite]: 50,
  1337. [PowerLevelAction.Kick]: 50,
  1338. [PowerLevelAction.RedactEvents]: 50,
  1339. [PowerLevelAction.NotifyRoom]: 50,
  1340. };
  1341. let requiredPower = defaultForActions[action];
  1342. let investigate = powerLevelsEvent;
  1343. action.split('.').forEach(k => (investigate = investigate?.[k]));
  1344. if (Number.isFinite(investigate)) requiredPower = investigate;
  1345. let userPower = 0;
  1346. if (Number.isFinite(powerLevelsEvent["users_default"])) userPower = powerLevelsEvent["users_default"];
  1347. if (Number.isFinite(powerLevelsEvent["users"]?.[userId])) userPower = powerLevelsEvent["users"][userId];
  1348. return userPower >= requiredPower;
  1349. }
  1350. /**
  1351. * Determines the boundary conditions for this client's ability to change another user's power level
  1352. * in a given room. This will identify the maximum possible level this client can change the user to,
  1353. * and if that change could even be possible. If the returned object indicates that the client can
  1354. * change the power level of the user, the client is able to set the power level to any value equal
  1355. * to or less than the maximum value.
  1356. * @param {string} targetUserId The user ID to compare against.
  1357. * @param {string} roomId The room ID to compare within.
  1358. * @returns {Promise<PowerLevelBounds>} The bounds of the client's ability to change the user's power level.
  1359. */
  1360. @timedMatrixClientFunctionCall()
  1361. public async calculatePowerLevelChangeBoundsOn(targetUserId: string, roomId: string): Promise<PowerLevelBounds> {
  1362. const myUserId = await this.getUserId();
  1363. const canChangePower = await this.userHasPowerLevelFor(myUserId, roomId, "m.room.power_levels", true);
  1364. if (!canChangePower) return { canModify: false, maximumPossibleLevel: 0 };
  1365. const powerLevelsEvent = await this.getRoomStateEvent(roomId, "m.room.power_levels", "");
  1366. if (!powerLevelsEvent) {
  1367. throw new Error("No power level event found");
  1368. }
  1369. let targetUserPower = 0;
  1370. let myUserPower = 0;
  1371. if (powerLevelsEvent["users"] && powerLevelsEvent["users"][targetUserId]) targetUserPower = powerLevelsEvent["users"][targetUserId];
  1372. if (powerLevelsEvent["users"] && powerLevelsEvent["users"][myUserId]) myUserPower = powerLevelsEvent["users"][myUserId];
  1373. if (myUserId === targetUserId) {
  1374. return { canModify: true, maximumPossibleLevel: myUserPower };
  1375. }
  1376. if (targetUserPower >= myUserPower) {
  1377. return { canModify: false, maximumPossibleLevel: myUserPower };
  1378. }
  1379. return { canModify: true, maximumPossibleLevel: myUserPower };
  1380. }
  1381. /**
  1382. * Sets the power level for a given user ID in the given room. Note that this is not safe to
  1383. * call multiple times concurrently as changes are not atomic. This will throw an error if
  1384. * the user lacks enough permission to change the power level, or if a power level event is
  1385. * missing from the room.
  1386. * @param {string} userId The user ID to change
  1387. * @param {string} roomId The room ID to change the power level in
  1388. * @param {number} newLevel The integer power level to set the user to.
  1389. * @returns {Promise<any>} Resolves when complete.
  1390. */
  1391. @timedMatrixClientFunctionCall()
  1392. public async setUserPowerLevel(userId: string, roomId: string, newLevel: number): Promise<any> {
  1393. const currentLevels = await this.getRoomStateEvent(roomId, "m.room.power_levels", "");
  1394. if (!currentLevels['users']) currentLevels['users'] = {};
  1395. currentLevels['users'][userId] = newLevel;
  1396. await this.sendStateEvent(roomId, "m.room.power_levels", "", currentLevels);
  1397. }
  1398. /**
  1399. * Converts a MXC URI to an HTTP URL.
  1400. * @param {string} mxc The MXC URI to convert
  1401. * @returns {string} The HTTP URL for the content.
  1402. */
  1403. public mxcToHttp(mxc: string): string {
  1404. if (!mxc.startsWith("mxc://")) throw new Error("Not a MXC URI");
  1405. const parts = mxc.substring("mxc://".length).split('/');
  1406. const originHomeserver = parts[0];
  1407. const mediaId = parts.slice(1, parts.length).join('/');
  1408. return `${this.homeserverUrl}/_matrix/media/v3/download/${encodeURIComponent(originHomeserver)}/${encodeURIComponent(mediaId)}`;
  1409. }
  1410. /**
  1411. * Converts a MXC URI to an HTTP URL for downsizing the content.
  1412. * @param {string} mxc The MXC URI to convert and downsize.
  1413. * @param {number} width The width, as an integer, for the thumbnail.
  1414. * @param {number} height The height, as an intenger, for the thumbnail.
  1415. * @param {"crop"|"scale"} method Whether to crop or scale (preserve aspect ratio) the content.
  1416. * @returns {string} The HTTP URL for the downsized content.
  1417. */
  1418. public mxcToHttpThumbnail(mxc: string, width: number, height: number, method: "crop" | "scale"): string {
  1419. const downloadUri = this.mxcToHttp(mxc);
  1420. return downloadUri.replace("/_matrix/media/v3/download", "/_matrix/media/v3/thumbnail")
  1421. + `?width=${width}&height=${height}&method=${encodeURIComponent(method)}`;
  1422. }
  1423. /**
  1424. * Uploads data to the homeserver's media repository. Note that this will <b>not</b> automatically encrypt
  1425. * media as it cannot determine if the media should be encrypted.
  1426. * @param {Buffer} data the content to upload.
  1427. * @param {string} contentType the content type of the file. Defaults to application/octet-stream
  1428. * @param {string} filename the name of the file. Optional.
  1429. * @returns {Promise<string>} resolves to the MXC URI of the content
  1430. */
  1431. @timedMatrixClientFunctionCall()
  1432. public uploadContent(data: Buffer, contentType = "application/octet-stream", filename: string = null): Promise<string> {
  1433. // TODO: Make doRequest take an object for options
  1434. return this.doRequest("POST", "/_matrix/media/v3/upload", { filename: filename }, data, 60000, false, contentType)
  1435. .then(response => response["content_uri"]);
  1436. }
  1437. /**
  1438. * Download content from the homeserver's media repository. Note that this will <b>not</b> automatically decrypt
  1439. * media as it cannot determine if the media is encrypted.
  1440. * @param {string} mxcUrl The MXC URI for the content.
  1441. * @param {string} allowRemote Indicates to the server that it should not attempt to fetch the
  1442. * media if it is deemed remote. This is to prevent routing loops where the server contacts itself.
  1443. * Defaults to true if not provided.
  1444. * @returns {Promise<{data: Buffer, contentType: string}>} Resolves to the downloaded content.
  1445. */
  1446. public async downloadContent(mxcUrl: string, allowRemote = true): Promise<{ data: Buffer, contentType: string }> {
  1447. if (!mxcUrl.toLowerCase().startsWith("mxc://")) {
  1448. throw Error("'mxcUrl' does not begin with mxc://");
  1449. }
  1450. const urlParts = mxcUrl.substr("mxc://".length).split("/");
  1451. const domain = encodeURIComponent(urlParts[0]);
  1452. const mediaId = encodeURIComponent(urlParts[1].split("/")[0]);
  1453. const path = `/_matrix/media/v3/download/${domain}/${mediaId}`;
  1454. const res = await this.doRequest("GET", path, { allow_remote: allowRemote }, null, null, true, null, true);
  1455. return {
  1456. data: res.body,
  1457. contentType: res.headers["content-type"],
  1458. };
  1459. }
  1460. /**
  1461. * Uploads data to the homeserver's media repository after downloading it from the
  1462. * provided URL.
  1463. * @param {string} url The URL to download content from.
  1464. * @returns {Promise<string>} Resolves to the MXC URI of the content
  1465. */
  1466. @timedMatrixClientFunctionCall()
  1467. public uploadContentFromUrl(url: string): Promise<string> {
  1468. return new Promise<{ body: Buffer, contentType: string }>((resolve, reject) => {
  1469. const requestId = ++this.requestId;
  1470. const params = {
  1471. uri: url,
  1472. method: "GET",
  1473. encoding: null,
  1474. };
  1475. getRequestFn()(params, (err, response, resBody) => {
  1476. if (err) {
  1477. LogService.error("MatrixClientLite", "(REQ-" + requestId + ")", extractRequestError(err));
  1478. reject(err);
  1479. } else {
  1480. const contentType = response.headers['content-type'] || "application/octet-stream";
  1481. LogService.trace("MatrixClientLite", "(REQ-" + requestId + " RESP-H" + response.statusCode + ")", "<data>");
  1482. if (response.statusCode < 200 || response.statusCode >= 300) {
  1483. LogService.error("MatrixClientLite", "(REQ-" + requestId + ")", "<data>");
  1484. reject(response);
  1485. } else resolve({ body: resBody, contentType: contentType });
  1486. }
  1487. });
  1488. }).then(obj => {
  1489. return this.uploadContent(obj.body, obj.contentType);
  1490. });
  1491. }
  1492. /**
  1493. * Determines the upgrade history for a given room as a doubly-linked list styled structure. Given
  1494. * a room ID in the history of upgrades, the resulting `previous` array will hold any rooms which
  1495. * are older than the given room. The resulting `newer` array will hold any rooms which are newer
  1496. * versions of the room. Both arrays will be defined, but may be empty individually. Element zero
  1497. * of each will always be the nearest to the given room ID and the last element will be the furthest
  1498. * from the room. The given room will never be in either array.
  1499. * @param {string} roomId the room ID to get the history of
  1500. * @returns {Promise<{previous: RoomReference[], newer: RoomReference[]}>} Resolves to the room's
  1501. * upgrade history
  1502. */
  1503. @timedMatrixClientFunctionCall()
  1504. public async getRoomUpgradeHistory(roomId: string): Promise<{ previous: RoomReference[], newer: RoomReference[], current: RoomReference }> {
  1505. const result = { previous: [], newer: [], current: null };
  1506. const chaseCreates = async (findRoomId) => {
  1507. try {
  1508. const createEvent = await this.getRoomStateEvent(findRoomId, "m.room.create", "");
  1509. if (!createEvent) return;
  1510. if (findRoomId === roomId && !result.current) {
  1511. const version = createEvent['room_version'] || '1';
  1512. result.current = {
  1513. roomId: roomId,
  1514. version: version,
  1515. refEventId: null,
  1516. };
  1517. }
  1518. if (createEvent['predecessor'] && createEvent['predecessor']['room_id']) {
  1519. const prevRoomId = createEvent['predecessor']['room_id'];
  1520. if (prevRoomId === findRoomId) return; // Recursion is bad
  1521. if (result.previous.find(r => r.roomId === prevRoomId)) return; // Already found
  1522. let tombstoneEventId = null;
  1523. let prevVersion = "1";
  1524. try {
  1525. const roomState = await this.getRoomState(prevRoomId);
  1526. const tombstone = roomState.find(e => e['type'] === 'm.room.tombstone' && e['state_key'] === '');
  1527. const create = roomState.find(e => e['type'] === 'm.room.create' && e['state_key'] === '');
  1528. if (tombstone) {
  1529. if (!tombstone['content']) tombstone['content'] = {};
  1530. const tombstoneRefRoomId = tombstone['content']['replacement_room'];
  1531. if (tombstoneRefRoomId === findRoomId) tombstoneEventId = tombstone['event_id'];
  1532. }
  1533. if (create) {
  1534. if (!create['content']) create['content'] = {};
  1535. prevVersion = create['content']['room_version'] || "1";
  1536. }
  1537. } catch (e) {
  1538. // state not available
  1539. }
  1540. result.previous.push({
  1541. roomId: prevRoomId,
  1542. version: prevVersion,
  1543. refEventId: tombstoneEventId,
  1544. });
  1545. return chaseCreates(prevRoomId);
  1546. }
  1547. } catch (e) {
  1548. // no create event - that's fine
  1549. }
  1550. };
  1551. const chaseTombstones = async (findRoomId) => {
  1552. try {
  1553. const tombstoneEvent = await this.getRoomStateEvent(findRoomId, "m.room.tombstone", "");
  1554. if (!tombstoneEvent) return;
  1555. if (!tombstoneEvent['replacement_room']) return;
  1556. const newRoomId = tombstoneEvent['replacement_room'];
  1557. if (newRoomId === findRoomId) return; // Recursion is bad
  1558. if (result.newer.find(r => r.roomId === newRoomId)) return; // Already found
  1559. let newRoomVersion = "1";
  1560. let createEventId = null;
  1561. try {
  1562. const roomState = await this.getRoomState(newRoomId);
  1563. const create = roomState.find(e => e['type'] === 'm.room.create' && e['state_key'] === '');
  1564. if (create) {
  1565. if (!create['content']) create['content'] = {};
  1566. const predecessor = create['content']['predecessor'] || {};
  1567. const refPrevRoomId = predecessor['room_id'];
  1568. if (refPrevRoomId === findRoomId) {
  1569. createEventId = create['event_id'];
  1570. }
  1571. newRoomVersion = create['content']['room_version'] || "1";
  1572. }
  1573. } catch (e) {
  1574. // state not available
  1575. }
  1576. result.newer.push({
  1577. roomId: newRoomId,
  1578. version: newRoomVersion,
  1579. refEventId: createEventId,
  1580. });
  1581. return await chaseTombstones(newRoomId);
  1582. } catch (e) {
  1583. // no tombstone - that's fine
  1584. }
  1585. };
  1586. await chaseCreates(roomId);
  1587. await chaseTombstones(roomId);
  1588. return result;
  1589. }
  1590. /**
  1591. * Creates a Space room.
  1592. * @param {SpaceCreateOptions} opts The creation options.
  1593. * @returns {Promise<Space>} Resolves to the created space.
  1594. */
  1595. @timedMatrixClientFunctionCall()
  1596. public async createSpace(opts: SpaceCreateOptions): Promise<Space> {
  1597. const roomCreateOpts: RoomCreateOptions = {
  1598. name: opts.name,
  1599. topic: opts.topic || "",
  1600. preset: opts.isPublic ? "public_chat" : "private_chat",
  1601. room_alias_name: opts.localpart,
  1602. initial_state: [
  1603. {
  1604. type: "m.room.history_visibility",
  1605. state_key: "",
  1606. content: {
  1607. history_visibility: opts.isPublic ? 'world_readable' : 'shared',
  1608. },
  1609. },
  1610. ],
  1611. creation_content: {
  1612. type: "m.space",
  1613. },
  1614. invite: opts.invites || [],
  1615. power_level_content_override: {
  1616. ban: 100,
  1617. events_default: 50,
  1618. invite: 50,
  1619. kick: 100,
  1620. notifications: {
  1621. room: 100,
  1622. },
  1623. redact: 100,
  1624. state_default: 100,
  1625. users: {
  1626. [await this.getUserId()]: 100,
  1627. },
  1628. users_default: 0,
  1629. },
  1630. };
  1631. if (opts.avatarUrl) {
  1632. roomCreateOpts.initial_state.push({
  1633. type: 'm.room.avatar',
  1634. state_key: "",
  1635. content: {
  1636. url: opts.avatarUrl,
  1637. },
  1638. });
  1639. }
  1640. const roomId = await this.createRoom(roomCreateOpts);
  1641. return new Space(roomId, this);
  1642. }
  1643. /**
  1644. * Gets a Space.
  1645. * This API does not work with unstable spaces (e.g. org.matrix.msc.1772.space)
  1646. *
  1647. * @throws If the room is not a space or there was an error
  1648. * @returns {Promise<Space>} Resolves to the space.
  1649. */
  1650. @timedMatrixClientFunctionCall()
  1651. public async getSpace(roomIdOrAlias: string): Promise<Space> {
  1652. const roomId = await this.resolveRoom(roomIdOrAlias);
  1653. const createEvent = await this.getRoomStateEvent(roomId, "m.room.create", "");
  1654. if (createEvent["type"] !== "m.space") {
  1655. throw new Error("Room is not a space");
  1656. }
  1657. return new Space(roomId, this);
  1658. }
  1659. /**
  1660. * Uploads One Time Keys for the current device.
  1661. * @param {OTKs} keys The keys to upload.
  1662. * @returns {Promise<OTKCounts>} Resolves to the current One Time Key counts when complete.
  1663. */
  1664. @timedMatrixClientFunctionCall()
  1665. @requiresCrypto()
  1666. public async uploadDeviceOneTimeKeys(keys: OTKs): Promise<OTKCounts> {
  1667. return this.doRequest("POST", "/_matrix/client/v3/keys/upload", null, {
  1668. one_time_keys: keys,
  1669. }).then(r => r['one_time_key_counts']);
  1670. }
  1671. /**
  1672. * Gets the current One Time Key counts.
  1673. * @returns {Promise<OTKCounts>} Resolves to the One Time Key counts.
  1674. */
  1675. @timedMatrixClientFunctionCall()
  1676. @requiresCrypto()
  1677. public async checkOneTimeKeyCounts(): Promise<OTKCounts> {
  1678. return this.doRequest("POST", "/_matrix/client/v3/keys/upload", null, {})
  1679. .then(r => r['one_time_key_counts']);
  1680. }
  1681. /**
  1682. * Uploads a fallback One Time Key to the server for usage. This will replace the existing fallback
  1683. * key.
  1684. * @param {FallbackKey} fallbackKey The fallback key.
  1685. * @returns {Promise<OTKCounts>} Resolves to the One Time Key counts.
  1686. */
  1687. @timedMatrixClientFunctionCall()
  1688. @requiresCrypto()
  1689. public async uploadFallbackKey(fallbackKey: FallbackKey): Promise<OTKCounts> {
  1690. const keyObj = {
  1691. [`${OTKAlgorithm.Signed}:${fallbackKey.keyId}`]: fallbackKey.key,
  1692. };
  1693. return this.doRequest("POST", "/_matrix/client/v3/keys/upload", null, {
  1694. "org.matrix.msc2732.fallback_keys": keyObj,
  1695. "fallback_keys": keyObj,
  1696. }).then(r => r['one_time_key_counts']);
  1697. }
  1698. /**
  1699. * Gets <b>unverified</b> device lists for the given users. The caller is expected to validate
  1700. * and verify the device lists, including that the returned devices belong to the claimed users.
  1701. *
  1702. * Failures with federation are reported in the returned object. Users which did not fail a federation
  1703. * lookup but have no devices will not appear in either the failures or in the returned devices.
  1704. *
  1705. * See https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-keys-query for more
  1706. * information.
  1707. * @param {string[]} userIds The user IDs to query.
  1708. * @param {number} federationTimeoutMs The default timeout for requesting devices over federation. Defaults to
  1709. * 10 seconds.
  1710. * @returns {Promise<MultiUserDeviceListResponse>} Resolves to the device list/errors for the requested user IDs.
  1711. */
  1712. @timedMatrixClientFunctionCall()
  1713. public async getUserDevices(userIds: string[], federationTimeoutMs = 10000): Promise<MultiUserDeviceListResponse> {
  1714. const req = {};
  1715. for (const userId of userIds) {
  1716. req[userId] = [];
  1717. }
  1718. return this.doRequest("POST", "/_matrix/client/v3/keys/query", {}, {
  1719. timeout: federationTimeoutMs,
  1720. device_keys: req,
  1721. });
  1722. }
  1723. /**
  1724. * Gets a device list for the client's own account, with metadata. The devices are not verified
  1725. * in this response, but should be active on the account.
  1726. * @returns {Promise<OwnUserDevice[]>} Resolves to the active devices on the account.
  1727. */
  1728. @timedMatrixClientFunctionCall()
  1729. public async getOwnDevices(): Promise<OwnUserDevice[]> {
  1730. return this.doRequest("GET", "/_matrix/client/v3/devices").then(r => {
  1731. return r['devices'];
  1732. });
  1733. }
  1734. /**
  1735. * Claims One Time Keys for a set of user devices, returning those keys. The caller is expected to verify
  1736. * and validate the returned keys.
  1737. *
  1738. * Failures with federation are reported in the returned object.
  1739. * @param {Record<string, Record<string, OTKAlgorithm>>} userDeviceMap The map of user IDs to device IDs to
  1740. * OTKAlgorithm to request a claim for.
  1741. * @param {number} federationTimeoutMs The default timeout for claiming keys over federation. Defaults to
  1742. * 10 seconds.
  1743. */
  1744. @timedMatrixClientFunctionCall()
  1745. @requiresCrypto()
  1746. public async claimOneTimeKeys(userDeviceMap: Record<string, Record<string, OTKAlgorithm>>, federationTimeoutMs = 10000): Promise<OTKClaimResponse> {
  1747. return this.doRequest("POST", "/_matrix/client/v3/keys/claim", {}, {
  1748. timeout: federationTimeoutMs,
  1749. one_time_keys: userDeviceMap,
  1750. });
  1751. }
  1752. /**
  1753. * Sends to-device messages to the respective users/devices.
  1754. * @param {string} type The message type being sent.
  1755. * @param {Record<string, Record<string, any>>} messages The messages to send, mapped as user ID to
  1756. * device ID (or "*" to denote all of the user's devices) to message payload (content).
  1757. * @returns {Promise<void>} Resolves when complete.
  1758. */
  1759. @timedMatrixClientFunctionCall()
  1760. public async sendToDevices(type: string, messages: Record<string, Record<string, any>>): Promise<void> {
  1761. const txnId = (new Date().getTime()) + "_TDEV__inc" + (++this.requestId);
  1762. return this.doRequest("PUT", `/_matrix/client/v3/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, null, {
  1763. messages: messages,
  1764. });
  1765. }
  1766. /**
  1767. * Get relations for a given event.
  1768. * @param {string} roomId The room ID to for the given event.
  1769. * @param {string} eventId The event ID to list relations for.
  1770. * @param {string?} relationType The type of relations (e.g. `m.room.member`) to filter for. Optional.
  1771. * @param {string?} eventType The type of event to look for (e.g. `m.room.member`). Optional.
  1772. * @returns {Promise<{chunk: any[]}>} Resolves to an object containing the chunk of relations
  1773. */
  1774. @timedMatrixClientFunctionCall()
  1775. public async getRelationsForEvent(roomId: string, eventId: string, relationType?: string, eventType?: string): Promise<{ chunk: any[] }> {
  1776. let url = `/_matrix/client/v1/rooms/${encodeURIComponent(roomId)}/relations/${encodeURIComponent(eventId)}`;
  1777. if (relationType) {
  1778. url += `/${relationType}`;
  1779. }
  1780. if (eventType) {
  1781. url += `/${eventType}`;
  1782. }
  1783. return this.doRequest("GET", url);
  1784. }
  1785. /**
  1786. * Performs a web request to the homeserver, applying appropriate authorization headers for
  1787. * this client.
  1788. * @param {"GET"|"POST"|"PUT"|"DELETE"} method The HTTP method to use in the request
  1789. * @param {string} endpoint The endpoint to call. For example: "/_matrix/client/v3/account/whoami"
  1790. * @param {any} qs The query string to send. Optional.
  1791. * @param {any} body The request body to send. Optional. Will be converted to JSON unless the type is a Buffer.
  1792. * @param {number} timeout The number of milliseconds to wait before timing out.
  1793. * @param {boolean} raw If true, the raw response will be returned instead of the response body.
  1794. * @param {string} contentType The content type to send. Only used if the `body` is a Buffer.
  1795. * @param {string} noEncoding Set to true to disable encoding, and return a Buffer. Defaults to false
  1796. * @returns {Promise<any>} Resolves to the response (body), rejected if a non-2xx status code was returned.
  1797. */
  1798. @timedMatrixClientFunctionCall()
  1799. public doRequest(method, endpoint, qs = null, body = null, timeout = 60000, raw = false, contentType = "application/json", noEncoding = false): Promise<any> {
  1800. if (this.impersonatedUserId) {
  1801. if (!qs) qs = { "user_id": this.impersonatedUserId };
  1802. else qs["user_id"] = this.impersonatedUserId;
  1803. }
  1804. if (this.impersonatedDeviceId) {
  1805. if (!qs) qs = { "org.matrix.msc3202.device_id": this.impersonatedDeviceId };
  1806. else qs["org.matrix.msc3202.device_id"] = this.impersonatedDeviceId;
  1807. }
  1808. const headers = {};
  1809. if (this.accessToken) {
  1810. headers["Authorization"] = `Bearer ${this.accessToken}`;
  1811. }
  1812. return doHttpRequest(this.homeserverUrl, method, endpoint, qs, body, headers, timeout, raw, contentType, noEncoding);
  1813. }
  1814. }
  1815. export interface RoomDirectoryLookupResponse {
  1816. roomId: string;
  1817. residentServers: string[];
  1818. }
  1819. export interface RoomReference {
  1820. /**
  1821. * The room ID being referenced
  1822. */
  1823. roomId: string;
  1824. /**
  1825. * The version of the room at the time
  1826. */
  1827. version: string;
  1828. /**
  1829. * If going backwards, the tombstone event ID, otherwise the creation
  1830. * event. If the room can't be verified, this will be null. Will be
  1831. * null if this reference is to the current room.
  1832. */
  1833. refEventId: string;
  1834. }