irc-client-0.2.3.0: Network/IRC/Client/Internal.hs
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Most of the hairy code. This isn't all internal, due to messy
-- dependencies, but I've tried to make this as \"internal\" as
-- reasonably possible.
module Network.IRC.Client.Internal where
import Control.Applicative ((<$>))
import Control.Concurrent (forkIO)
import Control.Concurrent.STM (atomically, readTVar, retry)
import Control.Monad (unless)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Reader (runReaderT)
import Data.ByteString (ByteString)
import Data.Conduit (Producer, Conduit, Consumer, (=$=), ($=), (=$), awaitForever, toProducer, yield)
import Data.Conduit.TMChan (closeTBMChan, isEmptyTBMChan, newTBMChanIO, sourceTBMChan, writeTBMChan)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.Time.Clock (NominalDiffTime, getCurrentTime)
import Data.Time.Format (formatTime)
import Network.IRC.Conduit (IrcEvent, IrcMessage, floodProtector, rawMessage, toByteString)
import Network.IRC.Client.Types
import System.Locale (defaultTimeLocale)
-- * Connecting to an IRC network
-- | Connect to a server using the supplied connection function.
connect' :: MonadIO m
=> (Int -> ByteString -> IO () -> Consumer (Either ByteString IrcEvent) IO () -> Producer IO IrcMessage -> IO ())
-> IRC ()
-> ByteString
-> Int
-> NominalDiffTime
-> m ConnectionConfig
connect' f dcHandler host port flood = liftIO $ do
queueS <- newTBMChanIO 16
return ConnectionConfig
{ _func = f
, _sendqueue = queueS
, _server = host
, _port = port
, _flood = flood
, _disconnect = dcHandler
}
-- * Event loop
-- | The event loop.
runner :: IRC ()
runner = do
state <- ircState
-- Set the nick and username
theNick <- _nick <$> instanceConfig
theUser <- _username <$> instanceConfig
theReal <- _realname <$> instanceConfig
-- Initialise the IRC session
let initialise = flip runReaderT state $ do
sendBS $ rawMessage "USER" [encodeUtf8 theUser, "-", "-", encodeUtf8 theReal]
send $ Nick theNick
mapM_ (send . Join) . _channels <$> instanceConfig
return ()
-- Run the event loop, and call the disconnect handler if the remote
-- end closes the socket.
flood <- _flood <$> connectionConfig
func <- _func <$> connectionConfig
port <- _port <$> connectionConfig
server <- _server <$> connectionConfig
queue <- _sendqueue <$> connectionConfig
antiflood <- liftIO $ floodProtector flood
dchandler <- _disconnect <$> connectionConfig
let source = toProducer $ sourceTBMChan queue $= antiflood $= logConduit False toByteString
let sink = forgetful =$= logConduit True _raw =$ eventSink state
liftIO $ func port server initialise sink source
disconnect
dchandler
-- | Forget failed decodings.
forgetful :: Monad m => Conduit (Either a b) m b
forgetful = awaitForever go where
go (Left _) = return ()
go (Right b) = yield b
-- | Block on receiving a message and invoke all matching handlers
-- concurrently.
eventSink :: MonadIO m => IRCState -> Consumer IrcEvent m ()
eventSink ircstate = awaitForever $ \event -> do
let event' = decodeUtf8 <$> event
ignored <- isIgnored ircstate event'
unless ignored $ do
handlers <- getHandlersFor event' . _eventHandlers <$> getInstanceConfig' ircstate
liftIO $ mapM_ (\h -> forkIO $ runReaderT (h event') ircstate) handlers
-- | Check if an event is ignored or not.
isIgnored :: MonadIO m => IRCState -> UnicodeEvent -> m Bool
isIgnored ircstate ev = do
iconf <- liftIO . atomically . readTVar . _instanceConfig $ ircstate
let ignoreList = _ignore iconf
return $
case _source ev of
User n -> (n, Nothing) `elem` ignoreList
Channel c n -> ((n, Nothing) `elem` ignoreList) || ((n, Just c) `elem` ignoreList)
Server _ -> False
-- |Get the event handlers for an event.
getHandlersFor :: Event a -> [EventHandler] -> [UnicodeEvent -> IRC ()]
getHandlersFor e ehs = [_eventFunc eh | eh <- ehs, _matchType eh `elem` [EEverything, eventType e]]
-- |A conduit which logs everything which goes through it.
logConduit :: MonadIO m => Bool -> (a -> ByteString) -> Conduit a m a
logConduit fromsrv f = awaitForever $ \x -> do
-- Print the log
liftIO $ do
now <- getCurrentTime
putStrLn $ unwords [ formatTime defaultTimeLocale "%c" now
, if fromsrv then "<---" else "--->"
, init . tail . show $ f x
]
-- And pass the message on
yield x
-- * Messaging
-- | Send a message as UTF-8, using TLS if enabled. This blocks if
-- messages are sent too rapidly.
send :: UnicodeMessage -> IRC ()
send = sendBS . fmap encodeUtf8
-- | Send a message, using TLS if enabled. This blocks if messages are
-- sent too rapidly.
sendBS :: IrcMessage -> IRC ()
sendBS msg = do
queue <- _sendqueue <$> connectionConfig
liftIO . atomically $ writeTBMChan queue msg
-- * Disconnecting
-- | Disconnect from the server, properly tearing down the TLS session
-- (if there is one).
disconnect :: IRC ()
disconnect = do
queueS <- _sendqueue <$> connectionConfig
-- Wait for all messages to be sent
liftIO . atomically $ do
empty <- isEmptyTBMChan queueS
unless empty retry
-- Then close the connection
disconnectNow
-- | Disconnect immediately, without waiting for messages to be sent.
disconnectNow :: IRC ()
disconnectNow = do
queueS <- _sendqueue <$> connectionConfig
liftIO . atomically $ closeTBMChan queueS