{-# LANGUAGE OverloadedStrings #-} -- | -- Module: Network.FastIRC.Session -- Copyright: (c) 2010 Ertugrul Soeylemez -- License: BSD3 -- Maintainer: Ertugrul Soeylemez -- 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