This commit is contained in:
Stephen Paul Weber 2021-06-28 22:36:37 -05:00
parent eac717044d
commit 16411dd54d
No known key found for this signature in database
GPG Key ID: D11C2911CE519CDE
4 changed files with 143 additions and 6 deletions

75
IQManager.hs Normal file
View File

@ -0,0 +1,75 @@
module IQManager (iqManager) where
import Prelude ()
import BasicPrelude
import Control.Concurrent.STM (
STM, TMVar, TVar, modifyTVar', newEmptyTMVar, newTVar, orElse,
readTVar, takeTMVar, tryPutTMVar, writeTVar
)
import Control.Concurrent.STM.Delay (newDelay, waitDelay)
import UnexceptionalIO.Trans (Unexceptional)
import qualified Data.Map.Strict as Map
import qualified Network.Protocol.XMPP as XMPP
import qualified Data.UUID as UUID
import qualified Data.UUID.V4 as UUID
import Util
type ResponseMap = Map.Map (Maybe Text) (TMVar XMPP.IQ)
iqSendTimeoutMicroseconds :: Int
iqSendTimeoutMicroseconds = 5000000
iqDefaultID :: (Unexceptional m) => XMPP.IQ -> m XMPP.IQ
iqDefaultID iq@XMPP.IQ { XMPP.iqID = Just _ } = return iq
iqDefaultID iq = do
uuid <- fromIO_ UUID.nextRandom
return $ iq {
XMPP.iqID = Just $ UUID.toText uuid
}
iqSenderUnexceptional :: (Unexceptional m) =>
(XMPP.IQ -> m ())
-> TVar ResponseMap
-> XMPP.IQ
-> m (STM (Maybe XMPP.IQ))
iqSenderUnexceptional sender responseMapVar iq = do
iqToSend <- iqDefaultID iq
timeout <- fromIO_ $ newDelay iqSendTimeoutMicroseconds
iqResponseVar <- atomicUIO newEmptyTMVar
atomicUIO $ modifyTVar' responseMapVar $
Map.insert (XMPP.iqID iqToSend) iqResponseVar
sender iqToSend
return (
(waitDelay timeout *> pure Nothing)
`orElse`
fmap Just (takeTMVar iqResponseVar)
)
iqReceiver :: (Unexceptional m) => TVar ResponseMap -> XMPP.IQ -> m (Maybe XMPP.IQ)
iqReceiver responseMapVar receivedIQ
| XMPP.iqType receivedIQ `elem` [XMPP.IQResult, XMPP.IQError] = do
maybeIqResponseVar <- atomicUIO $ do
responseMap <- readTVar responseMapVar
let (maybeIqResponseVar, responseMap') =
Map.updateLookupWithKey
(const $ const Nothing)
(XMPP.iqID receivedIQ) responseMap
writeTVar responseMapVar $! responseMap'
return maybeIqResponseVar
case maybeIqResponseVar of
Just iqResponseVar -> do
atomicUIO $ tryPutTMVar iqResponseVar receivedIQ
return Nothing
Nothing -> return (Just receivedIQ)
| otherwise = return $ Just receivedIQ
iqManager :: (Unexceptional m1, Unexceptional m2, Unexceptional m3) =>
(XMPP.IQ -> m2 ()) ->
m1 (XMPP.IQ -> m2 (STM (Maybe XMPP.IQ)), XMPP.IQ -> m3 (Maybe XMPP.IQ))
iqManager sender = do
responseMapVar <- atomicUIO $ newTVar Map.empty
return (
iqSenderUnexceptional sender responseMapVar,
iqReceiver responseMapVar
)

13
Util.hs
View File

@ -5,8 +5,12 @@ import BasicPrelude
import Control.Applicative (many) import Control.Applicative (many)
import Control.Concurrent import Control.Concurrent
(ThreadId, forkFinally, myThreadId, throwTo) (ThreadId, forkFinally, myThreadId, throwTo)
import Control.Concurrent.STM (STM, atomically)
import Data.Time.Clock (UTCTime) import Data.Time.Clock (UTCTime)
import Data.Time.Format (parseTimeM, defaultTimeLocale) import Data.Time.Format (parseTimeM, defaultTimeLocale)
import Data.Void (absurd)
import UnexceptionalIO (Unexceptional)
import qualified UnexceptionalIO as UIO
import qualified Control.Exception as Ex import qualified Control.Exception as Ex
import qualified Data.Attoparsec.Text as Atto import qualified Data.Attoparsec.Text as Atto
import qualified Data.Text as Text import qualified Data.Text as Text
@ -15,9 +19,18 @@ import qualified Network.Protocol.XMPP as XMPP
import qualified Config import qualified Config
instance Unexceptional XMPP.XMPP where
lift = liftIO . UIO.lift
s :: (IsString s) => String -> s s :: (IsString s) => String -> s
s = fromString s = fromString
fromIO_ :: (Unexceptional m) => IO a -> m a
fromIO_ = fmap (either absurd id) . UIO.fromIO' (error . show)
atomicUIO :: (Unexceptional m) => STM a -> m a
atomicUIO = fromIO_ . atomically
escapeJid :: Text -> Text escapeJid :: Text -> Text
escapeJid txt = mconcat result escapeJid txt = mconcat result
where where

View File

@ -17,16 +17,21 @@ common defs
basic-prelude >=0.7 && <0.8, basic-prelude >=0.7 && <0.8,
bytestring >=0.10 && <0.11, bytestring >=0.10 && <0.11,
containers >=0.5 && <0.6, containers >=0.5 && <0.6,
dhall >= 1.24 && < 2.0, dhall >= 1.24 && <2.0,
errors >=2.3 && <2.4, errors >=2.3 && <2.4,
network >= 2.6.3 && < 2.7, network >= 2.6.3 && <2.7,
network-protocol-xmpp >=0.4 && <0.5, network-protocol-xmpp >=0.4 && <0.5,
sqlite-simple >= 0.4 && <0.5, sqlite-simple >= 0.4 && <0.5,
stm >= 2.4 && <3.0,
stm-delay >= 0.1 && < 0.2,
text >=1.2 && <1.3, text >=1.2 && <1.3,
time >=1.5 && <2.0, time >=1.5 && <2.0,
unexceptionalio >= 0.5 && <0.6,
unexceptionalio-trans >= 0.5 && <0.6,
uuid >= 1.3 && <2.0,
xml-types >=0.3 && <0.4 xml-types >=0.3 && <0.4
executable gateway executable gateway
import: defs import: defs
main-is: gateway.hs main-is: gateway.hs
other-modules: Router, Util, Config, ConfigFile, Session other-modules: Router, Util, Config, ConfigFile, Session, IQManager

View File

@ -5,6 +5,9 @@ import BasicPrelude
import System.IO import System.IO
(stdout, stderr, hSetBuffering, BufferMode(LineBuffering)) (stdout, stderr, hSetBuffering, BufferMode(LineBuffering))
import Control.Error (exceptT, justZ) import Control.Error (exceptT, justZ)
import Control.Concurrent (threadDelay)
import Control.Concurrent.STM (STM)
import qualified Database.SQLite.Simple as DB
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.XML.Types as XML import qualified Data.XML.Types as XML
import qualified Network.Protocol.XMPP as XMPP import qualified Network.Protocol.XMPP as XMPP
@ -13,6 +16,7 @@ import qualified Config
import qualified Session import qualified Session
import Router import Router
import Util import Util
import IQManager
hasMucCode :: Int -> XMPP.Presence -> Bool hasMucCode :: Int -> XMPP.Presence -> Bool
hasMucCode code XMPP.Presence { XMPP.presencePayloads = p } = hasMucCode code XMPP.Presence { XMPP.presencePayloads = p } =
@ -126,11 +130,47 @@ handleIq _ _ = return ()
joinFromBridge :: Config.Config -> XMPP.JID -> XMPP.XMPP () joinFromBridge :: Config.Config -> XMPP.JID -> XMPP.XMPP ()
joinFromBridge config muc = do joinFromBridge config muc = do
Session.mkSession config XMPP.PresenceAvailable Nothing muc Session.mkSession config XMPP.PresenceAvailable Nothing target
XMPP.putStanza $ (mucJoin muc (Config.nick config)) { XMPP.putStanza presence
where
Just target = XMPP.presenceTo presence
presence = (mucJoin muc (Config.nick config)) {
XMPP.presenceFrom = Just $ Config.bridgeJid config XMPP.presenceFrom = Just $ Config.bridgeJid config
} }
pingSuccessError :: XML.Element -> [XML.Element]
pingSuccessError = uncurry (<|>) . (uncurry (<|>) . (
XML.isNamed (s"{urn:ietf:params:xml:ns:xmpp-stanzas}service-unavaliable")
&&&
XML.isNamed (s"{urn:ietf:params:xml:ns:xmpp-stanzas}feature-not-implemented")
) &&&
XML.isNamed (s"{urn:ietf:params:xml:ns:xmpp-stanzas}item-not-found")
)
selfPings :: Config.Config -> (XMPP.IQ -> XMPP.XMPP (STM (Maybe XMPP.IQ))) -> XMPP.XMPP ()
selfPings config sendIQ = forever $ do
liftIO $ threadDelay 60000000
sessions <- liftIO $ DB.query_ (Config.db config) (s"SELECT source_muc, source_nick, target_muc, target_nick FROM sessions")
forM_ sessions $ \(sourceMuc, sourceNick, targetMuc, targetNick) -> void $ forkXMPP $ do
let Just target = XMPP.parseJID (targetMuc ++ s"/" ++ targetNick)
reply <- (atomicUIO =<<) $ sendIQ $ (XMPP.emptyIQ XMPP.IQGet) {
XMPP.iqFrom = sourceJid sourceMuc sourceNick,
XMPP.iqTo = Just target,
XMPP.iqPayload = Just $ XML.Element (s"{urn:xmpp:ping}ping") [] []
}
if (XMPP.iqType <$> reply) == Just XMPP.IQResult then return () else
case pingSuccessError =<< XML.elementChildren =<< justZ (XMPP.iqPayload =<< reply) of
(_:_) -> return ()
_ | sourceMuc == mempty -> joinFromBridge config target
_ ->
Session.sendPresence config ((mucJoin target targetNick) {
XMPP.presenceFrom = XMPP.parseJID (sourceMuc ++ s"/" ++ sourceNick)
}) target
where
sourceJid muc nick
| muc == mempty = Just $ Config.bridgeJid config
| otherwise = proxyJid config <$> XMPP.parseJID (muc ++ s"/" ++ nick)
main :: IO () main :: IO ()
main = do main = do
hSetBuffering stdout LineBuffering hSetBuffering stdout LineBuffering
@ -145,13 +185,17 @@ main = do
exceptT print return $ exceptT print return $
runRoutedComponent server (Config.secret config) $ do runRoutedComponent server (Config.secret config) $ do
(sendIQ, iqReceiver) <- iqManager XMPP.putStanza
forM_ (Config.mucs config) $ \bridge -> do forM_ (Config.mucs config) $ \bridge -> do
joinFromBridge config (Config.muc1 bridge) joinFromBridge config (Config.muc1 bridge)
joinFromBridge config (Config.muc2 bridge) joinFromBridge config (Config.muc2 bridge)
void $ forkXMPP $ selfPings config sendIQ
return $ defaultRoutes { return $ defaultRoutes {
presenceRoute = handlePresence config, presenceRoute = handlePresence config,
presenceErrorRoute = handlePresenceError config, presenceErrorRoute = handlePresenceError config,
messageGroupChatRoute = handleGroupChat config, messageGroupChatRoute = handleGroupChat config,
messageRoute = handleMessage config, messageRoute = handleMessage config,
iqRoute = handleIq config iqRoute = \iq -> do
maybeIq <- iqReceiver iq
forM_ maybeIq $ handleIq config
} }