-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2004-2005 James Bunton <james@delx.cjb.net>
# Licensed for distribution under the GPL version 2, check COPYING for details
-import utils
-if(utils.checkTwisted()):
- from twisted.xish.domish import Element
-else:
- from tlib.domish import Element
+from debug import LogEvent, INFO, WARN, ERROR
+
from twisted.internet.defer import Deferred
from twisted.internet import reactor
+from tlib.xmlw import Element, jid
+
import sys
+
+import lang
+import utils
import config
-import debug
-import legacy
-XMPP_STANZAS = 'urn:ietf:params:xml:ns:xmpp-stanzas'
-DISCO = "http://jabber.org/protocol/disco"
-DISCO_ITEMS = DISCO + "#items"
-DISCO_INFO = DISCO + "#info"
+XMPP_STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas"
+DISCO = "http://jabber.org/protocol/disco"
+DISCO_ITEMS = DISCO + "#items"
+DISCO_INFO = DISCO + "#info"
+COMMANDS = "http://jabber.org/protocol/commands"
+CAPS = "http://jabber.org/protocol/caps"
+SUBSYNC = "http://delx.cjb.net/protocol/roster-subsync"
+MUC = "http://jabber.org/protocol/muc"
+MUC_USER = MUC + "#user"
+FEATURE_NEG = "http://jabber.org/protocol/feature-neg"
+SI = "http://jabber.org/protocol/si"
+FT = "http://jabber.org/protocol/si/profile/file-transfer"
+S5B = "http://jabber.org/protocol/bytestreams"
+IBB = "http://jabber.org/protocol/ibb"
+IQGATEWAY = "jabber:iq:gateway"
+IQVERSION = "jabber:iq:version"
+IQREGISTER = "jabber:iq:register"
+IQROSTER = "jabber:iq:roster"
+IQAVATAR = "jabber:iq:avatar"
+IQOOB = "jabber:iq:oob"
+XOOB = "jabber:x:oob"
+XCONFERENCE = "jabber:x:conference"
+XEVENT = "jabber:x:event"
+XDELAY = "jabber:x:delay"
+XAVATAR = "jabber:x:avatar"
+XDATA = "jabber:x:data"
+STORAGEAVATAR = "storage:client:avatar"
+XVCARDUPDATE = "vcard-temp:x:update"
+VCARDTEMP = "vcard-temp"
+
+
class ServerDiscovery:
+ """ Handles everything IQ related. You can send IQ stanzas and receive a Deferred
+ to notify you when a response comes, or if there's a timeout.
+ Also manages discovery for server & client """
+
+ # TODO rename this file & class to something more sensible
+
def __init__ (self, pytrans):
- debug.log("Discovery: Created server discovery manager")
+ LogEvent(INFO)
self.pytrans = pytrans
- self.identities = []
- self.features = []
+ self.identities = {}
+ self.features = {}
+ self.nodes = {}
self.deferredIqs = {} # A dict indexed by (jid, id) of deferreds to fire
- self.addFeature(DISCO, None)
+ self.addFeature(DISCO, None, config.jid)
+ self.addFeature(DISCO, None, "USER")
def sendIq(self, el, timeout=15):
""" Used for sending IQ packets.
Returns a deferred which will fire with the matching IQ response as it's sole argument. """
def checkDeferred():
if(not d.called):
- d.errback()
+ d.errback(Exception("Timeout"))
del self.deferredIqs[(jid, ID)]
jid = el.getAttribute("to")
reactor.callLater(timeout, checkDeferred)
return d
- def addIdentity(self, category, ctype, name):
- debug.log("Discovery: Adding identitity \"%s\" \"%s\" \"%s\"" % (category, ctype, name))
- self.identities.append((category, ctype, name))
+ def addIdentity(self, category, ctype, name, jid):
+ """ Adds an identity to this JID's discovery profile. If jid == "USER" then MSN users will get this identity. """
+ LogEvent(INFO)
+ if not self.identities.has_key(jid):
+ self.identities[jid] = []
+ self.identities[jid].append((category, ctype, name))
- def addFeature(self, var, handler):
- debug.log("Discovery: Adding feature support \"%s\" \"%s\"" % (var, handler))
- self.features.append((var, handler))
+ def addFeature(self, var, handler, jid):
+ """ Adds a feature to this JID's discovery profile. If jid == "USER" then MSN users will get this feature. """
+ LogEvent(INFO)
+ if not self.features.has_key(jid):
+ self.features[jid] = []
+ self.features[jid].append((var, handler))
+
+ def addNode(self, node, handler, name, jid, rootnode):
+ """ Adds a node to this JID's discovery profile. If jid == "USER" then MSN users will get this node. """
+ LogEvent(INFO)
+ if not self.nodes.has_key(jid):
+ self.nodes[jid] = {}
+ self.nodes[jid][node] = (handler, name, rootnode)
def onIq(self, el):
+ """ Decides what to do with an IQ """
fro = el.getAttribute("from")
to = el.getAttribute("to")
ID = el.getAttribute("id")
iqType = el.getAttribute("type")
+ ulang = utils.getLang(el)
+ try: # Stringprep
+ froj = jid.intern(fro)
+ to = jid.intern(to).full()
+ except Exception:
+ LogEvent(WARN, "", "Dropping IQ because of stringprep error")
# Check if it's a response to a send IQ
- if(self.deferredIqs.has_key((fro, ID)) and iqType in ["error", "result"]):
+ if self.deferredIqs.has_key((fro, ID)) and (iqType == "error" or iqType == "result"):
+ LogEvent(INFO, "", "Doing callback")
self.deferredIqs[(fro, ID)].callback(el)
del self.deferredIqs[(fro, ID)]
return
- if(iqType not in ["get", "set"]): return # Not interested
+ if not (iqType == "get" or iqType == "set"): return # Not interested
- debug.log("Discovery: Iq received \"%s\" \"%s\". Looking for handler" % (fro, ID))
+ LogEvent(INFO, "", "Looking for handler")
for query in el.elements():
- xmlns = query.defaultUri
-
- if(to.find('@') > 0): # Iq to a user
- self.sendIqNotSupported(to=fro, fro=config.jid, ID=ID, xmlns=DISCO)
+ xmlns = query.uri
+ node = query.getAttribute("node")
- else: # Iq to transport
- if(xmlns == DISCO_INFO):
- self.sendDiscoInfoResponse(to=fro, ID=ID)
- elif(xmlns == DISCO_ITEMS):
- self.sendDiscoItemsResponse(to=fro, ID=ID)
+ if xmlns.startswith(DISCO) and node:
+ if self.nodes.has_key(to) and self.nodes[to].has_key(node) and self.nodes[to][node][0]:
+ self.nodes[to][node][0](el)
+ return
else:
- handled = False
- for (feature, handler) in self.features:
- if(feature == xmlns and handler):
- debug.log("Discovery: Handler found \"%s\" \"%s\"" % (feature, handler))
- handler(el)
- handled = True
- if(not handled):
- debug.log("Discovery: Unknown Iq request \"%s\" \"%s\" \"%s\"" % (fro, ID, xmlns))
- self.sendIqNotSupported(to=fro, fro=config.jid, ID=ID, xmlns=DISCO)
+ # If the node we're browsing wasn't found, fall through and display the root disco
+ self.sendDiscoInfoResponse(to=fro, ID=ID, ulang=ulang, jid=to)
+ return
+ elif xmlns == DISCO_INFO:
+ self.sendDiscoInfoResponse(to=fro, ID=ID, ulang=ulang, jid=to)
+ return
+ elif xmlns == DISCO_ITEMS:
+ self.sendDiscoItemsResponse(to=fro, ID=ID, ulang=ulang, jid=to)
+ return
+
+ if to.find('@') > 0:
+ searchjid = "USER"
+ elif config.compjid and to == config.compjid:
+ searchjid = config.jid
+ else:
+ searchjid = to
+ for (feature, handler) in self.features.get(searchjid, []):
+ if feature == xmlns and handler:
+ LogEvent(INFO, "Handler found")
+ handler(el)
+ return
+
+ # Still hasn't been handled
+ LogEvent(WARN, "", "Unknown Iq request")
+ self.sendIqError(to=fro, fro=to, ID=ID, xmlns=DISCO, etype="cancel", condition="feature-not-implemented")
- def sendDiscoInfoResponse(self, to, ID):
- debug.log("Discovery: Replying to disco#info request from \"%s\" \"%s\"" % (to, ID))
+ def sendDiscoInfoResponse(self, to, ID, ulang, jid):
+ """ Send a service discovery disco#info stanza to the given 'to'. 'jid' is the JID that was queried. """
+ LogEvent(INFO)
iq = Element((None, "iq"))
iq.attributes["type"] = "result"
- iq.attributes["from"] = config.jid
+ iq.attributes["from"] = jid
iq.attributes["to"] = to
if(ID):
iq.attributes["id"] = ID
query = iq.addElement("query")
query.attributes["xmlns"] = DISCO_INFO
+ searchjid = jid
+ if jid.find('@') > 0: searchjid = "USER"
+ if config.compjid and jid == config.compjid: searchjid = config.jid
# Add any identities
- for (category, ctype, name) in self.identities:
+ for (category, ctype, name) in self.identities.get(searchjid, []):
identity = query.addElement("identity")
identity.attributes["category"] = category
identity.attributes["type"] = ctype
identity.attributes["name"] = name
# Add any supported features
- for (var, handler) in self.features:
+ for (var, handler) in self.features.get(searchjid, []):
feature = query.addElement("feature")
feature.attributes["var"] = var
+
self.pytrans.send(iq)
- def sendDiscoItemsResponse(self, to, ID):
- debug.log("Discovery: Replying to disco#items request from \"%s\" \"%s\"" % (to, ID))
+ def sendDiscoItemsResponse(self, to, ID, ulang, jid):
+ """ Send a service discovery disco#items stanza to the given 'to'. 'jid' is the JID that was queried. """
+ LogEvent(INFO)
iq = Element((None, "iq"))
iq.attributes["type"] = "result"
- iq.attributes["from"] = config.jid
+ iq.attributes["from"] = jid
iq.attributes["to"] = to
if(ID):
iq.attributes["id"] = ID
query = iq.addElement("query")
query.attributes["xmlns"] = DISCO_ITEMS
+
+ searchjid = jid
+ if jid.find('@') > 0: searchjid = "USER"
+ if config.compjid and jid == config.compjid: searchjid = config.jid
+ for node in self.nodes.get(searchjid, []):
+ handler, name, rootnode = self.nodes[jid][node]
+ if rootnode:
+ name = getattr(lang.get(ulang), name)
+ item = query.addElement("item")
+ item.attributes["jid"] = jid
+ item.attributes["node"] = node
+ item.attributes["name"] = name
self.pytrans.send(iq)
- def sendIqNotSupported(self, to, fro, ID, xmlns):
- debug.log("Discovery: Replying with error to unknown Iq request")
- iq = Element((None, "iq"))
- iq.attributes["type"] = "error"
- iq.attributes["from"] = fro
- iq.attributes["to"] = to
+ def sendIqError(self, to, fro, ID, xmlns, etype, condition):
+ """ Sends an IQ error response. See the XMPP RFC for details on the fields. """
+ el = Element((None, "iq"))
+ el.attributes["to"] = to
+ el.attributes["from"] = fro
if(ID):
- iq.attributes["id"] = ID
- error = iq.addElement("error")
- error.attributes["xmlns"] = xmlns
- error.attributes["type"] = "cancel"
- error.attributes["xmlns"] = XMPP_STANZAS
- text = error.addElement("text")
- text.attributes["xmlns"] = XMPP_STANZAS
- text.addContent("Not implemented.")
-
- self.pytrans.send(iq)
+ el.attributes["id"] = ID
+ el.attributes["type"] = "error"
+ error = el.addElement("error")
+ error.attributes["type"] = etype
+ error.attributes["code"] = str(utils.errorCodeMap[condition])
+ cond = error.addElement(condition)
+ cond.attributes["xmlns"] = XMPP_STANZAS
+ self.pytrans.send(el)
+
+
+class DiscoRequest:
+ def __init__(self, pytrans, jid):
+ LogEvent(INFO)
+ self.pytrans, self.jid = pytrans, jid
- def sendIqNotValid(self, to, ID, xmlns):
- debug.log("Discovery: Replying with error to invalid Iq request")
+ def doDisco(self):
+ ID = self.pytrans.makeMessageID()
iq = Element((None, "iq"))
- iq.attributes["type"] = "error"
+ iq.attributes["to"] = self.jid
iq.attributes["from"] = config.jid
- iq.attributes["to"] = to
- if(ID):
- iq.attributes["id"] = ID
- error = iq.addElement("error")
- error.attributes["xmlns"] = xmlns
- error.attributes["type"] = "modify"
- error.attributes["xmlns"] = XMPP_STANZAS
- text = error.addElement("text")
- text.attributes["xmlns"] = XMPP_STANZAS
- text.addContent("Not valid.")
+ iq.attributes["type"] = "get"
+ query = iq.addElement("query")
+ query.attributes["xmlns"] = DISCO_INFO
+
+ d = self.pytrans.discovery.sendIq(iq)
+ d.addCallback(self.discoResponse)
+ d.addErrback(self.discoFail)
+ return d
+
+ def discoResponse(self, el):
+ iqType = el.getAttribute("type")
+ if iqType != "result":
+ return []
+
+ fro = el.getAttribute("from")
+
+ features = []
+
+ for child in el.elements():
+ if child.name == "query":
+ query = child
+ break
+ else:
+ return []
+
+ for child in query.elements():
+ if child.name == "feature":
+ features.append(child.getAttribute("var"))
+
+ return features
+
+ def discoFail(self):
+ return []
- self.pytrans.send(iq)
+