webdevRefinery Forum: Writing a very simple IRC bot in Node.js - webdevRefinery Forum

Jump to content

Page 1 of 1
  • You cannot start a new topic
  • You cannot reply to this topic

User is offline callumacrae 

  • {{ post.author }}
  • Group: Members
  • Posts: 2862
  • Joined: 20-January 11
  • LocationWarwickshire, England
  • Expertise:HTML,CSS,PHP,Javascript,Node.js,SQL

Posted 28 May 2011 - 07:48 AM (#1)

Writing a very simple IRC bot in Node.js


So there a few crap IRC bot libraries for Node.js out there, one of which I am using for my IRC client, although I may change to this cos it's so easy. I decided to see how easy it would be to write my own, and it's... really easy!

We will be using the net module, specifically net.Socket.


The basic bot

The extremely extremely basic IRC bot: (does not respond to ping)

var net = require('net');
var irc = {};

irc.socket = new net.Socket();
irc.socket.on('connect', function () {
	console.log('Established connection, registering and shit...');
	setTimeout(function () {
		irc.raw('NICK nick');
		irc.raw('USER callum 8 * :Node.js IRC bot');
	}, 1000);
});

irc.socket.setEncoding('ascii');
irc.socket.setNoDelay();
irc.socket.connect(6667, 'irc.freenode.net');


That's it. That code will connect to freenode under the nick "nick", idle for a couple minutes, and time out.

We want it to do something though, so we will add "listeners" to make it easy and avoid core code edits:

irc.socket.on('data', function (data) {
	data = data.split('\n');
	for (var i = 0; i < data.length; i++) {
		console.log('RECV -', data[i]);
		if (data !== '') {
			irc.handle(data[i].slice(0, -1));
		}
	}
});

//handles incoming messages
irc.handle = function (data) {
	var i, info;
	for (i = 0; i < irc.listeners.length; i++) {
		info = irc.listeners[i][0].exec(data);
		if (info) {
			irc.listeners[i][1](info, data);
			if (irc.listeners[i][2]) {
				irc.listeners.splice(i, 1);
			}
		}
	}
};

irc.listeners = [];
irc.on = function (data, callback) {
	irc.listeners.push([data, callback, false])
};
irc.on_once = function (data, callback) {
	irc.listeners.push([data, callback, true]);
};


This is fairly simple: use
irc.on
with a regex and a callback, and when the regex matches something, the callback is called.
irc.on_once
is similar, but it'll only be called once - good for something like joining a channel and setting a callback to send a message when we have joined the channel, instead of hoping you're on a fast connection and risking a 404 error. We'll also add an
irc.raw
method, because we're too lazy to send everything manually:

irc.raw = function (data) {
	irc.socket.write(data + '\n', 'ascii', function () {
		console.log('SENT -', data);
	});
};


That writes the data to the socket, and then when it has been written logs it to console.


This is how you tell it to respond to ping:

irc.on(/^PING :(.+)$/i, function (info) {
	irc.raw('PONG :' + info[1]);
});



I said it was easy ;-)

The full code is here: https://gist.github.com/996827. The full code also contains configuration and automatic channel joining on connect, which is fairly self explanatory.



Additional features

Here are a few extra things you may wish to add:

