tennu
Version:
Tennu - Modular IRC Bot Framework for Node.js
121 lines (101 loc) • 4.57 kB
JavaScript
//! This plugin implements basic CTCP functionality for Tennu IRC bots.
//!
//! CTCP, or Client to Client Protocol, is a protocol that tunnels over IRC
//! for letting clients query information, and also for actions (/me). See
//! http://modern.ircdocs.horse/ctcp.html for more information about what
//! CTCP is, and how it is implemented by other clients.
//!
//! We implement the following CTCP messages unconditionally:
//! VERSION, SOURCE, CLIENTINFO, PING
//! Of note, we do not implement TIME since knowing the server time for
//! an IRC bot is not useful information to share, and UTC time is shared
//! between everybody.
//!
//! We also allow other plugins to create their own response to specific
//! CTCP requests. They must add a handler of `ctcp:handler-name` that
//! is given a `Ctcp` object and must return a string response or undefined.
//!
//! The `Ctcp` object is similar to a `Command` in that it's a `Privmsg`
//! but has `args` and `command` properties.
const format = require("util").format;
const packageMetadata = require("../package.json");
const create = require("lodash.create");
const isCtcp = function (message) {
return message[0] === "\u0001" && message[message.length - 1] === "\u0001";
};
function Ctcp (privmsg) {
const args = privmsg.message.slice(1, -1).split(/ +/);
const commandName = args.shift().toLowerCase();
return create(privmsg, {
args: args,
command: commandName
});
}
module.exports = {
init: function (client, deps) {
// invariant: keys must be normalized to uppercase.
const registry = {};
// We add base CTCP commands here instead of in the handlers
// because there is no guarantee of the order that `handlers` and
// `subscribe` are handled, so it's possible that `handlers` gets
// handled before `subscribe`, and the `ctcp:` prefix isn't actually
// available yet.
registry.CLIENTINFO = function (_ctcp) {
// You should notice that this behaviour can only be implemented
// in this plugin, unlike the rest of the CTCPs.
return Object.keys(registry).join(" ");
};
registry.VERSION = function (_ctcp) {
return format("Tennu %s", packageMetadata["version"]);
};
registry.SOURCE = function (ctcp) {
return "https://github.com/tennu/tennu | Docs: https://tennu.github.io"
};
registry.PING = function (ctcp) {
// Example message: "\u{1}PING Part to copy here\u{1}"
// We want to send back the "Part to copy here", so have to slice off
// the first 6 characters and the last character.
// We can't just do `ctcp.args.join(" ") because the part to copy
// could have multiple spaces in it.
return ctcp.message.slice(6, -1);
}
return {
handlers: {
"privmsg": function (privmsg) {
if (!isCtcp(privmsg.message)) {
return;
}
const ctcp = Ctcp(privmsg);
client.note(format("CTCP message '%s' detected.", ctcp.command));
if (ctcp.command.toUpperCase() in registry) {
const response = registry[ctcp.command.toUpperCase()](ctcp);
if (response) {
client.ctcpRespond(privmsg.nickname, ctcp.command, response);
}
} else {
client.note(format("Unknown CTCP request '%s'.", ctcp.command));
}
}
},
subscribe: {
prefix: "ctcp:",
emitter: {
on: function (ctcpName, handler) {
ctcpName = ctcpName.toUpperCase();
if (ctcpName in registry) {
throw new Error(format("CTCP Command '%s' already has a handler.", ctcpName));
}
registry[ctcpName] = handler;
},
off: function () {
throw new Error("Cannot remove CTCP command handlers once attached.");
},
once: function () {
throw new Error("Cannot only listen to a CTCP command once.");
}
}
}
};
},
requires: ["subscriber", "messages", "action"]
};