packages feed

haskell-xmpp-2.0.0: src/Network/XMPP/Stream.hs

{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}

-----------------------------------------------------------------------------
-- |
-- Module      :  Network.XMPP.Stream
-- Copyright   :  (c) Dmitry Astapov, 2006 ; pierre, 2007
-- License     :  BSD-style (see the file LICENSE)
-- Copyright   :  (c) riskbook, 2020
-- SPDX-License-Identifier:  BSD3
-- 
-- Maintainer  :  Dmitry Astapov <dastapov@gmail.com>, pierre <k.pierre.k@gmail.com>
-- Stability   :  experimental
-- Portability :  portable
--
-- An XMPP stream: means to create and use one
--
-----------------------------------------------------------------------------
module Network.XMPP.Stream
  ( XmppSendable(..)
  , Plugin(..)
  , XmppError(..)
  , startM, nextM, withNextM, selectM, xtractM, textractM, withSelectM
  , resetStreamHandle, getText, getText_
  , loopWithPlugins
  , getNextId
  , parse, parseM, waitAndProcess
  , withUUID
  ) where

import           Control.Monad                (void)
import           Control.Monad.State          (MonadState(..), gets, modify)
import           Control.Monad.Except         (runExceptT, throwError, lift)
import           Control.Monad.IO.Class       (MonadIO(..))
import           Control.Applicative          (Alternative, empty)
import           System.IO                    (Handle, hGetContents)
import           Data.Text                    (Text, unpack, pack)
import qualified Data.UUID.V4                 as UUID
import qualified Data.UUID                    as UUID
import           Data.Functor                 (($>))

import           Text.XML                     (Node)
import           Text.XML.HaXml.Lex           (xmlLex)
import           Text.XML.HaXml.Parse
import           Text.XML.HaXml.Posn          (Posn, noPos)
import           Text.XML.HaXml.Types
import qualified Text.XML.HaXml.Pretty        as P (content)
import           Text.XML.HaXml.Xtract.Parse  (xtract)
import           Text.ParserCombinators.Poly.State (onFail)

import           Network.XMPP.Print           (hPutNode, hPutXmpp)
import           Network.XMPP.Utils
import           Network.XMPP.Types
import           Network.XMPP.UTF8
import           Network.XMPP.XML
import           Network.XMPP.Stanza

-- Main 'workhorses' for Stream are 'xmppSend', 'nextM', 'peekM' and 'selectM':
-- | Sends message into Stream
class XmppSendable t a where
  xmppSend :: Monad t => a -> t ()

instance MonadIO m => XmppSendable (XmppMonad m) Node where
  xmppSend node = do
    h <- gets handle
    liftIO $ hPutNode h node

instance MonadIO m => XmppSendable (XmppMonad m) (Content Posn) where
  xmppSend content = do
    h <- gets handle
    liftIO $ hPutXmpp h content

