]> code.delx.au - pymsnt/blob - src/disco.py
Conference rooms now show up as such in service discovery.
[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 self.addFeature(DISCO, None, "ROOM")
68
69 def _makeSearchJID(self, jid):
70 if jid.find('@') > 0:
71 if jid.find('%') > 0:
72 return "USER"
73 else:
74 return "ROOM"
75 elif config.compjid and to == config.compjid:
76 return config.jid
77 else:
78 return jid
79
80 def sendIq(self, el, timeout=15):
81 """ Used for sending IQ packets.
82 The id attribute for the IQ will be autogenerated if it is not there yet.
83 Returns a deferred which will fire with the matching IQ response as it's sole argument. """
84 def checkDeferred():
85 if(not d.called):
86 d.errback(Exception("Timeout"))
87 del self.deferredIqs[(jid, ID)]
88
89 jid = el.getAttribute("to")
90 ID = el.getAttribute("id")
91 if(not ID):
92 ID = self.pytrans.makeMessageID()
93 el.attributes["id"] = ID
94 self.pytrans.send(el)
95 d = Deferred()
96 self.deferredIqs[(jid, ID)] = d
97 reactor.callLater(timeout, checkDeferred)
98 return d
99
100 def addIdentity(self, category, ctype, name, jid):
101 """ Adds an identity to this JID's discovery profile. If jid == "USER" then MSN users will get this identity, jid == "ROOM" is for groupchat rooms. """
102 LogEvent(INFO)
103 if not self.identities.has_key(jid):
104 self.identities[jid] = []
105 self.identities[jid].append((category, ctype, name))
106
107 def addFeature(self, var, handler, jid):
108 """ Adds a feature to this JID's discovery profile. If jid == "USER" then MSN users will get this feature, jid == "ROOM" is for groupchat rooms. """
109 LogEvent(INFO)
110 if not self.features.has_key(jid):
111 self.features[jid] = []
112 self.features[jid].append((var, handler))
113
114 def addNode(self, node, handler, name, jid, rootnode):
115 """ Adds a node to this JID's discovery profile. If jid == "USER" then MSN users will get this node, jid == "ROOM" is for groupchat rooms. """
116 LogEvent(INFO)
117 if not self.nodes.has_key(jid):
118 self.nodes[jid] = {}
119 self.nodes[jid][node] = (handler, name, rootnode)
120
121 def onIq(self, el):
122 """ Decides what to do with an IQ """
123 fro = el.getAttribute("from")
124 to = el.getAttribute("to")
125 ID = el.getAttribute("id")
126 iqType = el.getAttribute("type")
127 ulang = utils.getLang(el)
128 try: # Stringprep
129 froj = internJID(fro)
130 to = internJID(to).full()
131 except Exception:
132 LogEvent(WARN, "", "Dropping IQ because of stringprep error")
133
134 # Check if it's a response to a send IQ
135 if self.deferredIqs.has_key((fro, ID)) and (iqType == "error" or iqType == "result"):
136 LogEvent(INFO, "", "Doing callback")
137 self.deferredIqs[(fro, ID)].callback(el)
138 del self.deferredIqs[(fro, ID)]
139 return
140
141 if not (iqType == "get" or iqType == "set"): return # Not interested
142
143 LogEvent(INFO, "", "Looking for handler")
144
145 for query in el.elements():
146 xmlns = query.uri
147 node = query.getAttribute("node")
148
149 if xmlns.startswith(DISCO) and node:
150 if self.nodes.has_key(to) and self.nodes[to].has_key(node) and self.nodes[to][node][0]:
151 self.nodes[to][node][0](el)
152 return
153 else:
154 # If the node we're browsing wasn't found, fall through and display the root disco
155 self.sendDiscoInfoResponse(to=fro, ID=ID, ulang=ulang, jid=to)
156 return
157 elif xmlns == DISCO_INFO:
158 self.sendDiscoInfoResponse(to=fro, ID=ID, ulang=ulang, jid=to)
159 return
160 elif xmlns == DISCO_ITEMS:
161 self.sendDiscoItemsResponse(to=fro, ID=ID, ulang=ulang, jid=to)
162 return
163
164 for (feature, handler) in self.features.get(self._makeSearchJID(to), []):
165 if feature == xmlns and handler:
166 LogEvent(INFO, "Handler found")
167 handler(el)
168 return
169
170 # Still hasn't been handled
171 LogEvent(WARN, "", "Unknown Iq request")
172 self.sendIqError(to=fro, fro=to, ID=ID, xmlns=DISCO, etype="cancel", condition="feature-not-implemented")
173
174 def sendDiscoInfoResponse(self, to, ID, ulang, jid):
175 """ Send a service discovery disco#info stanza to the given 'to'. 'jid' is the JID that was queried. """
176 LogEvent(INFO)
177 iq = Element((None, "iq"))
178 iq.attributes["type"] = "result"
179 iq.attributes["from"] = jid
180 iq.attributes["to"] = to
181 if(ID):
182 iq.attributes["id"] = ID
183 query = iq.addElement("query")
184 query.attributes["xmlns"] = DISCO_INFO
185
186 searchjid = self._makeSearchJID(jid)
187
188 # Add any identities
189 for (category, ctype, name) in self.identities.get(searchjid, []):
190 identity = query.addElement("identity")
191 identity.attributes["category"] = category
192 identity.attributes["type"] = ctype
193 identity.attributes["name"] = name
194
195 # Add any supported features
196 for (var, handler) in self.features.get(searchjid, []):
197 feature = query.addElement("feature")
198 feature.attributes["var"] = var
199
200 self.pytrans.send(iq)
201
202 def sendDiscoItemsResponse(self, to, ID, ulang, jid):
203 """ Send a service discovery disco#items stanza to the given 'to'. 'jid' is the JID that was queried. """
204 LogEvent(INFO)
205 iq = Element((None, "iq"))
206 iq.attributes["type"] = "result"
207 iq.attributes["from"] = jid
208 iq.attributes["to"] = to
209 if(ID):
210 iq.attributes["id"] = ID
211 query = iq.addElement("query")
212 query.attributes["xmlns"] = DISCO_ITEMS
213
214 searchjid = self._makeSearchJID(jid)
215 for node in self.nodes.get(searchjid, []):
216 handler, name, rootnode = self.nodes[jid][node]
217 if rootnode:
218 name = getattr(lang.get(ulang), name)
219 item = query.addElement("item")
220 item.attributes["jid"] = jid
221 item.attributes["node"] = node
222 item.attributes["name"] = name
223
224 self.pytrans.send(iq)
225
226
227 def sendIqError(self, to, fro, ID, xmlns, etype, condition):
228 """ Sends an IQ error response. See the XMPP RFC for details on the fields. """
229 el = Element((None, "iq"))
230 el.attributes["to"] = to
231 el.attributes["from"] = fro
232 if(ID):
233 el.attributes["id"] = ID
234 el.attributes["type"] = "error"
235 error = el.addElement("error")
236 error.attributes["type"] = etype
237 error.attributes["code"] = str(utils.errorCodeMap[condition])
238 cond = error.addElement(condition)
239 cond.attributes["xmlns"] = XMPP_STANZAS
240 self.pytrans.send(el)
241
242
243 class DiscoRequest:
244 def __init__(self, pytrans, jid):
245 LogEvent(INFO)
246 self.pytrans, self.jid = pytrans, jid
247
248 def doDisco(self):
249 ID = self.pytrans.makeMessageID()
250 iq = Element((None, "iq"))
251 iq.attributes["to"] = self.jid
252 iq.attributes["from"] = config.jid
253 iq.attributes["type"] = "get"
254 query = iq.addElement("query")
255 query.attributes["xmlns"] = DISCO_INFO
256
257 d = self.pytrans.discovery.sendIq(iq)
258 d.addCallback(self.discoResponse)
259 d.addErrback(self.discoFail)
260 return d
261
262 def discoResponse(self, el):
263 iqType = el.getAttribute("type")
264 if iqType != "result":
265 return []
266
267 fro = el.getAttribute("from")
268
269 features = []
270
271 for child in el.elements():
272 if child.name == "query":
273 query = child
274 break
275 else:
276 return []
277
278 for child in query.elements():
279 if child.name == "feature":
280 features.append(child.getAttribute("var"))
281
282 return features
283
284 def discoFail(self):
285 return []
286
287
288