packages feed

notifications-tray-icon-0.1.0.0: src/DBus/Proxy.hs

{-# LANGUAGE OverloadedStrings #-}
module DBus.Proxy where

import           Control.Arrow
import           Control.Monad
import           Control.Monad.Trans.Class
import           Control.Monad.Trans.Except
import           DBus
import           DBus.Client
import           DBus.Internal.Message
import qualified DBus.Internal.Types as T
import qualified DBus.Introspection as I
import qualified DBus.TH as DBusTH
import           Data.Coerce
import           Data.Maybe
import           System.Log.Logger

data ProxyOptions = ProxyOptions Client BusName ObjectPath InterfaceName

maybeToEither :: b -> Maybe a -> Either b a
maybeToEither = flip maybe Right . Left

proxyAll :: Client -> BusName -> ObjectPath -> ObjectPath -> IO ()
proxyAll client busName pathToProxy registrationPath = either print return =<< runExceptT (do
  let introspectionCall =
        (methodCall pathToProxy
                     (interfaceName_ "org.freedesktop.DBus.Introspectable")
                     "Introspect")
                     { methodCallDestination = Just busName }
  returnValue <- ExceptT $ left methodErrorName <$> call client introspectionCall
  obj <- ExceptT $ return $ do
    xmlString <- maybeToEither errorInvalidParameters $
                 listToMaybe (methodReturnBody returnValue) >>= fromVariant
    maybeToEither errorFailed $ I.parseXML "/" xmlString
  let interfaces = I.objectInterfaces obj
  lift $ do
    logM "DBus.Proxy" DEBUG $ show obj
    when (length interfaces < 1) $
         logM "DBus.Proxy" WARNING "No interfaces found when attempting to proxy"
    mapM_ runInterface interfaces)
  where runInterface interface = do
           let proxyOptions = ProxyOptions client busName pathToProxy $
                              I.interfaceName interface
           buildAndRegisterInterface proxyOptions interface registrationPath

buildAndRegisterInterface :: ProxyOptions -> I.Interface -> ObjectPath -> IO ()
buildAndRegisterInterface
   options@(ProxyOptions client busName path interfaceName)
   introspectionInterface registrationPath = do
     let interface = buildInterface options introspectionInterface
     export client registrationPath interface
     forwardSignals options

buildInterface :: ProxyOptions -> I.Interface -> Interface
buildInterface options I.Interface
                 { I.interfaceName = name
                 , I.interfaceMethods = methods
                 , I.interfaceProperties = properties
                 } = Interface
  { interfaceName = name
  , interfaceMethods = map (buildMethod options) methods
  , interfaceProperties = map (buildProperty options) properties
  , interfaceSignals = []
  }

buildReply :: Either MethodError MethodReturn -> Reply
buildReply (Left MethodError { methodErrorName = errorName
                             , methodErrorBody = body
                             }) = ReplyError errorName body
buildReply (Right MethodReturn { methodReturnBody = body }) =
  ReplyReturn body

buildMethod :: ProxyOptions -> I.Method -> Method
buildMethod (ProxyOptions client busName path interfaceName)
            introspectionMethod = Method
  { methodName = I.methodName introspectionMethod
  , inSignature = T.Signature []            -- TODO: make signature accurate
  , outSignature = T.Signature []
  , methodHandler = lift . handler
  }
  where handler theMethodCall =
           buildReply <$> call client
                     theMethodCall { methodCallPath = path
                                , methodCallDestination = Just busName
                                }

buildProperty :: ProxyOptions -> I.Property -> Property
buildProperty (ProxyOptions client busName path interfaceName)
              introspectionProperty = Property
  { propertyName = propName
  , propertyType = T.TypeVariant -- TODO: make this accurate
  , propertyGetter = Just getter
  , propertySetter = Just $ void . setter
  }
  where propName = memberName_ $ I.propertyName introspectionProperty
        baseMethodCall = (methodCall path interfaceName propName)
                         { methodCallDestination = Just busName}
        getter = either (const $ toVariant ("" :: String)) id <$> getProperty client baseMethodCall
        setter = setProperty client baseMethodCall

forwardSignals :: ProxyOptions -> IO ()
forwardSignals (ProxyOptions client busName path interfaceName) = do
  -- TODO: Handle name owner changes?
  let forwardSignal = emit client
      updateSignalForwarding =
          void $ runExceptT $ do
            nameOwnerString <- ExceptT $ DBusTH.getNameOwner client $ coerce busName
            let matchRule = matchAny
                            { matchPath = Just path
                            , matchInterface = Just interfaceName
                            , matchSender = Just $ busName_ nameOwnerString
                            }
            lift $ addMatch client matchRule forwardSignal
  updateSignalForwarding