Source: components/intent.js

  1. "use strict";
  2. const Promise = require("bluebird");
  3. const MatrixUser = require("../models/users/matrix");
  4. const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
  5. const RoomMember = require("matrix-js-sdk").RoomMember;
  6. const ClientRequestCache = require("./client-request-cache");
  7. const STATE_EVENT_TYPES = [
  8. "m.room.name", "m.room.topic", "m.room.power_levels", "m.room.member",
  9. "m.room.join_rules", "m.room.history_visibility"
  10. ];
  11. const DEFAULT_CACHE_TTL = 90000;
  12. const DEFAULT_CACHE_SIZE = 1024;
  13. /**
  14. * Create an entity which can fulfil the intent of a given user.
  15. * @constructor
  16. * @param {MatrixClient} client The matrix client instance whose intent is being
  17. * fulfilled e.g. the entity joining the room when you call intent.join(roomId).
  18. * @param {MatrixClient} botClient The client instance for the AS bot itself.
  19. * This will be used to perform more priveleged actions such as creating new
  20. * rooms, sending invites, etc.
  21. * @param {Object} opts Options for this Intent instance.
  22. * @param {boolean} opts.registered True to inform this instance that the client
  23. * is already registered. No registration requests will be made from this Intent.
  24. * Default: false.
  25. * @param {boolean} opts.dontCheckPowerLevel True to not check for the right power
  26. * level before sending events. Default: false.
  27. *
  28. * @param {Object=} opts.backingStore An object with 4 functions, outlined below.
  29. * If this Object is supplied, ALL 4 functions must be supplied. If this Object
  30. * is not supplied, the Intent will maintain its own backing store for membership
  31. * and power levels, which may scale badly for lots of users.
  32. *
  33. * @param {Function} opts.backingStore.getMembership A function which is called with a
  34. * room ID and user ID which should return the membership status of this user as
  35. * a string e.g "join". `null` should be returned if the membership is unknown.
  36. *
  37. * @param {Function} opts.backingStore.getPowerLevelContent A function which is called
  38. * with a room ID which should return the power level content for this room, as an Object.
  39. * `null` should be returned if there is no known content.
  40. *
  41. * @param {Function} opts.backingStore.setMembership A function with the signature:
  42. * function(roomId, userId, membership) which will set the membership of the given user in
  43. * the given room. This has no return value.
  44. *
  45. * @param {Function} opts.backingStore.setPowerLevelContent A function with the signature:
  46. * function(roomId, content) which will set the power level content in the given room.
  47. * This has no return value.
  48. *
  49. * @param {boolean} opts.dontJoin True to not attempt to join a room before
  50. * sending messages into it. The surrounding code will have to ensure the correct
  51. * membership state itself in this case. Default: false.
  52. *
  53. * @param {boolean} [opts.enablePresence=true] True to send presence, false to no-op.
  54. *
  55. * @param {Number} opts.caching.ttl How long requests can stay in the cache, in milliseconds.
  56. * @param {Number} opts.caching.size How many entries should be kept in the cache, before the oldest is dropped.
  57. */
  58. function Intent(client, botClient, opts) {
  59. this.client = client;
  60. this.botClient = botClient;
  61. opts = opts || {};
  62. opts.enablePresence = opts.enablePresence !== false;
  63. if (opts.backingStore) {
  64. if (!opts.backingStore.setPowerLevelContent ||
  65. !opts.backingStore.getPowerLevelContent ||
  66. !opts.backingStore.setMembership ||
  67. !opts.backingStore.getMembership) {
  68. throw new Error("Intent backingStore missing required functions");
  69. }
  70. }
  71. else {
  72. this._membershipStates = {
  73. // room_id : "join|invite|leave|null" null=unknown
  74. };
  75. this._powerLevels = {
  76. // room_id: event.content
  77. };
  78. var self = this;
  79. opts.backingStore = {
  80. getMembership: function(roomId, userId) {
  81. if (userId !== self.client.credentials.userId) {
  82. return null;
  83. }
  84. return self._membershipStates[roomId];
  85. },
  86. setMembership: function(roomId, userId, membership) {
  87. if (userId !== self.client.credentials.userId) {
  88. return;
  89. }
  90. self._membershipStates[roomId] = membership;
  91. },
  92. setPowerLevelContent: function(roomId, content) {
  93. self._powerLevels[roomId] = content;
  94. },
  95. getPowerLevelContent: function(roomId) {
  96. return self._powerLevels[roomId];
  97. }
  98. }
  99. }
  100. if (!opts.caching) {
  101. opts.caching = { };
  102. }
  103. opts.caching.ttl = opts.caching.ttl === undefined ? DEFAULT_CACHE_TTL : opts.caching.ttl;
  104. opts.caching.size = opts.caching.size === undefined ? DEFAULT_CACHE_SIZE : opts.caching.ttl;
  105. this._requestCaches = {};
  106. this._requestCaches.profile = new ClientRequestCache(
  107. opts.caching.ttl,
  108. opts.caching.size,
  109. (_, userId, info) => {
  110. return this.getProfileInfo(userId, info, false);
  111. }
  112. );
  113. this._requestCaches.roomstate = new ClientRequestCache(
  114. opts.caching.ttl,
  115. opts.caching.size,
  116. (roomId) => {
  117. return this.roomState(roomId, false);
  118. }
  119. );
  120. this._requestCaches.event = new ClientRequestCache(
  121. opts.caching.ttl,
  122. opts.caching.size,
  123. (_, roomId, eventId) => {
  124. return this.getEvent(roomId, eventId, false);
  125. }
  126. );
  127. this.opts = opts;
  128. }
  129. /**
  130. * Return the client this Intent is acting on behalf of.
  131. * @return {MatrixClient} The client
  132. */
  133. Intent.prototype.getClient = function() {
  134. return this.client;
  135. };
  136. /**
  137. * <p>Send a plaintext message to a room.</p>
  138. * This will automatically make the client join the room so they can send the
  139. * message if they are not already joined. It will also make sure that the client
  140. * has sufficient power level to do this.
  141. * @param {string} roomId The room to send to.
  142. * @param {string} text The text string to send.
  143. * @return {Promise}
  144. */
  145. Intent.prototype.sendText = function(roomId, text) {
  146. return this.sendMessage(roomId, {
  147. body: text,
  148. msgtype: "m.text"
  149. });
  150. };
  151. /**
  152. * <p>Set the name of a room.</p>
  153. * This will automatically make the client join the room so they can set the
  154. * name if they are not already joined. It will also make sure that the client
  155. * has sufficient power level to do this.
  156. * @param {string} roomId The room to send to.
  157. * @param {string} name The room name.
  158. * @return {Promise}
  159. */
  160. Intent.prototype.setRoomName = function(roomId, name) {
  161. return this.sendStateEvent(roomId, "m.room.name", "", {
  162. name: name
  163. });
  164. };
  165. /**
  166. * <p>Set the topic of a room.</p>
  167. * This will automatically make the client join the room so they can set the
  168. * topic if they are not already joined. It will also make sure that the client
  169. * has sufficient power level to do this.
  170. * @param {string} roomId The room to send to.
  171. * @param {string} topic The room topic.
  172. * @return {Promise}
  173. */
  174. Intent.prototype.setRoomTopic = function(roomId, topic) {
  175. return this.sendStateEvent(roomId, "m.room.topic", "", {
  176. topic: topic
  177. });
  178. };
  179. /**
  180. * <p>Set the avatar of a room.</p>
  181. * This will automatically make the client join the room so they can set the
  182. * topic if they are not already joined. It will also make sure that the client
  183. * has sufficient power level to do this.
  184. * @param {string} roomId The room to send to.
  185. * @param {string} avatar The url of the avatar.
  186. * @param {string} info Extra information about the image. See m.room.avatar for details.
  187. * @return {Promise}
  188. */
  189. Intent.prototype.setRoomAvatar = function(roomId, avatar, info) {
  190. var content = {
  191. url: avatar
  192. };
  193. if (info) {
  194. content.info = info;
  195. }
  196. return this.sendStateEvent(roomId, "m.room.avatar", "", content);
  197. };
  198. /**
  199. * <p>Send a typing event to a room.</p>
  200. * This will automatically make the client join the room so they can send the
  201. * typing event if they are not already joined.
  202. * @param {string} roomId The room to send to.
  203. * @param {boolean} isTyping True if typing
  204. * @return {Promise}
  205. */
  206. Intent.prototype.sendTyping = function(roomId, isTyping) {
  207. var self = this;
  208. return self._ensureJoined(roomId).then(function() {
  209. return self._ensureHasPowerLevelFor(roomId, "m.typing");
  210. }).then(function() {
  211. return self.client.sendTyping(roomId, isTyping);
  212. });
  213. };
  214. /**
  215. * <p>Send a read receipt to a room.</p>
  216. * This will automatically make the client join the room so they can send the
  217. * receipt event if they are not already joined.
  218. * @param{string} roomId The room to send to.
  219. * @param{string} eventId The event ID to set the receipt mark to.
  220. * @return {Promise}
  221. */
  222. Intent.prototype.sendReadReceipt = function(roomId, eventId) {
  223. var self = this;
  224. var event = new MatrixEvent({
  225. room_id: roomId,
  226. event_id: eventId,
  227. });
  228. return self._ensureJoined(roomId).then(function() {
  229. return self.client.sendReadReceipt(event);
  230. });
  231. }
  232. /**
  233. * Set the power level of the given target.
  234. * @param {string} roomId The room to set the power level in.
  235. * @param {string} target The target user ID
  236. * @param {number} level The desired level
  237. * @return {Promise}
  238. */
  239. Intent.prototype.setPowerLevel = function(roomId, target, level) {
  240. var self = this;
  241. return self._ensureJoined(roomId).then(function() {
  242. return self._ensureHasPowerLevelFor(roomId, "m.room.power_levels");
  243. }).then(function(event) {
  244. return self.client.setPowerLevel(roomId, target, level, event);
  245. });
  246. };
  247. /**
  248. * <p>Send an <code>m.room.message</code> event to a room.</p>
  249. * This will automatically make the client join the room so they can send the
  250. * message if they are not already joined. It will also make sure that the client
  251. * has sufficient power level to do this.
  252. * @param {string} roomId The room to send to.
  253. * @param {Object} content The event content
  254. * @return {Promise}
  255. */
  256. Intent.prototype.sendMessage = function(roomId, content) {
  257. return this.sendEvent(roomId, "m.room.message", content);
  258. };
  259. /**
  260. * <p>Send a message event to a room.</p>
  261. * This will automatically make the client join the room so they can send the
  262. * message if they are not already joined. It will also make sure that the client
  263. * has sufficient power level to do this.
  264. * @param {string} roomId The room to send to.
  265. * @param {string} type The event type
  266. * @param {Object} content The event content
  267. * @return {Promise}
  268. */
  269. Intent.prototype.sendEvent = function(roomId, type, content) {
  270. var self = this;
  271. return self._ensureJoined(roomId).then(function() {
  272. return self._ensureHasPowerLevelFor(roomId, type);
  273. }).then(self._joinGuard(roomId, function() {
  274. return self.client.sendEvent(roomId, type, content);
  275. }));
  276. };
  277. /**
  278. * <p>Send a state event to a room.</p>
  279. * This will automatically make the client join the room so they can send the
  280. * state if they are not already joined. It will also make sure that the client
  281. * has sufficient power level to do this.
  282. * @param {string} roomId The room to send to.
  283. * @param {string} type The event type
  284. * @param {string} skey The state key
  285. * @param {Object} content The event content
  286. * @return {Promise}
  287. */
  288. Intent.prototype.sendStateEvent = function(roomId, type, skey, content) {
  289. var self = this;
  290. return self._ensureJoined(roomId).then(function() {
  291. return self._ensureHasPowerLevelFor(roomId, type);
  292. }).then(self._joinGuard(roomId, function() {
  293. return self.client.sendStateEvent(roomId, type, content, skey);
  294. }));
  295. };
  296. /**
  297. * <p>Get the current room state for a room.</p>
  298. * This will automatically make the client join the room so they can get the
  299. * state if they are not already joined.
  300. * @param {string} roomId The room to get the state from.
  301. * @param {boolean} [useCache=false] Should the request attempt to lookup
  302. * state from the cache.
  303. * @return {Promise}
  304. */
  305. Intent.prototype.roomState = function(roomId, useCache=false) {
  306. return this._ensureJoined(roomId).then(() => {
  307. if (useCache) {
  308. return this._requestCaches.roomstate.get(roomId);
  309. }
  310. return this.client.roomState(roomId);
  311. });
  312. };
  313. /**
  314. * Create a room with a set of options.
  315. * @param {Object} opts Options.
  316. * @param {boolean} opts.createAsClient True to create this room as a client and
  317. * not the bot: the bot will not join. False to create this room as the bot and
  318. * auto-join the client. Default: false.
  319. * @param {Object} opts.options Options to pass to the client SDK /createRoom API.
  320. * @return {Promise}
  321. */
  322. Intent.prototype.createRoom = function(opts) {
  323. var self = this;
  324. var cli = opts.createAsClient ? this.client : this.botClient;
  325. var options = opts.options || {};
  326. if (!opts.createAsClient) {
  327. // invite the client if they aren't already
  328. options.invite = options.invite || [];
  329. if (options.invite.indexOf(this.client.credentials.userId) === -1) {
  330. options.invite.push(this.client.credentials.userId);
  331. }
  332. }
  333. // make sure that the thing doing the room creation isn't inviting itself
  334. // else Synapse hard fails the operation with M_FORBIDDEN
  335. if (options.invite && options.invite.indexOf(cli.credentials.userId) !== -1) {
  336. options.invite.splice(options.invite.indexOf(cli.credentials.userId), 1);
  337. }
  338. return this._ensureRegistered().then(function() {
  339. return cli.createRoom(options);
  340. }).then(function(res) {
  341. // create a fake power level event to give the room creator ops if we
  342. // don't yet have a power level event.
  343. if (self.opts.backingStore.getPowerLevelContent(res.room_id)) {
  344. return res;
  345. }
  346. var users = {};
  347. users[cli.credentials.userId] = 100;
  348. self.opts.backingStore.setPowerLevelContent(res.room_id, {
  349. users_default: 0,
  350. events_default: 0,
  351. state_default: 50,
  352. users: users,
  353. events: {}
  354. });
  355. return res;
  356. });
  357. };
  358. /**
  359. * <p>Invite a user to a room.</p>
  360. * This will automatically make the client join the room so they can send the
  361. * invite if they are not already joined.
  362. * @param {string} roomId The room to invite the user to.
  363. * @param {string} target The user ID to invite.
  364. * @return {Promise} Resolved when invited, else rejected with an error.
  365. */
  366. Intent.prototype.invite = function(roomId, target) {
  367. var self = this;
  368. return this._ensureJoined(roomId).then(function() {
  369. return self.client.invite(roomId, target);
  370. });
  371. };
  372. /**
  373. * <p>Kick a user from a room.</p>
  374. * This will automatically make the client join the room so they can send the
  375. * kick if they are not already joined.
  376. * @param {string} roomId The room to kick the user from.
  377. * @param {string} target The target of the kick operation.
  378. * @param {string} reason Optional. The reason for the kick.
  379. * @return {Promise} Resolved when kickked, else rejected with an error.
  380. */
  381. Intent.prototype.kick = function(roomId, target, reason) {
  382. var self = this;
  383. return this._ensureJoined(roomId).then(function() {
  384. return self.client.kick(roomId, target, reason);
  385. });
  386. };
  387. /**
  388. * <p>Ban a user from a room.</p>
  389. * This will automatically make the client join the room so they can send the
  390. * ban if they are not already joined.
  391. * @param {string} roomId The room to ban the user from.
  392. * @param {string} target The target of the ban operation.
  393. * @param {string} reason Optional. The reason for the ban.
  394. * @return {Promise} Resolved when banned, else rejected with an error.
  395. */
  396. Intent.prototype.ban = function(roomId, target, reason) {
  397. var self = this;
  398. return this._ensureJoined(roomId).then(function() {
  399. return self.client.ban(roomId, target, reason);
  400. });
  401. };
  402. /**
  403. * <p>Unban a user from a room.</p>
  404. * This will automatically make the client join the room so they can send the
  405. * unban if they are not already joined.
  406. * @param {string} roomId The room to unban the user from.
  407. * @param {string} target The target of the unban operation.
  408. * @return {Promise} Resolved when unbanned, else rejected with an error.
  409. */
  410. Intent.prototype.unban = function(roomId, target) {
  411. var self = this;
  412. return this._ensureJoined(roomId).then(function() {
  413. return self.client.unban(roomId, target);
  414. });
  415. };
  416. /**
  417. * <p>Join a room</p>
  418. * This will automatically send an invite from the bot if it is an invite-only
  419. * room, which may make the bot attempt to join the room if it isn't already.
  420. * @param {string} roomId The room to join.
  421. * @param {string[]} viaServers The server names to try and join through in
  422. * addition to those that are automatically chosen.
  423. * @return {Promise}
  424. */
  425. Intent.prototype.join = function(roomId, viaServers) {
  426. return this._ensureJoined(roomId, false, viaServers);
  427. };
  428. /**
  429. * <p>Leave a room</p>
  430. * This will no-op if the user isn't in the room.
  431. * @param {string} roomId The room to leave.
  432. * @return {Promise}
  433. */
  434. Intent.prototype.leave = function(roomId) {
  435. return this.client.leave(roomId);
  436. };
  437. /**
  438. * <p>Get a user's profile information</p>
  439. * @param {string} userId The ID of the user whose profile to return
  440. * @param {string} info The profile field name to retrieve (e.g. 'displayname'
  441. * or 'avatar_url'), or null to fetch the entire profile information.
  442. * @param {boolean} [useCache=true] Should the request attempt to lookup
  443. * state from the cache.
  444. * @return {Promise} A Promise that resolves with the requested user's profile
  445. * information
  446. */
  447. Intent.prototype.getProfileInfo = function(userId, info, useCache=true) {
  448. return this._ensureRegistered().then(() => {
  449. if (useCache) {
  450. return this._requestCaches.profile.get(`${userId}:${info}`, userId, info);
  451. }
  452. return this.client.getProfileInfo(userId, info);
  453. });
  454. };
  455. /**
  456. * <p>Set the user's display name</p>
  457. * @param {string} name The new display name
  458. * @return {Promise}
  459. */
  460. Intent.prototype.setDisplayName = function(name) {
  461. var self = this;
  462. return self._ensureRegistered().then(function() {
  463. return self.client.setDisplayName(name);
  464. });
  465. };
  466. /**
  467. * <p>Set the user's avatar URL</p>
  468. * @param {string} url The new avatar URL
  469. * @return {Promise}
  470. */
  471. Intent.prototype.setAvatarUrl = function(url) {
  472. var self = this;
  473. return self._ensureRegistered().then(function() {
  474. return self.client.setAvatarUrl(url);
  475. });
  476. };
  477. /**
  478. * Create a new alias mapping.
  479. * @param {string} alias The room alias to create
  480. * @param {string} roomId The room ID the alias should point at.
  481. * @return {Promise}
  482. */
  483. Intent.prototype.createAlias = function(alias, roomId) {
  484. var self = this;
  485. return self._ensureRegistered().then(function() {
  486. return self.client.createAlias(alias, roomId);
  487. });
  488. };
  489. /**
  490. * Set the presence of this user.
  491. * @param {string} presence One of "online", "offline" or "unavailable".
  492. * @param {string} status_msg The status message to attach.
  493. * @return {Promise} Resolves if the presence was set or no-oped, rejects otherwise.
  494. */
  495. Intent.prototype.setPresence = function(presence, status_msg=undefined) {
  496. if (!this.opts.enablePresence) {
  497. return Promise.resolve();
  498. }
  499. return this._ensureRegistered().then(() => {
  500. return this.client.setPresence({presence, status_msg});
  501. });
  502. };
  503. /**
  504. * @typedef {
  505. * "m.event_not_handled"
  506. * | "m.event_too_old"
  507. * | "m.internal_error"
  508. * | "m.foreign_network_error"
  509. * | "m.event_unknown"
  510. * } BridgeErrorReason
  511. */
  512. /**
  513. * Signals that an error occured while handling an event by the bridge.
  514. *
  515. * **Warning**: This function is unstable and is likely to change pending the outcome
  516. * of https://github.com/matrix-org/matrix-doc/pull/2162.
  517. * @param {string} roomID ID of the room in which the error occured.
  518. * @param {string} eventID ID of the event for which the error occured.
  519. * @param {string} networkName Name of the bridged network.
  520. * @param {BridgeErrorReason} reason The reason why the bridge error occured.
  521. * @param {string} reason_body A human readable string d
  522. * @param {string[]} affectedUsers Array of regex matching all affected users.
  523. * @return {Promise}
  524. */
  525. Intent.prototype.unstableSignalBridgeError = function(
  526. roomID,
  527. eventID,
  528. networkName,
  529. reason,
  530. affectedUsers
  531. ) {
  532. return this.sendEvent(
  533. roomID,
  534. "de.nasnotfound.bridge_error",
  535. {
  536. network_name: networkName,
  537. reason: reason,
  538. affected_users: affectedUsers,
  539. "m.relates_to": {
  540. rel_type: "m.reference",
  541. event_id: eventID,
  542. },
  543. }
  544. );
  545. }
  546. /**
  547. * Get an event in a room.
  548. * This will automatically make the client join the room so they can get the
  549. * event if they are not already joined.
  550. * @param {string} roomId The room to fetch the event from.
  551. * @param {string} eventId The eventId of the event to fetch.
  552. * @param {boolean} [useCache=true] Should the request attempt to lookup from the cache.
  553. * @return {Promise} Resolves with the content of the event, or rejects if not found.
  554. */
  555. Intent.prototype.getEvent = function(roomId, eventId, useCache=true) {
  556. return this._ensureRegistered().then(() => {
  557. if (useCache) {
  558. return this._requestCaches.event.get(`${roomId}:${eventId}`, roomId, eventId);
  559. }
  560. return this.client.fetchRoomEvent(roomId, eventId);
  561. });
  562. };
  563. /**
  564. * Get a state event in a room.
  565. * This will automatically make the client join the room so they can get the
  566. * state if they are not already joined.
  567. * @param {string} roomId The room to get the state from.
  568. * @param {string} eventType The event type to fetch.
  569. * @param {string} [stateKey=""] The state key of the event to fetch.
  570. * @return {Promise}
  571. */
  572. Intent.prototype.getStateEvent = function(roomId, eventType, stateKey = "") {
  573. return this._ensureJoined(roomId).then(() => {
  574. return this.client.getStateEvent(roomId, eventType, stateKey);
  575. });
  576. };
  577. /**
  578. * Inform this Intent class of an incoming event. Various optimisations will be
  579. * done if this is provided. For example, a /join request won't be sent out if
  580. * it knows you've already been joined to the room. This function does nothing
  581. * if a backing store was provided to the Intent.
  582. * @param {Object} event The incoming event JSON
  583. */
  584. Intent.prototype.onEvent = function(event) {
  585. if (!this._membershipStates || !this._powerLevels) {
  586. return;
  587. }
  588. if (event.type === "m.room.member" &&
  589. event.state_key === this.client.credentials.userId) {
  590. this._membershipStates[event.room_id] = event.content.membership;
  591. }
  592. else if (event.type === "m.room.power_levels") {
  593. this._powerLevels[event.room_id] = event.content;
  594. }
  595. };
  596. // Guard a function which returns a promise which may reject if the user is not
  597. // in the room. If the promise rejects, join the room and retry the function.
  598. Intent.prototype._joinGuard = function(roomId, promiseFn) {
  599. var self = this;
  600. return function() {
  601. return promiseFn().catch(function(err) {
  602. if (err.errcode !== "M_FORBIDDEN") {
  603. // not a guardable error
  604. throw err;
  605. }
  606. return self._ensureJoined(roomId, true).then(function() {
  607. return promiseFn();
  608. })
  609. });
  610. };
  611. };
  612. Intent.prototype._ensureJoined = function(
  613. roomId, ignoreCache = false, viaServers = undefined, passthroughError = false
  614. ) {
  615. var self = this;
  616. var userId = self.client.credentials.userId;
  617. const opts = {
  618. syncRoom: false,
  619. };
  620. if (viaServers) {
  621. opts.viaServers = viaServers;
  622. }
  623. if (this.opts.backingStore.getMembership(roomId, userId) === "join" && !ignoreCache) {
  624. return Promise.resolve();
  625. }
  626. /* Logic:
  627. if client /join:
  628. SUCCESS
  629. else if bot /invite client:
  630. if client /join:
  631. SUCCESS
  632. else:
  633. FAIL (client couldn't join)
  634. else if bot /join:
  635. if bot /invite client and client /join:
  636. SUCCESS
  637. else:
  638. FAIL (bot couldn't invite)
  639. else:
  640. FAIL (bot can't get into the room)
  641. */
  642. var d = new Promise.defer();
  643. function mark(r, state) {
  644. self.opts.backingStore.setMembership(r, userId, state);
  645. if (state === "join") {
  646. d.resolve();
  647. }
  648. }
  649. var dontJoin = this.opts.dontJoin;
  650. self._ensureRegistered().done(function() {
  651. if (dontJoin) {
  652. d.resolve();
  653. return;
  654. }
  655. self.client.joinRoom(roomId, opts).then(function() {
  656. mark(roomId, "join");
  657. }, function(e) {
  658. if (e.errcode !== "M_FORBIDDEN" || self.botClient === self) {
  659. d.reject(passthroughError ? e : new Error("Failed to join room"));
  660. return;
  661. }
  662. // Try bot inviting client
  663. self.botClient.invite(roomId, userId).then(function() {
  664. return self.client.joinRoom(roomId, opts);
  665. }).done(function() {
  666. mark(roomId, "join");
  667. }, function(invErr) {
  668. // Try bot joining
  669. self.botClient.joinRoom(roomId, opts)
  670. .then(function() {
  671. return self.botClient.invite(roomId, userId);
  672. }).then(function() {
  673. return self.client.joinRoom(roomId, opts);
  674. }).done(function() {
  675. mark(roomId, "join");
  676. }, function(finalErr) {
  677. d.reject(passthroughError ? e : new Error("Failed to join room"));
  678. return;
  679. });
  680. });
  681. });
  682. }, function(e) {
  683. d.reject(e);
  684. });
  685. return d.promise;
  686. };
  687. Intent.prototype._ensureHasPowerLevelFor = function(roomId, eventType) {
  688. if (this.opts.dontCheckPowerLevel && eventType !== "m.room.power_levels") {
  689. return Promise.resolve();
  690. }
  691. var self = this;
  692. var userId = this.client.credentials.userId;
  693. var plContent = this.opts.backingStore.getPowerLevelContent(roomId);
  694. var promise = Promise.resolve(plContent);
  695. if (!plContent) {
  696. promise = this.client.getStateEvent(roomId, "m.room.power_levels", "");
  697. }
  698. return promise.then(function(eventContent) {
  699. self.opts.backingStore.setPowerLevelContent(roomId, eventContent);
  700. var event = {
  701. content: eventContent,
  702. room_id: roomId,
  703. user_id: "",
  704. event_id: "_",
  705. state_key: "",
  706. type: "m.room.power_levels"
  707. }
  708. var powerLevelEvent = new MatrixEvent(event);
  709. // What level do we need for this event type?
  710. var defaultLevel = event.content.events_default;
  711. if (STATE_EVENT_TYPES.indexOf(eventType) !== -1) {
  712. defaultLevel = event.content.state_default;
  713. }
  714. var requiredLevel = event.content.events[eventType] || defaultLevel;
  715. // Parse out what level the client has by abusing the JS SDK
  716. var roomMember = new RoomMember(roomId, userId);
  717. roomMember.setPowerLevelEvent(powerLevelEvent);
  718. if (requiredLevel > roomMember.powerLevel) {
  719. // can the bot update our power level?
  720. var bot = new RoomMember(roomId, self.botClient.credentials.userId);
  721. bot.setPowerLevelEvent(powerLevelEvent);
  722. var levelRequiredToModifyPowerLevels = event.content.events[
  723. "m.room.power_levels"
  724. ] || event.content.state_default;
  725. if (levelRequiredToModifyPowerLevels > bot.powerLevel) {
  726. // even the bot has no power here.. give up.
  727. throw new Error(
  728. "Cannot ensure client has power level for event " + eventType +
  729. " : client has " + roomMember.powerLevel + " and we require " +
  730. requiredLevel + " and the bot doesn't have permission to " +
  731. "edit the client's power level."
  732. );
  733. }
  734. // update the client's power level first
  735. return self.botClient.setPowerLevel(
  736. roomId, userId, requiredLevel, powerLevelEvent
  737. ).then(function() {
  738. // tweak the level for the client to reflect the new reality
  739. var userLevels = powerLevelEvent.getContent().users || {};
  740. userLevels[userId] = requiredLevel;
  741. powerLevelEvent.getContent().users = userLevels;
  742. return Promise.resolve(powerLevelEvent);
  743. });
  744. }
  745. return Promise.resolve(powerLevelEvent);
  746. });
  747. };
  748. Intent.prototype._ensureRegistered = function() {
  749. if (this.opts.registered) {
  750. return Promise.resolve("registered=true");
  751. }
  752. const userId = this.client.credentials.userId;
  753. const localpart = new MatrixUser(userId).localpart;
  754. return this.botClient.register(localpart).then((res) => {
  755. this.opts.registered = true;
  756. return res;
  757. }).catch((err) => {
  758. if (err.errcode === "M_USER_IN_USE") {
  759. this.opts.registered = true;
  760. return null;
  761. }
  762. throw err;
  763. });
  764. };
  765. module.exports = Intent;