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 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
- warp
- websockets
- aeson
- text
- bytestring
- uuid
- lifted-base
ghc-options:
- -Wall

View File

@ -1,14 +1,24 @@
{-# LANGUAGE DerivingVia #-}
module Lib
( runBothServers,
)
where
import ClassyPrelude
import Types.AppTypes
import WebServer (runWebServer)
import WebSocket (initMVarState, runWebSocketServer)
runBothServers :: IO ()
runBothServers = do
mVarState <- initMVarState
_ <- concurrently (runWebSocketServer mVarState) (runWebServer mVarState)
connectedClientsState <- initMVarState
let env =
Env
{ connectedClientsState = connectedClientsState,
profile = Dev
}
_ <- concurrently (unApp runWebSocketServer env) (unApp runWebServer env)
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
import ClassyPrelude
-- import AppTypes (HasConnectedClientState)
import ClassyPrelude hiding (decodeUtf8)
import Data.Text.Encoding
import Network.HTTP.Types
import Network.Wai
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
-- then add a MVar for storing the room Data, including users that are not in any room yet
newtype WebApp env a = WebApp {unWebApp :: env -> IO a}
deriving
( Functor,
Applicative,
Monad,
MonadReader env,
MonadIO
)
via ReaderT env IO
app :: MVar ServerState -> Application
app state req respond = do
putStrLn "I've done some IO here"
withMVar state $ \currenState -> broadcast "body of req" currenState
respond $
responseLBS
status200
[("Content-Type", "text/plain")]
""
data WebEnv = WebEnv
{ appEnv :: Env,
request :: Request,
respond :: Response -> IO ResponseReceived
}
runWebServer :: MVar ServerState -> IO ()
runWebServer state = do
putStrLn "http://localhost:8080/"
run 8080 $ app state
instance HasConnectedClientState WebEnv where
getConnectedClientState = connectedClientsState . appEnv
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 Data.UUID (UUID)
import Data.UUID.V4 (nextRandom)
import Network.WebSockets qualified as WS
import Types.AppTypes
( Env (connectedClientsState),
HasConnectedClientState (getConnectedClientState),
)
import Types.ConnectionState
( Client (..),
ConnectedClients,
ConnectedClientsState,
)
type Client = (Text, WS.Connection)
type ServerState = [Client]
addClient :: Client -> ServerState -> ServerState
addClient :: Client -> ConnectedClients -> ConnectedClients
addClient client clients = client : clients
removeClient :: Client -> ServerState -> ServerState
removeClient client = filter ((/= fst client) . fst)
removeClient :: UUID -> ConnectedClients -> ConnectedClients
removeClient toRemove = filter ((/= toRemove) . uuid)
newServerState :: ServerState
newServerState = []
newConnectedClients :: ConnectedClients
newConnectedClients = []
broadcast :: Text -> ServerState -> IO ()
broadcast :: Text -> ConnectedClients -> IO ()
broadcast message clients = do
putStrLn message
forM_ clients $ \(_, conn) -> WS.sendTextData conn message
forM_ clients $ \client -> WS.sendTextData (conn client) message
initMVarState :: IO (MVar ServerState)
initMVarState = newMVar newServerState
initMVarState :: IO (MVar ConnectedClients)
initMVarState = newMVar newConnectedClients
runWebSocketServer :: MVar ServerState -> IO ()
runWebSocketServer state = do
WS.runServer "127.0.0.1" 9160 $ webSocketApplication state
runWebSocketServer ::
( MonadIO m,
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
webSocketApplication state pending = do
runWSApp ::
( MonadIO m,
MonadReader Env m
) =>
m WS.ServerApp
runWSApp = do
env <- ask
return
( \pending -> do
putStrLn "pending request"
conn <- WS.acceptRequest pending
uuid <- nextRandom
let wsEnv = WSEnv {appEnv = env, connection = conn, clientId = uuid}
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
let client = ("hall", conn)
flip finally (disconnect client) $ do
client <- newClient msg
modifyMVar_ state $ \s -> do
let s' = addClient client s
return s'
flip finally disconnectWsClient $ do
forever $ do
currentMsg <- WS.receiveData conn
currentMsg <- getMessage
putStrLn currentMsg
where
disconnect client = do
putStrLn "disconnect"
modifyMVar state $ \s ->
let s' = removeClient client s
getMessage ::
( HasWSEnv env,
MonadIO m,
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
(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