instance MonadIO m => XmppSendable (XmppMonad m) (Stanza t 'Outgoing e) where
  xmppSend s = xmppSend (encodeStanza s :: Node)

data XmppError =
    StreamClosedError
  | MessageParseError Text Text
  | NonSupportedAuthMechanisms [Text] Text
  | AuthError Text
  | RanOutOfInput
  | UnknownVersion Text
  | UnknownError Text
  deriving (Eq, Show)

-- | Parses XML element producing Stanza
parse :: forall l e. (Alternative l, FromXML e) => Content Posn -> l (SomeStanza e)
parse m | xtractp id "/message" m  = mSucceed (decodeStanza m :: Maybe (Stanza 'Message 'Incoming e))
        | xtractp id "/presence" m = mSucceed (decodeStanza m :: Maybe (Stanza 'Presence 'Incoming e))
        | xtractp id "/iq" m       = mSucceed (decodeStanza m :: Maybe (Stanza 'IQ 'Incoming e))
        | otherwise                = empty
  where xtractp f p m = not . null $ xtract f p m
        mSucceed :: (Alternative l, FromXML e) => Maybe (Stanza t p e) -> l (SomeStanza e)
        mSucceed = maybe empty (pure . SomeStanza)

-- | Gets next message from stream and parses it
-- | We shall skip over unknown messages, rather than crashing
parseM :: (FromXML e, MonadIO m) => XmppMonad m (Either XmppError (SomeStanza e))
parseM = (fmap parse <$> nextM) >>= \case
  Right m -> maybe parseM (pure . Right) m
  Left  e -> pure $ Left e

-- | Skips all messages, that will return result `Nothing` from computation
-- | In other words - waits for appropriate message, defined by predicate
waitAndProcess
  :: (FromXML e, MonadIO m)
  => (SomeStanza e -> Maybe a)
  -> XmppMonad m (Either XmppError a)
waitAndProcess compute = parseM >>= \case
  Right m   -> maybe (waitAndProcess compute) (pure . Right) $ compute m
  Left  err -> pure $ Left err

withUUID :: MonadIO m => (UUID.UUID -> Stanza t p e) -> m (Stanza t p e)
withUUID setUUID = setUUID <$> liftIO UUID.nextRandom

-- | Selects next messages from stream
nextM :: MonadIO m => XmppMonad m (Either XmppError (Content Posn))
nextM = runExceptT $ do
  ls <- lift $ gets lexemes

  if null ls then throwError RanOutOfInput else pure ()

  case xmlParseWith (elemCloseTag $ N "stream:stream") ls of
    (Right (), rest) -> do
      modify (\stream -> stream { lexemes = rest })
      throwError StreamClosedError -- all subsequent queries will end by EOF exception
    _ -> case xmlParseWith element ls of
      (Right e, rest) -> do
        let msg = CElem e noPos
        lift $ debug $ "nextM: Got element: " ++ show (P.content msg)
        modify (\stream -> stream { lexemes = rest }) $> msg
      (Left err, _) ->
        throwError $ MessageParseError (pack $ show ls) $ pack $ show err

-- | Selects next message matching predicate
selectM
  :: MonadIO m
  => (Content Posn -> Bool)
  -> XmppMonad m (Either XmppError (Content Posn))
selectM p = runExceptT $ do
  m <- lift nextM >>= either throwError pure
  if p m then pure m else throwError $ UnknownError "Failed to select message"

-- | Pass in xtract query, return query result from the first message where it returns non-empty results
xtractM :: MonadIO m => Text -> XmppMonad m [Content Posn]
xtractM q =do
  eim <- selectM (not . null . xtract id (unpack q))
  case eim of
    Right m -> pure $ xtract id (unpack q) m
    Left _e -> pure [] -- TODO

textractM :: MonadIO m => Text -> XmppMonad m Text
textractM q = do
  res <- xtractM q
  pure $ case res of
    [] -> ""
    x  -> getText_ x

withNextM :: MonadIO m => (Content Posn -> b) -> XmppMonad m (Either XmppError b)
withNextM compute = fmap compute <$> nextM

withSelectM
  :: MonadIO m
  => (Content Posn -> Bool)
  -> (Content Posn -> b)
  -> XmppMonad m (Either XmppError b)
withSelectM predicate compute =
  selectM predicate >>= either (pure . Left) (pure . Right . compute)


-- | startM is a special accessor case, since it has to retrieve only opening tag of the '<stream>' message,
-- which encloses the whole XMPP stream. That's why it does it's own parsing, and does not rely on 'nextM'
startM :: MonadIO m => XmppMonad m (Either XmppError [Attribute])
startM = do
  (starter, rest) <- xmlParseWith streamStart <$> gets lexemes
  case starter of
    Left e -> pure $ Left $ UnknownError $ pack e
    Right (ElemTag (N "stream:stream") attrs) ->
      modify (\stream -> stream { lexemes = rest }) $> Right attrs
    Right _ ->
      pure $ Left $ UnknownError "Unexpected element at the beginning of XMPP stream!"
 where
  streamStart = void processinginstruction `onFail` return () >> elemOpenTag

-- | Replaces contents of the Stream with the contents coming from given handle.
resetStreamHandle :: (MonadIO m, MonadState Stream m) => Handle -> m ()
resetStreamHandle h =
  do c <- liftIO $ hGetContents h
     modify (\stream -> stream { handle=h , lexemes = xmlLex "stream" (fromUTF8 c) })

-------------------------------
-- Basic plugin support
data Plugin
    = Plugin
    { trigger :: String
    , body    :: Content Posn -> XmppMonad IO ()
    }

loopWithPlugins :: [Plugin] -> XmppMonad IO (Either Text ())
loopWithPlugins ps =
  let loop = nextM >>= \case
        Right m -> do
          let notEmpty p = not $ null $ xtract id (trigger p) m
          sequence_ [ body p m | p <- ps, notEmpty p ] >> loop
        Left _e -> loop
  in  loop

getNextId :: MonadIO m => XmppMonad m Int
getNextId = do
  i <- gets idx
  modify (\stream -> stream { idx = i + 1 })
  return i