This commit is contained in:
qvalentin 2023-01-27 18:34:28 +01:00
parent cdfc0849cf
commit 839f6df5f8
9 changed files with 346 additions and 61 deletions

View File

@ -4,3 +4,11 @@
`stack build` `stack build`
`stack stack run` `stack stack run`
## Debug
Using websocat
`websocat ws://127.0.0.1:9160`
`curl --data 'body data' localhost:8081`

View File

@ -26,6 +26,11 @@ dependencies:
- http-types - http-types
- warp - warp
- websockets - websockets
- aeson
- text
- bytestring
- uuid
- lifted-base
ghc-options: ghc-options:
- -Wall - -Wall

View File

@ -1,14 +1,24 @@
{-# LANGUAGE DerivingVia #-}
module Lib module Lib
( runBothServers, ( runBothServers,
) )
where where
import ClassyPrelude import ClassyPrelude
import Types.AppTypes
import WebServer (runWebServer) import WebServer (runWebServer)
import WebSocket (initMVarState, runWebSocketServer) import WebSocket (initMVarState, runWebSocketServer)
runBothServers :: IO () runBothServers :: IO ()
runBothServers = do runBothServers = do
mVarState <- initMVarState connectedClientsState <- initMVarState
_ <- concurrently (runWebSocketServer mVarState) (runWebServer mVarState)
let env =
Env
{ connectedClientsState = connectedClientsState,
profile = Dev
}
_ <- concurrently (unApp runWebSocketServer env) (unApp runWebServer env)
return () return ()

View File

@ -0,0 +1,29 @@
{-# LANGUAGE DerivingVia #-}
module Types.AppTypes (Env (..), App (..), getConnectedClientState, HasConnectedClientState, App, AppProfile (Prod, Dev)) where
import ClassyPrelude
import Types.ConnectionState (ConnectedClientsState)
data AppProfile = Prod | Dev
data Env = Env
{ connectedClientsState :: ConnectedClientsState,
profile :: AppProfile
}
class HasConnectedClientState a where
getConnectedClientState :: a -> ConnectedClientsState
instance HasConnectedClientState Env where
getConnectedClientState = connectedClientsState
newtype App env a = App {unApp :: env -> IO a}
deriving
( Functor,
Applicative,
Monad,
MonadReader env,
MonadIO
)
via ReaderT env IO

View File

@ -0,0 +1,21 @@
module Types.ConnectionState
( Client (..),
Client,
ConnectedClientsState,
ConnectedClients,
)
where
import ClassyPrelude
import Data.UUID (UUID)
import Network.WebSockets qualified as WS
data Client = Client
{ uuid :: UUID,
name :: Text,
conn :: WS.Connection
}
type ConnectedClientsState = MVar ConnectedClients
type ConnectedClients = [Client]

View File

@ -0,0 +1,18 @@
{-# LANGUAGE DeriveGeneric #-}
module Types.Participant (Participant) where
import ClassyPrelude
import Data.Aeson (FromJSON, ToJSON)
data Participant = Participant
{ jid :: Text,
email :: Text,
displayName :: Text,
avatarURL :: Text
}
deriving (Generic, Show)
instance ToJSON Participant
instance FromJSON Participant

View File

@ -0,0 +1,19 @@
{-# LANGUAGE DeriveGeneric #-}
module Types.RoomData (RoomData) where
import ClassyPrelude
import Data.Aeson (FromJSON, ToJSON)
import Types.Participant (Participant)
data RoomData = RoomData
{ roomName :: RoomName,
participants :: [Participant]
}
deriving (Generic, Show)
type RoomName = Text
instance ToJSON RoomData
instance FromJSON RoomData

View File

@ -1,25 +1,101 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
module WebServer (runWebServer) where module WebServer (runWebServer) where
import ClassyPrelude -- import AppTypes (HasConnectedClientState)
import ClassyPrelude hiding (decodeUtf8)
import Data.Text.Encoding
import Network.HTTP.Types import Network.HTTP.Types
import Network.Wai import Network.Wai
import Network.Wai.Handler.Warp (run) import Network.Wai.Handler.Warp (run)
import WebSocket (ServerState, broadcast) import Types.AppTypes (Env (connectedClientsState),
HasConnectedClientState (..), unApp)
import WebSocket (broadcast)
-- todo: use ReaderT instead of curring the state newtype WebApp env a = WebApp {unWebApp :: env -> IO a}
-- then add a MVar for storing the room Data, including users that are not in any room yet deriving
( Functor,
Applicative,
Monad,
MonadReader env,
MonadIO
)
via ReaderT env IO
app :: MVar ServerState -> Application data WebEnv = WebEnv
app state req respond = do { appEnv :: Env,
putStrLn "I've done some IO here" request :: Request,
withMVar state $ \currenState -> broadcast "body of req" currenState respond :: Response -> IO ResponseReceived
respond $ }
responseLBS
status200
[("Content-Type", "text/plain")]
""
runWebServer :: MVar ServerState -> IO () instance HasConnectedClientState WebEnv where
runWebServer state = do getConnectedClientState = connectedClientsState . appEnv
putStrLn "http://localhost:8080/"
run 8080 $ app state class HasWebEnv a where
getRequest :: a -> Request
getRespond :: a -> (Response -> IO ResponseReceived)
instance HasWebEnv WebEnv where
getRequest = request
getRespond = respond
broadCastToClients ::
( MonadIO m,
HasConnectedClientState env,
MonadReader env m
) =>
Text ->
m ()
broadCastToClients message = do
state <- getConnectedClientState <$> ask
liftIO $ withMVar state $ \currenState -> broadcast message currenState
getRequestBody ::
( MonadIO m,
HasWebEnv env,
MonadReader env m
) =>
m Text
getRequestBody = do
request <- getRequest <$> ask
liftIO $ (decodeUtf8 . toStrict) <$> consumeRequestBodyStrict request
app ::
( MonadIO m,
HasWebEnv env,
HasConnectedClientState env,
MonadReader env m
) =>
m ResponseReceived
app =
do
liftIO $ putStrLn "I've done some IO here"
getRequestBody >>= broadCastToClients
respond' <- getRespond <$> ask
liftIO $ respond' $ responseLBS status200 [("Content-Type", "text/plain")] ""
runWebApp ::
( MonadIO m,
MonadReader Env m
) =>
m Application
runWebApp = do
env <- ask
return
( \req res -> do
let webEnv = WebEnv {appEnv = env, request = req, respond = res}
unWebApp app webEnv
)
runWebServer ::
( MonadIO m,
MonadReader Env m
) =>
m ()
runWebServer = do
putStrLn "http://localhost:8081/"
runWebApp >>= liftIO . run 8081
return ()

View File

@ -1,52 +1,151 @@
module WebSocket (broadcast, initMVarState, runWebSocketServer, ServerState, Client) where {-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}
module WebSocket (broadcast, initMVarState, runWebSocketServer) where
import ClassyPrelude import ClassyPrelude
import Data.UUID (UUID)
import Data.UUID.V4 (nextRandom)
import Network.WebSockets qualified as WS import Network.WebSockets qualified as WS
import Types.AppTypes
( Env (connectedClientsState),
HasConnectedClientState (getConnectedClientState),
)
import Types.ConnectionState
( Client (..),
ConnectedClients,
ConnectedClientsState,
)
type Client = (Text, WS.Connection) addClient :: Client -> ConnectedClients -> ConnectedClients
type ServerState = [Client]
addClient :: Client -> ServerState -> ServerState
addClient client clients = client : clients addClient client clients = client : clients
removeClient :: Client -> ServerState -> ServerState removeClient :: UUID -> ConnectedClients -> ConnectedClients
removeClient client = filter ((/= fst client) . fst) removeClient toRemove = filter ((/= toRemove) . uuid)
newServerState :: ServerState newConnectedClients :: ConnectedClients
newServerState = [] newConnectedClients = []
broadcast :: Text -> ServerState -> IO () broadcast :: Text -> ConnectedClients -> IO ()
broadcast message clients = do broadcast message clients = do
putStrLn message putStrLn message
forM_ clients $ \(_, conn) -> WS.sendTextData conn message forM_ clients $ \client -> WS.sendTextData (conn client) message
initMVarState :: IO (MVar ServerState) initMVarState :: IO (MVar ConnectedClients)
initMVarState = newMVar newServerState initMVarState = newMVar newConnectedClients
runWebSocketServer :: MVar ServerState -> IO () runWebSocketServer ::
runWebSocketServer state = do ( MonadIO m,
WS.runServer "127.0.0.1" 9160 $ webSocketApplication state MonadReader Env m
) =>
m ()
runWebSocketServer = do
putStrLn "Websocket up at 127.0.0.1:9160"
state <- getConnectedClientState <$> ask
wsApp <- runWSApp
liftIO $ WS.runServer "127.0.0.1" 9160 $ wsApp
webSocketApplication :: MVar ServerState -> WS.ServerApp runWSApp ::
webSocketApplication state pending = do ( MonadIO m,
MonadReader Env m
) =>
m WS.ServerApp
runWSApp = do
env <- ask
return
( \pending -> do
putStrLn "pending request" putStrLn "pending request"
conn <- WS.acceptRequest pending conn <- WS.acceptRequest pending
uuid <- nextRandom
let wsEnv = WSEnv {appEnv = env, connection = conn, clientId = uuid}
WS.withPingThread conn 30 (return ()) $ do WS.withPingThread conn 30 (return ()) $ do
msg <- WS.receiveData conn unWSApp wsApp wsEnv
)
newClient ::
( MonadIO m,
HasWSEnv env,
MonadReader env m
) =>
Text ->
m Client
newClient name = do
env <- ask
return $ Client {uuid = getClientId env, name = name, conn = getConn env}
newtype WSApp env a = WSApp {unWSApp :: env -> IO a}
deriving
( Functor,
Applicative,
Monad,
MonadReader env,
MonadIO,
MonadUnliftIO
)
via ReaderT env IO
-- instance MonadBaseControl IO m => MonadBaseControl IO (WSApp env n)
-- where
wsApp ::
( HasWSEnv env,
HasConnectedClientState env,
MonadReader env m,
MonadUnliftIO m
) =>
m ()
wsApp = do
state <- getConnectedClientState <$> ask
msg <- getMessage
putStrLn msg putStrLn msg
let client = ("hall", conn) client <- newClient msg
flip finally (disconnect client) $ do
modifyMVar_ state $ \s -> do modifyMVar_ state $ \s -> do
let s' = addClient client s let s' = addClient client s
return s' return s'
flip finally disconnectWsClient $ do
forever $ do forever $ do
currentMsg <- WS.receiveData conn currentMsg <- getMessage
putStrLn currentMsg putStrLn currentMsg
where
disconnect client = do getMessage ::
putStrLn "disconnect" ( HasWSEnv env,
modifyMVar state $ \s -> MonadIO m,
let s' = removeClient client s MonadReader env m
) =>
m Text
getMessage = do
conn' <- getConn <$> ask
liftIO $ WS.receiveData conn'
disconnectWsClient ::
( MonadIO m,
HasWSEnv env,
HasConnectedClientState env,
MonadReader env m
) =>
m ()
disconnectWsClient = do
clientId <- getClientId <$> ask
state <- getConnectedClientState <$> ask
liftIO $ modifyMVar state $ \s ->
let s' = removeClient clientId s
in return in return
(s', s') (s', ())
data WSEnv = WSEnv
{ appEnv :: Env,
connection :: WS.Connection,
clientId :: UUID
}
instance HasConnectedClientState WSEnv where
getConnectedClientState = connectedClientsState . appEnv
class HasWSEnv a where
getConn :: a -> WS.Connection
getClientId :: a -> UUID
instance HasWSEnv WSEnv where
getConn = connection
getClientId = clientId