link/src/Link/Client.hs

154 lines
6.2 KiB
Haskell

module Link.Client where
import Control.Concurrent.STM
import Control.Concurrent
import Control.Exception hiding (handle)
import Control.Monad (void, forever, when, unless, forM_)
import Data.Time (getCurrentTime, diffUTCTime)
import System.IO (hGetLine, Handle)
import System.Timeout (timeout)
import Text.Printf (printf, hPrintf)
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import Link.Protocol
import Link.Types
sendMessage :: Client -> Message -> STM ()
sendMessage Client {..} = writeTChan clientChan
sendMessageIO :: Client -> Message -> IO ()
sendMessageIO client = atomically . sendMessage client
tellMessage :: Channel -> Message -> STM ()
tellMessage Channel {..} = writeTChan channelChan
printToHandle :: Handle -> String -> IO ()
printToHandle handle = hPrintf handle "%s\n"
sendResponse :: Client -> Message -> IO ()
sendResponse Client {..} = printToHandle clientHandle . formatMessage
runClient :: Server -> Client -> IO ()
runClient Server {..} client@Client {..} = do
clientAlive <- newMVar True
pingThread <- forkIO $ ping clientAlive `finally` killClient clientAlive
commandThread <- forkIO $ readCommands `finally` killClient clientAlive
run clientAlive `finally` do
killThread pingThread
killThread commandThread
clientChannelMap <- readTVarIO clientChannelChans
forM_ (Map.keys clientChannelMap) $ \channelName ->
handleMessage (Leave channelName) clientAlive
where
pingDelay = 120
pingDelayMicros = pingDelay * 1000 * 1000
killClient clientAlive = void $ swapMVar clientAlive False
ping clientAlive = do
sendMessageIO client Ping
threadDelay pingDelayMicros
now <- getCurrentTime
pongTime <- readMVar clientPongTime
if diffUTCTime now pongTime > fromIntegral pingDelay
then killClient clientAlive
else ping clientAlive
run clientAlive = do
alive <- readMVar clientAlive
if not alive
then printf "Closing connection: %s\n" (userName clientUser)
else do
r <- try . timeout pingDelayMicros . atomically $ do
clientChannelMap <- readTVar clientChannelChans
foldr (orElse . readTChan) retry $ clientChan : Map.elems clientChannelMap
case r of
Left (e :: SomeException) -> printf "Exception: %s\n" (show e)
Right g -> do
case g of
Nothing -> return ()
Just message -> handleMessage message clientAlive
run clientAlive
readCommands = forever $ do
command <- hGetLine clientHandle
printf "<%s>: %s\n" (userName clientUser) command
case parseCommand command of
Nothing -> printf "Could not parse command: %s\n" command
Just c -> sendMessageIO client c
handleMessage (Msg user msg) _ =
withMVar serverUsers $ \clientMap ->
case Map.lookup user clientMap of
Nothing -> sendResponse client $ NoSuchUser (userName user)
Just client' -> sendMessageIO client' $ MsgReply clientUser msg
handleMessage Pong _ = do
now <- getCurrentTime
void $ swapMVar clientPongTime now
handleMessage Quit clientAlive = killClient clientAlive
handleMessage (Join channelName) _ = atomically $ do
-- get user's channels
clientChannelMap <- readTVar clientChannelChans
-- if user has not already joined the channel
unless (Map.member channelName clientChannelMap) $ do
-- get server channels
channelMap <- readTVar serverChannels
channel@Channel {channelChan} <- case Map.lookup channelName channelMap of
Just (channel@Channel {channelUsers}) -> do
-- if the channel already exists on the server, add user to it
modifyTVar' channelUsers $ Set.insert clientUser
return channel
Nothing -> do
-- else create a new channel with this user in it
channel <- newChannel channelName $ Set.singleton clientUser
-- and add it to the server
modifyTVar' serverChannels $ Map.insert channelName channel
return channel
-- duplicate channel TChan for this user
clientChannelChan <- dupTChan channelChan
-- and add it to the users's channels
modifyTVar' clientChannelChans $ Map.insert channelName clientChannelChan
-- send a JOINED message to the channel for this user
tellMessage channel $ Joined channelName clientUser
handleMessage (Leave channelName) _ = atomically $ do
-- get server channels
channelMap <- readTVar serverChannels
case Map.lookup channelName channelMap of
-- if channel exists on the server
Just (channel@Channel {channelUsers}) -> do
-- remove this user from the channel
modifyTVar' channelUsers $ Set.delete clientUser
-- get users in the channel
users <- readTVar channelUsers
-- if there are no users in the channel
when (Set.null users) $
-- remove the channel from the server
modifyTVar' serverChannels $ Map.delete channelName
-- remove the channel from the user's channels
modifyTVar' clientChannelChans $ Map.delete channelName
-- send a LEFT message to the channel for this user
tellMessage channel $ Leaved channelName clientUser
-- nothing to do if the channel does not exist
Nothing -> return ()
handleMessage (Names channelName) _ = atomically $ do
channelMap <- readTVar serverChannels
users <- case Map.lookup channelName channelMap of
Just (Channel {channelUsers}) -> readTVar channelUsers
Nothing -> return Set.empty
sendMessage client $ NamesReply channelName users
handleMessage (Tell channelName msg) _ = atomically $ do
channelMap <- readTVar serverChannels
case Map.lookup channelName channelMap of
Just channel -> tellMessage channel $ TellReply channelName clientUser msg
Nothing -> return ()
handleMessage message _ = sendResponse client message