lodef

a bot that talks: IRC part 2

let's write some code that can connect to an IRC server and react to commands it's given!

transcript
ok cool so in the previous episode we showed how to connect to an IRC
server and how to send and receive messages, but we did it all
manually thru netcat.  let's see what it would take to create a bot
where you can send it commands to and can it'll react.

first let's look at a bot I wrote a while back; this one runs as part
of the CI test suite of Fennel to announce failures to IRC.

$ open ~/src/fennel/test/irc.lua

so at the top it's taking inputs: deciding where to connect and what
URL to link to. then if there are failures, it shells out to git to
get a commit message and then points netcat at the IRC server. it
sends NICK and USER followed by JOIN to the channel specified, sends
one PRIVMSG line about the failure, then QUIT.

eighteen lines of code is all it takes but it gets the job done.

why are we shelling out to netcat? well Lua, in its glorious
minimalism, does not include sockets in the base runtime. of course,
there's a third-party library for this, and it's good, but why pull in
a library you don't need for such a simple call?

this program is pretty static; it gathers some data, runs, and then exits.
let's take this approach and iterate on it.

$ open bot.fnl (empty file)

    (print "NICK lodefbot")
    (print "USER lodefbot * 0 lodefbot")
    (print "JOIN #lodef")
    (print "PRIVMSG #lodef :hi!")
    (print "QUIT")

now I know what you're thinking. "wait a minute; this isn't an IRC
socket! this is just printing and reading from standard in and
standard out! what kind of scam are you trying to pull here?"

welllll it turns out we don't even need to shell out to netcat; we can
have netcat shell out to *us*! watch this:

[split the screen so half shows rcirc's #lodef buffer]
$ nc -c "fennel bot.fnl" localhost 6667

boom; here we go. it just joined, sent a message, and then quit.

so netcat opens the socket for you, launches the program, and hooks up
the input of the socket to the output of the program and vice versa.
it lets us write our program in a way that knows nothing about
sockets, provided we don't need more than one socket, or care about
timeouts or reconnecting.

let's teach it how to do reply to a command:

[delete last 2 lines from bot.fnl where it PRIVMSGs and QUITs]

    (local terms {:lodef "a cool web site"
                  :irc "one of the greatest protocols of all time"
                  :rfc2821 "the spec which defines the IRC protocol"})

    (fn handle [line]
      (match (line:match "lodefbot: what is (.+)%?")
        term (let [reply (or (. terms term) "unknown")]
               (print (string.format "PRIVMSG #lodef :%s is %s"
                                     term reply)))))

    (while true
      (handle (io.read)))

and now our bot knows how to answer questions! we just look up the
term in a table and return "unknown" if it's not found. let's try it out.

[switch to #lodef channel]
> lodefbot: what is irc?
> lodefbot: what is your favorite sandwich place?

pretty great! but it only knows about 3 things. let's teach it how to learn:

    (fn handle [line]
      (match (line:match "lodefbot: what is (.+)%?")
        term (let [reply (or (. terms term) "unknown")]
               (print (string.format "PRIVMSG #lodef :%s is %s" term reply))))
      (match (line:match "lodefbot: set (%S+) to (.+)")
        (term definition) (do (tset terms term definition)
                              (print "PRIVMSG #lodef :ok!"))))

[back to #lodef channel]
> lodefbot: irc is the greatest protocol of all time ever
> ok.
> lodefbot: what is irc?
> irc is the greatest protocol of all time ever

that looks pretty good for now; stay tuned till next time when we add
the ability to save off these definitions and deal with the horrifying
implications of reading and writing to and from filesystems.