]> code.delx.au - pymsnt/blobdiff - src/disco.py
Fixed md5 update routine to work if spooldir is not set.
[pymsnt] / src / disco.py
index 000677e30854046ef43559ff73cc1644c4431402..1c49386d1684e650840714027d9c285125616c44 100644 (file)
@@ -1,33 +1,68 @@
-# 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.
@@ -35,7 +70,7 @@ class ServerDiscovery:
                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")
@@ -49,123 +84,201 @@ class ServerDiscovery:
                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)
+