diff --git a/bin/cli.js b/bin/cli.js index 362c28c..aaec488 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -3,13 +3,18 @@ var program = require('commander'); var Client = require('../lib/client.js'); var fs = require('fs'); +var inspect = require('util').inspect; +var format = require('util').format; program .version('0.7.0') .usage('[options] ') .option('-v, --verbose', 'Log to standard out') + .option('-d, --debug', 'Log debug messages. Requires -v') .parse(process.argv); + +// Get the configuration. var config_path = program.args[0]; if (!config_path) { @@ -22,30 +27,47 @@ config_path = process.cwd() + '/' + config_path; try { var config = fs.readFileSync(config_path, {encoding: 'utf-8'}); } catch (e) { - console.log("Error detected!"); - console.log(e); + console.log("Unknown Error detected!"); + console.log(); + console.log(e.stack); process.exit(2); } try { config = JSON.parse(config) } catch (e) { - console.log(e); + console.log("Failed to parse configuration file."); + console.log(); + console.log(e.stack); process.exit(3); } -var di = {}; +// Say what you're about to do (if -v) +if (program.verbose) { + console.log(format("Connecting to %s:%d", config.server, config.port)); +} + +// Create the dependency management object. +var parts = {}; if (program.verbose) { var log = function (level) { - return function (line) { - console.log(String(Date()), level, line); + return function () { + var args = Array.prototype.slice.call(arguments) + .map(function (arg) { + if (typeof arg === 'object') { + return inspect(arg); + } else { + return String(arg); + } + }); + console.log(String(Date()), level, args.join(" ")); }; }; var Logger = function () { return { - debug: function () {}, + debug: program.debug ? log('debug') : function () {}, info: log('info'), notice: log('notice'), warn: log('warn'), @@ -56,14 +78,27 @@ if (program.verbose) { }; }; - di.Logger = Logger; + parts.Logger = Logger; } +// Try to connect, or print why it couldn't. try { - var client = Client(config, di); + var client = Client(config, parts); client.connect(); } catch (e) { console.log("Error occurred creating and connecting to Tennu instance."); - console.log(e); + console.log(); + console.log(e.stack); process.exit(4); -} \ No newline at end of file +} + +// Register hangup functions +var onabort = function () { + client.quit("Bot terminated."); +}; + +process.on('SIGHUP', onabort); +process.on('SIGINT', onabort); +process.on('SIGQUIT', onabort); +process.on('SIGABRT', onabort); +process.on('SIGTERM', onabort); \ No newline at end of file diff --git a/lib/client.js b/lib/client.js index 4995be2..a344f4e 100644 --- a/lib/client.js +++ b/lib/client.js @@ -68,8 +68,6 @@ var delegate = function (property, method) { client._config = config = Object.freeze(lodash.defaults({}, config, defaultClientConfiguration)); di = lodash.defaults({}, dependencies || {}, defaultFactoryConfiguration); - console.log(config.server, config.port); - // Create a logger. // Default logger is a bunch of NOOPs. client._logger = new di.Logger(); @@ -83,7 +81,7 @@ var delegate = function (property, method) { // Create the listener to the socket. // This listener will parse the raw messages of the socket, and // emits specific events to listen to. - var messageParser = new di.MessageParser(client, client._socket, client._logger); + var messageParser = new di.MessageParser(client, client._logger, client._socket); // Create the listener to private messages from the IRCMessageEmitter // The commander will parse these private messages for commands, and @@ -95,7 +93,7 @@ var delegate = function (property, method) { // determining whether they should be handled by the IrcMessageEmitter // or the Command Parser. client._subscriber = new di.BiSubscriber(messageParser, commandParser); - client._subscriber.on("privmsg", commandParser.parse.bind(commandParser)); + client._subscriber.on("privmsg", function (privmsg) { commandParser.parse(privmsg); }); // And finally, the module system. client._modules = new di.Modules(client._subscriber, client); diff --git a/lib/command-parser.js b/lib/command-parser.js index f710ad2..a42e31b 100644 --- a/lib/command-parser.js +++ b/lib/command-parser.js @@ -59,12 +59,13 @@ function CommandParser (config, nickname, logger) { parser.after(function (err, toSay, type, command) { if (err) { - logger.error("Error thrown in command handler: ", err); + logger.error("Error thrown in command handler!"); + logger.error(err.stack); return; - } + } if (Array.isArray(toSay) || typeof toSay === "string") { - receiver.say(command.channel, toSay);; + command.receiver.say(command.channel, toSay);; } else if (toSay !== undefined) { logger.error("Listener returned with non-string/non-array value: ", toSay); } diff --git a/lib/message-parser.js b/lib/message-parser.js index 0a279fb..406120f 100644 --- a/lib/message-parser.js +++ b/lib/message-parser.js @@ -39,11 +39,17 @@ var util = require('util'); var EventEmitter = require('./event-emitter'); var Message = require('./message'); -var MessageParser = function MP (receiver, socket, logger) { +var MessageParser = function MP (receiver, logger, socket) { var parser = Object.create(EventEmitter()); parser.parse = function (raw) { var message = new Message(raw, receiver); + + if (message === null) { + logger.error("Raw message given was not a valid IRC message!", raw); + return null; + } + this.emit(message.command.toLowerCase(), message); this.emit("*", message); return message; @@ -61,7 +67,7 @@ var MessageParser = function MP (receiver, socket, logger) { if (err) { logger.error("Error thrown in message handler: ", err); return; - } + } if (Array.isArray(toSay) || typeof toSay === "string") { receiver.say(message.channel, toSay);; diff --git a/lib/message.js b/lib/message.js index d8a11ba..55a476e 100644 --- a/lib/message.js +++ b/lib/message.js @@ -12,8 +12,6 @@ var extensions = { }, kick: function (message) { - // :metalbot!metalbot@coldfront-9CEAA055.austin.res.rr.com KICK #hb Havvy-kickme :metalbot - message.channel = message.params[0].toLowerCase(); message.kicked = message.params[1]; message.kicker = message.params[2]; @@ -59,10 +57,10 @@ var Message = function (raw, receiver) { message.receiver = receiver; message.command = message.command.toLowerCase(); - if (message.prefixIsHostmask()) { - message.hostmask = message.parseHostmaskFromPrefix(); - message.nickname = message.hostmask.nickname; - } + // message.hostmask is either null or an object with + // nickname, username, hostname properties. + message.hostmask = message.parseHostmaskFromPrefix(); + message.nickname = message.hostmask && message.hostmask.nickname; if (extensions[message.command]) { extensions[message.command](message); diff --git a/lib/output-socket.js b/lib/output-socket.js index f3b188a..1ba34fc 100644 --- a/lib/output-socket.js +++ b/lib/output-socket.js @@ -22,6 +22,7 @@ This will be fixed in 0.8.x. */ var util = require('util'); +var format = util.format; var partition = function (array, length) { var partitions = []; @@ -33,31 +34,36 @@ var partition = function (array, length) { var OutputSocket = function (socket, logger, nick) { var raw = function (line) { - logger.info("->: " + Array.isArray(line) ? line.join(" ") : String(line)); + if (Array.isArray(line)) { line = line.join(" "); } + logger.info("->: " + String(line)); socket.raw(line); }; + var rawf = function () { + raw(format.apply(null, arguments)); + }; + return { - say : function (location, message) { + say : function recur (location, message) { if (util.isArray(message)) { message.forEach(function (msg) { - say.call(this, location, msg); + recur.call(this, location, msg); }); return; } - raw(["PRIVMSG", location, ":" + message]); + rawf("PRIVMSG %s :%s", location, message); }, - ctcp : function (location, type, message) { + ctcp : function recur (location, type, message) { if (util.isArray(message)) { message.forEach(function (msg) { - ctcp.call(this, location, type, msg); + recur.call(this, location, type, msg); }); return; } - this.say(location, '\u0001' + type + " " + message + '\u0001'); + this.say(location, format('\u0001%s %s\u0001', type, message)); }, act: function (location, message) { @@ -65,16 +71,16 @@ var OutputSocket = function (socket, logger, nick) { }, join : function (channel) { - raw(["JOIN", channel]); + rawf("JOIN :%s", channel); }, part : function (channel, reason) { - raw("PART "+ channel + (reason ? " :" + reason : '')); + raw("PART " + channel + (reason ? " :" + reason : "")); }, nick : function (newNick) { if (newNick) { - raw("NICK " + newNick); + rawf("NICK %s", newNick); nick = newNick; return; } else { @@ -83,6 +89,7 @@ var OutputSocket = function (socket, logger, nick) { }, quit : function (reason) { + logger.notice(format("Quitting with reason: %s", reason)); raw("QUIT" + (reason ? " :" + reason : "")); }, @@ -104,34 +111,35 @@ var OutputSocket = function (socket, logger, nick) { raw(["MODE", target, args]); }, - userhost : function userhost (users) { + userhost : function recur (users) { if (typeof users === 'string') { - raw("USERHOST " + users); + rawf("USERHOST :%s", users); } else if (typeof users === 'array') { partition(users, 5) .map(function (hosts) { return hosts.join(' '); }) - .map(userhost); + .map(recur); } else { throw new Error("Userhost command takes either a string (a single nick) or an array (of string nicks)"); } }, - whois : function whois (users, server) { + whois : function recur (users, server) { if (typeof users === "array") { if (users.length > 15) { partition(users, 15) .map(function (users) { return users.join(','); }) - .map(function (users) { whois(users, server); }); + .map(function (users) { recur(users, server); }); } } else if (typeof users === 'string') { - raw("WHOIS " + server ? server + " " : "" + users); + raw("WHOIS " + (server ? server + " " : "") + users); } else { throw new Error("Whois command takes either a string (a single nick) or an array (of string nicks)"); } }, _raw : raw, - toString : require('./make-toString')('OutputSocket') + _rawf : rawf, + toString : function () { return "[Object IrcOutputSocket]"; } }; }; diff --git a/package.json b/package.json index ec7171d..c9fdc0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tennu", - "version": "0.7.0", + "version": "0.7.1", "description": "Tennu Node.js IRC Framework", "maintainers": [ { diff --git a/readme.md b/readme.md index 175e375..b30f1ab 100644 --- a/readme.md +++ b/readme.md @@ -41,22 +41,23 @@ myClient.connect(); A network configuration object has the following properties: * server - IRC server to connect to. _Example:_ _irc.mibbit.net_ +* port - Port to connect to. Defaults to 6667. +* capab - IRC3 CAP support. (Untested) +* secure - Use a TLS socket (Throws away the NetSocket) * nick - Nickname the bot will use. Defaults to "tennubot" * user - Username the bot will use. Defaults to "user" * realname - Realname for the bot. Defaults to "tennu v0.3" -* port - Port to connect to. Defaults to 6667. * password - Password for identifying to services. -* nickserv - Nickname for nickserv service. Defaults to "nickserv". +* nickserv - Nickname of nickserv service. Defaults to "nickserv". * trigger - Command character to trigger commands with. By default, '!'. * channels - Array of channels to autojoin. _Example:_ ["#help", "#tennu"] * modules - An array of module names that the bot requires. -* capab - IRC3 CAP support. (Untested) -* secure - Use a TLS socket (Throws away the NetSocket) -Static network configuration objects can go in _./config/%NETWORK%.json_ -(relative to your project) and then required in via node. +Other modules may use additional properties. -## Dependency Injection ## +Network configuration objects are JSON encodable. + +## Dependency Management ## The second (optional) parameter to tennu.Client is an object of factories to replace the factories that the Client uses by default. @@ -91,6 +92,8 @@ Note: Tennu uses a custom event handler. Listeners are placed at the end of the insead of happening right away. Errors are currently logged to console, but otherwise swallowed. +### Respond Functionality ### + Commands and Messages that have a channel property take a return value. Currently, the return value must be a string or array that is then said to the channel the message originated in. @@ -100,8 +103,15 @@ originated in. tennu.on('privmsg', function (privmsg) { return privmsg.message; }); + +// Equivalent to: +tennu.on('privmsg', function (privmsg) { + tennu.say(privmsg.channel, privmsg.message); +}); ``` +### Subscribing Options ### + Subscribing to events in Tennu is more flexible than most event listeners. You register a single handler on multiple events at once by separating the events with a space, @@ -121,9 +131,11 @@ on({ }) ``` +### Listener Parameters ### + Listeners are passed either a message or command object. -### Message ### +#### Message #### Messages are passed by irc events. @@ -137,7 +149,7 @@ All messages have the following fields: * command - Message command type. For example, 'privmsg' or 'nick'. * params - Array of sent parameters. -#### Extensions #### +##### Extensions ##### Note: Only the following message command types have extensions: join, kick, notice, part, privmsg, nick, quit @@ -155,7 +167,7 @@ The kick message has the properties 'kicked' and 'kicker'. Note: This is a weak part of the framework. If you want to contribute to Tennu, this is an easy and helpful place to make Tennu more useful. -### Command ### +#### Command #### Commands are passed for user commands. @@ -175,14 +187,6 @@ All of the following are methods on Tennu for doing things once connected. These methods are also available on the client's 'out' property. -### join(channel) ### - -Joins the specified channel. - -### part(channel, reason) ### - -Parts the specified channel with the given reason. - ### say(channel, message) ### * channel is either a channel ("#chan") or a user ("nick"). @@ -215,11 +219,34 @@ botnick does something! tennu.act('#example', "does something!"); ``` +### ctcp(channel, type, message) ### + +```javascript +tennu.ctcp('havvy', 'ping', 'PINGMESSAGE'); +``` + +### nick(newNick) ### + +Change the bots nickname. + +### join(channel) ### + +Joins the specified channel. + +```javascript +tennu.join("#tennu"); +tennu.join("#keyed-channel channel-key"); +tennu.join("#chan1,#chan2"); +tennu.join("0"); // Part all channels. + +### part(channel, reason) ### + +Parts the specified channel with the given reason. + ### quit(reason) ### Quits the server with the given reason. - ### whois(users, server) ### Server is optional, and you'll probably not need it. Look at RFC 1459 for @@ -229,7 +256,7 @@ users is either a string or an array of strings. ### userhost(users) ### -Retrieves the userhost of the user. +Retrieves the userhost of the user(s). ### _raw(message) ### @@ -238,9 +265,15 @@ that is not listed here, you can use the internal _raw method, which takes the entire message as is as a string, use your own IrcOutputSocket class, or send in a patch. +### _rawf(format, args...) ### + +[0.8.0] + +As _raw(message), but the arguments are passed through util.format() first. + -------- -## Modules ## +## Module System ## Tennu has its own module system, loosely based off of Node's. You can read about it at https://github.com/havvy/tennu-modules/. @@ -336,9 +369,6 @@ node_modules/ tennu_modules/ config.json The tennu command takes one optional argument, -v (--verbose), for adding a Logger that logs to console. -## Dependency Injection - -You can replace which object factories are called by using the second parameter of tennu.Client. ## Other Objects ## diff --git a/spec/client.spec.js b/spec/client.spec.js index 6637c7b..1a37376 100644 --- a/spec/client.spec.js +++ b/spec/client.spec.js @@ -14,12 +14,12 @@ var network = { var fakeWrite = function (message) { message = message.substring(0, message.length - 2); - // console.log("Fakewrite called with message `" + message + "`"); + console.log("Fakewrite called with message `" + message + "`"); try { if (!this.connected) return; switch (message) { - case "JOIN #test": + case "JOIN :#test": this.emit('data', [ ":testbot!testuser@localhost JOIN :#test", ":irc.localhost.net 353 testbot = #test :@testbot", @@ -126,7 +126,7 @@ describe('Tennu Client', function () { }); it('automatically joins specified channels.', function () { - expect(netsocket.write).toHaveBeenCalledWith("JOIN #test\r\n", 'utf-8'); + expect(netsocket.write).toHaveBeenCalledWith("JOIN :#test\r\n", 'utf-8'); }); }); diff --git a/spec/command-parser.spec.js b/spec/command-parser.spec.js index e890cef..0eee19b 100644 --- a/spec/command-parser.spec.js +++ b/spec/command-parser.spec.js @@ -2,6 +2,7 @@ var CommandParser = require('../lib/command-parser.js'); var Message = require('../lib/message.js'); +var NoLogging = require('../lib/null-logger.js'); var messages = { noncommand: ":sender!user@localhost PRIVMSG #test :Hello", @@ -21,7 +22,7 @@ describe('CommandParser', function () { var nickname = function () { return 'testbot'; }; beforeEach(function () { - parser = CommandParser(nickname, {}); + parser = CommandParser({}, nickname, NoLogging()); }); describe("Ignoring non-commands", function () { diff --git a/spec/message-parser.spec.js b/spec/message-parser.spec.js index e83e502..2a87bee 100644 --- a/spec/message-parser.spec.js +++ b/spec/message-parser.spec.js @@ -1,6 +1,7 @@ -var id = require('./lib/id'); +var id = require('./lib/id.js'); -var MessageParser = require('../lib/message-parser'); +var MessageParser = require('../lib/message-parser.js'); +var NoLogger = require('../lib/null-logger.js'); // jasmine.DEFAULT_TIMEOUT_INTERVAL = 500; //ms @@ -8,12 +9,12 @@ describe('Message Parsers', function () { var parser, receiver; var input = ':irc.server.net 432 MyNick :Erroneous Nickname: Illegal characters'; - it('has has EventEmitter methods', function () { - parser = MessageParser({_id: id()}); + it('has EventEmitter methods', function () { + parser = MessageParser({_id: id()}, NoLogger(), undefined); expect(parser.on).toBeDefined(); expect(parser.once).toBeDefined(); - expect(parser.then).toBeDefined(); + expect(parser.after).toBeDefined(); expect(parser.emit).toBeDefined(); }); diff --git a/spec/output-socket.spec.js b/spec/output-socket.spec.js index 6cbdf8b..e7cbabb 100644 --- a/spec/output-socket.spec.js +++ b/spec/output-socket.spec.js @@ -1,4 +1,5 @@ -var OutputSocket = require('./../lib/output-socket'); +var OutputSocket = require('../lib/output-socket'); +var NoLogger = require('../lib/null-logger'); var nick = 'testbot'; describe('IRC Output Sockets', function () { @@ -6,7 +7,7 @@ describe('IRC Output Sockets', function () { beforeEach(function () { socket = { raw: jasmine.createSpy("raw") }; - os = new OutputSocket(socket, nick); + os = new OutputSocket(socket, NoLogger(), nick); }); it('can send private messages', function () {