Source: components/room-bridge-store.js

  1. /**
  2. * Room storage format:
  3. * {
  4. * id: "matrix|remote|link_key", // customisable
  5. * matrix_id: "room_id",
  6. * remote_id: "remote_room_id",
  7. * matrix: { serialised matrix room info },
  8. * remote: { serialised remote room info },
  9. * data: { ... any additional info ... }
  10. * }
  11. *
  12. * Each document can either represent a matrix room, a remote room, or
  13. * a mapping. They look like this:
  14. * MATRIX
  15. * {
  16. * id: "!room:id",
  17. * matrix_id: "!room:id",
  18. * matrix: { .. custom data eg name: "A happy place" .. }
  19. * }
  20. *
  21. * REMOTE (e.g. IRC)
  22. * {
  23. * id: "irc.freenode.net_#channame",
  24. * remote_id: "irc.freenode.net_#channame",
  25. * remote: { .. custom data e.g. is_pm_room: true .. }
  26. * }
  27. *
  28. * MAPPING
  29. * {
  30. * id: "!room:id__irc.freenode.net_#channame", // link key; customisable.
  31. * matrix_id: "!room:id",
  32. * remote_id: "irc.freenode.net_#channame",
  33. * matrix: { .. custom data .. },
  34. * remote: { .. custom data .. },
  35. * data: { .. custom data about the mapping ..}
  36. * }
  37. *
  38. * A unique, non-sparse index can be set on the 'id' key, and non-unique,
  39. * sparse indexes can be set on matrix_id and remote_id to make mappings
  40. * quicker to compute.
  41. *
  42. */
  43. "use strict";
  44. var BridgeStore = require("./bridge-store");
  45. var MatrixRoom = require("../models/rooms/matrix");
  46. var RemoteRoom = require("../models/rooms/remote");
  47. var util = require("util");
  48. /**
  49. * Construct a store suitable for room bridging information. Data is stored
  50. * as {@link RoomBridgeStore~Entry}s which have the following
  51. * <i>serialized</i> format:
  52. * <pre>
  53. * {
  54. * id: "unique_id", // customisable
  55. * matrix_id: "room_id",
  56. * remote_id: "remote_room_id",
  57. * matrix: { serialised matrix room info },
  58. * remote: { serialised remote room info },
  59. * data: { ... any additional info ... }
  60. * }
  61. * </pre>
  62. * <p>If a unique 'id' is not given, the store will generate one by concatenating
  63. * the <code>matrix_id</code> and the <code>remote_id</code>. The delimiter
  64. * used is a property on this store and can be modified.</p>
  65. * <p>The structure of Entry objects means that it is efficient to select based
  66. * off the 'id', 'matrix_id' or 'remote_id'. Additional indexes can be added
  67. * manually.</p>
  68. * @constructor
  69. * @param {Datastore} db The connected NEDB database instance
  70. * @param {Object} opts Options for this store.
  71. * @property {string} delimiter The delimiter between matrix and
  72. * remote IDs. Defaults to three spaces. If the schema of your remote IDs
  73. * allows spaces, you will need to change this.
  74. */
  75. function RoomBridgeStore(db, opts) {
  76. this.db = db;
  77. this.delimiter = " ";
  78. }
  79. util.inherits(RoomBridgeStore, BridgeStore);
  80. /**
  81. * Insert an entry, clobbering based on the ID of the entry.
  82. * @param {RoomBridgeStore~Entry} entry
  83. * @return {Promise}
  84. */
  85. RoomBridgeStore.prototype.upsertEntry = function(entry) {
  86. return this.upsert({
  87. id: entry.id
  88. }, serializeEntry(entry));
  89. }
  90. /**
  91. * Get an existing entry based on the provided entry ID.
  92. * @param {String} id The ID of the entry to retrieve.
  93. * @return {?RoomBridgeStore~Entry} A promise which resolves to the entry or null.
  94. */
  95. RoomBridgeStore.prototype.getEntryById = function(id) {
  96. return this.selectOne({
  97. id: id
  98. }, this.convertTo(function(doc) {
  99. return new Entry(doc);
  100. }));
  101. }
  102. /**
  103. * Get a list of entries based on the matrix_id of each entry.
  104. * @param {string} matrixId
  105. * @return {RoomBridgeStore~Entry[]}
  106. */
  107. RoomBridgeStore.prototype.getEntriesByMatrixId = function(matrixId) {
  108. return this.select({
  109. matrix_id: matrixId
  110. }, this.convertTo(function(doc) {
  111. return new Entry(doc);
  112. }));
  113. };
  114. /**
  115. * A batch version of <code>getEntriesByMatrixId</code>.
  116. * @param {String[]} ids
  117. * @return {Object.<string,RoomBridgeStore~Entry[]>} Resolves
  118. * to a map of room_id => Entry[]
  119. */
  120. RoomBridgeStore.prototype.getEntriesByMatrixIds = function(ids) {
  121. return this.select({
  122. matrix_id: {
  123. $in: ids
  124. }
  125. }).then(function(docs) {
  126. var entries = {};
  127. docs.forEach(function(doc) {
  128. if (!entries[doc.matrix_id]) {
  129. entries[doc.matrix_id] = [];
  130. }
  131. entries[doc.matrix_id].push(new Entry(doc));
  132. });
  133. return entries;
  134. });
  135. };
  136. /**
  137. * Get a list of entries based on the remote_id of each entry.
  138. * @param {String} remoteId
  139. * @return {RoomBridgeStore~Entry[]}
  140. */
  141. RoomBridgeStore.prototype.getEntriesByRemoteId = function(remoteId) {
  142. return this.select({
  143. remote_id: remoteId
  144. }, this.convertTo(function(doc) {
  145. return new Entry(doc);
  146. }));
  147. };
  148. /**
  149. * Create a link between a matrix room and remote room. This will create an entry with:
  150. * <ul>
  151. * <li>The matrix_id set to the matrix room ID.</li>
  152. * <li>The remote_id set to the remote room ID.</li>
  153. * <li>The id set to the id value given OR a concatenation of the matrix and remote IDs
  154. * if one is not provided.</li>
  155. * </ul>
  156. * @param {MatrixRoom} matrixRoom The matrix room
  157. * @param {RemoteRoom} remoteRoom The remote room
  158. * @param {Object=} data Information about this mapping.
  159. * @param {string=} linkId The id value to set. If not given, a unique ID will be
  160. * created from the matrix_id and remote_id.
  161. * @return {Promise}
  162. */
  163. RoomBridgeStore.prototype.linkRooms = function(matrixRoom, remoteRoom, data, linkId) {
  164. data = data || {};
  165. linkId = linkId || createUniqueId(
  166. matrixRoom.getId(), remoteRoom.getId(), this.delimiter
  167. );
  168. var self = this;
  169. return self.upsert({
  170. id: linkId
  171. }, {
  172. id: linkId,
  173. remote_id: remoteRoom.getId(),
  174. matrix_id: matrixRoom.getId(),
  175. remote: remoteRoom.serialize(),
  176. matrix: matrixRoom.serialize(),
  177. data: data
  178. });
  179. };
  180. /**
  181. * Create an entry with only a matrix room. Sets the 'id' of the entry to the
  182. * Matrix room ID. If an entry already exists with this 'id', it will be replaced.
  183. * This function is useful if you just want to store a room with some data and not
  184. * worry about any mappings.
  185. * @param {MatrixRoom} matrixRoom
  186. * @return {Promise}
  187. * @see RoomBridgeStore#getMatrixRoom
  188. */
  189. RoomBridgeStore.prototype.setMatrixRoom = function(matrixRoom) {
  190. return this.upsertEntry({
  191. id: matrixRoom.getId(),
  192. matrix_id: matrixRoom.getId(),
  193. matrix: matrixRoom
  194. });
  195. };
  196. /**
  197. * Get an entry's Matrix room based on the provided room_id. The entry MUST have
  198. * an 'id' of the room_id and there MUST be a Matrix room contained within the
  199. * entry for this to return.
  200. * @param {string} roomId
  201. * @return {?MatrixRoom}
  202. * @see RoomBridgeStore#setMatrixRoom
  203. */
  204. RoomBridgeStore.prototype.getMatrixRoom = function(roomId) {
  205. return this.getEntryById(roomId).then(function(e) {
  206. return e ? e.matrix : null;
  207. });
  208. };
  209. /**
  210. * Get all entries with the given remote_id which have a Matrix room within.
  211. * @param {string} remoteId
  212. * @return {MatrixRoom[]}
  213. */
  214. RoomBridgeStore.prototype.getLinkedMatrixRooms = function(remoteId) {
  215. return this.getEntriesByRemoteId(remoteId).then(function(entries) {
  216. return entries.filter(function(e) {
  217. return Boolean(e.matrix);
  218. }).map(function(e) {
  219. return e.matrix;
  220. });
  221. });
  222. };
  223. /**
  224. * Get all entries with the given matrix_id which have a Remote room within.
  225. * @param {string} matrixId
  226. * @return {RemoteRoom[]}
  227. */
  228. RoomBridgeStore.prototype.getLinkedRemoteRooms = function(matrixId) {
  229. return this.getEntriesByMatrixId(matrixId).then(function(entries) {
  230. return entries.filter(function(e) {
  231. return Boolean(e.remote);
  232. }).map(function(e) {
  233. return e.remote;
  234. });
  235. });
  236. };
  237. /**
  238. * A batched version of <code>getLinkedRemoteRooms</code>.
  239. * @param {string[]} matrixIds
  240. * @return {Object.<string, RemoteRoom>} A mapping of room_id to RemoteRoom.
  241. * @see RoomBridgeStore#getLinkedRemoteRooms
  242. */
  243. RoomBridgeStore.prototype.batchGetLinkedRemoteRooms = function(matrixIds) {
  244. return this.getEntriesByMatrixIds(matrixIds).then(function(entryMap) {
  245. Object.keys(entryMap).forEach(function(k) {
  246. entryMap[k] = entryMap[k].filter(function(e) {
  247. return Boolean(e.remote);
  248. }).map(function(e) {
  249. return e.remote;
  250. });
  251. })
  252. return entryMap;
  253. });
  254. };
  255. /**
  256. * Get a list of entries based on a RemoteRoom data value.
  257. * @param {Object} data The data values to retrieve based from.
  258. * @return {RoomBridgeStore~Entry[]} A list of entries
  259. * @example
  260. * remoteRoom.set("some_key", "some_val");
  261. * // store remoteRoom and then:
  262. * store.getEntriesByRemoteRoomData({
  263. * some_key: "some_val"
  264. * });
  265. */
  266. RoomBridgeStore.prototype.getEntriesByRemoteRoomData = function(data) {
  267. Object.keys(data).forEach(function(k) {
  268. var query = data[k];
  269. delete data[k];
  270. data["remote." + k] = query;
  271. });
  272. return this.select(data, this.convertTo(function(doc) {
  273. return new Entry(doc);
  274. }));
  275. };
  276. /**
  277. * Get a list of entries based on a MatrixRoom data value.
  278. * @param {Object} data The data values to retrieve based from.
  279. * @return {RoomBridgeStore~Entry[]} A list of entries
  280. * @example
  281. * matrixRoom.set("some_key", "some_val");
  282. * // store matrixRoom and then:
  283. * store.getEntriesByMatrixRoomData({
  284. * some_key: "some_val"
  285. * });
  286. */
  287. RoomBridgeStore.prototype.getEntriesByMatrixRoomData = function(data) {
  288. Object.keys(data).forEach(function(k) {
  289. var query = data[k];
  290. delete data[k];
  291. data["matrix.extras." + k] = query;
  292. });
  293. return this.select(data, this.convertTo(function(doc) {
  294. return new Entry(doc);
  295. }));
  296. };
  297. /**
  298. * Get a list of entries based on the link's data value.
  299. * @param {Object} data The data values to retrieve based from.
  300. * @return {RoomBridgeStore~Entry[]} A list of entries
  301. * @example
  302. * store.linkRooms(matrixRoom, remoteRoom, { some_key: "some_val" });
  303. * store.getEntriesByLinkData({
  304. * some_key: "some_val"
  305. * });
  306. */
  307. RoomBridgeStore.prototype.getEntriesByLinkData = function(data) {
  308. Object.keys(data).forEach(function(k) {
  309. var query = data[k];
  310. delete data[k];
  311. data["data." + k] = query;
  312. });
  313. return this.select(data, this.convertTo(function(doc) {
  314. return new Entry(doc);
  315. }));
  316. };
  317. /**
  318. * Remove entries based on remote room data.
  319. * @param {Object} data The data to match.
  320. * @return {Promise}
  321. * @example
  322. * remoteRoom.set("a_key", "a_val");
  323. * // store remoteRoom and then:
  324. * store.removeEntriesByRemoteRoomData({
  325. * a_key: "a_val"
  326. * });
  327. */
  328. RoomBridgeStore.prototype.removeEntriesByRemoteRoomData = function(data) {
  329. Object.keys(data).forEach(function(k) {
  330. var query = data[k];
  331. delete data[k];
  332. data["remote." + k] = query;
  333. });
  334. return this.delete(data);
  335. };
  336. /**
  337. * Remove entries with this remote room id.
  338. * @param {Object} remoteId The remote id.
  339. * @return {Promise}
  340. * @example
  341. * new RemoteRoom("foobar");
  342. * // store the RemoteRoom and then:
  343. * store.removeEntriesByRemoteRoomId("foobar");
  344. */
  345. RoomBridgeStore.prototype.removeEntriesByRemoteRoomId = function(remoteId) {
  346. return this.delete({
  347. remote_id: remoteId
  348. });
  349. };
  350. /**
  351. * Remove entries based on matrix room data.
  352. * @param {Object} data The data to match.
  353. * @return {Promise}
  354. * @example
  355. * matrixRoom.set("a_key", "a_val");
  356. * // store matrixRoom and then:
  357. * store.removeEntriesByMatrixRoomData({
  358. * a_key: "a_val"
  359. * });
  360. */
  361. RoomBridgeStore.prototype.removeEntriesByMatrixRoomData = function(data) {
  362. Object.keys(data).forEach(function(k) {
  363. var query = data[k];
  364. delete data[k];
  365. data["matrix.extras." + k] = query;
  366. });
  367. return this.delete(data);
  368. };
  369. /**
  370. * Remove entries with this matrix room id.
  371. * @param {Object} matrixId The matrix id.
  372. * @return {Promise}
  373. * @example
  374. * new MatrixRoom("!foobar:matrix.org");
  375. * // store the MatrixRoom and then:
  376. * store.removeEntriesByMatrixRoomId("!foobar:matrix.org");
  377. */
  378. RoomBridgeStore.prototype.removeEntriesByMatrixRoomId = function(matrixId) {
  379. return this.delete({
  380. matrix_id: matrixId
  381. });
  382. };
  383. /**
  384. * Remove entries based on the link's data value.
  385. * @param {Object} data The data to match.
  386. * @return {Promise}
  387. * @example
  388. * store.linkRooms(matrixRoom, remoteRoom, { a_key: "a_val" });
  389. * store.removeEntriesByLinkData({
  390. * a_key: "a_val"
  391. * });
  392. */
  393. RoomBridgeStore.prototype.removeEntriesByLinkData = function(data) {
  394. Object.keys(data).forEach(function(k) {
  395. var query = data[k];
  396. delete data[k];
  397. data["data." + k] = query;
  398. });
  399. return this.delete(data);
  400. };
  401. /**
  402. * Remove an existing entry based on the provided entry ID.
  403. * @param {String} id The ID of the entry to remove.
  404. * @return {Promise}
  405. * @example
  406. * store.removeEntryById("anid");
  407. */
  408. RoomBridgeStore.prototype.removeEntryById = function(id) {
  409. return this.delete({ id });
  410. }
  411. function createUniqueId(matrixRoomId, remoteRoomId, delimiter) {
  412. return (matrixRoomId || "") + delimiter + (remoteRoomId || "");
  413. }
  414. /**
  415. * Construct a new RoomBridgeStore Entry.
  416. * @constructor
  417. * @typedef RoomBridgeStore~Entry
  418. * @property {string} id The unique ID for this entry.
  419. * @property {?MatrixRoom} matrix The matrix room, if applicable.
  420. * @property {?RemoteRoom} remote The remote room, if applicable.
  421. * @property {?Object} data Information about this mapping, which may be an empty.
  422. */
  423. function Entry(doc) {
  424. doc = doc || {};
  425. this.id = doc.id;
  426. this.matrix = doc.matrix_id ? new MatrixRoom(doc.matrix_id, doc.matrix) : undefined;
  427. this.remote = doc.remote_id ? new RemoteRoom(doc.remote_id, doc.remote) : undefined;
  428. this.data = doc.data;
  429. }
  430. // not a member function so callers can provide a POJO
  431. function serializeEntry(entry) {
  432. return {
  433. id: entry.id,
  434. remote_id: entry.remote ? entry.remote.getId() : undefined,
  435. matrix_id: entry.matrix ? entry.matrix.getId() : undefined,
  436. remote: entry.remote ? entry.remote.serialize() : undefined,
  437. matrix: entry.matrix ? entry.matrix.serialize() : undefined,
  438. data: entry.data
  439. }
  440. }
  441. module.exports = RoomBridgeStore;