module Network.Xmpp.IM.Roster where
import Control.Applicative ((<$>))
import Control.Concurrent.STM
import Control.Monad
import Data.List (nub)
#if MIN_VERSION_containers(0, 5, 0)
import qualified Data.Map.Strict as Map
#else
import qualified Data.Map as Map
#endif
import Data.Maybe (isJust, fromMaybe)
import Data.Text (Text)
import Data.XML.Pickle
import Data.XML.Types
import System.Log.Logger
import Network.Xmpp.Concurrent.Basic
import Network.Xmpp.Concurrent.IQ
import Network.Xmpp.Concurrent.Types
import Network.Xmpp.IM.Roster.Types
import Network.Xmpp.Marshal
import Network.Xmpp.Types
timeout :: Maybe Integer
timeout = Just 3000000
rosterPush :: Item -> Session -> IO (Either IQSendError (Annotated IQResponse))
rosterPush item session = do
let el = pickleElem xpQuery (Query Nothing [fromItem item])
sendIQA' timeout Nothing Set Nothing el session
rosterAdd :: Jid
-> Maybe Text
-> [Text]
-> Session
-> IO (Either IQSendError (Annotated IQResponse))
rosterAdd j n gs session = do
let el = pickleElem xpQuery (Query Nothing
[QueryItem { qiApproved = Nothing
, qiAsk = False
, qiJid = j
, qiName = n
, qiSubscription = Nothing
, qiGroups = nub gs
}])
sendIQA' timeout Nothing Set Nothing el session
rosterRemove :: Jid -> Session -> IO Bool
rosterRemove j sess = do
roster <- getRoster sess
case Map.lookup j (items roster) of
Nothing -> return True
Just _ -> do
res <- rosterPush (Item False False j Nothing Remove []) sess
case res of
Right (IQResponseResult IQResult{}, _) -> return True
_ -> return False
getRoster :: Session -> IO Roster
getRoster session = atomically $ readTVar (rosterRef session)
initRoster :: Session -> IO ()
initRoster session = do
oldRoster <- getRoster session
mbRoster <- retrieveRoster (if isJust (ver oldRoster) then Just oldRoster
else Nothing ) session
case mbRoster of
Nothing -> errorM "Pontarius.Xmpp"
"Server did not return a roster: "
Just roster -> atomically $ writeTVar (rosterRef session) roster
handleRoster :: TVar Roster -> StanzaHandler
handleRoster ref out sta _ = case sta of
IQRequestS (iqr@IQRequest{iqRequestPayload =
iqb@Element{elementName = en}})
| nameNamespace en == Just "jabber:iq:roster" -> do
case iqRequestFrom iqr of
Just _from -> return [(sta, [])]
Nothing -> case unpickleElem xpQuery iqb of
Right Query{ queryVer = v
, queryItems = [update]
} -> do
handleUpdate v update
_ <- out $ result iqr
return []
_ -> do
errorM "Pontarius.Xmpp" "Invalid roster query"
_ <- out $ badRequest iqr
return []
_ -> return [(sta, [])]
where
handleUpdate v' update = atomically $ modifyTVar ref $ \(Roster v is) ->
Roster (v' `mplus` v) $ case qiSubscription update of
Just Remove -> Map.delete (qiJid update) is
_ -> Map.insert (qiJid update) (toItem update) is
badRequest (IQRequest iqid from _to lang _tp bd) =
IQErrorS $ IQError iqid Nothing from lang errBR (Just bd)
errBR = StanzaError Cancel BadRequest Nothing Nothing
result (IQRequest iqid from _to lang _tp _bd) =
IQResultS $ IQResult iqid Nothing from lang Nothing
retrieveRoster :: Maybe Roster -> Session -> IO (Maybe Roster)
retrieveRoster mbOldRoster sess = do
useVersioning <- isJust . rosterVer <$> getFeatures sess
let version = if useVersioning
then case mbOldRoster of
Nothing -> Just ""
Just oldRoster -> ver oldRoster
else Nothing
res <- sendIQ' timeout Nothing Get Nothing
(pickleElem xpQuery (Query version []))
sess
case res of
Left e -> do
errorM "Pontarius.Xmpp.Roster" $ "getRoster: " ++ show e
return Nothing
Right (IQResponseResult IQResult{iqResultPayload = Just ros})
-> case unpickleElem xpQuery ros of
Left _e -> do
errorM "Pontarius.Xmpp.Roster" "getRoster: invalid query element"
return Nothing
Right ros' -> return . Just $ toRoster ros'
Right (IQResponseResult IQResult{iqResultPayload = Nothing}) -> do
return mbOldRoster
Right (IQResponseError e) -> do
errorM "Pontarius.Xmpp.Roster" $ "getRoster: server returned error"
++ show e
return Nothing
where
toRoster (Query v is) = Roster v (Map.fromList
$ map (\i -> (qiJid i, toItem i))
is)
toItem :: QueryItem -> Item
toItem qi = Item { riApproved = fromMaybe False (qiApproved qi)
, riAsk = qiAsk qi
, riJid = qiJid qi
, riName = qiName qi
, riSubscription = fromMaybe None (qiSubscription qi)
, riGroups = nub $ qiGroups qi
}
fromItem :: Item -> QueryItem
fromItem i = QueryItem { qiApproved = Nothing
, qiAsk = False
, qiJid = riJid i
, qiName = riName i
, qiSubscription = case riSubscription i of
Remove -> Just Remove
_ -> Nothing
, qiGroups = nub $ riGroups i
}
xpItems :: PU [Node] [QueryItem]
xpItems = xpWrap (map (\((app_, ask_, jid_, name_, sub_), groups_) ->
QueryItem app_ ask_ jid_ name_ sub_ groups_))
(map (\(QueryItem app_ ask_ jid_ name_ sub_ groups_) ->
((app_, ask_, jid_, name_, sub_), groups_))) $
xpElems "{jabber:iq:roster}item"
(xp5Tuple
(xpAttribute' "approved" xpBool)
(xpWrap isJust
(\p -> if p then Just () else Nothing) $
xpOption $ xpAttribute_ "ask" "subscribe")
(xpAttribute "jid" xpJid)
(xpAttribute' "name" xpText)
(xpAttribute' "subscription" xpSubscription)
)
(xpFindMatches $ xpElemText "{jabber:iq:roster}group")
xpQuery :: PU [Node] Query
xpQuery = xpWrap (\(ver_, items_) -> Query ver_ items_ )
(\(Query ver_ items_) -> (ver_, items_)) $
xpElem "{jabber:iq:roster}query"
(xpAttribute' "ver" xpText)
xpItems
xpSubscription :: PU Text Subscription
xpSubscription = ("xpSubscription", "") <?>
xpPartial ( \input -> case subscriptionFromText input of
Nothing -> Left "Could not parse subscription."
Just j -> Right j)
subscriptionToText
where
subscriptionFromText "none" = Just None
subscriptionFromText "to" = Just To
subscriptionFromText "from" = Just From
subscriptionFromText "both" = Just Both
subscriptionFromText "remove" = Just Remove
subscriptionFromText _ = Nothing
subscriptionToText None = "none"
subscriptionToText To = "to"
subscriptionToText From = "from"
subscriptionToText Both = "both"
subscriptionToText Remove = "remove"