packages feed

clplug-0.4.0.0: src/Control/Plugin.hs

{-# LANGUAGE 
      LambdaCase
    , OverloadedStrings 
    , BlockArguments
    , RecordWildCards
    , DuplicateRecordFields
    , DeriveAnyClass
    , FlexibleContexts
    #-}

module Control.Plugin (
    plugin, 
    release, 
    reject,
    respond, 
    PluginApp, 
    PluginMonad,
    InitMonad,
    PluginReq, 
    Plug(..)
    ) where 

import Data.Lightning
import Control.Internal.Conduit
import Control.Exception
import Data.Conduit
import Data.Conduit.Combinators (sourceHandle, sinkHandle) 
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
import Data.Aeson 
import Data.Text (Text, unpack)  
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.State 
import Control.Monad.Reader
import Control.Concurrent hiding (yield) 
import Network.Socket as N
import System.IO

-- | Function called on every event subscribed to in the manifest.
type PluginApp a = PluginReq -> PluginMonad a ()
type PluginReq = (Maybe Id, Method, Params)

-- | Function called on initialization, returned value is the initial state.
type InitMonad a = ReaderT Plug IO a

-- | Plugin stack contains ReaderT (ask - rpc handle & config), stateT (get/put - polymorphic state) 
type PluginMonad a = ReaderT Plug (StateT a IO)

-- | Handles to lightning-rpc file and stdout plugin & configuration object.  
data Plug = Plug {
      rpc :: Handle
    , out :: Handle
    , conf :: Init
    }

data StartErr = ExpectManifest | ExpectInit deriving (Show, Exception) 

-- | Create main executable that can be installed as core lightning plugin. 
-- 1st arg is the manifest that configures the interface, 2nd arg is a function
-- with Plug reader that returns initial state, and 3rd arg is a function
-- that is called each time data is received. 
plugin :: Value -> InitMonad s -> PluginApp s -> IO ()
plugin manifest start app = do 
    liftIO $ mapM_ (`hSetBuffering` LineBuffering) [stdin,stdout] 
    runOnce $ await >>= \case 
        (Just (Right (Just i, "getmanifest", _))) -> yield $ Res manifest i 
        _ -> throw ExpectManifest
    runOnce $ await >>= \case      
        (Just (Right (Just i, "init", v))) -> case fromJSON v of 
            Success xi@(Init{..}) -> do 
                h  <- liftIO $ getrpc $ getRpcPath configuration
                let plug = (Plug h stdout xi) 
                s' <- liftIO $ runStartup plug start
                _ <- liftIO.forkIO $ runPlugin plug s' app 
                yield . Res (object ["result" .= ("continue" :: Text)]) $ i 
                where
            _ -> throw ExpectInit 
        _ -> throw ExpectInit 
    threadDelay maxBound

runStartup :: Plug -> InitMonad a -> IO a 
runStartup re = (`runReaderT` re)  

runPlugin :: Plug -> s -> PluginApp s -> IO () 
runPlugin re st = (`evalStateT` st) . (`runReaderT` re) . forever . runConduit . runner
    where
    runner app = sourceHandle stdin .| inConduit .| entry .| appInsert app

runOnce :: ConduitT (Either (Res Value) PluginReq) (Res Value) IO () -> IO () 
runOnce = runConduit.runner
    where 
    runner d = sourceHandle stdin .| inConduit .| entry .| d .| exit .| sinkHandle stdout

entry :: (Monad n) => ConduitT (ParseResult (Req Value)) (Either (Res Value) PluginReq) n () 
entry = await >>= maybe mempty (\case  
    Correct v -> yield $ Right (getReqId v, getMethod v, getParams v) 
    InvalidReq -> yield $ Left $ ErrRes ("Request Error"::Text) Nothing  
    ParseErr -> yield $ Left $ ErrRes ("Parser Err"::Text) Nothing )

appInsert :: PluginApp a -> ConduitT (Either (Res Value) PluginReq) Void (PluginMonad a) () 
appInsert app = await >>= maybe mempty \case  
    Left failed -> do
        Plug _ out _ <- ask
        liftIO $ runRes out failed 
    Right req -> lift (app req) >> pure () 

runRes :: Handle -> Res Value -> IO ()
runRes o r = runConduit $ (yield r) .| exit .| sinkHandle o

exit :: (Monad n) => ConduitT (Res Value) S.ByteString n () 
exit = await >>= maybe mempty (yield. L.toStrict . encode) 

getrpc :: Text -> IO Handle
getrpc d = do 
    soc <- socket AF_UNIX Stream 0
    N.connect soc $ SockAddrUnix $ unpack d
    socketToHandle soc ReadWriteMode

-- | Helper function to allow node to continue default behaviour. 
release :: Id -> PluginMonad a ()
release i = do 
    Plug _ out _ <- ask
    liftIO $ runRes out $ Res (object ["result" .= ("continue" :: Text)]) i

-- | Helper function to prevent node default behaviour. 
reject :: Id -> PluginMonad a ()
reject i = do 
    Plug _ out _ <- ask
    liftIO $ runRes out $ Res (object ["result" .= ("reject" :: Text)]) i

-- | Respond with arbitrary Value, custom rpc hooks will pass back through to terminal.
respond :: Value -> Id -> PluginMonad a ()
respond v i = do 
    Plug _ out _ <- ask
    liftIO $ runRes out $ Res v i 

getRpcPath :: InitConfig -> Text
getRpcPath conf = lightning5dir conf <> "/" <> rpc5file conf