Nick changes
When the bot's nick changes, it updates
irc.nick
. This can be handy for other things, including a few methods I'll be putting below this one.
	irc.on(/^[^ ]+ 001 ([0-9a-zA-Z\[\]\\`_\^{|}\-]+) :/, function (info) {
		irc.nick = info[1];
	});
	irc.on(/^:([^!]+)![^@]+@[^ ]+ NICK :(.+)$/, function (info) {
		if (info[1] === irc.nick) {
			irc.nick = info[2];
		}
	});


Join channel
This is fairly self explanatory, but just demonstrates how you can use
irc.on_once
for callbacks.
irc.join = function (chan, callback) {
	if (callback !== undefined) {
		irc.on_once(new RegExp('^:' + irc.info.nick + '![^@]+@[^ ]+ JOIN :' + chan), callback);
	}
	irc.info.names[chan] = {};
	irc.raw('JOIN ' + chan);
};

(Yeah I know, my regex for that one sucks. But it works.)


Send message to channel
This code splits the message into nicely sized chunks, and then sends them.
irc.msg = function (chan, msg) {
	var max_length, msgs, interval;
	max_length = 500 - chan.length;

	msgs =  msg.match(new RegExp('.{1,' + max_length + '}', 'g'));

	interval = setInterval(function () {
		irc.raw('PRIVMSG ' + chan + ' :' + msgs[0]);
		msgs.splice(0, 1);
		if (msgs.length === 0) {
			clearInterval(interval);
		}
	}, 1000);
};



Kick / ban from channel
I think this is the only complicated piece of code in the entire thing.
irc.kick
without ban specified is boring and just kicks the user, but with ban specified, it sends the ban command, waits for it to be executed, and then kicks the user, too.
irc.kick = function (nick, chan, msg, ban) {
	if (ban !== undefined) {
		irc.ban(nick, chan, function () {
			irc.kick(nick, chan, msg);
		});
		return;
	}
	irc.raw('KICK ' + chan + ' ' + nick + ((msg !== undefined) ? ' :' + msg : ''));
};

irc.ban = function (nick, chan, callback) {
	var host, regex;
	if (irc.info.names[chan][nick] === undefined) {
		return false;
	}
	host = irc.info.names[chan][nick].host;
	regex = '^:' + irc.info.nick + '![^@]+@[^ ]+ MODE ' + chan + ' \\+b \\*!\\*@' + host;

	if (callback !== undefined) {
		irc.on_once(new RegExp(regex), function () {
			callback();
		});
	}
	irc.raw('MODE ' + chan + ' +b *!*@' + host);
};



Remember: when in doubt, consult RFC 1459! (cmd+f is your friend)
Front-end developer and writer
Twitter | GitHub | phpBB Contributor and Website Team Member | lynxphp
1


User is offline Ricket 

  • Group: Members
  • Posts: 2
  • Joined: 18-June 11

Posted 18 June 2011 - 07:50 PM (#2)

Two (sort of three) corrections:

irc.socket.on: When you check for the empty string, I think you mean data[i], not data. And why are you calling handle with
data[i].slice(0, -1)
? This will chop off the last character of data[i]. Perhaps you thought the '\n' would be left on there from split, but it isn't, so you really are just slicing off the last character of everything. (Am I wrong?)

irc.socket.handle: After the
irc.listeners.splice(i, 1);
you need
i--;
- otherwise your loop will skip the next listener.

I wrote a test case which combines both of the above mistakes as proof: http://jsfiddle.net/Y7yhr/

Also, lines in IRC are terminated by \r\n, not just \n. All of your data strings have \r at the end of them. This can be okay if you're okay with it though; your current implementation probably works fine.

Overall I like this tutorial though! Thanks.
0


User is offline Ricket 

  • Group: Members
  • Posts: 2
  • Joined: 18-June 11

Posted 19 June 2011 - 10:19 PM (#3)

Just thought I'd give an update. I took your tutorial, modified it a bit, and brought it up to a full-fletched IRC bot. Another correction to your code is that the data event doesn't always return an entire line, so you have to use a buffer... It complicates things but is very necessary or else you'll miss messages when they get fragmented (and they DO get fragmented).

Anyway, I put up all my code on github so anyone can see it and even continue it if you want. Here's the link:

https://github.com/Ricket/nodebot

Thanks again for the tutorial, it really helped me get started. :)
0


User is offline callumacrae 

  • {{ post.author }}
  • Group: Members
  • Posts: 2862
  • Joined: 20-January 11
  • LocationWarwickshire, England
  • Expertise:HTML,CSS,PHP,Javascript,Node.js,SQL

Posted 19 June 2011 - 11:30 PM (#4)

Thanks! :D

Quote

Perhaps you thought the '\n' would be left on there from split, but it isn't, so you really are just slicing off the last character of everything.

...

Also, lines in IRC are terminated by \r\n, not just \n. All of your data strings have \r at the end of them.


;)

You're right about the splice though, I hadn't thought of that.
Front-end developer and writer
Twitter | GitHub | phpBB Contributor and Website Team Member | lynxphp
0


Share this topic:


Page 1 of 1
  • You cannot start a new topic
  • You cannot reply to this topic

1 User(s) are reading this topic
0 members, 1 guests, 0 anonymous users


Enter your sign in name and password


Sign in options
  Or sign in with these services