irc-conduit-0.1.0.0: Network/IRC/Conduit.hs
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE OverloadedStrings #-}
-- |Conduits for serialising and deserialising IRC messages.
--
-- The 'Event', 'Message', and 'Source' types are parameterised on the
-- underlying representation, and are functors. Decoding and encoding
-- only work in terms of 'ByteString's, but the generality is provided
-- so that programs using this library can operate in terms of 'Text',
-- or some other more useful representation, with great ease.
module Network.IRC.Conduit
( -- *Type synonyms
ChannelName
, NickName
, ServerName
, Reason
, IsModeSet
, ModeFlag
, ModeArg
, NumericArg
, Target
, IrcEvent
, IrcSource
, IrcMessage
-- *Messages
, Event(..)
, Source(..)
, Message(..)
-- *Conduits
, ircDecoder
, ircLossyDecoder
, ircEncoder
, floodProtector
-- *Networking
, ircClient
, ircTLSClient
, ircWithConn
-- *Utilities
, rawMessage
, toByteString
) where
import Control.Applicative ((*>))
import Control.Concurrent (newMVar, takeMVar, putMVar, threadDelay)
import Control.Concurrent.Async (Concurrently(..))
import Control.Monad (when)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.ByteString (ByteString)
import Data.Conduit (Conduit, Consumer, Producer, (=$), ($$), (=$=), awaitForever, yield)
import Data.Conduit.Network (AppData, clientSettings, runTCPClient, appSource, appSink)
import Data.Conduit.Network.TLS (tlsClientConfig, runTLSClient)
import Data.Monoid ((<>))
import Data.Time.Clock (NominalDiffTime, getCurrentTime, addUTCTime, diffUTCTime)
import Network.IRC.Conduit.Internal
import System.IO.Error (catchIOError)
-- *Conduits
-- |A conduit which takes as input bytestrings representing encoded
-- IRC messages, and decodes them to events. If decoding fails, the
-- original bytestring is just passed through.
ircDecoder :: Monad m => Conduit ByteString m (Either ByteString IrcEvent)
ircDecoder = chunked =$= awaitForever (yield . fromByteString)
-- |Like 'ircDecoder', but discards messages which could not be
-- decoded.
ircLossyDecoder :: Monad m => Conduit ByteString m IrcEvent
ircLossyDecoder = chunked =$= awaitForever lossy
where
lossy bs = either (\_ -> return ()) yield $ fromByteString bs
-- |A conduit which takes as input irc messages, and produces as
-- output the encoded bytestring representation.
ircEncoder :: Monad m => Conduit IrcMessage m ByteString
ircEncoder = awaitForever (yield . (<>"\r\n") . toByteString)
-- |A conduit which rate limits output sent downstream. Awaiting on
-- this conduit will block, even if there is output ready, until the
-- time limit has passed.
floodProtector :: MonadIO m
=> NominalDiffTime
-- ^The minimum time between sending adjacent messages.
-> IO (Conduit a m a)
floodProtector delay = do
now <- getCurrentTime
mvar <- newMVar now
return $ conduit mvar
where
conduit mvar = awaitForever $ \val -> do
-- Block until the delay has passed
liftIO $ do
lastT <- takeMVar mvar
now <- getCurrentTime
let next = addUTCTime delay lastT
when (next < now) $
threadDelay . ceiling $ 1000000 * diffUTCTime next now
-- Update the time
now' <- getCurrentTime
putMVar mvar now'
-- Send the value downstream
yield val
-- *Networking
-- |Connect to a network server, without TLS, and concurrently run the
-- producer and consumer.
ircClient :: Int
-- ^The port number
-> ByteString
-- ^The hostname
-> IO ()
-- ^Any initialisation work (started concurrently with the
-- producer and consumer)
-> Consumer (Either ByteString IrcEvent) IO ()
-- ^The consumer of irc events
-> Producer IO IrcMessage
-- ^The producer of irc messages
-> IO ()
ircClient port host = ircWithConn . runTCPClient $ clientSettings port host
-- |Like 'ircClient', but with TLS.
ircTLSClient :: Int -> ByteString -> IO () -> Consumer (Either ByteString IrcEvent) IO () -> Producer IO IrcMessage -> IO ()
ircTLSClient port host = ircWithConn . runTLSClient $ tlsClientConfig port host
-- |Run the IRC conduits using a provided connection.
ircWithConn :: ((AppData -> IO ()) -> IO ())
-- ^The initialised connection.
-> IO ()
-> Consumer (Either ByteString IrcEvent) IO ()
-> Producer IO IrcMessage
-> IO ()
ircWithConn runner start cons prod = go `catchIOError` ignore
where
-- Start the connection and concurrently run the initialiser,
-- event consumer, and message sources: terminating as soon as one
-- throws an exception.
go = runner $ \appdata ->
runConcurrently $
Concurrently start *>
Concurrently (appSource appdata =$= exceptionalConduit $$ ircDecoder =$ cons) *>
Concurrently (prod $$ ircEncoder =$ appSink appdata)
-- Ignore all exceptions and just halt.
ignore _ = return ()