Personal tools

Roll your own IRC bot

From HaskellWiki

(Difference between revisions)
Jump to: navigation, search
m (wibble)
(chapter 3.)
Line 185: Line 185:
 
== A simple interpreter ==
 
== A simple interpreter ==
   
[[Category:Tutorials]]</hask>
+
<haskell>
  +
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
  +
</haskell>
  +
  +
We add 3 features to the bot here, by modifying <hask>listen</hask>.
  +
Firstly, it responds to <hask>PING</hask> messages: <hask>if ping s then
  +
pong s ... </hask>. This is useful for servers that require pings to
  +
keep clients connected. We also add a new function, <hask>eval</hask>,
  +
which takes a cleaned up input string, and then dispatches bot commands
  +
where appropriate:
  +
  +
<haskell>
  +
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
  +
</haskell>
  +
  +
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 (<hask>id</hask> id is the Haskell identity
  +
function, which just returns its argument). Finally, if no other matches
  +
occur, we do nothing.
  +
  +
We add the <hask>privmsg</hask> function, a useful wrapper over
  +
<hask>write</hask> for sending <hask>PRIVMSG</hask> lines to the server.
  +
  +
Here's a transcript from our minimal bot running in channel:
  +
<code>
  +
15:12 -- tutbot [n=[email protected]] 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=[email protected]] has quit [Client Quit]
  +
</code>
  +
  +
Now, before we go further, let's refactor the code a bit.
  +
  +
== Roll your own monad ==
  +
  +
[[Category:Tutorials]]

Revision as of 04:27, 4 October 2006

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
The key here is the
main
function. This is the entry point

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 module
1.hs
, and we can then run it.

Use 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

Text.Printf
, which will be useful. We also set up a channel name and bot nickname. The
main
function has been extended to send messages back to the IRC server, using a
write

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've given
write
an explicit type to help document it, and

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).

The
write
function takes 3 arguments: a handle (our

socket), and then two strings representing an IRC protocol action, and

any arguments it takes.
write
then uses
hPrintf

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
, is as follows:
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,
forever
, as a normal function!
forever
takes a chunk of code as an

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=[email protected] 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=[email protected]] has joined #tutbot-testing
   15:02  dons> hello

And the bot logs to standard output:

   :dons!i=[email protected] 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
We add 3 features to the bot here, by modifying
listen
. Firstly, it responds to
PING
messages:
if ping s then
pong s ...
. This is useful for servers that require pings to keep clients connected. We also add a new function,
eval
,

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 (
id
id is the Haskell identity

function, which just returns its argument). Finally, if no other matches occur, we do nothing.

We add the
privmsg
function, a useful wrapper over
write
for sending
PRIVMSG
lines to the server.

Here's a transcript from our minimal bot running in channel:

   15:12 -- tutbot [n=[email protected]] 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=[email protected]] has quit [Client Quit]

Now, before we go further, let's refactor the code a bit.

4 Roll your own monad