I'm young and I make emotionally-charged posts in a stream-of-consciousness style. Drunk with passion, I will be wrong most of the time. Call me on it and help me learn. It is embarrassing to look back at my mistakes, but I will grow.

Sunday, August 12, 2007

A simple Erlang IRC bot

I've been experimenting with Erlang for a while now, and it's been an experience. I've had a little rougher time with it than most people, but most of the blogs I've been reading are written by exceptional or brilliant programmers, so I'm not too concerned :). I went ahead and wrote myself a little Erlang IRC bot, as I had a hard time finding one that I could extend that isn't 5 years old and way too big or doesn't even work when installed using apt-get install (not to mention I couldn't find its source, either). Here's what I came up with:
-module(bot).
-author("jonathan.roes@gmail.com").
-export([connect/2, loop/1]).
-define(nickname, "jroes-test").
-define(channel, "#jroes-test").

% Connect to an IRC server with a given Host and Port.  Set up the TCP option to
% give us messages on a line-by-line basis.
connect(Host, Port) ->
        {ok, Sock} = gen_tcp:connect(Host, Port, [{packet, line}]),
        % According to RFC1459, we need to tell the server our nickname and username
        gen_tcp:send(Sock, "NICK " ++ ?nickname ++ "\r\n"),
        gen_tcp:send(Sock, "USER " ++ ?nickname ++ " blah blah blah blah\r\n"),
        loop(Sock).
        
% Now that we're connected, receive TCP messages and parse them.
loop(Sock) ->
        receive
                {tcp, Sock, Data} ->
                        io:format("[~w] Received: ~s", [Sock, Data]),
                        parse_line(Sock, string:tokens(Data, ": ")),
                        loop(Sock);
                quit ->
                        io:format("[~w] Received quit message, exiting...~n", [Sock]),
                        gen_tcp:close(Sock),
                        exit(stopped)
        end.

% The following is an example of the message this fun intends to parse.  Here we see
% the limitation that tokenizing the string on both :'s and spaces puts on us.
% [#Port<0.124>] Received: :jroes!jroes@mask-2EDB8BDB.net PRIVMSG #jroes-test :jroes-test: wassup?
parse_line(Sock, [User,"PRIVMSG",Channel,?nickname|_]) ->
        Nick = lists:nth(1, string:tokens(User, "!")),
        irc_privmsg(Sock, Channel, "You talkin to me, " ++ Nick ++ "?");
        
% If the second token is "376", then join our channel.  376 indicates End of MOTD.
parse_line(Sock, [_,"376"|_]) ->
        gen_tcp:send(Sock, "JOIN :" ++ ?channel ++ "\r\n");

% The server will periodically send PINGs and expect you to PONG back to make sure
% you haven't lost the connection.
parse_line(Sock, ["PING"|Rest]) ->
        gen_tcp:send(Sock, "PONG " ++ Rest ++ "\r\n");

parse_line(_, _) ->
        0.

% This just helps us write a PRIVMSG back to a client without having to type
% the newlines and :'s ourselves so much.  It'll be more useful later.
irc_privmsg(Sock, To, Message) ->
        gen_tcp:send(Sock, "PRIVMSG " ++ To ++ " :" ++ Message ++ "\r\n").
To run and play with:
jroes@halcyon:~/src$ wget http://jroes.net/bot.erl
jroes@halcyon:~/src$ erl
1> c("bot.erl").
{ok,bot}
2> Bot = spawn(bot, connect, ["irc.server.com", 6667]).
3> Bot ! quit.
Next up: Extending it so the code can be changed/added to at runtime, retrieving data from a webservice.

13 comments:

David Cabana said...

I have to admit I'm not terribly interested in IRC, much less IRC bots. On the other hand, I'd love to know how you got Blogger to display the nice syntax coloring of the Erlang code. Very sharp.

Jonathan said...

Easy! :) M-x htmlize. I used this Emacs-lisp script to take my already syntax-highlighted buffer and turn it into pretty HTML+CSS goodness.

David Cabana said...

Thanks, Jonathan.

I use Emacs myself, but did not know about htmlize.el. That is one slick and useful package. I managed to get it running with no trouble. You made my day.

Ludovic Kuty said...

Nice little example. I wrote a tiny IRC bot in Java 3 years ago to familiarize myself with the IRC protocol. Then I translated it in Ruby.

I am glad to see the elegant Erlang version. Looking forward to reading what's coming next.

Vosilij said...

Damn, it is dirt easy, short and pragmatic. Thumbs up

Andre said...

I want that colorscheme for vim :P

Jonathan said...

Andre,

Switch to emacs and use viper mode. Then install the colortheme package. I use it.

You'll find that most vim commands you usually use will work. There are just too many good packages for emacs to resist.

Anonymous said...

Getting source: apt-get source manderlbot

toby said...

What license do you intend for this code? Can I upload it to a public Svn repo with GPL headers and your copyright+attribution?

Jonathan said...

I hereby release this code into the public domain. Do with it what you like!

It would be really cool if you dropped me a link to what you're working on though :)

toby said...

The PowerShell equivalent is, ah, instructive(!!)

toby said...

Eek, and Python doesn't fare much better...

Miaubiz said...

this is sweet.

as an aside, the incoming line ends with \r\n so if someone says:

bot: hello

you can't match with

Nick, "hello"
because it is actually
Nick, "hello\r\n"

I tokenized on \r\n aswell to ignore that end bit.