tennu
Version:
Tennu - Modular IRC Bot Framework for Node.js
249 lines (201 loc) • 10 kB
JavaScript
const inspect = require("util").inspect;
const format = require("util").format;
const create = require("lodash.create");
const Promise = require("bluebird");
const badResponseFormat = "Command handler for %s returned with invalid value: %s";
function Command (privmsg, command_text) {
const args = command_text.split(/ +/);
const commandName = args.shift().toLowerCase();
return create(privmsg, {
args: args,
command: commandName
});
}
function startsWith(string, prefix) {
return string.slice(0, prefix.length) === prefix;
}
module.exports = {
name: "commands",
configDefaults: {
"command-trigger": "!",
"command-ignore-list": []
},
init: function (client, deps) {
const trigger = client.config("command-trigger");
const ignoreList = client.config("command-ignore-list");
client.debug("ignoreList is " + require("util").inspect(ignoreList));
const isValidIgnorableCommand = function (ignoredCommand) {
return typeof ignoredCommand === "string" ||
(Array.isArray(ignoredCommand) &&
ignoredCommand.every(function (igoredCommand) { return typeof igoredCommand === "string"; }));
};
if (!ignoreList.every(isValidIgnorableCommand)) {
throw new Error("Invalid command-ignore-list configuration option value. " +
"Must either be a string or an array of multiple strings where the first value " +
"is the command to ignore and the rest are plugins to ignore it from.");
}
const ignoreCommandInSpecificPluginList = ignoreList
.filter(Array.isArray)
.map(function (commandPluginsList) {
return commandPluginsList.map(function (commandPlugin) { return commandPlugin.toLowerCase(); });
});
const globalIgnoreItems = ignoreList
.filter(function (ignoreItem) { return typeof ignoreItem === "string"; })
.map(function (ignoreItem) { return ignoreItem.toLowerCase(); });
// invariant: keys must be normalized to lower case.
const registry = {};
// Pushed to by the `commandMiddleware` hook; Read by the `privmsg` handler.
const middleware = [];
/// Returns false if privmsg is *not* a command query.
/// Otherwise, returns the string that is the command query.
/// e.g. "commandname arg1 arg2 ..."
function tryParseCommandString (privmsg) {
function removeTrigger (string) {
return string.slice(trigger.length);
}
// Message is a CTCP (probably)
// BTW, This prevents actions from being commands.
if (privmsg.message[0] === "\u0001") {
return false;
}
// Message starts with the command trigger.
if (startsWith(privmsg.message, trigger)) {
return removeTrigger(privmsg.message);
}
// Message is in a query.
if (privmsg.isQuery) {
return privmsg.message;
}
// Message starts with my nickname.
if (startsWith(privmsg.message.toLowerCase(), client.nickname().toLowerCase())) {
// Trimming in case of multiple spaces. e.g. (raw message)
// nick!user@host PRIVMSG #chan botname: do something
const message = privmsg.message.slice(privmsg.message.indexOf(" ") + 1).trim();
return startsWith(message, trigger) ? removeTrigger(message) : message;
}
return false;
};
function handleCommand(privmsg, commandText) {
const command = Command(privmsg, commandText);
client.note("PluginCommands", format("Command detected: %s", command.command));
const promiseOfCommandOrResponse = middleware.reduce(function (promiseOfCommandOrResponse, ware) {
return promiseOfCommandOrResponse.then(function (commandOrResponse) {
if (commandOrResponse === command) {
return ware(commandOrResponse);
} else {
return commandOrResponse;
}
});
}, Promise.resolve(command));
return promiseOfCommandOrResponse.then(function (commandOrResponse) {
if (commandOrResponse !== command) {
// It's a Response.
client.note("PluginCommands", "Command handled by middleware.");
return commandOrResponse;
}
// It's still the same command as above. We can continue using that variable.
const commandName = command.command;
client.note("PluginCommands", format("Command after middleware: %s", commandName));
const maybeCommandRegistryEntry = registry[commandName];
if (!maybeCommandRegistryEntry) {
client.note("PluginCommands", format("Handler for '%s' not found.", commandName));
return;
}
const commandRegistryEntry = maybeCommandRegistryEntry;
const handler = commandRegistryEntry.handler;
const plugin = commandRegistryEntry.plugin;
client.note("PluginCommands", format("Handler '%s:%s' found.", plugin, commandName));
return handler(command);
});
}
return {
handlers: {
"privmsg": function (privmsg) {
// TODO(Havvy): use R-Result to simplify.
const maybeCommandText = tryParseCommandString(privmsg);
if (!maybeCommandText) {
return;
}
return handleCommand(privmsg, maybeCommandText);
}
},
subscribe: {
prefix: "!",
acceptsMetadata: true,
emitter: {
on: function (commandName, handler, metadata) {
commandName = commandName.toLowerCase();
var plugin;
if (metadata && metadata.plugin) {
plugin = metadata.plugin;
} else {
plugin = "UnknownSource";
}
if (globalIgnoreItems.indexOf(commandName) !== -1) {
client.note("PluginCommands", format("Ignoring '%s:%s (global)'.", plugin, commandName));
return;
}
var ignoreMatchFound = ignoreCommandInSpecificPluginList.some(function (element, index, array) {
if (element[0] === commandName && element.slice(1, element.length).indexOf(plugin) !== -1) {
client.note("PluginCommands", format("Ignoring '%s:%s'.", plugin, commandName));
return true;
}
});
if (ignoreMatchFound) {
return;
}
if (commandName in registry) {
const errorMessage = format("Command '%s' already has a handler from plugin '%s'.", commandName, registry[commandName].plugin);
client.error("PluginCommands", errorMessage);
throw new Error(errorMessage);
}
client.note("PluginCommands", format("Registering '%s:%s'.", plugin, commandName));
registry[commandName] = {
handler: handler,
plugin: plugin
};
},
off: function (commandName) {
if (commandName in registry) {
delete registry[commandName];
} else {
const error = format("Cannot remove command handler '%s', as there is no such command handler.", commandName);
client.error(error);
throw new Error(error);
}
},
once: function () {
throw new Error("Cannot only listen to a command once.");
}
}
},
hooks: {
commandMiddleware: function (plugin, ware) {
if (typeof ware !== "function") {
const error = format("Plugin %s tried to add non-function middleware.", plugin);
client.error(error);
throw new Error(error);
}
middleware.push(ware);
}
},
exports: {
isCommand: function (message) {
return tryParseCommandString(message) !== false;
},
isHandledCommand: function (message) {
const maybeCommand = tryParseCommandString(message);
if (!maybeCommand) {
return false;
}
const command = Command(message, maybeCommand);
const commandName = command.command;
const commandRegistryEntry = registry[command.command];
return commandRegistryEntry !== undefined;
},
handleCommand: handleCommand
}
};
},
requires: ["subscriber", "messages", "self"]
};