Roll your own IRC bot
From HaskellWiki
This tutorial is designed as a practical guide to writing real world code in Haskell, and hopes to intuitively motivate and introduce some of the advanced features of Haskell to the novice programmer. Our goal is to write a concise, robust and elegant IRC bot in Haskell.
Contents |
1 Getting started
You'll need a reasonably recent version of GHC or Hugs. Our first step is to get on the network. So let's start by importing the Network package, and the standard IO library and defining a server to connect to.
import Network import System.IO server = "irc.freenode.org" port = 6667 main = do h <- connectTo server (PortNumber (fromIntegral port)) hSetBuffering h NoBuffering t <- hGetContents h print t
to a Haskell program. We first connect to the server, then set the buffering on the socket off. Once we've got a socket, we can then just read and print any data we receive.
Put this code in the moduleUse whichever system you like:
Using runhaskell:
$ runhaskell 1.hs "NOTICE AUTH :*** Looking up your hostname...\r\nNOTICE AUTH :*** Checking ident\r\nNOTICE AUTH :*** Found your hostname\r\n ...
Or we can just compile it to an executable with GHC:
$ ghc --make 1.hs -o tutbot Chasing modules from: 1.hs Compiling Main ( 1.hs, 1.o ) Linking ... $ ./tutbot "NOTICE AUTH :*** Looking up your hostname...\r\nNOTICE AUTH :*** Checking ident\r\nNOTICE AUTH :*** Found your hostname\r\n ...
Or using GHCi:
$ ghci 1.hs *Main> main "NOTICE AUTH :*** Looking up your hostname...\r\nNOTICE AUTH :*** Checking ident\r\nNOTICE AUTH :*** Found your hostname\r\n ...
Or in Hugs:
$ runhugs 1.hs "NOTICE AUTH :*** Looking up your hostname...\r\nNOTICE AUTH :*** Checking ident\r\nNOTICE AUTH :*** Found your hostname\r\n ...
Great! We're on the network.
2 Talking IRC
Now we're listening to the server, we better start sending some information back. Three details are important: the nick, the user name, and a channel to join. So let's send those.
import Network import System.IO import Text.Printf server = "irc.freenode.org" port = 6667 chan = "#tutbot-testing" nick = "tutbot" main = do h <- connectTo server (PortNumber (fromIntegral port)) hSetBuffering h NoBuffering write h "NICK" nick write h "USER" (nick++" 0 * :tutorial bot") write h "JOIN" chan listen h write :: Handle -> String -> String -> IO () write h s t = do hPrintf h "%s %s\r\n" s t printf "> %s %s\n" s t listen h = forever $ do s <- hGetLine h putStrLn s where forever a = a >> forever a
Now, we've done quite a few things here. Firstly, we import
function. Let's look at that a bit more closely:
write :: Handle -> String -> String -> IO () write h s t = do hPrintf h "%s %s\r\n" s t printf "> %s %s\n" s t
we'll use explicit types signatures from now on, as they're just good practice (though of course not required, as Haskell uses type inference to work out the types anyway).
Thesocket), and then two strings representing an IRC protocol action, and
any arguments it takes.to build an IRC message, and write it over the wire to the server. For debugging purposes we also print to standard output the message we send.
Our second function,listen :: Handle -> IO () listen h = forever $ do s <- hGetLine h putStrLn s where forever a = a >> forever a
This function takes a Handle argument, and sits in an infinite loop reading lines of text from the network, and printing them. We take advantage of two powerful features, lazy evaluation and higher order
functions, to roll our own loop control structure,argument, evaluates it, and recurses -- an infinite loop function. It is very common to roll our own control structures in Haskell this way, using higher order functions. No need to add new syntax to the language, when you can just write a normal function to implement whatever control flow you wish.
Let's run this thing:
$ runhaskell 2.hs > NICK tutbot > USER tutbot 0 * :tutorial bot > JOIN #tutbot-testing NOTICE AUTH :*** Looking up your hostname... NOTICE AUTH :*** Found your hostname, welcome back NOTICE AUTH :*** Checking ident NOTICE AUTH :*** No identd (auth) response :orwell.freenode.net 001 tutbot :Welcome to the freenode IRC Network tutbot :orwell.freenode.net 002 tutbot :Your host is orwell.freenode.net ... :tutbot!n=tutbot@aa.bb.cc.dd JOIN :#tutbot-testing :orwell.freenode.net MODE #tutbot-testing +ns :orwell.freenode.net 353 tutbot @ #tutbot-testing :@tutbot :orwell.freenode.net 366 tutbot #tutbot-testing :End of /NAMES list.
And we're in business! From an irc client, we can watch the bot connect:
15:02 -- tutbot [n=tutbot@aa.bb.cc.dd] has joined #tutbot-testing 15:02 dons> hello
And the bot logs to standard output:
:dons!i=dons@my.net PRIVMSG #tutbot-testing :hello
We can now implement some commands.
3 A simple interpreter
import Network import System.IO import Text.Printf import Data.List import System.Exit server = "irc.freenode.org" port = 6667 chan = "#tutbot-testing" nick = "tutbot" main :: IO () main = do h <- connectTo server (PortNumber (fromIntegral port)) hSetBuffering h NoBuffering write h "NICK" nick write h "USER" (nick++" 0 * :tutorial bot") write h "JOIN" chan listen h listen :: Handle -> IO () listen h = forever $ do s <- init `fmap` hGetLine h if ping s then pong s else eval h (clean s) putStrLn s where forever a = a >> forever a clean = drop 1 . dropWhile (/= ':') . drop 1 ping x = "PING :" `isPrefixOf` x pong x = write h "PONG" (':' : drop 6 x) eval :: Handle -> String -> IO () eval h "!quit" = write h "QUIT" ":Exiting" >> exitWith ExitSuccess eval h x | "!id " `isPrefixOf` x = privmsg h (drop 4 x) eval _ _ = return () -- ignore everything else privmsg :: Handle -> String -> IO () privmsg h s = write h "PRIVMSG" (chan ++ " :" ++ s) write :: Handle -> String -> String -> IO () write h s t = do hPrintf h "%s %s\r\n" s t printf "> %s %s\n" s t
pong s ...
which takes a cleaned up input string, and then dispatches bot commands where appropriate:
eval :: Handle -> String -> IO () eval h "!quit" = write h "QUIT" ":Exiting" >> exitWith ExitSuccess eval h x | "!id " `isPrefixOf` x = privmsg h (drop 4 x) eval _ _ = return () -- ignore everything else
So, if the single string "!quit" is received, we inform the server, and exit the program. If a string beginning with "!id" we echo any argument
string back to the server (function, which just returns its argument). Finally, if no other matches occur, we do nothing.
We add theHere's a transcript from our minimal bot running in channel:
15:12 -- tutbot [n=tutbot@aa.bb.cc.dd] has joined #tutbot-testing 15:13 dons> !id hello, world! 15:13 tutbot> hello, world! 15:13 dons> !id very pleased to meet you. 15:13 tutbot> very pleased to meet you. 15:13 dons> !quit 15:13 -- tutbot [n=tutbot@aa.bb.cc.dd] has quit [Client Quit]
Now, before we go further, let's refactor the code a bit.
