packages feed

hulk-0.1.1: src/Hulk/Server.hs

{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS -Wall -fno-warn-name-shadowing #-}
module Hulk.Server (start) where

import           Control.Applicative
import           Control.Concurrent
import           Control.Concurrent.Delay
import           Control.Monad
import           Control.Monad.Fix
import           Control.Monad.Reader
import qualified Data.Map                 as M
import           Data.Time
import           Network
import           Network.IRC
import           System.IO
import           System.IO.UTF8           as UTF8

import           Hulk.Client
import           Hulk.Providers           ()
import           Hulk.Types

-- | Start an IRC server with the given configuration.
start :: Config -> IO ()
start config = withSocketsDo $ do
  hSetBuffering stdout LineBuffering
  listenSock <- listenOn $ PortNumber (configListen config)
  envar <- newMVar Env { envClients = M.empty
                       , envNicks = M.empty
                       , envChannels = M.empty }
  forever $ do
    (handle,host,_port) <- accept listenSock
    hSetBuffering handle NoBuffering
    now <- getCurrentTime
    let conn = Conn { connRef = newRef handle
                    , connHostname = host
                    , connServerName = configHostname config
                    , connTime = now
                    }
    _ <- forkIO $ handleClient config handle envar conn
    return ()

-- | Handle a client connection.
handleClient :: Config -> Handle -> MVar Env -> Conn -> IO ()
handleClient config handle env conn = do
  let runHandle = runClientHandler config env handle conn
      runLine x y = runHandle $ makeLine x y
  pinger <- forkIO $ forever $ do delayMinutes 2; runLine PINGPONG []
  fix $ \loop -> do
    line <- catch (Right <$> UTF8.hGetLine handle)
                  (\e -> do killThread pinger
                            return $ Left e)
    case filter (not.newline) <$> line of
      Right []   -> loop
      Right line -> do runHandle (line++"\r"); loop
      Left _err  -> runLine DISCONNECT ["Connection lost."]

  where newline c = c=='\n' || c=='\r'

-- | Make an internal IRC event to give to the client handler.
makeLine :: Event -> [String] -> String
makeLine event params = (++"\r") $ encode $
  Message { msg_prefix = Nothing
          , msg_command = show event
          , msg_params = params }

-- | Handle a received line from the client.
runClientHandler :: Config -> MVar Env -> Handle -> Conn -> String -> IO ()
runClientHandler config env handle conn line = do
  now <- getCurrentTime
  modifyMVar_ env $ \env -> do
    (replies,env) <- runReaderT (runHulkIO $ handleLine env now conn line)
                                config
    mapM_ (handleReplies handle) replies
    return env

-- | Act on replies from the client.
handleReplies :: Handle -> Reply -> IO ()
handleReplies handle reply = do
  case reply of
    MessageReply ref msg -> sendMessage ref msg
    LogReply line -> logLine line
    Close -> hClose handle

-- | Send a message to a client.
sendMessage :: Ref -> Message -> IO ()
sendMessage (Ref handle) msg = do
  catch (UTF8.hPutStrLn handle (encode msg ++ "\r"))
        (\_ -> hClose handle)

-- | Add a line to the log file.
logLine :: String -> IO ()
logLine = UTF8.putStrLn