packages feed

skype4hs-0.0.0.0: src/Network/Skype/API/X11.hsc

module Network.Skype.API.X11 (
  Connection,
  DisplayAddress,
  connect,
  connectTo
) where

#include <X11/Xlib.h>

import Control.Concurrent (ThreadId, forkIO)
import Control.Concurrent.STM.TChan (TChan, newBroadcastTChanIO, writeTChan)
import Control.Exception (IOException)
import Control.Exception.Lifted (catch)
import Control.Monad (mplus)
import Control.Monad.Error (Error, strMsg)
import Control.Monad.Error.Class (MonadError, throwError)
import Control.Monad.Reader (ReaderT, asks)
import Control.Monad.STM (atomically)
import Control.Monad.Trans (MonadIO, liftIO)
import Control.Monad.Trans.Control (MonadBaseControl, control)
import Data.Maybe (listToMaybe)
import Data.Monoid (mappend, mempty)
import Foreign hiding (addForeignPtrFinalizer, newForeignPtr)
import Foreign.C.Types
import Foreign.Concurrent
import System.Environment (getEnv)
import Network.Skype.Command.Misc (authenticate)
import Network.Skype.Core

import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Builder as BS
import qualified Data.ByteString.Unsafe as BS
import qualified Graphics.X11.Xlib as X
import qualified Graphics.X11.Xlib.Extras as X

data Connection = Connection
  { skypeAPI :: SkypeAPI
  , skypeNotificatonChan :: TChan Notification
  , skypeThread :: ThreadId
  }

data SkypeAPI = SkypeAPI
  { skypeDisplay :: ForeignPtr X.Display
  , skypeWindow :: X.Window
  , skypeInstanceWindow :: X.Window
  , skypeMessageBeginAtom :: X.Atom
  , skypeMessageContinueAtom :: X.Atom
  }
  deriving (Show, Eq)

type DisplayAddress = String

instance MonadIO m => MonadSkype (ReaderT Connection m) where
  sendCommand command = asks skypeAPI >>= liftIO . flip sendTo command

  getNotificationChan = asks skypeNotificatonChan

openDisplay :: (MonadBaseControl IO m, MonadIO m, MonadError IOException m)
            => DisplayAddress
            -> m (ForeignPtr X.Display)
openDisplay address = do
  display'@(X.Display display) <- liftIO (X.openDisplay address) `catch` throwError
  liftIO $ newForeignPtr display $ X.closeDisplay display'

getDisplayAddress :: (MonadBaseControl IO m, MonadIO m, MonadError IOException m)
                  => m DisplayAddress
getDisplayAddress = liftIO (getEnv "SKYPEDISPLAY" `mplus` getEnv "DISPLAY")
                    `catch` throwError

createWindow :: (Error e, MonadIO m, MonadError e m)
             => X.Display -> X.Window -> m X.Window
createWindow display root = do
  let screen = X.defaultScreen display
      pixel  = X.blackPixel display screen

  window <- liftIO $ X.createSimpleWindow display root 0 0 1 1 0 pixel pixel

  if window == X.none
     then throwError $ strMsg "Can't create the window"
     else return window

createAtom :: (Error e, MonadIO m, MonadError e m)
           => X.Display -> String -> m X.Atom
createAtom display name = do
  atom <- liftIO $ X.internAtom display name True

  if atom == X.none
    then throwError $ strMsg $ "Can't create the atom: " ++ name
    else return atom

getSkypeInstanceWindow :: (Error e, MonadIO m, MonadError e m)
                       => X.Display -> X.Window -> m X.Window
getSkypeInstanceWindow display root = do
  instanceAtom <- createAtom display "_SKYPE_INSTANCE"

  status <- liftIO $ X.getWindowProperty32 display instanceAtom root

  case status >>= listToMaybe of
    Nothing       -> throwError $ strMsg "Skype instance window is not found"
    Just property -> return $ fromIntegral property .&. 0xffffffff

connect :: (MonadBaseControl IO m, MonadIO m, MonadError IOException m)
        => ApplicationName
        -> m Connection
connect appName = flip connectTo appName =<< getDisplayAddress

connectTo :: (MonadBaseControl IO m, MonadIO m, MonadError IOException m)
          => DisplayAddress
          -> ApplicationName
          -> m Connection
connectTo address appName = do
  api <- createAPI address
  notificationChan <- liftIO newBroadcastTChanIO
  thread <- liftIO $ forkIO $
            runEventLoop api $ atomically . writeTChan notificationChan

  let connection = Connection
                   { skypeAPI = api
                   , skypeNotificatonChan = notificationChan
                   , skypeThread = thread
                   }

  result <- runSkype connection $ authenticate appName

  either (throwError . strMsg . show) (const $ return connection) result

createAPI :: (MonadBaseControl IO m, MonadIO m, MonadError IOException m)
          => String
          -> m SkypeAPI
