pipes-irc-server/src/Pipes/IRC/Server.hs

234 lines
7.6 KiB
Haskell

{-# LANGUAGE OverloadedStrings #-}
module Pipes.IRC.Server
( startIrcServer
)
where
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Lens as L
import Control.Monad
import Control.Monad.RWS
import Data.ByteString.Char8 as BS
import Data.Map as M
import Data.Maybe
import Data.Set as S
import Data.Time.Clock
import Network.Socket as NS
import Pipes
import Pipes.Attoparsec
import Pipes.Concurrent as PC
import Pipes.IRC.Message
import Pipes.IRC.Server.EventHandler
import Pipes.IRC.Server.IrcMonad
import Pipes.IRC.Server.Log
import Pipes.IRC.Server.MessageHandler
import Pipes.IRC.Server.Server
import Pipes.IRC.Server.Types
import Pipes.Network.TCP as PN
version :: ByteString
version = "0.1a"
parseMessage :: Producer ByteString IO ()
-> Producer (Either ByteString IrcMessage) IO ()
parseMessage prod = do
void $ for (parsed parseMsgOrLine prod) $ \res ->
case res of
(Left _) -> yield $ Left "ERROR Bad Parse"
(Right val) -> yield $ Right val
return ()
renderMessage :: Pipe IrcMessage ByteString IO ()
renderMessage = forever $ do
msg <- await
let output = renderIrcMessage msg
yield output
filterMsgs :: Pipe (Either ByteString IrcMessage) IrcMessage IO ()
filterMsgs = forever $ do
cmd <- await
case cmd of
Left bs -> liftIO $ logLine $ BS.concat ["BAD COMMAND: ", bs]
Right c -> yield c
addIrcConnection :: ServerState -> IrcConnection -> IO Int
addIrcConnection srv client = do
let clients = srv ^. ircConnections
ids = srv ^. ircConnIds
cid <- atomically $ do
lastId <- readTVar ids
let newId = lastId + 1
writeTVar ids newId
return newId
atomically $ modifyTVar' clients $ M.insert cid client
return cid
delIrcConnection :: ServerState -> Int -> IO ()
delIrcConnection srv cid = atomically $ do
cs <- readTVar (srv ^. ircConnections)
case M.lookup cid cs of
Just conn -> do
let nn = case conn ^. reg of
Unreg{ _rcvdNick = Just n } -> n
RegUser{ _regdNick = NickName n _ _ } -> n
_ -> ""
modifyTVar' (srv ^. ircState) $ ircDelUser nn
_ -> return ()
modifyTVar' (srv ^. ircConnections) $ M.delete cid
ircMonadTransaction :: ServerState -> Int -> RegState -> IrcMonad ()
-> IO (RegState, [IrcEvent])
ircMonadTransaction srv cid userReg action = do
cmap <- readTVarIO $ srv ^. ircConnections
let hostname = view (at cid . traverse . hname) cmap
curTime <- getCurrentTime
atomically $ do
sState <- readTVar $ srv ^. ircState
let sConf = srv ^. ircConfig
let cState = ClientState { _clientReg = userReg
, _clientServer = sState
, _clientHost = hostname
, _clientConn = cid }
let (_, newState, events) = runRWS action sConf cState
writeTVar (srv ^. ircState) (newState ^. clientServer)
modifyTVar' (srv ^. ircConnections) $
M.adjust ((reg .~ newState ^. clientReg).(lastCom .~ curTime)) cid
return (newState ^. clientReg, events)
runIrcMonad :: ServerState -> Int -> IrcMonad () -> IO ()
runIrcMonad srv cid action = do
cs <- readTVarIO (srv ^. ircConnections)
let cReg = maybe (Unreg Nothing Nothing Nothing) (view reg) (cs ^. at cid)
(_, events) <- ircMonadTransaction srv cid cReg action
forM_ events $ ircEventHandler srv
cmdHandler :: ServerState -> Int -> Consumer IrcMessage IO ()
cmdHandler srv cid =
let cReg = Unreg Nothing Nothing Nothing
in do
conns <- liftIO $ readTVarIO $ srv ^. ircConnections
case M.lookup cid conns of
Just c -> handle (c ^. hname) cReg
Nothing -> return ()
where
handle h userReg = do
-- wait for the next command
nextMsg <- await
liftIO $ logMsg nextMsg (fromMaybe "unknown" h) userReg
-- run the handler in a transaction
(newReg, events) <-
liftIO $ ircMonadTransaction srv cid userReg (ircMessageHandler nextMsg)
-- handle resulting events
aliveL <- liftIO $ forM events $ ircEventHandler srv
-- loop for the next command
when (and aliveL) $ handle h newReg
idlePinger :: ServerState -> Int -> IO ()
idlePinger srv cid =
let
pingMsg = IrcMessage Nothing (Left PING)
[":" `append` (srv ^. ircConfig . ircHostName)]
oneMinute = 60 * 1000000 -- microseconds
getLastCom = do conns <- readTVarIO (srv ^. ircConnections)
return $ conns ! cid ^. lastCom
resetPong = atomically $ modifyTVar' (srv ^. ircConnections) $
M.adjust (gotPong .~ False) cid
checkPong = do conns <- readTVarIO (srv ^. ircConnections)
return $ conns ! cid ^. gotPong
timeoutLoop b = when b $ do
threadDelay oneMinute
curTime <- getCurrentTime
time <- getLastCom
let diffTime = toRational . diffUTCTime curTime $ time
if diffTime > 60
then do
resetPong
atomically $ do
conns <- readTVar (srv ^. ircConnections)
void $ PC.send (conns ! cid ^. out) pingMsg
return ()
threadDelay oneMinute
checkPong >>= timeoutLoop
else timeoutLoop True
in do
-- timeoutLoop will call itself repeatedly until a timeout occurs
timeoutLoop True
-- Then we need to send a timeout message
runIrcMonad srv cid $ doQuit (Just "Ping timeout")
listenHandler :: ServerState -> (Socket, SockAddr) -> IO ()
listenHandler srv (lsock, _) =
forever $ acceptFork lsock $ \(csock, caddr) -> do
let sockWriter = toSocket csock
sockReader = fromSocket csock 4096
(hName, _) <- getNameInfo [] True False caddr
(writeEnd, readEnd) <- spawn unbounded
curTime <- getCurrentTime
logLine $ BS.pack $
"Accepted connection from " ++ fromMaybe "unknown" hName
let client = IrcConnection
{ _sock = csock
, _addr = caddr
, _hname = fmap BS.pack hName
, _out = writeEnd
, _reg = Unreg Nothing Nothing Nothing
, _lastCom = curTime
, _gotPong = False
}
cid <- addIrcConnection srv client
let handler = cmdHandler srv cid
r <- async $ runEffect $
parseMessage sockReader >-> filterMsgs >-> handler
link r
idle <- async $ idlePinger srv cid
link idle
w <- async $ runEffect $
fromInput readEnd >-> renderMessage >-> sockWriter
link w
void $ waitAnyCancel [r, w, idle]
logLine $ BS.pack $
"Connection from " ++ fromMaybe "unknown" hName ++ " terminated"
delIrcConnection srv cid
mkIrcServer :: IrcConfig -> IO ServerState
mkIrcServer config = do
let nks = S.empty
urs = M.empty
chs = M.empty
srv = IrcServer nks urs chs version
tvState <- newTVarIO srv
tvCns <- newTVarIO M.empty
tvRef <- newTVarIO 0
return $ ServerState tvState config tvCns tvRef
startIrcServer :: IrcConfig -> IO (Async ())
startIrcServer config = do
srv <- mkIrcServer config
let sHost = srv ^. ircConfig . ircHost
sPort = srv ^. ircConfig . ircPort
logLine $ BS.pack $
mconcat ["Starting server on ", show sHost, " ", show sPort]
async $ PN.listen sHost sPort (listenHandler srv)