And as always, convention beats configuration.
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 |
|
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.
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
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…
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
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
.
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
irc_ping
[[ $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.
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!
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.
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."
I’ve played with lots of IRC bot software, and it all seems to fall on a continuum between two extremes:
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.”
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.”
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:
socat
?stdio
talking to the right port on the IRC server.
irc_msg
?echo
or printf
or whatever PRIVMSG
construction works best for you.
bash
for responses?case
statements?bash
for pattern-matching?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.
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.