createAPI address = do
  display <- openDisplay address

  withForeignPtr' display $ \display_ptr -> do
    let display' = X.Display display_ptr
    let root = X.defaultRootWindow display'

    window <- createWindow display' root

    liftIO $ addForeignPtrFinalizer display $
             X.destroyWindow display' window

    instanceWindow <- getSkypeInstanceWindow display' root

    messageBeginAtom <- createAtom display' "SKYPECONTROLAPI_MESSAGE_BEGIN"
    messageContinueAtom <- createAtom display' "SKYPECONTROLAPI_MESSAGE"

    return $ SkypeAPI
      { skypeDisplay             = display
      , skypeWindow              = window
      , skypeInstanceWindow      = instanceWindow
      , skypeMessageBeginAtom    = messageBeginAtom
      , skypeMessageContinueAtom = messageContinueAtom
      }

sendTo :: SkypeAPI -> BS.ByteString -> IO ()
sendTo api message =
  X.allocaXEvent $ \event_ptr ->
  withForeignPtr (skypeDisplay api) $ \display_ptr -> do
    #{poke XClientMessageEvent, type}         event_ptr (#{const ClientMessage} :: CInt)
    #{poke XClientMessageEvent, display}      event_ptr display_ptr
    #{poke XClientMessageEvent, window}       event_ptr $ skypeWindow api
    #{poke XClientMessageEvent, message_type} event_ptr $ skypeMessageBeginAtom api
    #{poke XClientMessageEvent, format}       event_ptr (8 :: CInt)  -- 8 bit values

    let display = X.Display display_ptr

    case splitPerChunk message of
      []       -> return ()
      (bs:[])  -> send display event_ptr bs
      (bs:bss) -> do
        send display event_ptr bs

        #{poke XClientMessageEvent, message_type} event_ptr $ skypeMessageContinueAtom api

        mapM_ (send display event_ptr) bss

  where
    send display event_ptr chunk = do
      let data_ptr = #{ptr XClientMessageEvent, data} event_ptr

      BS.unsafeUseAsCStringLen chunk $ uncurry $ copyArray data_ptr

      X.sendEvent display (skypeInstanceWindow api) False 0 event_ptr
      X.flush display

    splitPerChunk bs
      | BS.length bs == messageChunkSize = bs : BS.singleton 0 : []
      | BS.length bs < messageChunkSize  = BS.snoc bs 0 : []
      | otherwise                        = let (xs, ys) = BS.splitAt messageChunkSize bs
                                           in  xs : splitPerChunk ys

-- | Generalized version of 'withForeignPtr'.
withForeignPtr' :: (MonadBaseControl IO m) => ForeignPtr a -> (Ptr a -> m b) -> m b
withForeignPtr' fp action =
  control $ \runInIO -> withForeignPtr fp $ runInIO . action

ptrIndex :: (Eq a, Storable a) => Ptr a -> a -> Int -> IO (Maybe Int)
ptrIndex ptr value n = go 0 value $ take n $ iterate (flip plusPtr 1) ptr
  where
    go _ _ [] = return Nothing
    go acc x (y:ys) = do
      y' <- peek y
      if y' == x
        then return $ Just acc
        else go (acc + 1) x ys

messageChunkSize :: Int
messageChunkSize = #{const sizeof(((XClientMessageEvent *) 0)->data.b) / sizeof(((XClientMessageEvent *) 0)->data.b[0])}
{-# INLINE messageChunkSize #-}

runEventLoop :: SkypeAPI -> (BL.ByteString -> IO ()) -> IO ()
runEventLoop api action =
  X.allocaXEvent $ \event_ptr ->
  withForeignPtr (skypeDisplay api) $ \display_ptr ->
  loop mempty (X.Display display_ptr) event_ptr
  where
    loop builder display event_ptr = do
      X.nextEvent display event_ptr  -- will blocking

      eventType <- #{peek XClientMessageEvent, type} event_ptr

      if eventType == X.clientMessage
      then do
        messageType <- #{peek XClientMessageEvent, message_type} event_ptr

        if messageType == skypeMessageBeginAtom api ||
           messageType == skypeMessageContinueAtom api
        then do
          let data_ptr = #{ptr XClientMessageEvent, data} event_ptr

          maybeIndex <- ptrIndex data_ptr (0 :: CChar) messageChunkSize

          case maybeIndex of
            Just i -> do
              bs <- BS.packCStringLen (data_ptr, i)
              action $ BS.toLazyByteString $ builder `mappend` BS.byteString bs
              loop mempty display event_ptr

            Nothing -> do
              bs <- BS.packCStringLen (data_ptr, messageChunkSize)
              loop (builder `mappend` BS.byteString bs) display event_ptr
        else
          loop builder display event_ptr
      else
        loop builder display event_ptr