summaryrefslogtreecommitdiff
path: root/Presence/Stanza
diff options
context:
space:
mode:
authorJoe Crayne <joe@jerkface.net>2018-11-04 23:32:38 -0500
committerJoe Crayne <joe@jerkface.net>2018-11-05 00:02:04 -0500
commite8d00e729f1d6737180210d018f78e4b2efd8a35 (patch)
treea5b47ca8ba7f5389e696a4c9d4e48607e6803fdf /Presence/Stanza
parented3bfff125c3a81f2318ac7541123f3311e2d94e (diff)
Factored Stanza.{Types,Parse} out of XMPPServer.
Diffstat (limited to 'Presence/Stanza')
-rw-r--r--Presence/Stanza/Parse.hs261
-rw-r--r--Presence/Stanza/Type.hs184
2 files changed, 445 insertions, 0 deletions
diff --git a/Presence/Stanza/Parse.hs b/Presence/Stanza/Parse.hs
new file mode 100644
index 00000000..50e1e25b
--- /dev/null
+++ b/Presence/Stanza/Parse.hs
@@ -0,0 +1,261 @@
1module Stanza.Parse (grokStanza,errorTagLocalName) where
2
3import Control.Concurrent.STM
4import Control.Monad
5import Data.Char
6import Data.Function
7import Data.Maybe
8import qualified Data.Text as Text (pack, unpack, words)
9 ;import Data.Text (Text)
10
11import Control.Monad.Catch (MonadThrow)
12import Control.Monad.IO.Class (MonadIO, liftIO)
13import qualified Data.Map as Map
14import Data.XML.Types as XML
15import qualified Text.XML.Stream.Parse as XML
16
17import Control.Concurrent.STM.Util
18import ControlMaybe (handleIO_, (<&>))
19import EventUtil
20import Nesting
21import Stanza.Type
22
23-- | Identify an XMPP stanza based on the open-tag.
24grokStanza :: Text -> XML.Event -> NestingXML o IO (Maybe StanzaType)
25grokStanza "jabber:server" stanzaTag =
26 case () of
27 _ | stanzaTag `isServerIQOf` "get" -> grokStanzaIQGet stanzaTag
28 _ | stanzaTag `isServerIQOf` "result" -> grokStanzaIQResult stanzaTag
29 _ | tagName stanzaTag == "{jabber:server}presence" -> grokPresence "jabber:server" stanzaTag
30 _ | tagName stanzaTag == "{jabber:server}message" -> grokMessage "jabber:server" stanzaTag
31 _ -> return $ Just Unrecognized
32
33grokStanza "jabber:client" stanzaTag =
34 case () of
35 _ | stanzaTag `isClientIQOf` "get" -> grokStanzaIQGet stanzaTag
36 _ | stanzaTag `isClientIQOf` "set" -> grokStanzaIQSet stanzaTag
37 _ | stanzaTag `isClientIQOf` "result" -> grokStanzaIQResult stanzaTag
38 _ | tagName stanzaTag == "{jabber:client}presence" -> grokPresence "jabber:client" stanzaTag
39 _ | tagName stanzaTag == "{jabber:client}message" -> grokMessage "jabber:client" stanzaTag
40 _ -> return $ Just Unrecognized
41
42grokStanzaIQGet :: Monad m => XML.Event -> NestingXML o m (Maybe StanzaType)
43grokStanzaIQGet stanza = do
44 mtag <- nextElement
45 forM mtag $ \tag -> do
46 case tagName tag of
47 "{urn:xmpp:ping}ping" -> return Ping
48 "{jabber:iq:roster}query" -> return RequestRoster
49 name -> return $ UnrecognizedQuery name
50
51grokStanzaIQResult :: XML.Event -> NestingXML o IO (Maybe StanzaType)
52grokStanzaIQResult stanza = do
53 mtag <- nextElement
54 fromMaybe (return $ Just Pong) $ mtag <&> \tag -> do
55 case tagName tag of
56 "{jabber:iq:version}query" | nameNamespace (tagName stanza)==Just "jabber:client"
57 -> parseClientVersion
58 _ -> return Nothing
59
60grokStanzaIQSet :: XML.Event -> NestingXML o IO (Maybe StanzaType)
61grokStanzaIQSet stanza = do
62 mtag <- nextElement
63 case tagName <$> mtag of
64 Just "{urn:ietf:params:xml:ns:xmpp-bind}bind"
65 -> do mchild <- nextElement
66 case tagName <$> mchild of
67 Just "{urn:ietf:params:xml:ns:xmpp-bind}resource"
68 -> do rsc <- XML.content -- TODO: MonadThrow???
69 return . Just $ RequestResource Nothing (Just rsc)
70 Just _ -> return Nothing
71 Nothing -> return . Just $ RequestResource Nothing Nothing
72 Just "{urn:ietf:params:xml:ns:xmpp-session}session"
73 -> return $ Just SessionRequest
74 _ -> return Nothing
75
76grokPresence
77 :: ( MonadThrow m
78 , MonadIO m
79 ) => Text -> XML.Event -> NestingXML o m (Maybe StanzaType)
80grokPresence ns stanzaTag = do
81 let typ = lookupAttrib "type" (tagAttrs stanzaTag)
82 case typ of
83 Nothing -> parsePresenceStatus ns stanzaTag
84 Just "unavailable" -> fmap (fmap (\p -> p {presenceShow=Offline}))
85 $ parsePresenceStatus ns stanzaTag
86 Just "error" -> return . Just $ PresenceInformError
87 Just "unsubscribed" -> return . Just $ PresenceInformSubscription False
88 Just "subscribed" -> return . Just $ PresenceInformSubscription True
89 Just "probe" -> return . Just $ PresenceRequestStatus
90 Just "unsubscribe" -> return . Just $ PresenceRequestSubscription False
91 Just "subscribe" -> return . Just $ PresenceRequestSubscription True
92 _ -> return Nothing
93
94grokMessage
95 :: ( MonadThrow m
96 , MonadIO m
97 ) => Text -> XML.Event -> NestingXML o m (Maybe StanzaType)
98grokMessage ns stanzaTag = do
99 let typ = lookupAttrib "type" (tagAttrs stanzaTag)
100 case typ of
101 Just "error" -> do
102 mb <- findErrorTag ns
103 return $ do
104 e <- mb
105 return $ Error e stanzaTag
106 _ -> do t <- parseMessage ns stanzaTag
107 return $ Just t
108
109parseClientVersion :: NestingXML o IO (Maybe StanzaType)
110parseClientVersion = parseit Nothing Nothing
111 where
112 reportit mname mver = return $ do
113 name <- mname
114 ver <- mver
115 return NotifyClientVersion { versionName=name, versionVersion=ver }
116 parseit :: Maybe Text -> Maybe Text -> NestingXML o IO (Maybe StanzaType)
117 parseit mname mver = do
118 mtag <- nextElement
119 fromMaybe (reportit mname mver) $ mtag <&> \tag -> do
120 case tagName tag of
121 "{jabber:iq:version}name" -> do
122 x <- XML.content
123 parseit (Just x) mver
124 "{jabber:iq:version}version" -> do
125 x <- XML.content
126 parseit mname (Just x)
127 _ -> parseit mname mver
128
129parsePresenceStatus
130 :: ( MonadThrow m
131 , MonadIO m
132 ) => Text -> XML.Event -> NestingXML o m (Maybe StanzaType)
133parsePresenceStatus ns stanzaTag = do
134
135 let toStat "away" = Away
136 toStat "xa" = ExtendedAway
137 toStat "dnd" = DoNotDisturb
138 toStat "chat" = Chatty
139
140 showv <- liftIO . atomically $ newTVar Available
141 priov <- liftIO . atomically $ newTVar Nothing
142 statusv <- liftIO . atomically $ newTChan
143 fix $ \loop -> do
144 mtag <- nextElement
145 forM_ mtag $ \tag -> do
146 when (nameNamespace (tagName tag) == Just ns) $ do
147 case nameLocalName (tagName tag) of
148 "show" -> do t <- XML.content
149 liftIO . atomically $ writeTVar showv (toStat t)
150 "priority" -> do t <- XML.content
151 liftIO . handleIO_ (return ()) $ do
152 prio <- readIO (Text.unpack t)
153 atomically $ writeTVar priov (Just prio)
154 "status" -> do t <- XML.content
155 lang <- xmlLang
156 ioWriteChan statusv (maybe "" id lang,t)
157 _ -> return ()
158 loop
159 show <- liftIO . atomically $ readTVar showv
160 prio <- liftIO . atomically $ readTVar priov
161 status <- liftIO $ chanContents statusv -- Could use unsafeInterleaveIO to
162 -- avoid multiple passes, but whatever.
163 let wlist = do
164 w <- maybeToList $ lookupAttrib "whitelist" (tagAttrs stanzaTag)
165 Text.words w
166 return . Just $ PresenceStatus { presenceShow = show
167 , presencePriority = prio
168 , presenceStatus = status
169 , presenceWhiteList = wlist
170 }
171parseMessage
172 :: ( MonadThrow m
173 , MonadIO m
174 ) => Text -> XML.Event -> NestingXML o m StanzaType
175parseMessage ns stanza = do
176 let bodytag = Name { nameNamespace = Just ns
177 , nameLocalName = "body"
178 , namePrefix = Nothing }
179 subjecttag = Name { nameNamespace = Just ns
180 , nameLocalName = "subject"
181 , namePrefix = Nothing }
182 threadtag = Name { nameNamespace = Just ns
183 , nameLocalName = "thread"
184 , namePrefix = Nothing }
185 let emptyMsg = LangSpecificMessage { msgBody=Nothing, msgSubject=Nothing }
186 parseChildren (th,cmap) = do
187 child <- nextElement
188 lvl <- nesting
189 xmllang <- xmlLang
190 let lang = maybe "" id xmllang
191 let c = maybe emptyMsg id (Map.lookup lang cmap)
192 -- log $ " child: "<> bshow child
193 case child of
194 Just tag | tagName tag==bodytag
195 -> do
196 txt <- XML.content
197 awaitCloser lvl
198 parseChildren (th,Map.insert lang (c { msgBody=Just txt }) cmap)
199 Just tag | tagName tag==subjecttag
200 -> do
201 txt <- XML.content
202 awaitCloser lvl
203 parseChildren (th,Map.insert lang (c { msgSubject=Just txt }) cmap)
204 Just tag | tagName tag==threadtag
205 -> do
206 txt <- XML.content
207 awaitCloser lvl
208 parseChildren (th {msgThreadContent=txt},cmap)
209 Just tag -> do
210 -- let nm = tagName tag
211 -- attrs = tagAttrs tag
212 -- -- elems = msgElements c
213 -- txt <- XML.content
214 awaitCloser lvl
215 parseChildren (th,Map.insert lang c cmap)
216 Nothing -> return (th,cmap)
217 (th,langmap) <- parseChildren ( MessageThread {msgThreadParent=Nothing, msgThreadContent=""}
218 , Map.empty )
219 return Message {
220 msgLangMap = Map.toList langmap,
221 msgThread = if msgThreadContent th/="" then Just th else Nothing
222 }
223
224findErrorTag :: Monad m => Text -> NestingXML o m (Maybe StanzaError)
225findErrorTag ns = do
226 x <- nextElement
227 fmap join $ forM x $ \x ->
228 case tagName x of
229 n | nameNamespace n==Just ns && nameLocalName n=="error"
230 -> do
231 mtag <- findConditionTag
232 return $ do
233 tag <- {- trace ("mtag = "++show mtag) -} mtag
234 let t = nameLocalName (tagName tag)
235 conditionFromText t
236 _ -> findErrorTag ns
237
238findConditionTag :: Monad m => NestingXML o m (Maybe XML.Event)
239findConditionTag = do
240 mx <- nextElement
241 fmap join $ forM mx $ \x -> do
242 case nameNamespace (tagName x) of
243 Just "urn:ietf:params:xml:ns:xmpp-stanzas" -> return (Just x)
244 _ -> findConditionTag
245
246conditionFromText :: Text -> Maybe StanzaError
247conditionFromText t = fmap fst $ listToMaybe ss
248 where
249 es = [BadRequest .. UnexpectedRequest]
250 ts = map (\e->(e,errorTagLocalName e)) es
251 ss = dropWhile ((/=t) . snd) ts
252
253-- | Converts a CamelCase constructor to a hyphenated lower-case name for use
254-- as an xml tag.
255errorTagLocalName :: StanzaError -> Text
256errorTagLocalName e = Text.pack . drop 1 $ do
257 c <- show e
258 if 'A' <= c && c <= 'Z'
259 then [ '-', chr( ord c - ord 'A' + ord 'a') ]
260 else return c
261
diff --git a/Presence/Stanza/Type.hs b/Presence/Stanza/Type.hs
new file mode 100644
index 00000000..1d8041a9
--- /dev/null
+++ b/Presence/Stanza/Type.hs
@@ -0,0 +1,184 @@
1{-# LANGUAGE FlexibleInstances #-}
2module Stanza.Type where
3
4import Control.Concurrent.STM
5import Data.Int
6import Data.Text
7import Data.XML.Types as XML
8
9import Connection (PeerAddress(..))
10import ConnectionKey (ClientAddress(..))
11import LockedChan
12import Nesting (Lang)
13
14type Stanza = StanzaWrap (LockedChan XML.Event)
15
16data StanzaWrap a = Stanza
17 { stanzaType :: StanzaType
18 , stanzaId :: Maybe Text
19 , stanzaTo :: Maybe Text
20 , stanzaFrom :: Maybe Text
21 , stanzaChan :: a
22 , stanzaClosers :: TVar (Maybe [XML.Event])
23 , stanzaInterrupt :: TMVar ()
24 , stanzaOrigin :: StanzaOrigin
25 }
26
27data StanzaOrigin = LocalPeer
28 | PeerOrigin PeerAddress (TChan Stanza)
29 | ClientOrigin ClientAddress (TChan Stanza)
30
31data StanzaType
32 = Unrecognized
33 | Ping
34 | Pong
35 | RequestResource (Maybe Text) (Maybe Text) -- ^ Client's name for this host followed by client's requested resource id.
36 | SetResource
37 | SessionRequest
38 | UnrecognizedQuery Name
39 | RequestRoster
40 | Roster
41 | RosterEvent { rosterEventType :: RosterEventType
42 , rosterUser :: Text
43 , rosterContact :: Text }
44 | Error StanzaError XML.Event
45 | PresenceStatus { presenceShow :: JabberShow
46 , presencePriority :: Maybe Int8
47 , presenceStatus :: [(Lang,Text)]
48 , presenceWhiteList :: [Text]
49 }
50 | PresenceInformError
51 | PresenceInformSubscription Bool
52 | PresenceRequestStatus
53 | PresenceRequestSubscription Bool
54 | Message { msgThread :: Maybe MessageThread
55 , msgLangMap :: [(Lang,LangSpecificMessage)]
56 }
57 | NotifyClientVersion { versionName :: Text
58 , versionVersion :: Text }
59 | InternalEnableHack ClientHack
60 | InternalCacheId Text
61 deriving (Show,Eq)
62
63data RosterEventType
64 = RequestedSubscription
65 | NewBuddy -- preceded by PresenceInformSubscription True
66 | RemovedBuddy -- preceded by PresenceInformSubscription False
67 | PendingSubscriber -- same as PresenceRequestSubscription
68 | NewSubscriber
69 | RejectSubscriber
70 deriving (Show,Read,Ord,Eq,Enum)
71
72data ClientHack = SimulatedChatErrors
73 deriving (Show,Read,Ord,Eq,Enum)
74
75
76data LangSpecificMessage =
77 LangSpecificMessage { msgBody :: Maybe Text
78 , msgSubject :: Maybe Text
79 }
80 deriving (Show,Eq)
81
82data MessageThread = MessageThread {
83 msgThreadParent :: Maybe Text,
84 msgThreadContent :: Text
85 }
86 deriving (Show,Eq)
87
88
89data JabberShow = Offline
90 | ExtendedAway
91 | Away
92 | DoNotDisturb
93 | Available
94 | Chatty
95 deriving (Show,Enum,Ord,Eq,Read)
96
97class StanzaFirstTag a where
98 stanzaFirstTag :: StanzaWrap a -> IO XML.Event
99instance StanzaFirstTag (TChan XML.Event) where
100 stanzaFirstTag stanza = do
101 e <-atomically $ peekTChan (stanzaChan stanza)
102 return e
103instance StanzaFirstTag (LockedChan XML.Event) where
104 stanzaFirstTag stanza = do
105 e <-atomically $ peekLChan (stanzaChan stanza)
106 return e
107instance StanzaFirstTag XML.Event where
108 stanzaFirstTag stanza = return (stanzaChan stanza)
109
110data StanzaError
111 = BadRequest
112 | Conflict
113 | FeatureNotImplemented
114 | Forbidden
115 | Gone
116 | InternalServerError
117 | ItemNotFound
118 | JidMalformed
119 | NotAcceptable
120 | NotAllowed
121 | NotAuthorized
122 | PaymentRequired
123 | RecipientUnavailable
124 | Redirect
125 | RegistrationRequired
126 | RemoteServerNotFound
127 | RemoteServerTimeout
128 | ResourceConstraint
129 | ServiceUnavailable
130 | SubscriptionRequired
131 | UndefinedCondition
132 | UnexpectedRequest
133 deriving (Show,Enum,Ord,Eq)
134
135xep0086 :: StanzaError -> (Text, Int)
136xep0086 e = case e of
137 BadRequest -> ("modify", 400)
138 Conflict -> ("cancel", 409)
139 FeatureNotImplemented -> ("cancel", 501)
140 Forbidden -> ("auth", 403)
141 Gone -> ("modify", 302)
142 InternalServerError -> ("wait", 500)
143 ItemNotFound -> ("cancel", 404)
144 JidMalformed -> ("modify", 400)
145 NotAcceptable -> ("modify", 406)
146 NotAllowed -> ("cancel", 405)
147 NotAuthorized -> ("auth", 401)
148 PaymentRequired -> ("auth", 402)
149 RecipientUnavailable -> ("wait", 404)
150 Redirect -> ("modify", 302)
151 RegistrationRequired -> ("auth", 407)
152 RemoteServerNotFound -> ("cancel", 404)
153 RemoteServerTimeout -> ("wait", 504)
154 ResourceConstraint -> ("wait", 500)
155 ServiceUnavailable -> ("cancel", 503)
156 SubscriptionRequired -> ("auth", 407)
157 UndefinedCondition -> ("", 500)
158 UnexpectedRequest -> ("wait", 400)
159
160errorText :: StanzaError -> Text
161errorText e = case e of
162 BadRequest -> "Bad request"
163 Conflict -> "Conflict"
164 FeatureNotImplemented -> "This feature is not implemented"
165 Forbidden -> "Forbidden"
166 Gone -> "Recipient can no longer be contacted"
167 InternalServerError -> "Internal server error"
168 ItemNotFound -> "Item not found"
169 JidMalformed -> "JID Malformed"
170 NotAcceptable -> "Message was rejected"
171 NotAllowed -> "Not allowed"
172 NotAuthorized -> "Not authorized"
173 PaymentRequired -> "Payment is required"
174 RecipientUnavailable -> "Recipient is unavailable"
175 Redirect -> "Redirect"
176 RegistrationRequired -> "Registration required"
177 RemoteServerNotFound -> "Recipient's server not found"
178 RemoteServerTimeout -> "Remote server timeout"
179 ResourceConstraint -> "The server is low on resources"
180 ServiceUnavailable -> "The service is unavailable"
181 SubscriptionRequired -> "A subscription is required"
182 UndefinedCondition -> "Undefined condition"
183 UnexpectedRequest -> "Unexpected request"
184