Shazbot

A Terse IRC Bot

  1. Simple bots should be simple.
  2. Complicated bots shouldn’t be unmaintainable.
  3. Writing bots should be fun.

And as always, convention beats configuration.

How to Write a Simple Shazbot Simply

Here’s a complete shazbot:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash 
source irc.sh

coproc slashnet { socat - OPENSSL:irc.slashnet.org:6697,verify=0,keepalive; }

bot () {
  echo "NICK shazbot"
  echo "USER shazbot 0 * :I am a bot written in bash"
  while read line
  do
    irc_parse "$line"
    [[ $IRC_COMMAND == "PING" ]] && echo "PONG $IRC_TEXT"
    [[ $IRC_COMMAND == $RPL_ENDOFMOTD ]] && echo "JOIN #shazbot"
    if [[ $IRC_COMMAND == 'PRIVMSG' ]]; then
      if [[ ${IRC_CHANNEL::1} != '#' || $IRC_TEXT == shazbot[,:]* ]]; then
        irc_msg $IRC_CHANNEL "I'm a bit too simple, $IRC_SENDER."
      fi
    fi
  done
}

bot <&${slashnet[0]} >&${slashnet[1]} 

We’ll use this as a reference example to illustrate techniques for writing bots, as well as the very few features that are built into shazbot itself.

irc.sh

As you may have noticed, shazbot is written in bash. Yes that sounds utterly bonkers, but as with any shell script the important code is written in C. You just already have that code installed, even if you didn’t know it!

#!/bin/bash
source irc.sh

Your bot is also itself a bash script (not /bin/sh but bash!), and it uses the shazbot irc.sh library to give you some bot-building functions.

Currently the official location of these libraries is http://zork.net/~nick/shazbot and you can follow along by running:

$ bzr branch http://zork.net/~nick/shazbot

Building the bot has three basic steps

  1. Connecting to the server
  2. Speaking the IRC protocol
  3. Reacting to IRC events with your own code

Connecting to the Server

To connect to the correct IRC port on an IRC server, we set up a bash coprocess. This is like running a command in the background, but getting special file descriptors to talk to its stdin and stdout.

coproc slashnet { socat - OPENSSL:irc.slashnet.org:6697,verify=0,keepalive; }

We’re even using SSL for this one, and if we wanted we could do user certs for OFTC to log in–it’s all in the socat man page.

What? You don’t have socat installed? Okay, well we can do it plain-text, but someone could sniff your traffic:

coproc slashnet { nc -w 300 -i 1 irc.slashnet.org 6667; }

There we go, and notice how we’ve set a timeout of 300 seconds and told it to rate-limit sends to one per second. The nc man page has all kinds of nice TCP options you can use!

You don’t have nc installed either? Wow, you’re not making this easy for me. How about telnet? Can we do telnet?

coproc slashnet { telnet eu.slashnet.org 6667; }

I mean telnet has been around for ages, and it’s not installed by default everywhere these days, but… You’re kidding. Really? You’ve got a modern bash but access to none of these programs? Well, we can do this without them, I suppose…

Using the Network Connection

The bot needs a program or function to put all its decision-making code in. For this example, we’ll use a function just so we can keep everything in one small file.

If you made the slashnet coprocess above, you can hook a function up to it like so:

demobot () {
  # bot logic code goes here!
}

demobot <&${slashnet[0]} >&${slashnet[1]}

If you really don’t have any of the programs used for the coprocess, the best way is to use bash’s own TCP code like so:

demobot () {
  # bot logic code goes here!
}

demobot 6<>/dev/tcp/irc.slashnet.org/6667 <&6 >&6

Speaking the IRC Protocol

To prevent our bot from just connecting and disconnecting, we need to make it speak the IRC protocol to this server. The first thing we need to do is try to log in:

demobot () {
  echo "NICK demobot"
  echo "USER demobot 0 * :I am an IRC bot written in bash!"
}

This bot will also connect and disconnect, but it will take longer. That’s because the server sends it a PING command to make sure it’s really an IRC client before continuing. We need to actually parse the text coming on stdin and send replies on stdout.

Preventing Ping Timeouts

demobot () {
  echo "NICK demobot"
  echo "USER demobot 0 * :I am an IRC bot written in bash!"
  while read line; do
    echo "$line" >&2
    irc_parse "$line"
    irc_ping && continue
  done
}

This version of the bot not only logs in and responds to pings, but it logs what it sees to stderr so you can read all of the server text in your terminal.

Most importantly, it uses two functions from irc.sh that do some IRC drudge-work and help make writing bots fun:

