hask-irc/hask-irc-core/Network/IRC/Client.hs

197 lines
8.2 KiB
Haskell
Raw Normal View History

{-|
Module : Network.IRC.Client
Description : The IRC bot client used to create and run a bot.
Copyright : (c) Abhinav Sarkar, 2014
License : Apache-2.0
Maintainer : abhinav@abhinavsarkar.net
Stability : experimental
Portability : POSIX
-}
2014-05-22 01:08:36 +05:30
{-# LANGUAGE TemplateHaskell #-}
2014-05-04 04:28:08 +05:30
module Network.IRC.Client (runBot) where
2014-05-04 04:28:08 +05:30
2014-05-22 01:08:36 +05:30
import qualified System.Log.Logger as HSL
import ClassyPrelude
import Control.Concurrent.Lifted (fork, threadDelay, myThreadId)
2014-06-01 23:14:19 +05:30
import Control.Exception.Lifted (throwTo, AsyncException (UserInterrupt))
2014-05-21 11:20:53 +05:30
import Network (PortID (PortNumber), connectTo, withSocketsDo)
import System.IO (hSetBuffering, BufferMode(..))
2014-06-01 23:14:19 +05:30
import System.Log.Formatter (tfLogFormatter)
import System.Log.Handler (setFormatter)
import System.Log.Handler.Simple (streamHandler)
import System.Log.Logger (Priority (..), updateGlobalLogger, rootLoggerName,
setHandlers, setLevel)
2014-05-22 01:08:36 +05:30
import System.Log.Logger.TH (deriveLoggers)
2014-06-01 23:14:19 +05:30
import System.Posix.Signals (installHandler, sigINT, sigTERM, Handler (Catch))
2014-05-04 04:28:08 +05:30
import Network.IRC.Bot
import Network.IRC.Internal.Types
import Network.IRC.MessageBus
2014-06-08 07:12:33 +05:30
import Network.IRC.Types
import Network.IRC.Handlers.Core
import Network.IRC.Util
2014-05-21 00:06:37 +05:30
2014-05-22 01:08:36 +05:30
$(deriveLoggers "HSL" [HSL.DEBUG, HSL.ERROR])
data ConnectionResource = ConnectionResource
{ bot :: !Bot
2015-06-26 10:45:02 +05:30
, botStatus :: !(MVar BotStatus) -- TODO: is this really needed
, inChannel :: !(MessageChannel In)
, mainMsgChannel :: !(MessageChannel Message)
, handlerMsgChannels :: !(Map MsgHandlerName (MessageChannel Message))
}
2014-06-01 23:14:19 +05:30
connect :: BotConfig -> IO ConnectionResource
connect botConfig@BotConfig { .. } = do
2014-05-22 01:08:36 +05:30
debugM "Connecting ..."
socket <- connectToWithRetry
hSetBuffering socket LineBuffering
2014-05-22 01:08:36 +05:30
debugM "Connected"
messageBus <- newMessageBus
inBus <- newMessageBus
mvBotStatus <- newMVar Connected
inChannel <- newMessageChannel inBus
mainMsgChannel <- newMessageChannel messageBus
msgHandlersChans <- loadMsgHandlers messageBus
2015-06-26 10:45:02 +05:30
msgHandlerInfo' <- flip (`foldM` mempty) (mapToList msgHandlersChans)
$ \handlerInfo (handlerName, (handler, _)) -> do
handlerHelp <- getHelp handler botConfig
return $ insertMap handlerName handlerHelp handlerInfo
let botConfig' = botConfig { msgHandlerInfo = msgHandlerInfo'}
let msgHandlerChannels = map snd msgHandlersChans
let msgHandlers = map fst msgHandlersChans
2015-06-26 10:45:02 +05:30
return ConnectionResource { bot = (Bot botConfig' socket msgHandlers)
, botStatus = mvBotStatus
, inChannel = inChannel
, mainMsgChannel = mainMsgChannel
, handlerMsgChannels = msgHandlerChannels
}
where
connectToWithRetry = connectTo (unpack botServer) (PortNumber (fromIntegral botPort))
2015-06-26 10:45:02 +05:30
`catch` (\(e :: SomeException) -> do
errorM ("Error while connecting: " ++ show e ++ ". Retrying.")
threadDelay (5 * oneSec)
connectToWithRetry)
mkMsgHandler name messageBus =
case lookup name msgHandlerMakers of
Nothing -> return Nothing
Just maker -> do
messageChannel <- newMessageChannel messageBus
handler <- msgHandlerMaker maker botConfig messageChannel
return $ Just (handler, messageChannel)
2014-05-21 00:06:37 +05:30
loadMsgHandlers messageBus =
2014-05-22 20:59:02 +05:30
flip (`foldM` mempty) (mapKeys msgHandlerInfo) $ \hMap msgHandlerName -> do
debugM . unpack $ "Loading msg handler: " ++ msgHandlerName
mMsgHandler <- mkMsgHandler msgHandlerName messageBus
2014-05-22 20:59:02 +05:30
case mMsgHandler of
Nothing -> do
2014-05-22 20:59:02 +05:30
debugM . unpack $ "No msg handler found with name: " ++ msgHandlerName
return hMap
Just msgHandlerAndChannel -> return $ insertMap msgHandlerName msgHandlerAndChannel hMap
2014-05-21 00:38:01 +05:30
disconnect :: ConnectionResource -> IO ()
disconnect ConnectionResource { bot = Bot { .. }, .. } = do
2014-05-22 01:08:36 +05:30
debugM "Disconnecting ..."
sendMessage mainMsgChannel =<< newMessage QuitCmd
awaitMessageChannel mainMsgChannel
swapMVar botStatus Disconnected
awaitMessageChannel inChannel
forM_ handlerMsgChannels awaitMessageChannel
handle (\(_ :: SomeException) -> return ()) $ hClose botSocket
2014-05-22 01:08:36 +05:30
debugM "Disconnected"
2014-05-04 04:28:08 +05:30
2014-06-01 23:14:19 +05:30
runBotIntenal :: BotConfig -> IO ()
runBotIntenal botConfig' = withSocketsDo $ do
status <- run
case status of
Disconnected -> debugM "Restarting .." >> runBotIntenal botConfigWithCore
Errored -> debugM "Restarting .." >> runBotIntenal botConfigWithCore
NickNotAvailable -> debugM "Trying new nick" >> runBotIntenal botConfigWithNewNick
NickAvailable -> debugM "Trying original nick" >> runBotIntenal botConfigWithOrigNick
Interrupted -> return ()
_ -> error "Unsupported status"
where
botConfigWithCore = botConfig' {
2014-05-22 20:59:02 +05:30
msgHandlerInfo =
2015-06-26 10:45:02 +05:30
foldl' (flip (`insertMap` mempty)) mempty
(hashNub $ mapKeys (msgHandlerInfo botConfig') ++ mapKeys coreMsgHandlerMakers)
, msgHandlerMakers = coreMsgHandlerMakers <> msgHandlerMakers botConfig'
}
botConfigWithNewNick = botConfigWithCore {
botNick = Nick $ nickToText (botNick botConfigWithCore) ++ "_"
2014-05-22 20:59:02 +05:30
}
botConfigWithOrigNick = botConfigWithCore {
botNick = botOrigNick botConfigWithCore
}
handleErrors :: SomeException -> IO BotStatus
2014-05-15 12:02:31 +05:30
handleErrors e = case fromException e of
2014-05-22 01:08:36 +05:30
Just UserInterrupt -> debugM "User interrupt" >> return Interrupted
_ -> debugM ("Exception! " ++ show e) >> return Errored
2015-06-26 10:45:02 +05:30
-- TODO: handle handler errors?
runHandler :: BotConfig -> (MsgHandlerName, (MsgHandler, MessageChannel Message)) -> IO ()
2015-06-26 10:45:02 +05:30
runHandler botConfig (msgHandlerName, (handler, msgChannel)) = go =<< receiveMessage msgChannel
where
go msg@Message { .. }
| Just QuitCmd <- fromMessage message = do
debugM . unpack $ "Stopping msg handler: " ++ msgHandlerName
stopMsgHandler handler botConfig
closeMessageChannel msgChannel
| otherwise = do
resps <- handleMessage handler botConfig msg
forM_ resps $ sendMessage msgChannel
runHandler botConfig (msgHandlerName, (handler, msgChannel))
run = bracket (connect botConfigWithCore) disconnect $
\ConnectionResource { .. } ->
handle handleErrors $ do
let Bot { .. } = bot
2014-06-01 23:14:19 +05:30
debugM $ "Running with config:\n" ++ show botConfig
sendMessage mainMsgChannel =<< newMessage NickCmd
sendMessage mainMsgChannel =<< newMessage UserCmd
fork $ sendCommandLoop mainMsgChannel bot
2015-06-26 10:45:02 +05:30
`catch` (\(e :: SomeException) -> errorM $ "Error in sendCommandLoop: " ++ show e)
fork $ readMessageLoop botStatus inChannel bot oneSec
2015-06-26 10:45:02 +05:30
`catch` (\(e :: SomeException) -> errorM $ "Error in readMessageLoop: " ++ show e)
forM_ (mapToList . asMap $ mergeMaps msgHandlers handlerMsgChannels) $
void . fork . runHandler botConfig
2015-06-26 10:45:02 +05:30
runIRC bot Connected $ messageProcessLoop inChannel mainMsgChannel
2014-06-01 23:14:19 +05:30
-- | Creates and runs an IRC bot for given the config. This IO action runs forever.
runBot :: BotConfig -- ^ The bot config used to create the bot.
-> IO ()
2014-06-01 23:14:19 +05:30
runBot botConfig = do
-- setup signal handling
mainThreadId <- myThreadId
2015-06-26 10:45:02 +05:30
let interruptMainThread = throwTo mainThreadId UserInterrupt
installHandler sigINT (Catch interruptMainThread) Nothing
installHandler sigTERM (Catch interruptMainThread) Nothing
2014-06-01 23:14:19 +05:30
-- setup logging
hSetBuffering stdout LineBuffering
hSetBuffering stderr LineBuffering
2015-06-26 10:45:02 +05:30
stderrHandler <- streamHandler stderr DEBUG >>= \logHandler ->
return . setFormatter logHandler $
tfLogFormatter "%F %T" "[$utcTime] $loggername $prio $msg"
2014-06-01 23:14:19 +05:30
updateGlobalLogger rootLoggerName (setHandlers [stderrHandler] . setLevel DEBUG)
-- run
runBotIntenal botConfig