]> code.delx.au - pymsnt/blob - src/disco.py
4fd2221f840a0fd799042967aaf54fbf6360b182
[pymsnt] / src / disco.py
1 # Copyright 2004-2006 James Bunton <james@delx.cjb.net>
2 # Licensed for distribution under the GPL version 2, check COPYING for details
3
4 from debug import LogEvent, INFO, WARN, ERROR
5
6 from twisted.internet.defer import Deferred
7 from twisted.internet import reactor
8 from twisted.words.xish.domish import Element
9 from twisted.words.protocols.jabber.jid import internJID
10
11 import sys
12
13 import lang
14 import utils
15 import config
16
17 XMPP_STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas"
18 DISCO = "http://jabber.org/protocol/disco"
19 DISCO_ITEMS = DISCO + "#items"
20 DISCO_INFO = DISCO + "#info"
21 COMMANDS = "http://jabber.org/protocol/commands"
22 CAPS = "http://jabber.org/protocol/caps"
23 SUBSYNC = "http://delx.cjb.net/protocol/roster-subsync"
24 MUC = "http://jabber.org/protocol/muc"
25 MUC_USER = MUC + "#user"
26 FEATURE_NEG = "http://jabber.org/protocol/feature-neg"
27 SI = "http://jabber.org/protocol/si"
28 FT = "http://jabber.org/protocol/si/profile/file-transfer"
29 S5B = "http://jabber.org/protocol/bytestreams"
30 IBB = "http://jabber.org/protocol/ibb"
31 IQGATEWAY = "jabber:iq:gateway"
32 IQVERSION = "jabber:iq:version"
33 IQREGISTER = "jabber:iq:register"
34 IQROSTER = "jabber:iq:roster"
35 IQAVATAR = "jabber:iq:avatar"
36 IQOOB = "jabber:iq:oob"
37 XOOB = "jabber:x:oob"
38 XCONFERENCE = "jabber:x:conference"
39 XEVENT = "jabber:x:event"
40 XDELAY = "jabber:x:delay"
41 XAVATAR = "jabber:x:avatar"
42 XDATA = "jabber:x:data"
43 STORAGEAVATAR = "storage:client:avatar"
44 XVCARDUPDATE = "vcard-temp:x:update"
45 VCARDTEMP = "vcard-temp"
46
47
48
49
50 class ServerDiscovery:
51 """ Handles everything IQ related. You can send IQ stanzas and receive a Deferred
52 to notify you when a response comes, or if there's a timeout.
53 Also manages discovery for server & client """
54
55 # TODO rename this file & class to something more sensible
56
57 def __init__ (self, pytrans):
58 LogEvent(INFO)
59 self.pytrans = pytrans
60 self.identities = {}
61 self.features = {}
62 self.nodes = {}
63 self.deferredIqs = {} # A dict indexed by (jid, id) of deferreds to fire
64
65 self.addFeature(DISCO, None, config.jid)
66 self.addFeature(DISCO, None, "USER")
67
68 def sendIq(self, el, timeout=15):
69 """ Used for sending IQ packets.
70 The id attribute for the IQ will be autogenerated if it is not there yet.
71 Returns a deferred which will fire with the matching IQ response as it's sole argument. """
72 def checkDeferred():
73 if(not d.called):
74 d.errback(Exception("Timeout"))
75 del self.deferredIqs[(jid, ID)]
76
77 jid = el.getAttribute("to")
78 ID = el.getAttribute("id")
79 if(not ID):
80 ID = self.pytrans.makeMessageID()
81 el.attributes["id"] = ID
82 self.pytrans.send(el)
83 d = Deferred()
84 self.deferredIqs[(jid, ID)] = d
85 reactor.callLater(timeout, checkDeferred)
86 return d
87
88 def addIdentity(self, category, ctype, name, jid):
89 """ Adds an identity to this JID's discovery profile. If jid == "USER" then MSN users will get this identity. """
90 LogEvent(INFO)
91 if not self.identities.has_key(jid):
92 self.identities[jid] = []
93 self.identities[jid].append((category, ctype, name))
94
95 def addFeature(self, var, handler, jid):
96 """ Adds a feature to this JID's discovery profile. If jid == "USER" then MSN users will get this feature. """
97 LogEvent(INFO)
98 if not self.features.has_key(jid):
99 self.features[jid] = []
100 self.features[jid].append((var, handler))
101
102 def addNode(self, node, handler, name, jid, rootnode):
103 """ Adds a node to this JID's discovery profile. If jid == "USER" then MSN users will get this node. """
104 LogEvent(INFO)
105 if not self.nodes.has_key(jid):
106 self.nodes[jid] = {}
107 self.nodes[jid][node] = (handler, name, rootnode)
108
109 def onIq(self, el):
110 """ Decides what to do with an IQ """
111 fro = el.getAttribute("from")
112 to = el.getAttribute("to")
113 ID = el.getAttribute("id")
114 iqType = el.getAttribute("type")
115 ulang = utils.getLang(el)
116 try: # Stringprep
117 froj = internJID(fro)
118 to = internJID(to).full()
119 except Exception:
120 LogEvent(WARN, "", "Dropping IQ because of stringprep error")
121
122 # Check if it's a response to a send IQ
123 if self.deferredIqs.has_key((fro, ID)) and (iqType == "error" or iqType == "result"):
124 LogEvent(INFO, "", "Doing callback")
125 self.deferredIqs[(fro, ID)].callback(el)
126 del self.deferredIqs[(fro, ID)]
127 return
128
129 if not (iqType == "get" or iqType == "set"): return # Not interested
130
131 LogEvent(INFO, "", "Looking for handler")
132
133 for query in el.elements():
134 xmlns = query.uri
135 node = query.getAttribute("node")
136
137 if xmlns.startswith(DISCO) and node:
138 if self.nodes.has_key(to) and self.nodes[to].has_key(node) and self.nodes[to][node][0]:
139 self.nodes[to][node][0](el)
140 return
141 else:
142 # If the node we're browsing wasn't found, fall through and display the root disco
143 self.sendDiscoInfoResponse(to=fro, ID=ID, ulang=ulang, jid=to)
144 return
145 elif xmlns == DISCO_INFO:
146 self.sendDiscoInfoResponse(to=fro, ID=ID, ulang=ulang, jid=to)
147 return
148 elif xmlns == DISCO_ITEMS:
149 self.sendDiscoItemsResponse(to=fro, ID=ID, ulang=ulang, jid=to)
150 return
151
152 if to.find('@') > 0:
153 searchjid = "USER"
154 elif config.compjid and to == config.compjid:
155 searchjid = config.jid
156 else:
157 searchjid = to
158 for (feature, handler) in self.features.get(searchjid, []):
159 if feature == xmlns and handler:
160 LogEvent(INFO, "Handler found")
161 handler(el)
162 return
163
164 # Still hasn't been handled
165 LogEvent(WARN, "", "Unknown Iq request")
166 self.sendIqError(to=fro, fro=to, ID=ID, xmlns=DISCO, etype="cancel", condition="feature-not-implemented")
167
168 def sendDiscoInfoResponse(self, to, ID, ulang, jid):
169 """ Send a service discovery disco#info stanza to the given 'to'. 'jid' is the JID that was queried. """
170 LogEvent(INFO)
171 iq = Element((None, "iq"))
172 iq.attributes["type"] = "result"
173 iq.attributes["from"] = jid
174 iq.attributes["to"] = to
175 if(ID):
176 iq.attributes["id"] = ID
177 query = iq.addElement("query")
178 query.attributes["xmlns"] = DISCO_INFO
179
180 searchjid = jid
181 if jid.find('@') > 0: searchjid = "USER"
182 if config.compjid and jid == config.compjid: searchjid = config.jid
183 # Add any identities
184 for (category, ctype, name) in self.identities.get(searchjid, []):
185 identity = query.addElement("identity")
186 identity.attributes["category"] = category
187 identity.attributes["type"] = ctype
188 identity.attributes["name"] = name
189
190 # Add any supported features
191 for (var, handler) in self.features.get(searchjid, []):
192 feature = query.addElement("feature")
193 feature.attributes["var"] = var
194
195 self.pytrans.send(iq)
196
197 def sendDiscoItemsResponse(self, to, ID, ulang, jid):
198 """ Send a service discovery disco#items stanza to the given 'to'. 'jid' is the JID that was queried. """
199 LogEvent(INFO)
200 iq = Element((None, "iq"))
201 iq.attributes["type"] = "result"
202 iq.attributes["from"] = jid
203 iq.attributes["to"] = to
204 if(ID):
205 iq.attributes["id"] = ID
206 query = iq.addElement("query")
207 query.attributes["xmlns"] = DISCO_ITEMS
208
209 searchjid = jid
210 if jid.find('@') > 0: searchjid = "USER"
211 if config.compjid and jid == config.compjid: searchjid = config.jid
212 for node in self.nodes.get(searchjid, []):
213 handler, name, rootnode = self.nodes[jid][node]
214 if rootnode:
215 name = getattr(lang.get(ulang), name)
216 item = query.addElement("item")
217 item.attributes["jid"] = jid
218 item.attributes["node"] = node
219 item.attributes["name"] = name
220
221 self.pytrans.send(iq)
222
223
224 def sendIqError(self, to, fro, ID, xmlns, etype, condition):
225 """ Sends an IQ error response. See the XMPP RFC for details on the fields. """
226 el = Element((None, "iq"))
227 el.attributes["to"] = to
228 el.attributes["from"] = fro
229 if(ID):
230 el.attributes["id"] = ID
231 el.attributes["type"] = "error"
232 error = el.addElement("error")
233 error.attributes["type"] = etype
234 error.attributes["code"] = str(utils.errorCodeMap[condition])
235 cond = error.addElement(condition)
236 cond.attributes["xmlns"] = XMPP_STANZAS
237 self.pytrans.send(el)
238
239
240 class DiscoRequest:
241 def __init__(self, pytrans, jid):
242 LogEvent(INFO)
243 self.pytrans, self.jid = pytrans, jid
244
245 def doDisco(self):
246 ID = self.pytrans.makeMessageID()
247 iq = Element((None, "iq"))
248 iq.attributes["to"] = self.jid
249 iq.attributes["from"] = config.jid
250 iq.attributes["type"] = "get"
251 query = iq.addElement("query")
252 query.attributes["xmlns"] = DISCO_INFO
253
254 d = self.pytrans.discovery.sendIq(iq)
255 d.addCallback(self.discoResponse)
256 d.addErrback(self.discoFail)
257 return d
258
259 def discoResponse(self, el):
260 iqType = el.getAttribute("type")
261 if iqType != "result":
262 return []
263
264 fro = el.getAttribute("from")
265
266 features = []
267
268 for child in el.elements():
269 if child.name == "query":
270 query = child
271 break
272 else:
273 return []
274
275 for child in query.elements():
276 if child.name == "feature":
277 features.append(child.getAttribute("var"))
278
279 return features
280
281 def discoFail(self):
282 return []
283
284
285