Source: components/room-upgrade-handler.js

  1. const log = require("./logging").get("RoomUpgradeHandler");
  2. const MatrixRoom = require("../models/rooms/matrix");
  3. const MatrixUser = require("../models/users/matrix");
  4. /**
  5. * Handles migration of rooms when a room upgrade is performed.
  6. */
  7. class RoomUpgradeHandler {
  8. /**
  9. * @param {RoomUpgradeHandler~Options} opts
  10. * @param {Bridge} bridge The parent bridge.
  11. */
  12. constructor(opts, bridge) {
  13. if (opts.migrateGhosts !== false) {
  14. opts.migrateGhosts = true;
  15. }
  16. if (opts.migrateStoreEntries !== false) {
  17. opts.migrateStoreEntries = true;
  18. }
  19. this._opts = opts;
  20. this._bridge = bridge;
  21. this._waitingForInvite = new Map(); //newRoomId: oldRoomId
  22. }
  23. onTombstone(ev) {
  24. const movingTo = ev.content.replacement_room;
  25. log.info(`Got tombstone event for ${ev.room_id} -> ${movingTo}`);
  26. const joinVia = new MatrixUser(ev.sender).domain;
  27. // Try to join the new room.
  28. return this._joinNewRoom(movingTo, [joinVia]).then((couldJoin) => {
  29. if (couldJoin) {
  30. return this._onJoinedNewRoom(ev.room_id, movingTo);
  31. }
  32. this._waitingForInvite.set(movingTo, ev.room_id);
  33. return true;
  34. }).catch((err) => {
  35. log.error("Couldn't handle room upgrade: ", err);
  36. return false;
  37. });
  38. }
  39. _joinNewRoom(newRoomId, joinVia=[]) {
  40. const intent = this._bridge.getIntent();
  41. return intent.join(newRoomId, [joinVia]).then(() => {
  42. return true;
  43. }).catch((ex) => {
  44. if (ex.errcode === "M_FORBIDDEN") {
  45. return false;
  46. }
  47. throw Error("Failed to handle upgrade");
  48. })
  49. }
  50. onInvite(ev) {
  51. if (!this._waitingForInvite.has(ev.room_id)) {
  52. return false;
  53. }
  54. const oldRoomId = this._waitingForInvite.get(ev.room_id);
  55. this._waitingForInvite.delete(ev.room_id);
  56. log.debug(`Got invite to upgraded room ${ev.room_id}`);
  57. this._joinNewRoom(ev.room_id).then(() => {
  58. return this._onJoinedNewRoom(oldRoomId, ev.room_id);
  59. }).catch((err) => {
  60. log.error("Couldn't handle room upgrade: ", err);
  61. });
  62. return true;
  63. }
  64. async _onJoinedNewRoom(oldRoomId, newRoomId) {
  65. log.debug(`Joined ${newRoomId}`);
  66. const intent = this._bridge.getIntent();
  67. const asBot = this._bridge.getBot();
  68. if (this._opts.migrateStoreEntries) {
  69. const success = await this._migrateStoreEntries(oldRoomId, newRoomId);
  70. if (!success) {
  71. log.error("Failed to migrate room entries. Not continuing with migration.");
  72. return false;
  73. }
  74. }
  75. log.debug(`Migrated entries from ${oldRoomId} to ${newRoomId} successfully.`);
  76. if (this._opts.onRoomMigrated) {
  77. // This may or may not be a promise, so await it.
  78. await this._opts.onRoomMigrated(oldRoomId, newRoomId);
  79. }
  80. if (!this._opts.migrateGhosts) {
  81. return false;
  82. }
  83. try {
  84. const members = await asBot.getJoinedMembers(oldRoomId);
  85. const userIds = Object.keys(members).filter((u) => asBot.isRemoteUser(u));
  86. log.debug(`Migrating ${userIds.length} ghosts`);
  87. for (const userId of userIds) {
  88. const i = this._bridge.getIntent(userId);
  89. await i.leave(oldRoomId);
  90. await i.join(newRoomId);
  91. }
  92. intent.leave(oldRoomId);
  93. }
  94. catch (ex) {
  95. log.warn("Failed to migrate ghosts", ex);
  96. return false;
  97. }
  98. return true;
  99. }
  100. async _migrateStoreEntries(oldRoomId, newRoomId) {
  101. const roomStore = this._bridge.getRoomStore();
  102. const entries = await roomStore.getEntriesByMatrixId(oldRoomId);
  103. let success = false;
  104. // Upgrades are critical to get right, or a room will be stuck
  105. // until someone manaually intervenes. It's important to continue
  106. // migrating if at least one entry is successfully migrated.
  107. for (const entry of entries) {
  108. log.debug(`Migrating room entry ${entry.id}`);
  109. const existingId = entry.id;
  110. try {
  111. const newEntry = (
  112. this._opts.migrateEntry || this._migrateEntry)(entry, newRoomId);
  113. if (!newEntry) {
  114. continue;
  115. }
  116. // If migrateEntry changed the id of the room, then ensure
  117. // that we remove the old one.
  118. if (existingId !== newEntry.id) {
  119. await roomStore.removeEntryById(existingId);
  120. }
  121. await roomStore.upsertEntry(newEntry);
  122. success = true;
  123. }
  124. catch (ex) {
  125. log.error(`Failed to migrate room entry ${entry.id}.`);
  126. }
  127. }
  128. return success;
  129. }
  130. _migrateEntry(entry, newRoomId) {
  131. entry.matrix = new MatrixRoom(newRoomId, {
  132. name: entry.name,
  133. topic: entry.topic,
  134. extras: entry._extras,
  135. });
  136. return entry;
  137. }
  138. }
  139. module.exports = RoomUpgradeHandler;
  140. /**
  141. * Options to supply to the {@link RoomUpgradeHandler}.
  142. * @typedef RoomUpgradeHandler~Options
  143. * @type {Object}
  144. * @property {RoomUpgradeHandler~MigrateEntry} migrateEntry Called when
  145. * the handler wishes to migrate a MatrixRoom entry to a new room_id. If omitted,
  146. * {@link RoomUpgradeHandler~_migrateEntry} will be used instead.
  147. * @property {RoomUpgradeHandler~onRoomMigrated} onRoomMigrated This is called
  148. * when the entries of the room have been migrated, the bridge should do any cleanup it
  149. * needs of the old room and setup the new room (ex: Joining ghosts to the new room).
  150. * @property {bool} [consumeEvent=true] Consume tombstone or invite events that
  151. * are acted on by this handler.
  152. * @property {bool} [migrateGhosts=true] If true, migrate all ghost users across to
  153. * the new room.
  154. * @property {bool} [migrateStoreEntries=true] If true, migrate all ghost users across to
  155. * the new room.
  156. */
  157. /**
  158. * Invoked when iterating around a rooms entries. Should be used to update entries
  159. * with a new room id.
  160. *
  161. * @callback RoomUpgradeHandler~MigrateEntry
  162. * @param {RoomBridgeStore~Entry} entry The existing entry.
  163. * @param {string} newRoomId The new roomId.
  164. * @return {RoomBridgeStore~Entry} Return the entry to upsert it,
  165. * or null to ignore it.
  166. */
  167. /**
  168. * Invoked after a room has been upgraded and it's entries updated.
  169. *
  170. * @callback RoomUpgradeHandler~onRoomMigrated
  171. * @param {string} oldRoomId The old roomId.
  172. * @param {string} newRoomId The new roomId.
  173. */