UNPKG

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

Version:

TypeScript/JavaScript SDK for Matrix bots and appservices

180 lines (179 loc) 7.6 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DMs = void 0; const Crypto_1 = require("./models/Crypto"); const LogService_1 = require("./logging/LogService"); /** * Handles DM (direct messages) matching between users. Note that bots which * existed prior to this might not have DM rooms populated correctly - the * account data can be populated externally and that will be reflected here. * * Note that DM status is persisted across all access tokens for a user and * is not persisted with the regular stores. The DM map is instead tracked * on the homeserver as account data and thus survives the bot's own storage * being wiped. * @category Utilities */ class DMs { /** * Creates a new DM map. * @param {MatrixClient} client The client the DM map is for. */ constructor(client) { this.client = client; this.cached = new Map(); this.client.on("account_data", (ev) => { if (ev['type'] !== 'm.direct') return; // noinspection JSIgnoredPromiseFromCall this.updateFromAccountData(); }); this.client.on("room.invite", (rid, ev) => this.handleInvite(rid, ev)); } updateFromAccountData() { var _a; return __awaiter(this, void 0, void 0, function* () { // Don't trust the sync update let map = {}; try { map = yield this.client.getAccountData("m.direct"); } catch (e) { if (((_a = e.body) === null || _a === void 0 ? void 0 : _a.errcode) !== "M_NOT_FOUND" && e.statusCode !== 404) { LogService_1.LogService.warn("DMs", "Error getting m.direct account data: ", e); } } this.cached = new Map(); for (const [userId, roomIds] of Object.entries(map)) { this.cached.set(userId, roomIds); } }); } handleInvite(roomId, ev) { var _a; return __awaiter(this, void 0, void 0, function* () { if (((_a = ev['content']) === null || _a === void 0 ? void 0 : _a['is_direct']) === true) { const userId = ev['sender']; if (!this.cached.has(userId)) this.cached.set(userId, []); this.cached.set(userId, [roomId, ...this.cached.get(userId)]); yield this.persistCache(); } }); } persistCache() { return __awaiter(this, void 0, void 0, function* () { const obj = {}; for (const [uid, rids] of this.cached.entries()) { obj[uid] = rids; } yield this.client.setAccountData("m.direct", obj); }); } fixDms(userId) { return __awaiter(this, void 0, void 0, function* () { const currentRooms = this.cached.get(userId); if (!currentRooms) return; const toKeep = []; for (const roomId of currentRooms) { try { const members = yield this.client.getRoomMembers(roomId); const joined = members.filter(m => m.effectiveMembership === "join" || m.effectiveMembership === "invite"); if (joined.some(m => m.membershipFor === userId)) { toKeep.push(roomId); } } catch (e) { LogService_1.LogService.warn("DMs", `Unable to check ${roomId} for room members - assuming invalid DM`); } } if (toKeep.length === currentRooms.length) return; // no change if (toKeep.length > 0) { this.cached.set(userId, toKeep); } else { this.cached.delete(userId); } yield this.persistCache(); }); } /** * Forces an update of the DM cache. * @returns {Promise<void>} Resolves when complete. */ update() { return __awaiter(this, void 0, void 0, function* () { yield this.ready; // finish the existing call if present this.ready = this.updateFromAccountData(); return this.ready; }); } /** * Gets or creates a DM with a given user. If a DM needs to be created, it will * be created as an encrypted DM (if both the MatrixClient and target user support * crypto). Otherwise, the createFn can be used to override the call. Note that * when creating a DM room the room should have `is_direct: true` set. * @param {string} userId The user ID to get/create a DM for. * @param {Function} createFn Optional function to use to create the room. Resolves * to the created room ID. * @returns {Promise<string>} Resolves to the DM room ID. */ getOrCreateDm(userId, createFn) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { yield this.ready; yield this.fixDms(userId); const rooms = this.cached.get(userId); if (rooms === null || rooms === void 0 ? void 0 : rooms.length) return rooms[0]; let roomId; if (createFn) { roomId = yield createFn(userId); } else { let hasKeys = false; if (!!this.client.crypto) { const keys = yield this.client.getUserDevices([userId]); const userKeys = (_b = (_a = keys === null || keys === void 0 ? void 0 : keys.device_keys) === null || _a === void 0 ? void 0 : _a[userId]) !== null && _b !== void 0 ? _b : {}; hasKeys = Object.values(userKeys).filter(device => Object.values(device).length > 0).length > 0; } roomId = yield this.client.createRoom({ invite: [userId], is_direct: true, preset: "trusted_private_chat", initial_state: hasKeys ? [{ type: "m.room.encryption", state_key: "", content: { algorithm: Crypto_1.EncryptionAlgorithm.MegolmV1AesSha2 } }] : [], }); } if (!this.cached.has(userId)) this.cached.set(userId, []); this.cached.set(userId, [roomId, ...this.cached.get(userId)]); yield this.persistCache(); return roomId; }); } /** * Determines if a given room is a DM according to the cache. * @param {string} roomId The room ID. * @returns {boolean} True if the room ID is a cached DM room ID. */ isDm(roomId) { for (const val of this.cached.values()) { if (val.includes(roomId)) { return true; } } return false; } } exports.DMs = DMs;