hask-irc/Network/IRC/Client.hs

236 lines
9.0 KiB
Haskell

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Network.IRC.Client (run) where
import qualified Data.Text.Format as TF
import qualified Data.Text.Format.Params as TF
import ClassyPrelude
import Control.Exception.Lifted (mask_, AsyncException (UserInterrupt))
import Control.Concurrent.Lifted (fork, Chan, newChan, readChan, writeChan, threadDelay)
import Control.Monad.Reader (ask)
import Control.Monad.State (get, put)
import Network (PortID (PortNumber), connectTo, withSocketsDo)
import System.IO (hIsEOF, hSetBuffering, BufferMode(..))
import System.Timeout (timeout)
import Network.IRC.Handlers
import Network.IRC.Protocol
import Network.IRC.Types
oneSec :: Int
oneSec = 1000000
debug :: Text -> IO ()
debug msg = do
time <- getCurrentTime
TF.print "[{}] {}\n" $ TF.buildParams (formatTime defaultTimeLocale "%F %T" time, msg)
type Latch = MVar ()
latchIt :: Latch -> IO ()
latchIt latch = putMVar latch ()
awaitLatch :: Latch -> IO ()
awaitLatch latch = void $ takeMVar latch
type Channel a = (Chan a, Latch)
data Line = Timeout | EOF | Line !Message deriving (Show, Eq)
sendCommand :: Chan Command -> Command -> IO ()
sendCommand = writeChan
sendMessage :: Chan Line -> Message -> IO ()
sendMessage = (. Line) . writeChan
sendEvent :: Chan SomeEvent -> SomeEvent -> IO ()
sendEvent = writeChan
readLine :: Chan Line -> IO Line
readLine = readChan
sendCommandLoop :: Channel Command -> Bot -> IO ()
sendCommandLoop (commandChan, latch) bot@Bot { .. } = do
cmd <- readChan commandChan
time <- getCurrentTime
let mline = lineFromCommand botConfig cmd
case mline of
Nothing -> return ()
Just line -> do
TF.hprint socket "{}\r\n" $ TF.Only line
TF.print "[{}] > {}\n" $ TF.buildParams (formatTime defaultTimeLocale "%F %T" time, line)
case cmd of
QuitCmd -> latchIt latch
_ -> sendCommandLoop (commandChan, latch) bot
readLineLoop :: MVar BotStatus -> Channel Line -> Bot -> Int -> IO ()
readLineLoop mvBotStatus (lineChan, latch) bot@Bot { .. } timeoutDelay = do
botStatus <- readMVar mvBotStatus
case botStatus of
Disconnected -> latchIt latch
_ -> do
mLine <- timeout timeoutDelay readLine'
case mLine of
Nothing -> writeChan lineChan Timeout
Just line -> writeChan lineChan line
readLineLoop mvBotStatus (lineChan, latch) bot timeoutDelay
where
readLine' = do
eof <- hIsEOF socket
if eof
then return EOF
else do
line <- map initEx $ hGetLine socket
debug $ "< " ++ line
now <- getCurrentTime
return . Line $ msgFromLine botConfig now line
listenerLoop :: Chan Line -> Chan Command -> Int -> IRC ()
listenerLoop lineChan commandChan !idleFor = do
status <- get
bot@Bot { .. } <- ask
let nick = botNick botConfig
nStatus <- liftIO . mask_ $
if idleFor >= (oneSec * botTimeout botConfig)
then debug "Timeout" >> return Disconnected
else do
when (status == Kicked) $
threadDelay (5 * oneSec) >> sendCommand commandChan JoinCmd
mLine <- readLine lineChan
case mLine of
Timeout -> getCurrentTime >>= dispatchHandlers bot . IdleMsg >> return Idle
EOF -> debug "Connection closed" >> return Disconnected
Line message -> do
nStatus <- case message of
JoinMsg { .. } | userNick user == nick -> debug "Joined" >> return Joined
KickMsg { .. } | kickedNick == nick -> debug "Kicked" >> return Kicked
NickInUseMsg { .. } ->
debug "Nick already in use" >> return NickNotAvailable
ModeMsg { user = Self, .. } ->
sendCommand commandChan JoinCmd >> return Connected
_ -> return Connected
dispatchHandlers bot message
return nStatus
put nStatus
case nStatus of
Idle -> listenerLoop lineChan commandChan (idleFor + oneSec)
Disconnected -> return ()
NickNotAvailable -> return ()
_ -> listenerLoop lineChan commandChan 0
where
dispatchHandlers Bot { .. } message =
forM_ (mapToList msgHandlers) $ \(_, msgHandler) -> fork $
handle (\(e :: SomeException) ->
debug $ "Exception while processing message: " ++ pack (show e)) $ do
mCmd <- handleMessage msgHandler botConfig message
case mCmd of
Nothing -> return ()
Just cmd -> sendCommand commandChan cmd
eventProcessLoop :: Channel SomeEvent -> Chan Line -> Chan Command -> Bot -> IO ()
eventProcessLoop (eventChan, latch) lineChan commandChan bot@Bot {.. } = do
event <- readChan eventChan
case fromEvent event of
Just (QuitEvent, _) -> latchIt latch
_ -> do
debug $ "Event: " ++ pack (show event)
forM_ (mapToList msgHandlers) $ \(_, msgHandler) -> fork $
handle (\(ex :: SomeException) ->
debug $ "Exception while processing event: " ++ pack (show ex)) $ do
resp <- handleEvent msgHandler botConfig event
case resp of
RespMessage message -> sendMessage lineChan message
RespCommand command -> sendCommand commandChan command
RespEvent event' -> sendEvent eventChan event'
_ -> return ()
eventProcessLoop (eventChan, latch) lineChan commandChan bot
connect :: BotConfig -> IO (Bot, MVar BotStatus, Channel Line, Channel Command, Channel SomeEvent)
connect botConfig@BotConfig { .. } = do
debug "Connecting ..."
socket <- connectToWithRetry
hSetBuffering socket LineBuffering
debug "Connected"
lineChan <- newChannel
commandChan <- newChannel
eventChan <- newChannel
mvBotStatus <- newMVar Connected
msgHandlers <- loadMsgHandlers (fst eventChan)
return (Bot botConfig socket msgHandlers, mvBotStatus, lineChan, commandChan, eventChan)
where
connectToWithRetry = connectTo (unpack server) (PortNumber (fromIntegral port))
`catch` (\(e :: SomeException) -> do
debug ("Error while connecting: " ++ pack (show e) ++ ". Waiting.")
threadDelay (5 * oneSec)
connectToWithRetry)
newChannel = (,) <$> newChan <*> newEmptyMVar
loadMsgHandlers eventChan = flip (`foldM` mempty) msgHandlerNames $ \hMap msgHandlerName -> do
debug $ "Loading msg handler: " ++ msgHandlerName
mMsgHandler <- mkMsgHandler botConfig eventChan msgHandlerName
case mMsgHandler of
Nothing -> debug ("No msg handler found with name: " ++ msgHandlerName) >> return hMap
Just msgHandler -> return $ insertMap msgHandlerName msgHandler hMap
disconnect :: (Bot, MVar BotStatus, Channel Line, Channel Command, Channel SomeEvent) -> IO ()
disconnect (Bot { .. }, mvBotStatus, (_, readLatch), (commandChan, sendLatch), (eventChan, eventLatch)) = do
debug "Disconnecting ..."
sendCommand commandChan QuitCmd
awaitLatch sendLatch
swapMVar mvBotStatus Disconnected
awaitLatch readLatch
sendEvent eventChan =<< toEvent QuitEvent
awaitLatch eventLatch
unloadMsgHandlers
hClose socket
debug "Disconnected"
where
unloadMsgHandlers = forM_ (mapToList msgHandlers) $ \(msgHandlerName, msgHandler) -> do
debug $ "Unloading msg handler: " ++ msgHandlerName
stopMsgHandler msgHandler botConfig
run :: BotConfig -> IO ()
run botConfig' = withSocketsDo $ do
hSetBuffering stdout LineBuffering
debug "Running with config:"
print botConfig
status <- run_
case status of
Disconnected -> debug "Restarting .." >> run botConfig
Errored -> debug "Restarting .." >> run botConfig
Interrupted -> return ()
NickNotAvailable -> return ()
_ -> error "Unsupported status"
where
botConfig = botConfig' { msgHandlerNames = hashNub $ msgHandlerNames botConfig' ++ coreMsgHandlerNames }
handleErrors :: SomeException -> IO BotStatus
handleErrors e = case fromException e of
Just UserInterrupt -> debug "User interrupt" >> return Interrupted
_ -> debug ("Exception! " ++ pack (show e)) >> return Errored
run_ = bracket (connect botConfig) disconnect $
\(bot, mvBotStatus, (lineChan, readLatch), (commandChan, sendLatch), eventChannel) ->
handle handleErrors $ do
sendCommand commandChan NickCmd
sendCommand commandChan UserCmd
fork $ sendCommandLoop (commandChan, sendLatch) bot
fork $ readLineLoop mvBotStatus (lineChan, readLatch) bot oneSec
fork $ eventProcessLoop eventChannel lineChan commandChan bot
runIRC bot Connected (listenerLoop lineChan commandChan 0)