@rocket.chat/apps-engine
Version:
The engine code for the Rocket.Chat Apps which manages, runs, translates, coordinates and all of that.
436 lines (434 loc) • 20.8 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());
});
};
var __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppSlashCommandManager = void 0;
const AppStatus_1 = require("../../definition/AppStatus");
const metadata_1 = require("../../definition/metadata");
const slashcommands_1 = require("../../definition/slashcommands");
const errors_1 = require("../errors");
const Room_1 = require("../rooms/Room");
const AppSlashCommand_1 = require("./AppSlashCommand");
/**
* The command manager for the Apps.
*
* An App will add commands during their `initialize` method.
* Then once an App's `onEnable` is called and it returns true,
* only then will that App's commands be enabled.
*
* Registered means the command has been provided to the bridged system.
*/
class AppSlashCommandManager {
constructor(manager) {
this.manager = manager;
this.bridge = this.manager.getBridges().getCommandBridge();
this.accessors = this.manager.getAccessorManager();
this.touchedCommandsToApps = new Map();
this.appsTouchedCommands = new Map();
this.providedCommands = new Map();
this.modifiedCommands = new Map();
}
/**
* Checks whether an App can touch a command or not. There are only two ways an App can touch
* a command:
* 1. The command has yet to be touched
* 2. The app has already touched the command
*
* When do we consider an App touching a command? Whenever it adds, modifies,
* or removes one that it didn't provide.
*
* @param appId the app's id which to check for
* @param command the command to check about
* @returns whether or not the app can touch the command
*/
canCommandBeTouchedBy(appId, command) {
const cmd = command.toLowerCase().trim();
return cmd && (!this.touchedCommandsToApps.has(cmd) || this.touchedCommandsToApps.get(cmd) === appId);
}
/**
* Determines whether the command is already provided by an App or not.
* It is case insensitive.
*
* @param command the command to check if it exists or not
* @returns whether or not it is already provided
*/
isAlreadyDefined(command) {
const search = command.toLowerCase().trim();
let exists = false;
this.providedCommands.forEach((cmds) => {
if (cmds.has(search)) {
exists = true;
}
});
return exists;
}
/**
* Adds a command to *be* registered. This will *not register* it with the
* bridged system yet as this is only called on an App's
* `initialize` method and an App might not get enabled.
* When adding a command, it can *not* already exist in the system
* (to overwrite) and another App can *not* have already touched or provided it.
* Apps are on a first come first serve basis for providing and modifying commands.
*
* @param appId the app's id which the command belongs to
* @param command the command to add to the system
*/
addCommand(appId, command) {
return __awaiter(this, void 0, void 0, function* () {
command.command = command.command.toLowerCase().trim();
// Ensure the app can touch this command
if (!this.canCommandBeTouchedBy(appId, command.command)) {
throw new errors_1.CommandHasAlreadyBeenTouchedError(command.command);
}
// Verify the command doesn't exist already
if ((yield this.bridge.doDoesCommandExist(command.command, appId)) || this.isAlreadyDefined(command.command)) {
throw new errors_1.CommandAlreadyExistsError(command.command);
}
const app = this.manager.getOneById(appId);
if (!app) {
throw new Error('App must exist in order for a command to be added.');
}
if (!this.providedCommands.has(appId)) {
this.providedCommands.set(appId, new Map());
}
this.providedCommands.get(appId).set(command.command, new AppSlashCommand_1.AppSlashCommand(app, command));
// The app has now touched the command, so let's set it
this.setAsTouched(appId, command.command);
});
}
/**
* Modifies an existing command. The command must either be the App's
* own command or a system command. One App can not modify another
* App's command. Apps are on a first come first serve basis as to whether
* or not they can touch or provide a command. If App "A" first provides,
* or overwrites, a command then App "B" can not touch that command.
*
* @param appId the app's id of the command to modify
* @param command the modified command to replace the current one with
*/
modifyCommand(appId, command) {
return __awaiter(this, void 0, void 0, function* () {
command.command = command.command.toLowerCase().trim();
// Ensure the app can touch this command
if (!this.canCommandBeTouchedBy(appId, command.command)) {
throw new errors_1.CommandHasAlreadyBeenTouchedError(command.command);
}
const app = this.manager.getOneById(appId);
if (!app) {
throw new Error('App must exist in order to modify a command.');
}
const hasNotProvidedIt = !this.providedCommands.has(appId) || !this.providedCommands.get(appId).has(command.command);
// They haven't provided (added) it and the bridged system doesn't have it, error out
if (hasNotProvidedIt && !(yield this.bridge.doDoesCommandExist(command.command, appId))) {
throw new Error('You must first register a command before you can modify it.');
}
if (hasNotProvidedIt) {
yield this.bridge.doModifyCommand(command, appId);
const regInfo = new AppSlashCommand_1.AppSlashCommand(app, command);
regInfo.isDisabled = false;
regInfo.isEnabled = true;
regInfo.isRegistered = true;
this.modifiedCommands.set(command.command, regInfo);
}
else {
this.providedCommands.get(appId).get(command.command).slashCommand = command;
}
this.setAsTouched(appId, command.command);
});
}
/**
* Goes and enables a command in the bridged system. The command
* which is being enabled must either be the App's or a system
* command which has yet to be touched by an App.
*
* @param appId the id of the app enabling the command
* @param command the command which is being enabled
*/
enableCommand(appId, command) {
return __awaiter(this, void 0, void 0, function* () {
const cmd = command.toLowerCase().trim();
// Ensure the app can touch this command
if (!this.canCommandBeTouchedBy(appId, cmd)) {
throw new errors_1.CommandHasAlreadyBeenTouchedError(cmd);
}
// Handle if the App provided the command fist
if (this.providedCommands.has(appId) && this.providedCommands.get(appId).has(cmd)) {
const cmdInfo = this.providedCommands.get(appId).get(cmd);
// A command marked as disabled can then be "enabled" but not be registered.
// This happens when an App is not enabled and they change the status of
// command based upon a setting they provide which a User can change.
if (!cmdInfo.isRegistered) {
cmdInfo.isDisabled = false;
cmdInfo.isEnabled = true;
}
return;
}
if (!(yield this.bridge.doDoesCommandExist(cmd, appId))) {
throw new Error(`The command "${cmd}" does not exist to enable.`);
}
yield this.bridge.doEnableCommand(cmd, appId);
this.setAsTouched(appId, cmd);
});
}
/**
* Renders an existing slash command un-usable. Whether that command is provided
* by the App calling this or a command provided by the bridged system, we don't care.
* However, an App can not disable a command which has already been touched
* by another App in some way.
*
* @param appId the app's id which is disabling the command
* @param command the command to disable in the bridged system
*/
disableCommand(appId, command) {
return __awaiter(this, void 0, void 0, function* () {
const cmd = command.toLowerCase().trim();
// Ensure the app can touch this command
if (!this.canCommandBeTouchedBy(appId, cmd)) {
throw new errors_1.CommandHasAlreadyBeenTouchedError(cmd);
}
// Handle if the App provided the command fist
if (this.providedCommands.has(appId) && this.providedCommands.get(appId).has(cmd)) {
const cmdInfo = this.providedCommands.get(appId).get(cmd);
// A command marked as enabled can then be "disabled" but not yet be registered.
// This happens when an App is not enabled and they change the status of
// command based upon a setting they provide which a User can change.
if (!cmdInfo.isRegistered) {
cmdInfo.isDisabled = true;
cmdInfo.isEnabled = false;
}
return;
}
if (!(yield this.bridge.doDoesCommandExist(cmd, appId))) {
throw new Error(`The command "${cmd}" does not exist to disable.`);
}
yield this.bridge.doDisableCommand(cmd, appId);
this.setAsTouched(appId, cmd);
});
}
/**
* Registers all of the commands for the provided app inside
* of the bridged system which then enables them.
*
* @param appId The app's id of which to register it's commands with the bridged system
*/
registerCommands(appId) {
var _a, e_1, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
if (!this.providedCommands.has(appId)) {
return;
}
const commands = this.providedCommands.get(appId);
try {
for (var _d = true, commands_1 = __asyncValues(commands), commands_1_1; commands_1_1 = yield commands_1.next(), _a = commands_1_1.done, !_a; _d = true) {
_c = commands_1_1.value;
_d = false;
const [, appSlashCommand] = _c;
if (appSlashCommand.isDisabled) {
continue;
}
yield this.registerCommand(appId, appSlashCommand);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = commands_1.return)) yield _b.call(commands_1);
}
finally { if (e_1) throw e_1.error; }
}
});
}
/**
* Unregisters the commands from the system and restores the commands
* which the app modified in the system.
*
* @param appId the appId for the commands to purge
*/
unregisterCommands(appId) {
var _a, e_2, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
if (this.providedCommands.has(appId)) {
const commands = this.providedCommands.get(appId);
try {
for (var _d = true, commands_2 = __asyncValues(commands), commands_2_1; commands_2_1 = yield commands_2.next(), _a = commands_2_1.done, !_a; _d = true) {
_c = commands_2_1.value;
_d = false;
const [, appSlashCommand] = _c;
const cmd = appSlashCommand.slashCommand.command;
yield this.bridge.doUnregisterCommand(cmd, appId);
this.touchedCommandsToApps.delete(cmd);
if (!this.appsTouchedCommands.has(appId)) {
continue;
}
const ind = this.appsTouchedCommands.get(appId).indexOf(cmd);
this.appsTouchedCommands.get(appId).splice(ind, 1);
appSlashCommand.isRegistered = true;
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (!_d && !_a && (_b = commands_2.return)) yield _b.call(commands_2);
}
finally { if (e_2) throw e_2.error; }
}
this.providedCommands.delete(appId);
}
if (this.appsTouchedCommands.has(appId)) {
// The commands inside the appsTouchedCommands should now
// only be the ones which the App has enabled, disabled, or modified.
// We call restore to enable the commands provided by the bridged system
// or unmodify the commands modified by the App
this.appsTouchedCommands.get(appId).forEach((cmd) => {
// @NOTE this "restore" method isn't present in the bridge
// this.bridge.doRestoreCommand(cmd, appId);
this.modifiedCommands.get(cmd).isRegistered = false;
this.modifiedCommands.delete(cmd);
this.touchedCommandsToApps.delete(cmd);
});
this.appsTouchedCommands.delete(appId);
}
});
}
/**
* Executes an App's command.
*
* @param command the command to execute
* @param context the context in which the command was entered
*/
executeCommand(command, context) {
return __awaiter(this, void 0, void 0, function* () {
const cmd = command.toLowerCase().trim();
if (!this.shouldCommandFunctionsRun(cmd)) {
return;
}
const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd));
if (!app || AppStatus_1.AppStatusUtils.isDisabled(yield app.getStatus())) {
// Just in case someone decides to do something they shouldn't
// let's ensure the app actually exists
return;
}
const appCmd = this.retrieveCommandInfo(cmd, app.getID());
yield appCmd.runExecutorOrPreviewer(metadata_1.AppMethod._COMMAND_EXECUTOR, this.ensureContext(context), this.manager.getLogStorage(), this.accessors);
});
}
getPreviews(command, context) {
return __awaiter(this, void 0, void 0, function* () {
const cmd = command.toLowerCase().trim();
if (!this.shouldCommandFunctionsRun(cmd)) {
return;
}
const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd));
if (!app || AppStatus_1.AppStatusUtils.isDisabled(yield app.getStatus())) {
// Just in case someone decides to do something they shouldn't
// let's ensure the app actually exists
return;
}
const appCmd = this.retrieveCommandInfo(cmd, app.getID());
const result = yield appCmd.runExecutorOrPreviewer(metadata_1.AppMethod._COMMAND_PREVIEWER, this.ensureContext(context), this.manager.getLogStorage(), this.accessors);
if (!result) {
// Failed to get the preview, thus returning is fine
return;
}
return result;
});
}
executePreview(command, previewItem, context) {
return __awaiter(this, void 0, void 0, function* () {
const cmd = command.toLowerCase().trim();
if (!this.shouldCommandFunctionsRun(cmd)) {
return;
}
const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd));
if (!app || AppStatus_1.AppStatusUtils.isDisabled(yield app.getStatus())) {
// Just in case someone decides to do something they shouldn't
// let's ensure the app actually exists
return;
}
const appCmd = this.retrieveCommandInfo(cmd, app.getID());
yield appCmd.runPreviewExecutor(previewItem, this.ensureContext(context), this.manager.getLogStorage(), this.accessors);
});
}
ensureContext(context) {
// Due to the internal changes for the usernames property, we need to ensure the room
// is a class and not just an interface
let room;
if (context.getRoom() instanceof Room_1.Room) {
room = context.getRoom();
}
else {
room = new Room_1.Room(context.getRoom(), this.manager);
}
return new slashcommands_1.SlashCommandContext(context.getSender(), room, context.getArguments(), context.getThreadId(), context.getTriggerId());
}
/**
* Determines if the command's functions should run,
* this way the code isn't duplicated three times.
*
* @param command the lowercase and trimmed command
* @returns whether or not to continue
*/
shouldCommandFunctionsRun(command) {
// None of the Apps have touched the command to execute,
// thus we don't care so exit out
if (!this.touchedCommandsToApps.has(command)) {
return false;
}
const appId = this.touchedCommandsToApps.get(command);
const cmdInfo = this.retrieveCommandInfo(command, appId);
// Should the command information really not exist
// Or if the command hasn't been registered
// Or the command is disabled on our side
// then let's not execute it, as the App probably doesn't want it yet
if (!cmdInfo || !cmdInfo.isRegistered || cmdInfo.isDisabled) {
return false;
}
return true;
}
retrieveCommandInfo(command, appId) {
return this.modifiedCommands.get(command) || this.providedCommands.get(appId).get(command);
}
/**
* Sets that an App has been touched.
*
* @param appId the app's id which has touched the command
* @param command the command, lowercase and trimmed, which has been touched
*/
setAsTouched(appId, command) {
if (!this.appsTouchedCommands.has(appId)) {
this.appsTouchedCommands.set(appId, []);
}
if (!this.appsTouchedCommands.get(appId).includes(command)) {
this.appsTouchedCommands.get(appId).push(command);
}
this.touchedCommandsToApps.set(command, appId);
}
/**
* Actually goes and provide's the bridged system with the command information.
*
* @param appId the app which is providing the command
* @param info the command's registration information
*/
registerCommand(appId, info) {
return __awaiter(this, void 0, void 0, function* () {
yield this.bridge.doRegisterCommand(info.slashCommand, appId);
info.hasBeenRegistered();
});
}
}
exports.AppSlashCommandManager = AppSlashCommandManager;
//# sourceMappingURL=AppSlashCommandManager.js.map