irc_parse
This function chops up the line of text fed to it, and sets a number of environment variables containing pieces of the message we just received. Later we’ll use some of those to make decisions on how to react, but for now we just use…
irc_ping
This function is basically just [[ $IRC_COMMAND == "PING" ]] && echo "PONG $IRC_TEXT" which uses two of the $IRC_ variables set by irc_parse. This will keep the server happy that our software is properly connected and awake. You may have noticed that we just did this manually in our example at the top.

Reacting to IRC events with your own code

Of course a bot that just connects to a server and sits quietly isn’t very interesting. We want to join channels and answer back when users speak to us!

Joining Channels

Once we log in, the server sends a message of the day or “MOTD” file. When it’s done, it sends a line with a numeric command saying it’s finished. At that point, we can join channels and participate on the IRC server:

while read line; do
  irc_parse "$line"
  irc_ping && continue
  [[ $IRC_COMMAND == $RPL_ENDOFMOTD ]] && echo "JOIN #shazbot"
done

Note that we used the irc_ping function this time.

Talking Back

It’s no fun having a quiet bot, even if it will join your channel. Here’s another angle on the bot function’s main loop that uses the bash case statement:

while read line; do
  irc_parse "$line"
  irc_ping && continue
  case $IRC_COMMAND in
    PING)
      echo "PONG $IRC_TEXT";;
    $RPL_ENDOFMOTD)
      echo "JOIN #shazbot";;
    PRIVMSG)
      [[ $IRC_TEXT =~ 2[^[:digit:]]*4[^[:digit:]]*2 ]] && \
        echo "NOTICE $IRC_CHANNEL :242 sighting!"
      [[ $IRC_TEXT =~ political(ly)?\ correct ]] && \
        echo "PRIVMSG $IRC_CHANNEL :I think you mean \"not racist\", $IRC_SENDER."
      ;;
  esac
done

Here we react to PING and $RPL_ENDOFMOTD as we did before, but now we handle PRIVMSG (normal “talking” in IRC) by matching against regular expressions and crafting NOTICE and PRIVMSG replies of our own.

Of course, sometimes faffing about with IRC protocol responses manually is error-prone, especially when it comes to all the line-length prediction that gets long text cut off. To save yourself the trouble, the PRIVMSG) bits above could have been written as follows:

[[ $IRC_TEXT =~ 2[^[:digit:]]*4[^[:digit:]]*2 ]] && \
  irc_msg $IRC_CHANNEL "242 sighting!"
[[ $IRC_TEXT =~ political(ly)?\ correct ]] && \
  irc_msg -m $IRC_CHANNEL "I think you mean \"not racist\", $IRC_SENDER."

A Little History

I’ve played with lots of IRC bot software, and it all seems to fall on a continuum between two extremes:

Potato Programming

In some projects, code is just piled on higgledy-piggledy, with everything happening at the top layer. It’s fun at first, but eventually adding features is a chore that nobody relishes.

“Ugh, you mean I have to dig through that mess and hope I put my code in the right place? Never mind.”

Ceremonial Modularity

In other projects, people try to protect against the mess by constructing byzantine module systems1 with hooks and APIs and reams of standardized auto-generated documentation.

You can guess how fun it is to come to a system like that cold and try to write your first feature:

“Ugh, you mean I have to dig through all that text and hope I used your bits in the right way? Never mind.”

Convention over Configuration

All this talk of “Convention, not Configuration” you hear from Ruby on Rails advocates can sound like so much meaningless architecture-babble at first. But really all it means is that a useful library stays out of your way as much as possible.

At its core, the idea is that instead of giving you a big program that you configure, diving in head-first into dozens of mandatory choices, you’re given bits of code to use or ignore as you wish. So instead of default values, there’s just conventions on how people tend to use the tools provided.

So that’s how shazbot works:

You don’t want to use socat?
Fine, use whatever gets your stdio talking to the right port on the IRC server.
You don’t want to use irc_msg?
Sure, use echo or printf or whatever PRIVMSG construction works best for you.
You don’t want to use bash for responses?
Okay, have your pattern actions run external programs.
You don’t want to use case statements?
Yeah, use whatever flow control construct makes your code clear.
You don’t want to use bash for pattern-matching?
Great, pass the irc_parse info off to your awk or python script, or whatever.

Shazbot is more an approach to designing bots than it is anything in irc.sh. If you’re speaking the IRC protocol over stdio in a vaguely shell-scriptey aggregation of programs, that’s what we are trying to encourage.


  1. Miles Nordin once famously wrote of PAM:

    These people program the way Victorians dress. It takes two hours and three assistants to put on your clothes, and you have to change before dinner. But everything is modular.