{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# OPTIONS_HADDOCK hide #-} module Network.Xmpp.Concurrent.IQ where import Control.Applicative ((<$>)) import Control.Concurrent (forkIO) import Control.Concurrent.STM import Control.Concurrent.Thread.Delay (delay) import Control.Monad import Control.Monad.Error import Control.Monad.Trans import qualified Data.Map as Map import Data.Maybe import Data.Text (Text) import Data.XML.Pickle import Data.XML.Types import Lens.Family2 (toListOf, (&), (^.)) import Network.Xmpp.Concurrent.Basic import Network.Xmpp.Concurrent.Types import Network.Xmpp.Lens import Network.Xmpp.Stanza import Network.Xmpp.Types import System.Log.Logger -- | Sends an IQ, returns an STM action that returns the first inbound IQ with a -- matching ID that has type @result@ or @error@ or Nothing if the timeout was -- reached. -- -- When sending the action fails, an XmppFailure is returned. sendIQ :: Maybe Integer -- ^ Timeout . When the timeout is reached the response -- TMVar will be filled with 'IQResponseTimeout' and the -- id is removed from the list of IQ handlers. 'Nothing' -- deactivates the timeout -> Maybe Jid -- ^ Recipient (to) -> IQRequestType -- ^ IQ type (@Get@ or @Set@) -> Maybe LangTag -- ^ Language tag of the payload (@Nothing@ for -- default) -> Element -- ^ The IQ body (there has to be exactly one) -> [ExtendedAttribute] -- ^ Additional stanza attributes -> Session -> IO (Either XmppFailure (STM (Maybe (Annotated IQResponse)))) sendIQ timeOut t tp lang body attrs session = do newId <- idGenerator session j <- case t of Just t -> return $ Right t Nothing -> Left <$> getJid session ref <- atomically $ do resRef <- newEmptyTMVar let value = (j, resRef) (byNS, byId) <- readTVar (iqHandlers session) writeTVar (iqHandlers session) (byNS, Map.insert newId value byId) return resRef res <- sendStanza (IQRequestS $ IQRequest newId Nothing t lang tp body attrs) session case res of Right () -> do case timeOut of Nothing -> return () Just t -> void . forkIO $ do delay t doTimeOut (iqHandlers session) newId ref return . Right $ readTMVar ref Left e -> return $ Left e where doTimeOut handlers iqid var = atomically $ do p <- tryPutTMVar var Nothing when p $ do (byNS, byId) <- readTVar (iqHandlers session) writeTVar handlers (byNS, Map.delete iqid byId) return () -- | Like 'sendIQ', but waits for the answer IQ. sendIQA' :: Maybe Integer -> Maybe Jid -> IQRequestType -> Maybe LangTag -> Element -> [ExtendedAttribute] -> Session -> IO (Either IQSendError (Annotated IQResponse)) sendIQA' timeout to tp lang body attrs session = do ref <- sendIQ timeout to tp lang body attrs session either (return . Left . IQSendError) (fmap (maybe (Left IQTimeOut) Right) . atomically) ref -- | Like 'sendIQ', but waits for the answer IQ. Discards plugin Annotations sendIQ' :: Maybe Integer -> Maybe Jid -> IQRequestType -> Maybe LangTag -> Element -> [ExtendedAttribute] -> Session -> IO (Either IQSendError IQResponse) sendIQ' timeout to tp lang body attrs session = fmap fst <$> sendIQA' timeout to tp lang body attrs session -- | Register your interest in inbound IQ stanzas of a specific type and -- namespace. The returned STM action yields the received, matching IQ stanzas. -- -- If a handler for IQ stanzas with the given type and namespace is already -- registered, the producer will be wrapped in Left. In this case the returned -- request tickets may already be processed elsewhere. listenIQ :: IQRequestType -- ^ Type of IQs to receive ('Get' or 'Set') -> Text -- ^ Namespace of the child element -> Session -> IO (Either (STM IQRequestTicket) (STM IQRequestTicket)) listenIQ tp ns session = do let handlers = (iqHandlers session) atomically $ do (byNS, byID) <- readTVar handlers iqCh <- newTChan let (present, byNS') = Map.insertLookupWithKey' (\_ _ old -> old) (tp, ns) iqCh byNS writeTVar handlers (byNS', byID) case present of Nothing -> return . Right $ readTChan iqCh Just iqCh' -> do clonedChan <- cloneTChan iqCh' return . Left $ readTChan clonedChan -- | Unregister a previously registered IQ handler. No more IQ stanzas will be -- delivered to any of the returned producers. unlistenIQ :: IQRequestType -- ^ Type of IQ ('Get' or 'Set') -> Text -- ^ Namespace of the child element -> Session -> IO () unlistenIQ tp ns session = do let handlers = (iqHandlers session) atomically $ do (byNS, byID) <- readTVar handlers let byNS' = Map.delete (tp, ns) byNS writeTVar handlers (byNS', byID) return () -- | Answer an IQ request. Only the first answer ist sent and Just True is -- returned when the answer is sucessfully sent. If an error occured during -- sending Just False is returned (and another attempt can be -- undertaken). Subsequent answers after the first sucessful one are dropped and -- (False is returned in that case) answerIQ :: IQRequestTicket -> Either StanzaError (Maybe Element) -> [ExtendedAttribute] -> IO (Maybe (Either XmppFailure ())) answerIQ = answerTicket -- Class class IQRequestClass a where data IQResponseType a pickleRequest :: PU Element a pickleResponse :: PU [Element] (IQResponseType a) requestType :: a -> IQRequestType requestNamespace :: a -> Text data IQRequestError = IQRequestSendError XmppFailure | IQRequestTimeout | IQRequestUnpickleError UnpickleError deriving Show -- | Send an IQ request. May throw IQSendError, UnpickleError, sendIQRequest :: (IQRequestClass a, MonadError IQRequestError m, MonadIO m) => Maybe Integer -> Maybe Jid -> a -> Session -> m (Either IQError (IQResponseType a)) sendIQRequest timeout t req con = do mbRes <- liftIO $ sendIQ' timeout t (requestType req) Nothing (pickle pickleRequest req) [] con case mbRes of Left (IQTimeOut) -> throwError IQRequestTimeout Left (IQSendError e) -> throwError $ IQRequestSendError e Right (IQResponseError e) -> return $ Left e Right (IQResponseResult res) -> case unpickle pickleResponse (res & toListOf payloadT) of Left e -> throwError $ IQRequestUnpickleError e Right r -> return $ Right r type IQRequestHandler a = a -> IO (Either StanzaError (IQResponseType a)) runIQHandler :: IQRequestClass a => IQRequestHandler a -> Session -> IO () runIQHandler (handler :: a -> IO (Either StanzaError (IQResponseType a))) sess = do let prx = undefined :: a ns = (requestNamespace prx) mbChan <- listenIQ (requestType prx) ns sess case mbChan of Left _ -> warningM "Pontarius.Xmpp" $ "IQ namespace " ++ show ns ++ " is already handled" Right getNext -> forever $ do ticket <- atomically getNext case unpickle pickleRequest (iqRequestBody ticket ^. payload) of Left _ -> answerIQ ticket (Left $ mkStanzaError BadRequest) [] Right req -> do res <- handler req case res of Left e -> answerIQ ticket (Left e) [] Right r -> do let answer = (pickle pickleResponse r) answerIQ ticket (Right $ listToMaybe answer ) []