feat: add notify command and cli args

This commit is contained in:
qvalentin 2025-01-20 20:40:17 +01:00
parent 92a97e8f8a
commit fd18fa1a8b
10 changed files with 141 additions and 24 deletions

View file

@ -4,10 +4,24 @@ import ClassyPrelude
import GHC.IO.Encoding (setLocaleEncoding) import GHC.IO.Encoding (setLocaleEncoding)
import GHC.IO.Encoding.UTF8 (utf8) import GHC.IO.Encoding.UTF8 (utf8)
import Lib (runBothServers) import Lib (runBothServers)
import Options.Applicative
import Types.Config (ServerOptions, serverOptionsParser)
main :: IO () main :: IO ()
main = do main = do
setLocaleEncoding utf8 setLocaleEncoding utf8
hSetBuffering stdout LineBuffering hSetBuffering stdout LineBuffering
hSetBuffering stderr LineBuffering hSetBuffering stderr LineBuffering
runBothServers
opts <- execParser serverOptions
runBothServers opts
serverOptions :: ParserInfo ServerOptions
serverOptions =
info
(serverOptionsParser <**> helper)
( fullDesc
<> progDesc "Run the server with specified options"
<> header "Haskell Server - A configurable server application"
)

View file

@ -33,6 +33,7 @@ library
State.RoomDataState State.RoomDataState
State.RoomsState State.RoomsState
Types.AppTypes Types.AppTypes
Types.Config
Types.ConnectionState Types.ConnectionState
Types.Participant Types.Participant
Types.RoomData Types.RoomData
@ -62,6 +63,7 @@ library
, http-types , http-types
, lifted-base , lifted-base
, mtl , mtl
, optparse-applicative
, process , process
, text , text
, time , time
@ -90,6 +92,7 @@ executable jitsi-rooms-exe
, jitsi-rooms , jitsi-rooms
, lifted-base , lifted-base
, mtl , mtl
, optparse-applicative
, process , process
, text , text
, time , time

View file

@ -35,6 +35,7 @@ dependencies:
- time - time
- wai-extra - wai-extra
- process - process
- optparse-applicative
ghc-options: ghc-options:
- -Wall - -Wall

View file

@ -9,11 +9,12 @@ import ClassyPrelude
import State.ConnectedClientsState (initConnectionsState) import State.ConnectedClientsState (initConnectionsState)
import State.RoomsState (initRoomsState) import State.RoomsState (initRoomsState)
import Types.AppTypes import Types.AppTypes
import Types.Config (ServerOptions)
import WebServer (runWebServer) import WebServer (runWebServer)
import WebSocket.Server (runWebSocketServer) import WebSocket.Server (runWebSocketServer)
runBothServers :: IO () runBothServers :: ServerOptions -> IO ()
runBothServers = do runBothServers serverOptions = do
connectedClientsState <- initConnectionsState connectedClientsState <- initConnectionsState
roomsState <- initRoomsState roomsState <- initRoomsState
@ -21,7 +22,8 @@ runBothServers = do
Env Env
{ connectedClientsState = connectedClientsState, { connectedClientsState = connectedClientsState,
profile = Dev, profile = Dev,
roomsState = roomsState roomsState = roomsState,
config = serverOptions
} }
_ <- concurrently (unApp runWebSocketServer env) (unApp runWebServer env) _ <- concurrently (unApp runWebSocketServer env) (unApp runWebServer env)

View file

