From 624f7ba8b2fcda6675951dd8d41dcc99017484cf Mon Sep 17 00:00:00 2001 From: LLLL Colonq Date: Thu, 7 Nov 2024 22:37:32 -0500 Subject: Rename fig-frontend to fig-web (It was the backend anyway :3) --- .gitignore | 5 +- cabal.project | 2 +- fig-frontend/fig-frontend.cabal | 69 --------- fig-frontend/main/Main.hs | 32 ---- fig-frontend/src/Fig/Frontend.hs | 181 ---------------------- fig-frontend/src/Fig/Frontend/Auth.hs | 93 ------------ fig-frontend/src/Fig/Frontend/DB.hs | 64 -------- fig-frontend/src/Fig/Frontend/State.hs | 41 ----- fig-frontend/src/Fig/Frontend/Utils.hs | 53 ------- fig-web/fig-web.cabal | 69 +++++++++ fig-web/main/Main.hs | 32 ++++ fig-web/src/Fig/Web.hs | 181 ++++++++++++++++++++++ fig-web/src/Fig/Web/Auth.hs | 93 ++++++++++++ fig-web/src/Fig/Web/DB.hs | 64 ++++++++ fig-web/src/Fig/Web/State.hs | 41 +++++ fig-web/src/Fig/Web/Utils.hs | 51 +++++++ flake.lock | 268 +-------------------------------- flake.nix | 63 ++------ 18 files changed, 550 insertions(+), 852 deletions(-) delete mode 100644 fig-frontend/fig-frontend.cabal delete mode 100644 fig-frontend/main/Main.hs delete mode 100644 fig-frontend/src/Fig/Frontend.hs delete mode 100644 fig-frontend/src/Fig/Frontend/Auth.hs delete mode 100644 fig-frontend/src/Fig/Frontend/DB.hs delete mode 100644 fig-frontend/src/Fig/Frontend/State.hs delete mode 100644 fig-frontend/src/Fig/Frontend/Utils.hs create mode 100644 fig-web/fig-web.cabal create mode 100644 fig-web/main/Main.hs create mode 100644 fig-web/src/Fig/Web.hs create mode 100644 fig-web/src/Fig/Web/Auth.hs create mode 100644 fig-web/src/Fig/Web/DB.hs create mode 100644 fig-web/src/Fig/Web/State.hs create mode 100644 fig-web/src/Fig/Web/Utils.hs diff --git a/.gitignore b/.gitignore index 91b3204..5fbd06d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,5 @@ dist-newstyle node_modules fig-monitor-*.toml fig-bridge-*.toml -fig-frontend.toml -fig-frontend-assets -fig-frontend-client/output/* \ No newline at end of file +fig-web.toml +fig-web-assets \ No newline at end of file diff --git a/cabal.project b/cabal.project index 3130a7f..6307b0d 100644 --- a/cabal.project +++ b/cabal.project @@ -6,7 +6,7 @@ packages: fig-monitor-irc/ fig-monitor-bullfrog/ fig-bridge-irc-discord/ - fig-frontend/ + fig-web/ fig-bless/ fig-emulator-gb/ optimization: 2 diff --git a/fig-frontend/fig-frontend.cabal b/fig-frontend/fig-frontend.cabal deleted file mode 100644 index 9fc8f8e..0000000 --- a/fig-frontend/fig-frontend.cabal +++ /dev/null @@ -1,69 +0,0 @@ -cabal-version: 3.4 -name: fig-frontend -version: 0.1.0.0 - -common defaults - ghc-options: -Wall - default-language: GHC2021 - default-extensions: NoImplicitPrelude PackageImports LambdaCase MultiWayIf OverloadedStrings OverloadedLists OverloadedRecordDot DuplicateRecordFields RecordWildCards NoFieldSelectors BlockArguments ViewPatterns TypeFamilies DataKinds GADTs - -common deps - build-depends: - base - , aeson - , base64 - , binary - , bytestring - , containers - , data-default-class - , directory - , errors - , filepath - , hedis - , http-types - , http-client - , http-client-tls - , jose-jwt - , lens - , lucid2 - , megaparsec - , mtl - , network - , random - , req - , safe-exceptions - , scotty - , text - , time - , tomland - , transformers - , unordered-containers - , vector - , wai - , wai-extra - , wai-middleware-static - , wai-websockets - , warp - , websockets - , wuss - , fig-utils - , fig-bus - -library - import: defaults - import: deps - hs-source-dirs: src - exposed-modules: - Fig.Frontend - Fig.Frontend.Utils - Fig.Frontend.Auth - Fig.Frontend.State - Fig.Frontend.DB - -executable fig-frontend - import: defaults - import: deps - build-depends: fig-frontend, optparse-applicative - hs-source-dirs: - main - main-is: Main.hs diff --git a/fig-frontend/main/Main.hs b/fig-frontend/main/Main.hs deleted file mode 100644 index 7db5efe..0000000 --- a/fig-frontend/main/Main.hs +++ /dev/null @@ -1,32 +0,0 @@ -{-# Language ApplicativeDo #-} - -module Main where - -import Fig.Prelude - -import Options.Applicative - -import Fig.Frontend -import Fig.Frontend.Utils - -data Opts = Opts - { busHost :: Text - , busPort :: Text - , config :: FilePath - } - -parseOpts :: Parser Opts -parseOpts = do - busHost <- strOption (long "bus-host" <> metavar "HOST" <> help "Address of message bus" <> value "localhost") - busPort <- strOption (long "bus-port" <> metavar "PORT" <> help "Message bus port" <> showDefault <> value "32050") - config <- strOption (long "config" <> metavar "PATH" <> help "Path to config file" <> showDefault <> value "fig-frontend.toml") - pure Opts{..} - -main :: IO () -main = do - opts <- execParser $ info (parseOpts <**> helper) - ( fullDesc - <> header "fig-frontend - public-facing web applications" - ) - cfg <- loadConfig opts.config - server cfg (opts.busHost, opts.busPort) diff --git a/fig-frontend/src/Fig/Frontend.hs b/fig-frontend/src/Fig/Frontend.hs deleted file mode 100644 index 4c32078..0000000 --- a/fig-frontend/src/Fig/Frontend.hs +++ /dev/null @@ -1,181 +0,0 @@ -{-# Language QuasiQuotes #-} - -module Fig.Frontend where - -import Fig.Prelude - -import System.Random (randomRIO) - -import Control.Monad (unless) -import Control.Lens (use, (^?), Ixed (..)) -import qualified Control.Concurrent.Chan as Chan -import qualified Control.Concurrent.MVar as MVar - -import Data.Maybe (mapMaybe) -import qualified Data.Text as Text -import qualified Data.Text.Lazy as Text.L -import qualified Data.ByteString.Base64 as BS.Base64 -import qualified Data.Set as Set - -import qualified Network.Wai as Wai -import qualified Network.Wai.Middleware.Static as Wai.Static -import qualified Network.Wai.Handler.Warp as Warp -import qualified Network.WebSockets as WS - -import qualified Web.Scotty as Sc - -import Fig.Utils.SExpr -import Fig.Bus.Client -import Fig.Frontend.Utils -import Fig.Frontend.Auth -import Fig.Frontend.State -import qualified Fig.Frontend.DB as DB - -data LiveEvent - = LiveEventOnline !(Set.Set Text) - | LiveEventOffline !(Set.Set Text) - deriving (Show, Eq, Ord) - -server :: Config -> (Text, Text) -> IO () -server cfg busAddr = do - log $ "Frontend server running on port " <> tshow cfg.port - liveEvents <- Chan.newChan @LiveEvent - currentlyLive <- MVar.newMVar Set.empty - busClient busAddr - (\cmds -> do - log "Connected to bus!" - cmds.subscribe [sexp|(monitor twitch stream online)|] - Warp.run cfg.port =<< app cfg cmds liveEvents currentlyLive - ) - (\_cmds d -> do - case d of - SExprList (ev:rest) - | ev == [sexp|(monitor twitch stream online)|] -> do - let live = mapMaybe (\case SExprString s -> Just s; _ -> Nothing) rest - let new = Set.fromList live - old <- MVar.swapMVar currentlyLive new - let online = Set.difference new old - let offline = Set.difference old new - unless (Set.null online) . Chan.writeChan liveEvents $ LiveEventOnline online - unless (Set.null offline) . Chan.writeChan liveEvents $ LiveEventOnline offline - _ -> log $ "Invalid event: " <> tshow d - ) - (pure ()) - -sexprStr :: Text -> SExpr -sexprStr = SExprString . BS.Base64.encodeBase64 . encodeUtf8 - -app :: Config -> Commands IO -> Chan.Chan LiveEvent -> MVar.MVar (Set.Set Text) -> IO Wai.Application -app cfg cmds liveEvents currentlyLive = do - log "Connecting to database..." - db <- DB.connect cfg - log "Connected! Server active." - st <- stateRef - Sc.scottyApp do - Sc.middleware $ Wai.Static.staticPolicy $ Wai.Static.addBase cfg.assetPath - Sc.get "/" $ Sc.redirect "/index.html" - Sc.get "/api/check" $ authed cfg \auth -> do - Sc.json @[Text] [auth.id, auth.name] - Sc.put "/api/buffer" do - buf <- withState st $ use buffer - Sc.text $ Text.L.fromStrict buf - Sc.get "/api/motd" do - DB.get db "motd" >>= \case - Nothing -> Sc.text "" - Just val -> Sc.text . Text.L.fromStrict $ decodeUtf8 val - Sc.get "/api/catchphrase" do - let catchphrases = - [ "vtuber (male)" - , "man of letters" - , "cool guy, online" - , "internet clown man" - , "professional emacs fan" - , "web freak" - , "guy who really likes programming" - , "i use nixos btw" - , "(are these funny or cringe or both?)" - , "haha yay" - , "Joel" - ] :: [Text] - i <- randomRIO (0, length catchphrases - 1) - case catchphrases ^? ix i of - Nothing -> Sc.text "man of letters" - Just val -> Sc.text $ Text.L.fromStrict val - Sc.get "/api/user/:name" do - name <- Text.toLower <$> Sc.pathParam "name" - DB.get db ("user:" <> encodeUtf8 name) >>= \case - Nothing -> do - Sc.status status404 - Sc.text "user not found" - Just val -> Sc.text . Text.L.fromStrict $ decodeUtf8 val - Sc.post "/api/redeem" do - me <- Text.toLower <$> Sc.formParam "ayem" - name <- Sc.formParam "name" - input <- Sc.formParamMaybe "input" - liftIO $ cmds.publish [sexp|(frontend redeem incoming)|] - $ mconcat - [ [ sexprStr me - , sexprStr name - ] - , maybe [] ((:[]) . sexprStr) input - ] - Sc.text "it worked" - Sc.get "/api/songs" do - DB.hvals db "songnames" >>= \case - Nothing -> do - Sc.status status404 - Sc.text "no sounds found :(" - Just songs -> Sc.text . Text.L.fromStrict . pretty . SExprList @Void $ SExprString . decodeUtf8 <$> songs - Sc.get "/api/song/:hash" do - hash <- Sc.pathParam "hash" - DB.hget db "songnotes" hash >>= \case - Nothing -> do - Sc.status status404 - Sc.text "song not found" - Just val -> Sc.text . Text.L.fromStrict $ decodeUtf8 val - Sc.get "/api/poke/:name" do - target <- encodeUtf8 . Text.toLower <$> Sc.pathParam "name" - inbox <- fromMaybe [] <$> DB.smembers db ("pokeinbox:" <> target) - Sc.text . Text.L.fromStrict . pretty . SExprList @Void $ sexprStr . decodeUtf8 <$> inbox - Sc.post "/api/poke/:name" do - me <- encodeUtf8 . Text.toLower <$> Sc.formParam "ayem" - target <- encodeUtf8 . Text.toLower <$> Sc.pathParam "name" - DB.sismember db ("pokeinbox:" <> me) target >>= \case - True -> do - log . tshow $ "handshake between " <> me <> " and " <> target <> " complete!" - DB.srem db ("pokeinbox:" <> target) [me] - DB.srem db ("pokeinbox:" <> me) [target] - Sc.text "complete" - False -> do - log . tshow $ "partial handshake from " <> me <> " to " <> target - DB.sadd db ("pokeinbox:" <> target) [me] - Sc.text "partial" - Sc.get "/api/sentiment" do - s <- DB.get db "sentiment" >>= \case - Nothing -> pure "0" - Just x -> pure x - Sc.text . Text.L.fromStrict . decodeUtf8 $ s - Sc.post "/api/sentiment/green" do - DB.incr db "sentiment" - Sc.post "/api/sentiment/red" do - DB.decr db "sentiment" - Sc.get "/api/circle" do - live <- liftIO $ MVar.readMVar currentlyLive - Sc.text . Text.L.fromStrict . pretty . SExprList @Void $ SExprString <$> Set.toList live - websocket "/api/circle/events" \conn -> do - c <- Chan.dupChan liveEvents - forever do - ev <- liftIO $ Chan.readChan c - WS.sendTextData conn $ case ev of - LiveEventOnline online -> - pretty $ SExprList @Void - [ SExprString "online" - , SExprList $ SExprString <$> Set.toList online - ] - LiveEventOffline offline -> - pretty $ SExprList @Void - [ SExprString "offline" - , SExprList $ SExprString <$> Set.toList offline - ] - Sc.notFound do - Sc.text "not found" diff --git a/fig-frontend/src/Fig/Frontend/Auth.hs b/fig-frontend/src/Fig/Frontend/Auth.hs deleted file mode 100644 index 63bf804..0000000 --- a/fig-frontend/src/Fig/Frontend/Auth.hs +++ /dev/null @@ -1,93 +0,0 @@ -module Fig.Frontend.Auth where - -import Fig.Prelude - -import qualified Network.HTTP.Req as R - -import Data.Maybe (mapMaybe) -import qualified Data.Text as Text -import qualified Data.Text.Lazy as Text.Lazy -import qualified Data.Map.Strict as Map -import qualified Data.Aeson as Aeson -import qualified Data.Aeson.Types as Aeson - -import qualified Jose.Jwk as Jwk -import qualified Jose.Jwt as Jwt - -import qualified Web.Scotty as Sc -import qualified Web.Scotty.Cookie as Sc.C - -import Fig.Frontend.Utils - -data TokenContents = TokenContents - { aud :: !Text - , exp :: !Int - , iat :: !Int - , iss :: !Text - , sub :: !Text - , azp :: !(Maybe Text) - , nonce :: !Text - , preferred_username :: !Text - } deriving (Show, Eq, Generic) -instance Aeson.FromJSON TokenContents - -fetchJwk :: MonadIO m => m (Maybe Jwk.Jwk) -fetchJwk = do - resp <- R.responseBody <$> R.runReq R.defaultHttpConfig do - R.req R.GET (R.https "id.twitch.tv" R./: "oauth2" R./: "keys") R.NoReqBody R.jsonResponse mempty - let mkeys = Aeson.parseMaybe (Aeson..: "keys") resp - let mjwk = mkeys >>= headMay - log $ tshow mjwk - pure mjwk - -validateToken :: MonadIO m => ByteString -> m (Maybe TokenContents) -validateToken encodedToken = fetchJwk >>= \case - Nothing -> pure Nothing - Just jwk -> liftIO (Jwt.decode [jwk] Nothing encodedToken) >>= \case - Left err -> do - log $ "Failed to decode token: " <> tshow err - pure Nothing - Right jwt -> do - let contents = case jwt of - Jwt.Unsecured bs -> bs - Jwt.Jws (_, bs) -> bs - Jwt.Jwe (_, bs) -> bs - log $ tshow contents - pure $ Aeson.decodeStrict contents - -data Auth = Auth { id :: !Text, name :: !Text } deriving Show -checkAuth :: Config -> Sc.ActionM (Maybe Auth) -checkAuth cfg = - Sc.header "Authorization" - >>= \case - Just authstrLazy -> do - let authstr = drop 1 $ Text.splitOn " " $ Text.Lazy.toStrict authstrLazy - let pairs = Map.fromList $ flip mapMaybe authstr \s -> - case Text.splitOn "=" s of - [k, v] -> Just (k, Text.takeWhile (/='"') $ Text.drop 1 v) - _ -> Nothing - case (Map.lookup "token" pairs, Map.lookup "nonce" pairs) of - (Just token, Just nonce) -> do - log $ tshow token - log $ tshow nonce - validateToken (encodeUtf8 token) >>= \case - Just tc - | tc.aud == cfg.clientId - , tc.nonce == nonce - -> do - log $ tshow tc - pure . Just $ Auth - { name = tc.preferred_username - , id = tc.sub - } - _else -> do - pure Nothing - _else -> pure Nothing - _else -> pure Nothing - -authed :: Config -> (Auth -> Sc.ActionM ()) -> Sc.ActionM () -authed cfg f = checkAuth cfg >>= \case - Nothing -> do - Sc.status status401 - Sc.text "unauthorized" - Just auth -> f auth diff --git a/fig-frontend/src/Fig/Frontend/DB.hs b/fig-frontend/src/Fig/Frontend/DB.hs deleted file mode 100644 index 51da59e..0000000 --- a/fig-frontend/src/Fig/Frontend/DB.hs +++ /dev/null @@ -1,64 +0,0 @@ -module Fig.Frontend.DB where - -import Control.Error.Util (hush) - -import qualified Database.Redis as Redis - -import Fig.Prelude -import Fig.Frontend.Utils - -connect :: MonadIO m => Config -> m Redis.Connection -connect cfg = liftIO $ Redis.checkedConnect Redis.defaultConnectInfo - { Redis.connectHost = unpack cfg.dbHost - } - -get :: MonadIO m => Redis.Connection -> ByteString -> m (Maybe ByteString) -get c key = liftIO $ Redis.runRedis c do - v <- Redis.get key - pure . join $ hush v - -incr :: MonadIO m => Redis.Connection -> ByteString -> m () -incr c key = liftIO $ Redis.runRedis c do - void $ Redis.incr key - -decr :: MonadIO m => Redis.Connection -> ByteString -> m () -decr c key = liftIO $ Redis.runRedis c do - void $ Redis.decr key - -hget :: MonadIO m => Redis.Connection -> ByteString -> ByteString -> m (Maybe ByteString) -hget c key hkey = liftIO $ Redis.runRedis c do - v <- Redis.hget key hkey - pure . join $ hush v - -hvals :: MonadIO m => Redis.Connection -> ByteString -> m (Maybe [ByteString]) -hvals c key = liftIO $ Redis.runRedis c do - hush <$> Redis.hvals key - -sadd :: MonadIO m => Redis.Connection -> ByteString -> [ByteString] -> m () -sadd c key skeys = liftIO $ Redis.runRedis c do - _ <- Redis.sadd key skeys - pure () - -srem :: MonadIO m => Redis.Connection -> ByteString -> [ByteString] -> m () -srem c key skeys = liftIO $ Redis.runRedis c do - _ <- Redis.srem key skeys - pure () - -smembers :: MonadIO m => Redis.Connection -> ByteString -> m (Maybe [ByteString]) -smembers c key = liftIO $ Redis.runRedis c do - hush <$> Redis.smembers key - -sismember :: MonadIO m => Redis.Connection -> ByteString -> ByteString -> m Bool -sismember c key skey = liftIO $ Redis.runRedis c do - Redis.sismember key skey >>= hush >>> \case - Just x -> pure x - Nothing -> pure False - -lpop :: MonadIO m => Redis.Connection -> ByteString -> m (Maybe ByteString) -lpop c key = liftIO $ Redis.runRedis c do - join . hush <$> Redis.lpop key - -rpush :: MonadIO m => Redis.Connection -> ByteString -> ByteString -> m () -rpush c key val = liftIO $ Redis.runRedis c do - _ <- Redis.rpush key [val] - pure () diff --git a/fig-frontend/src/Fig/Frontend/State.hs b/fig-frontend/src/Fig/Frontend/State.hs deleted file mode 100644 index 6053105..0000000 --- a/fig-frontend/src/Fig/Frontend/State.hs +++ /dev/null @@ -1,41 +0,0 @@ -{-# Language TemplateHaskell #-} - -module Fig.Frontend.State where - -import Control.Lens.TH (makeLensesFor) -import Control.Lens ((<>=)) -import Control.Monad.State (runStateT) - -import Fig.Prelude - -import qualified Data.IORef as IORef - -newtype State = State - { buffer :: Text - } -makeLensesFor [("buffer", "buffer")] ''State - -defaultState :: State -defaultState = State - { buffer = "" - } - -type StateRef = IORef.IORef State - -stateRef :: IO StateRef -stateRef = IORef.newIORef defaultState - -withState :: - MonadIO m' => - StateRef -> - (forall m. (MonadIO m, MonadState State m) => m a) -> - m' a -withState ref f = do - s <- liftIO $ IORef.readIORef ref - (res, s') <- liftIO $ runStateT f s - liftIO $ IORef.writeIORef ref s' - pure res - -sayHi :: StateRef -> IO () -sayHi ref = withState ref do - buffer <>= "hi" diff --git a/fig-frontend/src/Fig/Frontend/Utils.hs b/fig-frontend/src/Fig/Frontend/Utils.hs deleted file mode 100644 index 090c6ba..0000000 --- a/fig-frontend/src/Fig/Frontend/Utils.hs +++ /dev/null @@ -1,53 +0,0 @@ -{-# Language RecordWildCards #-} -{-# Language ApplicativeDo #-} - -module Fig.Frontend.Utils - ( FigFrontendException(..) - , loadConfig - , Config(..) - , websocket - , module Network.HTTP.Types.Status - ) where - -import Fig.Prelude - -import Network.HTTP.Types.Status -import qualified Network.Wai.Handler.WebSockets as Wai.WS -import qualified Network.WebSockets as WS - -import qualified Web.Scotty as Sc - -import qualified Toml - -newtype FigFrontendException = FigFrontendException Text - deriving (Show, Eq, Ord) -instance Exception FigFrontendException - -data Config = Config - { port :: !Int - , assetPath :: !FilePath - , clientId :: !Text - , authToken :: !Text - , dbHost :: !Text - } deriving (Show, Eq, Ord) - -configCodec :: Toml.TomlCodec Config -configCodec = do - port <- Toml.int "port" Toml..= (\a -> a.port) - assetPath <- Toml.string "asset_path" Toml..= (\a -> a.assetPath) - clientId <- Toml.text "client_id" Toml..= (\a -> a.clientId) - authToken <- Toml.text "auth_token" Toml..= (\a -> a.authToken) - dbHost <- Toml.text "db_host" Toml..= (\a -> a.dbHost) - pure $ Config{..} - -loadConfig :: FilePath -> IO Config -loadConfig path = Toml.decodeFileEither configCodec path >>= \case - Left err -> throwM . FigFrontendException $ tshow err - Right config -> pure config - -websocket :: ByteString -> (WS.Connection -> IO ()) -> Sc.ScottyM () -websocket pat h = Sc.middleware $ Wai.WS.websocketsOr WS.defaultConnectionOptions handler - where - handler pending = if WS.requestPath (WS.pendingRequest pending) == pat - then WS.acceptRequest pending >>= h - else WS.rejectRequest pending "" diff --git a/fig-web/fig-web.cabal b/fig-web/fig-web.cabal new file mode 100644 index 0000000..3cf3502 --- /dev/null +++ b/fig-web/fig-web.cabal @@ -0,0 +1,69 @@ +cabal-version: 3.4 +name: fig-web +version: 0.1.0.0 + +common defaults + ghc-options: -Wall + default-language: GHC2021 + default-extensions: NoImplicitPrelude PackageImports LambdaCase MultiWayIf OverloadedStrings OverloadedLists OverloadedRecordDot DuplicateRecordFields RecordWildCards NoFieldSelectors BlockArguments ViewPatterns TypeFamilies DataKinds GADTs + +common deps + build-depends: + base + , aeson + , base64 + , binary + , bytestring + , containers + , data-default-class + , directory + , errors + , filepath + , hedis + , http-types + , http-client + , http-client-tls + , jose-jwt + , lens + , lucid2 + , megaparsec + , mtl + , network + , random + , req + , safe-exceptions + , scotty + , text + , time + , tomland + , transformers + , unordered-containers + , vector + , wai + , wai-extra + , wai-middleware-static + , wai-websockets + , warp + , websockets + , wuss + , fig-utils + , fig-bus + +library + import: defaults + import: deps + hs-source-dirs: src + exposed-modules: + Fig.Web + Fig.Web.Utils + Fig.Web.Auth + Fig.Web.State + Fig.Web.DB + +executable fig-web + import: defaults + import: deps + build-depends: fig-web, optparse-applicative + hs-source-dirs: + main + main-is: Main.hs diff --git a/fig-web/main/Main.hs b/fig-web/main/Main.hs new file mode 100644 index 0000000..890c010 --- /dev/null +++ b/fig-web/main/Main.hs @@ -0,0 +1,32 @@ +{-# Language ApplicativeDo #-} + +module Main where + +import Fig.Prelude + +import Options.Applicative + +import Fig.Web +import Fig.Web.Utils + +data Opts = Opts + { busHost :: Text + , busPort :: Text + , config :: FilePath + } + +parseOpts :: Parser Opts +parseOpts = do + busHost <- strOption (long "bus-host" <> metavar "HOST" <> help "Address of message bus" <> value "localhost") + busPort <- strOption (long "bus-port" <> metavar "PORT" <> help "Message bus port" <> showDefault <> value "32050") + config <- strOption (long "config" <> metavar "PATH" <> help "Path to config file" <> showDefault <> value "fig-web.toml") + pure Opts{..} + +main :: IO () +main = do + opts <- execParser $ info (parseOpts <**> helper) + ( fullDesc + <> header "fig-web - public-facing web applications" + ) + cfg <- loadConfig opts.config + server cfg (opts.busHost, opts.busPort) diff --git a/fig-web/src/Fig/Web.hs b/fig-web/src/Fig/Web.hs new file mode 100644 index 0000000..60dbbe7 --- /dev/null +++ b/fig-web/src/Fig/Web.hs @@ -0,0 +1,181 @@ +{-# Language QuasiQuotes #-} + +module Fig.Web where + +import Fig.Prelude + +import System.Random (randomRIO) + +import Control.Monad (unless) +import Control.Lens (use, (^?), Ixed (..)) +import qualified Control.Concurrent.Chan as Chan +import qualified Control.Concurrent.MVar as MVar + +import Data.Maybe (mapMaybe) +import qualified Data.Text as Text +import qualified Data.Text.Lazy as Text.L +import qualified Data.ByteString.Base64 as BS.Base64 +import qualified Data.Set as Set + +import qualified Network.Wai as Wai +-- import qualified Network.Wai.Middleware.Static as Wai.Static +import qualified Network.Wai.Handler.Warp as Warp +import qualified Network.WebSockets as WS + +import qualified Web.Scotty as Sc + +import Fig.Utils.SExpr +import Fig.Bus.Client +import Fig.Web.Utils +import Fig.Web.Auth +import Fig.Web.State +import qualified Fig.Web.DB as DB + +data LiveEvent + = LiveEventOnline !(Set.Set Text) + | LiveEventOffline !(Set.Set Text) + deriving (Show, Eq, Ord) + +server :: Config -> (Text, Text) -> IO () +server cfg busAddr = do + log $ "Web server running on port " <> tshow cfg.port + liveEvents <- Chan.newChan @LiveEvent + currentlyLive <- MVar.newMVar Set.empty + busClient busAddr + (\cmds -> do + log "Connected to bus!" + cmds.subscribe [sexp|(monitor twitch stream online)|] + Warp.run cfg.port =<< app cfg cmds liveEvents currentlyLive + ) + (\_cmds d -> do + case d of + SExprList (ev:rest) + | ev == [sexp|(monitor twitch stream online)|] -> do + let live = mapMaybe (\case SExprString s -> Just s; _ -> Nothing) rest + let new = Set.fromList live + old <- MVar.swapMVar currentlyLive new + let online = Set.difference new old + let offline = Set.difference old new + unless (Set.null online) . Chan.writeChan liveEvents $ LiveEventOnline online + unless (Set.null offline) . Chan.writeChan liveEvents $ LiveEventOnline offline + _ -> log $ "Invalid event: " <> tshow d + ) + (pure ()) + +sexprStr :: Text -> SExpr +sexprStr = SExprString . BS.Base64.encodeBase64 . encodeUtf8 + +app :: Config -> Commands IO -> Chan.Chan LiveEvent -> MVar.MVar (Set.Set Text) -> IO Wai.Application +app cfg cmds liveEvents currentlyLive = do + log "Connecting to database..." + db <- DB.connect cfg + log "Connected! Server active." + st <- stateRef + Sc.scottyApp do + -- Sc.middleware $ Wai.Static.staticPolicy $ Wai.Static.addBase cfg.assetPath + Sc.get "/" $ Sc.redirect "/index.html" + Sc.get "/api/check" $ authed cfg \auth -> do + Sc.json @[Text] [auth.id, auth.name] + Sc.put "/api/buffer" do + buf <- withState st $ use buffer + Sc.text $ Text.L.fromStrict buf + Sc.get "/api/motd" do + DB.get db "motd" >>= \case + Nothing -> Sc.text "" + Just val -> Sc.text . Text.L.fromStrict $ decodeUtf8 val + Sc.get "/api/catchphrase" do + let catchphrases = + [ "vtuber (male)" + , "man of letters" + , "cool guy, online" + , "internet clown man" + , "professional emacs fan" + , "web freak" + , "guy who really likes programming" + , "i use nixos btw" + , "(are these funny or cringe or both?)" + , "haha yay" + , "Joel" + ] :: [Text] + i <- randomRIO (0, length catchphrases - 1) + case catchphrases ^? ix i of + Nothing -> Sc.text "man of letters" + Just val -> Sc.text $ Text.L.fromStrict val + Sc.get "/api/user/:name" do + name <- Text.toLower <$> Sc.pathParam "name" + DB.get db ("user:" <> encodeUtf8 name) >>= \case + Nothing -> do + Sc.status status404 + Sc.text "user not found" + Just val -> Sc.text . Text.L.fromStrict $ decodeUtf8 val + Sc.post "/api/redeem" do + me <- Text.toLower <$> Sc.formParam "ayem" + name <- Sc.formParam "name" + input <- Sc.formParamMaybe "input" + liftIO $ cmds.publish [sexp|(frontend redeem incoming)|] + $ mconcat + [ [ sexprStr me + , sexprStr name + ] + , maybe [] ((:[]) . sexprStr) input + ] + Sc.text "it worked" + Sc.get "/api/songs" do + DB.hvals db "songnames" >>= \case + Nothing -> do + Sc.status status404 + Sc.text "no sounds found :(" + Just songs -> Sc.text . Text.L.fromStrict . pretty . SExprList @Void $ SExprString . decodeUtf8 <$> songs + Sc.get "/api/song/:hash" do + hash <- Sc.pathParam "hash" + DB.hget db "songnotes" hash >>= \case + Nothing -> do + Sc.status status404 + Sc.text "song not found" + Just val -> Sc.text . Text.L.fromStrict $ decodeUtf8 val + Sc.get "/api/poke/:name" do + target <- encodeUtf8 . Text.toLower <$> Sc.pathParam "name" + inbox <- fromMaybe [] <$> DB.smembers db ("pokeinbox:" <> target) + Sc.text . Text.L.fromStrict . pretty . SExprList @Void $ sexprStr . decodeUtf8 <$> inbox + Sc.post "/api/poke/:name" do + me <- encodeUtf8 . Text.toLower <$> Sc.formParam "ayem" + target <- encodeUtf8 . Text.toLower <$> Sc.pathParam "name" + DB.sismember db ("pokeinbox:" <> me) target >>= \case + True -> do + log . tshow $ "handshake between " <> me <> " and " <> target <> " complete!" + DB.srem db ("pokeinbox:" <> target) [me] + DB.srem db ("pokeinbox:" <> me) [target] + Sc.text "complete" + False -> do + log . tshow $ "partial handshake from " <> me <> " to " <> target + DB.sadd db ("pokeinbox:" <> target) [me] + Sc.text "partial" + Sc.get "/api/sentiment" do + s <- DB.get db "sentiment" >>= \case + Nothing -> pure "0" + Just x -> pure x + Sc.text . Text.L.fromStrict . decodeUtf8 $ s + Sc.post "/api/sentiment/green" do + DB.incr db "sentiment" + Sc.post "/api/sentiment/red" do + DB.decr db "sentiment" + Sc.get "/api/circle" do + live <- liftIO $ MVar.readMVar currentlyLive + Sc.text . Text.L.fromStrict . pretty . SExprList @Void $ SExprString <$> Set.toList live + websocket "/api/circle/events" \conn -> do + c <- Chan.dupChan liveEvents + forever do + ev <- liftIO $ Chan.readChan c + WS.sendTextData conn $ case ev of + LiveEventOnline online -> + pretty $ SExprList @Void + [ SExprString "online" + , SExprList $ SExprString <$> Set.toList online + ] + LiveEventOffline offline -> + pretty $ SExprList @Void + [ SExprString "offline" + , SExprList $ SExprString <$> Set.toList offline + ] + Sc.notFound do + Sc.text "not found" diff --git a/fig-web/src/Fig/Web/Auth.hs b/fig-web/src/Fig/Web/Auth.hs new file mode 100644 index 0000000..3076d1f --- /dev/null +++ b/fig-web/src/Fig/Web/Auth.hs @@ -0,0 +1,93 @@ +module Fig.Web.Auth where + +import Fig.Prelude + +import qualified Network.HTTP.Req as R + +import Data.Maybe (mapMaybe) +import qualified Data.Text as Text +import qualified Data.Text.Lazy as Text.Lazy +import qualified Data.Map.Strict as Map +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Types as Aeson + +import qualified Jose.Jwk as Jwk +import qualified Jose.Jwt as Jwt + +import qualified Web.Scotty as Sc +import qualified Web.Scotty.Cookie as Sc.C + +import Fig.Web.Utils + +data TokenContents = TokenContents + { aud :: !Text + , exp :: !Int + , iat :: !Int + , iss :: !Text + , sub :: !Text + , azp :: !(Maybe Text) + , nonce :: !Text + , preferred_username :: !Text + } deriving (Show, Eq, Generic) +instance Aeson.FromJSON TokenContents + +fetchJwk :: MonadIO m => m (Maybe Jwk.Jwk) +fetchJwk = do + resp <- R.responseBody <$> R.runReq R.defaultHttpConfig do + R.req R.GET (R.https "id.twitch.tv" R./: "oauth2" R./: "keys") R.NoReqBody R.jsonResponse mempty + let mkeys = Aeson.parseMaybe (Aeson..: "keys") resp + let mjwk = mkeys >>= headMay + log $ tshow mjwk + pure mjwk + +validateToken :: MonadIO m => ByteString -> m (Maybe TokenContents) +validateToken encodedToken = fetchJwk >>= \case + Nothing -> pure Nothing + Just jwk -> liftIO (Jwt.decode [jwk] Nothing encodedToken) >>= \case + Left err -> do + log $ "Failed to decode token: " <> tshow err + pure Nothing + Right jwt -> do + let contents = case jwt of + Jwt.Unsecured bs -> bs + Jwt.Jws (_, bs) -> bs + Jwt.Jwe (_, bs) -> bs + log $ tshow contents + pure $ Aeson.decodeStrict contents + +data Auth = Auth { id :: !Text, name :: !Text } deriving Show +checkAuth :: Config -> Sc.ActionM (Maybe Auth) +checkAuth cfg = + Sc.header "Authorization" + >>= \case + Just authstrLazy -> do + let authstr = drop 1 $ Text.splitOn " " $ Text.Lazy.toStrict authstrLazy + let pairs = Map.fromList $ flip mapMaybe authstr \s -> + case Text.splitOn "=" s of + [k, v] -> Just (k, Text.takeWhile (/='"') $ Text.drop 1 v) + _ -> Nothing + case (Map.lookup "token" pairs, Map.lookup "nonce" pairs) of + (Just token, Just nonce) -> do + log $ tshow token + log $ tshow nonce + validateToken (encodeUtf8 token) >>= \case + Just tc + | tc.aud == cfg.clientId + , tc.nonce == nonce + -> do + log $ tshow tc + pure . Just $ Auth + { name = tc.preferred_username + , id = tc.sub + } + _else -> do + pure Nothing + _else -> pure Nothing + _else -> pure Nothing + +authed :: Config -> (Auth -> Sc.ActionM ()) -> Sc.ActionM () +authed cfg f = checkAuth cfg >>= \case + Nothing -> do + Sc.status status401 + Sc.text "unauthorized" + Just auth -> f auth diff --git a/fig-web/src/Fig/Web/DB.hs b/fig-web/src/Fig/Web/DB.hs new file mode 100644 index 0000000..f166bdf --- /dev/null +++ b/fig-web/src/Fig/Web/DB.hs @@ -0,0 +1,64 @@ +module Fig.Web.DB where + +import Control.Error.Util (hush) + +import qualified Database.Redis as Redis + +import Fig.Prelude +import Fig.Web.Utils + +connect :: MonadIO m => Config -> m Redis.Connection +connect cfg = liftIO $ Redis.checkedConnect Redis.defaultConnectInfo + { Redis.connectHost = unpack cfg.dbHost + } + +get :: MonadIO m => Redis.Connection -> ByteString -> m (Maybe ByteString) +get c key = liftIO $ Redis.runRedis c do + v <- Redis.get key + pure . join $ hush v + +incr :: MonadIO m => Redis.Connection -> ByteString -> m () +incr c key = liftIO $ Redis.runRedis c do + void $ Redis.incr key + +decr :: MonadIO m => Redis.Connection -> ByteString -> m () +decr c key = liftIO $ Redis.runRedis c do + void $ Redis.decr key + +hget :: MonadIO m => Redis.Connection -> ByteString -> ByteString -> m (Maybe ByteString) +hget c key hkey = liftIO $ Redis.runRedis c do + v <- Redis.hget key hkey + pure . join $ hush v + +hvals :: MonadIO m => Redis.Connection -> ByteString -> m (Maybe [ByteString]) +hvals c key = liftIO $ Redis.runRedis c do + hush <$> Redis.hvals key + +sadd :: MonadIO m => Redis.Connection -> ByteString -> [ByteString] -> m () +sadd c key skeys = liftIO $ Redis.runRedis c do + _ <- Redis.sadd key skeys + pure () + +srem :: MonadIO m => Redis.Connection -> ByteString -> [ByteString] -> m () +srem c key skeys = liftIO $ Redis.runRedis c do + _ <- Redis.srem key skeys + pure () + +smembers :: MonadIO m => Redis.Connection -> ByteString -> m (Maybe [ByteString]) +smembers c key = liftIO $ Redis.runRedis c do + hush <$> Redis.smembers key + +sismember :: MonadIO m => Redis.Connection -> ByteString -> ByteString -> m Bool +sismember c key skey = liftIO $ Redis.runRedis c do + Redis.sismember key skey >>= hush >>> \case + Just x -> pure x + Nothing -> pure False + +lpop :: MonadIO m => Redis.Connection -> ByteString -> m (Maybe ByteString) +lpop c key = liftIO $ Redis.runRedis c do + join . hush <$> Redis.lpop key + +rpush :: MonadIO m => Redis.Connection -> ByteString -> ByteString -> m () +rpush c key val = liftIO $ Redis.runRedis c do + _ <- Redis.rpush key [val] + pure () diff --git a/fig-web/src/Fig/Web/State.hs b/fig-web/src/Fig/Web/State.hs new file mode 100644 index 0000000..11e0ece --- /dev/null +++ b/fig-web/src/Fig/Web/State.hs @@ -0,0 +1,41 @@ +{-# Language TemplateHaskell #-} + +module Fig.Web.State where + +import Control.Lens.TH (makeLensesFor) +import Control.Lens ((<>=)) +import Control.Monad.State (runStateT) + +import Fig.Prelude + +import qualified Data.IORef as IORef + +newtype State = State + { buffer :: Text + } +makeLensesFor [("buffer", "buffer")] ''State + +defaultState :: State +defaultState = State + { buffer = "" + } + +type StateRef = IORef.IORef State + +stateRef :: IO StateRef +stateRef = IORef.newIORef defaultState + +withState :: + MonadIO m' => + StateRef -> + (forall m. (MonadIO m, MonadState State m) => m a) -> + m' a +withState ref f = do + s <- liftIO $ IORef.readIORef ref + (res, s') <- liftIO $ runStateT f s + liftIO $ IORef.writeIORef ref s' + pure res + +sayHi :: StateRef -> IO () +sayHi ref = withState ref do + buffer <>= "hi" diff --git a/fig-web/src/Fig/Web/Utils.hs b/fig-web/src/Fig/Web/Utils.hs new file mode 100644 index 0000000..b6c385a --- /dev/null +++ b/fig-web/src/Fig/Web/Utils.hs @@ -0,0 +1,51 @@ +{-# Language RecordWildCards #-} +{-# Language ApplicativeDo #-} + +module Fig.Web.Utils + ( FigWebException(..) + , loadConfig + , Config(..) + , websocket + , module Network.HTTP.Types.Status + ) where + +import Fig.Prelude + +import Network.HTTP.Types.Status +import qualified Network.Wai.Handler.WebSockets as Wai.WS +import qualified Network.WebSockets as WS + +import qualified Web.Scotty as Sc + +import qualified Toml + +newtype FigWebException = FigWebException Text + deriving (Show, Eq, Ord) +instance Exception FigWebException + +data Config = Config + { port :: !Int + , clientId :: !Text + , authToken :: !Text + , dbHost :: !Text + } deriving (Show, Eq, Ord) + +configCodec :: Toml.TomlCodec Config +configCodec = do + port <- Toml.int "port" Toml..= (\a -> a.port) + clientId <- Toml.text "client_id" Toml..= (\a -> a.clientId) + authToken <- Toml.text "auth_token" Toml..= (\a -> a.authToken) + dbHost <- Toml.text "db_host" Toml..= (\a -> a.dbHost) + pure $ Config{..} + +loadConfig :: FilePath -> IO Config +loadConfig path = Toml.decodeFileEither configCodec path >>= \case + Left err -> throwM . FigWebException $ tshow err + Right config -> pure config + +websocket :: ByteString -> (WS.Connection -> IO ()) -> Sc.ScottyM () +websocket pat h = Sc.middleware $ Wai.WS.websocketsOr WS.defaultConnectionOptions handler + where + handler pending = if WS.requestPath (WS.pendingRequest pending) == pat + then WS.acceptRequest pending >>= h + else WS.rejectRequest pending "" diff --git a/flake.lock b/flake.lock index 66db5b4..2e7f74f 100644 --- a/flake.lock +++ b/flake.lock @@ -1,135 +1,5 @@ { "nodes": { - "docs-search": { - "flake": false, - "locked": { - "lastModified": 1675992564, - "narHash": "sha256-Tk9VSogFHXtXe9O9vuCEfM/PV/S7plMIO0I++fCZn7U=", - "owner": "purs-nix", - "repo": "purescript-docs-search", - "rev": "35822b1d6ce65b1a07f80dd9e2caf15c3ee83e2c", - "type": "github" - }, - "original": { - "owner": "purs-nix", - "repo": "purescript-docs-search", - "type": "github" - } - }, - "flake-utils": { - "locked": { - "lastModified": 1644229661, - "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_2": { - "locked": { - "lastModified": 1618217525, - "narHash": "sha256-WGrhVczjXTiswQaoxQ+0PTfbLNeOQM6M36zvLn78AYg=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "c6169a2772643c4a93a0b5ac1c61e296cba68544", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_3": { - "locked": { - "lastModified": 1618217525, - "narHash": "sha256-WGrhVczjXTiswQaoxQ+0PTfbLNeOQM6M36zvLn78AYg=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "c6169a2772643c4a93a0b5ac1c61e296cba68544", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "get-flake": { - "locked": { - "lastModified": 1644686428, - "narHash": "sha256-zkhYsURWFrvEZLkIoBeqFBzSu+cA2u5mo6M8vq9LN7M=", - "owner": "ursi", - "repo": "get-flake", - "rev": "703f15558daa56dfae19d1858bb3046afe68831a", - "type": "github" - }, - "original": { - "owner": "ursi", - "repo": "get-flake", - "type": "github" - } - }, - "lint-utils": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": [ - "purs-nix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1707777931, - "narHash": "sha256-PsPb5xMBZ9dPDP04o9vqKEUIEG80Z84/74fPuOMs0ZI=", - "owner": "homotopic", - "repo": "lint-utils", - "rev": "5f11e3e51d8f1aa4ed62a89e90f05953931e105a", - "type": "github" - }, - "original": { - "owner": "homotopic", - "repo": "lint-utils", - "type": "github" - } - }, - "make-shell": { - "locked": { - "lastModified": 1634940815, - "narHash": "sha256-P69OmveboXzS+es1vQGS4bt+ckwbeIExqxfGLjGuJqA=", - "owner": "ursi", - "repo": "nix-make-shell", - "rev": "8add91681170924e4d0591b22f294aee3f5516f9", - "type": "github" - }, - "original": { - "owner": "ursi", - "ref": "1", - "repo": "nix-make-shell", - "type": "github" - } - }, - "make-shell_2": { - "locked": { - "lastModified": 1634940815, - "narHash": "sha256-P69OmveboXzS+es1vQGS4bt+ckwbeIExqxfGLjGuJqA=", - "owner": "ursi", - "repo": "nix-make-shell", - "rev": "8add91681170924e4d0591b22f294aee3f5516f9", - "type": "github" - }, - "original": { - "owner": "ursi", - "ref": "1", - "repo": "nix-make-shell", - "type": "github" - } - }, "nixpkgs": { "locked": { "lastModified": 1708815994, @@ -146,145 +16,9 @@ "type": "github" } }, - "nixpkgs_2": { - "locked": { - "lastModified": 1704161960, - "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "63143ac2c9186be6d9da6035fa22620018c85932", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_3": { - "locked": { - "lastModified": 1656549732, - "narHash": "sha256-eILutFZGjfk2bEzfim8S/qyYc//0S1KsCeO+OWbtoR0=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "d3248619647234b5dc74a6921bcdf6dd8323eb22", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "parsec": { - "locked": { - "lastModified": 1635533376, - "narHash": "sha256-/HrG0UPGnI5VdkhrNrpDiM2+nhdL6lD/bqyGtYv0QDE=", - "owner": "nprindle", - "repo": "nix-parsec", - "rev": "1bf25dd9c5de1257a1c67de3c81c96d05e8beb5e", - "type": "github" - }, - "original": { - "owner": "nprindle", - "repo": "nix-parsec", - "type": "github" - } - }, - "ps-tools": { - "inputs": { - "make-shell": "make-shell_2", - "nixpkgs": "nixpkgs_3", - "utils": "utils" - }, - "locked": { - "lastModified": 1704567308, - "narHash": "sha256-WbFPIkKLtyQOPBUjintckKIYnfs7MvIbmfVsLRSAPlc=", - "owner": "purs-nix", - "repo": "purescript-tools", - "rev": "ac626313141cbee78f06eb3c5e90359f695aef9b", - "type": "github" - }, - "original": { - "owner": "purs-nix", - "repo": "purescript-tools", - "type": "github" - } - }, - "purs-nix": { - "inputs": { - "docs-search": "docs-search", - "get-flake": "get-flake", - "lint-utils": "lint-utils", - "make-shell": "make-shell", - "nixpkgs": "nixpkgs_2", - "parsec": "parsec", - "ps-tools": "ps-tools", - "utils": "utils_2" - }, - "locked": { - "lastModified": 1707933489, - "narHash": "sha256-LP05KSBQ02mgBDiVdW53h9ViFBtFQIo4dT3FCebucI0=", - "owner": "purs-nix", - "repo": "purs-nix", - "rev": "72c9a8b7df0e53ff8b24fef00d9ea74d3a5b6522", - "type": "github" - }, - "original": { - "owner": "purs-nix", - "ref": "ps-0.15", - "repo": "purs-nix", - "type": "github" - } - }, "root": { "inputs": { - "nixpkgs": "nixpkgs", - "ps-tools": [ - "purs-nix", - "ps-tools" - ], - "purs-nix": "purs-nix" - } - }, - "utils": { - "inputs": { - "flake-utils": "flake-utils_2" - }, - "locked": { - "lastModified": 1656044990, - "narHash": "sha256-f01BB7CaOyntOab9XnpH9HD63rGcnu2iyL4M2ubs5F8=", - "owner": "ursi", - "repo": "flake-utils", - "rev": "f53b674a2c90f6202a2f4cd491aba121775490b5", - "type": "github" - }, - "original": { - "owner": "ursi", - "ref": "8", - "repo": "flake-utils", - "type": "github" - } - }, - "utils_2": { - "inputs": { - "flake-utils": "flake-utils_3" - }, - "locked": { - "lastModified": 1656044990, - "narHash": "sha256-f01BB7CaOyntOab9XnpH9HD63rGcnu2iyL4M2ubs5F8=", - "owner": "ursi", - "repo": "flake-utils", - "rev": "f53b674a2c90f6202a2f4cd491aba121775490b5", - "type": "github" - }, - "original": { - "owner": "ursi", - "ref": "8", - "repo": "flake-utils", - "type": "github" + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index 4e31cff..0810720 100644 --- a/flake.nix +++ b/flake.nix @@ -3,16 +3,12 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - ps-tools.follows = "purs-nix/ps-tools"; - purs-nix.url = "github:purs-nix/purs-nix/ps-0.15"; }; outputs = { self, nixpkgs, ... }@inputs: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; - ps-tools = inputs.ps-tools.legacyPackages.${system}; - purs-nix = inputs.purs-nix { inherit system; }; haskellOverrides = self: super: { scotty = self.callHackageDirect { @@ -32,32 +28,12 @@ fig-bridge-irc-discord = self.callCabal2nix "fig-bridge-irc-discord" ./fig-bridge-irc-discord {}; fig-bless = self.callCabal2nix "fig-bless" ./fig-bless {}; fig-emulator-gb = self.callCabal2nix "fig-emulator-gb" ./fig-emulator-gb {}; - fig-frontend = self.callCabal2nix "fig-frontend" ./fig-frontend {}; + fig-web = self.callCabal2nix "fig-web" ./fig-web {}; }; haskellPackages = pkgs.haskell.packages.ghc94.override { overrides = haskellOverrides; }; - purescript = purs-nix.purs { - dependencies = [ - "console" - "effect" - "prelude" - "random" - "refs" - "web-html" - "web-dom" - "web-uievents" - "canvas" - "argonaut" - "fetch" - "fetch-argonaut" - ]; - dir = ./fig-frontend-client; - srcs = [ "src" ]; - }; - fig-frontend-client = purescript.bundle {}; - figBusModule = { config, lib, ... }: let cfg = config.colonq.services.fig-bus; @@ -77,7 +53,6 @@ }; config = lib.mkIf cfg.enable { systemd.services."colonq.fig-bus" = { - after = ["network-online.target"]; wantedBy = ["network-online.target"]; serviceConfig = { Restart = "on-failure"; @@ -277,12 +252,12 @@ }; }; }; - figFrontendModule = { config, lib, ... }: + figWebModule = { config, lib, ... }: let - cfg = config.colonq.services.fig-frontend; + cfg = config.colonq.services.fig-web; in { - options.colonq.services.fig-frontend = { - enable = lib.mkEnableOption "Enable the fig web frontend"; + options.colonq.services.fig-web = { + enable = lib.mkEnableOption "Enable the fig web server"; busHost = lib.mkOption { type = lib.types.str; default = "127.0.0.1"; @@ -296,9 +271,9 @@ configFile = lib.mkOption { type = lib.types.path; description = "Path to config file"; - default = pkgs.writeText "fig-frontend.toml" '' + default = pkgs.writeText "fig-web.toml" '' port = 8000 - asset_path = "./fig-frontend-assets" + asset_path = "./fig-web-assets" client_id = "" auth_token = "" db_host = "" @@ -306,17 +281,17 @@ }; }; config = lib.mkIf cfg.enable { - systemd.services."colonq.fig-frontend" = { + systemd.services."colonq.fig-web" = { wantedBy = ["multi-user.target"]; serviceConfig = { Restart = "on-failure"; - ExecStart = "${haskellPackages.fig-frontend}/bin/fig-frontend --bus-host ${cfg.busHost} --bus-port ${toString cfg.busPort} --config ${cfg.configFile}"; + ExecStart = "${haskellPackages.fig-web}/bin/fig-web --bus-host ${cfg.busHost} --bus-port ${toString cfg.busPort} --config ${cfg.configFile}"; DynamicUser = "yes"; - RuntimeDirectory = "colonq.fig-frontend"; + RuntimeDirectory = "colonq.fig-web"; RuntimeDirectoryMode = "0755"; - StateDirectory = "colonq.fig-frontend"; + StateDirectory = "colonq.fig-web"; StateDirectoryMode = "0700"; - CacheDirectory = "colonq.fig-frontend"; + CacheDirectory = "colonq.fig-web"; CacheDirectoryMode = "0750"; }; }; @@ -333,7 +308,7 @@ fig-monitor-bullfrog fig-bridge-irc-discord fig-bless - fig-frontend + fig-web fig-emulator-gb ]; withHoogle = true; @@ -341,13 +316,6 @@ haskellPackages.cabal-install haskellPackages.haskell-language-server pkgs.nodejs - (purescript.command {}) - ps-tools.for-0_15.purescript-language-server - purs-nix.esbuild - purs-nix.purescript - pkgs.m4 - pkgs.dhall - pkgs.dhall-json ]; }; packages.x86_64-linux = { @@ -360,9 +328,8 @@ figMonitorBullfrog = haskellPackages.fig-monitor-bullfrog; figBridgeIRCDiscord = haskellPackages.fig-bridge-irc-discord; figBless = haskellPackages.fig-bless; - # figBlessStatic = haskellPackagesStatic.fig-bless; figEmulatorGB = haskellPackages.fig-emulator-gb; - figFrontend = haskellPackages.fig-frontend; + figWeb = haskellPackages.fig-web; }; apps.x86_64-linux.default = { type = "app"; @@ -374,7 +341,7 @@ figMonitorDiscord = figMonitorDiscordModule; figMonitorIRC = figMonitorIRCModule; figBridgeIRCDiscord = figBridgeIRCDiscordModule; - figFrontend = figFrontendModule; + figWeb = figWebModule; }; }; } -- cgit v1.2.3