diff options
Diffstat (limited to 'dht/Presence/Presence.hs')
-rw-r--r-- | dht/Presence/Presence.hs | 1428 |
1 files changed, 1428 insertions, 0 deletions
diff --git a/dht/Presence/Presence.hs b/dht/Presence/Presence.hs new file mode 100644 index 00000000..8cdd1cdc --- /dev/null +++ b/dht/Presence/Presence.hs | |||
@@ -0,0 +1,1428 @@ | |||
1 | {-# LANGUAGE CPP #-} | ||
2 | {-# LANGUAGE ExistentialQuantification #-} | ||
3 | {-# LANGUAGE LambdaCase #-} | ||
4 | {-# LANGUAGE OverloadedStrings #-} | ||
5 | {-# LANGUAGE TupleSections #-} | ||
6 | module Presence where | ||
7 | |||
8 | import System.Directory | ||
9 | import System.IO.Error | ||
10 | #ifndef THREAD_DEBUG | ||
11 | import Control.Concurrent | ||
12 | #else | ||
13 | import Control.Concurrent.Lifted.Instrument | ||
14 | #endif | ||
15 | |||
16 | import Control.Concurrent.STM | ||
17 | import Control.Monad.Trans | ||
18 | import Network.Socket ( SockAddr(..) ) | ||
19 | import Data.Char | ||
20 | import Data.List (nub, (\\), intersect, groupBy, sort, sortBy ) | ||
21 | import Data.Ord (comparing ) | ||
22 | import Data.Monoid ((<>)) | ||
23 | import qualified Data.Text as Text | ||
24 | import qualified Data.Text.Encoding as Text | ||
25 | import Control.Monad | ||
26 | import Data.Text (Text) | ||
27 | import qualified Data.Map as Map | ||
28 | import Data.Map (Map) | ||
29 | import Control.Exception ({-evaluate,-}handle,SomeException(..)) | ||
30 | import System.Posix.User (getUserEntryForID,userName) | ||
31 | import qualified Data.ByteString.Lazy.Char8 as L | ||
32 | import qualified ConfigFiles | ||
33 | import Data.Maybe | ||
34 | import Data.Bits | ||
35 | import Data.Int (Int8) | ||
36 | import Data.XML.Types (Event) | ||
37 | import System.Posix.Types (UserID,CPid) | ||
38 | import Control.Applicative | ||
39 | import Crypto.PubKey.Curve25519 (SecretKey,toPublic) | ||
40 | |||
41 | import ControlMaybe | ||
42 | import DNSCache (parseAddress, strip_brackets, withPort) | ||
43 | import LockedChan (LockedChan) | ||
44 | import Text.Read (readMaybe) | ||
45 | import UTmp (ProcessID,users) | ||
46 | import LocalPeerCred | ||
47 | import XMPPServer | ||
48 | import ConsoleWriter | ||
49 | import ClientState | ||
50 | import Util | ||
51 | import qualified Connection | ||
52 | ;import Connection (PeerAddress (..), resolvePeer, reverseAddress) | ||
53 | import Network.Tox.NodeId (key2id,parseNoSpamId,nospam64,NoSpamId(..),ToxProgress,ToxContact(..)) | ||
54 | import Crypto.Tox (decodeSecret) | ||
55 | import DPut | ||
56 | import DebugTag | ||
57 | |||
58 | {- | ||
59 | isPeerKey :: ClientAddress -> Bool | ||
60 | isPeerKey k = case k of { PeerKey {} -> True ; _ -> False } | ||
61 | |||
62 | isClientKey :: ClientAddress -> Bool | ||
63 | isClientKey k = case k of { ClientKey {} -> True ; _ -> False } | ||
64 | -} | ||
65 | |||
66 | localJID :: Text -> Text -> Text -> IO Text | ||
67 | localJID user "." resource = do | ||
68 | hostname <- textHostName | ||
69 | return $ user <> "@" <> hostname <> "/" <> resource | ||
70 | localJID user profile resource = | ||
71 | return $ user <> "@" <> profile <> "/" <> resource | ||
72 | |||
73 | -- | These hooks will be invoked in order to connect to *.tox hosts in the | ||
74 | -- user's roster. | ||
75 | -- | ||
76 | -- The parameter k is a lookup key corresponding to an XMPP client. Each | ||
77 | -- unique value should be able to hold a reference to the ToxID identity which | ||
78 | -- should stay online until all interested keys have run 'deactivateAccount'. | ||
79 | data ToxManager k = ToxManager | ||
80 | -- | Put the given ToxID online. | ||
81 | { activateAccount :: k -> Text -> SecretKey -> IO () | ||
82 | -- | Take the given ToxID offline (assuming no other /k/ has a claim). | ||
83 | , deactivateAccount :: k -> Text -> IO () | ||
84 | , toxConnections :: Connection.Manager ToxProgress ToxContact | ||
85 | -- | Given a remote Tox key, return the address of a connected peer. | ||
86 | -- | ||
87 | -- The arguments are our public key (in base64 format) followed by | ||
88 | -- their public key (in base64 format). | ||
89 | , resolveToxPeer :: Text -> Text -> IO (Maybe PeerAddress) | ||
90 | } | ||
91 | |||
92 | type ClientProfile = Text | ||
93 | |||
94 | data PresenceState status = PresenceState | ||
95 | { clients :: TVar (Map ClientAddress ClientState) | ||
96 | , clientsByUser :: TVar (Map Text LocalPresence) | ||
97 | , clientsByProfile :: TVar (Map Text LocalPresence) | ||
98 | , remotesByPeer :: TVar (Map PeerAddress | ||
99 | (Map UserName RemotePresence)) | ||
100 | , server :: XMPPServer | ||
101 | , manager :: ClientProfile -> Connection.Manager status Text | ||
102 | , ckeyToChan :: TVar (Map ClientAddress Conn) | ||
103 | , pkeyToChan :: TVar (Map PeerAddress Conn) | ||
104 | , consoleWriter :: Maybe ConsoleWriter | ||
105 | , toxManager :: Maybe (ToxManager ClientAddress) | ||
106 | } | ||
107 | |||
108 | |||
109 | newPresenceState :: Maybe ConsoleWriter | ||
110 | -> Maybe (PresenceState status -> ToxManager ClientAddress) | ||
111 | -> XMPPServer | ||
112 | -> (ClientProfile -> Connection.Manager status Text) | ||
113 | -> IO (PresenceState status) | ||
114 | newPresenceState cw toxman sv man = atomically $ do | ||
115 | clients <- newTVar Map.empty | ||
116 | clientsByUser <- newTVar Map.empty | ||
117 | clientsByProfile <- newTVar Map.empty | ||
118 | remotesByPeer <- newTVar Map.empty | ||
119 | ckeyToChan <- newTVar Map.empty | ||
120 | pkeyToChan <- newTVar Map.empty | ||
121 | let st = PresenceState | ||
122 | { clients = clients | ||
123 | , clientsByUser = clientsByUser | ||
124 | , clientsByProfile = clientsByProfile | ||
125 | , remotesByPeer = remotesByPeer | ||
126 | , ckeyToChan = ckeyToChan | ||
127 | , pkeyToChan = pkeyToChan | ||
128 | , server = sv | ||
129 | , manager = man | ||
130 | , consoleWriter = cw | ||
131 | , toxManager = Nothing | ||
132 | } | ||
133 | return $ st { toxManager = fmap ($ st) toxman } | ||
134 | |||
135 | |||
136 | nameForClient :: PresenceState stat -> ClientAddress -> IO Text | ||
137 | nameForClient state k = do | ||
138 | mc <- atomically $ do | ||
139 | cmap <- readTVar (clients state) | ||
140 | return $ Map.lookup k cmap | ||
141 | case mc of | ||
142 | Nothing -> textHostName | ||
143 | Just client -> case clientProfile client of | ||
144 | "." -> textHostName | ||
145 | profile -> return profile | ||
146 | |||
147 | presenceHooks :: PresenceState stat -> Map Text MUC | ||
148 | -> Int | ||
149 | -> Maybe SockAddr -- ^ client-to-server bind address | ||
150 | -> Maybe SockAddr -- ^ server-to-server bind address | ||
151 | -> XMPPServerParameters | ||
152 | presenceHooks state chats verbosity mclient mpeer = XMPPServerParameters | ||
153 | { xmppChooseResourceName = chooseResourceName state | ||
154 | , xmppTellClientHisName = tellClientHisName state | ||
155 | , xmppTellMyNameToClient = nameForClient state | ||
156 | , xmppTellMyNameToPeer = \(Local addr) -> return $ addrToText addr | ||
157 | , xmppTellPeerHisName = return . peerKeyToText | ||
158 | , xmppNewConnection = newConn state | ||
159 | , xmppEOF = eofConn state | ||
160 | , xmppRosterBuddies = rosterGetBuddies state | ||
161 | , xmppRosterSubscribers = rosterGetSubscribers state | ||
162 | , xmppRosterSolicited = rosterGetSolicited state | ||
163 | , xmppRosterOthers = rosterGetOthers state | ||
164 | , xmppSubscribeToRoster = informSentRoster state | ||
165 | , xmppDeliverMessage = deliverMessage state | ||
166 | , xmppInformClientPresence = informClientPresence state | ||
167 | , xmppInformPeerPresence = informPeerPresence state | ||
168 | , xmppAnswerProbe = \k stanza chan -> answerProbe state (stanzaTo stanza) k chan | ||
169 | , xmppClientSubscriptionRequest = clientSubscriptionRequest state | ||
170 | , xmppPeerSubscriptionRequest = peerSubscriptionRequest state | ||
171 | , xmppClientInformSubscription = clientInformSubscription state | ||
172 | , xmppPeerInformSubscription = peerInformSubscription state | ||
173 | , xmppVerbosity = return verbosity | ||
174 | , xmppGroupChat = chats {- Map.singleton "chat" chat | ||
175 | { mucRoomList = return [("testroom",Just "testroom")] | ||
176 | , mucRoomOccupants = \case | ||
177 | "testroom" -> return [("fakeperson",Nothing)] | ||
178 | _ -> return [] | ||
179 | , mucReservedNick = \case | ||
180 | "testroom" -> return $ Just (return . Just) | ||
181 | _ -> return Nothing | ||
182 | , mucJoinRoom = \room nick caddr stanza -> do | ||
183 | who <- tellClientHisName state caddr | ||
184 | dput XJabber $ Text.unpack who ++ " joined " ++ Text.unpack room | ||
185 | ++ " with nick: " ++ Text.unpack nick | ||
186 | -- TODO: broadcast presence to all participants. | ||
187 | -- See 7.2.3 of XEP-0045 | ||
188 | -} | ||
189 | , xmppClientBind = mclient | ||
190 | , xmppPeerBind = mpeer | ||
191 | } | ||
192 | |||
193 | |||
194 | data LocalPresence = LocalPresence | ||
195 | { networkClients :: Map ClientAddress ClientState | ||
196 | -- TODO: loginClients | ||
197 | } | ||
198 | |||
199 | data RemotePresence = RemotePresence | ||
200 | { resources :: Map ResourceName Stanza | ||
201 | -- , localSubscribers :: Map Text () | ||
202 | -- ^ subset of clientsByUser who should be | ||
203 | -- notified about this presence. | ||
204 | } | ||
205 | |||
206 | |||
207 | |||
208 | pcSingletonNetworkClient :: ClientAddress -> ClientState -> LocalPresence | ||
209 | pcSingletonNetworkClient key client = | ||
210 | LocalPresence | ||
211 | { networkClients = Map.singleton key client | ||
212 | } | ||
213 | |||
214 | pcInsertNetworkClient :: ClientAddress -> ClientState -> LocalPresence -> LocalPresence | ||
215 | pcInsertNetworkClient key client pc = | ||
216 | pc { networkClients = Map.insert key client (networkClients pc) } | ||
217 | |||
218 | pcRemoveNewtworkClient :: ClientAddress | ||
219 | -> LocalPresence -> Maybe LocalPresence | ||
220 | pcRemoveNewtworkClient key pc = if pcIsEmpty pc' then Nothing | ||
221 | else Just pc' | ||
222 | where | ||
223 | pc' = pc { networkClients = Map.delete key (networkClients pc) } | ||
224 | |||
225 | pcIsEmpty :: LocalPresence -> Bool | ||
226 | pcIsEmpty pc = Map.null (networkClients pc) | ||
227 | |||
228 | |||
229 | |||
230 | getConsolePids :: PresenceState stat -> IO [(Text,ProcessID)] | ||
231 | getConsolePids state = do | ||
232 | us <- UTmp.users | ||
233 | return $ map (\(_,tty,pid)->(lazyByteStringToText tty,pid)) us | ||
234 | |||
235 | identifyTTY' :: [(Text, ProcessID)] | ||
236 | -> System.Posix.Types.UserID | ||
237 | -> L.ByteString | ||
238 | -> IO (Maybe Text, Maybe System.Posix.Types.CPid) | ||
239 | identifyTTY' ttypids uid inode = ttypid | ||
240 | where ttypids' = map (\(tty,pid)->(L.fromChunks [Text.encodeUtf8 tty], pid)) ttypids | ||
241 | ttypid = fmap textify $ identifyTTY ttypids' uid inode | ||
242 | textify (tty,pid) = (fmap lazyByteStringToText tty, pid) | ||
243 | |||
244 | chooseResourceName :: PresenceState stat | ||
245 | -> ClientAddress -> Remote SockAddr -> Maybe Text -> Maybe Text -> IO Text | ||
246 | chooseResourceName state k (Remote addr) clientsNameForMe desired = do | ||
247 | muid <- getLocalPeerCred' addr | ||
248 | (mtty,pid) <- getTTYandPID muid | ||
249 | user <- getJabberUserForId muid | ||
250 | status <- atomically $ newTVar Nothing | ||
251 | flgs <- atomically $ newTVar 0 | ||
252 | profile <- fmap (fromMaybe ".") | ||
253 | $ forM ((,) <$> clientsNameForMe <*> toxManager state) $ \(wanted_profile0,toxman) -> | ||
254 | case Text.splitAt 43 wanted_profile0 of | ||
255 | (pub,".tox") -> do | ||
256 | cdir <- ConfigFiles.configPath (L.fromChunks [Text.encodeUtf8 user]) "." "" | ||
257 | #if !MIN_VERSION_directory(1,2,5) | ||
258 | let listDirectory path = filter (`notElem` [".",".."]) <$> getDirectoryContents path | ||
259 | #endif | ||
260 | cfs <- map Text.pack <$> listDirectory cdir `catchIOError` (\e -> return []) | ||
261 | let profiles = filter (\f -> Text.toLower f == Text.toLower wanted_profile0) cfs | ||
262 | -- dput XMisc $ "Toxmpp profile " ++ show (user,wanted_profile0,profiles,cfs) | ||
263 | let wanted_profile = head $ profiles ++ [wanted_profile0] | ||
264 | secs <- configText ConfigFiles.getSecrets user wanted_profile | ||
265 | case secs of | ||
266 | sec:_ | Just s <- decodeSecret (Text.encodeUtf8 sec) | ||
267 | , map toLower (show $ key2id $ toPublic s) == map toLower (Text.unpack pub) | ||
268 | -> do activateAccount toxman k wanted_profile s | ||
269 | dput XMisc $ "loaded tox secret " ++ show sec | ||
270 | return wanted_profile | ||
271 | _ -> do | ||
272 | -- XXX: We should probably fail to connect when an | ||
273 | -- invalid Tox profile is used. For now, we'll | ||
274 | -- fall back to the Unix account login. | ||
275 | dput XMisc "failed to find tox secret" | ||
276 | return "." | ||
277 | ("*.tox","") -> do | ||
278 | dput XMisc $ "TODO: Match single tox key profile or generate first." | ||
279 | -- TODO: Match single tox key profile or generate first. | ||
280 | _todo | ||
281 | _ -> return "." | ||
282 | let client = ClientState { clientResource = maybe "fallback" id mtty | ||
283 | , clientUser = user | ||
284 | , clientProfile = profile | ||
285 | , clientPid = pid | ||
286 | , clientStatus = status | ||
287 | , clientFlags = flgs } | ||
288 | |||
289 | do -- forward-lookup of the buddies so that it is cached for reversing. | ||
290 | buds <- configText ConfigFiles.getBuddies (clientUser client) (clientProfile client) | ||
291 | forM_ buds $ \bud -> do | ||
292 | let (_,h,_) = splitJID bud | ||
293 | forkIO $ void $ resolvePeer (manager state $ clientProfile client) h | ||
294 | |||
295 | atomically $ do | ||
296 | modifyTVar' (clients state) $ Map.insert k client | ||
297 | let add mb = Just $ maybe (pcSingletonNetworkClient k client) | ||
298 | (pcInsertNetworkClient k client) | ||
299 | mb | ||
300 | modifyTVar' (clientsByUser state) $ Map.alter add (clientUser client) | ||
301 | modifyTVar' (clientsByProfile state) $ Map.alter add (clientProfile client) | ||
302 | |||
303 | localJID (clientUser client) (clientProfile client) (clientResource client) | ||
304 | |||
305 | where | ||
306 | getTTYandPID muid = do | ||
307 | -- us <- fmap (map (second fst) . Map.toList) . readTVarIO $ activeUsers state | ||
308 | ttypids <- getConsolePids state | ||
309 | -- let tailOf3 ((_,a),b) = (a,b) | ||
310 | (t,pid) <- case muid of | ||
311 | Just (uid,inode) -> identifyTTY' ttypids uid inode | ||
312 | Nothing -> return (Nothing,Nothing) | ||
313 | let rsc = t `mplus` fmap ( ("pid."<>) . Text.pack . show ) pid | ||
314 | return (rsc,pid) | ||
315 | |||
316 | getJabberUserForId muid = | ||
317 | maybe (return "nobody") | ||
318 | (\(uid,_) -> | ||
319 | handle (\(SomeException _) -> | ||
320 | return . (<> "uid.") . Text.pack . show $ uid) | ||
321 | $ do | ||
322 | user <- fmap userName $ getUserEntryForID uid | ||
323 | return (Text.pack user) | ||
324 | ) | ||
325 | muid | ||
326 | |||
327 | -- Perform action with 'ClientState' associated with the given 'ClientAddress'. | ||
328 | -- If there is no associated 'ClientState', then perform the supplied fallback | ||
329 | -- action. | ||
330 | forClient :: PresenceState stat -> ClientAddress -> IO b -> (ClientState -> IO b) -> IO b | ||
331 | forClient state k fallback f = do | ||
332 | mclient <- atomically $ do | ||
333 | cs <- readTVar (clients state) | ||
334 | return $ Map.lookup k cs | ||
335 | maybe fallback f mclient | ||
336 | |||
337 | tellClientHisName :: PresenceState stat -> ClientAddress -> IO Text | ||
338 | tellClientHisName state k = forClient state k fallback go | ||
339 | where | ||
340 | fallback = localJID "nobody" "." "fallback" | ||
341 | go client = localJID (clientUser client) (clientProfile client) (clientResource client) | ||
342 | |||
343 | toMapUnit :: Ord k => [k] -> Map k () | ||
344 | toMapUnit xs = Map.fromList $ map (,()) xs | ||
345 | |||
346 | resolveAllPeers :: Connection.Manager stat Text -> [Text] -> IO (Map PeerAddress ()) | ||
347 | resolveAllPeers man hosts = fmap (toMapUnit . concat) $ Prelude.mapM (fmap (take 1) . resolvePeer man) hosts | ||
348 | |||
349 | |||
350 | -- Read a roster file and start trying to connect to all relevent peers. | ||
351 | rosterGetStuff | ||
352 | :: (ConfigFiles.User -> ConfigFiles.Profile -> IO [L.ByteString]) | ||
353 | -> PresenceState stat -> ClientAddress -> IO [Text] | ||
354 | rosterGetStuff what state k = forClient state k (return []) | ||
355 | $ \client -> do | ||
356 | jids0 <- configText what (clientUser client) (clientProfile client) | ||
357 | let jids = map splitJID jids0 | ||
358 | -- Using case to bring 'status' type variable to Connection.Manager into scope. | ||
359 | case state of | ||
360 | PresenceState { server = sv } -> do | ||
361 | let conns = manager state $ clientProfile client | ||
362 | -- Grok peers to associate with from the roster: | ||
363 | let isTox = do (me , ".tox") <- Just $ Text.splitAt 43 (clientProfile client) | ||
364 | return me | ||
365 | noToxUsers (u,h,r) | ||
366 | | Text.isSuffixOf ".tox" h = unsplitJID (Nothing,h,r) | ||
367 | | otherwise = unsplitJID (u,h,r) | ||
368 | forM_ jids $ \(_,host,_) -> do | ||
369 | -- We need either conns :: Connection.Manager TCPStatus Text | ||
370 | -- or toxman :: ToxManager ClientAddress | ||
371 | -- It is decided by checking hostnames for .tox ending. | ||
372 | let policySetter = fromMaybe (Connection.setPolicy conns host) $ do | ||
373 | isTox | ||
374 | toxman <- toxManager state | ||
375 | (them, ".tox") <- Just $ Text.splitAt 43 host | ||
376 | meid <- readMaybe $ Text.unpack $ Text.take 43 (clientProfile client) | ||
377 | themid <- readMaybe $ Text.unpack them | ||
378 | return $ Connection.setPolicy (toxConnections toxman) | ||
379 | (ToxContact meid themid) | ||
380 | policySetter Connection.TryingToConnect | ||
381 | return $ fromMaybe jids0 $ do isTox | ||
382 | Just $ map noToxUsers jids | ||
383 | |||
384 | rosterGetBuddies :: PresenceState stat -> ClientAddress -> IO [Text] | ||
385 | rosterGetBuddies state k = rosterGetStuff ConfigFiles.getBuddies state k | ||
386 | |||
387 | rosterGetSolicited :: PresenceState stat -> ClientAddress -> IO [Text] | ||
388 | rosterGetSolicited = rosterGetStuff ConfigFiles.getSolicited | ||
389 | |||
390 | -- XXX: Should we be connecting to these peers? | ||
391 | rosterGetOthers :: PresenceState stat -> ClientAddress -> IO [Text] | ||
392 | rosterGetOthers = rosterGetStuff ConfigFiles.getOthers | ||
393 | |||
394 | rosterGetSubscribers :: PresenceState stat -> ClientAddress -> IO [Text] | ||
395 | rosterGetSubscribers = rosterGetStuff ConfigFiles.getSubscribers | ||
396 | |||
397 | data Conn = Conn { connChan :: TChan Stanza | ||
398 | , auxData :: ConnectionData } | ||
399 | |||
400 | -- Read config file as Text content rather than UTF8 bytestrings. | ||
401 | configText :: Functor f => | ||
402 | (ConfigFiles.User -> ConfigFiles.Profile -> f [L.ByteString]) | ||
403 | -> Text -- user | ||
404 | -> Text -- profile | ||
405 | -> f [Text] -- items | ||
406 | configText what u p = fmap (map lazyByteStringToText) | ||
407 | $ what (textToLazyByteString u) (Text.unpack p) | ||
408 | |||
409 | getBuddies' :: Text -> Text -> IO [Text] | ||
410 | getBuddies' = configText ConfigFiles.getBuddies | ||
411 | getSolicited' :: Text -> Text -> IO [Text] | ||
412 | getSolicited' = configText ConfigFiles.getSolicited | ||
413 | |||
414 | -- | Obtain from roster all buddies and pending buddies (called solicited | ||
415 | -- regardless of whether we've yet delivered a friend-request) matching the | ||
416 | -- supplied side-effecting predicate. | ||
417 | -- | ||
418 | -- Returned tuple: | ||
419 | -- | ||
420 | -- * Bool - True if buddy (should send probe). | ||
421 | -- False if solicited (should send friend-request). | ||
422 | -- | ||
423 | -- * Maybe Username - Username field of contact. | ||
424 | -- | ||
425 | -- * Text - Unix user who owns this roster entry. | ||
426 | -- | ||
427 | -- * Text - Hostname as it appears in roster. | ||
428 | -- | ||
429 | getBuddiesAndSolicited :: PresenceState stat | ||
430 | -> Text -- ^ Config profile: "." or tox host. | ||
431 | -> (Text -> IO Bool) -- ^ Return True if you want this hostname. | ||
432 | -> IO [(Bool, Maybe UserName, Text, Text)] | ||
433 | getBuddiesAndSolicited state profile pred | ||
434 | -- XXX: The following O(n²) nub may be a little | ||
435 | -- too onerous. | ||
436 | = fmap nub $ do | ||
437 | cbu <- atomically $ readTVar $ clientsByUser state | ||
438 | fmap concat $ sequence $ do | ||
439 | (user,LocalPresence cmap) <- Map.toList cbu | ||
440 | (isbud, getter) <- [(True ,getBuddies' ) | ||
441 | ,(False,getSolicited')] | ||
442 | return $ do | ||
443 | buds <- map splitJID <$> getter user profile | ||
444 | fmap concat $ forM buds $ \(u,h,r) -> do | ||
445 | interested <- pred h | ||
446 | if interested | ||
447 | then return [(isbud,u,user,h)] | ||
448 | else return [] | ||
449 | |||
450 | sendProbesAndSolicitations :: PresenceState stat -> PeerAddress -> Local SockAddr -> TChan Stanza -> IO () | ||
451 | sendProbesAndSolicitations state k (Local laddr) chan = do | ||
452 | prof <- atomically $ do | ||
453 | pktc <- readTVar (pkeyToChan state) | ||
454 | return $ maybe "." (cdProfile . auxData) $ Map.lookup k pktc | ||
455 | -- get all buddies & solicited matching k for all users | ||
456 | xs <- getBuddiesAndSolicited state prof $ \case | ||
457 | h | ".tox" `Text.isSuffixOf` h -> return False -- Tox probes/solicitations are handled in ToxToXMPP module. | ||
458 | h -> do | ||
459 | addrs <- nub <$> resolvePeer (manager state $ prof) h | ||
460 | return $ k `elem` addrs -- Roster item resolves to /k/ peer. | ||
461 | forM_ xs $ \(isbud,u,user,h) -> do | ||
462 | let make = if isbud then presenceProbe | ||
463 | else presenceSolicitation | ||
464 | toh = peerKeyToText k | ||
465 | jid = unsplitJID (u,toh,Nothing) | ||
466 | me = addrToText laddr -- xmppTellMyNameToPeer | ||
467 | from = if isbud then me -- probe from server | ||
468 | else -- solicitation from particular user | ||
469 | unsplitJID (Just user,me,Nothing) | ||
470 | stanza <- make from jid | ||
471 | -- send probes for buddies, solicitations for solicited. | ||
472 | dput XJabber $ "probing "++show k++" for: " ++ show (isbud,jid) | ||
473 | atomically $ writeTChan chan stanza | ||
474 | -- reverse xs `seq` return () | ||
475 | |||
476 | |||
477 | newConn :: PresenceState stat -> SockAddr -> ConnectionData -> TChan Stanza -> IO () | ||
478 | newConn state saddr cdta outchan = | ||
479 | case classifyConnection saddr cdta of | ||
480 | Left (pkey,laddr) -> do | ||
481 | atomically $ modifyTVar' (pkeyToChan state) | ||
482 | $ Map.insert pkey Conn { connChan = outchan | ||
483 | , auxData = cdta } | ||
484 | sendProbesAndSolicitations state pkey laddr outchan | ||
485 | Right (ckey,_) -> do | ||
486 | atomically $ modifyTVar' (ckeyToChan state) | ||
487 | $ Map.insert ckey Conn { connChan = outchan | ||
488 | , auxData = cdta } | ||
489 | |||
490 | delclient :: (Alternative m, Monad m) => | ||
491 | ClientAddress -> m LocalPresence -> m LocalPresence | ||
492 | delclient k mlp = do | ||
493 | lp <- mlp | ||
494 | let nc = Map.delete k $ networkClients lp | ||
495 | guard $ not (Map.null nc) | ||
496 | return $ lp { networkClients = nc } | ||
497 | |||
498 | eofConn :: PresenceState stat -> SockAddr -> ConnectionData -> IO () | ||
499 | eofConn state saddr cdta = do | ||
500 | case classifyConnection saddr cdta of | ||
501 | Left (k,_) -> do | ||
502 | h <- case cdType cdta of | ||
503 | -- TODO: This should be cached (perhaps by rewriteJIDForClient?) so that we | ||
504 | -- guarantee that the OFFLINE message matches the ONLINE message. | ||
505 | -- For now, we reverse-resolve the peer key. | ||
506 | XMPP -> -- For XMPP peers, informPeerPresence expects a textual | ||
507 | -- representation of the IP address to reverse-resolve. | ||
508 | return $ peerKeyToText k | ||
509 | Tox -> do | ||
510 | -- For Tox peers, informPeerPresence expects the actual hostname | ||
511 | -- so we will use the one that the peer told us at greeting time. | ||
512 | m <- atomically $ swapTVar (cdRemoteName cdta) Nothing | ||
513 | case m of | ||
514 | Nothing -> do | ||
515 | dput XJabber $ "BUG: Tox peer didn't inform us of its name." | ||
516 | -- The following fallback behavior is probably wrong. | ||
517 | return $ peerKeyToText k | ||
518 | Just toxname -> return toxname | ||
519 | -- ioToSource terminated. | ||
520 | -- | ||
521 | -- dhtd: Network.Socket.getAddrInfo | ||
522 | -- (called with preferred socket type/protocol: AddrInfo | ||
523 | -- { addrFlags = [AI_NUMERICHOST], addrFamily = AF_UNSPEC | ||
524 | -- , addrSocketType = NoSocketType, addrProtocol = 0 | ||
525 | -- , addrAddress = <assumed to be undefined> | ||
526 | -- , addrCanonName = <assumed to be undefined>} | ||
527 | -- , host name: Just "DdhbLjiwaV0GAiGKgesNPbvj2TbhrBHEWEEc5icfvQN.tox" | ||
528 | -- , service name: Just "0") | ||
529 | -- : does not exist (Name or service not known) | ||
530 | |||
531 | jids <- atomically $ do | ||
532 | rbp <- readTVar (remotesByPeer state) | ||
533 | return $ do | ||
534 | umap <- maybeToList $ Map.lookup k rbp | ||
535 | (u,rp) <- Map.toList umap | ||
536 | r <- Map.keys (resources rp) | ||
537 | let excludeEmpty "" = Nothing | ||
538 | excludeEmpty x = Just x | ||
539 | return $ unsplitJID (excludeEmpty u, h, excludeEmpty r) | ||
540 | -- EOF PEER PeerAddress [d768:82dd:3e86:a6ba:8fb3:6f9c:6327:75d8%4236342772]:0: | ||
541 | -- ["@[d768:82dd:3e86:a6ba:8fb3:6f9c:6327:75d8%4236342772]/"] | ||
542 | -- dput XJabber $ "EOF PEER "++show k++": "++show jids | ||
543 | forM_ jids $ \jid -> do | ||
544 | stanza <- makePresenceStanza "jabber:client" (Just jid) Offline | ||
545 | informPeerPresence state k stanza | ||
546 | Right (k,_) -> do | ||
547 | forClient state k (return ()) $ \client -> do | ||
548 | forM_ (toxManager state) $ \toxman -> do | ||
549 | case Text.splitAt 43 (clientProfile client) of | ||
550 | (pub,".tox") -> deactivateAccount toxman k (clientProfile client) | ||
551 | _ -> return () | ||
552 | stanza <- makePresenceStanza "jabber:server" Nothing Offline | ||
553 | informClientPresence state k stanza | ||
554 | atomically $ do | ||
555 | modifyTVar' (clientsByUser state) $ Map.alter (delclient k) (clientUser client) | ||
556 | modifyTVar' (clientsByProfile state) $ Map.alter (delclient k) (clientProfile client) | ||
557 | atomically $ case classifyConnection saddr cdta of | ||
558 | Left (pkey,_) -> modifyTVar' (pkeyToChan state) $ Map.delete pkey | ||
559 | Right (ckey,_) -> modifyTVar' (ckeyToChan state) $ Map.delete ckey | ||
560 | |||
561 | {- | ||
562 | parseRemoteAddress :: Text -> IO (Maybe (Remote SockAddr)) | ||
563 | parseRemoteAddress s = fmap Remote <$> parseAddress s | ||
564 | -} | ||
565 | |||
566 | -- This attempts to reverse resolve a peers address to give the human-friendly | ||
567 | -- domain name as it appears in the roster. It prefers host names that occur | ||
568 | -- in the given list of JIDs, but will fall back to any reverse-resolved name | ||
569 | -- and if it was unable to reverse the address, it will yield an ip address. | ||
570 | peerKeyToResolvedName :: Connection.Manager s Text -> [Text] -> PeerAddress -> IO Text | ||
571 | peerKeyToResolvedName man buds pk = do | ||
572 | ns <- reverseAddress man pk | ||
573 | let hs = map (\jid -> let (_,h,_)=splitJID jid in h) buds | ||
574 | ns' = sortBy (comparing $ not . flip elem hs) ns | ||
575 | return $ fromMaybe (peerKeyToText pk) (listToMaybe ns') | ||
576 | |||
577 | |||
578 | -- | The given address is taken to be the local address for the socket this JID | ||
579 | -- came in on. The returned JID parts are suitable for unsplitJID to create a | ||
580 | -- valid JID for communicating to a client. The returned Bool is True when the | ||
581 | -- host part refers to this local host (i.e. it equals the given SockAddr). | ||
582 | -- If there are multiple results, it will prefer one which is a member of the | ||
583 | -- given list in the last argument. | ||
584 | rewriteJIDForClient :: Connection.Manager s Text -> Local SockAddr -> Text -> [Text] -> IO (Bool,(Maybe Text,Text,Maybe Text)) | ||
585 | rewriteJIDForClient man (Local laddr) jid buds = do | ||
586 | let (n,h,r) = splitJID jid | ||
587 | -- dput XJabber $ "rewriteJIDForClient parsing " ++ show h | ||
588 | maddr <- parseAddress (strip_brackets h) | ||
589 | fromMaybe (return (False,(n,ip6literal h,r))) $ maddr <&> \saddr -> do | ||
590 | let mine = sameAddress laddr saddr | ||
591 | h' <- if mine then textHostName | ||
592 | else peerKeyToResolvedName man buds (addrToPeerKey $ Remote saddr) | ||
593 | return (mine,(n,h',r)) | ||
594 | |||
595 | -- Given a local address and an IP-address JID, we return True if the JID is | ||
596 | -- local, False otherwise. Additionally, a list of equivalent hostname JIDS | ||
597 | -- are returned. | ||
598 | multiplyJIDForClient :: Connection.Manager s Text -> ClientAddress -> Text -> IO (Bool,[(Maybe Text,Text,Maybe Text)]) | ||
599 | multiplyJIDForClient man k jid = do | ||
600 | let (n,h,r) = splitJID jid | ||
601 | -- dput XJabber $ "multiplyJIDForClient parsing " ++ show h | ||
602 | maddr <- parseAddress (strip_brackets h) | ||
603 | fromMaybe (return (False,[(n,ip6literal h,r)])) $ maddr <&> \saddr -> do | ||
604 | let Local laddr = addrFromClientKey k | ||
605 | mine = sameAddress laddr saddr | ||
606 | names <- if mine then fmap (:[]) textHostName | ||
607 | else reverseAddress man (addrToPeerKey $ Remote saddr) | ||
608 | return (mine,map (\h' -> (n,h',r)) names) | ||
609 | |||
610 | |||
611 | guardPortStrippedAddress :: Text -> Local SockAddr -> IO (Maybe ()) | ||
612 | guardPortStrippedAddress h (Local laddr) = do | ||
613 | -- dput XJabber $ "guardPortStrippedAddress parsing " ++ show h | ||
614 | maddr <- fmap (fmap (`withPort` 0)) $ parseAddress (strip_brackets h) | ||
615 | let laddr' = laddr `withPort` 0 | ||
616 | return $ maddr >>= guard . (==laddr') | ||
617 | |||
618 | |||
619 | -- | Accepts a textual representation of a domainname | ||
620 | -- JID suitable for client connections, and returns the | ||
621 | -- coresponding ipv6 address JID suitable for peers paired | ||
622 | -- with a PeerAddress with the address part of that JID in | ||
623 | -- binary form. If no suitable address could be resolved | ||
624 | -- for the given name, Nothing is returned. | ||
625 | rewriteJIDForPeer :: Connection.Manager s Text -> Text -> IO (Maybe (Text,PeerAddress)) | ||
626 | rewriteJIDForPeer man jid = do | ||
627 | let (n,h,r) = splitJID jid | ||
628 | maddr <- fmap listToMaybe $ resolvePeer man h | ||
629 | return $ flip fmap maddr $ \addr -> | ||
630 | let h' = peerKeyToText addr | ||
631 | to' = unsplitJID (n,h',r) | ||
632 | in (to',addr) | ||
633 | |||
634 | deliverToConsole :: PresenceState stat -> IO () -> Stanza -> IO () | ||
635 | deliverToConsole PresenceState{ consoleWriter = Just cw } fail msg = do | ||
636 | did1 <- writeActiveTTY cw msg | ||
637 | did2 <- writeAllPty cw msg | ||
638 | if not (did1 || did2) then fail else return () | ||
639 | deliverToConsole _ fail _ = fail | ||
640 | |||
641 | -- | deliver <message/> or error stanza | ||
642 | deliverMessage :: PresenceState stat | ||
643 | -> IO () | ||
644 | -> StanzaWrap (LockedChan Event) | ||
645 | -> IO () | ||
646 | deliverMessage state fail msg = | ||
647 | case stanzaOrigin msg of | ||
648 | ClientOrigin senderk _ -> do | ||
649 | -- Case 1. Client -> Peer | ||
650 | mto <- join $ atomically $ do | ||
651 | mclient <- Map.lookup senderk <$> readTVar (clients state) | ||
652 | return $ do | ||
653 | dput XJabber $ "deliverMessage: to="++show (stanzaTo msg,fmap clientProfile mclient) | ||
654 | fromMaybe -- Resolve XMPP peer. | ||
655 | (fmap join $ mapM (uncurry $ rewriteJIDForPeer . manager state) | ||
656 | $ (,) <$> (clientProfile <$> mclient) <*> stanzaTo msg) | ||
657 | $ do | ||
658 | client <- mclient | ||
659 | to <- stanzaTo msg | ||
660 | let (mu,th,rsc) = splitJID to | ||
661 | (toxman,me,_) <- weAreTox state client th | ||
662 | return $ do | ||
663 | dput XJabber $ "deliverMessage: weAreTox="++show me | ||
664 | -- In case the client sends us a lower-cased version of the base64 | ||
665 | -- tox key hostname, we resolve it by comparing it with roster entries. | ||
666 | xs <- getBuddiesAndSolicited state (clientProfile client) $ \case | ||
667 | rh | (_,".tox") <- Text.splitAt 43 rh | ||
668 | , Text.toLower rh == Text.toLower th | ||
669 | -> return True | ||
670 | _ -> return False | ||
671 | fmap join $ forM (listToMaybe xs) $ \(_,rmu,_,h) -> do | ||
672 | let (them,_) = Text.splitAt 43 h | ||
673 | maddr <- resolveToxPeer toxman me them | ||
674 | let to' = unsplitJID (mu,h,rsc) | ||
675 | return $ fmap (to',) maddr | ||
676 | fromMaybe (do dput XJabber $ "Unable to resolve "++show (stanzaTo msg) | ||
677 | fail {- reverse lookup failure -}) | ||
678 | $ mto <&> \(to',k) -> do | ||
679 | chans <- atomically $ readTVar (pkeyToChan state) | ||
680 | fromMaybe (do dput XJabber $ "Peer unavailable: "++ show k | ||
681 | fail) | ||
682 | $ (Map.lookup k chans) <&> \conn -> do | ||
683 | -- original 'from' address is discarded. | ||
684 | from' <- forClient state senderk (return Nothing) | ||
685 | $ return . Just . clientJID conn | ||
686 | -- dup <- atomically $ cloneStanza (msg { stanzaTo=Just to', stanzaFrom=Just from' }) | ||
687 | let dup = (msg { stanzaTo=Just to', stanzaFrom=from' }) | ||
688 | sendModifiedStanzaToPeer dup (connChan conn) | ||
689 | PeerOrigin senderk _ -> do | ||
690 | (pchans,cchans) <- atomically $ do | ||
691 | pc <- readTVar (pkeyToChan state) | ||
692 | cc <- readTVar (ckeyToChan state) | ||
693 | return (pc,cc) | ||
694 | fromMaybe (do dput XJabber $ "Unknown peer " ++ show senderk | ||
695 | fail) | ||
696 | $ Map.lookup senderk pchans | ||
697 | <&> \(Conn { connChan = sender_chan | ||
698 | , auxData = ConnectionData (Left laddr) ctyp cprof _ }) -> do | ||
699 | fromMaybe (do dput XJabber $ "Message missing \"to\" attribute." | ||
700 | fail) | ||
701 | $ (stanzaTo msg) <&> \to -> do | ||
702 | (mine,(n,h,r)) <- case (ctyp,cprof) of | ||
703 | (Tox,prof) -> let (n,h,r) = splitJID to | ||
704 | in return ( h==prof, (n,h,r) ) | ||
705 | _ -> rewriteJIDForClient (manager state cprof) laddr to [] | ||
706 | if not mine then do dput XJabber $ "Address mis-match " ++ show (laddr,to) | ||
707 | fail | ||
708 | else do | ||
709 | let to' = unsplitJID (n,h,r) | ||
710 | let (cmapVar,ckey) = case ctyp of | ||
711 | Tox -> (clientsByProfile state , Just cprof ) | ||
712 | XMPP -> (clientsByUser state , n ) | ||
713 | cmap <- atomically . readTVar $ cmapVar | ||
714 | chans <- fmap (fromMaybe []) $ do | ||
715 | forM (ckey >>= flip Map.lookup cmap) $ \presence_container -> do | ||
716 | let ks = Map.keys (networkClients presence_container) | ||
717 | chans = do | ||
718 | (k,client) <- Map.toList $ networkClients presence_container | ||
719 | chan <- maybeToList $ Map.lookup k cchans | ||
720 | return (clientProfile client, clientUser client, chan) | ||
721 | forM chans $ \(profile,user,chan) -> do | ||
722 | buds <- configText ConfigFiles.getBuddies user profile | ||
723 | from' <- case ctyp of | ||
724 | Tox -> return $ stanzaFrom msg | ||
725 | XMPP -> do | ||
726 | forM (stanzaFrom msg) $ \from -> do | ||
727 | (_,trip) <- rewriteJIDForClient (manager state cprof) laddr from buds | ||
728 | return $ unsplitJID trip | ||
729 | to' <- case ctyp of | ||
730 | XMPP -> return $ stanzaTo msg | ||
731 | Tox -> return $ Just $ unsplitJID (Just user, profile, Nothing) | ||
732 | return (from',chan) | ||
733 | dput XJabber $ "chan count: " ++ show (length chans) | ||
734 | if null chans then when (ctyp == XMPP) $ do | ||
735 | forM_ (stanzaFrom msg) $ \from -> do | ||
736 | from' <- do | ||
737 | -- Fallback to "." profile when no clients. | ||
738 | buds <- maybe (return []) | ||
739 | (\n -> configText ConfigFiles.getBuddies n ".") | ||
740 | n | ||
741 | (_,trip) <- rewriteJIDForClient (manager state cprof) laddr from buds | ||
742 | return . Just $ unsplitJID trip | ||
743 | let msg' = msg { stanzaTo=Just to' | ||
744 | , stanzaFrom=from' } | ||
745 | deliverToConsole state fail msg' | ||
746 | else do | ||
747 | forM_ chans $ \(from',Conn { connChan=chan}) -> do | ||
748 | -- TODO: Cloning isn't really necessary unless there are multiple | ||
749 | -- destinations and we should probably transition to minimal cloning, | ||
750 | -- or else we should distinguish between announcable stanzas and | ||
751 | -- consumable stanzas and announcables use write-only broadcast | ||
752 | -- channels that must be cloned in order to be consumed. | ||
753 | -- For now, we are doing redundant cloning. | ||
754 | let msg' = msg { stanzaTo=Just to' | ||
755 | , stanzaFrom=from' } | ||
756 | dup <- cloneStanza msg' | ||
757 | sendModifiedStanzaToClient dup | ||
758 | chan | ||
759 | |||
760 | |||
761 | setClientFlag :: PresenceState stat -> ClientAddress -> Int8 -> IO () | ||
762 | setClientFlag state k flag = | ||
763 | atomically $ do | ||
764 | cmap <- readTVar (clients state) | ||
765 | forM_ (Map.lookup k cmap) $ \client -> do | ||
766 | setClientFlag0 client flag | ||
767 | |||
768 | setClientFlag0 :: ClientState -> Int8 -> STM () | ||
769 | setClientFlag0 client flag = | ||
770 | modifyTVar' (clientFlags client) (\flgs -> flgs .|. flag) | ||
771 | |||
772 | informSentRoster :: PresenceState stat -> ClientAddress -> IO () | ||
773 | informSentRoster state k = do | ||
774 | setClientFlag state k cf_interested | ||
775 | |||
776 | |||
777 | subscribedPeers :: Connection.Manager s Text -> Text -> Text -> IO [PeerAddress] | ||
778 | subscribedPeers man user profile = do | ||
779 | jids <- configText ConfigFiles.getSubscribers user profile | ||
780 | let hosts = map ((\(_,h,_)->h) . splitJID) jids | ||
781 | fmap Map.keys $ resolveAllPeers man hosts | ||
782 | |||
783 | -- | this JID is suitable for peers, not clients. | ||
784 | clientJID :: Conn -> ClientState -> Text | ||
785 | clientJID con client = unsplitJID ( Just $ clientUser client | ||
786 | , either (\(Local a) -> addrToText a) -- my host name, for peers | ||
787 | (error $ unlines [ "clientJID wrongly used for client connection!" | ||
788 | , "TODO: my host name for clients? nameForClient? localJID?"]) | ||
789 | $ cdAddr $ auxData con | ||
790 | , Just $ clientResource client) | ||
791 | |||
792 | -- | Send presence notification to subscribed peers. | ||
793 | -- Note that a full JID from address will be added to the | ||
794 | -- stanza if it is not present. | ||
795 | informClientPresence :: PresenceState stat | ||
796 | -> ClientAddress -> StanzaWrap (LockedChan Event) -> IO () | ||
797 | informClientPresence state k stanza = do | ||
798 | forClient state k (return ()) $ \client -> do | ||
799 | informClientPresence0 state (Just k) client stanza | ||
800 | |||
801 | informClientPresence0 :: PresenceState stat | ||
802 | -> Maybe ClientAddress | ||
803 | -> ClientState | ||
804 | -> StanzaWrap (LockedChan Event) | ||
805 | -> IO () | ||
806 | informClientPresence0 state mbk client stanza = do | ||
807 | dup <- cloneStanza stanza | ||
808 | atomically $ writeTVar (clientStatus client) $ Just dup | ||
809 | is_avail <- atomically $ clientIsAvailable client | ||
810 | when (not is_avail) $ do | ||
811 | atomically $ setClientFlag0 client cf_available | ||
812 | maybe (return ()) (sendCachedPresence state) mbk | ||
813 | addrs <- subscribedPeers (manager state $ clientProfile client) (clientUser client) (clientProfile client) | ||
814 | dput XJabber $ "informClientPresence(subscribedPeers) "++show (clientProfile client,addrs) | ||
815 | ktc <- atomically $ readTVar (pkeyToChan state) | ||
816 | let connected = mapMaybe (flip Map.lookup ktc) addrs | ||
817 | forM_ connected $ \con -> do | ||
818 | let from' = clientJID con client | ||
819 | mto <- maybe (return Nothing) | ||
820 | (fmap (fmap fst) . rewriteJIDForPeer (manager state $ clientProfile client)) | ||
821 | (stanzaTo stanza) | ||
822 | dup <- cloneStanza stanza | ||
823 | sendModifiedStanzaToPeer dup { stanzaFrom = Just from' | ||
824 | , stanzaTo = mto } | ||
825 | (connChan con) | ||
826 | |||
827 | informPeerPresence :: PresenceState stat | ||
828 | -> PeerAddress | ||
829 | -> StanzaWrap (LockedChan Event) | ||
830 | -> IO () | ||
831 | informPeerPresence state k stanza = do | ||
832 | -- Presence must indicate full JID with resource... | ||
833 | dput XJabber $ "xmppInformPeerPresence checking from address..." | ||
834 | forM_ (stanzaFrom stanza) $ \from -> do | ||
835 | let (muser0,h,mresource0) = splitJID from | ||
836 | -- We'll allow the case that user and resource are simultaneously | ||
837 | -- absent. They will be stored in the remotesByPeer map using the | ||
838 | -- empty string. This is to accommodate the tox protocol which didn't | ||
839 | -- anticipate a single peer would have multiple users or front-ends. | ||
840 | (muser,mresource) = case (muser0,mresource0) of | ||
841 | (Nothing,Nothing) -> (Just "", Just "") | ||
842 | _ -> (muser0,mresource0) | ||
843 | dput XJabber $ "xmppInformPeerPresence from = " ++ show from | ||
844 | -- forM_ mresource $ \resource -> do | ||
845 | forM_ muser $ \user -> do | ||
846 | |||
847 | clients <- atomically $ do | ||
848 | |||
849 | -- Update remotesByPeer... | ||
850 | rbp <- readTVar (remotesByPeer state) | ||
851 | let umap = maybe Map.empty id $ Map.lookup k rbp | ||
852 | rp = case (presenceShow $ stanzaType stanza) of | ||
853 | Offline -> | ||
854 | maybe Map.empty | ||
855 | (\resource -> | ||
856 | maybe (Map.empty) | ||
857 | (Map.delete resource . resources) | ||
858 | $ Map.lookup user umap) | ||
859 | mresource | ||
860 | |||
861 | _ ->maybe Map.empty | ||
862 | (\resource -> | ||
863 | maybe (Map.singleton resource stanza) | ||
864 | (Map.insert resource stanza . resources ) | ||
865 | $ Map.lookup user umap) | ||
866 | mresource | ||
867 | umap' = Map.insert user (RemotePresence rp) umap | ||
868 | |||
869 | fromMaybe (return []) $ case presenceShow $ stanzaType stanza of | ||
870 | Offline -> Just () | ||
871 | _ -> mresource >> Just () | ||
872 | <&> \_ -> do | ||
873 | writeTVar (remotesByPeer state) $ Map.insert k umap' rbp | ||
874 | -- TODO: Store or delete the stanza (remotesByPeer) | ||
875 | |||
876 | -- all clients, we'll filter available/authorized later | ||
877 | |||
878 | ktc <- readTVar (ckeyToChan state) | ||
879 | cmap <- readTVar (clients state) | ||
880 | return $ do | ||
881 | (ck,client) <- Map.toList cmap | ||
882 | con <- maybeToList $ Map.lookup ck ktc | ||
883 | return (ck,con,client) | ||
884 | dput XJabber $ "xmppInformPeerPresence (length clients="++show (length clients)++")" | ||
885 | (ctyp,cprof) <- atomically $ do | ||
886 | mconn <- Map.lookup k <$> readTVar (pkeyToChan state) | ||
887 | return $ fromMaybe (XMPP,".") $ do | ||
888 | ConnectionData _ ctyp cprof _ <- auxData <$> mconn | ||
889 | return (ctyp,cprof) | ||
890 | forM_ clients $ \(ck,con,client) -> do | ||
891 | -- (TODO: appropriately authorized clients only.) | ||
892 | -- For now, all "available" clients (available = sent initial presence) | ||
893 | is_avail <- atomically $ clientIsAvailable client | ||
894 | when is_avail $ do | ||
895 | -- reversing for client: ("DdhbLjiwaV0GAiGKgesNPbvj2TbhrBHEWEEc5icfvQN.tox" | ||
896 | -- ,XMPP,"OrjBG.GyWuQhGc1pb0KssgmYAocohFh35Vx8mREC9Nu.tox",".") | ||
897 | dput XJabber $ "reversing for client: " ++ show (from,ctyp,clientProfile client,cprof) | ||
898 | froms <- case ctyp of | ||
899 | Tox | clientProfile client == cprof -> return [from] | ||
900 | _ -> do -- flip (maybe $ return [from]) k . const $ do | ||
901 | (_,trip) <- multiplyJIDForClient (manager state $ clientProfile client) ck from | ||
902 | return (map unsplitJID trip) | ||
903 | |||
904 | dput XJabber $ "sending to client: " ++ show (stanzaType stanza,froms) | ||
905 | forM_ froms $ \from' -> do | ||
906 | dup <- cloneStanza stanza | ||
907 | sendModifiedStanzaToClient (dup { stanzaFrom=Just from' }) | ||
908 | (connChan con) | ||
909 | |||
910 | consoleClients :: PresenceState stat -> STM (Map Text ClientState) | ||
911 | consoleClients PresenceState{ consoleWriter = Just cw } = readTVar (cwClients cw) | ||
912 | consoleClients _ = return Map.empty | ||
913 | |||
914 | |||
915 | answerProbe :: PresenceState stat -> Maybe Text -> PeerAddress -> TChan Stanza -> IO () | ||
916 | answerProbe state mto k chan = do | ||
917 | -- dput XJabber $ "answerProbe! " ++ show (stanzaType stanza) | ||
918 | ktc <- atomically $ readTVar (pkeyToChan state) | ||
919 | muser <- fmap join $ sequence $ do | ||
920 | to <- mto | ||
921 | conn <- Map.lookup k ktc | ||
922 | let (mu,h,_) = splitJID to -- TODO: currently resource-id is ignored on presence | ||
923 | -- probes. Is this correct? Check the spec. | ||
924 | Left laddr = cdAddr $ auxData conn | ||
925 | ch = addrToText a where Local a = laddr | ||
926 | u <- mu | ||
927 | Just $ do | ||
928 | guardPortStrippedAddress h laddr | ||
929 | <&> maybe Nothing (\_ -> Just (u,conn,ch)) | ||
930 | |||
931 | forM_ muser $ \(u,conn,ch) -> do | ||
932 | |||
933 | profiles <- releventProfiles (cdType $ auxData conn) u | ||
934 | forM_ profiles $ \profile -> do | ||
935 | |||
936 | -- only subscribed peers should get probe replies | ||
937 | let man = manager state $ cdProfile $ auxData conn | ||
938 | resolved_subs <- resolvedFromRoster man ConfigFiles.getSubscribers u profile | ||
939 | let gaddrs = groupBy sameHost (sort resolved_subs) | ||
940 | sameHost a b = (snd a == snd b) -- (==) `on` snd | ||
941 | whitelist = do | ||
942 | xs <- gaddrs -- group of subscribed jids on the same host | ||
943 | x <- take 1 xs -- the host from the group | ||
944 | guard $ snd x==k -- only hosts matching the key /k/ | ||
945 | mapMaybe fst xs -- all users subscribed at the remote peer /k/ | ||
946 | |||
947 | -- TODO: notify remote peer that they are unsubscribed? | ||
948 | -- reply <- makeInformSubscription "jabber:server" to from False | ||
949 | when (not $ null whitelist) $ do | ||
950 | |||
951 | replies <- catMaybes <$> do -- runTraversableT $ do | ||
952 | cbu <- atomically $ readTVar (clientsByUser state) -- Map Text LocalPresence | ||
953 | let lpres = maybeToList $ Map.lookup u cbu | ||
954 | cw <- atomically $ consoleClients state -- Map Text ClientState | ||
955 | forM ((lpres >>= Map.elems . networkClients) ++ Map.elems cw) $ \clientState -> do | ||
956 | -- liftIOMaybe :: IO (Maybe a) -> TraversableT [] IO a | ||
957 | mstanza <- atomically $ readTVar (clientStatus clientState) | ||
958 | forM mstanza $ \stanza0 -> do | ||
959 | stanza <- cloneStanza stanza0 | ||
960 | let jid = unsplitJID (Just $ clientUser clientState | ||
961 | , ch | ||
962 | ,Just $ clientResource clientState) | ||
963 | return stanza { stanzaFrom = Just jid | ||
964 | , stanzaType = (stanzaType stanza) | ||
965 | { presenceWhiteList = whitelist } | ||
966 | } | ||
967 | |||
968 | forM_ replies $ \reply -> do | ||
969 | sendModifiedStanzaToPeer reply chan | ||
970 | |||
971 | -- if no presence, send offline message | ||
972 | when (null replies) $ do | ||
973 | let jid = unsplitJID (Just u,ch,Nothing) | ||
974 | pstanza <- makePresenceStanza "jabber:server" (Just jid) Offline | ||
975 | atomically $ writeTChan (connChan conn) pstanza | ||
976 | |||
977 | -- Send friend requests and remote presences stored in remotesByPeer to XMPP | ||
978 | -- clients. | ||
979 | sendCachedPresence :: PresenceState stat -> ClientAddress -> IO () | ||
980 | sendCachedPresence state k = do | ||
981 | forClient state k (return ()) $ \client -> do | ||
982 | rbp <- atomically $ readTVar (remotesByPeer state) | ||
983 | jids <- configText ConfigFiles.getBuddies (clientUser client) (clientProfile client) | ||
984 | let hosts = map ((\(_,h,_)->h) . splitJID) jids | ||
985 | addrs <- resolveAllPeers (manager state $ clientProfile client) hosts | ||
986 | let onlines = rbp `Map.intersection` addrs | ||
987 | mcon <- atomically $ do ktc <- readTVar (ckeyToChan state) | ||
988 | return $ Map.lookup k ktc | ||
989 | forM_ mcon $ \con -> do | ||
990 | forM_ (Map.toList onlines) $ \(pk, umap) -> do | ||
991 | forM_ (Map.toList umap) $ \(user,rp) -> do | ||
992 | let h = peerKeyToText pk | ||
993 | forM_ (Map.toList $ resources rp) $ \(resource,stanza) -> do | ||
994 | let jid = unsplitJID (Just user,h,Just resource) | ||
995 | (mine,js) <- multiplyJIDForClient (manager state $ clientProfile client) k jid | ||
996 | forM_ js $ \jid -> do | ||
997 | let from' = unsplitJID jid | ||
998 | dup <- cloneStanza stanza | ||
999 | sendModifiedStanzaToClient (dup { stanzaFrom=Just from' }) | ||
1000 | (connChan con) | ||
1001 | |||
1002 | pending <- configText ConfigFiles.getPending (clientUser client) (clientProfile client) | ||
1003 | hostname <- nameForClient state k | ||
1004 | forM_ pending $ \pending_jid -> do | ||
1005 | let cjid = unsplitJID ( Just $ clientUser client | ||
1006 | , hostname | ||
1007 | , Nothing ) | ||
1008 | ask <- presenceSolicitation pending_jid cjid | ||
1009 | sendModifiedStanzaToClient ask (connChan con) | ||
1010 | |||
1011 | -- Note: relying on self peer connection to send | ||
1012 | -- send local buddies. | ||
1013 | return () | ||
1014 | |||
1015 | addToRosterFile :: | ||
1016 | Connection.Manager s Text | ||
1017 | -> (ConfigFiles.User | ||
1018 | -> ConfigFiles.Profile | ||
1019 | -> (L.ByteString -> IO (Maybe L.ByteString)) | ||
1020 | -> Maybe L.ByteString | ||
1021 | -> t1) | ||
1022 | -> Text -- user | ||
1023 | -> Text -- profile | ||
1024 | -> Text -> [PeerAddress] -> t1 | ||
1025 | addToRosterFile man doit whose profile to addrs = | ||
1026 | modifyRosterFile man doit whose profile to addrs True False | ||
1027 | |||
1028 | removeFromRosterFile :: | ||
1029 | Connection.Manager s Text | ||
1030 | -> (ConfigFiles.User | ||
1031 | -> ConfigFiles.Profile | ||
1032 | -> (L.ByteString -> IO (Maybe L.ByteString)) | ||
1033 | -> Maybe L.ByteString | ||
1034 | -> t1) | ||
1035 | -> Text -- user | ||
1036 | -> Text -- profile | ||
1037 | -> Text -> [PeerAddress] -> t1 | ||
1038 | removeFromRosterFile man doit whose profile to addrs = | ||
1039 | modifyRosterFile man doit whose profile to addrs False False | ||
1040 | |||
1041 | -- | Sanity-checked roster file manipulation. Primarily, this function handles | ||
1042 | -- hostname aliases. | ||
1043 | modifyRosterFile :: | ||
1044 | Connection.Manager s Text | ||
1045 | -> (ConfigFiles.User | ||
1046 | -> ConfigFiles.Profile | ||
1047 | -> (L.ByteString -> IO (Maybe L.ByteString)) | ||
1048 | -> Maybe L.ByteString | ||
1049 | -> t1) -- ^ Lower-level modification function | ||
1050 | -- indicating which file is being modified. | ||
1051 | -- Valid choices from ConfigFiles module: | ||
1052 | -- | ||
1053 | -- * modifySolicited | ||
1054 | -- | ||
1055 | -- * modifyBuddies | ||
1056 | -- | ||
1057 | -- * modifyPending | ||
1058 | -- | ||
1059 | -- * modifySubscribers | ||
1060 | -> Text -- ^ user | ||
1061 | -> Text -- ^ profile | ||
1062 | -> Text -- ^ JID that will be added or removed a hostname | ||
1063 | -> [PeerAddress] -- ^ Alias addresses for hostname in the JID. | ||
1064 | -> Bool -- ^ True if adding, otherwise False | ||
1065 | -> Bool -- ^ True to allow deleting all users at a host. | ||
1066 | -> t1 | ||
1067 | modifyRosterFile man doit whose profile to addrs bAdd bWildCard = do | ||
1068 | let (mu,_,_) = splitJID to | ||
1069 | -- For each jid in the file, this function will decide whether to keep | ||
1070 | -- it (possibly modified) which is indicated by Just _ or to remove the | ||
1071 | -- item from the file which is indicated by Nothing. | ||
1072 | cmp :: L.ByteString -> IO (Maybe L.ByteString) | ||
1073 | cmp jid = do | ||
1074 | let (msu,stored_h,mr) = splitJID (lazyByteStringToText jid) | ||
1075 | keep = return (Just jid) :: IO (Maybe L.ByteString) | ||
1076 | delete = return Nothing :: IO (Maybe L.ByteString) | ||
1077 | iocheck = do | ||
1078 | stored_addrs <- resolvePeer man stored_h -- TODO: don't resolve .tox peers. | ||
1079 | case stored_addrs of | ||
1080 | [] -> keep -- do not delete if failed to resolve | ||
1081 | xs | null (xs \\ addrs) -> delete -- hostname alias, delete | ||
1082 | _ -> keep | ||
1083 | fmap join $ sequence $ do | ||
1084 | guard $ isNothing mr -- delete if resource specified in file. | ||
1085 | if mu == msu || bWildCard | ||
1086 | then Just iocheck -- do not delete unless hostname alias | ||
1087 | else Just keep -- do not delete if user field doesn't match. | ||
1088 | doit (textToLazyByteString whose) (Text.unpack profile) | ||
1089 | cmp | ||
1090 | (guard bAdd >> Just (textToLazyByteString to)) | ||
1091 | |||
1092 | |||
1093 | clientSubscriptionRequest :: PresenceState stat -> IO () -> ClientAddress -> Stanza -> TChan Stanza -> IO () | ||
1094 | clientSubscriptionRequest state fail k stanza chan = do | ||
1095 | forClient state k fail $ \client -> do | ||
1096 | fromMaybe fail $ (splitJID <$> stanzaTo stanza) <&> \(mu,h,_) -> do | ||
1097 | dput XJabber $ "Forwarding solicitation to peer" | ||
1098 | let to0 = unsplitJID (mu,h,Nothing) -- deleted resource | ||
1099 | cuser = clientUser client | ||
1100 | cprof = clientProfile client | ||
1101 | man = manager state cprof | ||
1102 | mto = if ".tox" `Text.isSuffixOf` cprof | ||
1103 | then case parseNoSpamId to0 of | ||
1104 | Right toxjid@(NoSpamId nspam _) -> Just ( Text.pack $ '$' : nospam64 nspam | ||
1105 | , Text.pack $ show toxjid | ||
1106 | , return [] ) | ||
1107 | Left _ | Text.isSuffixOf ".tox" h -> Nothing | ||
1108 | Left _ | Text.all isHexDigit h | ||
1109 | && Text.length h == 76 -> Nothing | ||
1110 | Left _ -> fmap (\u -> (u, to0 ,resolvePeer man h)) mu | ||
1111 | else fmap (\u -> (u, to0 ,resolvePeer man h)) mu | ||
1112 | fromMaybe fail $ mto <&> \(u,to,resolv) -> do | ||
1113 | -- add to-address to from's solicited | ||
1114 | dput XJabber $ unlines [ "to0=" ++ Text.unpack to0 | ||
1115 | , "to=" ++ show (Text.unpack to) ] | ||
1116 | addrs <- resolv | ||
1117 | addToRosterFile man ConfigFiles.modifySolicited cuser cprof to addrs | ||
1118 | removeFromRosterFile man ConfigFiles.modifyBuddies cuser cprof to addrs | ||
1119 | resolved_subs <- resolvedFromRoster man ConfigFiles.getSubscribers cuser cprof | ||
1120 | let is_subscribed = not . null $ [ (mu, a) | a <- addrs ] | ||
1121 | `intersect` resolved_subs | ||
1122 | -- subscribers: "from" | ||
1123 | -- buddies: "to" | ||
1124 | |||
1125 | case state of | ||
1126 | PresenceState { server = svVar } -> do | ||
1127 | |||
1128 | (cktc,pktc,(sv,conns)) <- atomically $ do | ||
1129 | cktc <- readTVar $ ckeyToChan state | ||
1130 | pktc <- readTVar $ pkeyToChan state | ||
1131 | return (cktc,pktc,(server state,man)) | ||
1132 | |||
1133 | -- Update roster for each client. | ||
1134 | case stanzaType stanza of | ||
1135 | PresenceRequestSubscription True -> do | ||
1136 | hostname <- nameForClient state k | ||
1137 | let cjid = unsplitJID (Just $ clientUser client, hostname,Nothing) | ||
1138 | chans <- clientCons state cktc (clientUser client) | ||
1139 | forM_ chans $ \( Conn { connChan=chan }, client ) -> do | ||
1140 | -- roster update ask="subscribe" | ||
1141 | update <- myMakeRosterUpdate (clientProfile client) cjid to | ||
1142 | [ ("ask","subscribe") | ||
1143 | , if is_subscribed then ("subscription","from") | ||
1144 | else ("subscription","none") | ||
1145 | ] | ||
1146 | sendModifiedStanzaToClient update chan | ||
1147 | when (to /= to0) $ do | ||
1148 | removal <- myMakeRosterUpdate (clientProfile client) cjid to0 | ||
1149 | [ ("subscription","remove") ] | ||
1150 | sendModifiedStanzaToClient removal chan | ||
1151 | _ -> return () | ||
1152 | |||
1153 | -- Send friend request to peer. | ||
1154 | let dsts = pktc `Map.intersection` toMapUnit addrs | ||
1155 | forM_ (Map.toList dsts) $ \(pk,con) -> do | ||
1156 | -- if already connected, send solicitation ... | ||
1157 | -- let from = clientJID con client | ||
1158 | let Left laddr = cdAddr $ auxData con | ||
1159 | from = unsplitJID ( Just $ clientUser client | ||
1160 | , (\(Local a) -> addrToText a) $ laddr | ||
1161 | , Nothing ) | ||
1162 | mb <- rewriteJIDForPeer (manager state $ cdProfile $ auxData con) to | ||
1163 | forM_ mb $ \(to',addr) -> do | ||
1164 | dup <- cloneStanza stanza | ||
1165 | sendModifiedStanzaToPeer (dup { stanzaTo = Just to' | ||
1166 | , stanzaFrom = Just from }) | ||
1167 | (connChan con) | ||
1168 | let policySetter = fromMaybe (Connection.setPolicy conns h) $ do | ||
1169 | (toxman,_,_) <- weAreTox state client h | ||
1170 | meid <- readMaybe $ Text.unpack $ Text.take 43 (clientProfile client) | ||
1171 | themid <- readMaybe $ Text.unpack h | ||
1172 | Just $ Connection.setPolicy (toxConnections toxman) (ToxContact meid themid) | ||
1173 | -- Add peer if we are not already associated ... | ||
1174 | policySetter Connection.TryingToConnect | ||
1175 | |||
1176 | weAreTox :: PresenceState stat -> ClientState -> Text -> Maybe (ToxManager ClientAddress,Text{- me -},Text{- them -}) | ||
1177 | weAreTox state client h = do | ||
1178 | toxman <- toxManager state | ||
1179 | (me , ".tox") <- Just $ Text.splitAt 43 (clientProfile client) | ||
1180 | (them, ".tox") <- Just $ Text.splitAt 43 h | ||
1181 | return (toxman,me,them) | ||
1182 | |||
1183 | resolvedFromRoster | ||
1184 | :: Connection.Manager s Text | ||
1185 | -> (ConfigFiles.User -> ConfigFiles.Profile -> IO [L.ByteString]) | ||
1186 | -> UserName -> Text -> IO [(Maybe UserName, PeerAddress)] | ||
1187 | resolvedFromRoster man doit u profile = concat <$> do | ||
1188 | subs <- configText doit u profile | ||
1189 | forM (splitJID `fmap` subs) $ \(mu,h,_) -> do | ||
1190 | addrs <- fmap nub $ resolvePeer man h | ||
1191 | return $ map (mu,) addrs | ||
1192 | |||
1193 | clientCons :: PresenceState stat | ||
1194 | -> Map ClientAddress t -> Text -> IO [(t, ClientState)] | ||
1195 | clientCons state ktc u = map snd <$> clientCons' state ktc u | ||
1196 | |||
1197 | clientCons' :: PresenceState stat | ||
1198 | -> Map ClientAddress t -> Text -> IO [(ClientAddress,(t, ClientState))] | ||
1199 | clientCons' state ktc u = do | ||
1200 | mlp <- atomically $ do | ||
1201 | cmap <- readTVar $ clientsByUser state | ||
1202 | return $ Map.lookup u cmap | ||
1203 | let ks = do lp <- maybeToList mlp | ||
1204 | Map.toList (networkClients lp) | ||
1205 | doit (k,client) = do | ||
1206 | con <- Map.lookup k ktc | ||
1207 | return (k,(con,client)) | ||
1208 | return $ mapMaybe doit ks | ||
1209 | |||
1210 | releventProfiles :: ConnectionType -> Text -> IO [Text] | ||
1211 | releventProfiles XMPP _ = return ["."] | ||
1212 | releventProfiles ctyp user = do | ||
1213 | -- TODO: Return all the ".tox" profiles that a user has under his | ||
1214 | -- .presence/ directory. | ||
1215 | return [] | ||
1216 | |||
1217 | peerSubscriptionRequest :: PresenceState stat -> IO () -> PeerAddress -> Stanza -> TChan Stanza -> IO () | ||
1218 | peerSubscriptionRequest state fail k stanza chan = do | ||
1219 | dput XJabber $ "Handling pending subscription from remote" | ||
1220 | fromMaybe fail $ (stanzaFrom stanza) <&> \from -> do | ||
1221 | fromMaybe fail $ (stanzaTo stanza) <&> \to -> do | ||
1222 | let (mto_u,h,_) = splitJID to | ||
1223 | (mfrom_u,from_h,_) = splitJID from | ||
1224 | to <- return $ unsplitJID (mto_u,h,Nothing) -- delete resource | ||
1225 | from <- return $ unsplitJID (mfrom_u,from_h,Nothing) -- delete resource | ||
1226 | (pktc,cktc,cmap) <- atomically $ do | ||
1227 | cktc <- readTVar (ckeyToChan state) | ||
1228 | pktc <- readTVar (pkeyToChan state) | ||
1229 | cmap <- readTVar (clients state) | ||
1230 | return (pktc,cktc,cmap) | ||
1231 | fromMaybe fail $ (Map.lookup k pktc) | ||
1232 | <&> \Conn { auxData=ConnectionData (Left laddr) ctyp profile _ } -> do | ||
1233 | (mine,totup) <- case (ctyp,profile) of | ||
1234 | (Tox,p) -> let (u,h,r) = splitJID to | ||
1235 | in return ( h == p, (u,h,r) ) | ||
1236 | _ -> rewriteJIDForClient (manager state profile) laddr to [] | ||
1237 | if not mine then fail else do | ||
1238 | (_,fromtup) <- rewriteJIDForClient (manager state profile) laddr from [] | ||
1239 | fromMaybe fail $ mto_u <&> \u -> do | ||
1240 | fromMaybe fail $ mfrom_u <&> \from_u -> do | ||
1241 | resolved_subs <- resolvedFromRoster (manager state profile) ConfigFiles.getSubscribers u profile | ||
1242 | let already_subscribed = elem (mfrom_u,k) resolved_subs | ||
1243 | is_wanted = case stanzaType stanza of | ||
1244 | PresenceRequestSubscription b -> b | ||
1245 | _ -> False -- Shouldn't happen. | ||
1246 | -- Section 8 says (for presence of type "subscribe", the server MUST | ||
1247 | -- adhere to the rules defined under Section 3 and summarized under | ||
1248 | -- see Appendix A. (pariticularly Appendex A.3.1) | ||
1249 | if already_subscribed == is_wanted | ||
1250 | then do | ||
1251 | -- contact ∈ subscribers --> SHOULD NOT, already handled | ||
1252 | -- already subscribed, reply and quit | ||
1253 | -- (note: swapping to and from for reply) | ||
1254 | reply <- makeInformSubscription "jabber:server" to from is_wanted | ||
1255 | sendModifiedStanzaToPeer reply chan | ||
1256 | answerProbe state (Just to) k chan | ||
1257 | else do | ||
1258 | |||
1259 | -- TODO: if peer-connection is to self, then auto-approve local user. | ||
1260 | |||
1261 | -- add from-address to to's pending | ||
1262 | addrs <- resolvePeer (manager state profile) from_h | ||
1263 | |||
1264 | -- Catch exception in case the user does not exist | ||
1265 | if null addrs then fail else do | ||
1266 | |||
1267 | let from' = unsplitJID fromtup | ||
1268 | |||
1269 | -- Update roster files (subscribe: add to pending, unsubscribe: remove from subscribers). | ||
1270 | already_pending <- | ||
1271 | if is_wanted then | ||
1272 | addToRosterFile (manager state profile) ConfigFiles.modifyPending u profile from' addrs | ||
1273 | else do | ||
1274 | removeFromRosterFile (manager state profile) ConfigFiles.modifySubscribers u profile from' addrs | ||
1275 | reply <- makeInformSubscription "jabber:server" to from is_wanted | ||
1276 | sendModifiedStanzaToPeer reply chan | ||
1277 | return False | ||
1278 | |||
1279 | -- contact ∉ subscribers & contact ∈ pending --> SHOULD NOT | ||
1280 | when (not already_pending) $ do | ||
1281 | -- contact ∉ subscribers & contact ∉ pending --> MUST | ||
1282 | |||
1283 | chans <- clientCons state cktc u | ||
1284 | forM_ chans $ \( Conn { connChan=chan }, client ) -> do | ||
1285 | -- send to clients | ||
1286 | -- TODO: interested/available clients only? | ||
1287 | dup <- cloneStanza stanza | ||
1288 | sendModifiedStanzaToClient dup { stanzaFrom = Just $ from' | ||
1289 | , stanzaTo = Just $ unsplitJID totup } | ||
1290 | chan | ||
1291 | |||
1292 | myMakeRosterUpdate prf tojid contact as | ||
1293 | | ".tox" `Text.isSuffixOf` prf | ||
1294 | , (Just u,h,r) <- splitJID contact | ||
1295 | , ".tox" `Text.isSuffixOf` u = XMPPServer.makeRosterUpdate tojid (unsplitJID (Nothing,h,r)) as | ||
1296 | myMakeRosterUpdate _ tojid contact as = XMPPServer.makeRosterUpdate tojid contact as | ||
1297 | |||
1298 | |||
1299 | clientInformSubscription :: PresenceState stat | ||
1300 | -> IO () | ||
1301 | -> ClientAddress | ||
1302 | -> StanzaWrap (LockedChan Event) | ||
1303 | -> IO () | ||
1304 | clientInformSubscription state fail k stanza = do | ||
1305 | forClient state k fail $ \client -> do | ||
1306 | fromMaybe fail $ (stanzaTo stanza) <&> \to -> do | ||
1307 | dput XJabber $ "clientInformSubscription" | ||
1308 | let (mu,h,mr) = splitJID to | ||
1309 | man = manager state $ clientProfile client | ||
1310 | addrs <- resolvePeer man h | ||
1311 | -- remove from pending | ||
1312 | buds <- resolvedFromRoster man ConfigFiles.getBuddies (clientUser client) (clientProfile client) | ||
1313 | let is_buddy = not . null $ map (mu,) addrs `intersect` buds | ||
1314 | removeFromRosterFile man ConfigFiles.modifyPending (clientUser client) (clientProfile client) to addrs | ||
1315 | let (relationship,addf,remf) = | ||
1316 | case stanzaType stanza of | ||
1317 | PresenceInformSubscription True -> | ||
1318 | ( ("subscription", if is_buddy then "both" | ||
1319 | else "from" ) | ||
1320 | , ConfigFiles.modifySubscribers | ||
1321 | , ConfigFiles.modifyOthers ) | ||
1322 | _ -> ( ("subscription", if is_buddy then "to" | ||
1323 | else "none" ) | ||
1324 | , ConfigFiles.modifyOthers | ||
1325 | , ConfigFiles.modifySubscribers ) | ||
1326 | addToRosterFile man addf (clientUser client) (clientProfile client) to addrs | ||
1327 | removeFromRosterFile man remf (clientUser client) (clientProfile client) to addrs | ||
1328 | |||
1329 | do | ||
1330 | cbu <- atomically $ readTVar (clientsByUser state) | ||
1331 | dput XJabber $ "cbu = " ++ show (fmap (fmap clientPid . networkClients) cbu) | ||
1332 | |||
1333 | -- send roster update to clients | ||
1334 | (clients,ktc,pktc) <- atomically $ do | ||
1335 | cbu <- readTVar (clientsByUser state) | ||
1336 | let mlp = Map.lookup (clientUser client) cbu | ||
1337 | let cs = maybe [] (Map.toList . networkClients) mlp | ||
1338 | ktc <- readTVar (ckeyToChan state) | ||
1339 | pktc <- readTVar (pkeyToChan state) | ||
1340 | return (cs,ktc,pktc) | ||
1341 | forM_ clients $ \(ck, client) -> do | ||
1342 | is_intereseted <- atomically $ clientIsInterested client | ||
1343 | dput XJabber $ "clientIsInterested: "++show is_intereseted | ||
1344 | is_intereseted <- atomically $ clientIsInterested client | ||
1345 | when is_intereseted $ do | ||
1346 | forM_ (Map.lookup ck ktc) $ \con -> do | ||
1347 | hostname <- nameForClient state ck | ||
1348 | -- TODO: Should cjid include the resource? | ||
1349 | let cjid = unsplitJID (mu, hostname, Nothing) | ||
1350 | update <- myMakeRosterUpdate (clientProfile client) cjid to [relationship] | ||
1351 | sendModifiedStanzaToClient update (connChan con) | ||
1352 | |||
1353 | -- notify peer | ||
1354 | let dsts = toMapUnit addrs | ||
1355 | cdsts = pktc `Map.intersection` dsts | ||
1356 | forM_ (Map.toList cdsts) $ \(pk,con) -> do | ||
1357 | let from = clientJID con client | ||
1358 | to' = unsplitJID (mu, peerKeyToText pk, Nothing) | ||
1359 | dup <- cloneStanza stanza | ||
1360 | sendModifiedStanzaToPeer (dup { stanzaTo = Just $ to' | ||
1361 | , stanzaFrom = Just from }) | ||
1362 | (connChan con) | ||
1363 | answerProbe state (Just from) pk (connChan con) | ||
1364 | |||
1365 | peerInformSubscription :: PresenceState stat | ||
1366 | -> IO () | ||
1367 | -> PeerAddress | ||
1368 | -> StanzaWrap (LockedChan Event) | ||
1369 | -> IO () | ||
1370 | peerInformSubscription state fail k stanza = do | ||
1371 | dput XJabber $ "TODO: peerInformSubscription" | ||
1372 | -- remove from solicited | ||
1373 | fromMaybe fail $ (stanzaFrom stanza) <&> \from -> do | ||
1374 | (ktc,cktc,cmap) <- atomically $ do | ||
1375 | pktc <- readTVar (pkeyToChan state) | ||
1376 | cktc <- readTVar (ckeyToChan state) | ||
1377 | cmap <- readTVar (clients state) | ||
1378 | return (pktc,cktc,cmap) | ||
1379 | fromMaybe fail $ Map.lookup k ktc | ||
1380 | <&> \(Conn { connChan=sender_chan | ||
1381 | , auxData =ConnectionData (Left laddr) ctyp profile _ }) -> do | ||
1382 | let man = manager state profile | ||
1383 | (from_u,from_h,_) <- case ctyp of | ||
1384 | Tox -> return $ splitJID from | ||
1385 | XMPP -> snd <$> rewriteJIDForClient man laddr from [] | ||
1386 | let from'' = unsplitJID (from_u,from_h,Nothing) | ||
1387 | muser = do | ||
1388 | to <- stanzaTo stanza | ||
1389 | let (mu,to_h,to_r) = splitJID to | ||
1390 | mu | ||
1391 | -- TODO muser = Nothing when wanted=False | ||
1392 | -- should probably mean unsubscribed for all users. | ||
1393 | -- This would allow us to answer anonymous probes with 'unsubscribed'. | ||
1394 | fromMaybe fail $ muser <&> \user -> do | ||
1395 | |||
1396 | addrs <- resolvePeer man from_h | ||
1397 | was_solicited <- removeFromRosterFile man ConfigFiles.modifySolicited user profile from'' addrs | ||
1398 | |||
1399 | subs <- resolvedFromRoster man ConfigFiles.getSubscribers user profile | ||
1400 | let is_sub = not . null $ map (from_u,) addrs `intersect` subs | ||
1401 | dput XJabber $ "DEBUG peerInformSubscription (is_sub,typ)=" ++ show (is_sub,stanzaType stanza) | ||
1402 | let (relationship,addf,remf) = | ||
1403 | case stanzaType stanza of | ||
1404 | PresenceInformSubscription True -> | ||
1405 | ( ("subscription", if is_sub then "both" | ||
1406 | else "to" ) | ||
1407 | , ConfigFiles.modifyBuddies | ||
1408 | , ConfigFiles.modifyOthers ) | ||
1409 | _ -> ( ("subscription", if is_sub then "from" | ||
1410 | else "none") | ||
1411 | , ConfigFiles.modifyOthers | ||
1412 | , ConfigFiles.modifyBuddies ) | ||
1413 | addToRosterFile man addf user profile from'' addrs | ||
1414 | removeFromRosterFile man remf user profile from'' addrs | ||
1415 | |||
1416 | chans <- clientCons' state cktc user | ||
1417 | forM_ chans $ \(ckey,(Conn { connChan=chan }, client)) -> do | ||
1418 | hostname <- nameForClient state ckey | ||
1419 | let to' = unsplitJID (Just user, hostname, Nothing) | ||
1420 | update <- myMakeRosterUpdate (clientProfile client) to' from'' [relationship] | ||
1421 | is_intereseted <- atomically $ clientIsInterested client | ||
1422 | when is_intereseted $ do | ||
1423 | sendModifiedStanzaToClient update chan | ||
1424 | -- TODO: interested/availabe clients only? | ||
1425 | dup <- cloneStanza stanza | ||
1426 | sendModifiedStanzaToClient dup { stanzaFrom = Just $ from'' | ||
1427 | , stanzaTo = Just to' } | ||
1428 | chan | ||