@rocket.chat/forked-matrix-bot-sdk
Version:
TypeScript/JavaScript SDK for Matrix bots and appservices
180 lines (179 loc) • 7.6 kB
JavaScript
"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;