@ -27,6 +27,8 @@ import State.RoomsState
) )
import System.Process import System.Process
import Text.Printf (printf) import Text.Printf (printf)
import Types.AppTypes (HasConfig (getConfig))
import Types.Config (ServerOptions (..))
import Types.RoomData (RoomData, prettyPrintOpenedRoom) import Types.RoomData (RoomData, prettyPrintOpenedRoom)
import Types.WebEnv import Types.WebEnv
( HasWebEnv (getRequest), ( HasWebEnv (getRequest),
@ -40,7 +42,8 @@ roomDataHandler ::
MonadError ResponseReceived m, MonadError ResponseReceived m,
MonadRoomDataStateRead m, MonadRoomDataStateRead m,
MonadRoomDataStateModify m, MonadRoomDataStateModify m,
MonadBroadcast m MonadBroadcast m,
HasConfig env
) => ) =>
m ResponseReceived m ResponseReceived
roomDataHandler = do roomDataHandler = do
@ -115,16 +118,21 @@ success = do
[("Content-Type", "text/plain")] [("Content-Type", "text/plain")]
"" ""
notifyRoomOpend :: (MonadIO m) => RoomData -> m () notifyRoomOpend :: (MonadIO m, HasConfig a0, MonadReader a0 m) => RoomData -> m ()
notifyRoomOpend room = do notifyRoomOpend room = do
config' <- getConfig <$> ask
let ServerOptions {notifyExecutable = notifyExecutable'} = config'
let (name, user) = prettyPrintOpenedRoom room let (name, user) = prettyPrintOpenedRoom room
liftIO $ printf "Room %s opened by %s\n" name user liftIO $ printf "Room %s opened by %s\n" name user
exitCode <- liftIO $ system $ printf "/nix/store/23nz5mjk3dj7027byc6g5avx3mfwwnqm-libnotify-0.8.3/bin/notify-send 'Room %s opened by %s'" name user let command = printf "%s open '%s' '%s'" notifyExecutable' name user
when (exitCode /= ExitSuccess) $ liftIO $ printf "Failed to notify room %s opened by %s\n" name user exitCode <- liftIO $ system command
when (exitCode /= ExitSuccess) $ liftIO $ printf "Failed to notify room %s opened by %s running command %s\n" name user command
notifyRoomClosed :: (MonadIO m) => RoomData -> m () notifyRoomClosed :: (MonadIO m, HasConfig a0, MonadReader a0 m) => RoomData -> m ()
notifyRoomClosed room = do notifyRoomClosed room = do
config' <- getConfig <$> ask
let ServerOptions {notifyExecutable = notifyExecutable'} = config'
let (name, _) = prettyPrintOpenedRoom room let (name, _) = prettyPrintOpenedRoom room
liftIO $ printf "Room %s closed\n" name liftIO $ printf "Room %s closed\n" name
exitCode <- liftIO $ system $ printf "/nix/store/23nz5mjk3dj7027byc6g5avx3mfwwnqm-libnotify-0.8.3/bin/notify-send 'Room %s closed'" name exitCode <- liftIO $ system $ printf "%s closed '%s'" notifyExecutable' name
when (exitCode /= ExitSuccess) $ liftIO $ printf "Failed to notify room %s closed\n" name when (exitCode /= ExitSuccess) $ liftIO $ printf "Failed to notify room %s closed\n" name

View file

@ -1,6 +1,6 @@
{-# LANGUAGE DerivingVia #-} {-# LANGUAGE DerivingVia #-}
module Types.AppTypes (Env (..), App (..), getConnectedClientState, AppProfile (Prod, Dev)) where module Types.AppTypes (Env (..), App (..), HasConfig (getConfig), getConnectedClientState, AppProfile (Prod, Dev)) where
import ClassyPrelude import ClassyPrelude
import State.ConnectedClientsState import State.ConnectedClientsState
@ -8,13 +8,15 @@ import State.RoomsState
( HasRoomsState (getRoomsState), ( HasRoomsState (getRoomsState),
RoomsState, RoomsState,
) )
import Types.Config (ServerOptions)
data AppProfile = Prod | Dev data AppProfile = Prod | Dev
data Env = Env data Env = Env
{ connectedClientsState :: ConnectedClientsState, { connectedClientsState :: ConnectedClientsState,
roomsState :: RoomsState, roomsState :: RoomsState,
profile :: AppProfile profile :: AppProfile,
config :: ServerOptions
} }
instance HasConnectedClientState Env where instance HasConnectedClientState Env where
@ -23,6 +25,12 @@ instance HasConnectedClientState Env where
instance HasRoomsState Env where instance HasRoomsState Env where
getRoomsState = roomsState getRoomsState = roomsState
class HasConfig a where
getConfig :: a -> ServerOptions
instance HasConfig Env where
getConfig = config
newtype App env a = App {unApp :: env -> IO a} newtype App env a = App {unApp :: env -> IO a}
deriving deriving
( Functor, ( Functor,

View file

@ -0,0 +1,61 @@
module Types.Config
( ServerOptions (..),
serverOptionsParser,
)
where
import ClassyPrelude
( Applicative ((<*>)),
Int,
Semigroup ((<>)),
Show,
String,
(<$>),
)
import Options.Applicative (Parser, auto, help, long, metavar, option, short, showDefault, strOption, value)
data ServerOptions = ServerOptions
{ port :: Int, -- Main server port
websocketPort :: Int, -- WebSocket server port
listenAddress :: String, -- Address to bind the server
notifyExecutable :: String -- Path to the notify executable
}
deriving (Show)
serverOptionsParser :: Parser ServerOptions
serverOptionsParser =
ServerOptions
<$> option
auto
( long "port"
<> short 'p'
<> metavar "PORT"
<> help "Port number for the main server (default: 8081)"
<> value 8081
<> showDefault
)
<*> option
auto
( long "websocketPort"
<> short 'w'
<> metavar "WS_PORT"
<> help "Port number for the WebSocket server (default: 9160)"
<> value 9160
<> showDefault
)
<*> strOption
( long "listenAddress"
<> short 'l'
<> metavar "ADDRESS"
<> help "IP address or hostname to bind the server (default: 127.0.0.1)"
<> value "127.0.0.1"
<> showDefault
)
<*> strOption
( long "notifyExecutable"
<> short 'n'
<> metavar "EXECUTABLE"
<> help "Path to the notify executable (default: /usr/bin/notify)"
<> value "/usr/bin/notify"
<> showDefault
)

View file

@ -10,6 +10,7 @@ import State.ConnectedClientsState (HasConnectedClientState (getConnectedClientS
import State.RoomsState (HasRoomsState (getRoomsState)) import State.RoomsState (HasRoomsState (getRoomsState))
import Types.AppTypes import Types.AppTypes
( Env (..), ( Env (..),
HasConfig (getConfig),
) )
class HasWebEnv a where class HasWebEnv a where
@ -31,3 +32,6 @@ instance HasRoomsState WebEnv where
instance HasWebEnv WebEnv where instance HasWebEnv WebEnv where
getRequest = request getRequest = request
getRespond = respond getRespond = respond
instance HasConfig WebEnv where
getConfig = getConfig . appEnv

View file

@ -29,7 +29,9 @@ import State.RoomsState
( getRoomState, ( getRoomState,
updateRoomState, updateRoomState,
) )
import Types.AppTypes (Env (..)) import Text.Printf (printf)
import Types.AppTypes (Env (..), HasConfig (getConfig))
import Types.Config (ServerOptions (..))
import Types.WebEnv import Types.WebEnv
newtype ExceptTApp e a = E {unExceptTApp :: IO (Either e a)} newtype ExceptTApp e a = E {unExceptTApp :: IO (Either e a)}
@ -82,7 +84,8 @@ app ::
MonadError ResponseReceived m, MonadError ResponseReceived m,
MonadRoomDataStateModify m, MonadRoomDataStateModify m,
MonadRoomDataStateRead m, MonadRoomDataStateRead m,
MonadBroadcast m MonadBroadcast m,
HasConfig env
) => ) =>
m ResponseReceived m ResponseReceived
app = requestPathHandler app = requestPathHandler
@ -94,7 +97,8 @@ requestPathHandler ::
MonadError ResponseReceived m, MonadError ResponseReceived m,
MonadRoomDataStateModify m, MonadRoomDataStateModify m,
MonadRoomDataStateRead m, MonadRoomDataStateRead m,
MonadBroadcast m MonadBroadcast m,
HasConfig env
) => ) =>
m ResponseReceived m ResponseReceived
requestPathHandler = do requestPathHandler = do
@ -122,7 +126,9 @@ notFound = do
runWebApp :: runWebApp ::
( MonadIO m, ( MonadIO m,
MonadReader Env m MonadReader Env m,
HasConfig Env,
HasConfig WebEnv
) => ) =>
m Application m Application
runWebApp = do runWebApp = do
@ -141,10 +147,14 @@ runWebApp = do
runWebServer :: runWebServer ::
( MonadIO m, ( MonadIO m,
MonadReader Env m MonadReader Env m,
HasConfig Env,
HasConfig WebEnv
) => ) =>
m () m ()
runWebServer = do runWebServer = do
putStrLn "Webserver up and running at http://localhost:8081/" config' <- getConfig <$> ask
runWebApp >>= liftIO . (run 8081 . logStdout) let ServerOptions {port = webPort, listenAddress = address} = config'
putStrLn $ pack $ printf "Webserver up and running at http://%s:%d/" address webPort
runWebApp >>= liftIO . (run webPort . logStdout)
return () return ()

View file

@ -1,4 +1,5 @@
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MonoLocalBinds #-}
module WebSocket.Server module WebSocket.Server
( runWebSocketServer, ( runWebSocketServer,
@ -9,22 +10,27 @@ where
import ClassyPrelude import ClassyPrelude
import Data.UUID.V4 (nextRandom) import Data.UUID.V4 (nextRandom)
import Network.WebSockets qualified as WS import Network.WebSockets qualified as WS
import Types.AppTypes import Text.Printf
import Types.AppTypes (Env, HasConfig (getConfig))
import Types.Config (ServerOptions (..))
import WebSocket.WSApp (WSApp (..), WSEnv (..), wsApp) import WebSocket.WSApp (WSApp (..), WSEnv (..), wsApp)
runWebSocketServer :: runWebSocketServer ::
( MonadIO m, ( MonadIO m,
MonadReader Env m Types.AppTypes.HasConfig Types.AppTypes.Env,
MonadReader Types.AppTypes.Env m
) => ) =>
m () m ()
runWebSocketServer = do runWebSocketServer = do
putStrLn "Websocket up at 0.0.0.0:9160" config' <- getConfig <$> ask
let ServerOptions {websocketPort = wsPort, listenAddress = address} = config'
putStrLn $ pack $ printf "WebSocket server up and running at ws://%s:%d/" address wsPort
wsApp' <- runWSApp wsApp' <- runWSApp
liftIO $ WS.runServer "0.0.0.0" 9160 wsApp' liftIO $ WS.runServer address wsPort wsApp'
runWSApp :: runWSApp ::
( MonadIO m, ( MonadIO m,
MonadReader Env m MonadReader Types.AppTypes.Env m
) => ) =>
m WS.ServerApp m WS.ServerApp
runWSApp = do runWSApp = do