UNPKG

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

Version:

TypeScript/JavaScript SDK for Matrix bots and appservices

385 lines (384 loc) 18.5 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; 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.Intent = void 0; const __1 = require(".."); // noinspection TypeScriptPreferShortImport const decorators_1 = require("../metrics/decorators"); const UnstableAppserviceApis_1 = require("./UnstableAppserviceApis"); /** * An Intent is an intelligent client that tracks things like the user's membership * in rooms to ensure the action being performed is possible. This is very similar * to how Intents work in the matrix-js-sdk in that the Intent will ensure that the * user is joined to the room before posting a message, for example. * @category Application services */ class Intent { /** * Creates a new intent. Intended to be created by application services. * @param {IAppserviceOptions} options The options for the application service. * @param {string} impersonateUserId The user ID to impersonate. * @param {Appservice} appservice The application service itself. */ constructor(options, impersonateUserId, appservice) { this.options = options; this.impersonateUserId = impersonateUserId; this.appservice = appservice; this.knownJoinedRooms = []; this.metrics = new __1.Metrics(appservice.metrics); this.storage = options.storage; this.cryptoStorage = options.cryptoStorage; this.makeClient(false); } makeClient(withCrypto, accessToken) { var _a, _b, _c; let cryptoStore; const storage = (_b = (_a = this.storage) === null || _a === void 0 ? void 0 : _a.storageForUser) === null || _b === void 0 ? void 0 : _b.call(_a, this.userId); if (withCrypto) { cryptoStore = (_c = this.cryptoStorage) === null || _c === void 0 ? void 0 : _c.storageForUser(this.userId); if (!cryptoStore) { throw new Error("Tried to set up client with crypto when not available"); } if (!storage) { throw new Error("Tried to set up client with crypto, but no persistent storage"); } } this.client = new __1.MatrixClient(this.options.homeserverUrl, accessToken !== null && accessToken !== void 0 ? accessToken : this.options.registration.as_token, storage, cryptoStore); this.client.metrics = new __1.Metrics(this.appservice.metrics); // Metrics only go up by one parent this.unstableApisInstance = new UnstableAppserviceApis_1.UnstableAppserviceApis(this.client); if (this.impersonateUserId !== this.appservice.botUserId) { this.client.impersonateUserId(this.impersonateUserId); } if (this.options.joinStrategy) { this.client.setJoinStrategy(this.options.joinStrategy); } } /** * Gets the user ID this intent is for. */ get userId() { return this.impersonateUserId; } /** * Gets the underlying MatrixClient that powers this Intent. */ get underlyingClient() { return this.client; } /** * Gets the unstable API access class. This is generally not recommended to be * used by appservices. * @return {UnstableAppserviceApis} The unstable API access class. */ get unstableApis() { return this.unstableApisInstance; } /** * Sets up crypto on the client if it hasn't already been set up. * @returns {Promise<void>} Resolves when complete. */ enableEncryption() { return __awaiter(this, void 0, void 0, function* () { if (!this.cryptoSetupPromise) { this.cryptoSetupPromise = new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d, _e; try { // Prepare a client first yield this.ensureRegistered(); const storage = (_b = (_a = this.storage) === null || _a === void 0 ? void 0 : _a.storageForUser) === null || _b === void 0 ? void 0 : _b.call(_a, this.userId); this.client.impersonateUserId(this.userId); // make sure the devices call works const cryptoStore = (_c = this.cryptoStorage) === null || _c === void 0 ? void 0 : _c.storageForUser(this.userId); if (!cryptoStore) { // noinspection ExceptionCaughtLocallyJS throw new Error("Failed to create crypto store"); } // Try to impersonate a device ID const ownDevices = yield this.client.getOwnDevices(); let deviceId = yield cryptoStore.getDeviceId(); if (!deviceId || !ownDevices.some(d => d.device_id === deviceId)) { const deviceKeys = yield this.client.getUserDevices([this.userId]); const userDeviceKeys = deviceKeys.device_keys[this.userId]; if (userDeviceKeys) { // We really should be validating signatures here, but we're actively looking // for devices without keys to impersonate, so it should be fine. In theory, // those devices won't even be present but we're cautious. const devicesWithKeys = Array.from(Object.entries(userDeviceKeys)) .filter(d => { var _a; return d[0] === d[1].device_id && !!((_a = d[1].keys) === null || _a === void 0 ? void 0 : _a[`${__1.DeviceKeyAlgorithm.Curve25519}:${d[1].device_id}`]); }); deviceId = (_e = (_d = devicesWithKeys[0]) === null || _d === void 0 ? void 0 : _d[1]) === null || _e === void 0 ? void 0 : _e.device_id; } } let prepared = false; if (deviceId) { this.makeClient(true); this.client.impersonateUserId(this.userId, deviceId); // verify that the server supports impersonating the device const respDeviceId = (yield this.client.getWhoAmI()).device_id; prepared = (respDeviceId === deviceId); } if (!prepared) { // XXX: We work around servers that don't support device_id impersonation const accessToken = yield Promise.resolve(storage === null || storage === void 0 ? void 0 : storage.readValue("accessToken")); if (!accessToken) { const loginBody = { type: "uk.half-shot.msc2778.login.application_service", identifier: { type: "m.id.user", user: this.userId, }, }; this.client.impersonateUserId(null); // avoid confusing homeserver const res = yield this.client.doRequest("POST", "/_matrix/client/r0/login", {}, loginBody); this.makeClient(true, res['access_token']); storage.storeValue("accessToken", this.client.accessToken); prepared = true; } else { this.makeClient(true, accessToken); prepared = true; } } if (!prepared) { // noinspection ExceptionCaughtLocallyJS throw new Error("Unable to establish a device ID"); } // Now set up crypto yield this.client.crypto.prepare(yield this.client.getJoinedRooms()); resolve(); } catch (e) { reject(e); } })); } return this.cryptoSetupPromise; }); } /** * Gets the joined rooms for the intent. Note that by working around * the intent to join rooms may yield inaccurate results. * @returns {Promise<string[]>} Resolves to an array of room IDs where * the intent is joined. */ getJoinedRooms() { return __awaiter(this, void 0, void 0, function* () { yield this.ensureRegistered(); if (this.knownJoinedRooms.length === 0) yield this.refreshJoinedRooms(); return this.knownJoinedRooms.map(r => r); // clone }); } /** * Leaves the given room. * @param {string} roomId The room ID to leave * @returns {Promise<any>} Resolves when the room has been left. */ leaveRoom(roomId) { return __awaiter(this, void 0, void 0, function* () { yield this.ensureRegistered(); return this.client.leaveRoom(roomId).then(() => __awaiter(this, void 0, void 0, function* () { // Recalculate joined rooms now that we've left a room yield this.refreshJoinedRooms(); })); }); } /** * Joins the given room * @param {string} roomIdOrAlias the room ID or alias to join * @returns {Promise<string>} resolves to the joined room ID */ joinRoom(roomIdOrAlias) { return __awaiter(this, void 0, void 0, function* () { yield this.ensureRegistered(); return this.client.joinRoom(roomIdOrAlias).then((roomId) => __awaiter(this, void 0, void 0, function* () { // Recalculate joined rooms now that we've joined a room yield this.refreshJoinedRooms(); return roomId; })); }); } /** * Sends a text message to a room. * @param {string} roomId The room ID to send text to. * @param {string} body The message body to send. * @param {"m.text" | "m.emote" | "m.notice"} msgtype The message type to send. * @returns {Promise<string>} Resolves to the event ID of the sent message. */ sendText(roomId, body, msgtype = "m.text") { return __awaiter(this, void 0, void 0, function* () { return this.sendEvent(roomId, { body: body, msgtype: msgtype }); }); } /** * Sends an event to a room. * @param {string} roomId The room ID to send the event to. * @param {any} content The content of the event. * @returns {Promise<string>} Resolves to the event ID of the sent event. */ sendEvent(roomId, content) { return __awaiter(this, void 0, void 0, function* () { yield this.ensureRegisteredAndJoined(roomId); return this.client.sendMessage(roomId, content); }); } /** * Ensures the user is registered and joined to the given room. * @param {string} roomId The room ID to join * @returns {Promise<any>} Resolves when complete */ ensureRegisteredAndJoined(roomId) { return __awaiter(this, void 0, void 0, function* () { yield this.ensureRegistered(); yield this.ensureJoined(roomId); }); } /** * Ensures the user is joined to the given room * @param {string} roomId The room ID to join * @returns {Promise<any>} Resolves when complete */ ensureJoined(roomId) { return __awaiter(this, void 0, void 0, function* () { if (this.knownJoinedRooms.indexOf(roomId) !== -1) { return; } yield this.refreshJoinedRooms(); if (this.knownJoinedRooms.indexOf(roomId) !== -1) { return; } const returnedRoomId = yield this.client.joinRoom(roomId); if (!this.knownJoinedRooms.includes(returnedRoomId)) { this.knownJoinedRooms.push(returnedRoomId); } return returnedRoomId; }); } /** * Refreshes which rooms the user is joined to, potentially saving time on * calls like ensureJoined() * @returns {Promise<string[]>} Resolves to the joined room IDs for the user. */ refreshJoinedRooms() { return __awaiter(this, void 0, void 0, function* () { this.knownJoinedRooms = yield this.client.getJoinedRooms(); return this.knownJoinedRooms.map(r => r); // clone }); } /** * Ensures the user is registered * @returns {Promise<any>} Resolves when complete */ ensureRegistered() { return __awaiter(this, void 0, void 0, function* () { if (!(yield Promise.resolve(this.storage.isUserRegistered(this.userId)))) { try { const result = yield this.client.doRequest("POST", "/_matrix/client/r0/register", null, { type: "m.login.application_service", username: this.userId.substring(1).split(":")[0], }); // HACK: Workaround for unit tests if (result['errcode']) { // noinspection ExceptionCaughtLocallyJS throw { body: result }; } } catch (err) { if (typeof (err.body) === "string") err.body = JSON.parse(err.body); if (err.body && err.body["errcode"] === "M_USER_IN_USE") { yield Promise.resolve(this.storage.addRegisteredUser(this.userId)); if (this.userId === this.appservice.botUserId) { return null; } else { __1.LogService.error("Appservice", "Error registering user: User ID is in use"); return null; } } else { __1.LogService.error("Appservice", "Encountered error registering user: "); __1.LogService.error("Appservice", (0, __1.extractRequestError)(err)); } throw err; } yield Promise.resolve(this.storage.addRegisteredUser(this.userId)); } }); } } __decorate([ (0, decorators_1.timedIntentFunctionCall)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], Intent.prototype, "enableEncryption", null); __decorate([ (0, decorators_1.timedIntentFunctionCall)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], Intent.prototype, "getJoinedRooms", null); __decorate([ (0, decorators_1.timedIntentFunctionCall)(), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], Intent.prototype, "leaveRoom", null); __decorate([ (0, decorators_1.timedIntentFunctionCall)(), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], Intent.prototype, "joinRoom", null); __decorate([ (0, decorators_1.timedIntentFunctionCall)(), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String, String]), __metadata("design:returntype", Promise) ], Intent.prototype, "sendText", null); __decorate([ (0, decorators_1.timedIntentFunctionCall)(), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], Intent.prototype, "sendEvent", null); __decorate([ (0, decorators_1.timedIntentFunctionCall)(), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], Intent.prototype, "ensureRegisteredAndJoined", null); __decorate([ (0, decorators_1.timedIntentFunctionCall)(), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], Intent.prototype, "ensureJoined", null); __decorate([ (0, decorators_1.timedIntentFunctionCall)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], Intent.prototype, "refreshJoinedRooms", null); __decorate([ (0, decorators_1.timedIntentFunctionCall)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], Intent.prototype, "ensureRegistered", null); exports.Intent = Intent;