diff options
Diffstat (limited to 'Presence/Stanza/Parse.hs')
-rw-r--r-- | Presence/Stanza/Parse.hs | 261 |
1 files changed, 261 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 @@ | |||
1 | module Stanza.Parse (grokStanza,errorTagLocalName) where | ||
2 | |||
3 | import Control.Concurrent.STM | ||
4 | import Control.Monad | ||
5 | import Data.Char | ||
6 | import Data.Function | ||
7 | import Data.Maybe | ||
8 | import qualified Data.Text as Text (pack, unpack, words) | ||
9 | ;import Data.Text (Text) | ||
10 | |||
11 | import Control.Monad.Catch (MonadThrow) | ||
12 | import Control.Monad.IO.Class (MonadIO, liftIO) | ||
13 | import qualified Data.Map as Map | ||
14 | import Data.XML.Types as XML | ||
15 | import qualified Text.XML.Stream.Parse as XML | ||
16 | |||
17 | import Control.Concurrent.STM.Util | ||
18 | import ControlMaybe (handleIO_, (<&>)) | ||
19 | import EventUtil | ||
20 | import Nesting | ||
21 | import Stanza.Type | ||
22 | |||
23 | -- | Identify an XMPP stanza based on the open-tag. | ||
24 | grokStanza :: Text -> XML.Event -> NestingXML o IO (Maybe StanzaType) | ||
25 | grokStanza "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 | |||
33 | grokStanza "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 | |||
42 | grokStanzaIQGet :: Monad m => XML.Event -> NestingXML o m (Maybe StanzaType) | ||
43 | grokStanzaIQGet 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 | |||
51 | grokStanzaIQResult :: XML.Event -> NestingXML o IO (Maybe StanzaType) | ||
52 | grokStanzaIQResult 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 | |||
60 | grokStanzaIQSet :: XML.Event -> NestingXML o IO (Maybe StanzaType) | ||
61 | grokStanzaIQSet 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 | |||
76 | grokPresence | ||
77 | :: ( MonadThrow m | ||
78 | , MonadIO m | ||
79 | ) => Text -> XML.Event -> NestingXML o m (Maybe StanzaType) | ||
80 | grokPresence 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 | |||
94 | grokMessage | ||
95 | :: ( MonadThrow m | ||
96 | , MonadIO m | ||
97 | ) => Text -> XML.Event -> NestingXML o m (Maybe StanzaType) | ||
98 | grokMessage 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 | |||
109 | parseClientVersion :: NestingXML o IO (Maybe StanzaType) | ||
110 | parseClientVersion = 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 | |||
129 | parsePresenceStatus | ||
130 | :: ( MonadThrow m | ||
131 | , MonadIO m | ||
132 | ) => Text -> XML.Event -> NestingXML o m (Maybe StanzaType) | ||
133 | parsePresenceStatus 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 | } | ||
171 | parseMessage | ||
172 | :: ( MonadThrow m | ||
173 | , MonadIO m | ||
174 | ) => Text -> XML.Event -> NestingXML o m StanzaType | ||
175 | parseMessage 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 | |||
224 | findErrorTag :: Monad m => Text -> NestingXML o m (Maybe StanzaError) | ||
225 | findErrorTag 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 | |||
238 | findConditionTag :: Monad m => NestingXML o m (Maybe XML.Event) | ||
239 | findConditionTag = 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 | |||
246 | conditionFromText :: Text -> Maybe StanzaError | ||
247 | conditionFromText 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. | ||
255 | errorTagLocalName :: StanzaError -> Text | ||
256 | errorTagLocalName 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 | |||