diff options
-rw-r--r-- | Presence/Presence.hs | 48 | ||||
-rw-r--r-- | Presence/XMPPServer.hs | 1 | ||||
-rw-r--r-- | ToxManager.hs | 14 | ||||
-rw-r--r-- | ToxToXMPP.hs | 70 | ||||
-rw-r--r-- | XMPPToTox.hs | 67 | ||||
-rw-r--r-- | examples/dhtd.hs | 14 | ||||
-rw-r--r-- | examples/test-xmpp.hs | 41 |
7 files changed, 213 insertions, 42 deletions
diff --git a/Presence/Presence.hs b/Presence/Presence.hs index e8a69066..53de4e93 100644 --- a/Presence/Presence.hs +++ b/Presence/Presence.hs | |||
@@ -85,6 +85,11 @@ data ToxManager k = ToxManager | |||
85 | -- their public key (in hostname format) and the Policy to set for this | 85 | -- their public key (in hostname format) and the Policy to set for this |
86 | -- link. | 86 | -- link. |
87 | , setToxConnectionPolicy :: Text -> Text -> Connection.Policy -> IO () | 87 | , setToxConnectionPolicy :: Text -> Text -> Connection.Policy -> IO () |
88 | -- | Given a remote Tox key, return the address of a connected peer. | ||
89 | -- | ||
90 | -- The arguments are our public key (in base64 format) followed by | ||
91 | -- their public key (in base64 format). | ||
92 | , resolveToxPeer :: Text -> Text -> IO (Maybe PeerAddress) | ||
88 | } | 93 | } |
89 | 94 | ||
90 | data PresenceState = forall status. PresenceState | 95 | data PresenceState = forall status. PresenceState |
@@ -577,10 +582,25 @@ deliverMessage state fail msg = | |||
577 | case stanzaOrigin msg of | 582 | case stanzaOrigin msg of |
578 | ClientOrigin senderk _ -> do | 583 | ClientOrigin senderk _ -> do |
579 | -- Case 1. Client -> Peer | 584 | -- Case 1. Client -> Peer |
580 | mto <- fmap join $ mapM rewriteJIDForPeer (stanzaTo msg) | 585 | mto <- join $ atomically $ do |
581 | fromMaybe fail {- reverse lookup failure -} $ mto <&> \(to',k) -> do | 586 | mclient <- Map.lookup senderk <$> readTVar (clients state) |
587 | return | ||
588 | $ fromMaybe -- Resolve XMPP peer. | ||
589 | (fmap join $ mapM rewriteJIDForPeer (stanzaTo msg)) | ||
590 | $ do (mu,h,rsc) <- splitJID <$> stanzaTo msg | ||
591 | u <- mu | ||
592 | client <- mclient | ||
593 | (toxman,me,them) <- weAreTox state client h | ||
594 | return -- Resolve Tox peer. | ||
595 | $ do maddr <- resolveToxPeer toxman me them | ||
596 | return $ fmap (u,) maddr | ||
597 | fromMaybe (do dput XJabber $ "Unable to resolve "++show (stanzaTo msg) | ||
598 | fail {- reverse lookup failure -}) | ||
599 | $ mto <&> \(to',k) -> do | ||
582 | chans <- atomically $ readTVar (pkeyToChan state) | 600 | chans <- atomically $ readTVar (pkeyToChan state) |
583 | fromMaybe fail $ (Map.lookup k chans) <&> \conn -> do | 601 | fromMaybe (do dput XJabber $ "Peer unavailable: "++ show k |
602 | fail) | ||
603 | $ (Map.lookup k chans) <&> \conn -> do | ||
584 | -- original 'from' address is discarded. | 604 | -- original 'from' address is discarded. |
585 | from' <- forClient state senderk (return Nothing) | 605 | from' <- forClient state senderk (return Nothing) |
586 | $ return . Just . clientJID conn | 606 | $ return . Just . clientJID conn |
@@ -592,12 +612,18 @@ deliverMessage state fail msg = | |||
592 | pc <- readTVar (pkeyToChan state) | 612 | pc <- readTVar (pkeyToChan state) |
593 | cc <- readTVar (ckeyToChan state) | 613 | cc <- readTVar (ckeyToChan state) |
594 | return (pc,cc) | 614 | return (pc,cc) |
595 | fromMaybe fail $ (Map.lookup senderk pchans) | 615 | fromMaybe (do dput XJabber $ "Unknown peer " ++ show senderk |
616 | fail) | ||
617 | $ Map.lookup senderk pchans | ||
596 | <&> \(Conn { connChan = sender_chan | 618 | <&> \(Conn { connChan = sender_chan |
597 | , auxData = ConnectionData (Left laddr) ctyp cprof }) -> do | 619 | , auxData = ConnectionData (Left laddr) ctyp cprof }) -> do |
598 | fromMaybe fail $ (stanzaTo msg) <&> \to -> do | 620 | fromMaybe (do dput XJabber $ "Message missing \"to\" attribute." |
621 | fail) | ||
622 | $ (stanzaTo msg) <&> \to -> do | ||
599 | (mine,(n,h,r)) <- rewriteJIDForClient laddr to [] | 623 | (mine,(n,h,r)) <- rewriteJIDForClient laddr to [] |
600 | if not mine then fail else do | 624 | if not mine then do dput XJabber $ "Address mis-match " ++ show (laddr,to) |
625 | fail | ||
626 | else do | ||
601 | let to' = unsplitJID (n,h,r) | 627 | let to' = unsplitJID (n,h,r) |
602 | let (cmapVar,ckey) = case ctyp of | 628 | let (cmapVar,ckey) = case ctyp of |
603 | Tox -> (clientsByProfile state , Just cprof ) | 629 | Tox -> (clientsByProfile state , Just cprof ) |
@@ -1024,14 +1050,18 @@ clientSubscriptionRequest state fail k stanza chan = do | |||
1024 | , stanzaFrom = Just from }) | 1050 | , stanzaFrom = Just from }) |
1025 | (connChan con) | 1051 | (connChan con) |
1026 | let policySetter = fromMaybe (Connection.setPolicy conns h) $ do | 1052 | let policySetter = fromMaybe (Connection.setPolicy conns h) $ do |
1027 | toxman <- toxManager state | 1053 | (toxman,_,_) <- weAreTox state client h |
1028 | (me , ".tox") <- Just $ Text.splitAt 43 (clientProfile client) | ||
1029 | (them, ".tox") <- Just $ Text.splitAt 43 h | ||
1030 | Just $ setToxConnectionPolicy toxman (clientProfile client) h | 1054 | Just $ setToxConnectionPolicy toxman (clientProfile client) h |
1031 | -- Add peer if we are not already associated ... | 1055 | -- Add peer if we are not already associated ... |
1032 | policySetter Connection.TryingToConnect | 1056 | policySetter Connection.TryingToConnect |
1033 | atomically $ putTMVar svVar (sv,conns) | 1057 | atomically $ putTMVar svVar (sv,conns) |
1034 | 1058 | ||
1059 | weAreTox :: PresenceState -> ClientState -> Text -> Maybe (ToxManager ClientAddress,Text{- me -},Text{- them -}) | ||
1060 | weAreTox state client h = do | ||
1061 | toxman <- toxManager state | ||
1062 | (me , ".tox") <- Just $ Text.splitAt 43 (clientProfile client) | ||
1063 | (them, ".tox") <- Just $ Text.splitAt 43 h | ||
1064 | return (toxman,me,them) | ||
1035 | 1065 | ||
1036 | resolvedFromRoster | 1066 | resolvedFromRoster |
1037 | :: (ConfigFiles.User -> ConfigFiles.Profile -> IO [L.ByteString]) | 1067 | :: (ConfigFiles.User -> ConfigFiles.Profile -> IO [L.ByteString]) |
diff --git a/Presence/XMPPServer.hs b/Presence/XMPPServer.hs index 79335efc..ae861a61 100644 --- a/Presence/XMPPServer.hs +++ b/Presence/XMPPServer.hs | |||
@@ -41,6 +41,7 @@ module XMPPServer | |||
41 | , flushPassThrough | 41 | , flushPassThrough |
42 | , greet' | 42 | , greet' |
43 | , (<&>) | 43 | , (<&>) |
44 | , grokStanza | ||
44 | ) where | 45 | ) where |
45 | 46 | ||
46 | import ConnectionKey | 47 | import ConnectionKey |
diff --git a/ToxManager.hs b/ToxManager.hs index d16a5d2e..6d0149cf 100644 --- a/ToxManager.hs +++ b/ToxManager.hs | |||
@@ -26,7 +26,7 @@ import qualified Network.Tox.Onion.Transport as Tox | |||
26 | import Presence | 26 | import Presence |
27 | import Text.Read | 27 | import Text.Read |
28 | import ToxToXMPP | 28 | import ToxToXMPP |
29 | import XMPPServer (ClientAddress) | 29 | import XMPPServer |
30 | import DPut | 30 | import DPut |
31 | 31 | ||
32 | 32 | ||
@@ -140,5 +140,17 @@ toxman announcer toxbkts tox presence = ToxManager | |||
140 | Just acc -> setContactPolicy (Tox.id2key themid) TryingToConnect acc | 140 | Just acc -> setContactPolicy (Tox.id2key themid) TryingToConnect acc |
141 | -- If unscheduled and unconnected, schedule recurring search for this contact. | 141 | -- If unscheduled and unconnected, schedule recurring search for this contact. |
142 | _ -> return () -- Remove contact. | 142 | _ -> return () -- Remove contact. |
143 | , resolveToxPeer = \me them -> do | ||
144 | let lookupContact accs | ||
145 | = do meid <- readMaybe $ T.unpack me | ||
146 | themid <- readMaybe $ T.unpack them | ||
147 | acc <- HashMap.lookup meid accs | ||
148 | return $ HashMap.lookup themid <$> readTVar (contacts acc) | ||
149 | atomically $ do | ||
150 | accs <- let ContactInfo{ accounts } = Tox.toxContactInfo tox | ||
151 | in readTVar accounts | ||
152 | mc <- join <$> sequence (lookupContact accs) | ||
153 | maddr <- join <$> mapM (readTVar . contactLastSeenAddr) mc | ||
154 | return $ addrToPeerKey . Remote . Tox.nodeAddr . snd <$> maddr | ||
143 | } | 155 | } |
144 | 156 | ||
diff --git a/ToxToXMPP.hs b/ToxToXMPP.hs index b75ada6a..9979526a 100644 --- a/ToxToXMPP.hs +++ b/ToxToXMPP.hs | |||
@@ -15,14 +15,19 @@ module ToxToXMPP | |||
15 | , interweave | 15 | , interweave |
16 | ) where | 16 | ) where |
17 | 17 | ||
18 | import Data.Conduit as C | 18 | import Control.Applicative |
19 | import qualified Data.Conduit.List as CL | 19 | import Data.Conduit as C |
20 | import Data.XML.Types as XML | 20 | import qualified Data.Conduit.List as CL |
21 | import Data.Monoid | ||
22 | import Data.Text.Encoding as T | ||
23 | import Data.XML.Types as XML | ||
21 | import EventUtil | 24 | import EventUtil |
22 | import Network.Tox.Crypto.Transport as Tox | 25 | import Network.Tox.Crypto.Transport as Tox |
23 | import Network.Tox.Handshake (HandshakeParams (..)) | 26 | import Network.Tox.Handshake (HandshakeParams (..)) |
24 | import Util (unsplitJID) | 27 | import qualified Text.XML.Stream.Parse as XML |
25 | import XMPPServer as XMPP | 28 | import Util (unsplitJID) |
29 | import XMPPServer as XMPP | ||
30 | |||
26 | 31 | ||
27 | import Announcer | 32 | import Announcer |
28 | import Announcer.Tox | 33 | import Announcer.Tox |
@@ -71,31 +76,39 @@ import GHC.Conc (labelThread) | |||
71 | #endif | 76 | #endif |
72 | import DPut | 77 | import DPut |
73 | import Nesting | 78 | import Nesting |
74 | 79 | import XMPPToTox | |
75 | xmppToTox :: Conduit XML.Event IO Tox.CryptoMessage | ||
76 | xmppToTox = doNestingXML $ do | ||
77 | eventBeginDocument <- await | ||
78 | streamTag <- await | ||
79 | fix $ \loop -> do | ||
80 | e <- nextElement | ||
81 | -- dput DPut.XMan $ "xmppToTox: " ++ show e | ||
82 | -- | ||
83 | -- (yield e >> awaitForever yield) $$ prettyPrint "xmpp->Tox" | ||
84 | -- prettyPrint | ||
85 | loop | ||
86 | 80 | ||
87 | toxToXmpp :: Monad m => SockAddr -> PublicKey -> Text -> Conduit Tox.CryptoMessage m XML.Event | 81 | toxToXmpp :: Monad m => SockAddr -> PublicKey -> Text -> Conduit Tox.CryptoMessage m XML.Event |
88 | toxToXmpp laddr me theirhost = do | 82 | toxToXmpp laddr me theirhost = do |
89 | CL.sourceList $ XMPP.greet' "jabber:server" theirhost | 83 | CL.sourceList $ XMPP.greet' "jabber:server" theirhost |
90 | let me_u = T.pack $ show (key2id me) | 84 | let me_u = T.pack $ show (key2id me) |
91 | awaitForever $ \toxmsg -> do | 85 | awaitForever $ \case |
92 | xmppInstantMessage "jabber:server" | 86 | |
93 | (Just theirhost) -- /from/ | 87 | UpToN { msgID = MESSAGE |
94 | (Just $ unsplitJID (Just me_u,T.pack (show laddr),Nothing)) -- /to/ should match local address of this node. | 88 | , msgBytes = bs } |
95 | (T.pack $ show $ msgID toxmsg) | 89 | -> do |
96 | 90 | xmppInstantMessage "jabber:server" | |
97 | xmppInstantMessage :: Monad m => Text -> Maybe Text -> Maybe Text -> Text -> ConduitM i Event m () | 91 | (Just $ "root@" <> theirhost) -- /from/ |
98 | xmppInstantMessage namespace mfrom mto text = do | 92 | (Just $ unsplitJID (Just me_u,T.pack (show laddr),Nothing)) -- /to/ should match local address of this node. |
93 | [] | ||
94 | (T.decodeUtf8 bs) | ||
95 | |||
96 | toxmsg | msgID toxmsg == PacketRequest -> return () | ||
97 | |||
98 | toxmsg -> do | ||
99 | xmppInstantMessage "jabber:server" | ||
100 | (Just theirhost) -- /from/ | ||
101 | (Just $ unsplitJID (Just me_u,T.pack (show laddr),Nothing)) -- /to/ should match local address of this node. | ||
102 | [ attr "style" "font-weight:bold; color:red" ] | ||
103 | (T.pack $ show $ msgID toxmsg) | ||
104 | |||
105 | xmppInstantMessage :: Monad m => Text | ||
106 | -> Maybe Text | ||
107 | -> Maybe Text | ||
108 | -> [(Name, [Content])] | ||
109 | -> Text | ||
110 | -> ConduitM i Event m () | ||
111 | xmppInstantMessage namespace mfrom mto style text = do | ||
99 | let ns n = n { nameNamespace = Just namespace } | 112 | let ns n = n { nameNamespace = Just namespace } |
100 | C.yield $ EventBeginElement (ns "message") | 113 | C.yield $ EventBeginElement (ns "message") |
101 | ( maybe id (\t->(attr "from" t:)) mfrom | 114 | ( maybe id (\t->(attr "from" t:)) mfrom |
@@ -106,8 +119,7 @@ xmppInstantMessage namespace mfrom mto text = do | |||
106 | C.yield $ EventEndElement (ns "body") | 119 | C.yield $ EventEndElement (ns "body") |
107 | C.yield $ EventBeginElement "{http://jabber.org/protocol/xhtml-im}html" [] | 120 | C.yield $ EventBeginElement "{http://jabber.org/protocol/xhtml-im}html" [] |
108 | C.yield $ EventBeginElement "{http://www.w3.org/1999/xhtml}body" [] | 121 | C.yield $ EventBeginElement "{http://www.w3.org/1999/xhtml}body" [] |
109 | C.yield $ EventBeginElement "{http://www.w3.org/1999/xhtml}p" | 122 | C.yield $ EventBeginElement "{http://www.w3.org/1999/xhtml}p" style |
110 | [ attr "style" "font-weight:bold; color:red" ] | ||
111 | C.yield $ EventContent $ ContentText text | 123 | C.yield $ EventContent $ ContentText text |
112 | C.yield $ EventEndElement "{http://www.w3.org/1999/xhtml}p" | 124 | C.yield $ EventEndElement "{http://www.w3.org/1999/xhtml}p" |
113 | C.yield $ EventEndElement "{http://www.w3.org/1999/xhtml}body" | 125 | C.yield $ EventEndElement "{http://www.w3.org/1999/xhtml}body" |
diff --git a/XMPPToTox.hs b/XMPPToTox.hs new file mode 100644 index 00000000..7ca4330e --- /dev/null +++ b/XMPPToTox.hs | |||
@@ -0,0 +1,67 @@ | |||
1 | {-# LANGUAGE LambdaCase #-} | ||
2 | {-# LANGUAGE NoMonomorphismRestriction #-} | ||
3 | module XMPPToTox | ||
4 | ( module XMPPToTox | ||
5 | , CryptoMessage(..) | ||
6 | , MessageID(..) | ||
7 | ) where | ||
8 | |||
9 | import Control.Applicative | ||
10 | import Control.Monad | ||
11 | import Control.Monad.Catch | ||
12 | import Data.Conduit | ||
13 | import Data.Function | ||
14 | import Data.Monoid | ||
15 | import Data.Text (Text) | ||
16 | import Data.Text.Encoding as T | ||
17 | import Data.XML.Types as XML | ||
18 | import Network.Tox.Crypto.Transport (CryptoMessage (..), MessageID (..)) | ||
19 | import Text.XML.Stream.Parse as XML | ||
20 | |||
21 | -- Debugging. Not real Tox message. | ||
22 | funnyMessage :: MonadThrow m => Text -> ConduitM i CryptoMessage m () | ||
23 | funnyMessage txt = yield $ UpToN Padding (T.encodeUtf8 txt) | ||
24 | |||
25 | sendMsg :: MonadThrow m => Text -> ConduitM i CryptoMessage m () | ||
26 | sendMsg txt = yield $ UpToN MESSAGE (T.encodeUtf8 txt) | ||
27 | |||
28 | |||
29 | eom :: MonadThrow m => ConduitM Event o m () | ||
30 | eom = many_ ignoreAnyTreeContent | ||
31 | |||
32 | msgToTox :: MonadThrow m => | ||
33 | ConduitM Event CryptoMessage m (Maybe ()) | ||
34 | msgToTox = tag' "{jabber:server}message" | ||
35 | (requireAttr "type" >>= \case | ||
36 | "chat" -> ignoreAttrs | ||
37 | _ -> empty) | ||
38 | $ \_ -> many_ $ choose | ||
39 | [ tagIgnoreAttrs "{jabber:server}body" | ||
40 | $ do content >>= sendMsg | ||
41 | eom | ||
42 | , tagIgnoreAttrs "{http://jabber.org/protocol/chatstates}composing" | ||
43 | $ do yield $ TwoByte TYPING 1 | ||
44 | eom | ||
45 | , tagIgnoreAttrs "{http://jabber.org/protocol/chatstates}paused" | ||
46 | $ do yield $ TwoByte TYPING 0 | ||
47 | eom | ||
48 | , ignoreAnyTreeContent | ||
49 | ] | ||
50 | |||
51 | unknownToTox :: MonadThrow m => | ||
52 | ConduitM Event CryptoMessage m (Maybe ()) | ||
53 | unknownToTox = tag anyName (\n -> ignoreAttrs >> return n) $ \n -> do | ||
54 | funnyMessage $ nameLocalName n | ||
55 | eom | ||
56 | |||
57 | |||
58 | xmppToTox :: MonadThrow m => Conduit XML.Event m CryptoMessage | ||
59 | xmppToTox = do | ||
60 | eventBeginDocument <- await | ||
61 | streamTag <- await | ||
62 | fix $ \loop -> do | ||
63 | got <- choose | ||
64 | [ msgToTox | ||
65 | , unknownToTox | ||
66 | ] | ||
67 | forM_ got $ \_ -> loop | ||
diff --git a/examples/dhtd.hs b/examples/dhtd.hs index fefec650..e099334e 100644 --- a/examples/dhtd.hs +++ b/examples/dhtd.hs | |||
@@ -1982,14 +1982,22 @@ main = do | |||
1982 | xmppSink = newXmmpSink netcrypto | 1982 | xmppSink = newXmmpSink netcrypto |
1983 | forM_ msv $ \sv -> do | 1983 | forM_ msv $ \sv -> do |
1984 | let Tox.HaveDHTKey saddr = Tox.ncSockAddr netcrypto | 1984 | let Tox.HaveDHTKey saddr = Tox.ncSockAddr netcrypto |
1985 | announceToxJabberPeer (Tox.ncMyPublicKey netcrypto) (Tox.ncTheirPublicKey netcrypto) (xmppEventChannel sv) addrTox saddr pingflag xmppSrc xmppSink | 1985 | Tox.HaveDHTKey dkey = Tox.ncTheirDHTKey netcrypto |
1986 | nid = Tox.key2id dkey | ||
1987 | them = Tox.ncTheirPublicKey netcrypto | ||
1988 | me = Tox.ncMyPublicKey netcrypto | ||
1989 | announceToxJabberPeer me them (xmppEventChannel sv) addrTox saddr pingflag xmppSrc xmppSink | ||
1986 | forM_ mbtox $ \tox -> do | 1990 | forM_ mbtox $ \tox -> do |
1987 | let ContactInfo{accounts} = Tox.toxContactInfo tox | 1991 | let ContactInfo{accounts} = Tox.toxContactInfo tox |
1988 | mbacc <- HashMap.lookup (Tox.key2id $ Tox.ncMyPublicKey netcrypto) | 1992 | mbacc <- HashMap.lookup (Tox.key2id me) |
1989 | <$> atomically (readTVar accounts) | 1993 | <$> atomically (readTVar accounts) |
1994 | -- TODO: Add account if it doesn't exist? | ||
1990 | forM_ mbacc $ \acnt -> do | 1995 | forM_ mbacc $ \acnt -> do |
1991 | now <- getPOSIXTime | 1996 | now <- getPOSIXTime |
1992 | atomically $ setEstablished now (Tox.ncTheirPublicKey netcrypto) acnt | 1997 | forM_ (either (const Nothing) Just $ Tox.nodeInfo nid saddr) |
1998 | $ \ni -> do | ||
1999 | atomically $ do setEstablished now them acnt | ||
2000 | setContactAddr now them ni acnt | ||
1993 | atomically $ do | 2001 | atomically $ do |
1994 | supply <- readTVar (Tox.listenerIDSupply netCryptoSessionsState) | 2002 | supply <- readTVar (Tox.listenerIDSupply netCryptoSessionsState) |
1995 | let (listenerId,supply') = freshId supply | 2003 | let (listenerId,supply') = freshId supply |
diff --git a/examples/test-xmpp.hs b/examples/test-xmpp.hs new file mode 100644 index 00000000..a8e20c3c --- /dev/null +++ b/examples/test-xmpp.hs | |||
@@ -0,0 +1,41 @@ | |||
1 | |||
2 | import Control.Monad.IO.Class | ||
3 | import Control.Monad.Trans.Resource | ||
4 | -- import Control.Monad.Trans.Class | ||
5 | import Data.Conduit | ||
6 | import Data.Conduit.List as CL | ||
7 | -- import Data.XML.Types | ||
8 | import System.Environment | ||
9 | import Text.XML.Stream.Parse | ||
10 | |||
11 | import XMPPToTox | ||
12 | |||
13 | {- | ||
14 | parse :: ConduitM Event o (ResourceT IO) () | ||
15 | parse = do | ||
16 | return () | ||
17 | -} | ||
18 | |||
19 | showTox :: CryptoMessage -> ResourceT IO () | ||
20 | showTox = liftIO . print | ||
21 | |||
22 | main :: IO () | ||
23 | main = do | ||
24 | args <- getArgs | ||
25 | let xmlfile = args !! 0 | ||
26 | |||
27 | -- runConduit :: Monad m => ConduitM () Void m r -> m r | ||
28 | |||
29 | -- test-xmpp.hs:19:51: warning: [-Wdeprecations] | ||
30 | -- In the use of ‘$$’ | ||
31 | -- (imported from Data.Conduit, but defined in conduit-1.3.0.3:Data.Conduit.Internal.Conduit): | ||
32 | -- Deprecated: "Use runConduit and .|" | ||
33 | -- | ||
34 | -- runResourceT $ parseFile def xmlfile =$= parse $$ return () | ||
35 | |||
36 | runResourceT $ runConduit $ do | ||
37 | parseFile def xmlfile | ||
38 | .| xmppToTox | ||
39 | -- CL.mapM_ :: Monad m => (a -> m ()) -> ConduitT a o m () | ||
40 | .| CL.mapM_ showTox | ||
41 | |||