{-# LANGUAGE CPP #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-} module Presence where import System.Directory import System.IO.Error #ifndef THREAD_DEBUG import Control.Concurrent #else import Control.Concurrent.Lifted.Instrument #endif import Control.Concurrent.STM import Control.Monad.Trans import Network.Socket ( SockAddr(..) ) import Data.Char import Data.List (nub, (\\), intersect, groupBy, sort, sortBy ) import Data.Ord (comparing ) import Data.Monoid ((<>)) import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import Control.Monad import Data.Text (Text) import qualified Data.Map as Map import Data.Map (Map) import Control.Exception ({-evaluate,-}handle,SomeException(..)) import System.Posix.User (getUserEntryForID,userName) import qualified Data.ByteString.Lazy.Char8 as L import qualified ConfigFiles import Data.Maybe import Data.Bits import Data.Int (Int8) import Data.XML.Types (Event) import System.Posix.Types (UserID,CPid) import Control.Applicative import Crypto.PubKey.Curve25519 (SecretKey,toPublic) import ControlMaybe import LockedChan (LockedChan) import TraversableT import UTmp (ProcessID,users) import LocalPeerCred import XMPPServer import PeerResolve import ConsoleWriter import ClientState import Util import qualified Connection import Network.Tox.NodeId (key2id) import Crypto.Tox (decodeSecret) import DPut {- isPeerKey :: ClientAddress -> Bool isPeerKey k = case k of { PeerKey {} -> True ; _ -> False } isClientKey :: ClientAddress -> Bool isClientKey k = case k of { ClientKey {} -> True ; _ -> False } -} localJID :: Text -> Text -> Text -> IO Text localJID user "." resource = do hostname <- textHostName return $ user <> "@" <> hostname <> "/" <> resource localJID user profile resource = return $ user <> "@" <> profile <> "/" <> resource -- | These hooks will be invoked in order to connect to *.tox hosts in the -- user's roster. -- -- The parameter k is a lookup key corresponding to an XMPP client. Each -- unique value should be able to hold a reference to the ToxID identity which -- should stay online until all interested keys have run 'deactivateAccount'. data ToxManager k = ToxManager -- | Put the given ToxID online. { activateAccount :: k -> Text -> SecretKey -> IO () -- | Take the given ToxID offline (assuming no other /k/ has a claim). , deactivateAccount :: k -> Text -> IO () -- | Try to connect to the remote peer (or not). -- -- The arguments are our public key (in hostname format) followed by -- their public key (in hostname format) and the Policy to set for this -- link. , setToxConnectionPolicy :: Text -> Text -> Connection.Policy -> IO () } data PresenceState = forall status. PresenceState { clients :: TVar (Map ClientAddress ClientState) , clientsByUser :: TVar (Map Text LocalPresence) , clientsByProfile :: TVar (Map Text LocalPresence) , remotesByPeer :: TVar (Map PeerAddress (Map UserName RemotePresence)) , server :: TMVar (XMPPServer, Connection.Manager status Text) , ckeyToChan :: TVar (Map ClientAddress Conn) , pkeyToChan :: TVar (Map PeerAddress Conn) , consoleWriter :: Maybe ConsoleWriter , toxManager :: Maybe (ToxManager ClientAddress) } newPresenceState :: Maybe ConsoleWriter -> Maybe (PresenceState -> ToxManager ClientAddress) -> TMVar (XMPPServer, Connection.Manager status Text) -> IO PresenceState newPresenceState cw toxman xmpp = atomically $ do clients <- newTVar Map.empty clientsByUser <- newTVar Map.empty clientsByProfile <- newTVar Map.empty remotesByPeer <- newTVar Map.empty ckeyToChan <- newTVar Map.empty pkeyToChan <- newTVar Map.empty let st = PresenceState { clients = clients , clientsByUser = clientsByUser , clientsByProfile = clientsByProfile , remotesByPeer = remotesByPeer , ckeyToChan = ckeyToChan , pkeyToChan = pkeyToChan , server = xmpp , consoleWriter = cw , toxManager = Nothing } return $ st { toxManager = fmap ($ st) toxman } nameForClient :: PresenceState -> ClientAddress -> IO Text nameForClient state k = do mc <- atomically $ do cmap <- readTVar (clients state) return $ Map.lookup k cmap case mc of Nothing -> textHostName Just client -> case clientProfile client of "." -> textHostName profile -> return profile presenceHooks :: PresenceState -> Int -> Maybe SockAddr -- ^ client-to-server bind address -> Maybe SockAddr -- ^ server-to-server bind address -> XMPPServerParameters presenceHooks state verbosity mclient mpeer = XMPPServerParameters { xmppChooseResourceName = chooseResourceName state , xmppTellClientHisName = tellClientHisName state , xmppTellMyNameToClient = nameForClient state , xmppTellMyNameToPeer = \(Local addr) -> return $ addrToText addr , xmppTellPeerHisName = return . peerKeyToText , xmppNewConnection = newConn state , xmppEOF = eofConn state , xmppRosterBuddies = rosterGetBuddies state , xmppRosterSubscribers = rosterGetSubscribers state , xmppRosterSolicited = rosterGetSolicited state , xmppRosterOthers = rosterGetOthers state , xmppSubscribeToRoster = informSentRoster state , xmppDeliverMessage = deliverMessage state , xmppInformClientPresence = informClientPresence state , xmppInformPeerPresence = informPeerPresence state , xmppAnswerProbe = \k stanza chan -> answerProbe state (stanzaTo stanza) k chan , xmppClientSubscriptionRequest = clientSubscriptionRequest state , xmppPeerSubscriptionRequest = peerSubscriptionRequest state , xmppClientInformSubscription = clientInformSubscription state , xmppPeerInformSubscription = peerInformSubscription state , xmppVerbosity = return verbosity , xmppClientBind = mclient , xmppPeerBind = mpeer } data LocalPresence = LocalPresence { networkClients :: Map ClientAddress ClientState -- TODO: loginClients } data RemotePresence = RemotePresence { resources :: Map Text Stanza -- , localSubscribers :: Map Text () -- ^ subset of clientsByUser who should be -- notified about this presence. } pcSingletonNetworkClient :: ClientAddress -> ClientState -> LocalPresence pcSingletonNetworkClient key client = LocalPresence { networkClients = Map.singleton key client } pcInsertNetworkClient :: ClientAddress -> ClientState -> LocalPresence -> LocalPresence pcInsertNetworkClient key client pc = pc { networkClients = Map.insert key client (networkClients pc) } pcRemoveNewtworkClient :: ClientAddress -> LocalPresence -> Maybe LocalPresence pcRemoveNewtworkClient key pc = if pcIsEmpty pc' then Nothing else Just pc' where pc' = pc { networkClients = Map.delete key (networkClients pc) } pcIsEmpty :: LocalPresence -> Bool pcIsEmpty pc = Map.null (networkClients pc) getConsolePids :: PresenceState -> IO [(Text,ProcessID)] getConsolePids state = do us <- UTmp.users return $ map (\(_,tty,pid)->(lazyByteStringToText tty,pid)) us identifyTTY' :: [(Text, ProcessID)] -> System.Posix.Types.UserID -> L.ByteString -> IO (Maybe Text, Maybe System.Posix.Types.CPid) identifyTTY' ttypids uid inode = ttypid where ttypids' = map (\(tty,pid)->(L.fromChunks [Text.encodeUtf8 tty], pid)) ttypids ttypid = fmap textify $ identifyTTY ttypids' uid inode textify (tty,pid) = (fmap lazyByteStringToText tty, pid) chooseResourceName :: PresenceState -> ClientAddress -> Remote SockAddr -> Maybe Text -> Maybe Text -> IO Text chooseResourceName state k (Remote addr) clientsNameForMe desired = do muid <- getLocalPeerCred' addr (mtty,pid) <- getTTYandPID muid user <- getJabberUserForId muid status <- atomically $ newTVar Nothing flgs <- atomically $ newTVar 0 profile <- fmap (fromMaybe ".") $ forM ((,) <$> clientsNameForMe <*> toxManager state) $ \(wanted_profile0,toxman) -> case Text.splitAt 43 wanted_profile0 of (pub,".tox") -> do cdir <- ConfigFiles.configPath (L.fromChunks [Text.encodeUtf8 user]) "." "" #if !MIN_VERSION_directory(1,2,5) let listDirectory path = filter (`notElem` [".",".."]) <$> getDirectoryContents path #endif cfs <- map Text.pack <$> listDirectory cdir `catchIOError` (\e -> return []) let profiles = filter (\f -> Text.toLower f == Text.toLower wanted_profile0) cfs -- dput XMisc $ "Toxmpp profile " ++ show (user,wanted_profile0,profiles,cfs) let wanted_profile = head $ profiles ++ [wanted_profile0] secs <- configText ConfigFiles.getSecrets user wanted_profile case secs of sec:_ | Just s <- decodeSecret (Text.encodeUtf8 sec) , map toLower (show $ key2id $ toPublic s) == map toLower (Text.unpack pub) -> do activateAccount toxman k wanted_profile s dput XMisc $ "loaded tox secret " ++ show sec return wanted_profile _ -> do -- XXX: We should probably fail to connect when an -- invalid Tox profile is used. For now, we'll -- fall back to the Unix account login. dput XMisc "failed to find tox secret" return "." ("*.tox","") -> do dput XMisc $ "TODO: Match single tox key profile or generate first." -- TODO: Match single tox key profile or generate first. _todo _ -> return "." let client = ClientState { clientResource = maybe "fallback" id mtty , clientUser = user , clientProfile = profile , clientPid = pid , clientStatus = status , clientFlags = flgs } do -- forward-lookup of the buddies so that it is cached for reversing. buds <- configText ConfigFiles.getBuddies (clientUser client) (clientProfile client) forM_ buds $ \bud -> do let (_,h,_) = splitJID bud forkIO $ void $ resolvePeer h atomically $ do modifyTVar' (clients state) $ Map.insert k client let add mb = Just $ maybe (pcSingletonNetworkClient k client) (pcInsertNetworkClient k client) mb modifyTVar' (clientsByUser state) $ Map.alter add (clientUser client) modifyTVar' (clientsByProfile state) $ Map.alter add (clientProfile client) localJID (clientUser client) (clientProfile client) (clientResource client) where getTTYandPID muid = do -- us <- fmap (map (second fst) . Map.toList) . readTVarIO $ activeUsers state ttypids <- getConsolePids state -- let tailOf3 ((_,a),b) = (a,b) (t,pid) <- case muid of Just (uid,inode) -> identifyTTY' ttypids uid inode Nothing -> return (Nothing,Nothing) let rsc = t `mplus` fmap ( ("pid."<>) . Text.pack . show ) pid return (rsc,pid) getJabberUserForId muid = maybe (return "nobody") (\(uid,_) -> handle (\(SomeException _) -> return . (<> "uid.") . Text.pack . show $ uid) $ do user <- fmap userName $ getUserEntryForID uid return (Text.pack user) ) muid -- Perform action with 'ClientState' associated with the given 'ClientAddress'. -- If there is no associated 'ClientState', then perform the supplied fallback -- action. forClient :: PresenceState -> ClientAddress -> IO b -> (ClientState -> IO b) -> IO b forClient state k fallback f = do mclient <- atomically $ do cs <- readTVar (clients state) return $ Map.lookup k cs maybe fallback f mclient tellClientHisName :: PresenceState -> ClientAddress -> IO Text tellClientHisName state k = forClient state k fallback go where fallback = localJID "nobody" "." "fallback" go client = localJID (clientUser client) (clientProfile client) (clientResource client) toMapUnit :: Ord k => [k] -> Map k () toMapUnit xs = Map.fromList $ map (,()) xs resolveAllPeers :: [Text] -> IO (Map PeerAddress ()) resolveAllPeers hosts = fmap (toMapUnit . concat) $ Prelude.mapM (fmap (take 1) . resolvePeer) hosts -- Read a roster file and start trying to connect to all relevent peers. rosterGetStuff :: (ConfigFiles.User -> ConfigFiles.Profile -> IO [L.ByteString]) -> PresenceState -> ClientAddress -> IO [Text] rosterGetStuff what state k = forClient state k (return []) $ \client -> do jids <- configText what (clientUser client) (clientProfile client) let hosts = map ((\(_,h,_)->h) . splitJID) jids -- Using case to bring 'status' type variable to Connection.Manager into scope. case state of PresenceState { server = svVar } -> do (sv,conns) <- atomically $ takeTMVar svVar -- Grok peers to associate with from the roster: forM_ hosts $ \host -> do -- We need either conns :: Connection.Manager TCPStatus Text -- or toxman :: ToxManager ClientAddress -- It is decided by checking hostnames for .tox ending. let policySetter = fromMaybe (Connection.setPolicy conns host) $ do toxman <- toxManager state (me , ".tox") <- Just $ Text.splitAt 43 (clientProfile client) (them, ".tox") <- Just $ Text.splitAt 43 host Just $ setToxConnectionPolicy toxman (clientProfile client) host policySetter Connection.TryingToConnect atomically $ putTMVar svVar (sv,conns) return jids rosterGetBuddies :: PresenceState -> ClientAddress -> IO [Text] rosterGetBuddies state k = rosterGetStuff ConfigFiles.getBuddies state k rosterGetSolicited :: PresenceState -> ClientAddress -> IO [Text] rosterGetSolicited = rosterGetStuff ConfigFiles.getSolicited -- XXX: Should we be connecting to these peers? rosterGetOthers :: PresenceState -> ClientAddress -> IO [Text] rosterGetOthers = rosterGetStuff ConfigFiles.getOthers rosterGetSubscribers :: PresenceState -> ClientAddress -> IO [Text] rosterGetSubscribers = rosterGetStuff ConfigFiles.getSubscribers data Conn = Conn { connChan :: TChan Stanza , auxData :: ConnectionData } -- Read config file as Text content rather than UTF8 bytestrings. configText :: Functor f => (ConfigFiles.User -> ConfigFiles.Profile -> f [L.ByteString]) -> Text -- user -> Text -- profile -> f [Text] -- items configText what u p = fmap (map lazyByteStringToText) $ what (textToLazyByteString u) (Text.unpack p) getBuddies' :: Text -> Text -> IO [Text] getBuddies' = configText ConfigFiles.getBuddies getSolicited' :: Text -> Text -> IO [Text] getSolicited' = configText ConfigFiles.getSolicited -- | Obtain from roster all buddies and pending buddies (called solicited -- regardless of whether we've yet delivered a friend-request) matching the -- supplied side-effecting predicate. -- -- Returned tuple: -- -- * Bool - True if buddy (should send probe). -- False if solicited (should send friend-request). -- -- * Maybe Username - Username field of contact. -- -- * Text - Unix user who owns this roster entry. -- -- * Text - Profile, "." for xmpp, "tox" for a tox-enabled client. -- getBuddiesAndSolicited :: PresenceState -> Text -- ^ Config profile: "." or tox host. -> (Text -> IO Bool) -- ^ Return True if you want this hostname. -> IO [(Bool, Maybe UserName, Text, Text)] getBuddiesAndSolicited state profile pred -- XXX: The following O(n²) nub may be a little -- too onerous. = fmap nub $ do cbu <- atomically $ readTVar $ clientsByUser state fmap concat $ sequence $ do (user,LocalPresence cmap) <- Map.toList cbu (isbud, getter) <- [(True ,getBuddies' ) ,(False,getSolicited')] return $ do buds <- map splitJID <$> getter user profile fmap concat $ forM buds $ \(u,h,r) -> do interested <- pred h if interested then return [(isbud,u,user,profile)] else return [] sendProbesAndSolicitations :: PresenceState -> PeerAddress -> Local SockAddr -> TChan Stanza -> IO () sendProbesAndSolicitations state k (Local laddr) chan = do prof <- atomically $ do pktc <- readTVar (pkeyToChan state) return $ maybe "." (cdProfile . auxData) $ Map.lookup k pktc -- get all buddies & solicited matching k for all users xs <- getBuddiesAndSolicited state prof $ \case h | ".tox" `Text.isSuffixOf` h -> return False -- Tox probes/solicitations are handled in ToxToXMPP module. h -> do addrs <- nub <$> resolvePeer h return $ k `elem` addrs -- Roster item resolves to /k/ peer. forM_ xs $ \(isbud,u,user,profile) -> do let make = if isbud then presenceProbe else presenceSolicitation toh = peerKeyToText k jid = unsplitJID (u,toh,Nothing) me = addrToText laddr -- xmppTellMyNameToPeer from = if isbud then me -- probe from server else -- solicitation from particular user unsplitJID (Just user,me,Nothing) stanza <- make from jid -- send probes for buddies, solicitations for solicited. putStrLn $ "probing "++show k++" for: " ++ show (isbud,jid) atomically $ writeTChan chan stanza -- reverse xs `seq` return () newConn :: PresenceState -> SockAddr -> ConnectionData -> TChan Stanza -> IO () newConn state saddr cdta outchan = case classifyConnection saddr cdta of Left (pkey,laddr) -> do atomically $ modifyTVar' (pkeyToChan state) $ Map.insert pkey Conn { connChan = outchan , auxData = cdta } sendProbesAndSolicitations state pkey laddr outchan Right (ckey,_) -> do atomically $ modifyTVar' (ckeyToChan state) $ Map.insert ckey Conn { connChan = outchan , auxData = cdta } delclient :: (Alternative m, Monad m) => ClientAddress -> m LocalPresence -> m LocalPresence delclient k mlp = do lp <- mlp let nc = Map.delete k $ networkClients lp guard $ not (Map.null nc) return $ lp { networkClients = nc } eofConn :: PresenceState -> SockAddr -> ConnectionData -> IO () eofConn state saddr cdta = do atomically $ case classifyConnection saddr cdta of Left (pkey,_) -> modifyTVar' (pkeyToChan state) $ Map.delete pkey Right (ckey,_) -> modifyTVar' (ckeyToChan state) $ Map.delete ckey case classifyConnection saddr cdta of Left (k,_) -> do let h = peerKeyToText k jids <- atomically $ do rbp <- readTVar (remotesByPeer state) return $ do umap <- maybeToList $ Map.lookup k rbp (u,rp) <- Map.toList umap r <- Map.keys (resources rp) return $ unsplitJID (Just u, h, Just r) forM_ jids $ \jid -> do stanza <- makePresenceStanza "jabber:client" (Just jid) Offline informPeerPresence state k stanza Right (k,_) -> do forClient state k (return ()) $ \client -> do forM_ (toxManager state) $ \toxman -> do case Text.splitAt 43 (clientProfile client) of (pub,".tox") -> deactivateAccount toxman k (clientProfile client) _ -> return () stanza <- makePresenceStanza "jabber:server" Nothing Offline informClientPresence state k stanza atomically $ do modifyTVar' (clientsByUser state) $ Map.alter (delclient k) (clientUser client) modifyTVar' (clientsByProfile state) $ Map.alter (delclient k) (clientProfile client) parseRemoteAddress :: Text -> IO (Maybe (Remote SockAddr)) parseRemoteAddress s = fmap Remote <$> parseAddress s -- This attempts to reverse resolve a peers address to give the human-friendly -- domain name as it appears in the roster. It prefers host names that occur -- in the given list of JIDs, but will fall back to any reverse-resolved name -- and if it was unable to reverse the address, it will yield an ip address. peerKeyToResolvedName :: [Text] -> PeerAddress -> IO Text peerKeyToResolvedName buds pk = do ns <- peerKeyToResolvedNames pk let hs = map (\jid -> let (_,h,_)=splitJID jid in h) buds ns' = sortBy (comparing $ not . flip elem hs) ns return $ fromMaybe (peerKeyToText pk) (listToMaybe ns') -- | The given address is taken to be the local address for the socket this JID -- came in on. The returned JID parts are suitable for unsplitJID to create a -- valid JID for communicating to a client. The returned Bool is True when the -- host part refers to this local host (i.e. it equals the given SockAddr). -- If there are multiple results, it will prefer one which is a member of the -- given list in the last argument. rewriteJIDForClient :: Local SockAddr -> Text -> [Text] -> IO (Bool,(Maybe Text,Text,Maybe Text)) rewriteJIDForClient (Local laddr) jid buds = do let (n,h,r) = splitJID jid maddr <- parseAddress (strip_brackets h) fromMaybe (return (False,(n,ip6literal h,r))) $ maddr <&> \saddr -> do let mine = sameAddress laddr saddr h' <- if mine then textHostName else peerKeyToResolvedName buds (addrToPeerKey $ Remote saddr) return (mine,(n,h',r)) -- Given a local address and an IP-address JID, we return True if the JID is -- local, False otherwise. Additionally, a list of equivalent hostname JIDS -- are returned. multiplyJIDForClient :: ClientAddress -> Text -> IO (Bool,[(Maybe Text,Text,Maybe Text)]) multiplyJIDForClient k jid = do let (n,h,r) = splitJID jid maddr <- parseAddress (strip_brackets h) fromMaybe (return (False,[(n,ip6literal h,r)])) $ maddr <&> \saddr -> do let Local laddr = addrFromClientKey k mine = sameAddress laddr saddr names <- if mine then fmap (:[]) textHostName else peerKeyToResolvedNames (addrToPeerKey $ Remote saddr) return (mine,map (\h' -> (n,h',r)) names) guardPortStrippedAddress :: Text -> Local SockAddr -> IO (Maybe ()) guardPortStrippedAddress h (Local laddr) = do maddr <- fmap (fmap (`withPort` 0)) $ parseAddress (strip_brackets h) let laddr' = laddr `withPort` 0 return $ maddr >>= guard . (==laddr') -- | Accepts a textual representation of a domainname -- JID suitable for client connections, and returns the -- coresponding ipv6 address JID suitable for peers paired -- with a PeerAddress with the address part of that JID in -- binary form. If no suitable address could be resolved -- for the given name, Nothing is returned. rewriteJIDForPeer :: Text -> IO (Maybe (Text,PeerAddress)) rewriteJIDForPeer jid = do let (n,h,r) = splitJID jid maddr <- fmap listToMaybe $ resolvePeer h return $ flip fmap maddr $ \addr -> let h' = peerKeyToText addr to' = unsplitJID (n,h',r) in (to',addr) deliverToConsole :: PresenceState -> IO () -> Stanza -> IO () deliverToConsole PresenceState{ consoleWriter = Just cw } fail msg = do did1 <- writeActiveTTY cw msg did2 <- writeAllPty cw msg if not (did1 || did2) then fail else return () deliverToConsole _ fail _ = fail -- | deliver or error stanza deliverMessage :: PresenceState -> IO () -> StanzaWrap (LockedChan Event) -> IO () deliverMessage state fail msg = case stanzaOrigin msg of ClientOrigin senderk _ -> do -- Case 1. Client -> Peer mto <- fmap join $ mapM rewriteJIDForPeer (stanzaTo msg) fromMaybe fail {- reverse lookup failure -} $ mto <&> \(to',k) -> do chans <- atomically $ readTVar (pkeyToChan state) fromMaybe fail $ (Map.lookup k chans) <&> \conn -> do -- original 'from' address is discarded. from' <- forClient state senderk (return Nothing) $ return . Just . clientJID conn -- dup <- atomically $ cloneStanza (msg { stanzaTo=Just to', stanzaFrom=Just from' }) let dup = (msg { stanzaTo=Just to', stanzaFrom=from' }) sendModifiedStanzaToPeer dup (connChan conn) PeerOrigin senderk _ -> do (pchans,cchans) <- atomically $ do pc <- readTVar (pkeyToChan state) cc <- readTVar (ckeyToChan state) return (pc,cc) fromMaybe fail $ (Map.lookup senderk pchans) <&> \(Conn { connChan = sender_chan , auxData = ConnectionData (Left laddr) ctyp cprof }) -> do fromMaybe fail $ (stanzaTo msg) <&> \to -> do (mine,(n,h,r)) <- rewriteJIDForClient laddr to [] if not mine then fail else do let to' = unsplitJID (n,h,r) let (cmapVar,ckey) = case ctyp of Tox -> (clientsByProfile state , Just cprof ) XMPP -> (clientsByUser state , n ) cmap <- atomically . readTVar $ cmapVar chans <- fmap (fromMaybe []) $ do forM (ckey >>= flip Map.lookup cmap) $ \presence_container -> do let ks = Map.keys (networkClients presence_container) chans = do (k,client) <- Map.toList $ networkClients presence_container chan <- maybeToList $ Map.lookup k cchans return (clientProfile client, clientUser client, chan) forM chans $ \(profile,user,chan) -> do buds <- configText ConfigFiles.getBuddies user profile from' <- case ctyp of Tox -> return $ stanzaFrom msg XMPP -> do forM (stanzaFrom msg) $ \from -> do (_,trip) <- rewriteJIDForClient laddr from buds return $ unsplitJID trip to' <- case ctyp of XMPP -> return $ stanzaTo msg Tox -> return $ Just $ unsplitJID (Just user, profile, Nothing) return (from',chan) putStrLn $ "chan count: " ++ show (length chans) if null chans then when (ctyp == XMPP) $ do forM_ (stanzaFrom msg) $ \from -> do from' <- do -- Fallback to "." profile when no clients. buds <- maybe (return []) (\n -> configText ConfigFiles.getBuddies n ".") n (_,trip) <- rewriteJIDForClient laddr from buds return . Just $ unsplitJID trip let msg' = msg { stanzaTo=Just to' , stanzaFrom=from' } deliverToConsole state fail msg' else do forM_ chans $ \(from',Conn { connChan=chan}) -> do -- TODO: Cloning isn't really neccessary unless there are multiple -- destinations and we should probably transition to minimal cloning, -- or else we should distinguish between announcable stanzas and -- consumable stanzas and announcables use write-only broadcast -- channels that must be cloned in order to be consumed. -- For now, we are doing redundant cloning. let msg' = msg { stanzaTo=Just to' , stanzaFrom=from' } dup <- cloneStanza msg' sendModifiedStanzaToClient dup chan setClientFlag :: PresenceState -> ClientAddress -> Int8 -> IO () setClientFlag state k flag = atomically $ do cmap <- readTVar (clients state) forM_ (Map.lookup k cmap) $ \client -> do setClientFlag0 client flag setClientFlag0 :: ClientState -> Int8 -> STM () setClientFlag0 client flag = modifyTVar' (clientFlags client) (\flgs -> flgs .|. flag) informSentRoster :: PresenceState -> ClientAddress -> IO () informSentRoster state k = do setClientFlag state k cf_interested subscribedPeers :: Text -> Text -> IO [PeerAddress] subscribedPeers user profile = do jids <- configText ConfigFiles.getSubscribers user profile let hosts = map ((\(_,h,_)->h) . splitJID) jids fmap Map.keys $ resolveAllPeers hosts -- | this JID is suitable for peers, not clients. clientJID :: Conn -> ClientState -> Text clientJID con client = unsplitJID ( Just $ clientUser client , either (\(Local a) -> addrToText a) -- my host name, for peers (error $ unlines [ "clientJID wrongly used for client connection!" , "TODO: my host name for clients? nameForClient? localJID?"]) $ cdAddr $ auxData con , Just $ clientResource client) -- | Send presence notification to subscribed peers. -- Note that a full JID from address will be added to the -- stanza if it is not present. informClientPresence :: PresenceState -> ClientAddress -> StanzaWrap (LockedChan Event) -> IO () informClientPresence state k stanza = do forClient state k (return ()) $ \client -> do informClientPresence0 state (Just k) client stanza informClientPresence0 :: PresenceState -> Maybe ClientAddress -> ClientState -> StanzaWrap (LockedChan Event) -> IO () informClientPresence0 state mbk client stanza = do dup <- cloneStanza stanza atomically $ writeTVar (clientStatus client) $ Just dup is_avail <- atomically $ clientIsAvailable client when (not is_avail) $ do atomically $ setClientFlag0 client cf_available maybe (return ()) (sendCachedPresence state) mbk addrs <- subscribedPeers (clientUser client) (clientProfile client) ktc <- atomically $ readTVar (pkeyToChan state) let connected = mapMaybe (flip Map.lookup ktc) addrs forM_ connected $ \con -> do let from' = clientJID con client mto <- runTraversableT $ do to <- liftT $ stanzaTo stanza (to',_) <- liftMT $ rewriteJIDForPeer to return to' dup <- cloneStanza stanza sendModifiedStanzaToPeer dup { stanzaFrom = Just from' , stanzaTo = mto } (connChan con) informPeerPresence :: PresenceState -> PeerAddress -> StanzaWrap (LockedChan Event) -> IO () informPeerPresence state k stanza = do -- Presence must indicate full JID with resource... putStrLn $ "xmppInformPeerPresence checking from address..." forM_ (stanzaFrom stanza) $ \from -> do let (muser,h,mresource) = splitJID from putStrLn $ "xmppInformPeerPresence from = " ++ show from -- forM_ mresource $ \resource -> do forM_ muser $ \user -> do clients <- atomically $ do -- Update remotesByPeer... rbp <- readTVar (remotesByPeer state) let umap = maybe Map.empty id $ Map.lookup k rbp rp = case (presenceShow $ stanzaType stanza) of Offline -> maybe Map.empty (\resource -> maybe (Map.empty) (Map.delete resource . resources) $ Map.lookup user umap) mresource _ ->maybe Map.empty (\resource -> maybe (Map.singleton resource stanza) (Map.insert resource stanza . resources ) $ Map.lookup user umap) mresource umap' = Map.insert user (RemotePresence rp) umap fromMaybe (return []) $ case presenceShow $ stanzaType stanza of Offline -> Just () _ -> mresource >> Just () <&> \_ -> do writeTVar (remotesByPeer state) $ Map.insert k umap' rbp -- TODO: Store or delete the stanza (remotesByPeer) -- all clients, we'll filter available/authorized later ktc <- readTVar (ckeyToChan state) runTraversableT $ do (ck,client) <- liftMT $ fmap Map.toList $ readTVar (clients state) con <- liftMaybe $ Map.lookup ck ktc return (ck,con,client) putStrLn $ "xmppInformPeerPresence (length clients="++show (length clients)++")" forM_ clients $ \(ck,con,client) -> do -- (TODO: appropriately authorized clients only.) -- For now, all "available" clients (available = sent initial presence) is_avail <- atomically $ clientIsAvailable client when is_avail $ do putStrLn $ "reversing for client: " ++ show from froms <- do -- flip (maybe $ return [from]) k . const $ do (_,trip) <- multiplyJIDForClient ck from return (map unsplitJID trip) putStrLn $ "sending to client: " ++ show (stanzaType stanza,froms) forM_ froms $ \from' -> do dup <- cloneStanza stanza sendModifiedStanzaToClient (dup { stanzaFrom=Just from' }) (connChan con) consoleClients :: PresenceState -> STM (Map Text ClientState) consoleClients PresenceState{ consoleWriter = Just cw } = readTVar (cwClients cw) consoleClients _ = return Map.empty answerProbe :: PresenceState -> Maybe Text -> PeerAddress -> TChan Stanza -> IO () answerProbe state mto k chan = do -- putStrLn $ "answerProbe! " ++ show (stanzaType stanza) ktc <- atomically $ readTVar (pkeyToChan state) muser <- runTraversableT $ do to <- liftT $ mto conn <- liftT $ Map.lookup k ktc let (mu,h,_) = splitJID to -- TODO: currently resource-id is ignored on presence -- probes. Is this correct? Check the spec. Left laddr = cdAddr $ auxData conn liftMT $ guardPortStrippedAddress h laddr u <- liftT mu -- ORIG let ch = addrToText (auxAddr conn) -- ORIG return (u,conn,ch) let ch = addrToText a where Local a = laddr return (u,conn,ch) forM_ muser $ \(u,conn,ch) -> do profiles <- releventProfiles (cdType $ auxData conn) u forM_ profiles $ \profile -> do -- only subscribed peers should get probe replies resolved_subs <- resolvedFromRoster ConfigFiles.getSubscribers u profile let gaddrs = groupBy sameHost (sort resolved_subs) sameHost a b = (snd a == snd b) -- (==) `on` snd whitelist = do xs <- gaddrs -- group of subscribed jids on the same host x <- take 1 xs -- the host from the group guard $ snd x==k -- only hosts matching the key /k/ mapMaybe fst xs -- all users subscribed at the remote peer /k/ -- TODO: notify remote peer that they are unsubscribed? -- reply <- makeInformSubscription "jabber:server" to from False when (not $ null whitelist) $ do replies <- runTraversableT $ do cbu <- lift . atomically $ readTVar (clientsByUser state) let lpres = maybeToList $ Map.lookup u cbu cw <- lift . atomically $ consoleClients state clientState <- liftT $ (lpres >>= Map.elems . networkClients) ++ Map.elems cw stanza <- liftIOMaybe $ atomically (readTVar (clientStatus clientState)) stanza <- lift $ cloneStanza stanza let jid = unsplitJID (Just $ clientUser clientState , ch ,Just $ clientResource clientState) return stanza { stanzaFrom = Just jid , stanzaType = (stanzaType stanza) { presenceWhiteList = whitelist } } forM_ replies $ \reply -> do sendModifiedStanzaToPeer reply chan -- if no presence, send offline message when (null replies) $ do let jid = unsplitJID (Just u,ch,Nothing) pstanza <- makePresenceStanza "jabber:server" (Just jid) Offline atomically $ writeTChan (connChan conn) pstanza -- Send friend requests and remote presences stored in remotesByPeer to XMPP -- clients. sendCachedPresence :: PresenceState -> ClientAddress -> IO () sendCachedPresence state k = do forClient state k (return ()) $ \client -> do rbp <- atomically $ readTVar (remotesByPeer state) jids <- configText ConfigFiles.getBuddies (clientUser client) (clientProfile client) let hosts = map ((\(_,h,_)->h) . splitJID) jids addrs <- resolveAllPeers hosts let onlines = rbp `Map.intersection` addrs mcon <- atomically $ do ktc <- readTVar (ckeyToChan state) return $ Map.lookup k ktc forM_ mcon $ \con -> do forM_ (Map.toList onlines) $ \(pk, umap) -> do forM_ (Map.toList umap) $ \(user,rp) -> do let h = peerKeyToText pk forM_ (Map.toList $ resources rp) $ \(resource,stanza) -> do let jid = unsplitJID (Just user,h,Just resource) (mine,js) <- multiplyJIDForClient k jid forM_ js $ \jid -> do let from' = unsplitJID jid dup <- cloneStanza stanza sendModifiedStanzaToClient (dup { stanzaFrom=Just from' }) (connChan con) pending <- configText ConfigFiles.getPending (clientUser client) (clientProfile client) hostname <- nameForClient state k forM_ pending $ \pending_jid -> do let cjid = unsplitJID ( Just $ clientUser client , hostname , Nothing ) ask <- presenceSolicitation pending_jid cjid sendModifiedStanzaToClient ask (connChan con) -- Note: relying on self peer connection to send -- send local buddies. return () addToRosterFile :: (ConfigFiles.User -> ConfigFiles.Profile -> (L.ByteString -> IO (Maybe L.ByteString)) -> Maybe L.ByteString -> t1) -> Text -- user -> Text -- profile -> Text -> [PeerAddress] -> t1 addToRosterFile doit whose profile to addrs = modifyRosterFile doit whose profile to addrs True removeFromRosterFile :: (ConfigFiles.User -> ConfigFiles.Profile -> (L.ByteString -> IO (Maybe L.ByteString)) -> Maybe L.ByteString -> t1) -> Text -- user -> Text -- profile -> Text -> [PeerAddress] -> t1 removeFromRosterFile doit whose profile to addrs = modifyRosterFile doit whose profile to addrs False -- | Sanity-checked roster file manipulation. Primarily, this function handles -- hostname aliases. modifyRosterFile :: (ConfigFiles.User -> ConfigFiles.Profile -> (L.ByteString -> IO (Maybe L.ByteString)) -> Maybe L.ByteString -> t1) -- ^ Lower-level modification function -- indicating which file is being modified. -- Valid choices from ConfigFiles module: -- -- * modifySolicited -- -- * modifyBuddies -- -- * modifyPending -- -- * modifySubscribers -> Text -- ^ user -> Text -- ^ profile -> Text -- ^ JID that will be added or removed a hostname -> [PeerAddress] -- ^ Alias addresses for hostname in the JID. -> Bool -- ^ True if adding, otherwise False -> t1 modifyRosterFile doit whose profile to addrs bAdd = do let (mu,_,_) = splitJID to -- For each jid in the file, this function will decide whether to keep -- it (possibly modified) which is indicated by Just _ or to remove the -- item from the file which is indicatd by Nothing. cmp :: L.ByteString -> IO (Maybe L.ByteString) cmp jid = do let (msu,stored_h,mr) = splitJID (lazyByteStringToText jid) keep = return (Just jid) :: IO (Maybe L.ByteString) delete = return Nothing :: IO (Maybe L.ByteString) iocheck = do stored_addrs <- resolvePeer stored_h -- TODO: don't resolve .tox peers. case stored_addrs of [] -> keep -- do not delete if failed to resolve xs | null (xs \\ addrs) -> delete -- hostname alias, delete _ -> keep fmap join $ sequence $ do guard $ isNothing mr -- delete if resource specified in file. stored_u <- msu -- delete if user not specified in file. case mu of Nothing -> Just keep -- do not delete if /to/ has no user field Just u | u /= stored_u -> Just keep -- do not delete if users don't match Just _ -> Just iocheck -- do not delete unless hostname alias doit (textToLazyByteString whose) (Text.unpack profile) cmp (guard bAdd >> Just (textToLazyByteString to)) clientSubscriptionRequest :: PresenceState -> IO () -> ClientAddress -> Stanza -> TChan Stanza -> IO () clientSubscriptionRequest state fail k stanza chan = do forClient state k fail $ \client -> do fromMaybe fail $ (splitJID <$> stanzaTo stanza) <&> \(mu,h,_) -> do putStrLn $ "Forwarding solictation to peer" let to = unsplitJID (mu,h,Nothing) -- deleted resource cuser = clientUser client cprof = clientProfile client fromMaybe fail $ mu <&> \u -> do -- add to-address to from's solicited addrs <- if all (".tox" `Text.isSuffixOf`) [cprof,h] then return [] -- Avoid resolving .tox peers. else resolvePeer h addToRosterFile ConfigFiles.modifySolicited cuser cprof to addrs removeFromRosterFile ConfigFiles.modifyBuddies cuser cprof to addrs resolved_subs <- resolvedFromRoster ConfigFiles.getSubscribers cuser cprof let is_subscribed = not . null $ [ (mu, a) | a <- addrs ] `intersect` resolved_subs -- subscribers: "from" -- buddies: "to" case state of PresenceState { server = svVar } -> do (cktc,pktc,(sv,conns)) <- atomically $ do cktc <- readTVar $ ckeyToChan state pktc <- readTVar $ pkeyToChan state sc <- takeTMVar svVar return (cktc,pktc,sc) -- Update roster for each client. case stanzaType stanza of PresenceRequestSubscription True -> do hostname <- nameForClient state k let cjid = unsplitJID (Just $ clientUser client, hostname,Nothing) chans <- clientCons state cktc (clientUser client) forM_ chans $ \( Conn { connChan=chan }, client ) -> do -- roster update ask="subscribe" update <- makeRosterUpdate cjid to [ ("ask","subscribe") , if is_subscribed then ("subscription","from") else ("subscription","none") ] sendModifiedStanzaToClient update chan _ -> return () -- Send friend request to peer. let dsts = pktc `Map.intersection` toMapUnit addrs forM_ (Map.toList dsts) $ \(pk,con) -> do -- if already connected, send solicitation ... -- let from = clientJID con client let Left laddr = cdAddr $ auxData con from = unsplitJID ( Just $ clientUser client , (\(Local a) -> addrToText a) $ laddr , Nothing ) mb <- rewriteJIDForPeer to forM_ mb $ \(to',addr) -> do dup <- cloneStanza stanza sendModifiedStanzaToPeer (dup { stanzaTo = Just to' , stanzaFrom = Just from }) (connChan con) let policySetter = fromMaybe (Connection.setPolicy conns h) $ do toxman <- toxManager state (me , ".tox") <- Just $ Text.splitAt 43 (clientProfile client) (them, ".tox") <- Just $ Text.splitAt 43 h Just $ setToxConnectionPolicy toxman (clientProfile client) h -- Add peer if we are not already associated ... policySetter Connection.TryingToConnect atomically $ putTMVar svVar (sv,conns) resolvedFromRoster :: (ConfigFiles.User -> ConfigFiles.Profile -> IO [L.ByteString]) -> UserName -> Text -> IO [(Maybe UserName, PeerAddress)] resolvedFromRoster doit u profile = do subs <- configText doit u profile runTraversableT $ do (mu,h,_) <- liftT $ splitJID `fmap` subs addr <- liftMT $ fmap nub $ resolvePeer h return (mu,addr) clientCons :: PresenceState -> Map ClientAddress t -> Text -> IO [(t, ClientState)] clientCons state ktc u = map snd <$> clientCons' state ktc u clientCons' :: PresenceState -> Map ClientAddress t -> Text -> IO [(ClientAddress,(t, ClientState))] clientCons' state ktc u = do mlp <- atomically $ do cmap <- readTVar $ clientsByUser state return $ Map.lookup u cmap let ks = do lp <- maybeToList mlp Map.toList (networkClients lp) doit (k,client) = do con <- Map.lookup k ktc return (k,(con,client)) return $ mapMaybe doit ks releventProfiles :: ConnectionType -> Text -> IO [Text] releventProfiles XMPP _ = return ["."] releventProfiles ctyp user = do -- TODO: Return all the ".tox" profiles that a user has under his -- .presence/ directory. return [] peerSubscriptionRequest :: PresenceState -> IO () -> PeerAddress -> Stanza -> TChan Stanza -> IO () peerSubscriptionRequest state fail k stanza chan = do putStrLn $ "Handling pending subscription from remote" fromMaybe fail $ (stanzaFrom stanza) <&> \from -> do fromMaybe fail $ (stanzaTo stanza) <&> \to -> do let (mto_u,h,_) = splitJID to (mfrom_u,from_h,_) = splitJID from to <- return $ unsplitJID (mto_u,h,Nothing) -- delete resource from <- return $ unsplitJID (mfrom_u,from_h,Nothing) -- delete resource (pktc,cktc,cmap) <- atomically $ do cktc <- readTVar (ckeyToChan state) pktc <- readTVar (pkeyToChan state) cmap <- readTVar (clients state) return (pktc,cktc,cmap) fromMaybe fail $ (Map.lookup k pktc) <&> \Conn { auxData=ConnectionData (Left laddr) ctyp profile } -> do (mine,totup) <- rewriteJIDForClient laddr to [] if not mine then fail else do (_,fromtup) <- rewriteJIDForClient laddr from [] fromMaybe fail $ mto_u <&> \u -> do fromMaybe fail $ mfrom_u <&> \from_u -> do resolved_subs <- resolvedFromRoster ConfigFiles.getSubscribers u profile let already_subscribed = elem (mfrom_u,k) resolved_subs is_wanted = case stanzaType stanza of PresenceRequestSubscription b -> b _ -> False -- Shouldn't happen. -- Section 8 says (for presence of type "subscribe", the server MUST -- adhere to the rules defined under Section 3 and summarized under -- see Appendix A. (pariticularly Appendex A.3.1) if already_subscribed == is_wanted then do -- contact ∈ subscribers --> SHOULD NOT, already handled -- already subscribed, reply and quit -- (note: swapping to and from for reply) reply <- makeInformSubscription "jabber:server" to from is_wanted sendModifiedStanzaToPeer reply chan answerProbe state (Just to) k chan else do -- TODO: if peer-connection is to self, then auto-approve local user. -- add from-address to to's pending addrs <- resolvePeer from_h -- Catch exception in case the user does not exist if null addrs then fail else do let from' = unsplitJID fromtup -- Update roster files (subscribe: add to pending, unsubscribe: remove from subscribers). already_pending <- if is_wanted then addToRosterFile ConfigFiles.modifyPending u profile from' addrs else do removeFromRosterFile ConfigFiles.modifySubscribers u profile from' addrs reply <- makeInformSubscription "jabber:server" to from is_wanted sendModifiedStanzaToPeer reply chan return False -- contact ∉ subscribers & contact ∈ pending --> SHOULD NOT when (not already_pending) $ do -- contact ∉ subscribers & contact ∉ pending --> MUST chans <- clientCons state cktc u forM_ chans $ \( Conn { connChan=chan }, client ) -> do -- send to clients -- TODO: interested/available clients only? dup <- cloneStanza stanza sendModifiedStanzaToClient dup { stanzaFrom = Just $ from' , stanzaTo = Just $ unsplitJID totup } chan clientInformSubscription :: PresenceState -> IO () -> ClientAddress -> StanzaWrap (LockedChan Event) -> IO () clientInformSubscription state fail k stanza = do forClient state k fail $ \client -> do fromMaybe fail $ (stanzaTo stanza) <&> \to -> do putStrLn $ "clientInformSubscription" let (mu,h,mr) = splitJID to addrs <- resolvePeer h -- remove from pending buds <- resolvedFromRoster ConfigFiles.getBuddies (clientUser client) (clientProfile client) let is_buddy = not . null $ map (mu,) addrs `intersect` buds removeFromRosterFile ConfigFiles.modifyPending (clientUser client) (clientProfile client) to addrs let (relationship,addf,remf) = case stanzaType stanza of PresenceInformSubscription True -> ( ("subscription", if is_buddy then "both" else "from" ) , ConfigFiles.modifySubscribers , ConfigFiles.modifyOthers ) _ -> ( ("subscription", if is_buddy then "to" else "none" ) , ConfigFiles.modifyOthers , ConfigFiles.modifySubscribers ) addToRosterFile addf (clientUser client) (clientProfile client) to addrs removeFromRosterFile remf (clientUser client) (clientProfile client) to addrs do cbu <- atomically $ readTVar (clientsByUser state) putStrLn $ "cbu = " ++ show (fmap (fmap clientPid . networkClients) cbu) -- send roster update to clients (clients,ktc,pktc) <- atomically $ do cbu <- readTVar (clientsByUser state) let mlp = Map.lookup (clientUser client) cbu let cs = maybe [] (Map.toList . networkClients) mlp ktc <- readTVar (ckeyToChan state) pktc <- readTVar (pkeyToChan state) return (cs,ktc,pktc) forM_ clients $ \(ck, client) -> do is_intereseted <- atomically $ clientIsInterested client putStrLn $ "clientIsInterested: "++show is_intereseted is_intereseted <- atomically $ clientIsInterested client when is_intereseted $ do forM_ (Map.lookup ck ktc) $ \con -> do hostname <- nameForClient state ck -- TODO: Should cjid include the resource? let cjid = unsplitJID (mu, hostname, Nothing) update <- makeRosterUpdate cjid to [relationship] sendModifiedStanzaToClient update (connChan con) -- notify peer let dsts = toMapUnit addrs cdsts = pktc `Map.intersection` dsts forM_ (Map.toList cdsts) $ \(pk,con) -> do let from = clientJID con client to' = unsplitJID (mu, peerKeyToText pk, Nothing) dup <- cloneStanza stanza sendModifiedStanzaToPeer (dup { stanzaTo = Just $ to' , stanzaFrom = Just from }) (connChan con) answerProbe state (Just from) pk (connChan con) peerInformSubscription :: PresenceState -> IO () -> PeerAddress -> StanzaWrap (LockedChan Event) -> IO () peerInformSubscription state fail k stanza = do putStrLn $ "TODO: peerInformSubscription" -- remove from solicited fromMaybe fail $ (stanzaFrom stanza) <&> \from -> do (ktc,cktc,cmap) <- atomically $ do pktc <- readTVar (pkeyToChan state) cktc <- readTVar (ckeyToChan state) cmap <- readTVar (clients state) return (pktc,cktc,cmap) fromMaybe fail $ (Map.lookup k ktc) <&> \(Conn { connChan=sender_chan , auxData =ConnectionData (Left laddr) ctyp profile}) -> do (_,(from_u,from_h,_)) <- rewriteJIDForClient laddr from [] let from'' = unsplitJID (from_u,from_h,Nothing) muser = do to <- stanzaTo stanza let (mu,to_h,to_r) = splitJID to mu -- TODO muser = Nothing when wanted=False -- should probably mean unsubscribed for all users. -- This would allow us to answer anonymous probes with 'unsubscribed'. fromMaybe fail $ muser <&> \user -> do addrs <- resolvePeer from_h was_solicited <- removeFromRosterFile ConfigFiles.modifySolicited user profile from'' addrs subs <- resolvedFromRoster ConfigFiles.getSubscribers user profile let is_sub = not . null $ map (from_u,) addrs `intersect` subs putStrLn $ "DEBUG peerInformSubscription (is_sub,typ)=" ++ show (is_sub,stanzaType stanza) let (relationship,addf,remf) = case stanzaType stanza of PresenceInformSubscription True -> ( ("subscription", if is_sub then "both" else "to" ) , ConfigFiles.modifyBuddies , ConfigFiles.modifyOthers ) _ -> ( ("subscription", if is_sub then "from" else "none") , ConfigFiles.modifyOthers , ConfigFiles.modifyBuddies ) addToRosterFile addf user profile from'' addrs removeFromRosterFile remf user profile from'' addrs chans <- clientCons' state cktc user forM_ chans $ \(ckey,(Conn { connChan=chan }, client)) -> do hostname <- nameForClient state ckey let to' = unsplitJID (Just user, hostname, Nothing) update <- makeRosterUpdate to' from'' [relationship] is_intereseted <- atomically $ clientIsInterested client when is_intereseted $ do sendModifiedStanzaToClient update chan -- TODO: interested/availabe clients only? dup <- cloneStanza stanza sendModifiedStanzaToClient dup { stanzaFrom = Just $ from'' , stanzaTo = Just to' } chan