fastirc-0.2.0: Network/FastIRC/Session.hs
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module: Network.FastIRC.Session
-- Copyright: (c) 2010 Ertugrul Soeylemez
-- License: BSD3
-- Maintainer: Ertugrul Soeylemez <es@ertes.de>
-- Stability: alpha
--
-- This module implements a framework for IRC client software.
-- Essentially it consists of a dumb bot, which connects to and stays on
-- an IRC server waiting for commands.
--
-- Using the 'onEvent' function (or the convenience functions
-- 'onConnect', 'onDisconnect', etc.) you can attach event handlers to
-- certain events. These event handlers are run in the 'Bot' monad,
-- which encapsulates the current state of the bot.
--
-- Please note that even though unlikely you should expect that parts of
-- this interface will be changed in future revisions.
module Network.FastIRC.Session
( -- * Types
Bot,
BotCommand(..),
BotInfo(..),
BotSession,
Event(..),
EventHandler,
Params(..),
-- * Functions
ircSendCmd,
ircSendMsg,
ircSendString,
onEvent,
sendBotCmd,
startBot,
-- * Event utility functions
onConnect,
onDisconnect,
onError,
onLoggedIn,
onMessage,
onQuit,
-- * Bot monad
getBotInfo
)
where
import qualified Data.ByteString.Char8 as B
import qualified Data.Map as M
import Control.Applicative
import Control.Concurrent
import Data.Map (Map)
import Data.Unique
import MonadLib
import Network.Fancy
import Network.FastIRC.IO
import Network.FastIRC.Messages
import Network.FastIRC.ServerSet
import Network.FastIRC.Types
import System.IO
-- | Bot monad.
type Bot = ContT () (StateT Config (ReaderT Params IO))
-- | Commands to be sent to the bot.
data BotCommand
-- | Add an event handler.
= BotAddHandler (EventHandler -> IO ()) (Event -> Bot ())
| BotDispatch Event -- ^ Dispatch simulated event.
| BotError String -- ^ Simulate an error.
| BotQuit (Maybe CommandArg) -- ^ Send a quit message.
| BotRecv Message -- ^ Simulate receiving of a message.
| BotSendCmd Command -- ^ Send a command to the IRC server.
| BotSendMsg Message -- ^ Send a message to the IRC server.
| BotSendString MsgString -- ^ Send a raw string to the IRC server.
| BotTerminate -- ^ Immediately kill the bot.
-- | Runtime bot information.
data BotInfo =
BotInfo {
botCurrentNick :: Maybe NickName
}
-- | Bot session descriptor.
data BotSession =
BotSession {
botCmdChan :: Chan BotCommand -- ^ Command channel.
}
-- | Bot configuration at run-time.
data Config =
Config {
-- | Event handlers.
botEventHandlers :: Map EventHandler (Event -> Bot ()),
botEventChan :: Chan Event, -- ^ Event channel.
botHandle :: Handle, -- ^ Connection handle.
botInfo :: BotInfo, -- ^ Current information.
botIsQuitting :: Bool, -- ^ Quit command issued?
botKillerThread :: Maybe ThreadId, -- ^ Killer thread.
botServers :: ServerSet, -- ^ Nicknames known to be servers.
botSession :: BotSession -- ^ Session information.
}
-- | A bot event.
data Event
= ConnectedEvent -- ^ Bot connected.
| DisconnectedEvent -- ^ Bot disconnected (either error or on demand).
| ErrorEvent String -- ^ Connection failed or disconnected on error.
| LoggedInEvent -- ^ Bot logged in (received numeric 001).
| MessageEvent Message -- ^ Received message from server.
| QuitEvent -- ^ Bot disconnected on demand.
deriving (Eq, Read, Show)
-- | Event handler identifier.
type EventHandler = Unique
-- | Parameters for an IRC client connection.
data Params =
Params {
botGetNick :: IO NickName, -- ^ IRC nick name generator.
botGetUser :: IO UserName, -- ^ IRC user name generator.
botGetRealName :: IO RealName, -- ^ IRC real name generator.
botPassword :: Maybe CommandArg, -- ^ IRC server password.
botServerAddr :: Address -- ^ IRC server address.
}
-- | Core bot management thread.
botManager :: Params -> Config -> IO ()
botManager params cfg = do
-- Initialize bot.
let eventChan = botEventChan $ cfg
cmdChan = botCmdChan . botSession $ cfg
h = botHandle $ cfg
writeChan eventChan ConnectedEvent
dispatchThread <- forkIO $
getChanContents eventChan >>= writeList2Chan cmdChan . map BotDispatch
netThread <- forkIO $ networkHandler cmdChan (botHandle cfg)
-- Main loop.
runBot params cfg $ do
sendLogin
forever $ do
bcmd <- inBase $ readChan cmdChan
case bcmd of
BotAddHandler reportId f -> do
hid <- inBase newUnique
handlers <- botEventHandlers <$> get
sets_ (\cfg -> cfg { botEventHandlers = M.insert hid f handlers })
inBase $ reportId hid
BotDispatch ev -> do
handlerList <- M.elems . botEventHandlers <$> get
mapM_ ($ ev) handlerList
BotError err -> do
isQuitting <- botIsQuitting <$> get
unless isQuitting . inBase . writeChan eventChan $ ErrorEvent err
die
BotQuit reason -> do
inBase $ hPutCommand h (QuitCmd reason)
ktid <- inBase . forkIO $
threadDelay 1000000 >>
writeChan cmdChan BotTerminate
sets_ $ \cfg -> cfg { botIsQuitting = True,
botKillerThread = Just ktid }
BotRecv msg ->
inBase (writeChan eventChan $ MessageEvent msg) >>
handleMsg msg
BotSendCmd cmd -> inBase $ hPutCommand h cmd
BotSendMsg msg -> inBase $ hPutMessage h msg
BotSendString str -> inBase $ B.hPutStr h str
BotTerminate -> die
-- Clean up.
killThread dispatchThread
killThread netThread
where
networkHandler :: Chan BotCommand -> Handle -> IO ()
networkHandler cmdChan h = do
res <- try $ hGetMessage h
case res of
Left err -> writeChan cmdChan $ BotError (show err)
Right msg ->
writeChan cmdChan (BotRecv msg) >>
networkHandler cmdChan h
die :: Bot ()
die = do
isQuitting <- botIsQuitting <$> get
ktidM <- botKillerThread <$> get
handlerList <- M.elems . botEventHandlers <$> get
when isQuitting $ mapM_ ($ QuitEvent) handlerList
case ktidM of
Just ktid -> inBase $ killThread ktid
Nothing -> return ()
mapM_ ($ DisconnectedEvent) handlerList
abort ()
-- | Default bot information.
defBotInfo :: BotInfo
defBotInfo =
BotInfo { botCurrentNick = Nothing }
-- | Handle an incoming IRC message.
handleMsg :: Message -> Bot ()
handleMsg msg = do
h <- botHandle <$> get
let origin = msgOrigin msg
cmd = msgCommand msg
eventChan <- botEventChan <$> get
case cmd of
NumericCmd 1 (myNick:_) -> do
inBase $ writeChan eventChan LoggedInEvent
sets_ $ \cfg -> let bi = (botInfo cfg) { botCurrentNick = Just myNick }
in cfg { botInfo = bi }
PingCmd a b -> inBase $ hPutCommand h (PongCmd a b)
_ -> return ()
-- | Send a command to the IRC server.
ircSendCmd :: BotSession -> Command -> IO ()
ircSendCmd bs = sendBotCmd bs . BotSendCmd
-- | Send a message (with origin) to the IRC server. Note that IRC
-- servers ignore the origin prefix, so in general you would want to use
-- 'ircSendCmd' instead.
ircSendMsg :: BotSession -> Message -> IO ()
ircSendMsg bs = sendBotCmd bs . BotSendMsg
-- | Send a raw message string to the IRC server. This is what most IRC
-- clients call /quote.
ircSendString :: BotSession -> MsgString -> IO ()
ircSendString bs = sendBotCmd bs . BotSendString
-- | Add an event handler.
onEvent :: BotSession -> (Event -> Bot ()) -> IO EventHandler
onEvent bs f = do
let cmdChan = botCmdChan bs
answerVar <- newEmptyMVar
writeChan cmdChan $ BotAddHandler (putMVar answerVar) f
takeMVar answerVar
-- | Run a 'Bot' monad computation.
runBot :: Params -> Config -> Bot () -> IO ()
runBot params cfg =
fmap fst .
runReaderT params .
runStateT cfg .
runContT return
-- | Send bot command to a bot.
sendBotCmd :: BotSession -> BotCommand -> IO ()
sendBotCmd bs cmd = writeChan (botCmdChan bs) cmd
-- | Send login commands.
sendLogin :: Bot ()
sendLogin = do
h <- botHandle <$> get
nick <- asks botGetNick >>= inBase
user <- asks botGetUser >>= inBase
real <- asks botGetRealName >>= inBase
addr <- asks botServerAddr
pass <- asks botPassword
let (host, port) =
case addr of
IP h p -> (B.pack h, B.pack $ show p)
IPv4 h p -> (B.pack h, B.pack $ show p)
IPv6 h p -> (B.pack h, B.pack $ show p)
_ -> ("localhost", "6667")
inBase $ do
case pass of
Just pwd -> hPutCommand h $ PassCmd pwd
Nothing -> return ()
hPutCommand h $ NickCmd nick Nothing
hPutCommand h $ UserCmd user host port real
-- | Launch an IRC bot.
startBot :: Params -> IO (Either IOError BotSession)
startBot params = do
cmdChan <- newChan
eventChan <- newChan
errorVar <- newEmptyMVar
let session = BotSession { botCmdChan = cmdChan }
forkIO $
let comp =
withStream (botServerAddr params) $ \h ->
let cfg =
Config {
botEventHandlers = M.empty,
botEventChan = eventChan,
botHandle = h,
botInfo = defBotInfo,
botIsQuitting = False,
botKillerThread = Nothing,
botServers = emptyServers,
botSession = session
}
in do
hSetBuffering h NoBuffering
putMVar errorVar Nothing
res <- try $ botManager params cfg
case res of
Left err -> do
hPutStrLn stderr "Warning (fastirc): unexpected exception:"
hPrint stderr err
hPutStrLn stderr "Please report this to the author."
Right _ -> return ()
in comp `catch` (putMVar errorVar . Just)
error <- takeMVar errorVar
case error of
Nothing -> return (Right session)
Just err -> return (Left err)
-- | Action to run on connect.
onConnect :: BotSession -> Bot () -> IO EventHandler
onConnect bs c = onEvent bs $ \ev -> case ev of ConnectedEvent -> c; _ -> return ()
-- | Action to run on disconnect.
onDisconnect :: BotSession -> Bot () -> IO EventHandler
onDisconnect bs c = onEvent bs $ \ev -> case ev of DisconnectedEvent -> c; _ -> return ()
-- | Action to run on error (connection failed/aborted).
onError :: BotSession -> (String -> Bot ()) -> IO EventHandler
onError bs f = onEvent bs $ \ev -> case ev of ErrorEvent str -> f str; _ -> return ()
-- | Action to run after login (numeric 001 received).
onLoggedIn :: BotSession -> Bot () -> IO EventHandler
onLoggedIn bs c = onEvent bs $ \ev -> case ev of LoggedInEvent -> c; _ -> return ()
-- | Action to run when a message arrives.
onMessage :: BotSession -> (Message -> Bot ()) -> IO EventHandler
onMessage bs f = onEvent bs $ \ev -> case ev of MessageEvent msg -> f msg; _ -> return ()
-- | Action to run on quit.
onQuit :: BotSession -> Bot () -> IO EventHandler
onQuit bs c = onEvent bs $ \ev -> case ev of QuitEvent -> c; _ -> return ()
-- | Get current bot information.
getBotInfo :: Bot BotInfo
getBotInfo = botInfo <$> get