const Command = require('./command.js');
const FileSystemUtils = require('./util/file_system_utils.js');
const HelpCommandHelper = require('./help_command_helper.js');
const Constants = require('./constants.js');
const SettingsConverters = require('./settings_converters.js');
const SettingsValidators = require('./settings_validators.js');
const assert = require('assert');
const handleError = require('./handle_error.js');
const COMMAND_CATEGORY_NAME = 'Enabled commands';
const DISABLED_COMMANDS_FAIL_SILENTLY_SETTING_NAME = 'Disabled commands fail silently';
const PREFIXES_SETTING_NAME = 'Command prefixes';
const PREFIXES_SETTING_UNIQUE_ID = 'prefixes';
function getDuplicateAlias(command, otherCommands) {
for (let alias of command.aliases) {
if (otherCommands.find(cmd => cmd.aliases.indexOf(alias) !== -1)) {
return alias;
}
}
}
function createSettingsForCommands(userCommands) {
return userCommands
.map(command => command.createEnabledSetting())
.filter(setting => !!setting);
}
function savePrefixes(persistence, settingUniqueId, serverId, channelId, userId, newInternalValue) {
return persistence.editPrefixesForServerId(serverId, newInternalValue);
}
function getPrefixes(persistence, setting, serverId) {
return persistence.getPrefixesForServer(serverId);
}
function createPrefixesSetting(defaultPrefixes) {
return {
userFacingName: PREFIXES_SETTING_NAME,
description: 'This setting controls what command prefix(es) I will respond to.',
defaultUserFacingValue: defaultPrefixes.join(' '),
allowedValuesDescription: 'A **space separated** list of prefixes',
uniqueId: PREFIXES_SETTING_UNIQUE_ID,
serverSetting: true,
userSetting: false,
channelSetting: false,
requireConfirmation: true,
convertUserFacingValueToInternalValue: SettingsConverters.createStringToStringArrayConverter(' '),
convertInternalValueToUserFacingValue: SettingsConverters.createStringArrayToStringConverter(' '),
validateInternalValue: SettingsValidators.isStringArray,
updateSetting: savePrefixes,
getInternalSettingValue: getPrefixes,
};
}
function createDisabledCommandsFailSilentlySetting() {
return {
userFacingName: DISABLED_COMMANDS_FAIL_SILENTLY_SETTING_NAME,
description: 'If this setting is true, then I will do nothing when a user tries to use a disabled command. If this setting is false, then when a user tries to use a disabled command I will tell them that it\'s disabled.',
defaultUserFacingValue: 'Disabled',
allowedValuesDescription: '**Enabled** or **Disabled**',
uniqueId: Constants.DISABLED_COMMANDS_FAIL_SILENTLY_SETTING_ID,
serverSetting: true,
channelSetting: true,
userSetting: false,
convertUserFacingValueToInternalValue: SettingsConverters.createStringToBooleanConverter('enabled', 'disabled'),
convertInternalValueToUserFacingValue: SettingsConverters.createBooleanToStringConverter('Enabled', 'Disabled'),
validateInternalValue: SettingsValidators.isBoolean,
};
}
function createSettingsCategoryForCommands(userCommands) {
let children = createSettingsForCommands(userCommands);
if (!children || children.length === 0) {
return;
}
children.push(createDisabledCommandsFailSilentlySetting());
return {
userFacingName: COMMAND_CATEGORY_NAME,
children: children,
};
}
/**
* Responsible for delegating messages to command handlers.
* The CommandManager can be accessed via {@link Monochrome#getCommandManager}.
* @hideconstructor
*/
class CommandManager {
constructor(directory, prefixes, monochrome) {
this.monochrome_ = monochrome;
this.commands_ = [];
this.directory_ = directory;
this.prefixes_ = prefixes;
this.persistence_ = monochrome.getPersistence();
this.logger = monochrome.getLogger().child({
component: 'Monochrome::CommandManager',
});
}
/**
* Get the HelpCommandHelper which provides assistance for creating a help command.
* @returns {HelpCommandHelper}
*/
getHelpCommandHelper() {
assert(this.helpCommandHelper_, 'Help command helper not available');
return this.helpCommandHelper_;
}
load() {
this.commands_ = [];
if (this.directory_) {
const commandFiles = FileSystemUtils.getFilesInDirectory(this.directory_);
for (let commandFile of commandFiles) {
try {
let newCommandData = require(commandFile);
let newCommand = new Command(newCommandData, this.monochrome_);
if (this.commands_.find(existingCommand => existingCommand.uniqueId === newCommand.uniqueId)) {
throw new Error(`There is another command with the same uniqueId`);
}
let duplicateAlias = getDuplicateAlias(newCommand, this.commands_);
if (duplicateAlias) {
throw new Error(`There is another command that also has the alias: ${duplicateAlias}`);
}
this.commands_.push(newCommand);
} catch (err) {
this.logger.error({
event: 'FAILED TO LOAD COMMAND',
detail: commandFile,
err,
});
}
}
}
this.helpCommandHelper_ = new HelpCommandHelper(this.commands_, this.monochrome_.getSettings(), this.monochrome_.getPersistence());
const settingsCategory = createSettingsCategoryForCommands(this.commands_);
this.monochrome_.getSettings().addNodeToRoot(settingsCategory);
if (this.prefixes_ && (this.prefixes_.length > 1 || !!this.prefixes_[0])) {
const prefixesSetting = createPrefixesSetting(this.prefixes_);
this.monochrome_.getSettings().addNodeToRoot(prefixesSetting);
}
}
loadInteractions() {
const interactions = this.commands_.map(command => command.createInteraction()).filter(Boolean);
if (interactions.length > 0) {
const eris = this.monochrome_.getErisBot();
eris.bulkEditCommands(interactions).catch((err) => {
this.logger.error({
event: 'FAILED TO LOAD INTERACTIONS',
err,
});
});
}
}
createFakeSuffix(interation) {
return interation.data.options?.map(op => {
if (op.type === 5) {
return op.value ? op.name : '';
}
return op.value;
}).filter(Boolean).join(' ') ?? '';
}
async processInteraction(bot, interaction) {
try {
interaction.prefix = this.monochrome_.getPersistence().getPrimaryPrefixForMessage(interaction);
interaction.isInteraction = true;
const commandToExecute = this.commands_.find(
command => command.interaction && command.aliases[0] === interaction.data.name
);
const compatibilityMode = commandToExecute.interactionCompatibilityMode();
let suffix = undefined;
const focusedOption = interaction.data.options?.find(d => d.focused);
const isAutoCompleteInteraction = interaction.type === 4;
if (compatibilityMode) {
suffix = this.createFakeSuffix(interaction);
let initialMessageSent = false;
interaction.channel.createMessage = (...args) => {
if (isAutoCompleteInteraction) {
return;
}
if (!initialMessageSent) {
initialMessageSent = true;
return interaction.editOriginalMessage(...args);
}
return bot.createMessage(interaction.channel.id, ...args);
};
}
if (isAutoCompleteInteraction) {
const choices = await commandToExecute.autoCompleteInteraction(bot, interaction, focusedOption);
await interaction.result(choices);
} else {
await interaction.acknowledge();
await commandToExecute.handle(bot, interaction, suffix);
this.logger.info({
event: 'INTERACTION EXECUTED',
commandId: commandToExecute.uniqueId,
message: interaction,
detail: commandToExecute.uniqueId,
});
}
} catch (err) {
handleError(this.logger, 'INTERACTION ERROR', this.monochrome_, interaction, err, false);
}
}
processInput(bot, msg) {
let serverId = msg.channel.guild ? msg.channel.guild.id : msg.channel.id;
let prefixes = this.persistence_.getPrefixesForServer(serverId);
let msgContent = msg.content;
msgContent = msgContent.replace('\u3000', ' ');
let spaceIndex = msgContent.indexOf(' ');
let commandText = '';
if (spaceIndex === -1) {
commandText = msgContent;
} else {
commandText = msgContent.substring(0, spaceIndex);
}
commandText = commandText.toLowerCase();
for (let prefix of prefixes) {
for (let command of this.commands_) {
for (let alias of command.aliases) {
const prefixedAlias = prefix + alias;
if (commandText === prefixedAlias) {
return this.executeCommand_(bot, msg, command, msgContent, spaceIndex, prefix);
}
if (command.canHandleExtension && commandText.startsWith(prefixedAlias)) {
let extension = commandText.replace(prefixedAlias, '');
if (command.canHandleExtension(extension)) {
return this.executeCommand_(bot, msg, command, msgContent, spaceIndex, prefix, extension);
}
}
}
}
}
return false;
}
async executeCommand_(bot, msg, commandToExecute, msgContent, spaceIndex, prefix, extension) {
msg.prefix = prefix;
msg.extension = extension;
let suffix = '';
if (spaceIndex !== -1) {
suffix = msgContent.substring(spaceIndex + 1).trim();
}
try {
await commandToExecute.handle(bot, msg, suffix);
this.logger.info({
event: 'COMMAND EXECUTED',
commandId: commandToExecute.uniqueId,
message: msg,
detail: commandToExecute.uniqueId,
});
} catch (err) {
handleError(this.logger, 'COMMAND ERROR', this.monochrome_, msg, err, false);
}
return commandToExecute;
}
}
module.exports = CommandManager;